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

в 10:18, , рубрики: generics, java, kotlin, Блог компании REDMADROBOT, разработка мобильных приложений, Разработка под android

Generics в Kotlin vs. Generics в JAVA: cходства, различия, особенности - 1
Эта статья про Generics в Kotlin — особенности их использования, сходства и различия с Generics в Java.

В двух словах про Generics
Если коротко, то Generics — это способ сказать, что класс, интерфейс или метод будут работать не с каким-то конкретным типом, а просто с каким-то. С каким именно будет определено из контекста. Например:

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

Автор: REDMADROBOT

Источник

Поделиться новостью

* - обязательные к заполнению поля