Generics в Kotlin vs. Generics в JAVA: cходства, различия, особенности

311e39e6f7c148118698f6c251141ec4.png
Эта статья про Generics в Kotlin — особенности их использования, сходства и различия с Generics в Java.

В двух словах про Generics
Если коротко, то Generics — это способ сказать, что класс, интерфейс или метод будут работать не с каким-то конкретным типом, а просто с каким-то. С каким именно будет определено из контекста. Например:
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 objs = strs;
//objs - List, так что туда можно положить Integer
objs.add(1);
//но strs - List, так что get() должен возвращать String
String s = strs.get(0);


Инвариантность 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 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 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 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

© Habrahabr.ru