Chào mừng bạn đến với bài hướng dẫn ví dụ về Functional Interface trong Java 8. Java từ trước đến nay vốn là một ngôn ngữ lập trình hướng đối tượng. Điều đó có nghĩa là mọi thứ trong lập trình Java đều xoay quanh các đối tượng (ngoại trừ một số kiểu dữ liệu nguyên thủ để đơn giản hóa).
Trong Java, chúng ta không có các hàm độc lập – mọi hàm đều thuộc về một class, và chúng ta cần sử dụng lớp hoặc đối tượng để gọi bất kỳ hàm nào.
Functional Interface là gì?
Nếu chúng ta nhìn vào một số ngôn ngữ lập trình khác như C++, JavaScript; chúng được gọi là ngôn ngữ lập trình hàm (functional programming languages) vì chúng ta có thể viết các hàm riêng lẻ và sử dụng khi cần. Một số ngôn ngữ trong đó hỗ trợ cả lập trình hướng đối tượng lẫn lập trình hàm. Việc lập trình hướng đối tượng không hề xấu, nhưng nó khiến chương trình trở nên dài dòng và rườm rà. Ví dụ, giả sử chúng ta cần tạo một instance của Runnable. Thông thường, ta làm điều đó bằng cách sử dụng lớp ẩn danh như bên dưới.
Runnable r = new Runnable(){
@Override
public void run() {
System.out.println("My Runnable");
}};
Nếu bạn nhìn vào đoạn mã trên, phần thực sự quan trọng là phần mã nằm trong phương thức run(). Phần còn lại chỉ là do cấu trúc chương trình Java yêu cầu. Functional Interfaces và Lambda Expressions trong Java 8 giúp chúng ta viết mã ngắn gọn và sạch hơn bằng cách loại bỏ rất nhiều mã lặp lại (boiler-plate code).
Đặc điểm của Functional Interface
Một interface chỉ có duy nhất một phương thức trừu tượng thì được gọi là Functional Interface. Annotation @FunctionalInterface
được thêm vào để giúp đánh dấu một interface là functional. Việc sử dụng annotation này không bắt buộc, nhưng là một thực hành tốt để tránh việc vô tình thêm các phương thức khác. Nếu một interface được chú thích bằng @FunctionalInterface
và ta cố gắng thêm nhiều hơn một phương thức trừu tượng, trình biên dịch sẽ báo lỗi.
Lợi ích lớn nhất của functional interfaces trong Java 8 là chúng ta có thể dùng lambda expressions để khởi tạo chúng và không cần phải viết các lớp ẩn danh dài dòng. API Collections trong Java 8 đã được viết lại và một API mới là Stream API đã được giới thiệu, sử dụng rất nhiều functional interface. Java 8 đã định nghĩa rất nhiều functional interface trong gói java.util.function
.
Một số functional interface hữu ích trong Java 8 là Consumer
,Supplier
, Function
, Predicate
. Bạn có thể tìm hiểu thêm về chúng trong phần ví dụ về Java 8 Stream. java.lang.Runnable
là một ví dụ điển hình của functional interface vì nó chỉ có một phương thức trừu tượng là run()
. Đoạn mã bên dưới cung cấp một số hướng dẫn về functional interface:
interface Foo { boolean equals(Object obj); }
// Not functional because equals is already an implicit member (Object class)
interface Comparator<T> {
boolean equals(Object obj);
int compare(T o1, T o2);
}
// Functional because Comparator has only one abstract non-Object method
interface Foo {
int m();
Object clone();
}
// Not functional because method Object.clone is not public
interface X { int m(Iterable<String> arg); }
interface Y { int m(Iterable<String> arg); }
interface Z extends X, Y {}
// Functional: two methods, but they have the same signature
interface X { Iterable m(Iterable<String> arg); }
interface Y { Iterable<String> m(Iterable arg); }
interface Z extends X, Y {}
// Functional: Y.m is a subsignature & return-type-substitutable
interface X { int m(Iterable<String> arg); }
interface Y { int m(Iterable<Integer> arg); }
interface Z extends X, Y {}
// Not functional: No method has a subsignature of all abstract methods
interface X { int m(Iterable<String> arg, Class c); }
interface Y { int m(Iterable arg, Class<?> c); }
interface Z extends X, Y {}
// Not functional: No method has a subsignature of all abstract methods
interface X { long m(); }
interface Y { int m(); }
interface Z extends X, Y {}
// Compiler error: no method is return type substitutable
interface Foo<T> { void m(T arg); }
interface Bar<T> { void m(T arg); }
interface FooBar<X, Y> extends Foo<X>, Bar<Y> {}
// Compiler error: different signatures, same erasure
Biểu thức Lambda
Biểu thức Lambda (Lambda Expression) là cách giúp chúng ta hình dung lập trình hàm trong thế giới Java hướng đối tượng. Đối tượng là nền tảng của ngôn ngữ lập trình Java, và chúng ta không thể có một hàm mà không gắn liền với đối tượng.
Đó là lý do vì sao Java chỉ hỗ trợ lambda expression với functional interfaces. Vì functional interface chỉ có một phương thức trừu tượng, nên sẽ không xảy ra nhầm lẫn khi áp dụng lambda expression vào phương thức đó. Cú pháp của lambda expression là (tham số) -> (phần thân). Giờ hãy xem làm thế nào để viết lại Runnable ở trên bằng lambda expression.
Runnable r1 = () -> System.out.println("My Runnable");
Hãy cùng tìm hiểu chuyện gì đang diễn ra trong lambda expression bên trên:
- Runnable là một functional interface, vì vậy ta có thể dùng lambda expression để tạo instance của nó.
- Vì phương thức run() không có tham số, nên lambda expression của ta cũng không có tham số.
Tương tự như các khối if-else, nếu phần thân chỉ có một câu lệnh duy nhất, ta có thể bỏ dấu {}. Nếu có nhiều câu lệnh, vẫn phải dùng {} như trong các phương thức thông thường.
Tại sao chúng ta cần Lambda Expression?
1. Giảm số dòng mã
Một lợi ích rõ ràng của lambda expression là giúp giảm đáng kể số lượng mã phải viết. Như ta đã thấy, việc tạo thể hiện của functional interface bằng lambda dễ dàng và ngắn gọn hơn nhiều so với dùng lớp ẩn danh.
2. Hỗ trợ xử lý tuần tự và song song
Một lợi ích khác là lambda expression cho phép ta tận dụng khả năng xử lý tuần tự và song song trong Stream API. Ví dụ, giả sử ta cần viết một hàm kiểm tra xem một số có phải là số nguyên tố không. Thông thường, chúng ta viết như sau. Đoạn mã này chưa được tối ưu hoàn toàn, nhưng đủ tốt để làm ví dụ minh họa, nên mong bạn thông cảm nhé.
//Traditional approach
private static boolean isPrime(int number) {
if(number < 2) return false;
for(int i=2; i<number; i++){
if(number % i == 0) return false;
}
return true;
}
Vấn đề của đoạn mã trên là nó xử lý tuần tự. Nếu number rất lớn, nó sẽ mất nhiều thời gian. Một vấn đề khác là có quá nhiều điểm kết thúc (return), khiến mã khó đọc. Hãy xem chúng ta có thể viết lại phương thức này bằng lambda expression và Stream API như thế nào.
//Declarative approach
private static boolean isPrime(int number) {
return number > 1
&& IntStream.range(2, number).noneMatch(
index -> number % index == 0);
}
IntStream
là một chuỗi các phần tử kiểu int nguyên thủy, hỗ trợ các thao tác gom nhóm tuần tự và song song. Đây là phiên bản đặc biệt của Stream
dành cho kiểu nguyên thủy int. Để dễ đọc hơn, ta có thể viết lại phương thức như sau.
private static boolean isPrime(int number) {
IntPredicate isDivisible = index -> number % index == 0;
return number > 1
&& IntStream.range(2, number).noneMatch(
isDivisible);
}
Nếu bạn chưa quen với IntStream, phương thức range() trả về một IntStream tuần tự, có thứ tự từ startInclusive (bao gồm) đến endExclusive (không bao gồm), bước nhảy là 1. Phương thức noneMatch() trả về true nếu không có phần tử nào trong stream thỏa mãn điều kiện (predicate) đã cho. Nó có thể không cần duyệt hết tất cả phần tử nếu đã đủ để xác định kết quả.
3. Truyền hành vi vào phương thức
Giờ hãy xem làm thế nào để sử dụng lambda expression để truyền hành vi (logic) vào một phương thức qua ví dụ đơn giản. Giả sử chúng ta cần viết một phương thức để tính tổng các số trong danh sách nếu chúng thỏa mãn một điều kiện nhất định. Ta có thể dùng Predicate và viết như sau.
public static int sumWithCondition(List<Integer> numbers, Predicate<Integer> predicate) {
return numbers.parallelStream()
.filter(predicate)
.mapToInt(i -> i)
.sum();
}
Sử dụng mẫu:
//sum of all numbers
sumWithCondition(numbers, n -> true)
//sum of all even numbers
sumWithCondition(numbers, i -> i%2==0)
//sum of all numbers greater than 5
sumWithCondition(numbers, i -> i>5)
4. Tăng hiệu suất với cơ chế đánh giá lười
Một lợi thế nữa của lambda expression là đánh giá lười (lazy evaluation)., Ví dụ, giả sử ta cần viết một phương thức tìm số lẻ lớn nhất trong khoảng từ 3 đến 11, rồi trả về bình phương của số đó. Thông thường ta viết như sau:
private static int findSquareOfMaxOdd(List<Integer> numbers) {
int max = 0;
for (int i : numbers) {
if (i % 2 != 0 && i > 3 && i < 11 && i > max) {
max = i;
}
}
return max * max;
}
Chương trình trên luôn chạy theo thứ tự tuần tự. Tuy nhiên, ta có thể dùng Stream API để viết lại theo phong cách lập trình hàm và tận dụng được lợi ích của đánh giá lười. Hãy cùng xem cách chúng ta có thể viết lại đoạn mã này theo phong cách lập trình hàm bằng cách sử dụng Stream API và lambda expressions.
public static int findSquareOfMaxOdd(List<Integer> numbers) {
return numbers.stream()
.filter(NumberTest::isOdd) //Predicate is functional interface and
.filter(NumberTest::isGreaterThan3) // we are using lambdas to initialize it
.filter(NumberTest::isLessThan11) // rather than anonymous inner classes
.max(Comparator.naturalOrder())
.map(i -> i * i)
.get();
}
public static boolean isOdd(int i) {
return i % 2 != 0;
}
public static boolean isGreaterThan3(int i){
return i > 3;
}
public static boolean isLessThan11(int i){
return i < 11;
}
Nếu bạn thắc mắc về toán tử :: (double colon) – nó được giới thiệu trong Java 8 và dùng để tham chiếu phương thức (method references). Trình biên dịch Java sẽ tự động ánh xạ đối số với phương thức được gọi. Đây là cách viết ngắn gọn của lambda expressions như i -> isGreaterThan3(i)
hoặc i -> NumberTest.isGreaterThan3(i)
.
Các ví dụ về Lambda Expression
Dưới đây là một vài đoạn mã ví dụ về lambda expression, kèm theo chú thích ngắn để giải thích chúng.
() -> {} // No parameters; void result
() -> 42 // No parameters, expression body
() -> null // No parameters, expression body
() -> { return 42; } // No parameters, block body with return
() -> { System.gc(); } // No parameters, void block body
// Complex block body with multiple returns
() -> {
if (true) return 10;
else {
int result = 15;
for (int i = 1; i < 10; i++)
result *= i;
return result;
}
}
(int x) -> x+1 // Single declared-type argument
(int x) -> { return x+1; } // same as above
(x) -> x+1 // Single inferred-type argument, same as below
x -> x+1 // Parenthesis optional for single inferred-type case
(String s) -> s.length() // Single declared-type argument
(Thread t) -> { t.start(); } // Single declared-type argument
s -> s.length() // Single inferred-type argument
t -> { t.start(); } // Single inferred-type argument
(int x, int y) -> x+y // Multiple declared-type parameters
(x,y) -> x+y // Multiple inferred-type parameters
(x, final y) -> x+y // Illegal: can't modify inferred-type parameters
(x, int y) -> x+y // Illegal: can't mix inferred and declared types
Tham chiếu phương thức và hàm khởi tạo
Tham chiếu phương thức (Method reference) được dùng để tham chiếu đến một phương thức mà không gọi nó ngay. Tham chiếu hàm khởi tạo (Constructor reference) tương tự, nhưng dùng để tham chiếu đến constructor mà không tạo một đối tượng mới ngay lập tức. Ví dụ method và constructor reference:
System::getProperty
System.out::println
"abc"::length
ArrayList::new
int[]::new
Đó là tất cả những gì bạn cần biết về Functional Interface và Lambda Expression trong Java 8. Mình thực sự khuyên bạn nên bắt đầu sử dụng chúng, vì cú pháp này còn khá mới trong Java và sẽ cần một chút thời gian để làm quen. Bạn cũng nên xem qua các tính năng nổi bật của Java 8 để khám phá toàn bộ những cải tiến và thay đổi trong bản phát hành Java 8.