انتقل إلى المحتوى
أمين أبوزيد
منشورBEG-035-L01

تمرير السلوك كقيمة

  • الدرس 1 من 5
  • 11 دقيقة قراءة
  • مبتدئ
في هذا الدرس 11 موضوعات

هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.

Lesson Overview

Until now, every value you passed to a method was data: a number, a string, an object. Sometimes what you really want to hand over is an action: "sort these by length", "keep only the adults", "run this when you are done". This module is about doing exactly that, and it begins with the idea behind it all: behavior can be passed around like a value.

We will use one picture throughout the module. Imagine asking a helper to cook for you. You can cook the dish yourself, or you can hand the helper a recipe card and let them follow it. A method that takes behavior is a helper that accepts recipe cards. The method decides when and how often to follow the card; you decide what is on it.

Why this matters: Java's collections, the streams you will meet in BEG-036, CompletableFuture in the intermediate level, and a lot of Spring configuration all take behavior as an argument. If you cannot read a lambda, you cannot read modern Java.

This lesson builds on interfaces (BEG-024) and on the Comparator interface from BEG-032, where you already wrote a class that implements one method just to describe an order.

Learning Objectives

By the end of this lesson you will be able to:

  • Explain what it means to pass behavior (code) to a method as an argument.
  • Define a functional interface and recognize one.
  • Write an anonymous class that implements a one-method interface, and explain why it is noisy.
  • Write the same behavior as a lambda expression and say which interface it implements.
  • Write a method that accepts behavior through a functional interface parameter.
  • Explain why this removes duplicated loops.

Written Explanation

Data versus behavior

Look at a method you could easily write today: keep only the adults from a list of people.

java
static List<Person> adults(List<Person> people) {
    List<Person> result = new ArrayList<>();
    for (Person p : people) {
        if (p.getAge() >= 18) {
            result.add(p);
        }
    }
    return result;
}

Now you need "people whose name starts with A". You copy the method and change one line. Tomorrow it is "people from one city". Every method has the same loop; only the condition differs. The condition is behavior, a small piece of logic, and we want to hand it in from outside.

That is the recipe-card idea: the method keeps the stable part (loop through the list and collect), and the caller supplies the part that changes (the test).

Step 1: an interface that describes one action

In BEG-024 you learned that an interface is a contract. We can write a contract for "a test on a person":

java
interface PersonFilter {
    boolean matches(Person p);
}

An interface with exactly one abstract method is called a functional interface. The Java documentation for @FunctionalInterface explains the exact rule: default methods do not count (they have a body), and neither does an abstract method that merely re-declares a public method of Object, such as equals. You have already used two functional interfaces without the name: Runnable (one method, run) and Comparator<T> (one method, compare, plus default and static helper methods).

Now the method takes the contract as a parameter:

java
static List<Person> keep(List<Person> people, PersonFilter filter) {
    List<Person> result = new ArrayList<>();
    for (Person p : people) {
        if (filter.matches(p)) {
            result.add(p);
        }
    }
    return result;
}

One loop serves every condition. The only question left is how to write the condition at the call site.

Step 2: the old way, an anonymous class

Before Java 8 you had to create an object that implements the interface. If you need it only once, you can write the class without a name, right where you use it. This is an anonymous class:

java
keep(people, new PersonFilter() {
    @Override
    public boolean matches(Person p) {
        return p.getAge() >= 18;
    }
});

Count the lines: five lines, and only one carries information, p.getAge() >= 18. The rest is ceremony: new PersonFilter(), the method name, the modifier, the types, the @Override. All of it is forced, because the compiler already knows that keep wants a PersonFilter and that PersonFilter has one method.

Step 3: the new way, a lambda

A lambda expression is a short piece of behavior, written without the ceremony:

java
keep(people, p -> p.getAge() >= 18);

Read p -> p.getAge() >= 18 as "given a person p, answer p.getAge() >= 18". It is the recipe card with nothing else on it. The arrow -> separates the parameters from the body. The next lesson covers the exact syntax; today we focus on the idea.

How does the compiler know what this card means? It looks at where the card is going. The parameter of keep has type PersonFilter, whose only abstract method takes a Person and returns boolean, so the lambda must be an implementation of that method. The Oracle Java Tutorials describe this: a lambda has no type of its own, and the compiler takes its type from the context, the target type.

So a lambda is simply a compact way to implement a functional interface. Two rules follow:

  1. A lambda can only appear where a functional interface is expected. Object o = () -> 1; does not compile, because Object does not tell the compiler which method the lambda implements.
  2. The same lambda text can mean different things in different places, because the target type decides. (a, b) -> a.length() - b.length() is a Comparator<String> when passed to sort.

Anonymous class versus lambda: same job, less noise

Anonymous classLambda
Needs a functional interfaceOne-method interface or any classFunctional interface only
Method name and types writtenYesNo (inferred)
Lines for a simple testAbout 51
this insideThe anonymous object itselfThe surrounding object

The last row matters only occasionally; the Oracle tutorial notes that a lambda does not introduce a new scope, so this in a lambda still means the enclosing class. You do not need it yet. What you need is the habit: if a one-method interface wants an object just to hold one small piece of logic, write a lambda.

Behavior as a value

Because a lambda is an object of a functional interface type, you can treat it like any other value:

  • Store it in a variable: PersonFilter adult = p -> p.getAge() >= 18;
  • Pass it to a method: keep(people, adult);
  • Return it from a method.
  • Keep several in a list.

That is what "passing behavior as a value" means. Nothing magical happens to the method. You are still passing an object; the lambda simply makes creating that object cheap enough that you stop avoiding the idea.

Examples

Example 1: a Runnable twice (simple)

Runnable has one method, run(), with no parameters and no result.

java
Runnable old = new Runnable() {
    @Override
    public void run() {
        System.out.println("hello (anonymous class)");
    }
};
Runnable fresh = () -> System.out.println("hello (lambda)");
old.run();
fresh.run();

Output:

text
hello (anonymous class)
hello (lambda)

Both objects behave the same. () means "no parameters" and the body is one statement.

Example 2: sorting by length (practical)

In BEG-032 you wrote Comparator classes. An anonymous class is one step shorter, and a lambda is shorter again:

java
List<String> words = new ArrayList<>(List.of("pear", "fig", "banana", "kiwi"));
Comparator<String> byLengthOld = new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return Integer.compare(a.length(), b.length());
    }
};
words.sort(byLengthOld);
System.out.println(words);
Comparator<String> byLengthNew = (a, b) -> Integer.compare(b.length(), a.length());
words.sort(byLengthNew);
System.out.println(words);

Output:

text
[fig, pear, kiwi, banana]
[banana, pear, kiwi, fig]

Notice the second comparator swaps a and b, so it sorts longest first. pear stays ahead of kiwi in both results: List.sort is stable, so equal lengths keep their order (you saw this in BEG-032).

Example 3: one loop, many conditions (realistic)

Using PersonFilter and keep from above, with Person having getName() and getAge():

java
List<Person> people = List.of(new Person("Amal", 17), new Person("Omar", 34),
        new Person("Adam", 52), new Person("Sara", 12));
System.out.println(keep(people, p -> p.getAge() >= 18));
System.out.println(keep(people, p -> p.getName().startsWith("A")));

Output:

text
[Omar(34), Adam(52)]
[Amal(17), Adam(52)]

The loop was written once. Each call supplies its own condition. In a real backend this is the shape of "filter orders by status", "filter users by role", and so on.

Backend Insight

Backend code is full of "do this at that moment" decisions the framework makes for you: run this when a request arrives, retry this when it fails, filter these rows, transform that result. The framework owns when; your code owns what. Passing behavior is how you hand over the "what".

You will see this in:

  • Collections: list.sort(comparator) and list.removeIf(condition) take behavior.
  • Streams (BEG-036): filter, map, and forEach are all behavior-taking methods.
  • Concurrency: a Runnable is the unit of work handed to a thread or an executor, and CompletableFuture chains behavior to run after a result arrives.
  • Spring: many configuration APIs accept a lambda to describe how something should be set up.

If you understand today's lesson, all of these read as "a method that accepts a recipe card".

Common Mistakes

Mistake 1: Trying to use a lambda where no functional interface is expected

  • Incorrect: Object task = () -> System.out.println("hi");
  • Correct: Runnable task = () -> System.out.println("hi");

A lambda needs a target type with exactly one abstract method. Without one the compiler cannot tell what the lambda means.

Mistake 2: Thinking a lambda is a new kind of method

A lambda is not a free-floating function. It is an object that implements an interface. That is why you must name the interface (by a variable type or a parameter type) and why .run() or .matches(...) is still how you call it.

Mistake 3: Copying loops instead of passing the condition

If you have two methods that differ only in one if line, you probably want one method with a functional interface parameter.

Mistake 4: Assuming an interface with two abstract methods can be a lambda

A lambda implements one method. An interface with two abstract methods is not a functional interface, so a lambda cannot implement it. (Comparator is fine: its other methods are default or static, or re-declare Object methods.)

Exercises

Try each exercise yourself before opening the solutions. Use the Person class and the PersonFilter and keep shown above.

Level A: Foundation

A1. Which of these can be the target of a lambda: Runnable, Comparator<String>, List<String>, or an interface with two abstract methods?

أظهر الحل A1

A1. Runnable and Comparator<String> have exactly one abstract method, so they work. List<String> is not a functional interface (it has many abstract methods), and neither is an interface with two abstract methods.

A2. Write a lambda for PersonFilter that matches people named "Sara".

أظهر الحل A2

A2. p -> p.getName().equals("Sara")

A3. In one sentence, say what stays the same and what changes between keep(people, p -> p.getAge() >= 18) and keep(people, p -> p.getAge() < 18).

أظهر الحل A3

A3. The loop and the collecting stay the same; only the condition (the behavior passed in) changes.

Level B: Application

B1. Call keep to find teenagers, aged 13 to 19 inclusive.

أظهر الحل B1

B1. keep(people, p -> p.getAge() >= 13 && p.getAge() <= 19)

B2. Write a one-method interface Discount with a method double apply(double price), a method finalPrice(double price, Discount d) that returns d.apply(price), and a call that takes 10 percent off 200.0.

أظهر الحل B2

B2.

java
interface Discount {
    double apply(double price);
}

static double finalPrice(double price, Discount d) {
    return d.apply(price);
}

// call:
finalPrice(200.0, price -> price * 0.9)   // 180.0

B3. Sort a List<Person> by name with a lambda Comparator (use String's compareTo).

أظهر الحل B3

B3. people.sort((a, b) -> a.getName().compareTo(b.getName())); The list must be modifiable (for example new ArrayList<>(...)), because List.of returns an unmodifiable list.

Level C: Challenge

C1. Write static int countMatching(List<Person> people, PersonFilter f) and use it to count adults.

أظهر الحل C1

C1.

java
static int countMatching(List<Person> people, PersonFilter f) {
    int n = 0;
    for (Person p : people) {
        if (f.matches(p)) {
            n++;
        }
    }
    return n;
}
// countMatching(people, p -> p.getAge() >= 18)

C2. Write static Runnable greeter(String name) that returns a Runnable printing Hello, followed by the name. Call it with "Layla" and run the result.

أظهر الحل C2

C2.

java
static Runnable greeter(String name) {
    return () -> System.out.println("Hello, " + name);
}
// greeter("Layla").run();   prints: Hello, Layla

The lambda uses the parameter name, which is never reassigned. The next lesson explains that rule.

Mini Challenge

Take a small Product class with getName() and getPrice() (write it yourself). Create a list of five products. Then:

  1. Write an interface ProductFilter with one method.
  2. Write keepProducts(List<Product>, ProductFilter).
  3. Call it three times with three different lambdas: cheap items, names starting with a letter you choose, and price between two numbers.

Check your work: the loop exists only once, and every call differs only in its lambda.

Summary & Next Lesson

Key takeaways

  • Methods can receive behavior, not just data, by taking an object of an interface type.
  • A functional interface has exactly one abstract method. Default methods and Object re-declarations do not count.
  • An anonymous class can implement it, but one line of logic costs about five lines of code.
  • A lambda is a compact implementation of a functional interface. The compiler finds out which one from the target type.
  • Passing the changing part (the condition) lets one loop replace many copies.

Next lesson

The next lesson, "Lambda syntax", covers the exact shapes a lambda can take (parameters, expression body, block body) and the rule about using variables from outside the lambda.

Self-check

  1. What is a functional interface?
  2. How does the compiler know which interface a lambda implements?
  3. Why is an anonymous class noisy for a simple condition?
  4. Name two functional interfaces you have already used.
  5. Can a lambda stand where an Object is expected?
أظهر الإجابات
  1. An interface with exactly one abstract method.
  2. From the target type: the type expected where the lambda appears, such as a parameter or a variable type.
  3. Most of its lines (the class name, method name, modifiers, types) are forced and carry no information; only the body is new.
  4. Runnable and Comparator.
  5. No. Object has no single abstract method to implement, so the compiler cannot give the lambda a meaning.

References

التطبيق

طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.

المختبر والتمارين الاختبار

تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.

اكتب للبحث في المستويات والمراحل والوحدات والدروس.