Lambda ·
‹ Previous Next ›
⏱ 5 min read Modified: 2026-09-07

Интерфейс UnaryOperator в Java

Такой код не компилируется, хотя выглядит абсолютно логично:

Function<String, String> trim = String::trim;

List<String> names = new ArrayList<>(List.of("  Иван  ", "  Пётр  "));
names.replaceAll(trim); // error: incompatible types: Function<String,String>
                        // cannot be converted to UnaryOperator<String>

Метод List.replaceAll() принимает не Function, а UnaryOperator. Оба описывают преобразование «строка в строку», но с точки зрения компилятора это разные типы. Разберёмся, зачем в JDK понадобился отдельный интерфейс и где он реально применяется.

Что такое UnaryOperator

UnaryOperator — это встроенный функциональный интерфейс Java из пакета java.util.function, добавленный в Java SE 8. Он описывает операцию над одним операндом, результат которой имеет тот же тип, что и операнд. Интерфейс UnaryOperator<T> расширяет Function<T, T> — то есть это частный случай функции, у которой тип аргумента и тип возвращаемого значения совпадают.

@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {

    static <T> UnaryOperator<T> identity() {
        return t -> t;
    }
}

Функциональный дескриптор — так называют сигнатуру единственного абстрактного метода интерфейса — здесь выглядит предельно просто: T -> T.

Обратите внимание: в теле интерфейса нет ни одного абстрактного метода. Единственный абстрактный метод apply(T t) унаследован от Function, а параметризация Function<T, T> «схлопывает» его сигнатуру до T apply(T t). Именно поэтому UnaryOperator остаётся корректным функциональным интерфейсом и помечен аннотацией @FunctionalInterface.

Простейший пример — приведение строки к верхнему регистру:

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

        // то же самое через ссылку на метод
        UnaryOperator<String> toUpperRef = String::toUpperCase;
        System.out.println(toUpperRef.apply("java")); // JAVA
    }
}

Методы интерфейса UnaryOperator

Собственный метод у UnaryOperator ровно один — статический identity(). Всё остальное приходит по наследству от Function.

Метод Сигнатура Откуда Что делает
apply T apply(T t) Наследуется от Function Единственный абстрактный метод; его и реализует лямбда
identity static <T> UnaryOperator<T> identity() Объявлен в UnaryOperator Возвращает оператор, отдающий аргумент без изменений
andThen <V> Function<T, V> andThen(Function<? super T, ? extends V> after) Наследуется от Function Сначала текущая операция, затем after
compose <V> Function<V, T> compose(Function<? super V, ? extends T> before) Наследуется от Function Сначала before, затем текущая операция

Важно

Интерфейс UnaryOperator не переопределяет andThen() и compose(), поэтому они возвращают Function, а не UnaryOperator. Строка UnaryOperator<String> both = trim.andThen(toUpper); не скомпилируется — нужен либо тип Function<String, String>, либо явная лямбда s -> toUpper.apply(trim.apply(s)).

Примеры использования

Массовое изменение элементов списка: List.replaceAll()

Это самое частое место, где UnaryOperator встречается в реальном коде. Метод заменяет каждый элемент списка результатом применения оператора — прямо на месте, без создания новой коллекции.

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("  иван  ", " пётр", "мария "));

        UnaryOperator<String> normalize = s -> s.trim().toUpperCase();
        names.replaceAll(normalize);

        System.out.println(names); // [ИВАН, ПЁТР, МАРИЯ]
    }
}

Преобразование потока: Stream.map()

Stream.map() объявлен через Function, но UnaryOperator<T> является его подтипом, поэтому передаётся туда без каких-либо приведений.

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]

Генерация последовательности: Stream.iterate()

Метод Stream.iterate(T seed, UnaryOperator<T> f) строит поток, каждый раз применяя оператор к предыдущему значению. Именно здесь совпадение типов принципиально: результат шага становится аргументом следующего шага.

// бесконечный поток, ограничиваем через limit
Stream.iterate(1, n -> n * 2)
      .limit(6)
      .forEach(n -> System.out.print(n + " ")); // 1 2 4 8 16 32

// начиная с Java 9 есть перегрузка с условием остановки
Stream.iterate(1, n -> n <= 32, n -> n * 2)
      .forEach(n -> System.out.print(n + " ")); // 1 2 4 8 16 32

Атомарное обновление значения

Классы из java.util.concurrent.atomic используют UnaryOperator для потокобезопасного пересчёта текущего значения.

AtomicReference<String> ref = new AtomicReference<>("java");

ref.updateAndGet(s -> s + " 21");
System.out.println(ref.get()); // java 21

UnaryOperator.identity() как значение по умолчанию

Когда преобразование опционально, удобно подставить «пустой» оператор вместо проверок на null в каждой ветке.

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;
}

Обратите внимание

Список, созданный через List.of(...) или Collections.unmodifiableList(...), неизменяемый: вызов replaceAll() на нём бросит UnsupportedOperationException. Оборачивайте такой список в new ArrayList<>(...) или используйте stream().map(...) — этот вариант возвращает новую коллекцию и ничего не мутирует.

UnaryOperator, Function и BinaryOperator: в чём разница

Все эти интерфейсы описывают вычисление значения, а различаются количеством аргументов и соотношением типов.

Интерфейс Дескриптор Абстрактный метод Когда применять
Function<T, R> T -> R R apply(T t) Тип результата отличается от типа аргумента
UnaryOperator<T> T -> T T apply(T t) (унаследован) Один аргумент, результат того же типа
BinaryOperator<T> (T, T) -> T T apply(T a, T b) (унаследован) Два аргумента одного типа и такой же результат
BiFunction<T, U, R> (T, U) -> R R apply(T t, U u) Два аргумента разных типов, произвольный результат
IntUnaryOperator int -> int int applyAsInt(int v) Работа с int без упаковки в Integer

Главный практический вывод: UnaryOperator<T> можно передать всюду, где ожидается Function<T, T>, но не наоборот. Наследование одностороннее — UnaryOperator является подтипом Function, поэтому обычную функцию нельзя подставить туда, где по контракту требуется оператор.

Примитивные варианты: IntUnaryOperator и другие

Для примитивов в java.util.function есть три отдельных интерфейса: IntUnaryOperator, LongUnaryOperator и DoubleUnaryOperator. Они не расширяют UnaryOperator и не являются обобщёнными.

IntUnaryOperator increment = x -> x + 1;
System.out.println(increment.applyAsInt(41)); // 42

// у примитивных вариантов andThen и compose возвращают тот же тип
IntUnaryOperator doubler = x -> x * 2;
IntUnaryOperator doubleThenIncrement = doubler.andThen(increment);
System.out.println(doubleThenIncrement.applyAsInt(10)); // 21

// атомарный счётчик
AtomicInteger counter = new AtomicInteger(10);
counter.updateAndGet(x -> x * 3); // 30

Совет по производительности

Запись UnaryOperator<Integer> на каждом вызове выполняет распаковку и обратную упаковку значения. В горячем коде и при работе с IntStream используйте IntUnaryOperator — он оперирует примитивом напрямую и не создаёт лишних объектов. В отличие от обобщённой версии, его методы andThen() и compose() возвращают IntUnaryOperator, поэтому цепочки собираются без потери типа.

На чём чаще всего ошибаются

  • Передают Function<T, T> вместо UnaryOperator<T>. Компилятор откажет: наследование идёт только в одну сторону. Либо объявляйте переменную сразу как UnaryOperator, либо оборачивайте ссылкой на метод: list.replaceAll(f::apply).
  • Ожидают, что andThen() вернёт UnaryOperator. Он возвращает Function, потому что UnaryOperator не переопределяет этот метод.
  • Вызывают replaceAll() на неизменяемом списке. Результат — UnsupportedOperationException во время выполнения, а не ошибка компиляции.
  • Меняют состояние объекта внутри оператора. Оператор должен возвращать новое значение, а не мутировать аргумент; побочные эффекты особенно опасны в параллельных потоках.
  • Возвращают null из оператора. После replaceAll() список молча заполнится null-ами, и NullPointerException выстрелит далеко от места настоящей ошибки.
  • Путают с BinaryOperator. «Unary» — один операнд, «binary» — два. В Stream.reduce() нужен BinaryOperator, в Stream.iterate()UnaryOperator.

Часто задаваемые вопросы

Почему нельзя передать Function в List.replaceAll()?

Потому что UnaryOperator<T> наследует Function<T, T>, а не наоборот. Любой UnaryOperator является Function, но не любая функция — оператор, поэтому подстановка вверх по иерархии невозможна. Обходной путь — ссылка на метод: names.replaceAll(trim::apply), она создаёт новый оператор поверх существующей функции.

Что вернёт andThen() у UnaryOperator?

Тип Function<T, V>, а не UnaryOperator<T>. Интерфейс UnaryOperator не переопределяет andThen() и compose(), поэтому композиция двух операторов даёт обычную функцию. Если нужен именно оператор, объедините вызовы в лямбде вручную. Это частый вопрос на собеседовании.

Чем UnaryOperator.identity() отличается от Function.identity()?

Поведение одинаковое — обе возвращают лямбду t -> t, отдающую аргумент без изменений. Отличается только статический тип результата: UnaryOperator.identity() даёт UnaryOperator<T>, а Function.identity()Function<T, T>. Выбирайте ту версию, тип которой ожидает принимающий метод.

Когда использовать IntUnaryOperator вместо UnaryOperator?

Всегда, когда работаете с примитивами int: IntUnaryOperator избегает автоупаковки и не создаёт объектов Integer на каждом вызове. Он обязателен в API примитивных потоков, например в IntStream.map() и AtomicInteger.updateAndGet(). Аналогично устроены LongUnaryOperator и DoubleUnaryOperator.

Video Explanation

Prefer video format? Watch this lesson with examples and explanations.

Comments

Please log in or register to have a possibility to add comment.