
Эта статья про Generics в Kotlin — особенности их использования, сходства и различия с Generics в Java.
В двух словах про Generics
Если коротко, то Generics — это способ сказать, что класс, интерфейс или метод будут работать не с каким-то конкретным типом, а просто с каким-то. С каким именно будет определено из контекста. Например:
Заранее неизвестно, объекты какого класса будут содержаться в списке, но это определится при его использовании:
Теперь это не просто список, а список строк. Generics помогают обеспечить типобезопасность: в List можно попытаться положить любой объект, но в List<String> — только String или один из его потомков.
public interface List<E> extends Collection<E> { //... }
Заранее неизвестно, объекты какого класса будут содержаться в списке, но это определится при его использовании:
List<String> list = new ArrayList<>();
Теперь это не просто список, а список строк. Generics помогают обеспечить типобезопасность: в List можно попытаться положить любой объект, но в List<String> — только String или один из его потомков.
Я разделю рассказ про Generics на две части: собственно Generics и использование Wildcards. Пока речь не заходит про Wildcards, использование Generics в Kotlin мало чем отличается от Java.
Те же generic-классы:
// Java public class SomeGenericClass <T> { private T mSomeField; public void setSomeField(T someData) { mSomeField = someData; } public T getSomeField() { return mSomeField; } }
// Kotlin class SomeGenericClass <T> { private var mSomeField: T? = null fun setSomeField(someData: T?) { mSomeField = someData } fun getSomeField(): T? { return mSomeField } }
Те же generic-методы:
// Java public <K> K makeSomething(K someData) { K localData = someData; //... return localData; }
// Kotlin fun <K> makeSomething(someData : K) : K { var localData = someData //... return localData }
Generics могут быть дополнительно ограничены и в Java:
// Java public <K extends Number> K makeSomething(K someData) { K localData = someData; //... return localData; }
И в Kotlin:
// Kotlin fun <K : Number> makeSomething(someData : K) : K { var localData = someData //... return localData }
Такие ограничения обозначают, что вместо K может быть использован не любой класс, а только удовлетворяющий условию (в данном случае — Number или класс, его наследующий).
//корректно makeSomething(1) //некорректно makeSomething(“string”)
Ограничения могут быть и комплексными, например, показывающими, что передаваемый в метод объект должен наследовать какой-то класс и реализовывать какой-то интерфейс, например:
//Java public static <T extends Interaction & Fragment> SomeFragment newInstance(T interactor) { SomeFragment fragment = new SomeFragment(); fragment.setTargetFragment(interactor, 0); return fragment; }
//Kotlin fun <T> newInstance(interactor : T) : SomeFragment where T : Interaction, T : Fragment { val fragment = SomeFragment() fragment.setTargetFragment(interactor, 0) return fragment }
Обратите внимание, что в Kotlin для комплексных ограничений используется другой синтаксис: добавилось немного синтаксического сахара. Можно не указывать параметр типа, если он может быть определен по контексту:
// Kotlin val someGenericClassInstance = SomeGenericClass("This is String")
А в Java придется:
// Java SomeGenericClass<String> someGenericClassInstance = new SomeGenericClass<>("This is String");
Таким образом, главное, что нужно знать про Generics при переходе с Java на Kotlin — делайте все так же, как делали в Java. Попытки сделать что-то по-новому, “по-котлиновски”, скорее всего приведут только к новым сложностям.
Wildcards
Перейдем ко второй части. Wildcards — особый случай, вызывающий больше всего сложностей и в Kotlin, и в Java. Основная проблема Generics — их инвариантность: List<String> не является потомком List<Object>. В противном случае могли бы происходить ошибки вида:
//Java List<String> strs = new ArrayList<String>(); List<Object> objs = strs; //objs - List<Object>, так что туда можно положить Integer objs.add(1); //но strs - List<String>, так что get() должен возвращать String String s = strs.get(0);
Инвариантность Generics позволяет не допустить этого, но, с другой стороны, вносит дополнительные ограничения. Так, при использовании обычных Generics невозможно List<String> передать в метод, ожидающий в качестве параметра List<Object>. Во многих случаях удобно иметь такую возможность.
Wildcards позволяют разрешить такое поведение, обозначив, что в данном месте ожидается какой-то параметр типа, но не какой-то конкретный. При этом Wildcards тоже могут быть особым образом ограничены, что разбивает вопрос на 3 части:
- неограниченные Wildcards
- ковариантные Wildcards
- контравариантные Wildcards
Ограниченные “снизу” ковариантные Wildcards используются в случаях, когда ожидается generic-класс от какого-то класса или его потомков. Например:
// Java public interface Container<T> { T getData(); void putData(T data); } static void hideView(Container<? extends View> viewContainer) { viewContainer.getData().setVisibility(View.GONE); }
Здесь метод hideView ожидает объект, реализующий интерфейс Container, но не любой и не только содержащий View, а содержащий View или какой-либо другой класс, наследующий View. Это и называется ковариантностью.
В Kotlin такое поведение может быть реализовано похожим образом:
// Kotlin interface Container<T> { fun getData() : T fun putData(data : T) } fun hideView (viewContainer : Container<out View>) { viewContainer.getData().visibility = View.GONE; }
При этом на использование параметра, объявленного как Wildcards, накладываются дополнительные ограничения.
В Java ковариантные Wildcards можно использовать для получения данных без ограничений, при этом возвращаться данные будут в соответствии с обозначенной границей (в примере выше getData() вернет View, даже если на самом деле контейнер содержал TextView). Но положить в него нельзя ничего, кроме null, иначе бы это вызвало те же проблемы, что возникли бы у Generics, не будь они инвариантными.
//Java static void hideView(Container<? extends View> viewContainer) { //getData() вернет View даже если на самом деле в контейнере содержится, например, TextView viewContainer.getData().setVisibility(View.GONE); //положить внутрь можно только null, так как неизвестно, какой именно класс должен содержаться в этом контейнере //такой вызов корректен viewContainer.putData(null); //а такой - некорректен, потому что на самом деле это может быть, например, Container<ImageView> viewContainer.putData(new View(App.getContext())); }
В Kotlin ограничения почти такие же. Из-за особенностей типов в Kotlin положить внутрь такого параметра нельзя даже null. Ключевое слово out отлично описывает происходящее.
//Kotlin fun hideView (viewContainer : Container<out View>) { viewContainer.getData().visibility = View.GONE; //некорректен даже такой вызов, потому что неизвестно, содержит контейнер View (или какого-то из его потомков) или View? viewContainer.putData(null) }
Ограниченные “сверху” контравариантные Wildcards используются для обозначения мест, где ожидается generic-класс от какого-то класса или его предков. Традиционный пример контравариантных Wildcards — компараторы:
// Java public static <T> void sort(List<T> list, Comparator<? super T> comparator) { //... }
Допустим, в метод в качестве первого параметра передан List<String>, а в качестве второго — компаратор от любого из предков String, например, CharSequence: Comparator<CharSequence>. Так как String является потомком CharSequence, любые поля и методы, необходимые компаратору, будут и у объектов класса String:
//Java class LengthComparator implements Comparator<CharSequence> { @Override public int compare(CharSequence obj1, CharSequence obj2) { //с объектами класса String в качестве параметров тоже будет работать if (obj1.length() == obj2.length()) return 0; if (obj1.length() < obj2.length()) return -1; return 1; } }
В Kotlin реализация аналогична:
// Kotlin fun <T> sort(list : List<T>, comparator: Comparator<in T>) { //… }
Есть у контравариантных Wildcards и вполне ожидаемые ограничения: считать значение из таких Wildcards можно, но возвращаться будет Object в Java и Any? в Kotlin.
На этом этапе повторюсь: переходя с Java на Kotlin, следует делать все так же, как и делали. Хоть в официальной документации про Wildcards и написано “Kotlin doesn’t have any” (“В Kotlin их нет”), предлагаемый взамен механизм type projections (рассмотренный выше) во всех привычных случаях работает аналогично, никакие новые подходы не требуются.
Но не обошлось и без новшеств. Кроме type projections, полностью аналогичного привычной модели Wildcards в Java, Kotlin предлагает еще один механизм — declaration-side variance.
В случае если заранее известно, что generic-класс будет использоваться только как ковариантный (или только как контравариантный), указать это можно во время написания generic-класса, а не в момент его использования. В качестве примера опять же подойдут компараторы. Переписанный на Kotlin, java.util.Comparator мог бы выглядеть так:
// Kotlin interface Comparator<in T> { fun compare(lhs: T, rhs: T): Int override fun equals(other : Any?): Boolean }
И тогда его использование будет выглядеть следующим образом:
// Kotlin fun <T> sort(list : List<T>, comparator: Comparator<T>) { //… }
При этом ограничения на использование параметра comparator будут такие же, как если бы <in T> было указано не на стороне декларации интерфейса, а на стороне его использования.
Аналогичным образом при декларации класса может быть определено и ковариантное поведение.
Последний не разобранный случай — Wildcards без ограничений. Такие, очевидно, используются в случаях, когда подходит generic от любого класса:
// Java public interface Container<T> { T getData(); void putData(T data); } static boolean isNull(Container<?> container) { return container.getData() != null; }
В Kotlin аналогичный механизм называется star-projection. Во всех тривиальных случаях единственное его отличие от неограниченных Wildcards в Java — использование символа “*” вместо “?”:
// Kotlin interface Container<T> { fun getData() : T fun putData(data : T); } fun isNull(container : Container<*>) : Boolean { return container.getData() != null; }
В Java неограниченные Wildcards используются по следующим правилам: положить в них можно только null, считывается всегда Object. В Kotlin положить внутрь нельзя ничего, а считывается объект класса Any?..
При совместном использовании declaration-side variance и star-projection нужно учитывать, что ограничения суммируются. Так, при использовании контравариантного declaration-side variance (позволяющего положить внутрь что угодно, но считать только Any?) вместе со star-projection положить внутрь нельзя будет ничего (ограничение star-projection), а возвращаться будет все тот же Any? (в этом их ограничения совпадают).
Прочитать про Generics в целом можно по ссылкам:
www.oracle.com/technetwork/articles/java/juneau-generics-2255374.html
www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html

