Functional interface là interface có đúng một abstract method, và đó là toàn bộ định nghĩa. Nó quan trọng vì lambda không có type của riêng nó: lambda được convert sang một functional interface, nên interface một abstract method là thứ duy nhất mà lambda có thể gán vào.
Package java.util.function cung cấp sẵn những shape mà nếu không có bạn sẽ phải viết lại mãi. Nhìn vào thì thấy một bức tường 43 cái tên, và phần lớn tutorial trình bày nó như một danh sách để học thuộc. Nó không phải danh sách. Nó là một cái lưới nhỏ, và khi đã định vị được một cái tên trên lưới thì phần còn lại suy ra được chứ không cần nhớ.
![]()
Mọi chương trình, mọi con số và mọi thông báo compile bên dưới đều được tạo ra bằng cách compile và chạy code trên OpenJDK 21.0.6 (arm64). Số lượng interface được đọc trực tiếp từ module image của chính JDK đó chứ không lấy từ trí nhớ.
Mọi functional interface đều là một ô trên cùng một lưới
Hai câu hỏi mô tả trọn vẹn một shape: abstract method duy nhất nhận bao nhiêu argument, và nó trả về cái gì. Trả lời được cả hai thì cái tên tự hiện ra.

Bốn ô được dùng hằng ngày:
import java.util.function.*;
public class Shapes {
public static void main(String[] args) {
Supplier<String> today = () -> "2026-10-17";
Consumer<String> log = s -> System.out.println("[log] " + s);
Function<String, Integer> length = String::length;
Predicate<String> blank = String::isBlank;
System.out.println(today.get());
log.accept("order created");
System.out.println(length.apply("functional"));
System.out.println(blank.test(" ") + " " + blank.test(" x "));
}
}
2026-10-17
[log] order created
10
true false
Phần còn lại của lưới vẫn là bốn ý đó, chỉ đổi số lượng argument hoặc đổi type:
import java.util.function.*;
public class Shapes2 {
public static void main(String[] args) {
UnaryOperator<String> trim = String::trim;
BinaryOperator<Integer> max = Integer::max;
BiFunction<String, Integer, String> repeat = String::repeat;
BiConsumer<String, Integer> show = (k, v) -> System.out.println(k + "=" + v);
BiPredicate<String, Integer> fits = (s, n) -> s.length() <= n;
Runnable task = () -> System.out.println("ran");
System.out.println("[" + trim.apply(" padded ") + "]");
System.out.println(max.apply(2, 3));
System.out.println(repeat.apply("ab", 3));
show.accept("retries", 3);
System.out.println(fits.test("abc", 3));
task.run();
}
}
[padded]
3
ababab
retries=3
true
ran
Đó là chín reference shape — những shape mà type parameter là object bình thường. Để ý mỗi họ dùng một tên method khác nhau, và điều đó là cố ý: get, accept, apply và test cho bạn biết shape ngay tại chỗ gọi, kể cả khi tên biến không nói gì.
| Interface | Method | Signature | Dùng khi |
|---|---|---|---|
Supplier<T> | get | () -> T | một giá trị cần được tạo ra sau, hoặc không tạo ra lần nào |
Consumer<T> | accept | T -> void | làm gì đó với một giá trị và không trả về gì |
Function<T,R> | apply | T -> R | biến một giá trị thành một giá trị khác |
Predicate<T> | test | T -> boolean | hỏi một giá trị câu hỏi có hoặc không |
UnaryOperator<T> | apply | T -> T | type ra trùng type vào |
BiConsumer<T,U> | accept | (T, U) -> void | hai giá trị vào, không có gì ra |
BiFunction<T,U,R> | apply | (T, U) -> R | hai giá trị vào, một giá trị thứ ba ra |
BiPredicate<T,U> | test | (T, U) -> boolean | hai giá trị vào, một câu trả lời có hoặc không |
BinaryOperator<T> | apply | (T, T) -> T | hai giá trị cùng type gộp thành một |
UnaryOperator<T> và BinaryOperator<T> không thêm gì ngoài một cái tên hẹp hơn: chúng extend Function<T,T> và BiFunction<T,T,T> rồi khai báo đúng không abstract method nào của riêng mình. Reflection trên OpenJDK 21.0.6 xác nhận điều đó — UnaryOperator.class.getDeclaredMethods() chỉ báo một thành viên không phải synthetic, là static identity.
Góc không nhận gì và không trả gì không có mặt trong java.util.function vì java.lang.Runnable đã chiếm chỗ đó từ Java 1.0. Java 8 gắn thêm @FunctionalInterface cho nó, và nó làm target cho lambda y như mọi interface khác.
Đếm package này trên JDK đó bằng cách liệt kê /modules/java.base/java/util/function trong filesystem jrt: cho ra 43 type, tất cả đều là interface, tất cả đều có đúng một abstract method, tất cả đều được gắn @FunctionalInterface. Chín cái là reference shape ở trên. 34 cái còn lại là primitive specialisation của chính chín shape đó, và phần dưới sẽ nói kỹ.
Annotation @FunctionalInterface chỉ là một kiểm tra lúc compile
Annotation không làm cho một interface trở thành functional. Quy tắc một abstract method mới làm việc đó, và compiler áp dụng quy tắc ấy dù bạn có viết annotation hay không:
interface Rule { // no @FunctionalInterface anywhere
boolean holds(int n);
}
public class NoAnnotation {
public static void main(String[] args) {
Rule even = n -> n % 2 == 0;
System.out.println(even.holds(4));
System.out.println(Rule.class.isAnnotationPresent(FunctionalInterface.class));
}
}
true
false
Lambda compile được, interface là functional, mà annotation thì không có. Cái annotation mua về là một hàng rào ở phía khai báo: nó làm hỏng build ngay khi interface thôi không còn functional, nên đồng nghiệp thêm abstract method thứ hai sẽ biết ngay thay vì làm vỡ mọi chỗ gọi.
@FunctionalInterface
interface TwoJobs {
void start();
void stop();
}
E1.java:1: error: Unexpected @FunctionalInterface annotation
@FunctionalInterface
^
TwoJobs is not a functional interface
multiple non-overriding abstract methods found in interface TwoJobs
1 error
Interface không có abstract method nào cũng hỏng y như vậy, chỉ khác dòng thứ hai:
E2.java:1: error: Unexpected @FunctionalInterface annotation
@FunctionalInterface
^
Marker is not a functional interface
no abstract method found in interface Marker
1 error
Và gắn nó lên một class cũng thế:
E3.java:1: error: Unexpected @FunctionalInterface annotation
@FunctionalInterface
^
NotAnInterface is not a functional interface
1 error
Có @FunctionalInterface | Không có | |
|---|---|---|
| Lambda dùng được không | được, nếu có một abstract method | được, nếu có một abstract method |
| Thêm abstract method thứ hai | build hỏng ngay tại khai báo | build hỏng tại từng lambda |
| Hành vi lúc runtime | giống hệt | giống hệt |
| Retention | RUNTIME, reflection đọc được | không có gì để đọc |
JDK áp dụng nó không đồng đều, và chính điều đó nói lên nhiều thứ. Cả 43 interface trong java.util.function đều có, java.lang.Runnable, java.util.concurrent.Callable và java.util.Comparator cũng vậy. Nhưng java.lang.Iterable, java.lang.AutoCloseable và java.util.concurrent.Executor mỗi cái có đúng một abstract method — iterator, close, execute — và theo đúng quy tắc của ngôn ngữ thì chúng là functional interface, nhưng không mang annotation. Annotation là một lời tuyên bố ý định: type này sinh ra để được implement bằng lambda. Iterable thì không.
Những method nào không tính là abstract method
Có bốn loại thành viên nằm được trong một functional interface mà không tiêu mất cái slot abstract duy nhất.
Method default, static và private đều có body, nên chúng không abstract và không bao giờ bị tính:
@FunctionalInterface
interface Validator<T> {
boolean validate(T value);
default Validator<T> andAlso(Validator<T> other) {
return v -> validate(v) && other.validate(v);
}
static <T> Validator<T> alwaysTrue() {
return v -> true;
}
private String tag() { return "validator"; }
default String describe() { return tag() + " with one abstract method"; }
}
Loại thứ tư mới là thứ làm người ta bất ngờ: một method public abstract override một method public của java.lang.Object cũng không bị tính. Mọi class implement đều đã thừa hưởng phần cài đặt từ Object, nên không còn gì để lambda phải cung cấp. Interface dưới đây khai báo bốn abstract method mà vẫn là functional:
@FunctionalInterface
interface Renderer {
String render();
@Override String toString();
@Override int hashCode();
@Override boolean equals(Object other);
}
public class Sam {
public static void main(String[] args) {
Renderer r = () -> "rendered";
System.out.println(r.render());
Validator<String> notEmpty = s -> !s.isEmpty();
Validator<String> under8 = s -> s.length() < 8;
System.out.println(notEmpty.andAlso(under8).validate("hello"));
System.out.println(notEmpty.andAlso(under8).validate("hello world"));
System.out.println(Validator.<String>alwaysTrue().validate(""));
System.out.println(notEmpty.describe());
}
}
rendered
true
false
true
validator with one abstract method
Đây không phải chuyện lạ giữ lại để hỏi đố. Chính JDK dùng nó: reflection trên OpenJDK 21.0.6 cho thấy java.util.Comparator khai báo hai abstract method, compare(Object, Object) và equals(Object), trong khi vẫn mang @FunctionalInterface. Cái equals khai báo lại nằm đó để ghi rõ rằng equality của một comparator có ý nghĩa chặt hơn của Object; nó không tốn gì của interface cả.
Quy tắc này hẹp hơn câu "mọi method mà Object có". Nó chỉ áp dụng cho những method public trong Object. clone() là protected, nên khai báo lại nó là một abstract method thứ hai thật sự:
@FunctionalInterface
interface Copyable {
void apply();
Object clone();
}
E5.java:1: error: Unexpected @FunctionalInterface annotation
@FunctionalInterface
^
Copyable is not a functional interface
multiple non-overriding abstract methods found in interface Copyable
1 error
| Thành viên | Có tiêu mất slot abstract duy nhất không? |
|---|---|
method default | không |
method static | không |
method private hoặc private static | không |
public abstract override equals, hashCode, toString | không |
public abstract override clone hoặc finalize | có — chúng là protected trong Object |
| một abstract method thứ hai không liên quan | có |
| abstract method thừa hưởng từ superinterface với cùng signature | không — vẫn là một method đó |
andThen và compose: cùng hai function, hai kết quả
Function mang hai default method để dán hai function thành một. Chúng chỉ khác nhau ở chỗ argument đi vào đầu nào, và dùng nhầm là một bug im lặng chứ không phải lỗi compile, vì cả hai đều trả về Function đúng type.

import java.util.function.*;
public class Compose {
public static void main(String[] args) {
Function<Integer, Integer> plus1 = n -> n + 1;
Function<Integer, Integer> times10 = n -> n * 10;
System.out.println("plus1.andThen(times10).apply(3) = " + plus1.andThen(times10).apply(3));
System.out.println("plus1.compose(times10).apply(3) = " + plus1.compose(times10).apply(3));
System.out.println("mirror image: "
+ (plus1.andThen(times10).apply(3) == times10.compose(plus1).apply(3)));
Function<String, Integer> len = String::length;
Function<Integer, String> stars = n -> "*".repeat(n);
System.out.println(len.andThen(stars).apply("hello"));
System.out.println(Function.<String>identity().andThen(String::toUpperCase).apply("id"));
}
}
plus1.andThen(times10).apply(3) = 40
plus1.compose(times10).apply(3) = 31
mirror image: true
*****
ID
Đọc f.andThen(g) là "f, rồi tới g" — từ trái sang phải, đúng thứ tự viết ra. Đọc f.compose(g) theo lối toán học viết composition — argument đi vào g trước rồi kết quả được đưa cho f, nên code đọc từ trong ra ngoài. f.andThen(g) và g.compose(f) dựng ra cùng một pipeline, đó là lý do dòng thứ ba in ra true.
f.andThen(g) | f.compose(g) | |
|---|---|---|
| Chạy trước | f | g |
| Thứ tự đọc | trái sang phải | phải sang trái |
| Tương đương với | g.compose(f) | g.andThen(f) |
Signature trên Function<T,R> | nhận Function<? super R, ? extends V> | nhận Function<? super V, ? extends T> |
| Có trên | Function, UnaryOperator, Consumer (chỉ andThen) | Function, UnaryOperator |
andThen là chỗ type được phép đổi: len.andThen(stars) biến một Function<String,Integer> và một Function<Integer,String> thành Function<String,String>. Function.identity() trả về function đưa lại chính argument của nó, là phần tử trung hòa của cả hai phép trên và chủ yếu hữu ích khi làm chỗ trống trong một chuỗi dựng theo điều kiện.
Có một điểm bất đối xứng đáng nhớ: UnaryOperator<T> thừa hưởng andThen và compose từ Function<T,T>, và những method đó trả về Function chứ không phải UnaryOperator. Ghép hai UnaryOperator lại sẽ nhận về Function<T,T>, nên một biến khai báo UnaryOperator<String> sẽ không nhận kết quả đó nếu không bọc thêm một lambda.
Các combinator của Predicate và Consumer
Predicate là shape giàu nhất, với ba default method và hai static factory:
import java.util.function.*;
public class Preds {
public static void main(String[] args) {
Predicate<String> notEmpty = s -> !s.isEmpty();
Predicate<String> under6 = s -> s.length() < 6;
System.out.println(notEmpty.and(under6).test("java"));
System.out.println(notEmpty.and(under6).test("javascript"));
System.out.println(notEmpty.or(under6).test(""));
System.out.println(notEmpty.negate().test(""));
System.out.println(Predicate.isEqual("java").test("java"));
System.out.println(Predicate.not(under6).test("javascript"));
// isEqual delegates to equals() and tolerates a null target
Predicate<Object> isNull = Predicate.isEqual(null);
System.out.println(isNull.test(null) + " " + isNull.test("x"));
// and/or short-circuit, so the second predicate is never called here
Predicate<String> boom = s -> { throw new IllegalStateException("never runs"); };
System.out.println(under6.negate().and(boom).test("java"));
}
}
true
false
true
true
true
true
true false
false
Hai dòng cuối mới là chỗ đáng để ý. Predicate.isEqual(null) tạo ra một predicate chỉ đúng với null, vì factory kiểm tra target null rồi trả về Objects::isNull thay vì gọi equals trên hư không. Còn and short-circuit y hệt toán tử logic AND: under6.negate() là false với "java", nên boom không bao giờ được gọi và IllegalStateException không bao giờ xảy ra.
| Thành viên | Từ | Cho bạn cái gì |
|---|---|---|
p.and(q) | Java 8 | AND có short-circuit, bỏ qua q khi p là false |
p.or(q) | Java 8 | OR có short-circuit, bỏ qua q khi p là true |
p.negate() | Java 8 | phủ định logic của p |
Predicate.isEqual(target) | Java 8 | target.equals(x), và x == null khi target là null |
Predicate.not(p) | Java 11 | giống p.negate(), nhưng dùng được với method reference |
Predicate.not tồn tại vì negate() cần một receiver đã có type Predicate, mà một method reference trần thì chưa có type cho tới khi được gán. not(String::isBlank) compile được; String::isBlank.negate() thì không. Compile đúng file đó với --release 10 báo cannot find symbol: method not(Predicate<String>), tức là nó thuộc Java 11.
Consumer chỉ có một combinator, và nó hành xử khác Function.andThen theo một cách đáng quan tâm:
import java.util.*;
import java.util.function.*;
public class Cons {
public static void main(String[] args) {
List<String> log = new ArrayList<>();
Consumer<String> record = log::add;
Consumer<String> shout = s -> log.add(s.toUpperCase());
record.andThen(shout).accept("ping");
System.out.println(log);
// andThen returns void, so a throw in the first consumer skips the second
Consumer<String> bad = s -> { throw new IllegalStateException("first"); };
try {
bad.andThen(record).accept("pong");
} catch (IllegalStateException e) {
System.out.println("threw: " + e.getMessage() + ", log still " + log);
}
}
}
[ping, PING]
threw: first, log still [ping, PING]
Consumer.andThen không nối kết quả — làm gì có kết quả — nó chạy cả hai consumer trên cùng một input, theo thứ tự. Nếu cái thứ nhất ném exception thì cái thứ hai không chạy, đúng hành vi của hai câu lệnh viết liền nhau, và nên biết trước điều đó trước khi dựng một audit trail bằng nó.
Primitive specialisation và phần boxing chúng loại bỏ
34 interface còn lại trong package tồn tại vì một lý do: generic không nhận int, nên Function<Integer, Integer> phải box lúc vào và unbox lúc ra. Các specialisation thay signature Object đã bị erase bằng một signature primitive.

Quy tắc đặt tên có ba mảnh, và nắm được chúng hơn hẳn việc học thuộc 34 cái tên:
| Mảnh | Ý nghĩa | Ví dụ | Signature |
|---|---|---|---|
prefix Int / Long / Double | argument mang primitive đó | IntPredicate | int -> boolean |
prefix ToInt / ToLong / ToDouble | giá trị trả về mang primitive đó | ToIntFunction<T> | T -> int |
| cả hai, theo thứ tự | primitive vào, primitive ra | IntToLongFunction | int -> long |
prefix Obj | có một object argument đứng trước | ObjIntConsumer<T> | (T, int) -> void |
UnaryOperator không cần prefix thứ hai | cùng primitive vào và ra | IntUnaryOperator | int -> int |
Từ quy tắc này rút ra hai hệ quả. Chỉ int, long và double có specialisation — không có ByteFunction, không có CharPredicate, không có FloatUnaryOperator, và code làm việc với những type đó hoặc phải box hoặc phải widen. Và giá trị trả về boolean không cần prefix ToBoolean nào cả, vì Predicate đã trả về boolean rồi; specialisation boolean duy nhất là BooleanSupplier, cho trường hợp không có argument mà Predicate không diễn tả được.
Tên method cũng đổi theo type trả về: IntFunction.apply trả về một object, còn ToIntFunction.applyAsInt, IntSupplier.getAsInt và BooleanSupplier.getAsBoolean đều tự đổi tên để giá trị primitive trả về nhìn thấy được ngay tại chỗ gọi.
Cái giá của việc không specialise nhìn thấy được trong bytecode. Hai method dưới đây làm cùng một việc:
import java.util.function.*;
public class Bytes {
static int boxedCall(Function<Integer, Integer> f, int n) { return f.apply(n); }
static int primCall(IntUnaryOperator f, int n) { return f.applyAsInt(n); }
}
static int boxedCall(java.util.function.Function<java.lang.Integer, java.lang.Integer>, int);
Code:
0: aload_0
1: iload_1
2: invokestatic #7 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
5: invokeinterface #13, 2 // InterfaceMethod java/util/function/Function.apply:(Ljava/lang/Object;)Ljava/lang/Object;
10: checkcast #8 // class java/lang/Integer
13: invokevirtual #19 // Method java/lang/Integer.intValue:()I
16: ireturn
static int primCall(java.util.function.IntUnaryOperator, int);
Code:
0: aload_0
1: iload_1
2: invokeinterface #23, 2 // InterfaceMethod java/util/function/IntUnaryOperator.applyAsInt:(I)I
7: ireturn
Bốn instruction thừa: một valueOf để box, một checkcast vì erasure để lại method của interface trả về Object, và một intValue để unbox. valueOf mới là cái allocate.
Phần allocate đó đếm được chứ không cần đo thời gian. com.sun.management.ThreadMXBean.getThreadAllocatedBytes báo một thread đã allocate bao nhiêu byte, nên đọc nó ở hai đầu vòng lặp là ra con số chính xác:
import com.sun.management.ThreadMXBean;
import java.lang.management.ManagementFactory;
import java.util.function.*;
public class Boxing {
static final ThreadMXBean BEAN = (ThreadMXBean) ManagementFactory.getThreadMXBean();
static final int N = 1_000_000;
static long sink;
static long allocated(Runnable body) {
long id = Thread.currentThread().threadId();
long before = BEAN.getThreadAllocatedBytes(id);
body.run();
return BEAN.getThreadAllocatedBytes(id) - before;
}
public static void main(String[] args) {
Function<Integer, Integer> boxed = n -> n + 1;
IntUnaryOperator prim = n -> n + 1;
long a = allocated(() -> { for (int i = 0; i < N; i++) sink += boxed.apply(1000 + i); });
long b = allocated(() -> { for (int i = 0; i < N; i++) sink += prim.applyAsInt(1000 + i); });
System.out.println("Function<Integer,Integer> : " + a + " bytes");
System.out.println("IntUnaryOperator : " + b + " bytes");
System.out.println("per boxed call : " + a / N + " bytes");
}
}
Chạy nó với java -Xint để tắt JIT, như vậy con số không phụ thuộc vào việc optimiser hôm nay muốn làm gì:
Function<Integer,Integer> : 32000168 bytes
IntUnaryOperator : 0 bytes
per boxed call : 32 bytes
Ba mươi hai byte mỗi lần gọi, chạy lại lần nào cũng ra đúng con số đó: hai object Integer mỗi cái mười sáu byte — một cho argument đã box, một cho kết quả đã box. Bản primitive không allocate gì cả, vì chẳng có gì để allocate.
Muốn chắc 32 byte đó đúng là boxing thì kiểm tra rất dễ, vì Integer.valueOf trả về instance có sẵn trong cache cho giá trị trong khoảng -128..127 và allocate khi ra ngoài khoảng đó:
import com.sun.management.ThreadMXBean;
import java.lang.management.ManagementFactory;
import java.util.function.*;
public class Cache {
static final ThreadMXBean BEAN = (ThreadMXBean) ManagementFactory.getThreadMXBean();
static final int N = 1_000_000;
static long sink;
static long allocated(Runnable r) {
long id = Thread.currentThread().threadId();
long b = BEAN.getThreadAllocatedBytes(id);
r.run();
return BEAN.getThreadAllocatedBytes(id) - b;
}
public static void main(String[] args) {
Function<Integer, Integer> f = n -> n;
System.out.println("values 0..126, inside the Integer cache : "
+ allocated(() -> { for (int i = 0; i < N; i++) sink += f.apply(i % 127); }) + " bytes");
System.out.println("values 1000..1126, outside it : "
+ allocated(() -> { for (int i = 0; i < N; i++) sink += f.apply(1000 + i % 127); }) + " bytes");
}
}
values 0..126, inside the Integer cache : 168 bytes
values 1000..1126, outside it : 16000000 bytes
Cùng một vòng lặp, cùng một function, cùng số lần gọi. Khác biệt duy nhất là giá trị có nằm trong cache hay không, và khác biệt đó đúng bằng mười sáu byte mỗi lần gọi.
⚠️ Không có gì ở đây là lý do để mặc định bỏ qua
Function. Khi bật JIT, escape analysis xóa được một phần lớn số allocate này, và một lambda boxed gọi vài nghìn lần không phải vấn đề đáng đặt tên. Các specialisation xứng đáng chỗ đứng trong vòng lặp số học nóng và trong stream pipeline chạy qua hàng triệu element — đúng những chỗ JDK dùng chúng.
Tự viết functional interface khi shape của JDK không vừa
Có ba tình huống 43 shape kia không phủ hết, và cả ba đều là lý do chính đáng để khai báo một interface của riêng mình.
Một cái tên có nghĩa. Function<Integer, Long> không nói gì về con số đó là gì. Một interface một method với tên thật ghi rõ ý định và cho parameter một cái tên hiện ra trong IDE:
@FunctionalInterface
interface PricingRule {
long priceInCents(int quantity);
}
Nhiều hơn hai parameter. JDK dừng ở hai, nên shape ba argument phải tự khai báo. Thêm andThen của riêng mình chỉ tốn vài dòng và làm nó ghép được:
@FunctionalInterface
interface TriFunction<A, B, C, R> {
R apply(A a, B b, C c);
default <V> TriFunction<A, B, C, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (a, b, c) -> after.apply(apply(a, b, c));
}
}
Một checked exception. Đây là trường hợp hay gặp nhất, và là chỗ cần mô tả cách chữa một cách thành thật. Function.apply không khai báo checked exception nào, nên một method reference ném checked exception thì không vừa:
A2.java:7: error: incompatible thrown types IOException in functional expression
Function<Path, String> read = Files::readString;
^
1 error
Cách chữa là một functional interface có mệnh đề throws. Nó chạy hoàn hảo chừng nào code của chính bạn là bên gọi:
@FunctionalInterface
interface ThrowingFunction<T, R, E extends Exception> {
R apply(T t) throws E;
}
Rắc rối bắt đầu khi một method của JDK đòi một Function thật, và lối thoát duy nhất là một adapter biến checked exception thành unchecked:
import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.function.*;
public class Own {
static <T, R, E extends Exception> Function<T, R> unchecked(ThrowingFunction<T, R, E> f) {
return t -> {
try {
return f.apply(t);
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw e instanceof IOException io
? new UncheckedIOException(io)
: new IllegalStateException(e);
}
};
}
public static void main(String[] args) throws IOException {
Path p = Files.createTempFile("fi", ".txt");
Files.writeString(p, "line one\nline two\n");
ThrowingFunction<Path, String, IOException> read = Files::readString;
System.out.println(read.apply(p).lines().count() + " lines read");
Function<Path, String> adapted = unchecked(Files::readString);
System.out.println(adapted.apply(p).strip().replace("\n", " | "));
try {
adapted.apply(Path.of("/no/such/file.txt"));
} catch (UncheckedIOException e) {
System.out.println("caught " + e.getClass().getSimpleName()
+ ", cause " + e.getCause().getClass().getName());
}
PricingRule flat = qty -> 250L * qty;
System.out.println("3 units = " + flat.priceInCents(3) + " cents");
TriFunction<String, String, String, String> path =
(a, b, c) -> a + "/" + b + "/" + c;
System.out.println(path.andThen(String::toUpperCase).apply("api", "v2", "orders"));
Files.deleteIfExists(p);
}
}
2 lines read
line one | line two
caught UncheckedIOException, cause java.nio.file.NoSuchFileException
3 units = 750 cents
API/V2/ORDERS
Hãy gọi đúng tên nó: một workaround, kèm ba cái giá mà bạn đang chấp nhận.
Thứ nhất là checked type biến mất khỏi signature. adapted chỉ là một Function<Path,String> bình thường và không có gì báo cho bên gọi biết nó có thể hỏng; cái compiler đáng lẽ bắt họ xử lý IOException bây giờ không nói gì nữa.
Thứ hai là exception về tới nơi trong trạng thái bị bọc, nên chỗ gọi phải catch cái wrapper rồi mở ra, và stack trace có thêm một frame chẳng mang ý nghĩa gì.
Thứ ba lộ ra ngay lúc viết adapter. Để type parameter thành wildcard thay vì một E có tên thì wrapper nhìn gọn hơn nhưng không compile được:
Wrap.java:13: error: exception IOException is never thrown in body of corresponding try statement
} catch (IOException e) {
^
Wrap.java:12: error: unreported exception CAP#1; must be caught or declared to be thrown
return f.apply(t);
^
where CAP#1 is a fresh type-variable:
CAP#1 extends Exception from capture of ? extends Exception
2 errors
Type sau capture không phải IOException, nên với compiler thì catch (IOException e) là code chết. Adapter buộc phải catch Exception rồi phân loại lúc runtime, tức là đánh đổi thêm một mảnh an toàn tĩnh nữa. Nếu thao tác đó thật sự có thể hỏng, một vòng lặp bình thường với try bao quanh thường là câu trả lời tốt hơn.
Những cái bẫy sống sót qua code review
Hai overload cùng shape là ambiguous. Lambda không có type của riêng nó, nên compiler không thể dựa vào nó để chọn giữa hai overload mà parameter là hai functional interface giống hệt nhau về cấu trúc:
import java.util.function.*;
public class A1 {
static void run(Supplier<String> s) { System.out.println("supplier " + s.get()); }
static void run(Callable s) { System.out.println("callable"); }
interface Callable { String call(); }
public static void main(String[] args) {
run(() -> "hello");
}
}
A1.java:10: error: reference to run is ambiguous
run(() -> "hello");
^
both method run(Supplier<String>) in A1 and method run(Callable) in A1 match
1 error
Cast lambda — run((Supplier<String>) () -> "hello") — thì hết lỗi, nhưng cách chữa thật sự là đừng công bố hai overload cùng shape. Để ý overload khác shape thì không sao: một overload Runnable và một overload Supplier<String> sống chung thoải mái, vì lambda có body void chỉ vừa cái thứ nhất còn lambda trả về giá trị chỉ vừa cái thứ hai.
Một Consumer âm thầm vứt giá trị trả về. Body lambda chỉ gồm một lời gọi method là một statement expression, tức là nó tương thích void, tức là nó compile được thành Consumer và ném kết quả đi:
import java.util.*;
import java.util.function.*;
public class Traps {
public static void main(String[] args) {
Consumer<String> shout = s -> s.toUpperCase();
shout.accept("ada");
System.out.println("nothing happened");
Set<String> seen = new LinkedHashSet<>();
Consumer<String> record = s -> seen.add(s);
record.accept("x");
record.accept("x");
System.out.println(seen + " (the second add returned false and nobody saw it)");
List<String> queue = new ArrayList<>(List.of("a"));
Supplier<String> size = () -> "size=" + queue.size();
queue.add("b");
queue.add("c");
System.out.println(size.get());
int[] calls = { 0 };
Supplier<Integer> counter = () -> ++calls[0];
System.out.println(counter.get() + " " + counter.get() + " " + counter.get());
System.out.println("the body ran " + calls[0] + " times");
Map<String, String> cache = new HashMap<>(Map.of("k", "cached"));
System.out.println(cache.getOrDefault("k", expensive("eager")));
System.out.println(cache.computeIfAbsent("k", key -> expensive("lazy")));
}
static String expensive(String tag) {
System.out.println(" [expensive(" + tag + ") ran]");
return "computed";
}
}
nothing happened
[x] (the second add returned false and nobody saw it)
size=3
1 2 3
the body ran 3 times
[expensive(eager) ran]
cached
cached
s -> s.toUpperCase() compile được, chạy được, và không làm gì cả, vì String là immutable và chuỗi mới đi vào hư không. s -> seen.add(s) vứt mất boolean đáng lẽ báo cho bạn biết element đã có sẵn. Compiler chỉ phản đối khi body không phải statement expression:
A3.java:5: error: incompatible types: lambda body is not compatible with a void functional interface
Consumer<String> c = s -> s.length() + 1;
^
(consider using a block lambda body, or use a statement expression instead)
Supplier là một công thức, không phải một giá trị. Nó capture một reference rồi đọc xuyên qua reference đó lúc get() được gọi, nên size.get() in ra size=3 chứ không phải size=1 — list đã lớn lên trong khoảng giữa lúc lambda được tạo và lúc nó được gọi. Và body chạy lại ở mọi lần gọi chứ không phải một lần: counter.get() in ra 1 2 3 và bộ đếm lên tới 3. Supplier không memoise; muốn vậy thì tính giá trị một lần rồi giữ lại.
Chính sự lười đó là toàn bộ ý nghĩa của shape này, và hai dòng cuối cho thấy vì sao. getOrDefault("k", expensive("eager")) tính argument thứ hai trước cả khi lời gọi diễn ra, nên expensive vẫn chạy dù key đã có sẵn và giá trị bị vứt đi. computeIfAbsent("k", key -> ...) thì không gọi lambda lần nào. Cũng chính sự phân biệt đó là lý do Optional.orElseGet nhận một Supplier còn Optional.orElse nhận một giá trị bình thường.
FAQ
Functional interface trong Java là gì?
Là interface có đúng một abstract method. Method duy nhất đó là thứ mà lambda hoặc method reference cung cấp body, nên functional interface là loại type duy nhất mà một lambda có thể convert sang. Method default, static và private không tính vào giới hạn, và những khai báo lại public abstract của equals, hashCode hay toString cũng không, vì mọi class implement đều đã thừa hưởng chúng từ java.lang.Object.
Có bắt buộc phải có @FunctionalInterface để dùng lambda không?
Không. Annotation là một khẳng định lúc compile chứ không phải điều kiện bắt buộc: interface Rule ở trên không có annotation, mà Rule even = n -> n % 2 == 0; vẫn compile và chạy được. Cái annotation thêm vào là một build hỏng ngay tại khai báo khi abstract method thứ hai xuất hiện, thay vì build hỏng ở từng lambda đã dùng nó. JDK áp dụng nó có chọn lọc — Runnable, Callable và Comparator thì có, còn Iterable, AutoCloseable và Executor là functional interface mà không có, vì chúng không sinh ra để viết thành lambda.
Một functional interface có thể có nhiều hơn một abstract method không?
Nó khai báo được nhiều hơn một, miễn là sau khi trừ các trường hợp miễn trừ chỉ còn lại đúng một. java.util.Comparator khai báo compare và equals, và vẫn là functional interface trên OpenJDK 21.0.6, vì equals(Object) override một method public của Object. Miễn trừ này chỉ áp dụng cho method public của Object: khai báo Object clone() — vốn là protected trong Object — là một abstract method thứ hai thật sự và cho ra multiple non-overriding abstract methods found in interface Copyable.
andThen và compose khác nhau ở đâu?
Ở thứ tự hai function chạy. f.andThen(g) chạy f trước rồi đưa kết quả cho g; f.compose(g) chạy g trước rồi đưa kết quả cho f. Với plus1 và times10 cùng input là 3, plus1.andThen(times10) cho 40 còn plus1.compose(times10) cho 31. Chúng là ảnh gương của nhau: f.andThen(g) và g.compose(f) dựng ra cùng một pipeline. Consumer chỉ có andThen, và nó chạy cả hai consumer trên cùng một input chứ không nối kết quả.
Khi nào nên dùng IntFunction thay cho Function<Integer, R>?
Khi số lần gọi đủ nhiều để phần allocate trở nên đáng kể. Đo trên OpenJDK 21.0.6 với java -Xint, một triệu lần gọi qua Function<Integer,Integer> allocate 32.000.168 byte — hai object Integer 16 byte mỗi lần gọi, một cho argument và một cho kết quả — trong khi IntUnaryOperator allocate bằng không. Trong code ứng dụng bình thường khác biệt đó không nhìn thấy được, và escape analysis của JIT cũng đã xóa phần lớn. Trong một vòng lặp số học nóng, hoặc một stream chạy qua hàng triệu element, đó đúng là khác biệt mà các specialisation sinh ra để mua về.
Tại sao Java không có TriFunction?
JDK dừng ở hai argument vì tổ hợp sẽ vượt tầm kiểm soát: ba mức arity nhân với các reference shape và các specialisation int, long, double sẽ đẩy package vượt xa con số 43 hiện tại, cho một shape hiếm gặp trong thực tế. Tự viết chỉ tốn bốn dòng, và một function ba argument thường là dấu hiệu rằng hai trong ba argument đó nên nằm chung trong một record.
Làm sao viết lambda ném checked exception?
Với functional interface của JDK thì không được — Files::readString dùng như một Function cho ra incompatible thrown types IOException in functional expression. Hãy khai báo interface của riêng bạn với mệnh đề throws và dùng nó ở nơi bạn kiểm soát được API gọi. Ở nơi không kiểm soát được, một adapter catch rồi bọc lại là cách chữa thông thường, và cái giá là mất checked type trong signature, thêm một lớp wrapper quanh exception, và mất luôn khả năng viết catch (IOException e) ngay trong adapter khi type của exception là một type variable.
Kết luận
Bốn mươi ba interface là rất nhiều để nhớ và gần như không có gì để hiểu. Chúng là chín shape, định nghĩa bởi bao nhiêu argument đi vào và cái gì đi ra, cộng thêm 34 primitive specialisation máy móc mà tên gọi suy ra được từ ba prefix. Một abstract method là toàn bộ hợp đồng; @FunctionalInterface chỉ kiểm tra nó, còn default, static, private và những method override Object đều miễn phí — đó là cách Comparator vừa khai báo được equals vừa vẫn làm target cho lambda.
Phần còn lại là composition và chi phí. andThen đọc xuôi còn compose đọc ngược, và compiler sẽ không bắt được lỗi nhầm vì cả hai đều hợp lệ về type. Predicate.and và or có short-circuit, Consumer.andThen tỏa ra chứ không nối lại, và Supplier là một công thức chạy lại ở mọi lần get() rồi đọc reference nó capture ngay tại thời điểm đó chứ không phải lúc được viết ra. Chỗ nào shape của JDK không vừa — một checked exception, ba parameter, một cái tên mang ý nghĩa — thì tự khai báo rẻ hơn và thường sáng hơn là bẻ cong một Function cho vừa.
Bài tiếp theo trong series: Optional — nó thật sự dùng để làm gì, vì sao get() là method gần như không bao giờ nên gọi, map, flatMap và filter, khác biệt giữa orElse và orElseGet mà cái bẫy cuối cùng của bài này đã hé ra, và những chỗ Optional không thuộc về.