Chuyển tới nội dung chính

Lambda Expressions

Mục tiêu bài học

Sau bài này, bạn sẽ:

  • Hiểu được Lambda Expression là gì và lợi ích của nó
  • Nắm được cú pháp lambda: parameters, arrow operator, body
  • Phân biệt được expression body và block body
  • Hiểu được variable capture và effectively final
  • So sánh được lambda với anonymous inner class

Bài trước: Giới thiệu Functional Programming — Đã tìm hiểu về FP, Pure Functions và First-Class Functions. Bài này sẽ học cách viết anonymous functions ngắn gọn với Lambda Expressions.

Lambda Expression là gì?

Lambda expression là một hàm vô danh (anonymous function) - một function không có tên, có thể được truyền như một argument hoặc gán cho một biến.

Lambda được giới thiệu từ Java 8 (2014) để viết code functional programming ngắn gọn hơn.

// Trước Java 8: Anonymous inner class
Runnable oldWay = new Runnable() {
@Override
public void run() {
System.out.println("Hello World");
}
};

// Java 8+: Lambda expression
Runnable newWay = () -> System.out.println("Hello World");
Lợi ích của Lambda
  • Ngắn gọn: Giảm boilerplate code
  • Dễ đọc: Tập trung vào logic, không phải cú pháp
  • Functional programming: Enable Stream API và functional style
  • Type inference: Compiler tự suy luận kiểu dữ liệu

Cú pháp Lambda Expression

Cú pháp cơ bản

(parameters) -> expression

hoặc

(parameters) -> { statements; }

Các thành phần

Thành phầnMô tảVí dụ
ParametersDanh sách tham số (có thể rỗng)(x, y), (String s), ()
Arrow operatorToán tử mũi tên ->->
BodyExpression hoặc block of statementsx * x, { return x * x; }

Các dạng viết Lambda

1. No parameters (Không tham số)

// Runnable: void run()
Runnable r = () -> System.out.println("No parameters");

// Supplier<T>: T get()
Supplier<Double> randomValue = () -> Math.random();
System.out.println(randomValue.get());

2. Single parameter (Một tham số)

// ✅ Có thể bỏ ngoặc đơn
Consumer<String> print = message -> System.out.println(message);

// Cũng hợp lệ, nhưng redundant
Consumer<String> printVerbose = (message) -> System.out.println(message);

// Function<T, R>: R apply(T)
Function<Integer, Integer> square = x -> x * x;
System.out.println(square.apply(5)); // 25
Convention

Khi chỉ có 1 parameter, nên bỏ ngoặc đơn để code ngắn gọn hơn:

list.forEach(item -> System.out.println(item));  // ✅ Recommended
list.forEach((item) -> System.out.println(item)); // ⚠️ Verbose

3. Multiple parameters (Nhiều tham số)

// BiFunction<T, U, R>: R apply(T, U)
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
System.out.println(add.apply(10, 20)); // 30

// Comparator<T>: int compare(T, T)
Comparator<String> lengthComparator = (s1, s2) -> s1.length() - s2.length();

// BinaryOperator<T>: T apply(T, T)
BinaryOperator<Integer> multiply = (x, y) -> x * y;
Lưu ý

Với nhiều parameters, bắt buộc phải có ngoặc đơn:

BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;  // ✅
BiFunction<Integer, Integer, Integer> add = a, b -> a + b; // ❌ Compile error

4. Explicit type declarations (Khai báo kiểu rõ ràng)

// Type inference (compiler tự suy luận)
BiFunction<Integer, Integer, Integer> add1 = (a, b) -> a + b;

// Explicit types (khai báo rõ ràng)
BiFunction<Integer, Integer, Integer> add2 = (Integer a, Integer b) -> a + b;
Khi nào cần explicit types?
  • Thường không cần: Compiler thường suy luận đúng
  • Cần khi: Code phức tạp, nhiều overloaded methods, hoặc muốn tăng readability

Lambda Body: Expression vs Block

Thân biểu thức (Expression Body) - Single Expression

// ✅ Single expression - không cần return, không cần dấu { }
Function<Integer, Integer> square = x -> x * x;

Predicate<String> isEmpty = s -> s.isEmpty();

BiFunction<Integer, Integer, Integer> max = (a, b) -> a > b ? a : b;

Thân khối lệnh (Block Body) - Multiple Statements

// ✅ Multiple statements - cần { }, cần return nếu có giá trị trả về
Function<Integer, Integer> processNumber = x -> {
System.out.println("Processing: " + x);
int result = x * x;
System.out.println("Result: " + result);
return result; // Bắt buộc phải có return
};

// Consumer with block
Consumer<List<String>> printList = list -> {
System.out.println("List size: " + list.size());
for (String item : list) {
System.out.println("- " + item);
}
// Không cần return vì Consumer trả về void
};
Common Mistake
// ❌ Block body nhưng quên return
Function<Integer, Integer> wrong = x -> {
x * x; // ❌ Compile error: missing return statement
};

// ✅ Phải có return
Function<Integer, Integer> correct = x -> {
return x * x;
};

// ✅ Hoặc dùng expression body
Function<Integer, Integer> better = x -> x * x;

Bắt giữ biến (Variable Capture) - Effectively Final

Lambda có thể sử dụng các biến từ scope bên ngoài, nhưng biến đó phải là effectively final (không thay đổi giá trị sau khi khởi tạo).

public class VariableCaptureDemo {
public static void main(String[] args) {
int factor = 10; // Effectively final

// ✅ Lambda capture 'factor' - OK
Function<Integer, Integer> multiplier = x -> x * factor;
System.out.println(multiplier.apply(5)); // 50

// ❌ Nếu thay đổi 'factor', compile error
// factor = 20; // ❌ Error: variable used in lambda should be final or effectively final

// ✅ Khai báo final explicitly
final int divisor = 2;
Function<Integer, Integer> divider = x -> x / divisor;
}

public void captureInstanceVariable() {
// ✅ Instance variables có thể thay đổi (không phải effectively final requirement)
List<String> list = new ArrayList<>();

Runnable r = () -> {
list.add("Item"); // ✅ OK - list reference không đổi
// list = new ArrayList<>(); // ❌ Không thể gán lại reference
};
}
}

Tại sao phải Effectively Final?

Lý doGiải thích
An toàn luồng (Thread safety)Lambda có thể chạy ở thread khác, nếu biến thay đổi → race condition
Tính nhất quán của bao đóng (Closure)Lambda capture snapshot của biến, không phải reference
Compiler optimizationGiúp compiler optimize lambda execution

Effectively final theo JLS §4.12.4

Theo Java Language Specification, một biến là effectively final nếu nó không bao giờ được gán lại sau khi khởi tạo — dù không có từ khóa final tường minh.

int x = 10;        // effectively final — không bao giờ gán lại
final int y = 20; // explicitly final

int z = 30;
z = 40; // z KHÔNG phải effectively final — đã gán lại

// Lambda chỉ có thể dùng x và y, không dùng được z
Runnable r = () -> System.out.println(x + y);

Thiết kế này không phải ngẫu nhiên. Lambda trong Java bắt giữ giá trị (capture by value) của biến, không phải tham chiếu đến biến. Nếu biến thay đổi sau khi lambda được tạo, giá trị trong lambda sẽ "cũ" — gây ra bug khó tìm. Quy tắc effectively final ngăn chặn vấn đề này ngay từ lúc biên dịch.

Workaround nếu cần mutable variable

Dùng wrapper object hoặc array:

// ✅ Dùng array (reference không đổi, value thay đổi)
int[] counter = {0};
list.forEach(item -> counter[0]++);

// ✅ Dùng wrapper class
AtomicInteger atomicCounter = new AtomicInteger(0);
list.forEach(item -> atomicCounter.incrementAndGet());

Trường hợp đặc biệt: Mutable array/object trick

Kỹ thuật dùng array int[] counter = {0} hoạt động vì reference của array là effectively final, dù nội dung thay đổi:

List<String> list = Arrays.asList("A", "B", "C");

// ✅ Compile OK - reference 'counter' không đổi
int[] counter = {0};
list.forEach(item -> counter[0]++);
System.out.println("Count: " + counter[0]); // 3

LƯU Ý QUAN TRỌNG: Kỹ thuật này KHÔNG thread-safe! Nếu nhiều threads cùng modify counter[0], sẽ xảy ra race condition:

// ❌ KHÔNG an toàn với multi-threading
int[] counter = {0};
list.parallelStream().forEach(item -> counter[0]++); // Race condition!

// ✅ Thread-safe alternative: dùng AtomicInteger
AtomicInteger safeCounter = new AtomicInteger(0);
list.parallelStream().forEach(item -> safeCounter.incrementAndGet()); // Safe

Best practice: Nếu cần mutable state trong lambda:

  • Single thread: Array trick OK (nhưng code smell - nên refactor)
  • Multi-thread: PHẢI dùng AtomicInteger, AtomicReference, hoặc synchronization
  • Tốt nhất: Tránh side effects, dùng functional approach (reduce, collect)

Lambda Scope: this keyword

Trong lambda, từ khóa this refer đến enclosing class instance, không phải lambda instance (khác với anonymous class).

public class LambdaScopeDemo {
private String name = "Outer";

public void testLambdaScope() {
// Lambda: 'this' refers to LambdaScopeDemo instance
Runnable lambdaRunnable = () -> {
System.out.println("Lambda this: " + this.name); // "Outer"
System.out.println("Lambda this class: " + this.getClass().getName());
// LambdaScopeDemo
};

// Anonymous class: 'this' refers to anonymous class instance
Runnable anonymousRunnable = new Runnable() {
private String name = "Inner";

@Override
public void run() {
System.out.println("Anonymous this: " + this.name); // "Inner"
System.out.println("Anonymous this class: " + this.getClass().getName());
// LambdaScopeDemo$1
System.out.println("Outer this: " + LambdaScopeDemo.this.name); // "Outer"
}
};

lambdaRunnable.run();
anonymousRunnable.run();
}

public static void main(String[] args) {
new LambdaScopeDemo().testLambdaScope();
}
}

Output:

Lambda this: Outer
Lambda this class: com.example.LambdaScopeDemo
Anonymous this: Inner
Anonymous this class: com.example.LambdaScopeDemo$1
Outer this: Outer

Lambda vs Anonymous Inner Class

So sánh chi tiết

Khía cạnhLambda ExpressionAnonymous Inner Class
Cú phápNgắn gọn: x -> x * 2Dài dòng: new Interface() { ... }
Functional interfaceChỉ dùng được với functional interfaceDùng với bất kỳ interface/class nào
this keywordRefers to enclosing classRefers to anonymous class
Variable captureChỉ effectively finalChỉ effectively final
CompileKhông tạo .class file riêngTạo OuterClass$1.class file
PerformanceNhanh hơn (invokedynamic)Chậm hơn (tạo object mỗi lần)
ScopeKhông có scope riêngCó scope riêng (có thể khai báo variables)
Access modifiersKhông cóCó thể là final, static, etc.

Ví dụ so sánh

import java.util.*;

public class LambdaVsAnonymousClass {
public static void main(String[] args) {
List<String> list = Arrays.asList("Java", "Python", "C++", "JavaScript");

// ❌ Anonymous Inner Class - dài dòng
Collections.sort(list, new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
});

// ✅ Lambda Expression - ngắn gọn
Collections.sort(list, (s1, s2) -> s1.length() - s2.length());

// ✅ Method Reference - ngắn gọn nhất
Collections.sort(list, Comparator.comparingInt(String::length));
}
}

Bên trong hoạt động ra sao: invokedynamic

Nhiều người nghĩ lambda chỉ là "đường cú pháp" (syntactic sugar) cho lớp vô danh (anonymous class). Thực tế, JVM xử lý lambda hoàn toàn khác — hiệu quả hơn nhiều.

Lambda KHÔNG tạo class file

Khi bạn viết anonymous class, compiler tạo một file .class riêng (ví dụ MyClass$1.class). Nhưng với lambda, không có file .class nào được tạo tại compile time.

Thay vào đó, compiler tạo một lệnh invokedynamic trong bytecode. Tại runtime, JVM sử dụng LambdaMetafactory để tạo implementation cho lambda — một cách linh hoạt và tối ưu hơn.

// Bytecode pseudocode cho lambda
// Thay vì: new MyClass$1() (anonymous class)
// Lambda dùng: invokedynamic → LambdaMetafactory.metafactory(...)

Tại sao invokedynamic nhanh hơn?

Khía cạnhAnonymous ClassLambda (invokedynamic)
Class loadingTạo và load class mới mỗi lầnKhông tạo class file
MemoryObject mới mỗi lần gọiJVM có thể cache và tái sử dụng
Khởi độngChậm (class loading overhead)Nhanh (bootstrap một lần)
Tối ưu hóaHạn chếJVM tự do chọn strategy tối ưu nhất
Tham khảo

Đọc thêm: JEP 276 — Dynamic Linking of Language-Defined Object Models và bài trình bày của Brian Goetz "Lambda: A Peek Under the Hood" tại JavaOne 2013.

Các trường hợp đặc biệt hay gặp trong OCP

Lambda và Checked Exceptions

Lambda trong Runnable không thể ném checked exception, nhưng Callable thì có thể:

// ❌ Compile error — Runnable.run() không throws Exception
Runnable r = () -> {
throw new IOException("error"); // ❌ Unhandled exception
};

// ✅ Callable.call() throws Exception
Callable<String> c = () -> {
throw new IOException("error"); // ✅ OK
return "result";
};

Vấn đề thực tế: Stream API và các functional interfaces chuẩn (Function, Consumer, v.v.) không declare checked exceptions, nhưng code thực tế thường gặp I/O operations:

List<Path> paths = List.of(Path.of("file1.txt"), Path.of("file2.txt"));

// ❌ KHÔNG compile — Files.readString() throws IOException
// paths.stream().map(path -> Files.readString(path));

Giải pháp 1: Try-catch trong lambda

// ✅ Wrap exception trong lambda body
List<String> contents = paths.stream()
.map(path -> {
try {
return Files.readString(path);
} catch (IOException e) {
throw new UncheckedIOException(e); // Wrap as unchecked
}
})
.toList();

Nhược điểm: Boilerplate code lặp lại nhiều lần, giảm tính concise của lambda.

Giải pháp 2: ThrowingFunction wrapper (Advanced)

Tạo functional interface cho phép throws exception, kết hợp với wrapper method:

@FunctionalInterface
interface ThrowingFunction<T, R> {
R apply(T t) throws Exception;
}

static <T, R> Function<T, R> unchecked(ThrowingFunction<T, R> f) {
return t -> {
try {
return f.apply(t);
} catch (Exception e) {
throw new RuntimeException(e); // Hoặc UncheckedIOException
}
};
}

Sử dụng:

import static YourClass.unchecked; // Static import

// ✅ Code ngắn gọn, dễ đọc
List<String> contents = paths.stream()
.map(unchecked(path -> Files.readString(path)))
.toList();

// Hoặc với method reference
List<String> contents = paths.stream()
.map(unchecked(Files::readString))
.toList();

Thư viện bên ngoài: Nhiều libraries cung cấp utilities tương tự:

  • Vavr: io.vavr.CheckedFunction1
  • jOOλ: org.jooq.lambda.Unchecked
Tham khảo

Xem thêm: Modern Java in Action (Ch.3) — Functional-style error handling.

var trong tham số lambda (Java 11+)

Từ Java 11, bạn có thể dùng var cho tham số lambda — hữu ích khi cần annotation:

// Không có var — không thể thêm annotation
BiFunction<String, String, String> f1 = (a, b) -> a + b;

// Có var — có thể thêm annotation
BiFunction<String, String, String> f2 = (@NonNull var a, @NonNull var b) -> a + b;

// ⚠️ Phải dùng var cho TẤT CẢ tham số, không được trộn
// ❌ (var a, String b) -> a + b // Compile error

Lỗi suy luận kiểu khi overload

public class OverloadDemo {
static void execute(Runnable r) { r.run(); }
static void execute(Callable<Integer> c) throws Exception { c.call(); }

public static void main(String[] args) {
// ❌ Ambiguous — compiler không biết chọn overload nào
// execute(() -> System.out.println("hello"));

// ✅ Cast tường minh
execute((Runnable) () -> System.out.println("hello"));
}
}

Ví dụ thực tế

1. Sorting với Comparator

import java.util.*;

class Employee {
private String name;
private int age;
private double salary;

public Employee(String name, int age, double salary) {
this.name = name;
this.age = age;
this.salary = salary;
}

// Getters
public String getName() { return name; }
public int getAge() { return age; }
public double getSalary() { return salary; }

@Override
public String toString() {
return String.format("%s (%d tuổi, %.0f VND)", name, age, salary);
}
}

public class SortingExample {
public static void main(String[] args) {
List<Employee> employees = Arrays.asList(
new Employee("An", 30, 15000000),
new Employee("Bình", 25, 12000000),
new Employee("Cường", 35, 20000000)
);

// Sort by age
employees.sort((e1, e2) -> e1.getAge() - e2.getAge());
System.out.println("Sorted by age: " + employees);

// Sort by salary descending
employees.sort((e1, e2) -> Double.compare(e2.getSalary(), e1.getSalary()));
System.out.println("Sorted by salary desc: " + employees);

// Sort by name length
employees.sort(Comparator.comparingInt(e -> e.getName().length()));
System.out.println("Sorted by name length: " + employees);
}
}

2. Event Handling (Swing/JavaFX)

import javax.swing.*;

public class EventHandlingExample {
public static void main(String[] args) {
JFrame frame = new JFrame("Lambda Event Demo");
JButton button = new JButton("Click Me");

// ❌ Old way: Anonymous inner class
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("Button clicked!");
}
});

// ✅ Lambda way
button.addActionListener(e -> System.out.println("Button clicked!"));

// ✅ Lambda with multiple statements
button.addActionListener(e -> {
System.out.println("Button clicked at: " + System.currentTimeMillis());
JOptionPane.showMessageDialog(frame, "Hello Lambda!");
});

frame.add(button);
frame.setSize(300, 200);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
}

3. Runnable và Thread

public class RunnableExample {
public static void main(String[] args) {
// ❌ Old way
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Thread 1 running");
}
});

// ✅ Lambda way
Thread thread2 = new Thread(() -> System.out.println("Thread 2 running"));

// ✅ Lambda with block
Thread thread3 = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
System.out.println("Thread 3: " + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});

thread1.start();
thread2.start();
thread3.start();
}
}

4. Custom Functional Interface

@FunctionalInterface
interface MathOperation {
int operate(int a, int b);
}

@FunctionalInterface
interface StringProcessor {
String process(String input);
}

public class CustomFunctionalInterfaceExample {
public static void main(String[] args) {
// MathOperation
MathOperation addition = (a, b) -> a + b;
MathOperation subtraction = (a, b) -> a - b;
MathOperation multiplication = (a, b) -> a * b;

System.out.println("10 + 5 = " + addition.operate(10, 5));
System.out.println("10 - 5 = " + subtraction.operate(10, 5));
System.out.println("10 * 5 = " + multiplication.operate(10, 5));

// StringProcessor
StringProcessor toUpperCase = s -> s.toUpperCase();
StringProcessor reverse = s -> new StringBuilder(s).reverse().toString();
StringProcessor addPrefix = s -> "Processed: " + s;

String input = "hello";
System.out.println(toUpperCase.process(input)); // HELLO
System.out.println(reverse.process(input)); // olleh
System.out.println(addPrefix.process(input)); // Processed: hello
}
}

Best Practices

✅ DO

// 1. Keep lambdas short (1-3 lines)
list.forEach(item -> System.out.println(item));

// 2. Use method references khi có thể
list.forEach(System.out::println); // Thay vì: item -> System.out.println(item)

// 3. Descriptive parameter names
employees.sort((emp1, emp2) -> emp1.getAge() - emp2.getAge());

// 4. Extract complex lambdas to methods
Predicate<Employee> isHighEarner = this::checkIfHighEarner;
list.stream().filter(isHighEarner);

❌ DON'T

// 1. Avoid long lambdas - extract to method
list.forEach(item -> {
// 50 lines of code... ❌ Too long!
});

// 2. Don't use single-letter names for complex logic
employees.filter(e -> e.getAge() > 30 && e.getSalary() > 5000); // ❌ 'e' unclear in complex logic

// 3. Don't modify external state in lambdas
int[] sum = {0};
list.forEach(x -> sum[0] += x); // ❌ Side effect

// 4. Don't use lambdas for recursion
// ❌ Lambdas không thể gọi chính nó (không có tên)

Tóm tắt

  • Lambda expression là anonymous function, cú pháp: (params) -> expression/block
  • Parameter styles: No param (), single param x, multiple params (a, b)
  • Body types: Expression body x -> x * 2, block body x -> { return x * 2; }
  • Variable capture: Chỉ capture effectively final variables
  • this keyword: Refers to enclosing class, không phải lambda
  • vs Anonymous class: Lambda ngắn gọn hơn, nhanh hơn, nhưng chỉ cho functional interfaces

Thử thách: Output là gì?

Câu 1: Effectively final

int x = 10;
Runnable r1 = () -> System.out.println(x);
// x = 20; // Dòng này bị comment
Runnable r2 = () -> System.out.println(x);
r2.run();
r1.run();
Đáp án

Output:

10
10

Cả hai lambda đều bắt giữ giá trị x = 10. Vì x không bao giờ bị gán lại nên nó effectively final. Thứ tự output là r2 rồi r1r2.run() được gọi trước.

Câu 2: this trong lambda vs anonymous class

public class Quiz {
String name = "Outer";

void test() {
Runnable lambda = () -> System.out.println(this.name);

Runnable anon = new Runnable() {
String name = "Inner";
public void run() { System.out.println(this.name); }
};

lambda.run();
anon.run();
}

public static void main(String[] args) {
new Quiz().test();
}
}
Đáp án

Output:

Outer
Inner

Trong lambda, this trỏ đến instance của lớp bao ngoài (Quiz), nên this.name"Outer". Trong anonymous class, this trỏ đến chính anonymous class, nên this.name"Inner".

Câu 3: Suy luận kiểu

// Dòng nào KHÔNG compile?
Object o1 = (Runnable) () -> System.out.println("A"); // Dòng 1
Object o2 = (Callable<Integer>) () -> 42; // Dòng 2
// Object o3 = () -> System.out.println("C"); // Dòng 3
Runnable o4 = () -> System.out.println("D"); // Dòng 4
Đáp án

Dòng 3 không compile (đã bị comment). Lambda phải có target typeObject không phải functional interface nên compiler không thể suy luận kiểu cho lambda. Dòng 1 và 2 compile được nhờ cast tường minh. Dòng 4 compile vì Runnable là functional interface.

Bài tập

Bài 1: Viết Lambda Expressions

Viết lambda expressions cho các functional interfaces sau:

// 1. Predicate<Integer>: kiểm tra số chẵn
Predicate<Integer> isEven = // TODO

// 2. Function<String, Integer>: độ dài string
Function<String, Integer> stringLength = // TODO

// 3. BiFunction<Integer, Integer, Integer>: tìm max
BiFunction<Integer, Integer, Integer> max = // TODO

// 4. Consumer<List<String>>: in tất cả elements
Consumer<List<String>> printAll = // TODO

Bài 2: Convert Anonymous Class to Lambda

Convert anonymous classes sau sang lambda:

// 1.
Comparator<String> comp = new Comparator<String>() {
@Override
public int compare(String s1, String s2) {
return s2.compareTo(s1);
}
};

// 2.
Thread t = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(i);
}
}
});

Bài 3: Effectively Final

Sửa code sau để compile thành công:

public void processData(List<Integer> numbers) {
int threshold = 10;

numbers.forEach(n -> {
if (n > threshold) {
System.out.println(n);
}
});

threshold = 20; // ❌ Error: variable used in lambda should be final

numbers.forEach(n -> {
if (n > threshold) {
System.out.println(n);
}
});
}

Bài tiếp theo: Functional Interfaces →

Tài liệu tham khảo

Sách

  • Modern Java in Action (Manning, 2019) — Ch.3: Lambda Expressions
    • Lambda syntax, method references, type inference
    • Functional-style error handling with checked exceptions
    • Best practices and performance considerations
  • Effective Java 3rd Edition — Item 42, 43
    • Item 42: Prefer lambdas to anonymous classes
    • Item 43: Prefer method references to lambdas

Specifications & Official Docs

Bài học liên quan