Узагальнення в Kotlin vs. Узагальнення в JAVA: подібності, відмінності, особливості


Ця стаття про Узагальнення в Kotlin — особливості їх використання, подібності та відмінності з Узагальнення в Java.

В двох словах про УзагальненняЯкщо коротко, то Узагальнення — це спосіб сказати, що клас, інтерфейс або метод будуть працювати не з якимось конкретним типом, а просто з якимось. З яким саме буде визначено з контексту. Наприклад:

public interface List<E> extends Collection<E> {
//...
}

Заздалегідь невідомо, об'єкти якого класу будуть міститися у списку, але це визначиться при його використанні:

List < String> list = new ArrayList<>();

Тепер це не просто список, а список рядків. Узагальнення допомагають забезпечити типобезопасность: List можна спробувати покласти будь-який об'єкт, але List<String> — тільки String або один з його нащадків.
Я розділю розповідь про Узагальнення на дві частини: власне Узагальнення і використання Wildcards. Поки мова не заходить про Wildcards, використання Узагальнення в 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
}

Узагальнення можуть бути додатково обмежені і в 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");

Таким чином, головне, що потрібно знати про Узагальнення при переході з Java на Kotlin — робіть все так само, як робили в Java. Спроби зробити щось по-новому, «по-котлиновски», швидше за все призведуть тільки до нових труднощів.

Wildcards
Перейдемо до другої частини. Wildcards — особливий випадок, що викликає найбільше труднощів і в Kotlin, і в Java. Основна проблема Узагальнення — їх інваріантність: 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);

Інваріантність Узагальнення дозволяє не допустити цього, але, з іншого боку, вносить додаткові обмеження. Так, при використанні звичайних Узагальнення неможливо 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, інакше б це викликало ті ж проблеми, що виникли б у Узагальнення, не будь вони інваріантними.

//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? (в цьому їх обмеження збігаються).

Прочитати про Узагальнення в цілому можна за посиланнями:
www.oracle.com/technetwork/articles/java/juneau-generics-2255374.html
www.angelikalanger.com/GenericsFAQ/JavaGenericsFAQ.html
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.