Consumer Interface in Java

Three lines that behave differently from what most developers expect:
Consumer<String> printUpper = s -> System.out.println(s.toUpperCase());
printUpper.andThen(s -> System.out.println(s.length()));
printUpper.accept("hello"); // HELLO - and that's it, the length is never printed andThen() changes nothing in the original Consumer. It builds and returns a new one. If you do not store that result and call accept() on it, the second action is silently dropped. Let's go through the interface so that behaviour like this becomes obvious.
What Is Consumer<T>?
Consumer<T> is a built-in functional interface from the java.util.function package, available since Java SE 8. It takes one argument and returns nothing: its whole purpose is a side effect such as printing, logging, mutating an object, or sending a message.
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
} The function descriptor is:
T -> void Read it as "one object of type T goes in, nothing comes out". That is why Consumer is the only basic functional interface that makes sense purely because of side effects: strip everything that touches the outside world out of the lambda body, and the call becomes pointless.
The accept() Method
accept(T t) is the single abstract method of the interface, and it is the one that performs the action:
import java.util.function.Consumer;
public class ConsumerExample1 {
public static void main(String[] args) {
Consumer<String> printUpperCase = str -> System.out.println(str.toUpperCase());
printUpperCase.accept("hello"); // HELLO
}
} The argument does not have to be a String - any reference type works, including your own classes:
public class Exam {
private final String name;
private final String version;
public Exam(String name, String version) {
this.name = name;
this.version = version;
}
public String getName() {
return name;
}
public String getVersion() {
return version;
}
} Consumer<Exam> printExam = e -> System.out.println(e.getName() + " " + e.getVersion());
printExam.accept(new Exam("Java Core", "21")); // Java Core 21
printExam.accept(new Exam("Spring", "6")); // Spring 6 A lambda can often be replaced by a method reference - the code gets shorter and reads better:
Consumer<String> print = System.out::println;
print.accept("hello"); // hello
Consumer<List<String>> clear = List::clear; // reference to an instance method Good to know
The method reference System.out::println evaluates System.out once — when the Consumer is created, not on every accept() call. If you later swap the output stream with System.setOut(), the already created Consumer keeps writing to the old stream. The lambda s -> System.out.println(s) behaves differently: it reads System.out on each call.
Composing Consumers with andThen()
Consumer has exactly one default method. It returns a composed Consumer that runs two actions in sequence over the same argument:
default Consumer<T> andThen(Consumer<? super T> after) The JDK implementation is tiny, and it explains the entire behaviour of the method:
default Consumer<T> andThen(Consumer<? super T> after) {
Objects.requireNonNull(after);
return (T t) -> { accept(t); after.accept(t); };
} Note the distinction: accept() returns nothing, while andThen() returns a new Consumer object. You have to store it or use it right away:
import java.util.function.Consumer;
public class ConsumerExample2 {
public static void main(String[] args) {
Consumer<String> printUpperCase = str -> System.out.println(str.toUpperCase());
Consumer<String> printLowerCase = str -> System.out.println(str.toLowerCase());
Consumer<String> printBoth = printUpperCase.andThen(printLowerCase);
printBoth.accept("Hello World");
// HELLO WORLD
// hello world
}
} Calls to andThen() can be chained as far as you like - execution order matches the order you wrote them, left to right:
Consumer<String> pipeline = printUpperCase
.andThen(printLowerCase)
.andThen(s -> System.out.println("length: " + s.length()));
pipeline.accept("Java");
// JAVA
// java
// length: 4 Where Consumer Is Used in the JDK
Tutorials often say the main use of Consumer is printing to the console. That is just the most visible example. In practice you meet the interface less in your own declarations and more in the signatures of standard library methods:
Iterable.forEach(Consumer<? super T>)- iterating over any collection;Stream.forEach(Consumer<? super T>)- a terminal stream operation;Stream.peek(Consumer<? super T>)- an intermediate operation for debugging a pipeline;Optional.ifPresent(Consumer<? super T>)- an action to run only if a value is present;Map.forEach(BiConsumer<? super K, ? super V>)- iterating over key-value pairs.
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
public class ConsumerExample3 {
public static void main(String[] args) {
Consumer<String> greet = name -> System.out.println("Hello, " + name + "!");
List<String> names = List.of("Anna", "Bob");
names.forEach(greet);
// Hello, Anna!
// Hello, Bob!
Optional.of("Clara").ifPresent(greet); // Hello, Clara!
Map<String, Integer> ages = Map.of("Anna", 30);
ages.forEach((k, v) -> System.out.println(k + " -> " + v)); // Anna -> 30
}
} BiConsumer, IntConsumer and the Rest of the Family
The java.util.function package ships several relatives of Consumer: one for two arguments and several for primitives. The primitive versions exist to avoid autoboxing and the extra objects it creates.
| Interface | Abstract method | Descriptor | When you need it |
|---|---|---|---|
Consumer<T> | void accept(T t) | T -> void | The default case: one action on one object |
BiConsumer<T, U> | void accept(T t, U u) | (T, U) -> void | Two arguments, for example Map.forEach() |
IntConsumer | void accept(int value) | int -> void | IntStream.forEach() without boxing into Integer |
LongConsumer | void accept(long value) | long -> void | The same for long values |
DoubleConsumer | void accept(double value) | double -> void | The same for double values |
ObjIntConsumer<T> | void accept(T t, int value) | (T, int) -> void | Accumulators in IntStream.collect() |
BiConsumer, IntConsumer, LongConsumer and DoubleConsumer also declare andThen(), taking a parameter of their own type. ObjIntConsumer and its siblings have no andThen() at all.
import java.util.function.IntConsumer;
import java.util.stream.IntStream;
IntConsumer printSquare = n -> System.out.println(n * n);
IntStream.rangeClosed(1, 3).forEach(printSquare);
// 1
// 4
// 9 Where Developers Get Tripped Up
1. Throwing away the result of andThen()
The exact case from the top of this lesson. A Consumer is immutable: andThen() builds a new object and leaves the old variable untouched.
printUpper.andThen(printLower); // result discarded, nothing changed
printUpper.accept("hi"); // HI
Consumer<String> both = printUpper.andThen(printLower); // this is the correct way
both.accept("hi"); // HI, then hi 2. An exception in the first action cancels the second
A composed Consumer does not catch anything. If the first action throws, the second one never runs and the exception propagates to the caller.
Consumer<String> broken = s -> { throw new IllegalStateException("boom"); };
Consumer<String> print = System.out::println;
broken.andThen(print).accept("hi");
// IllegalStateException: boom - "hi" is never printed 3. Trying to modify a collection element by assignment
Assigning to the lambda parameter compiles, but it only changes a local copy of the reference. The collection stays as it was.
List<String> names = new ArrayList<>(List.of("anna", "bob"));
names.forEach(s -> s = s.toUpperCase()); // does nothing useful
System.out.println(names); // [anna, bob]
names.replaceAll(String::toUpperCase); // this actually works
System.out.println(names); // [ANNA, BOB] 4. A checked exception inside the lambda
accept() declares no throws clause, so a checked exception must be handled inside the lambda body - otherwise the code does not compile.
Consumer<Path> reader = path -> Files.readAllLines(path); // compile error: IOException
Consumer<Path> safeReader = path -> { // this compiles
try {
Files.readAllLines(path).forEach(System.out::println);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}; 5. Passing null to andThen()
Because of Objects.requireNonNull(after), a NullPointerException is thrown immediately at the andThen() call, not later at accept(). That is convenient: the mistake surfaces exactly where the chain is assembled.
Important
If the action has to return a value, you need Function rather than Consumer (and map() rather than forEach()). A clear sign of the wrong choice: the body of the Consumer writes into an external list or field just to carry a result outside. In a parallel stream that same code also stops being thread-safe.
Frequently Asked Questions
How is Consumer different from Supplier, Function and Runnable?
The only difference is the presence of an input and an output. Consumer takes an argument and returns nothing (T → void). Supplier is the opposite: no arguments, one returned value (void → T). Function takes an argument and returns a result (T → R). Runnable takes nothing and returns nothing (void → void). All four are stateless and describe exactly one action.
In which order does andThen() run the actions, and what happens on an exception?
Strictly left to right: first accept() of the original Consumer, then accept() of the one passed to andThen(). Both receive the same argument. Nothing is caught: if the first action throws, the second never runs and the exception propagates out of the composed accept() call.
Why is the parameter declared as Consumer<? super T> instead of Consumer<T>?
This is the PECS rule: producer extends, consumer super. The object of type T is only passed inwards, so any handler that accepts T or a supertype of T will do. Thanks to that you can attach a Consumer<Object> — a generic logger, for instance — to a Consumer<String>. With a rigid Consumer<T> signature that combination would not compile.
Can you modify an object inside a Consumer?
You can change the state of the object that was passed in: a call such as user.setName("Anna") inside the lambda works, because the object itself is mutated. Assigning a new reference to the lambda parameter is useless — only the local variable changes and the caller never sees it. To replace collection elements use replaceAll() or map() in a stream.
Comments