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

Method References

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

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

  • Hiểu được Method Reference là gì và khi nào dùng thay lambda
  • Sử dụng được 4 loại method references: Static, Instance (particular/arbitrary), Constructor
  • Phân biệt được Instance method of particular object vs arbitrary object
  • Biết cách dùng constructor references cho factory patterns
  • Áp dụng method references trong Stream operations

Bài trước: Stream Operations chi tiết — Đã học các operations để xử lý Stream. Bài này sẽ giới thiệu Method References - cách viết tắt cho lambdas.

Method Reference là gì?

Method Reference là một cách viết tắt (shorthand notation) cho lambda expression khi lambda chỉ gọi một method đã tồn tại.

Method reference sử dụng toán tử :: (double colon).

import java.util.*;
import java.util.function.*;

public class MethodReferenceIntro {
public static void main(String[] args) {
List<String> names = Arrays.asList("John", "Jane", "Jack");

// ❌ Lambda expression
names.forEach(name -> System.out.println(name));

// ✅ Method reference - ngắn gọn hơn
names.forEach(System.out::println);

// ❌ Lambda
Function<String, Integer> lengthLambda = s -> s.length();

// ✅ Method reference
Function<String, Integer> lengthRef = String::length;
}
}
Lợi ích của Method References
  1. Ngắn gọn (concise): Ngắn gọn hơn lambda
  2. Dễ đọc (readable): Dễ đọc khi method name rõ nghĩa
  3. Tái sử dụng (reusable): Tận dụng methods đã có
  4. Ít lỗi hơn (less error-prone): Ít lỗi cú pháp hơn

4 Loại Method References

Java hỗ trợ 4 loại method references:

LoạiCú phápLambda tương đươngVí dụ
1. Static methodClassName::staticMethod(args) -> ClassName.staticMethod(args)Integer::parseInt
2. Instance method of particular objectinstance::method(args) -> instance.method(args)System.out::println
3. Instance method of arbitrary objectClassName::instanceMethod(obj, args) -> obj.instanceMethod(args)String::length
4. ConstructorClassName::new(args) -> new ClassName(args)ArrayList::new

Quy tắc khớp chữ ký (Signature Matching)

Khi dùng method reference, compiler cần khớp chữ ký của method với functional interface. Quy tắc:

  • Static method: Tham số của functional interface → tham số của static method
    • Function<String, Integer> khớp Integer.parseInt(String)
  • Instance (particular): Tham số của functional interface → tham số của instance method
    • Consumer<String> khớp System.out.println(String)
  • Instance (arbitrary): Tham số ĐẦU TIÊN → đối tượng gọi method, còn lại → tham số
    • Function<String, Integer> khớp String::length(String s) → s.length()
    • BiFunction<String, String, Boolean> khớp String::startsWith(String s, String prefix) → s.startsWith(prefix)
  • Constructor: Tham số → tham số constructor
    • Function<String, Person> khớp Person::new nếu có constructor Person(String)
Method Reference với Overloaded Methods

Khi method được tham chiếu có nhiều bản overload, compiler chọn bản phù hợp dựa trên target type (kiểu functional interface đích). Nhưng đôi khi compiler không thể chọn và báo lỗi "reference to method is ambiguous":

// String có 2 overloads của valueOf:
// static String valueOf(Object obj)
// static String valueOf(int i)
// ... và nhiều overloads khác

// ✅ Compiler chọn đúng dựa trên target type
Function<Integer, String> f = String::valueOf; // chọn valueOf(int)
Function<Object, String> g = String::valueOf; // chọn valueOf(Object)

// ❌ Có thể gây ambiguity trong một số trường hợp
// BiFunction<char[], int, String> h = String::valueOf; // Có thể ambiguous

Khi gặp lỗi ambiguity, hãy chuyển sang dùng lambda expression để chỉ rõ method nào cần gọi.

1. Static Method Reference

Reference đến một static method của một class.

Cú pháp

ClassName::staticMethodName

Ví dụ

import java.util.*;
import java.util.function.*;

public class StaticMethodReference {
public static void main(String[] args) {
// Integer::parseInt
List<String> numbers = Arrays.asList("1", "2", "3", "4", "5");

// ❌ Lambda
List<Integer> parsed1 = numbers.stream()
.map(s -> Integer.parseInt(s))
.collect(Collectors.toList());

// ✅ Method reference
List<Integer> parsed2 = numbers.stream()
.map(Integer::parseInt)
.collect(Collectors.toList());

// Math::max
BiFunction<Integer, Integer, Integer> maxLambda = (a, b) -> Math.max(a, b);
BiFunction<Integer, Integer, Integer> maxRef = Math::max;
System.out.println(maxRef.apply(10, 20)); // 20

// Math::sqrt
Function<Double, Double> sqrtLambda = d -> Math.sqrt(d);
Function<Double, Double> sqrtRef = Math::sqrt;

// Custom static method
List<String> words = Arrays.asList("Java", "Python", "C++");
words.stream()
.map(StaticMethodReference::toUpperCaseWithPrefix)
.forEach(System.out::println);
}

// Custom static method
public static String toUpperCaseWithPrefix(String s) {
return "Language: " + s.toUpperCase();
}
}

Các ví dụ phổ biến

// String static methods
Predicate<String> isNumeric = s -> s.matches("\\d+"); // ❌ Lambda
// (No direct method reference for matches with argument)

// Collections static methods
Comparator<Integer> reverseOrder = Comparator.reverseOrder(); // Method call, not reference

// Custom utility class
class StringUtils {
public static boolean isEmpty(String s) {
return s == null || s.isEmpty();
}

public static String capitalize(String s) {
return s.substring(0, 1).toUpperCase() + s.substring(1);
}
}

List<String> words = Arrays.asList("java", "python", "c++");

// Use static method references
words.stream()
.map(StringUtils::capitalize)
.forEach(System.out::println);

2. Instance Method Reference (Particular Object)

Reference đến một instance method của một object cụ thể (đã tồn tại).

Cú pháp

objectInstance::instanceMethodName

Ví dụ

import java.util.*;
import java.util.function.*;

public class InstanceMethodOfParticularObject {
public static void main(String[] args) {
// System.out::println
List<String> names = Arrays.asList("John", "Jane", "Jack");

// ❌ Lambda
names.forEach(name -> System.out.println(name));

// ✅ Method reference
names.forEach(System.out::println);

// String instance method
String prefix = "Hello, ";

// ❌ Lambda
Function<String, String> greetLambda = name -> prefix.concat(name);

// ✅ Method reference
Function<String, String> greetRef = prefix::concat;
System.out.println(greetRef.apply("World")); // "Hello, World"

// Custom object
Printer printer = new Printer("[LOG] ");
List<String> messages = Arrays.asList("Started", "Processing", "Completed");

// ❌ Lambda
messages.forEach(msg -> printer.print(msg));

// ✅ Method reference
messages.forEach(printer::print);
}
}

class Printer {
private String prefix;

public Printer(String prefix) {
this.prefix = prefix;
}

public void print(String message) {
System.out.println(prefix + message);
}
}

Tham chiếu qua thissuper

Trong instance context, bạn có thể dùng this::methodsuper::method:

class Parent {
String greet(String name) {
return "Hello, " + name;
}
}

class Child extends Parent {
@Override
String greet(String name) {
return "Hi, " + name;
}

void demo() {
Function<String, String> childGreet = this::greet; // "Hi, ..."
Function<String, String> parentGreet = super::greet; // "Hello, ..."

System.out.println(childGreet.apply("An")); // Hi, An
System.out.println(parentGreet.apply("An")); // Hello, An
}
}

super::method hữu ích khi bạn muốn tham chiếu đến implementation của lớp cha trong một lambda hoặc stream pipeline.

Ví dụ với instance state

class DiscountCalculator {
private double discountRate;

public DiscountCalculator(double discountRate) {
this.discountRate = discountRate;
}

public double applyDiscount(double price) {
return price * (1 - discountRate);
}
}

public class InstanceMethodExample {
public static void main(String[] args) {
List<Double> prices = Arrays.asList(100.0, 200.0, 300.0);

DiscountCalculator calculator = new DiscountCalculator(0.2); // 20% discount

// ❌ Lambda
List<Double> discounted1 = prices.stream()
.map(price -> calculator.applyDiscount(price))
.collect(Collectors.toList());

// ✅ Method reference
List<Double> discounted2 = prices.stream()
.map(calculator::applyDiscount)
.collect(Collectors.toList());

System.out.println(discounted2); // [80.0, 160.0, 240.0]
}
}

3. Instance Method Reference (Arbitrary Object)

Reference đến một instance method của một object thuộc type chưa biết (sẽ được cung cấp trong stream).

Đây là loại khó hiểu nhất nhưng hữu ích nhất.

Cú pháp

ClassName::instanceMethodName

Sự khác biệt với loại 2

LoạiCú phápLambda tương đươngObject
Particular objectinstance::method(args) -> instance.method(args)Cố định (known)
Arbitrary objectClassName::method(obj, args) -> obj.method(args)Từ parameter đầu tiên (unknown)

Ví dụ

import java.util.*;
import java.util.function.*;

public class InstanceMethodOfArbitraryObject {
public static void main(String[] args) {
// String::length
List<String> words = Arrays.asList("Java", "Python", "C++");

// ❌ Lambda
List<Integer> lengths1 = words.stream()
.map(word -> word.length()) // 'word' là arbitrary object
.collect(Collectors.toList());

// ✅ Method reference
List<Integer> lengths2 = words.stream()
.map(String::length) // String là class, không phải instance cụ thể
.collect(Collectors.toList());

// String::toUpperCase
// ❌ Lambda
List<String> upper1 = words.stream()
.map(word -> word.toUpperCase())
.collect(Collectors.toList());

// ✅ Method reference
List<String> upper2 = words.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());

// Comparator
// ❌ Lambda
words.sort((s1, s2) -> s1.compareToIgnoreCase(s2));

// ✅ Method reference
words.sort(String::compareToIgnoreCase);

// Custom class
List<Person> people = Arrays.asList(
new Person("John", 25),
new Person("Jane", 30),
new Person("Jack", 20)
);

// ❌ Lambda
List<String> names1 = people.stream()
.map(person -> person.getName())
.collect(Collectors.toList());

// ✅ Method reference
List<String> names2 = people.stream()
.map(Person::getName)
.collect(Collectors.toList());
}
}

class Person {
private String name;
private int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() { return name; }
public int getAge() { return age; }
}

Ví dụ với BiFunction

// BiFunction<T, U, R>: (T, U) -> R
// String::concat: (String s1, String s2) -> s1.concat(s2)

BiFunction<String, String, String> concatLambda = (s1, s2) -> s1.concat(s2);
BiFunction<String, String, String> concatRef = String::concat;

String result = concatRef.apply("Hello", "World");
System.out.println(result); // "HelloWorld"

// String::startsWith: (String s, String prefix) -> s.startsWith(prefix)
BiFunction<String, String, Boolean> startsWithLambda = (s, prefix) -> s.startsWith(prefix);
BiFunction<String, String, Boolean> startsWithRef = String::startsWith;

boolean starts = startsWithRef.apply("Java", "Ja");
System.out.println(starts); // true

Khi nào dùng Arbitrary Object Reference?

Dùng khi:

  1. Stream operations: map(), filter() với instance methods
  2. Sorting: Comparator với instance methods
  3. Mapping: Transform objects sang properties
List<Employee> employees = getEmployees();

// Get all names
List<String> names = employees.stream()
.map(Employee::getName) // ✅ Arbitrary object reference
.collect(Collectors.toList());

// Sort by age
employees.sort(Comparator.comparingInt(Employee::getAge)); // ✅

// Filter active employees
List<Employee> active = employees.stream()
.filter(Employee::isActive) // ✅ Assuming boolean isActive()
.collect(Collectors.toList());
OCP Trap: String::compareTo là loại nào?
Comparator<String> comp = String::compareTo;

Nhiều người nhầm đây là static method reference vì cú pháp ClassName::method. Nhưng thực tế đây là instance method of arbitrary object (loại 3):

  • compareTo là instance method (không phải static)
  • Lambda tương đương: (s1, s2) -> s1.compareTo(s2)
  • s1 là đối tượng gọi method (arbitrary), s2 là tham số

Quy tắc phân biệt: Xem method có phải static không. Nếu KHÔNG phải static mà cú pháp là ClassName::method → đó là loại 3 (arbitrary object).

4. Constructor Reference

Reference đến một constructor để tạo object mới.

Cú pháp

ClassName::new

Ví dụ cơ bản

import java.util.*;
import java.util.function.*;
import java.util.stream.*;

public class ConstructorReference {
public static void main(String[] args) {
// No-arg constructor
// ❌ Lambda
Supplier<List<String>> listSupplier1 = () -> new ArrayList<>();

// ✅ Constructor reference
Supplier<List<String>> listSupplier2 = ArrayList::new;
List<String> list = listSupplier2.get();

// Single-arg constructor
// ❌ Lambda
Function<String, Person> personCreator1 = name -> new Person(name);

// ✅ Constructor reference
Function<String, Person> personCreator2 = Person::new;
Person person = personCreator2.apply("John");

// Two-arg constructor
// ❌ Lambda
BiFunction<String, Integer, Person> personCreator3 = (name, age) -> new Person(name, age);

// ✅ Constructor reference
BiFunction<String, Integer, Person> personCreator4 = Person::new;
Person person2 = personCreator4.apply("Jane", 25);

// Stream với constructor reference
List<String> names = Arrays.asList("John", "Jane", "Jack");

// ❌ Lambda
List<Person> people1 = names.stream()
.map(name -> new Person(name))
.collect(Collectors.toList());

// ✅ Constructor reference
List<Person> people2 = names.stream()
.map(Person::new)
.collect(Collectors.toList());
}
}

class Person {
private String name;
private int age;

// Constructor 1: Single-arg
public Person(String name) {
this(name, 0);
}

// Constructor 2: Two-arg
public Person(String name, int age) {
this.name = name;
this.age = age;
}

// Getters, toString...
}

Array Constructor Reference

// Array constructor reference
// ❌ Lambda
Function<Integer, String[]> arrayCreator1 = size -> new String[size];

// ✅ Constructor reference
Function<Integer, String[]> arrayCreator2 = String[]::new;
String[] array = arrayCreator2.apply(10);

// Trong Stream toArray()
List<String> words = Arrays.asList("Java", "Python", "C++");

// ❌ Lambda
String[] array1 = words.stream().toArray(size -> new String[size]);

// ✅ Constructor reference
String[] array2 = words.stream().toArray(String[]::new);

Constructor reference với generic

// ✅ Constructor reference — compiler suy luận generic type
Supplier<List<String>> listSupplier = ArrayList::new;
List<String> list = listSupplier.get(); // ArrayList<String>

// ⚠️ Chú ý: generic type được suy luận từ target type
Supplier<List<Integer>> intListSupplier = ArrayList::new;
List<Integer> intList = intListSupplier.get(); // ArrayList<Integer>

// Cùng ArrayList::new nhưng khác generic type!

Factory Pattern với Constructor Reference

import java.util.*;
import java.util.function.*;

interface Shape {
void draw();
}

class Circle implements Shape {
public void draw() { System.out.println("Drawing Circle"); }
}

class Rectangle implements Shape {
public void draw() { System.out.println("Drawing Rectangle"); }
}

class ShapeFactory {
private static Map<String, Supplier<Shape>> shapeMap = new HashMap<>();

static {
// ✅ Constructor references
shapeMap.put("circle", Circle::new);
shapeMap.put("rectangle", Rectangle::new);
}

public static Shape createShape(String type) {
Supplier<Shape> supplier = shapeMap.get(type.toLowerCase());
if (supplier == null) {
throw new IllegalArgumentException("Unknown shape: " + type);
}
return supplier.get();
}
}

public class ConstructorReferenceFactory {
public static void main(String[] args) {
Shape circle = ShapeFactory.createShape("circle");
circle.draw(); // Drawing Circle

Shape rectangle = ShapeFactory.createShape("rectangle");
rectangle.draw(); // Drawing Rectangle
}
}

Khi nào dùng Method Reference vs Lambda?

✅ Dùng Method Reference khi:

// 1. Lambda chỉ gọi 1 method với chính parameters của nó
list.forEach(item -> System.out.println(item)); // ❌
list.forEach(System.out::println); // ✅

// 2. Method name rõ nghĩa
numbers.stream()
.map(n -> String.valueOf(n)) // ❌
.map(String::valueOf); // ✅

// 3. Reuse existing methods
people.stream()
.map(p -> p.getName()) // ❌
.map(Person::getName); // ✅

❌ Dùng Lambda khi:

// 1. Logic phức tạp hơn một method call
list.forEach(item -> {
System.out.println("Processing: " + item);
processItem(item);
}); // ✅ Lambda

// 2. Cần thêm arguments
numbers.stream()
.map(n -> Math.pow(n, 2)) // ✅ Lambda (thêm argument 2)
// Không thể dùng Math::pow vì cần 2 arguments

// 3. Cần transform arguments
words.stream()
.map(s -> s.substring(0, 3)) // ✅ Lambda (transform arguments)
// Không thể dùng String::substring trực tiếp

// 4. Dễ hiểu hơn với lambda
Predicate<String> isLong = s -> s.length() > 10; // ✅ Clear
// vs
Predicate<String> isLong = ...; // Không có method sẵn

Bảng tổng hợp 4 loại Method References

LoạiCú phápLambdaVí dụUse Case
Static methodClass::staticMethod(args) -> Class.staticMethod(args)Integer::parseIntStatic utility methods
Instance (particular)obj::method(args) -> obj.method(args)System.out::printlnSpecific object's method
Instance (arbitrary)Class::method(obj, args) -> obj.method(args)String::lengthStream mapping, comparing
ConstructorClass::new(args) -> new Class(args)ArrayList::newObject creation, factories

Ví dụ tổng hợp: Stream-based Data Processor

import java.util.*;
import java.util.stream.*;

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

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

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

public boolean isHighEarner() {
return salary > 6000;
}

@Override
public String toString() {
return String.format("%s (%s, %d years, $%.0f)", name, department, age, salary);
}
}

class EmployeeUtils {
// Static method
public static boolean isAdult(Employee e) {
return e.getAge() >= 18;
}

// Static method
public static String formatName(String name) {
return name.toUpperCase();
}
}

public class MethodReferenceComprehensive {
public static void main(String[] args) {
List<Employee> employees = Arrays.asList(
new Employee("An", "IT", 25, 5000),
new Employee("Bình", "HR", 30, 7000),
new Employee("Cường", "IT", 28, 8000),
new Employee("Dung", "Finance", 26, 6500),
new Employee("Em", "HR", 22, 4500)
);

System.out.println("=== 1. Static Method Reference ===");
// Filter adults using static method
List<Employee> adults = employees.stream()
.filter(EmployeeUtils::isAdult) // Static method reference
.collect(Collectors.toList());
System.out.println("Adults: " + adults.size());

System.out.println("\n=== 2. Instance Method (Arbitrary Object) ===");
// Get all names
List<String> names = employees.stream()
.map(Employee::getName) // Instance method of arbitrary object
.collect(Collectors.toList());
System.out.println("Names: " + names);

// Get all departments
Set<String> departments = employees.stream()
.map(Employee::getDepartment) // Instance method reference
.collect(Collectors.toSet());
System.out.println("Departments: " + departments);

// Filter high earners using instance method
List<Employee> highEarners = employees.stream()
.filter(Employee::isHighEarner) // Instance method reference (boolean)
.collect(Collectors.toList());
System.out.println("High earners: " + highEarners.size());

System.out.println("\n=== 3. Instance Method (Particular Object) ===");
// Print all employees
System.out.println("All employees:");
employees.forEach(System.out::println); // Instance method of particular object

System.out.println("\n=== 4. Constructor Reference ===");
// Create new employees from names
List<String> newNames = Arrays.asList("Phúc", "Giang", "Hùng");
List<Employee> newEmployees = newNames.stream()
.map(name -> new Employee(name, "New", 25, 5000))
// Constructor reference không hoàn toàn fit vì cần nhiều params
.collect(Collectors.toList());

// Better example: Simple constructor
Supplier<ArrayList<Employee>> listFactory = ArrayList::new;
ArrayList<Employee> newList = listFactory.get();

System.out.println("\n=== 5. Combined Operations ===");
// Complex pipeline with multiple method references
Map<String, Double> avgSalaryByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment, // Method reference
Collectors.averagingDouble(Employee::getSalary) // Method reference
));
System.out.println("Average salary by department:");
avgSalaryByDept.forEach((dept, avg) ->
System.out.printf(" %s: $%.2f%n", dept, avg));

System.out.println("\n=== 6. Sorting with Method References ===");
// Sort by name
List<Employee> sortedByName = employees.stream()
.sorted(Comparator.comparing(Employee::getName)) // Method reference
.collect(Collectors.toList());
System.out.println("Sorted by name: " +
sortedByName.stream().map(Employee::getName).collect(Collectors.toList()));

// Sort by salary descending
List<Employee> sortedBySalary = employees.stream()
.sorted(Comparator.comparingDouble(Employee::getSalary).reversed())
.limit(3)
.collect(Collectors.toList());
System.out.println("Top 3 by salary:");
sortedBySalary.forEach(System.out::println);

System.out.println("\n=== 7. Transform Names ===");
// Format names using static method
List<String> formattedNames = employees.stream()
.map(Employee::getName) // Instance method reference
.map(EmployeeUtils::formatName) // Static method reference
.collect(Collectors.toList());
System.out.println("Formatted names: " + formattedNames);
}
}

Best Practices

✅ DO

// 1. Use method references for simple method calls
list.forEach(System.out::println);

// 2. Use constructor references for object creation
Stream.of("a", "b", "c").map(String::new);

// 3. Chain method references in streams
employees.stream()
.map(Employee::getName)
.map(String::toUpperCase)
.forEach(System.out::println);

// 4. Use method references with Comparator
list.sort(Comparator.comparing(Person::getName));

❌ DON'T

// 1. Don't force method references when lambda is clearer
numbers.stream()
.map(n -> n * 2 + 1) // ✅ Lambda is clearer
// No simple method reference for this

// 2. Don't create methods just to use method references
// ❌ Unnecessary method
public static int doubleAndAddOne(int n) {
return n * 2 + 1;
}
numbers.stream().map(MyClass::doubleAndAddOne);

// ✅ Just use lambda
numbers.stream().map(n -> n * 2 + 1);

// 3. Don't use method references for complex logic
// ❌ Method reference hides complexity
list.forEach(this::complexProcessing);

// ✅ Lambda makes complexity visible
list.forEach(item -> {
// Complex multi-step processing visible here
validate(item);
transform(item);
save(item);
});

Tóm tắt

  • Method Reference là shorthand cho lambda khi lambda chỉ gọi 1 method
  • 4 loại: Static method, Instance method (particular/arbitrary), Constructor
  • Static: ClassName::staticMethod - cho utility methods
  • Instance (particular): instance::method - cho object cụ thể
  • Instance (arbitrary): ClassName::method - cho stream operations
  • Constructor: ClassName::new - cho object creation
  • Khi nào dùng: Lambda chỉ gọi 1 method, method name rõ nghĩa
  • Khi nào không: Logic phức tạp, cần transform arguments, lambda rõ ràng hơn

Bài tập

Bài 1: Convert Lambda to Method Reference

Convert các lambda expressions sau sang method references:

// 1.
list.forEach(item -> System.out.println(item));

// 2.
numbers.stream().map(n -> String.valueOf(n));

// 3.
words.stream().map(s -> s.length());

// 4.
people.stream().sorted((p1, p2) -> p1.getName().compareTo(p2.getName()));

// 5.
Stream.of("a", "b", "c").map(s -> new Person(s));

Bài 2: Identify Method Reference Types

Xác định loại method reference (1, 2, 3, hay 4):

// a.
list.forEach(System.out::println);

// b.
words.stream().map(String::toUpperCase);

// c.
numbers.stream().map(Integer::parseInt);

// d.
names.stream().map(Person::new);

// e.
Printer printer = new Printer();
list.forEach(printer::print);

Bài 3: Stream-based Data Processor

Viết stream pipeline xử lý danh sách Product:

class Product {
String name;
double price;
String category;

// Constructor, getters...
}

List<Product> products = getProducts();

// TODO: Sử dụng method references để:
// 1. Filter products với price > 100
// 2. Group by category
// 3. Get product names per category
// 4. Sort names alphabetically
// 5. Print results

Chúc mừng! Bạn đã hoàn thành Module 10: Functional Programming trong Java.

Next steps:

  • Thực hành các bài tập trong từng lesson
  • Áp dụng functional programming vào projects
  • Tìm hiểu thêm về Stream API advanced topics
  • Explore Reactive Programming với Project Reactor hoặc RxJava

Đọc thêm