Back to Blog
Java

Java UnaryOperator: Usage and Examples

java unaryoperator: Learn how to use Java's UnaryOperator functional interface with lambdas, method references, and composition for clean, reusable transformations.

Functional InterfacesLambda ExpressionsMethod ReferencesJava StreamsJava 8
Diagram showing a unary operator transforming an input value to an output value in Java

The java unaryoperator is a functional interface that represents an operation on a single operand that returns a result of the same type. It is a specialization of Function<T, R> where both the input and output are the same type T. This makes it a natural fit for transformations that do not change the type of the value being processed.

Understanding the UnaryOperator Functional Interface

UnaryOperator<T> is defined in the java.util.function package. Its single abstract method is T apply(T t). Because it extends Function<T, T>, it inherits the default methods andThen and compose, which are essential for building transformation pipelines.

The interface is primarily used in contexts where a function accepts one argument and produces a result of the same type. Common examples include string manipulation, numeric calculations, and object state updates. Using UnaryOperator instead of a generic Function makes the code more readable and communicates the intent clearly: the operation is a unary transformation that preserves the type.

Using UnaryOperator with Lambda Expressions

The most straightforward way to create a UnaryOperator is with a lambda expression. The lambda must take one argument and return a value of the same type.

UnaryOperator<Integer> increment = n -> n + 1; System.out.println(increment.apply(5)); // 6

Here, the lambda n -> n + 1 matches the apply method signature. The type of n is inferred as Integer because the operator is declared with that generic type.

You can also use a block lambda when the operation requires multiple statements:

UnaryOperator<String> trimAndLower = s -> { String trimmed = s.trim(); return trimmed.toLowerCase(); };

Block lambdas are useful when the transformation logic is more complex, but they reduce the conciseness that lambdas are known for. For simple operations, an expression lambda is preferable.

Composing UnaryOperator Operations

The andThen and compose methods allow you to chain multiple UnaryOperator instances into a single pipeline. andThen applies the current operator first and then the argument operator. compose does the reverse: it applies the argument operator first and then the current one.

UnaryOperator<String> addExclamation = s -> s + "!"; UnaryOperator<String> toUpper = String::toUpperCase; UnaryOperator<String> shout = toUpper.andThen(addExclamation); System.out.println(shout.apply("hello")); // HELLO!

In this example, toUpper is applied first, then addExclamation. If you wanted the exclamation added before uppercasing, you would use compose:

UnaryOperator<String> shoutComposed = toUpper.compose(addExclamation); System.out.println(shoutComposed.apply("hello")); // HELLO!

Note that compose is less commonly used with UnaryOperator because the order is often intuitive with andThen. However, understanding both is important when you are integrating a UnaryOperator into a larger Function chain.

Method References as UnaryOperator

Method references provide a compact syntax for UnaryOperator when the operation already exists as a method. For example, String::toLowerCase is a valid UnaryOperator<String> because it takes a String and returns a String.

UnaryOperator<String> lower = String::toLowerCase; UnaryOperator<Integer> abs = Math::abs;

Method references work for both static and instance methods. The key requirement is that the method signature matches apply—one argument of type T and a return type of T. This makes method references ideal for delegating to well-known utility methods without writing a lambda.

Common Use Cases in Streams and Collections

UnaryOperator appears frequently in the Stream API and in collection operations. The Stream.map method accepts a Function, and a UnaryOperator can be passed directly because it is a subtype. For example, transforming every element of a stream:

List<String> names = List.of("alice", "bob", "carol"); List<String> upperNames = names.stream() .map(String::toUpperCase) .collect(Collectors.toList());

Here, String::toUpperCase is a UnaryOperator<String> that fits the map parameter.

Another common use is List.replaceAll, which takes a UnaryOperator to replace each element in place:

List<Integer> numbers = new ArrayList<>(List.of(1, 2, 3)); numbers.replaceAll(n -> n * 2); System.out.println(numbers); // [2, 4, 6]

The replaceAll method is defined on List and directly accepts a UnaryOperator, making it a natural fit for in-place transformations.

Performance and Maintainability Considerations

When using UnaryOperator, be mindful of object allocation. Each lambda expression creates a new instance of the functional interface, which can add overhead in hot loops. If the same operation is applied many times, consider storing the UnaryOperator in a static final field to reuse it.

private static final UnaryOperator<String> TRIM = String::trim;

Reusing the instance avoids repeated allocation and can improve performance in high-frequency code paths. However, modern JVMs often optimize lambda allocation, so the impact is usually negligible unless the operation is extremely hot.

From a maintainability perspective, UnaryOperator makes code more declarative. It allows you to separate transformation logic from the control flow, making it easier to test and reuse. On the other hand, overusing it for trivial operations can reduce readability. A simple for loop with a direct assignment is often clearer than a lambda chain for a one-off transformation.

When Not to Use UnaryOperator

UnaryOperator is not appropriate when the input and output types differ. For those cases, use Function<T, R>. For example, a function that converts a String to an Integer should be a Function<String, Integer>, not a UnaryOperator.

Also avoid UnaryOperator when the operation has side effects or depends on external state. The interface is designed for pure functions—operations that return a new value without modifying the input or any shared state. If you need to mutate an object, a Consumer or a plain method is more suitable.

Finally, consider whether a method reference or a direct method call is clearer. If the transformation is a one-off and not reused, writing s -> s.trim() inline might be simpler than defining a named UnaryOperator. The choice should be guided by how often the operation is reused and how much it contributes to the overall readability of the code.

java unaryoperator: Practical Usage and Code Examples | RYUSLOG DEV