В таких языках программирования, как C#, Kotlin, Groovy, Scala есть возможность расширять класс путем добавления нового функционала, при этом не требуется наследование или изменение самого изначального класса. Это реализовано с помощью специальных выражений, называемых расширения. Java, в отличие от этих языков, не имеет такой возможности из коробки и даже не планирует в ближайших релизах. Благодаря Lombok это стало возможным. Методы расширения были реализованы в Lombok еще 8 лет назад (с поддержкой Eclipse), но для многих все упиралось в поддержку плагином в IDEA (код компилировался, но IDE его не распознавала как валидный). Lombok плагин теперь предустановлен в IDEA 2021.1 EAP, и теперь он поддерживает методы расширения lombok (спасибо Anna Kozlova, Tagir Valeev, NekoCaffeine и Michail Plushnikov).
Рассмотрим пример классического статического импорта:


import static org.apache.commons.lang3.StringUtils.capitalize;

public class ExtensionMethods {
    public static void main(String[] args) {
        String str = "test";
        String capitalized = capitalize(str);
        // "Test"
        System.out.println(capitalized);
    }
}

при переходе на метод расширения код станет выглядеть так:


import lombok.experimental.ExtensionMethod;
import org.apache.commons.lang3.StringUtils;

@ExtensionMethod(StringUtils.class)
public class ExtensionMethods {
    public static void main(String[] args) {
        String str = "test";
        String capitalized = str.capitalize();
        // "Test"
        System.out.println(capitalized);
    }
}

Заворачивания аргументов в скобки заменяются на цепочки вызовов, т.е. код вида call3(call2(call1(arg))) превратится в


arg.call1()
    .call2()
    .call3();

Во многих ситуациях это может облегчить чтение кода, особенно когда цепочки длинные, здесь есть некая аналогия со Stream Api или преобразования значения java.util.Optional.
Фактически это просто синтаксический сахар. Код при компи��яции будет заменен на вызов статического метода. Первый аргумент статического метода и станет объектом "this".


null-значения


В отличие от обычных instance-методов, методы расширения могут работать и с null-значениями, т.е. подобный вызов вполне допустим:


import org.apache.commons.lang3.StringUtils;

@ExtensionMethod(StringUtils.class)
public class MethodExtensions {
    public static void main(String[] args) throws Exception {
        String nullStr = null;
        // "isEmpty=true"
        System.out.println("isEmpty=" + nullStr.trimToEmpty().isEmpty());
    }
}

Еще примеры


Можно добавить в проект на JDK 8 метод, который появится только в JDK 11:


@UtilityClass
public class CollectionExtensions {
    public static <T> T[] toArray(Collection<T> list, IntFunction<T[]> generator) {
        return list.stream().toArray(generator);
    }
}

@ExtensionMethod(CollectionExtensions.class)
public class MethodExtensions {
    public static void main(String[] args) throws Exception {
        List<Integer> list = Arrays.asList(1, 2, 3);
        // toArray(IntFunction<T[]>) добавлен только в Java 11
        Integer[] array = list.toArray(Integer[]::new);
        // "[1, 2, 3]"
        System.out.println(Arrays.toString(array));
    }
}

Или добавить более лаконичный вызов Stream.collect(toList()):


@UtilityClass
public class StreamExtensions {
    public static <T> List<T> toList(Stream<T> stream) {
        return stream.collect(Collectors.toList());
    }
}

@ExtensionMethod(StreamExtensions.class)
public class MethodExtensions {
    public static void main(String[] args) throws Exception {
        List<Integer> list = Arrays.asList(3, 1, 2);
        List<Integer> sorted = list.stream()
                .sorted()
                .toList();

        // "[1, 2, 3]"
        System.out.println(sorted);
    }
}

Настройка проекта


  • Установите последнюю версию IDEA EAP, важно: EAP версии не стабильны, зато бесплатны. Плагин доступен и в Ultimate, и в Community Edition.
  • Добавьте зависимость lombok: maven

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.16</version>
    <scope>provided</scope>
</dependency>

либо для gradle:


compileOnly 'org.projectlombok:lombok:1.18.16'
annotationProcessor 'org.projectlombok:lombok:1.18.16'
testCompileOnly 'org.projectlombok:lombok:1.18.16'
testAnnotationProcessor 'org.projectlombok:lombok:1.18.16'

  • Убедитесь, что включена опция проекта Build, Execution, Deployment -> Compiler -> Annotations processor -> Enable annotation processing
  • Добавьте аннотацию @ExtensionMethod на класс (откуда будет вызов), перечисляя все утилитные классы, из которых необходимо импортировать вызовы.
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Что думаете про методы расширения?
25.18%Уже использую в Kotlin, Groovy, Scala, etc.35
28.78%Уже использую в Lombok / обязательно попробую40
12.95%Использую Lombok, но идея расширений не нравится18
32.37%Lombok — зло45
0.72%Другое (напишите в комментарии)1
Проголосовали 139 пользователей. Воздержались 36 пользователей.