Lesson 28Phase 3: Abstraction and design
Enums, lambdas, and functional interfaces
A fixed set of named values, treating behavior as data, and the functional interface that makes it work.
By BasithUpdated 60 min
- enum
- lambda
- functional interface
- first-class functions
- method reference
This lesson has two ideas. The first is a type whose values are a small, fixed set of names, such as the days of the week or the suits in a pack of cards. The second is a way to hand a piece of behavior to a method, the way you already hand it a number. At the end the two meet, in an enum whose values each carry their own behavior.
- Declare an
enum, compare its values with==, and switch on it, including a switch expression that covers every value. - Use
values(),ordinal(),name()andvalueOf, and give an enum fields, a constructor and methods. - Say what makes an interface a functional interface, and use the ready-made ones in
java.util.functionsuch asPredicateandFunction. - Write lambdas with zero, one or two parameters, with a short body or a block body, and pass them to methods.
- Treat behavior as a value: keep a lambda in a variable or an array, pass it, and return it from a method.
- Read and write method references such as
Math::max,Integer::sumandSystem.out::println.
static final int MON = 0 from static and final. You know interfaces and how a class keeps one with implements, and in inner classes you wrote an anonymous class that implements an interface on the spot. You have also handed a short rule, score -> score < 50, to removeIf in ArrayList, copying its shape. This lesson explains what that rule is.Numbers standing in for meanings#
Suppose a program needs to track the day of the week. With the tools you have, you might use plain int constants, the kind of class constant you met in static and final:
public class Main { static final int MON = 0; static final int TUE = 1; static final int WED = 2; public static void main(String[] args) { int today = 99; System.out.println("today is " + today); }}This compiles and runs, and that is the problem. today is an int, so Java happily lets it hold 99, or -4, or any other number that is not a day at all. A method that takes a day really takes an int, and whoever reads the code has to remember that 2 means Wednesday. The meaning lives in your head, not in the program.
Key ideaWhen the valid values of something form a small, fixed set, an
intis too loose. It allows billions of values you never meant to be legal, and it throws away the names.
An enum: a type with a fixed set of named values#
An enum (short for enumeration, a list of things named one by one) is a type you define whose only legal values are the ones you name. You list them once, and from then on a variable of that type can hold one of those values and nothing else.
public class Main { enum Day { MON, TUE, WED, THU, FRI, SAT, SUN } public static void main(String[] args) { Day today = Day.WED; System.out.println("Today is " + today); if (today == Day.SAT || today == Day.SUN) { System.out.println("The shop is closed"); } else { System.out.println("The shop is open"); } }}It prints Today is WED and The shop is open. Read the declaration first. enum Day introduces a new type called Day, just as writing a class does. Inside the braces is the complete list of values it can ever have, MON to SUN. Each one is a constant, so they are written in capitals.
Day today = Day.WED; reads almost like English: today is a Day, set to Wednesday. You reach a value with the type name, a dot, then the value. Try Day today = Day.FUNDAY; and the program does not compile: javac reports cannot find symbol, because FUNDAY is not in the list. The mistake that the int version let through is caught before the program runs.
"Today is " + today, Java uses the name you wrote: WED. That is far friendlier than printing 2 and leaving the reader to decode it, and you did not have to write a toString for it.enum Day { MON, TUE, WED, THU, FRI, SAT, SUN } and a variable Day today, which line does not compile?An enum is a class#
An enum is a special kind of class. Each constant is an object of that class, made once when Java sets up the enum, and Java declares each one as a public static final field of the enum. That is why you write Day.WED, the same way you wrote Goblin.count in static and final. Every enum quietly extends Java's class java.lang.Enum, which gives it methods such as name() and ordinal(), and Java writes two more, values() and valueOf, for each enum you declare.
You cannot make more Day objects. new Day() does not compile; javac says enum classes may not be instantiated. The seven constants are all the days there will ever be.
Enums in a switch#
Because an enum has a small, known set of values, it pairs naturally with switch. In the caselabels you write the constant's name on its own, without Day. in front, because the switch already knows it is looking at a Day.
public class Main { enum Day { MON, TUE, WED, THU, FRI, SAT, SUN } public static void main(String[] args) { Day today = Day.FRI; switch (today) { case SAT: case SUN: System.out.println("Weekend"); break; case FRI: System.out.println("Almost the weekend"); break; default: System.out.println("A normal working day"); } }}Trace it. today is FRI. The switch jumps to case FRI, prints Almost the weekend, and the break stops it falling into default. The two weekend cases stacked together, SAT then SUN with nothing between them, share one action: that is the deliberate fall-through you met in the switch lesson.
The arrow form and switch expressions work on enums too, and with an enum they bring one extra benefit:
public class Main { enum Day { MON, TUE, WED, THU, FRI, SAT, SUN } static String kind(Day day) { return switch (day) { case SAT, SUN -> "Weekend"; case FRI -> "Almost the weekend"; case MON, TUE, WED, THU -> "A working day"; }; } public static void main(String[] args) { System.out.println(kind(Day.SUN)); System.out.println(kind(Day.TUE)); }}It prints Weekend and A working day. Notice there is no default. A switch expression must produce a value for every possible input, and for an enum javac can check the list: these cases cover all seven days. Leave THU out and javac refuses with the switch expression does not cover all possible input values. So if you later add a constant to the enum, every switch expression that forgot it stops compiling, and the compiler points you at each place to fix.
action with a switch expression over the traffic light: RED gives Stop, YELLOW gives Slow down, and GREEN gives Go. Write no default; the three cases cover every light.Built-in methods: values, ordinal, name and valueOf#
Every enum gets a few methods for free. values()hands back an array of all the constants in the order you declared them, so you can loop over them. Each constant's ordinal() is its position in that list, counting from 0, and name() is its name as a String. valueOf goes the other way, from a name to the constant.
public class Main { enum Direction { NORTH, EAST, SOUTH, WEST } public static void main(String[] args) { for (Direction d : Direction.values()) { System.out.println(d + " is at position " + d.ordinal()); } Direction typed = Direction.valueOf("EAST"); System.out.println(typed.name() + " " + (typed == Direction.EAST)); Direction oops = Direction.valueOf("east"); System.out.println(oops); }}IllegalArgumentException: No enum constant Main.Direction.east
A method was given a value it does not accept.
The loop prints each direction with its position, 0 to 3. valueOf("EAST") finds the constant, so the next line prints EAST true. The last lookup fails: names must match exactly, capital letters included, so valueOf("east") stops the program with an IllegalArgumentException and the message No enum constant Main.Direction.east.
ordinal() and values() together let you step through the list. Decide what this prints.
public class Main { enum Day { MON, TUE, WED, THU, FRI, SAT, SUN } static Day next(Day d) { Day[] all = Day.values(); return all[(d.ordinal() + 1) % all.length]; } public static void main(String[] args) { System.out.println(next(Day.WED)); System.out.println(next(Day.SUN)); }}next finds the day after the one it is given. What do the two lines print?
It prints THU and MON. For WED, ordinal() is 2, so next picks position 3, THU. For SUN, ordinal() is 6; 6 + 1 is 7, and 7 % 7 is 0, so the week wraps around to MON. The % keeps the position inside the array.
ordinal() is fine for moving around the list, as next does. Do not save it in a file or use it as an id number: put a new constant in the middle of the list and every position after it shifts. If a constant needs a number of its own, give the enum a field for it, as the next example does.Fields, constructors and methods#
Since an enum is a class, each constant can carry its own data, and the enum can have methods. Suppose each planet should know its own gravity, so that it can work out what something weighs there.
public class Main { enum Planet { MERCURY(3.7), EARTH(9.8), JUPITER(24.8); private final double gravity; Planet(double gravity) { this.gravity = gravity; } double weightFor(double mass) { return mass * gravity; } } public static void main(String[] args) { double mass = 70.0; System.out.println("On Earth: " + Planet.EARTH.weightFor(mass)); System.out.println("On Jupiter: " + Planet.JUPITER.weightFor(mass)); }}It prints On Earth: 686.0 and On Jupiter: 1736.0. Each constant is written with a number after it, like EARTH(9.8). That number goes to the enum's constructor, which stores it in the gravity field of that constant. weightFor is an ordinary method using the field: Planet.EARTH.weightFor(70.0) is 70.0 * 9.8, which is 686.0.
Two rules come with this. The list of constants comes first and ends with a semicolon; the fields, constructor and methods follow it. Put the field first and javac stops with enum constant expected here. And you never call the constructor with new: Java calls it once for each constant, with the values in that constant's parentheses.
Behavior you want to pass around#
Now the second idea. So far, when you wanted to give a method some information, you passed a value: a number, a string, an object. Sometimes the thing you want to hand over is an action: “take these two numbers and do this to them”. How do you package an action so it can be passed like a value?
You already have the tool: an interface. Define an interface with one method that describes the shape of the action, and any object that implements it is a packaged action you can pass around.
public class Main { interface Operation { int apply(int a, int b); } static int compute(Operation op, int a, int b) { return op.apply(a, b); } public static void main(String[] args) { Operation add = new Add(); int result = compute(add, 3, 4); System.out.println("3 add 4 = " + result); }} class Add implements Main.Operation { public int apply(int a, int b) { return a + b; }}Look closely at compute. Its first parameter is an Operation: an action. It does not know or care what the action does; it calls op.apply(a, b) and returns the result. Hand it an Add object, and compute(add, 3, 4) runs add.apply(3, 4), which is 3 + 4, so the program prints 3 add 4 = 7. The behavior was passed in, and compute could just as well subtract or multiply, depending only on which Operation it is given.
Operation is declared inside Main, so the Add class outside it calls it Main.Operation, just as Person.Address was named in inner classes. An interface declared inside a class is always static, so using it needs no Main object.
Notice that the interface has exactly one method. That matters enough to have a name: an interface with exactly one abstract method is a functional interface. With only one method, there is no doubt about what implementing it means: there is one job to fill in.
Key ideaA functional interface is an interface with exactly one abstract method. Because there is only one method to provide, Java lets you provide just that method's body, with no class and no ceremony. That shortcut is the lambda.
Comparator, whose one abstract method compares two objects, is how you sort things in your own order in comparing and sorting. Runnable, whose one method run() takes nothing and returns nothing, describes a job for a thread in threads. Once you look for “an interface with one abstract method”, you find the pattern everywhere.The ceremony problem#
Writing a whole separate Add class to carry one line of logic, a + b, is a lot of packaging for very little content. You met one way to trim it in inner classes: an anonymous class, which implements the interface on the spot without naming a class.
public class Main { interface Operation { int apply(int a, int b); } static int compute(Operation op, int a, int b) { return op.apply(a, b); } public static void main(String[] args) { Operation add = new Operation() { public int apply(int a, int b) { return a + b; } }; System.out.println("3 add 4 = " + compute(add, 3, 4)); }}Better, but still noisy. The only part that says anything is return a + b;. Everything around it, new Operation() { public int apply(int a, int b) { ... } }, is scaffolding the compiler could work out for itself. It already knows the type is Operation, that the one method is apply, and what its parameter types are.
The lambda: just the body of the one method#
A lambdais a compact way to write that one method's body and nothing else. You give the parameters, an arrow ->, and the result. Java fills in the rest from the interface.
public class Main { interface Operation { int apply(int a, int b); } static int compute(Operation op, int a, int b) { return op.apply(a, b); } public static void main(String[] args) { Operation add = (a, b) -> a + b; Operation multiply = (a, b) -> a * b; System.out.println("3 add 4 = " + compute(add, 3, 4)); System.out.println("3 multiply 4 = " + compute(multiply, 3, 4)); }}It prints 3 add 4 = 7 and 3 multiply 4 = 12. Step through it and watch what happens when compute calls op.apply(a, b): the trace jumps into the lambda's body. Read (a, b) -> a + b as “given a and b, produce a + b”. The left of the arrow is the parameters; the right is what to return. There is no method name, because Operation has only one method. There are no parameter types, because Java reads them from apply(int a, int b).
add is still an Operation, a value you can store in a variable and pass to compute exactly as before. It is the same machinery as the class version, with almost none of the noise.
new Operation() { ... } and the method header.public int apply(int a, int b).(a, b) -> a + b only means something once Java knows which functional interface it fills in, from Operation add = ... or from a parameter that expects an Operation. Without that, it does not compile: var f = (a, b) -> a + b; gives cannot infer type for local variable f, and Object o = () -> 5; gives Object is not a functional interface.Lambda shapes#
A lambda's shape follows the method it implements:
- No parameters: empty parentheses, as in
() -> "Hello!". That is the shape you copied forassertThrowsin testing with JUnit. - One parameter: the parentheses may be left out, as in
n -> n % 2 == 0. That is the shape of the rule you gaveremoveIf. - Two or more parameters: parentheses, as in
(a, b) -> a * b. - A body of several statements: braces, with
returnwritten out, as in an ordinary method.
public class Main { interface Greeting { String make(); } interface Check { boolean test(int n); } interface Operation { int apply(int a, int b); } public static void main(String[] args) { Greeting hello = () -> "Hello!"; Check isEven = n -> n % 2 == 0; Operation doubleBigger = (a, b) -> { int bigger = Math.max(a, b); return bigger * 2; }; System.out.println(hello.make()); System.out.println(isEven.test(7) + " " + isEven.test(10)); System.out.println(doubleBigger.apply(3, 8)); }}It prints Hello!, false true and 16. Each lambda is called through its interface's one method: make, test and apply.
Creating a lambda and running it are two separate moments. Decide what this program prints, and in what order.
public class Main { interface Operation { int apply(int a, int b); } public static void main(String[] args) { Operation op = (a, b) -> { System.out.println("working on " + a + " and " + b); return a * b; }; System.out.println("lambda made"); System.out.println(op.apply(3, 4)); }}The lambda prints a line when it runs. In what order do the three lines appear?
It prints lambda made, then working on 3 and 4, then 12. Creating the lambda runs none of its body; it only packages the code. The body runs when op.apply(3, 4) calls it, and it would run again on every later call.
local variables referenced from a lambda expression must be final or effectively final.Methods that take behavior#
The point of all this is to write methods that take behavior as a parameter. countMatching below counts how many numbers pass a test, and the test itself is a parameter: whoever calls the method decides what passing means.
countMatching walks the array and counts the numbers for which check.test(n) is true; main uses it to count the scores of 50 or more, so the program prints 3.Tap a line, then tap where it should go. Or use the arrows.
countMatching is finished. Replace the three placeholder lambdas in main so that the program counts the scores of 50 or more, the even scores, and the scores from 40 to 49.Ready-made functional interfaces#
You do not have to write a new interface for every shape of behavior. The package java.util.function holds dozens of general-purpose functional interfaces, and most code uses those. The type in angle brackets says what the behavior works on, as in ArrayList<String>, and Integer stands in for int there, as you saw in ArrayList. Here are the six you will meet most:
| Interface | Its one method | What it is for | A lambda for it |
|---|---|---|---|
Predicate<T> | boolean test(T t) | A yes-or-no test | word -> word.length() > 5 |
Function<T, R> | R apply(T t) | Turning a T into an R | word -> word.length() |
Consumer<T> | void accept(T t) | Doing something with a T, returning nothing | word -> System.out.println(word) |
Supplier<T> | T get() | Producing a T from nothing | () -> "Hello" |
UnaryOperator<T> | T apply(T t) | Changing a T into another T | word -> word.toUpperCase() |
BinaryOperator<T> | T apply(T a, T b) | Combining two Ts into one | (a, b) -> a + b |
import java.util.function.Consumer;import java.util.function.Function;import java.util.function.Predicate;import java.util.function.Supplier; public class Main { public static void main(String[] args) { Predicate<String> isLong = word -> word.length() > 5; Function<String, Integer> length = word -> word.length(); Consumer<String> shout = word -> System.out.println(word.toUpperCase() + "!"); Supplier<String> greeting = () -> "Hello"; System.out.println(isLong.test("banana")); System.out.println(length.apply("kiwi")); shout.accept("hey"); System.out.println(greeting.get()); }}It prints true, 4, HEY! and Hello. The type of each variable names the interface, so you call its one method by name: test, apply, accept and get.
Now you can read the rule you handed to removeIf. ArrayList's removeIf takes a Predicate and calls its test method on every element, removing the ones that pass. forEach takes a Consumer and calls it on every element:
import java.util.ArrayList;import java.util.List; public class Main { public static void main(String[] args) { ArrayList<Integer> scores = new ArrayList<>(List.of(72, 45, 90, 38)); scores.removeIf(score -> score < 50); scores.forEach(score -> System.out.println("kept " + score)); }}It prints kept 72 and kept 90. Inside removeIf, Java calls your lambda once for each score and removes 45 and 38, the two for which it answered true. Then forEach calls the second lambda once for each score that is left.
String and answers true or false. Which type should the variable have?Behavior as a value#
A lambda is a value. You can keep it in a variable, as you have been doing, and you can also put it in an array, pass it to a method and return it from a method. A language in which functions can be handled like any other value is said to have first-class functions. Java gets there through functional interfaces: the interface is the type, and the lambda is the value.
public class Main { interface Operation { int apply(int a, int b); } public static void main(String[] args) { Operation[] ops = { (a, b) -> a + b, (a, b) -> a - b, (a, b) -> a * b }; String[] names = { "add", "subtract", "multiply" }; for (int i = 0; i < ops.length; i++) { System.out.println(names[i] + "(6, 2) = " + ops[i].apply(6, 2)); } }}It prints add(6, 2) = 8, subtract(6, 2) = 4 and multiply(6, 2) = 12. An array of behaviors: ops[0] adds, ops[2] multiplies, and the loop calls each one on the same two numbers. Nothing about arrays had to change for them to hold actions.
A method can also build a lambda and hand it back:
import java.util.function.Function; public class Main { static Function<Integer, Integer> makeAdder(int amount) { return x -> x + amount; } public static void main(String[] args) { Function<Integer, Integer> addTen = makeAdder(10); Function<Integer, Integer> addOne = makeAdder(1); System.out.println(addTen.apply(5)); System.out.println(addOne.apply(5)); System.out.println(addTen.apply(addOne.apply(0))); }}It prints 15, 6 and 11. makeAdder(10) returns a lambda that remembers its amount, 10, and makeAdder(1) returns another that remembers 1. Each call makes a new lambda with its own amount, which it can keep because a parameter that never changes is effectively final. The last line feeds one result into the other: addOne.apply(0) is 1, and addTen.apply(1) is 11.
lambda x: x + 1 in Python or x => x + 1in JavaScript. Java's lambdas do the same job, with one difference: every lambda must fit a functional interface, which gives it a type.Method references: naming a method that already exists#
Sometimes a lambda does nothing but call a method that already exists, passing its arguments straight on: (a, b) -> Math.max(a, b) is like that. For this case Java has a shorter form, the method reference, written with two colons: Math::max.
import java.util.ArrayList;import java.util.List; public class Main { interface Operation { int apply(int a, int b); } static int compute(Operation op, int a, int b) { return op.apply(a, b); } public static void main(String[] args) { Operation larger = Math::max; Operation total = Integer::sum; System.out.println("max of 8 and 5 is " + compute(larger, 8, 5)); System.out.println("sum of 8 and 5 is " + compute(total, 8, 5)); ArrayList<String> names = new ArrayList<>(List.of("ada", "ben")); names.replaceAll(String::toUpperCase); names.forEach(System.out::println); }}It prints max of 8 and 5 is 8, sum of 8 and 5 is 13, then ADA and BEN. Each method reference stands for a lambda:
Math::maxandInteger::sumname static methods. They mean(a, b) -> Math.max(a, b)and(a, b) -> Integer.sum(a, b);Integer.sumsimply adds twoints.String::toUpperCasenames a method that each element runs on itself:word -> word.toUpperCase().replaceAlltakes aUnaryOperatorand replaces every element with its result.System.out::printlnnames a method of one particular object,System.out:name -> System.out.println(name).
Use a method reference when the lambda only passes its arguments on; otherwise keep the lambda. There is a fourth kind, for constructors, and streams puts all four side by side.
Putting the two ideas together#
Here the two halves of this lesson meet. A constant of an enum can carry data, and a lambda is a value, so each constant can carry its own behavior:
public class Main { interface Operation { int apply(int a, int b); } enum Op { PLUS("+", (a, b) -> a + b), MINUS("-", (a, b) -> a - b), TIMES("*", (a, b) -> a * b); final String symbol; final Operation rule; Op(String symbol, Operation rule) { this.symbol = symbol; this.rule = rule; } } public static void main(String[] args) { for (Op op : Op.values()) { System.out.println("6 " + op.symbol + " 2 = " + op.rule.apply(6, 2)); } }}It prints 6 + 2 = 8, 6 - 2 = 4 and 6 * 2 = 12. Each constant hands a symbol and a lambda to the constructor, which stores both. The loop over Op.values()then uses each constant's own rule. Adding a DIVIDE constant later is one more line in the list, and the loop needs no change.
Key ideaAn enum fixes the values a type may have. A lambda packs one piece of behavior into a value. Together they let each value of a type carry the behavior that belongs to it.
Comparator, the rule for sorting in your own order. HashMap and HashSet uses lambdas and Integer::sum to count and group things in a map. And streams builds whole data-processing pipelines out of lambdas and method references, which collectors and Optional carry further.Try it
Answer each one in your head or on paper first, then open the answer.
enum Suit { HEARTS, DIAMONDS, CLUBS, SPADES } safer than four int constants 0 to 3 for the suits of a card?Operation, with its single int apply(int a, int b), one?Operation sub = new Operation() { public int apply(int a, int b) { return a - b; } };Day switch, with its colon cases, what does the program print if today is Day.SUN instead of Day.FRI? Trace it.(a, b) -> a + b has no written type. How does Java know it is an Operation and not something else?java.util.function interface fits each job, and what lambda would you write? (a) a test of whether a word starts with a capital letter, (b) a rule that adds 20% tax to a price, (c) printing each name in a list.Practice
Work through these on your own. Each one builds on this page.
enum Size for a coffee shop with SMALL, MEDIUM and LARGE, where each size carries its volume in millilitres and its price in cents. Give it a method describe() that returns text like MEDIUM: 350 ml for 320 cents, and loop over Size.values() to print every size.Operation example. Add two more lambdas, one for integer division (a, b) -> a / b and one for the remainder (a, b) -> a % b. Pass each to compute with the numbers 17 and 5, and predict both results before you run it.ArrayList<String> of six words of different lengths. Use removeIf with a lambda to remove every word shorter than four letters, then replaceAll(String::toUpperCase), then print the list. Write down which java.util.function interface each call takes.makeAdder as examples. Why could a plain array hold actions without arrays changing at all?Math::max and Integer::sum that fits Operation, whose method takes two ints and returns an int, and write the plain lambda it stands for. Explain why the two forms do the same thing.Further reading#
- Enums on dev.java, including fields, methods and a different body for each constant
- Writing your first lambdas on dev.java, the start of the official lambda tutorial
- The java.util.function package in the Java SE 25 API documentation, with every ready-made functional interface
- Enum classes in the Java Language Specification, section 8.9
Short guides on this topic
Quick answers to what people search for about it, with code to run.
Spotted a mistake, or a part that did not make sense? Report it by email