Недавно в 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 . |