UnaryOperator Interface in Java
This line looks perfectly reasonable, yet the compiler rejects it:
UnaryOperator<String> trim = String::trim;
UnaryOperator<String> normalize = trim.andThen(String::toUpperCase);
// error: incompatible types: Function<String,String>
// cannot be converted to UnaryOperator<String> Both operations turn a String into a String, so the composition obviously produces a string-to-string transformation. The compiler disagrees, because UnaryOperator borrows andThen() from its parent interface and never narrows the return type. That single detail explains most of the confusion around this interface — so let's look at what UnaryOperator really is and where it shows up in real code.
What Is UnaryOperator in Java
UnaryOperator is a built-in functional interface from the java.util.function package, added in Java SE 8. It represents an operation on a single operand that produces a result of the same type as the operand. UnaryOperator<T> extends Function<T, T>, so it is simply a specialisation of a function whose input and output types coincide.
@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {
static <T> UnaryOperator<T> identity() {
return t -> t;
}
} Its functional descriptor — the signature of the single abstract method a lambda has to implement — is about as simple as it gets: T -> T.
Notice that the interface body contains no abstract method at all. The single abstract method apply(T t) is inherited from Function, and the Function<T, T> parameterization collapses its signature to T apply(T t). That is exactly why UnaryOperator still qualifies as a functional interface and carries the @FunctionalInterface annotation.
The smallest possible example — uppercasing a string:
import java.util.function.UnaryOperator;
public class UnaryOperatorExample {
public static void main(String[] args) {
UnaryOperator<String> toUpper = s -> s.toUpperCase();
System.out.println(toUpper.apply("examclouds")); // EXAMCLOUDS
// the same thing written as a method reference
UnaryOperator<String> toUpperRef = String::toUpperCase;
System.out.println(toUpperRef.apply("java")); // JAVA
}
} Methods of the UnaryOperator Interface
UnaryOperator declares exactly one member of its own: the static factory identity(). Everything else arrives through inheritance from Function, with R substituted by T.
| Method | Signature | Declared in | What it does |
|---|---|---|---|
apply | T apply(T t) | Inherited from Function | The single abstract method; this is what your lambda implements |
identity | static <T> UnaryOperator<T> identity() | Declared in UnaryOperator | Returns an operator that gives the argument back unchanged |
andThen | <V> Function<T, V> andThen(Function<? super T, ? extends V> after) | Inherited from Function | Runs this operation first, then after |
compose | <V> Function<V, T> compose(Function<? super V, ? extends T> before) | Inherited from Function | Runs before first, then this operation |
Important
Because UnaryOperator does not override andThen() and compose(), both of them return a Function, not a UnaryOperator. Either store the result in a Function<String, String> variable, or build the composition by hand: UnaryOperator<String> normalize = s -> trim.apply(s).toUpperCase();.
UnaryOperator Examples in Practice
Updating every element of a list: List.replaceAll()
This is the place where most developers meet UnaryOperator for the first time. The method replaces each element with the result of applying the operator — in place, without allocating a new collection.
import java.util.ArrayList;
import java.util.List;
import java.util.function.UnaryOperator;
public class ReplaceAllExample {
public static void main(String[] args) {
List<String> names = new ArrayList<>(List.of(" ann ", " bob", "cody "));
UnaryOperator<String> normalize = s -> s.trim().toUpperCase();
names.replaceAll(normalize);
System.out.println(names); // [ANN, BOB, CODY]
}
} Note that replaceAll() insists on a UnaryOperator. A variable declared as Function<String, String> will not compile there, even though the lambda behind it is identical — see the comparison section below.
Transforming a stream: Stream.map()
Stream.map() is declared in terms of Function, and since UnaryOperator<T> is a subtype of Function<T, T>, an operator can be handed over with no cast at all.
UnaryOperator<Integer> square = n -> n * n;
List<Integer> squares = Stream.of(1, 2, 3, 4)
.map(square)
.toList();
System.out.println(squares); // [1, 4, 9, 16] Generating a sequence: Stream.iterate()
Stream.iterate(T seed, UnaryOperator<T> f) builds a stream by feeding each value back into the operator. This is where the matching input and output types are not a stylistic choice but a hard requirement: the result of one step becomes the argument of the next.
// infinite stream, bounded with limit()
Stream.iterate(1, n -> n * 2)
.limit(6)
.forEach(n -> System.out.print(n + " ")); // 1 2 4 8 16 32
// Java 9 added an overload with a stop condition
Stream.iterate(1, n -> n <= 32, n -> n * 2)
.forEach(n -> System.out.print(n + " ")); // 1 2 4 8 16 32 Atomic updates
Classes from java.util.concurrent.atomic accept a UnaryOperator to recompute the current value in a thread-safe way. The operator may be invoked more than once if another thread wins the race, so it must stay free of side effects.
AtomicReference<String> ref = new AtomicReference<>("java");
ref.updateAndGet(s -> s + " 21");
System.out.println(ref.get()); // java 21 UnaryOperator.identity() as a default value
When the transformation is optional, an identity operator replaces null checks scattered across every branch.
public static List<String> format(List<String> source, UnaryOperator<String> formatter) {
UnaryOperator<String> safe = (formatter == null) ? UnaryOperator.identity() : formatter;
List<String> copy = new ArrayList<>(source);
copy.replaceAll(safe);
return copy;
} Watch out
A list created with List.of(...) or Collections.unmodifiableList(...) is immutable: calling replaceAll() on it throws UnsupportedOperationException at runtime. Wrap it in new ArrayList<>(...) first, or use stream().map(...), which returns a new collection and mutates nothing.
UnaryOperator vs Function vs BinaryOperator
All of these interfaces describe a computation that produces a value; they differ in the number of arguments and in how the types relate to each other.
| Interface | Descriptor | Abstract method | When to use it |
|---|---|---|---|
Function<T, R> | T -> R | R apply(T t) | The result type differs from the argument type |
UnaryOperator<T> | T -> T | T apply(T t) (inherited) | One argument, result of the same type |
BinaryOperator<T> | (T, T) -> T | T apply(T a, T b) (inherited) | Two arguments of one type and the same result type |
BiFunction<T, U, R> | (T, U) -> R | R apply(T t, U u) | Two arguments of different types, any result type |
IntUnaryOperator | int -> int | int applyAsInt(int v) | Working with int without boxing into Integer |
The practical takeaway: a UnaryOperator<T> can go anywhere a Function<T, T> is expected, but not the other way round. Inheritance runs in one direction only — UnaryOperator is a subtype of Function, so a plain function cannot be substituted where the contract demands an operator.
Function<String, String> trim = String::trim;
List<String> names = new ArrayList<>(List.of(" ann ", " bob"));
names.replaceAll(trim); // does not compile
names.replaceAll(trim::apply); // compiles: a method reference adapts it Primitive Variants: IntUnaryOperator and Friends
java.util.function ships three primitive counterparts: IntUnaryOperator, LongUnaryOperator and DoubleUnaryOperator. They are not generic and they do not extend UnaryOperator — they are separate interfaces with their own abstract methods (applyAsInt, applyAsLong, applyAsDouble).
IntUnaryOperator increment = x -> x + 1;
System.out.println(increment.applyAsInt(41)); // 42
// in the primitive variants, andThen and compose keep the type
IntUnaryOperator doubler = x -> x * 2;
IntUnaryOperator doubleThenIncrement = doubler.andThen(increment);
System.out.println(doubleThenIncrement.applyAsInt(10)); // 21
// primitive stream and atomic counter
System.out.println(IntStream.of(1, 2, 3).map(doubler).sum()); // 12
AtomicInteger counter = new AtomicInteger(10);
counter.updateAndGet(x -> x * 3); // 30 Performance note
A UnaryOperator<Integer> unboxes and re-boxes the value on every single call. In hot code, and everywhere you touch an IntStream, prefer IntUnaryOperator: it works on the primitive directly and allocates nothing. As a bonus, its andThen() and compose() return IntUnaryOperator, so chains keep their type instead of degrading to a generic function.
Where Developers Get Tripped Up
- Passing a
Function<T, T>where aUnaryOperator<T>is required. The compiler refuses, because inheritance goes only one way. Declare the variable asUnaryOperatorfrom the start, or adapt it with a method reference:list.replaceAll(f::apply). - Expecting
andThen()to give back aUnaryOperator. It returns aFunction, sinceUnaryOperatornever overrides that method. - Calling
replaceAll()on an immutable list. The result is anUnsupportedOperationExceptionat runtime, not a compile error. - Mutating the argument instead of returning a new value. An operator should compute a result; side effects are especially dangerous in parallel streams and in
updateAndGet(), where the lambda can be retried. - Returning
nullfrom the operator. AfterreplaceAll()the list quietly fills with nulls, and theNullPointerExceptionsurfaces far away from the real cause. - Confusing it with
BinaryOperator. "Unary" means one operand, "binary" means two:Stream.reduce()wants aBinaryOperator,Stream.iterate()wants aUnaryOperator.
Frequently Asked Questions
Why can't I pass a Function to List.replaceAll()?
Because UnaryOperator<T> extends Function<T, T> and not the reverse. Every UnaryOperator is a Function, but not every function is an operator, so you cannot substitute a supertype where the subtype is required. The quickest fix is a method reference: names.replaceAll(trim::apply), which wraps the existing function in a brand-new operator.
Does UnaryOperator declare its own abstract method?
No, and this catches people out in interviews. The body of UnaryOperator contains only the static identity() method. Its single abstract method, apply(T t), is inherited from Function, and extending Function<T, T> narrows the signature to T apply(T t). That is enough for the interface to remain a valid @FunctionalInterface.
What is the difference between UnaryOperator.identity() and Function.identity()?
The behaviour is the same: both return the lambda t -> t, which hands the argument back untouched. Only the static type of the result differs — UnaryOperator.identity() gives you a UnaryOperator<T>, while Function.identity() gives you a Function<T, T>. Pick whichever matches the type the receiving method expects.
When should I use IntUnaryOperator instead of UnaryOperator?
Whenever you operate on int values. IntUnaryOperator avoids autoboxing and creates no Integer objects per call, and it is mandatory in primitive-stream APIs such as IntStream.map() and AtomicInteger.updateAndGet(). LongUnaryOperator and DoubleUnaryOperator work exactly the same way for long and double.
Comments