الواجهات الوظيفية القياسية
في هذا الدرس 11 موضوعات
هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.
Lesson Overview
In the first two lessons we wrote our own functional interfaces (PersonFilter, Op) to receive behavior. That worked, but you would soon be inventing the same interface again and again: "takes something, answers yes or no", "takes something, returns something else". The Java library already ships the common ones, so you rarely need to write your own.
In the recipe-card picture, these are the standard card formats. Some cards ask a yes/no question about an ingredient, some turn one ingredient into another, some just do something with it, some produce an ingredient from nothing. Once you know the four or five formats, you can read the signature of almost any method that takes behavior.
Why this matters: the methods in the collections framework and in streams (BEG-036) are declared in terms of these interfaces. If you know them, a signature such as filter(Predicate<T>) tells you what to pass without reading any documentation.
This lesson builds on "Lambda syntax": every lambda here follows the shapes you just learned, and the types come from the target type.
Learning Objectives
By the end of this lesson you will be able to:
- Name the five everyday interfaces in
java.util.functionand the method of each. - Pick the right interface from a description of inputs and output.
- Write a lambda for each of them.
- Write a method that accepts a
Predicate, aFunction, aConsumer, or aSupplier. - Combine predicates with
and,or, andnegate, and chain functions withandThen. - Explain why a custom functional interface is usually not needed.
Written Explanation
The five formats
All of these live in the package java.util.function. You need import java.util.function.*; (or one import per interface) to use them.
| Interface | In | Out | Method | Think of it as |
|---|---|---|---|---|
Predicate<T> | one T | boolean | test | a yes/no question |
Function<T, R> | one T | an R | apply | a conversion |
Consumer<T> | one T | nothing (void) | accept | an action on a value |
Supplier<T> | nothing | a T | get | a source of values |
BiFunction<T, U, R> | a T and a U | an R | apply | a conversion of two inputs |
The Java documentation describes them in the same terms: a Predicate is a boolean-valued function of one argument, a Function accepts one argument and produces a result, a Consumer accepts one input and returns no result, a Supplier is a supplier of results, and a BiFunction accepts two arguments and produces a result. The package documentation calls the single method of such an interface its functional method.
A trick for remembering the names: Predicate says something is true or false. Function maps an input to an output. A Consumer consumes a value and gives nothing back. A Supplier supplies a value without needing one.
Note that T, R, and U are the type parameters you met in BEG-031: Function<String, Integer> takes a String and returns an Integer. Type arguments must be classes, so you write Integer, not int; the library also has primitive-specialized versions such as IntPredicate and ToIntFunction, which this lesson leaves for later.
One lambda for each
Predicate<String> empty = s -> s.isEmpty();
Function<String, Integer> len = s -> s.length();
Consumer<String> print = s -> System.out.println(s);
Supplier<String> ready = () -> "ready";
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;Using them calls the functional method by name:
empty.test(""); // true
len.apply("lambda"); // 6
print.accept("printed");
ready.get(); // "ready"
add.apply(2, 3); // 5The lambda text never mentions test or apply; the interface supplies that name, and the lambda supplies the body.
Methods that accept them
The real power appears when your method takes one of these. Using the generic methods you learned in BEG-031, we can write the building blocks once and reuse them for any type:
static <T> List<T> filter(List<T> in, Predicate<T> p) {
List<T> out = new ArrayList<>();
for (T t : in) {
if (p.test(t)) {
out.add(t);
}
}
return out;
}
static <T, R> List<R> transform(List<T> in, Function<T, R> f) {
List<R> out = new ArrayList<>();
for (T t : in) {
out.add(f.apply(t));
}
return out;
}
static <T> void each(List<T> in, Consumer<T> c) {
for (T t : in) {
c.accept(t);
}
}
static String orDefault(String value, Supplier<String> fallback) {
return (value == null || value.isEmpty()) ? fallback.get() : value;
}Look at the last one. Why a Supplier<String> rather than a plain String fallback? Because the supplier is only called when needed. If computing the fallback were expensive (a database read, say), passing a plain value would compute it every time; passing a Supplier computes it only when the value is missing. That ability to delay work is one of the quiet advantages of passing behavior.
You will not write many methods like these in real projects. The JDK already has removeIf, forEach, Comparator.comparing, and, in the next module, filter, map, and forEach on streams, which have exactly the shapes of filter, transform, and each above. Building them once yourself is the best way to understand them.
Combining small pieces
Predicates and functions are objects, so they have methods. The documentation lists, among others, and, or, and negate on Predicate, and andThen on Function:
Predicate<String> longName = s -> s.length() > 4;
Predicate<String> startsWithL = s -> s.startsWith("L");
longName.and(startsWithL); // both must be true
longName.negate(); // the opposite
Function<String, Integer> lenThenDouble = len.andThen(n -> n * 2);This is optional vocabulary today. The point: small, named behaviors compose into larger ones, instead of one big lambda with many &&.
Should I write my own interface?
Usually no. If the shape is "one input, boolean out", use Predicate; "nothing in, value out", use Supplier. Write your own functional interface when the standard names would mislead readers (a Discount that reads better than Function<Double, Double>), when the shape is unusual (three inputs), or when the interface is part of your public API and deserves a domain name. Marking your own interface with @FunctionalInterface is optional, but it makes the compiler enforce the one-abstract-method rule and documents your intent.
Examples
Example 1: the five basics (simple)
Predicate<String> empty = s -> s.isEmpty();
Function<String, Integer> len = s -> s.length();
Consumer<String> print = s -> System.out.println(s);
Supplier<String> ready = () -> "ready";
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;
System.out.println(empty.test("") + " " + empty.test("x"));
System.out.println(len.apply("lambda"));
print.accept("printed");
System.out.println(ready.get());
System.out.println(add.apply(2, 3));Output:
true false
6
printed
ready
5Example 2: filter, transform, each (practical)
List<String> names = List.of("Layla", "", "Omar", "Sara", "");
List<String> nonEmpty = filter(names, s -> !s.isEmpty());
System.out.println(nonEmpty);
System.out.println(transform(nonEmpty, s -> s.length()));
each(nonEmpty, s -> System.out.println("Hi " + s));Output:
[Layla, Omar, Sara]
[5, 4, 4]
Hi Layla
Hi Omar
Hi SaraA Predicate selected the names, a Function turned each name into its length, a Consumer performed an action on each. Each helper contains a loop; each call contains one small idea.
Example 3: a lazy fallback and combined predicates (realistic)
System.out.println(orDefault("", () -> "anonymous"));
System.out.println(orDefault("Omar", () -> "anonymous"));
Predicate<String> longName = s -> s.length() > 4;
Predicate<String> startsWithL = s -> s.startsWith("L");
System.out.println(filter(nonEmpty, longName.and(startsWithL)));
System.out.println(filter(nonEmpty, longName.negate()));Output:
anonymous
Omar
[Layla]
[Omar, Sara]The supplier ran only for the empty string. The combined predicate kept only Layla, and negate selected the short names.
Backend Insight
The standard interfaces are the vocabulary of modern Java backends:
- A Predicate is "does this order qualify?", "is this user active?": validation and filtering rules.
- A Function is "turn this entity into a DTO", "extract the id": mapping between layers.
- A Consumer is "log this", "publish this event": an action performed for its effect.
- A Supplier is "create a new ID", "load the configuration when first needed": a lazy source.
- A BiFunction is "combine two partial results": merging, as you will see in grouping and reduction.
When you read a Spring or a stream-based codebase, translate each lambda by naming its interface: "this is a predicate on orders", "this is a mapper from entity to DTO". Code is far easier to follow when you can name the role of every card.
Common Mistakes
Mistake 1: Calling the wrong method name
- Incorrect:
empty.apply("")for aPredicate. - Correct:
empty.test("").
The method name belongs to the interface: test, apply, accept, get. Use the table above until it sticks.
Mistake 2: Using a primitive as a type argument
- Incorrect:
Function<String, int> len = ... - Correct:
Function<String, Integer> len = ...
Generics take class types; int is boxed to Integer.
Mistake 3: Expecting a Consumer to return something
A Consumer returns void. If you need a result, you want a Function. If you need a yes/no, you want a Predicate.
Mistake 4: Inventing a new interface for a standard shape
If your interface is boolean check(T t), it is a Predicate<T> under another name, and other libraries cannot accept it. Reuse the standard one, unless a domain name truly helps.
Mistake 5: Putting heavy logic in a Predicate lambda
A predicate with ten conditions in one lambda is hard to read. Split it into named predicates and combine them with and or or, or move the logic into a method.
Exercises
Try each exercise yourself before opening the solutions. Assume import java.util.*; and import java.util.function.*;, and the filter and transform helpers above.
Level A: Foundation
A1. Pick the interface: (a) decide whether a user is an admin; (b) create a new random ID with no input; (c) turn an order into its total; (d) send a value to a log.
أظهر الحل A1
A1. (a) Predicate<User>. (b) Supplier<String> (or another type). (c) Function<Order, Double> (or another number type). (d) Consumer<String>.
A2. Write a Predicate<String> that is true for strings longer than 3 characters.
أظهر الحل A2
A2. Predicate<String> over3 = s -> s.length() > 3;
A3. What is the name of the method you call on a Supplier? On a Consumer?
أظهر الحل A3
A3. get on a Supplier, accept on a Consumer.
Level B: Application
B1. Write a Function<Double, Double> that returns half of a price. What is the result for 100.0?
أظهر الحل B1
B1. Function<Double, Double> half = p -> p / 2; For 100.0, half.apply(100.0) is 50.0.
B2. Write a Consumer<String> that prints the text with the prefix LOG: .
أظهر الحل B2
B2. Consumer<String> log = s -> System.out.println("LOG: " + s); Calling log.accept("started") prints LOG: started.
B3. From List.of("Sara", "Sam", "Salim", "Omar"), use filter to keep names longer than 3 characters that start with "S", combining two predicates with and.
أظهر الحل B3
B3.
Predicate<String> over3 = s -> s.length() > 3;
filter(names, over3.and(s -> s.startsWith("S")))The result is [Sara, Salim]: Sam is too short and Omar does not start with S.
Level C: Challenge
C1. Write a generic method static <T> int count(List<T> in, Predicate<T> p) that returns how many items satisfy p. Use it on the list from B3 to count names longer than 3 characters.
أظهر الحل C1
C1.
static <T> int count(List<T> in, Predicate<T> p) {
int n = 0;
for (T t : in) {
if (p.test(t)) {
n++;
}
}
return n;
}
// count(names, over3) -> 3 (Sara, Salim, Omar)C2. A method retry(Supplier<String> action, int times) is meant to call action.get() up to times times. Why is a Supplier a good fit, and what would be wrong with a Runnable?
أظهر الحل C2
C2. The method needs a result from each attempt, and Supplier has get() returning a value, so the method can return or inspect it. A Runnable returns nothing, so the caller would have no way to get the result back.
Mini Challenge
Write a small program with a list of at least six words. Without writing any new interface, use only the standard ones to:
- Keep words that are not blank (
Predicate). - Convert each remaining word to upper case (
Function). - Print each (
Consumer). - Use a
Supplier<String>to provide a fallback word if the list ends up empty. - Add a
BiFunction<String, Integer, String>that repeats a word a given number of times, and printrepeat.apply("ab", 3).
Check your work: every lambda has an interface name you can say aloud (predicate, function, consumer, supplier), and you called each one with the correct method.
Summary & Next Lesson
Key takeaways
java.util.functionprovides ready-made functional interfaces, so you rarely write your own.Predicate<T>:test, returnsboolean.Function<T, R>:apply.Consumer<T>:accept, returns nothing.Supplier<T>:get, takes nothing.BiFunction<T, U, R>:applywith two inputs.- Type arguments are classes (
Integer, notint). - Methods that take these interfaces let one loop serve many behaviors; a
Supplieralso delays work until needed. - Predicates combine with
and,or,negate; functions chain withandThen.
Next lesson
The next lesson, "Method references", shows a shorter way to write lambdas that only call one existing method, such as String::length instead of s -> s.length().
Self-check
- Which interface takes a value and returns
boolean? - Which takes nothing and returns a value?
- What is the method name of
Function? - Why can't you write
Function<String, int>? - Why might you pass a
Supplierinstead of a plain value?
أظهر الإجابات
Predicate.Supplier.apply.- Generic type arguments must be classes; use
Integer. - So the value is computed only if it is actually needed.
References
- java.util.function package, Java SE 21 API. Supports: the one-line descriptions of
Predicate,Function,Consumer,Supplier, andBiFunction, the term "functional method", and primitive-specialized variants. - Lambda Expressions, The Java Tutorials (Oracle). Supports:
test,accept, andapplyas the methods ofPredicate,Consumer, andFunction, and the advice to use the standard interfaces. - The method names
get(Supplier),apply(BiFunction), and the default methodsand,negate, andandThenwere not stated on the pages fetched; they were verified by compiling and running the examples with JDK 21 (outputs shown are real). The lazySupplierexplanation is the course's own teaching point.
التطبيق
طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.
تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.