← Fundamentals
Guided lesson 5 questions · 6 languages

Functions as values and higher-order operations

Pass behavior into a function, and get new behavior back.

A function is a value: in Java, a lambda or a method reference is an object that can be stored, passed to a method, and returned from one. A method that takes or returns such a value is a higher-order function. A method reference such as Main::addBonus passes the method; a call such as addBonus(8) passes its result.

Before you begin

Why this matters

Sorting, streams, and many library methods take a lambda or a method reference from you. If you can read the interface a method expects and tell a method reference from a call, you can predict what such code does and read the compiler’s errors when the two are mixed up.

What this helps you reason about

  • Follow a method reference you passed in: the method that received it calls it, once per item.
  • Read IntUnaryOperator and Function<Integer, Integer>, and call each through its own method.
  • Spot a call written where Main::addBonus was needed, and the reverse.

A Java method cannot be passed by itself, but a lambda or a method reference can, as an object of an interface with one method. This lesson shows how a method calls behavior it receives, how a method builds behavior and returns it, which interfaces Java uses as function types, and the mistake of calling a method where the method itself was needed.

Already know this? The short version
  • applyToEach(scores, Main::addBonus) passes a method reference, and applyToEach calls it with rule.applyAsInt once per element. A function passed this way is a callback; a method that takes or returns one is a higher-order function.
  • A factory such as makeScorer(3) runs once and returns a lambda as an IntUnaryOperator; the lambda runs each time applyAsInt is called, after the factory’s frame is gone.
  • Function types are functional interfaces: IntUnaryOperator (applyAsInt), Function<Integer, Integer> (apply), IntBinaryOperator, Predicate<T> (test). rule(8) does not compile: call the interface’s method.
  • Main::addBonus passes the method and addBonus(8) passes its result. javac rejects an int where an IntUnaryOperator is expected, but "text " + rule compiles and prints the lambda’s class name.

Go to the final check· Go to the practice tickets

Words in this lesson (6)
callback
A function passed to another function, which calls it, for example once for each element of an array.
higher-order function
A function that takes a function as an argument, returns a function, or both.
functional interface
An interface with exactly one abstract method, such as IntUnaryOperator with applyAsInt. A lambda or a method reference can be used wherever one is expected.
method reference
An expression such as Main::addBonus that turns an existing method into a value of a functional interface, without calling the method.
factory
A function whose job is to make and return a new value, such as a new function built from its arguments.
lambda
A function written as a value, without a name, such as n -> n * 3. Its type is an interface with one method, such as IntSupplier, and calling that method runs the lambda.

Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.

01 / Callbacks

How does a function accept behavior?

Function calls showed that a lambda or a method reference is a value, passed without being called. applyToEach below uses this. It goes through an array of scores and builds a new array, but it does not decide what to do with each score. Its caller passes that in, as the IntUnaryOperator rule, and applyToEach calls rule.applyAsInt(scores[i]) for each score.

Main::addBonus is a method reference: it turns the existing method addBonus into a value of the interface, without calling it. A function passed to another function, which calls it, is called a callback. A function that takes a function as an argument, or returns one, is called a higher-order function. applyToEach is a higher-order function, and Main::doublePoints would make the same applyToEach double each score.

Sorting works the same way: list.sort(byYear) passes a Comparator that decides which of two elements comes first. Ordering, sorting, and comparators follows that callback in detail.

Main.java
static int[] applyToEach(int[] scores, IntUnaryOperator rule) {
    int[] results = new int[scores.length];
    for (int i = 0; i < scores.length; i++) {
        results[i] = rule.applyAsInt(scores[i]);
    }
    return results;
}

int[] results = applyToEach(scores, Main::addBonus);
// [8, 10, 9]

A higher-order method receives behavior as an interface object, and decides when to call it.

Coming from JavaScript: In JavaScript, addBonus alone is the function. In Java, addBonus alone is not an expression; write Main::addBonus, or a lambda such as points -> addBonus(points).

Precisely: four kinds of method reference

Main::addBonus refers to a static method. A method reference can also name a method of one object (counter::add), an instance method of any object of a class (String::length, which takes the object as its first argument), or a constructor (ArrayList::new). JLS 15.13 lists them.

Experiment

Follow a rule into the method that calls it

applyToEach receives the rule as an IntUnaryOperator and calls it as rule.applyAsInt(scores[i]), once per score. Which method runs is decided by the caller, in the call to applyToEach. applyToEach never names addBonus or doublePoints.

Why it matters To change what a higher-order function does with each item, change the function you pass to it. The higher-order function stays the same.

Predict the printed numbers, and which frames are in the frame list while addBonus runs. Then change applyToEach(scores, Main::addBonus) so that it passes Main::doublePoints instead, and predict again.

import java.util.Arrays;
import java.util.function.IntUnaryOperator;

class Main {
    static int[] applyToEach(int[] scores, IntUnaryOperator rule) {
        int[] results = new int[scores.length];
        for (int i = 0; i < scores.length; i++) {
            results[i] = rule.applyAsInt(scores[i]);
        }
        return results;
    }

    static int addBonus(int points) {
        return points + 2;
    }

    static int doublePoints(int points) {
        return points * 2;
    }

    public static void main(String[] args) {
        int[] scores = {6, 8, 7};
        int[] results = applyToEach(scores, Main::addBonus);
        System.out.println(Arrays.toString(results));
    }
}
What to look for

applyToEach lists rule as IntUnaryOperator lambda@ and a number, followed by {}: Java turned the method reference into an object of the interface, and the panel shows such objects as lambdas. The number after @ is the debugger’s id for an object, not an address. Each time line 8 runs rule.applyAsInt(scores[i]), Main.addBonus appears on top of Main.applyToEach, which waits, on top of Main.main, with points 6, then 8, then 7. results fills to [8, 10, 9]. The run prints [8, 10, 9]. After the edit, rule looks the same in the panel, but the frames on top are Main.doublePoints, and the run prints [12, 16, 14].

Expected output

[8, 10, 9]

02 / Factories

How does a function return new behavior?

A method can also return behavior. makeScorer below takes a difficulty and returns a lambda that multiplies points by it, as an IntUnaryOperator. A function whose job is to make and return a new value, here a lambda, is often called a factory.

Calling makeScorer(3) runs makeScorer once. It computes no score: it returns the lambda, and hard holds it. The lambda runs later, each time hard.applyAsInt(6) or hard.applyAsInt(8) is called, when makeScorer’s frame is gone. It still has difficulty because it keeps a copy, as Closures and captured variables showed.

applyToEach(scores, makeScorer(2)) passes the result of a call, and that is right here: the result is an IntUnaryOperator. makeScorer(3).applyAsInt(8) does both steps in one expression.

Main.java
static IntUnaryOperator makeScorer(int difficulty) {
    return points -> points * difficulty;
}

IntUnaryOperator hard = makeScorer(3);
int first = hard.applyAsInt(6); // 18
int[] doubled = applyToEach(scores, makeScorer(2));
// [12, 16, 14]

A factory runs once and returns a lambda; the lambda runs at each call of its method.

Experiment

Make a scorer, then call it

Calling makeScorer runs its body once: it makes a new function and returns it. The returned function runs only when it is called, after makeScorer has returned, and it still has difficulty because it captured it. In the edit, makeScorer(3).applyAsInt(8) calls makeScorer again and then calls the function it returns.

Why it matters A factory runs when it is called. The function it returns runs each time that function is called. To use the same rule many times, call the factory once and keep its result.

Predict the two numbers, and whether makeScorer’s frame is in the frame list while the scorer runs. Then change hard.applyAsInt(8) to makeScorer(3).applyAsInt(8), and predict: how many times does makeScorer run now?

import java.util.function.IntUnaryOperator;

class Main {
    static IntUnaryOperator makeScorer(int difficulty) {
        return points -> points * difficulty;
    }

    public static void main(String[] args) {
        IntUnaryOperator hard = makeScorer(3);
        int first = hard.applyAsInt(6);
        int second = hard.applyAsInt(8);
        System.out.println(first + " " + second);
    }
}
What to look for

Main.makeScorer’s frame has difficulty 3. When it returns, main lists hard as IntUnaryOperator lambda@ and a number, then {arg$1: 3}: arg$1 is a name Java generates for the first captured value, here difficulty. Each call to hard.applyAsInt gets a frame named Main.lambda in makeScorer, with difficulty 3 and points, directly on top of Main.main: Main.makeScorer is not in the list. The run prints 18 24. After the edit, Main.makeScorer’s frame appears again before the second lambda frame, and the run still prints 18 24.

Expected output

18 24

03 / Types

How do you write the type of a function?

Java has no function types of its own. A functional interface plays that role: an interface with exactly one abstract method. java.util.function has the common ones. IntUnaryOperator takes an int and returns an int, through applyAsInt. Function<Integer, Integer> does the same with Integer objects, through apply. IntBinaryOperator takes two ints, and Predicate<T> returns a boolean, through test.

A lambda or a method reference has no type of its own: it takes the type the code expects. The same points -> points + 2 can be an IntUnaryOperator in one place and a Function<Integer, Integer> in another.

To run the behavior, call the interface’s method. rule(8) does not compile, because rule is a variable, not a method: javac reports error: cannot find symbol, symbol: method rule(int).

Main.java
IntUnaryOperator a = points -> points + 2;
Function<Integer, Integer> b = points -> points + 2;
IntBinaryOperator sum = (x, y) -> x + y;
int ten = a.applyAsInt(8);
Integer alsoTen = b.apply(8);
// a(8) does not compile: cannot find symbol

A functional interface is Java’s function type; call the behavior through the interface’s method.

Precisely: @FunctionalInterface

Any interface with one abstract method can hold a lambda (JLS 9.8). The annotation @FunctionalInterface asks javac to check that an interface has exactly one. The Int versions avoid boxing every int into an Integer, which Java final, primitives, and boxing explains.

04 / Pass or call

What goes wrong when a function is called instead of passed?

A method reference passes the method. A call, with parentheses, runs the method at once and passes its result. Mixing the two up is the most common mistake with higher-order functions, and it goes two ways.

Calling too early: describe(addBonus(8)) runs addBonus before describe starts and passes the int 10. javac refuses it: error: incompatible types: int cannot be converted to IntUnaryOperator.

Forgetting the call can compile. "8 becomes " + rule turns the lambda object into text, and the program prints its class name, such as Main$$Lambda$1/0x… followed by @ and a number.

Main.java
static String describe(IntUnaryOperator rule) {
    return "8 becomes " + rule.applyAsInt(8);
}

System.out.println(describe(Main::addBonus)); // 8 becomes 10
// describe(addBonus(8)) does not compile.
// "8 becomes " + rule prints Main$$Lambda$...

Pass Main::addBonus when the receiver will call it. Write addBonus(8) only where the result itself is needed.

Coming from Python: Python passes add_bonus and calls rule(8). Java passes Main::addBonus and calls rule.applyAsInt(8).

Experiment

Call a method where the method was expected

addBonus(8) calls the method and gives the int 10, and describe needs an IntUnaryOperator. The other direction compiles: "8 becomes " + rule turns the object into text, such as 8 becomes Main$$Lambda$1/0x… followed by @ and a number.

Why it matters int cannot be converted to IntUnaryOperator means a call was written where the method itself was needed: write Main::addBonus.

Predict the printed line. Then change describe(Main::addBonus) to describe(addBonus(8)), and read what the compiler says.

import java.util.function.IntUnaryOperator;

class Main {
    static String describe(IntUnaryOperator rule) {
        return "8 becomes " + rule.applyAsInt(8);
    }

    static int addBonus(int points) {
        return points + 2;
    }

    public static void main(String[] args) {
        System.out.println(describe(Main::addBonus));
    }
}
What to look for

describe lists rule as IntUnaryOperator lambda@ and a number, and rule.applyAsInt(8) puts Main.addBonus on top of Main.describe, with points 8. The run prints 8 becomes 10. After the edit, javac reports: error: incompatible types: int cannot be converted to IntUnaryOperator, with the mark after addBonus.

Expected output

8 becomes 10

05 / Try it

Can you follow behavior passed in and returned?

One method is passed, one lambda is built by a factory, and one call returns an IntUnaryOperator. Decide when each one runs.

Main.java
import java.util.function.IntUnaryOperator;

class Main {
    static int applyTwice(IntUnaryOperator rule, int points) {
        return rule.applyAsInt(rule.applyAsInt(points));
    }

    static IntUnaryOperator makeScorer(int difficulty) {
        return points -> points * difficulty;
    }

    static int addBonus(int points) {
        return points + 2;
    }

    public static void main(String[] args) {
        int a = applyTwice(Main::addBonus, 5);
        int b = applyTwice(makeScorer(2), 3);
        System.out.println(a + " " + b);
    }
}

What is printed?

Reveal the reasoning

9 12. applyTwice calls the rule it receives two times, the second time on the first result. Main::addBonus turns 5 into 7, then 9. makeScorer(2) runs once, before applyTwice starts, and its result is a function that doubles: 3 becomes 6, then 12.

Practice tickets

  1. HOF-101 · The board shows each athlete’s worst result as their best → Free
  2. HOF-102 · Every win counts as a first-round win → Sign in · Pro
  3. HOF-103 · Practice marks fall below zero or stay above the scale → Sign in · Pro

Keep exploring

Stack and heap →

Primary references