Статьи

Беспараметрический универсальный метод Антипаттерн

Недавно в Stack Overflow и Reddit был опубликован очень интересный вопрос о дженериках Java. Рассмотрим следующий метод:

1
2
3
<X extends CharSequence> X getCharSequence() {
    return (X) "hello";
}

Несмотря на то, что небезопасное приведение выглядит немного странным, и вы можете догадаться, что здесь что-то не так, вы все равно можете пойти дальше и скомпилировать следующее назначение в Java 8:

1
Integer x = getCharSequence();

Это, очевидно, неправильно, потому что Integer является final , и, таким образом, нет никакого возможного подтипа Integer который также может реализовывать CharSequence . Тем не менее, система универсальных типов Java не заботится о том, чтобы классы были final финальными, и, таким образом, она выводит тип пересечения Integer & CharSequence для X до передачи этого типа обратно в Integer . С точки зрения компилятора все в порядке. Во время выполнения: ClassCastException

Хотя вышесказанное кажется «явно сомнительным», настоящая проблема заключается в другом.

(Почти) никогда не корректно, чтобы метод был универсальным только для возвращаемого типа

Есть исключения из этого правила. Эти исключения являются такими методами:

1
2
3
class Collections {
    public static <T> List<T> emptyList() { ... }
}

Этот метод не имеет параметров, и все же он возвращает общий List<T> . Почему это может гарантировать правильность, независимо от конкретного вывода для <T> ? Из-за своей семантики. Независимо от того, ищете ли вы пустой List<String> или пустой List<Integer> , можно обеспечить одинаковую реализацию для любого из этих T, несмотря на стирание, из-за семантики пустоты (и неизменяемости!).

Другое исключение — это компоновщики, такие как javax.persistence.criteria.CriteriaBuilder.Coalesce< , который создается из общего метода без параметров:

1
<T> Coalesce<T> coalesce();

Методы Builder — это методы, которые создают изначально пустые объекты. Пустота является ключевой здесь.

Однако для большинства других методов это не так, включая приведенный выше getCharSequence() . Единственное гарантированное правильное возвращаемое значение для этого метода — null

1
2
3
<X extends CharSequence> X getCharSequence() {
    return null;
}

… Потому что в Java null — это значение, которое может быть назначено (и приведено) любому ссылочному типу. Но это не намерение автора этого метода.

Думайте с точки зрения функционального программирования

Методы являются функциями (в основном) и, как таковые, как ожидается, не будут иметь побочных эффектов. Функция без параметров всегда должна возвращать одно и то же возвращаемое значение. Так же, как и emptyList() .

Но на самом деле эти методы не беспараметрически. У них есть параметр типа <T> или <X extendds CharSequence> . Опять же, из-за стирания универсального типа этот параметр «на самом деле не имеет значения» в Java, потому что, если не считать его реализацию, его нельзя исследовать внутри метода / функции.

Итак, запомните это:

(Почти) никогда не корректно, чтобы метод был универсальным только для возвращаемого типа

Наиболее важно, если ваш сценарий использования состоит в том, чтобы просто избежать пре-Java 5 приведения, например:

1
Integer integer = (Integer) getCharSequence();

Хотите найти оскорбительные методы в своем коде?

Я использую Guava для сканирования пути к классам, вы можете использовать что-то еще. Этот фрагмент создаст все общие методы без параметров в вашем пути к классам:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.lang.reflect.Method;
import java.util.Comparator;
import java.util.stream.Stream;
  
import com.google.common.reflect.ClassPath;
  
public class Scanner {
  
    public static void main(String[] args) throws Exception {
        ClassPath
           .from(Thread.currentThread().getContextClassLoader())
           .getTopLevelClasses()
           .stream()
           .filter(info -> !info.getPackageName().startsWith("slick")
                        && !info.getPackageName().startsWith("scala"))
           .flatMap(info -> {
               try {
                   return Stream.of(info.load());
               }
               catch (Throwable ignore) {
                   return Stream.empty();
               }
           })
           .flatMap(c -> {
               try {
                   return Stream.of(c.getMethods());
               }
               catch (Throwable ignore) {
                   return Stream.<Method> of();
               }
           })
           .filter(m -> m.getTypeParameters().length > 0 && m.getParameterCount() == 0)
           .sorted(Comparator.comparing(Method::toString))
           .map(Method::toGenericString)
           .forEach(System.out::println);
    }
}
Ссылка: Обобщенный метод без параметров Antipattern от нашего партнера JCG Лукаса Эдера из блога JAVA, SQL и JOOQ .