Pull to refresh

Методы расширения в Java

Reading time3 min
Views15K


В таких языках программирования, как 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 на класс (откуда будет вызов), перечисляя все утилитные классы, из которых необходимо импортировать вызовы.
Only registered users can participate in poll. Log in, please.
Что думаете про методы расширения?
25.19% Уже использую в Kotlin, Groovy, Scala, etc.34
27.41% Уже использую в Lombok / обязательно попробую37
13.33% Использую Lombok, но идея расширений не нравится18
33.33% Lombok — зло45
0.74% Другое (напишите в комментарии)1
135 users voted. 32 users abstained.
Tags:
Hubs:
+14
Comments42

Articles