Generics в Kotlin vs. Generics в JAVA: cходства, различия, особенности
Эта статья про Generics в Kotlin — особенности их использования, сходства и различия с Generics в Java.
public interface List extends Collection {
//...
}
Заранее неизвестно, объекты какого класса будут содержаться в списке, но это определится при его использовании:
List list = new ArrayList<>();
Теперь это не просто список, а список строк. Generics помогают обеспечить типобезопасность: в List можно попытаться положить любой объект, но в List — только String или один из его потомков.
Я разделю рассказ про Generics на две части: собственно Generics и использование Wildcards. Пока речь не заходит про Wildcards, использование Generics в Kotlin мало чем отличается от Java.
Те же generic-классы:
// Java
public class SomeGenericClass {
private T mSomeField;
public void setSomeField(T someData) {
mSomeField = someData;
}
public T getSomeField() {
return mSomeField;
}
}
// Kotlin
class SomeGenericClass {
private var mSomeField: T? = null
fun setSomeField(someData: T?) {
mSomeField = someData
}
fun getSomeField(): T? {
return mSomeField
}
}
Те же generic-методы:
// Java
public K makeSomething(K someData) {
K localData = someData;
//...
return localData;
}
// Kotlin
fun makeSomething(someData : K) : K {
var localData = someData
//...
return localData
}
Generics могут быть дополнительно ограничены и в Java:
// Java
public K makeSomething(K someData) {
K localData = someData;
//...
return localData;
}
И в Kotlin:
// Kotlin
fun makeSomething(someData : K) : K {
var localData = someData
//...
return localData
}
Такие ограничения обозначают, что вместо K может быть использован не любой класс, а только удовлетворяющий условию (в данном случае — Number или класс, его наследующий).
//корректно
makeSomething(1)
//некорректно
makeSomething("string”)
Ограничения могут быть и комплексными, например, показывающими, что передаваемый в метод объект должен наследовать какой-то класс и реализовывать какой-то интерфейс, например:
//Java
public static SomeFragment newInstance(T interactor) {
SomeFragment fragment = new SomeFragment();
fragment.setTargetFragment(interactor, 0);
return fragment;
}
//Kotlin
fun 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 someGenericClassInstance = new SomeGenericClass<>("This is String");
Таким образом, главное, что нужно знать про Generics при переходе с Java на Kotlin — делайте все так же, как делали в Java. Попытки сделать что-то по-новому, «по-котлиновски», скорее всего приведут только к новым сложностям.
Wildcards
Перейдем ко второй части. Wildcards — особый случай, вызывающий больше всего сложностей и в Kotlin, и в Java. Основная проблема Generics — их инвариантность: List не является потомком List. В противном случае могли бы происходить ошибки вида:
//Java
List strs = new ArrayList();
List
Инвариантность Generics позволяет не допустить этого, но, с другой стороны, вносит дополнительные ограничения. Так, при использовании обычных Generics невозможно List передать в метод, ожидающий в качестве параметра List. Во многих случаях удобно иметь такую возможность.
Wildcards позволяют разрешить такое поведение, обозначив, что в данном месте ожидается какой-то параметр типа, но не какой-то конкретный. При этом Wildcards тоже могут быть особым образом ограничены, что разбивает вопрос на 3 части:
- неограниченные Wildcards
- ковариантные Wildcards
- контравариантные Wildcards
Ограниченные «снизу» ковариантные Wildcards используются в случаях, когда ожидается generic-класс от какого-то класса или его потомков. Например:
// Java
public interface Container {
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 {
fun getData() : T
fun putData(data : T)
}
fun hideView (viewContainer : Container) {
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
viewContainer.putData(new View(App.getContext()));
}
В Kotlin ограничения почти такие же. Из-за особенностей типов в Kotlin положить внутрь такого параметра нельзя даже null. Ключевое слово out отлично описывает происходящее.
//Kotlin
fun hideView (viewContainer : Container) {
viewContainer.getData().visibility = View.GONE;
//некорректен даже такой вызов, потому что неизвестно, содержит контейнер View (или какого-то из его потомков) или View?
viewContainer.putData(null)
}
Ограниченные «сверху» контравариантные Wildcards используются для обозначения мест, где ожидается generic-класс от какого-то класса или его предков. Традиционный пример контравариантных Wildcards — компараторы:
// Java
public static void sort(List list, Comparator super T> comparator) {
//...
}
Допустим, в метод в качестве первого параметра передан List, а в качестве второго — компаратор от любого из предков String, например, CharSequence: Comparator. Так как String является потомком CharSequence, любые поля и методы, необходимые компаратору, будут и у объектов класса String:
//Java
class LengthComparator implements Comparator {
@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 sort(list : List, comparator: Comparator) {
//…
}
Есть у контравариантных 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 {
fun compare(lhs: T, rhs: T): Int
override fun equals(other : Any?): Boolean
}
И тогда его использование будет выглядеть следующим образом:
// Kotlin
fun sort(list : List, comparator: Comparator) {
//…
}
При этом ограничения на использование параметра comparator будут такие же, как если бы было указано не на стороне декларации интерфейса, а на стороне его использования.
Аналогичным образом при декларации класса может быть определено и ковариантное поведение.
Последний не разобранный случай — Wildcards без ограничений. Такие, очевидно, используются в случаях, когда подходит generic от любого класса:
// Java
public interface Container {
T getData();
void putData(T data);
}
static boolean isNull(Container> container) {
return container.getData() != null;
}
В Kotlin аналогичный механизм называется star-projection. Во всех тривиальных случаях единственное его отличие от неограниченных Wildcards в Java — использование символа »*» вместо »?»:
// Kotlin
interface Container {
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