Обобщения (Generics): in, out, where
Как и в Java, в Kotlin классы могут иметь типовые параметры.
class Box(t: T)
Для того чтобы создать объект такого класса, необходимо предоставить тип в качестве аргумента.
val box: Box = Box(1)
Но если параметры могут быть выведены из контекста (в аргументах конструктора или в некоторых других случаях), можно опустить указание типа.
val box = Box(1) // 1 имеет тип Int, поэтому компилятор отмечает для себя, что тип переменной box — Box
Вариативность
Одним из самых сложных мест в системе типов Java являются маски (ориг. wildcards) (см. Java Generics FAQ). А в Kotlin их нет. Вместо этого, в Kotlin есть вариативность на уровне объявления и проекции типов.
` is _not_ a subtype of `List `. If `List` were not _invariant_, it would have been no better than Java’s arrays, as the following code would have compiled but caused an exception at runtime: —>
Давайте подумаем, зачем Java нужны эти загадочные маски. Проблема хорошо описана в книге Effective Java, Item 28: Use bounded wildcards to increase API flexibility. Прежде всего, обобщённые типы в Java являются инвариантными (ориг. invariant). Это означает, что List не является подтипом List . Если бы List был изменяемым, он был бы ничем не лучше массива в Java, потому что после компиляции данный код вызвал бы ошибку во время выполнения.
// Java List strs = new ArrayList(); List objs = strs; // . Причина вышеуказанной проблемы заключена здесь, Java запрещает так делать objs.add(1); // Тут мы помещаем Integer в список String'ов String s = strs.get(0); // . ClassCastException: не можем кастовать Integer к String
Java запрещает подобные вещи, гарантируя тем самым безопасность выполнения кода. Но у такого подхода есть свои последствия. Рассмотрим, например, метод addAll() интерфейса Collection . Какова сигнатура данного метода? Интуитивно мы бы указали её таким образом:
// Java interface Collection . < void addAll(Collectionitems); >
Но тогда мы бы не могли выполнять следующую простую операцию (которая является абсолютно безопасной):
// Java void copyAll(Collection to, Collection from) < to.addAll(from); // . Не скомпилируется с наивным объявлением метода addAll: // Collectionне является подтипом Collection >
(В Java вы, вероятно, познали это на своём горьком опыте, см. Effective Java, Item 25: Prefer lists to arrays)
Вот почему сигнатура addAll() на самом деле такая:
// Java interface Collection . < void addAll(Collectionitems); >
` _is_ a subtype of `Collection `. In other words, the wildcard with an _extends_\-bound (_upper_ bound) makes the type _covariant_. —>
Маска для аргумента ? extends E указывает на то, что этот метод принимает коллекцию объектов E или некоего типа унаследованного от E , а не только сам E . Это значит, что вы можете безопасно читать объекты типа E из содержимого (элементы коллекции являются экземплярами подкласса E), но не можете их присваивать, потому что не знаете, какие объекты соответствуют этому неизвестному подтипу E . Минуя это ограничение, желаемый результат может быть достигнут: Collection является подтипом Collection . Другими словами, маска с extends-связкой (верхнее связывание) делает тип ковариантным (ориг. covariant).
`, a _supertype_ of `List `. —>
Ключом к пониманию, почему этот трюк работает, является довольно простая мысль: использование коллекции String ‘ов и чтение из неё Object нормально только в случае, если вы только берёте элементы из коллекции. Наоборот, если вы только вносите элементы в коллекцию, то нормально брать коллекцию Object ‘ов и помещать в неё String ‘и: в Java есть List , супертип List .
` (for example, you can call `add(String)` or `set(int, String)`). If you call something that returns `T` in `List `, you don’t get a `String`, but rather an `Object`. —>
Это называется контрвариантностью (ориг.: contravariance). Вы можете вызвать только те методы, которые принимают String в качестве аргумента в List (например, вы можете вызвать add(String) или set(int, String) ). В случае, если вы вызываете из List что-то с возвращаемым значением T , вы получаете не String , а Object .
Джошуа Блох (Joshua Bloch) называет объекты:
- Производителями (ориг.: producers), если вы только читаете из них,
- Потребителями (ориг.: consumers), если вы только записываете в них.
«For maximum flexibility, use wildcard types on input parameters that represent producers or consumers», > and proposes the following mnemonic: > >_PECS stands for Producer-Extends, Consumer-Super._ —>
Для максимальной гибкости используйте маски на входных параметрах, которые представляют производителей или потребителей, и предлагает следующую мнемонику:
PECS обозначает Producer-Extends, Consumer-Super.
If you use a producer-object, say, `List `, you are not allowed to call `add()` or `set()` on this object, > but this does not mean that it is _immutable_: for example, nothing prevents you from calling `clear()` > to remove all the items from the list, since `clear()` does not take any parameters at all. > >The only thing guaranteed by wildcards (or other types of variance) is _type safety_. Immutability is a completely different story. —>
Если вы используете объект-производитель, предположим, List , вы не можете вызвать методы add() или set() этого объекта. Но это не значит, что объект является неизменяемым: например, ничто не мешает вам вызвать метод clear() для того, чтобы очистить список, так как clear() не имеет аргументов.
Единственное, что гарантируют маски (или другие типы вариантности) — безопасность типов. Неизменяемость — совершенно другая история.
Вариантность на уровне объявления
` that does not have any methods that take `T` as a parameter, only methods that return `T`: —>
Допустим, у вас есть обобщённый интерфейс Source , у которого нет методов, принимающих T в качестве аргумента. Только методы, возвращающие T :
// Java interface Source
` in a variable of type `Source ` — there are no consumer-methods to call. But Java does not know this, and still prohibits it: —>
Тогда было бы вполне безопасно хранить ссылки на экземпляр Source в переменной типа Source — не нужно вызывать никакие методы-потребители. Но Java не знает этого и не воспринимает такой код.
// Java void demo(Source strs) < Sourceobjects = strs; // . Запрещено в Java // . >
`. Doing so is meaningless, because you can call all the same methods on such a variable as before, so there’s no value added by the more complex type. But the compiler does not know that. —>
Чтобы исправить это, вам нужно объявить объекты типа Source , что в каком-то роде бессмысленно, потому что у переменной такого типа вы можете вызывать только те методы, что и ранее, стало быть, более сложный тип не добавляет смысла. Но компилятор этого не понимает.
`, and never consumed. To do this, use the `out` modifier: —>
В Kotlin существует способ объяснить вещь такого рода компилятору. Он называется вариантность на уровне объявления: вы можете объявить типовой параметр T класса Source таким образом, чтобы удостовериться, что он только возвращается (производится) членами Source , и никогда не потребляется. Чтобы сделать это, вам необходимо использовать модификатор out .
interface Source < fun nextT(): T >fun demo(strs: Source) < val objects: Source= strs // Всё в порядке, т.к. T — out-параметр // . >
` can safely be a supertype of `C `. —>
Общее правило таково: когда параметр T класса С объявлен как out , он может использоваться только в out-местах в членах C . Но зато C может быть родителем C , и это будет безопасно.
Другими словами, класс C ковариантен в параметре T ; или T является ковариантным типовым параметром. C производит экземпляры типа T , но не потребляет их.
Модификатор out определяет вариантность, и так как он указывается на месте объявления типового параметра, речь идёт о вариативности на месте объявления. Эта концепция противопоставлена вариативности на месте использования из Java, где маски при использовании типа делают типы ковариантными.
В дополнении к out , Kotlin предоставляет дополнительный модификатор in . Он делает параметризованный тип контравариантным: он может только потребляться, но не может производиться. Comparable является хорошим примером такого класса:
interface Comparable < operator fun compareTo(other: T): Int >fun demo(x: Comparable) < x.compareTo(1.0) // 1.0 имеет тип Double, расширяющий Number // Таким образом, мы можем присвоить значение x переменной типа Comparableval y: Comparable = x // OK! >
Слова in и out говорят сами за себя (так как они довольно успешно используются в C# уже долгое время), таким образом, мнемоника, приведённая выше, не так уж и нужна, и её можно перефразировать следующим образом:
Экзистенциальная Трансформация: Consumer in, Producer out! 🙂
Проекции типов
Вариативность на месте использования
Объявлять параметризованный тип T как out очень удобно: при его использовании не будет никаких проблем с подтипами. И это действительно так в случае с классами, которые могут быть ограничены на только возвращение T . А как быть с теми классами, которые ещё и принимают T ? Хороший пример этого: класс Array :
class Array(val size: Int) < operator fun get(index: Int): T < . >operator fun set(index: Int, value: T) < . >>
Этот класс не может быть ни ко-, ни контравариантным по T , что ведёт к некоторому снижению гибкости. Рассмотрим следующую функцию:
fun copy(from: Array, to: Array)
По задумке, это функция должна копировать значения из одного массива в другой. Давайте попробуем сделать это на практике:
val ints: Array = arrayOf(1, 2, 3) val any = Array(3) < "" >copy(ints, any) // ^ Ошибка: тип Array, а ожидалось Array
` is _invariant_ in `T`, and so neither `Array ` nor `Array ` is a subtype of the other. Why not? Again, this is because `copy` could have an unexpected behavior, for example, it may attempt to write a `String` to `from`, and if you actually pass an array of `Int` there, a `ClassCastException` will be thrown later. —>
Здесь вы встречаете уже знакомую вам проблему: Array инвариантен по T , таким образом Array не является подтипом Array . Почему? Опять же, потому что копирование потенциально опасно, например может произойти попытка записать, скажем, значение типа String в from . И если мы на самом деле передадим туда массив Int , будет выброшен ClassCastException .
Чтобы запретить функции copy записывать в from , вы можете сделать следующее:
fun copy(from: Array, to: Array)
` while being slightly simpler. —>
Произошедшее здесь называется проекция типов: мы сказали, что from — не просто массив, а ограниченный (спроецированный): мы можем вызывать только те методы, которые возвращают параметризованный тип T , что в этом случае означает, что мы можем вызывать только get() . Таков наш подход к вариативности на месте использования, и он соответствует Array из Java, но в более простом виде.
Вы также можете проецировать тип с помощью in .
fun fill(dest: Array, value: String)
` corresponds to Java’s `Array `. This means that you can pass an array of `CharSequence` or an array of `Object` to the `fill()` function. —>
Array соответствует Array из Java, то есть мы можем передать массив CharSequence или массив Object в функцию fill() .
«Звёздные» проекции
Иногда возникает ситуация, когда вы ничего не знаете о типе аргумента, но всё равно хотите использовать его безопасным образом. Этой безопасности можно добиться путём определения такой проекции параметризованного типа, при которой его экземпляр будет подтипом этой проекции.
Для этого в Kotlin есть так называемый синтаксис star-projection:
- Для Foo , где T — ковариантный параметризованный тип с верхней границей TUpper , Foo является эквивалентом Foo . Это значит, что когда T неизвестен, вы можете безопасно читать значения типа TUpper из Foo ;
- Для Foo , где T — контравариантный параметризованный тип, Foo является эквивалентом Foo . Это значит, что вы не можете безопасно писать в Foo при неизвестном T ;
- Для Foo , где T — инвариантный параметризованный тип с верхней границей TUpper , Foo является эквивалентом Foo при чтении значений и Foo при записи значений.
Если параметризованный тип имеет несколько параметров, каждый из них проецируется независимо. Например, если тип объявлен как interface Function , вы можете представить следующую «звёздную» проекцию:
- Function означает Function ;
- Function означает Function ;
- Function означает Function .
«Звёздные» проекции очень похожи на сырые (raw) типы из Java, за тем исключением того, что они безопасны.
Обобщённые функции
Функции, как и классы, могут иметь типовые параметры. Типовые параметры помещаются перед именем функции.
fun singletonList(item: T): List < // . >fun T.basicToString(): String < // функция-расширение // . >
Для вызова обобщённой функции, укажите тип аргументов на месте вызова после имени функции.
val l = singletonList(1)
Аргументы типа можно опустить, если их можно вывести из контекста, поэтому следующий пример также работает:
val l = singletonList(1)
Обобщённые ограничения
Набор всех возможных типов, которые могут быть переданы в качестве параметра, может быть ограничен с помощью обобщённых ограничений.
Верхние границы
Самый распространённый тип ограничений — верхняя граница, которая соответствует ключевому слову extends из Java.
fun > sort(list: List)
` can be substituted for `T`. For example: —>
Тип, указанный после двоеточия, является верхней границей: только подтип Comparable может быть передан в T . Например:
sort(listOf(1, 2, 3)) // Всё в порядке. Int — подтип Comparable sort(listOf(HashMap())) // Ошибка: HashMap не является подтипом Comparable
По умолчанию (если не указана явно) верхняя граница — Any? . В угловых скобках может быть указана только одна верхняя граница. Для указания нескольких верхних границ нужно использовать отдельное условие where .
fun copyWhenGreater(list: List, threshold: T): List where T : CharSequence, T : Comparable < return list.filter < it >threshold >.map < it.toString() >>
Переданный тип должен одновременно удовлетворять всем условиям where . В приведенном выше примере тип T должен реализовывать и CharSequence , и Comparable .
Удаление типа
` and `Foo ` are erased to just `Foo `. —>
Проверки безопасности типов, выполняемые Kotlin для использования общих объявлений, выполняются во время компиляции. Во время выполнения экземпляры общих типов не содержат никакой информации об их фактических аргументах типа. Говорят, информация о типе будет удалена. Например, экземпляры Foo и Foo удаляются до Foo .
Поэтому нет общего способа проверить, был ли создан экземпляр общего типа с определенными аргументами типа во время выполнения, и компилятор запрещает такие is -проверки.
`, cannot be checked at runtime. These [unchecked casts](typecasts.md#unchecked-casts) can be used when type safety is implied by high-level program logic but cannot be inferred directly by the compiler. The compiler issues a warning on unchecked casts, and at runtime, only the non-generic part is checked (equivalent to `foo as List `). —>
Приведение типов к обобщенным типам с конкретными аргументами типа, например foo as List , не может быть проверено во время выполнения. Эти непроверенные приведения могут использоваться, когда безопасность типов подразумевается программной логикой высокого уровня, но не может быть выведена непосредственно компилятором. Компилятор выдает предупреждение о непроверенных приведениях, и во время выполнения проверяется только необобщенная часть (эквивалентно foo as List ).
Типовые аргументы вызовов обобщенных функций также проверяются только во время компиляции. Внутри тел функций параметры типа нельзя использовать для проверки типов, а приведение типов к параметрам типа ( foo as T ) не проверено. Однако параметры вещественного типа встроенных функций заменяются фактическими аргументами типа в теле встроенной функции на стороне вызовов и поэтому могут использоваться для проверки и приведения типов с теми же ограничениями для экземпляров обобщенных типов, как описано выше.
© 2015—2023 Open Source Community
Difference between Any type and Generics in Kotlin
I guessed it should be valid. However, the IntelliJ IDEA linter shows a type mismatch error That being said, I would change the return type of my parse function to Any, right? So that, what is the difference between using Any type and Generics in Kotlin? In which cases should use each of those? I did read the following question but not understood at all about star-projection in Kotlin due to the fact I am quite new.
asked Oct 19, 2017 at 17:37
345 1 1 gold badge 8 8 silver badges 25 25 bronze badges
This Java answer might help you stackoverflow.com/questions/5207115/java-generics-t-vs-object
Oct 19, 2017 at 17:45
Did you find an answer?
Dec 1, 2017 at 7:23
3 Answers 3
Your return type it defined as T , but there is nothing assuring that T and a:Any are related. T may be more restrictive than Any , in which case you can’t return a boolean or whatever you provided for a .
The following will work, by changing the return type from T to Any :
fun parse(a: Any): Any = when (a) < is String ->a else -> false >
Any alternate option, if you really want to return type T:
inline fun parse(a: Any): T? = when (a) < is T ->a else -> null >
answered Oct 19, 2017 at 18:30
506 4 4 silver badges 12 12 bronze badges
Your example does not use T and thus it’s nonsense to make it generic anyways.
Think about this: As a client you put something into a function, e.g. an XML- ByteArray which the function is supposed to parse into an Object . Calling the function you do not want to have it return Any (Casting sucks) but want the function return the type of the parsed object. THIS can be achieved with generics:
fun parse(xml: ByteArray): T < val ctx: JAXBContext = JAXBContext.newInstance() val any = ctx.createUnmarshaller().unmarshal(ByteArrayInputStream(xml)) return any as T >val int = parse("123".toByteArray()) val string = parse("123".toByteArray())
Look at the method calls: You tell with generics what type is expected to be returned. The code is not useful and only supposed to give you an idea of generics.
answered Oct 19, 2017 at 18:47
77.6k 17 17 gold badges 168 168 silver badges 196 196 bronze badges
I guessed it should be valid
Why would it be? You return a String in one branch and a Boolean in the other. So the common type for the entire when expression is Any and that’s what the compiler (and IDEA) says is «found». Your code also says it should be T (which is «required»).
Your generic method should work for any T , e.g. for Int , but Any isn’t a subtype of Int and so the code isn’t valid.
So that, what is the difference between using Any type and Generics in Kotlin?
This is like asking «what is the difference between using numbers and files»: they don’t have much in common in the first place. You use generics to write code which can work with all types T (or with all types satisfying some constraint); you use Any when you want the specific type Any .
Generics: in, out, where
Classes in Kotlin can have type parameters, just like in Java:
class Box(t: T)
To create an instance of such a class, simply provide the type arguments:
val box: Box
But if the parameters can be inferred, for example, from the constructor arguments, you can omit the type arguments:
val box = Box(1) // 1 has type Int, so the compiler figures out that it is Box
Variance
One of the trickiest aspects of Java’s type system is the wildcard types (see Java Generics FAQ). Kotlin doesn’t have these. Instead, Kotlin has declaration-site variance and type projections.
Let’s think about why Java needs these mysterious wildcards. The problem is explained well in Effective Java, 3rd Edition, Item 31: Use bounded wildcards to increase API flexibility. First, generic types in Java are invariant, meaning that List is not a subtype of List . If List were not invariant, it would have been no better than Java’s arrays, as the following code would have compiled but caused an exception at runtime:
// Java List
Java prohibits such things in order to guarantee run-time safety. But this has implications. For example, consider the addAll() method from the Collection interface. What’s the signature of this method? Intuitively, you’d write it this way:
// Java interface Collection
But then, you would not be able to do the following (which is perfectly safe):
// Java void copyAll(Collection
(In Java, you probably learned this the hard way, see Effective Java, 3rd Edition, Item 28: Prefer lists to arrays)
That’s why the actual signature of addAll() is the following:
// Java interface Collection
The wildcard type argument ? extends E indicates that this method accepts a collection of objects of E or a subtype of E , not just E itself. This means that you can safely read E ‘s from items (elements of this collection are instances of a subclass of E), but cannot write to it as you don’t know what objects comply with that unknown subtype of E . In return for this limitation, you get the desired behavior: Collection is a subtype of Collection . In other words, the wildcard with an extends-bound (upper bound) makes the type covariant.
The key to understanding why this works is rather simple: if you can only take items from a collection, then using a collection of String s and reading Object s from it is fine. Conversely, if you can only put items into the collection, it’s okay to take a collection of Object s and put String s into it: in Java there is List , which accepts String s or any of its supertypes.
The latter is called contravariance, and you can only call methods that take String as an argument on List (for example, you can call add(String) or set(int, String) ). If you call something that returns T in List , you don’t get a String , but rather an Object .
Joshua Bloch gives the name Producers to objects you only read from and Consumers to those you only write to. He recommends:
«For maximum flexibility, use wildcard types on input parameters that represent producers or consumers», and proposes the following mnemonic:
PECS stands for Producer-Extends, Consumer-Super.
If you use a producer-object, say, List , you are not allowed to call add() or set() on this object, but this does not mean that it is immutable: for example, nothing prevents you from calling clear() to remove all the items from the list, since clear() does not take any parameters at all.
The only thing guaranteed by wildcards (or other types of variance) is type safety. Immutability is a completely different story.
Declaration-site variance
Let’s suppose that there is a generic interface Source that does not have any methods that take T as a parameter, only methods that return T :
// Java interface Source
Then, it would be perfectly safe to store a reference to an instance of Source in a variable of type Source — there are no consumer-methods to call. But Java does not know this, and still prohibits it:
// Java void demo(Source
To fix this, you should declare objects of type Source . Doing so is meaningless, because you can call all the same methods on such a variable as before, so there’s no value added by the more complex type. But the compiler does not know that.
In Kotlin, there is a way to explain this sort of thing to the compiler. This is called declaration-site variance: you can annotate the type parameter T of Source to make sure that it is only returned (produced) from members of Source , and never consumed. To do this, use the out modifier:
interface Source
The general rule is this: when a type parameter T of a class C is declared out , it may occur only in the out-position in the members of C , but in return C can safely be a supertype of C .
In other words, you can say that the class C is covariant in the parameter T , or that T is a covariant type parameter. You can think of C as being a producer of T ‘s, and NOT a consumer of T ‘s.
The out modifier is called a variance annotation, and since it is provided at the type parameter declaration site, it provides declaration-site variance. This is in contrast with Java’s use-site variance where wildcards in the type usages make the types covariant.
In addition to out , Kotlin provides a complementary variance annotation: in . It makes a type parameter contravariant, meaning it can only be consumed and never produced. A good example of a contravariant type is Comparable :
interface Comparable
The words in and out seem to be self-explanatory (as they’ve already been used successfully in C# for quite some time), and so the mnemonic mentioned above is not really needed. It can in fact be rephrased at a higher level of abstraction:
The Existential Transformation: Consumer in, Producer out!🙂
Type projections
Use-site variance: type projections
It is very easy to declare a type parameter T as out and avoid trouble with subtyping on the use site, but some classes can’t actually be restricted to only return T ‘s! A good example of this is Array :
class Array
This class can be neither co- nor contravariant in T . And this imposes certain inflexibilities. Consider the following function:
fun copy(from: Array, to: Array)
This function is supposed to copy items from one array to another. Let’s try to apply it in practice:
val ints: Array
Here you run into the same familiar problem: Array is invariant in T , and so neither Array nor Array is a subtype of the other. Why not? Again, this is because copy could have an unexpected behavior, for example, it may attempt to write a String to from , and if you actually pass an array of Int there, a ClassCastException will be thrown later.
To prohibit the copy function from writing to from , you can do the following:
fun copy(from: Array, to: Array)
This is type projection, which means that from is not a simple array, but is rather a restricted (projected) one. You can only call methods that return the type parameter T , which in this case means that you can only call get() . This is our approach to use-site variance, and it corresponds to Java’s Array while being slightly simpler.
You can project a type with in as well:
fun fill(dest: Array, value: String)
Array corresponds to Java’s Array . This means that you can pass an array of CharSequence or an array of Object to the fill() function.
Star-projections
Sometimes you want to say that you know nothing about the type argument, but you still want to use it in a safe way. The safe way here is to define such a projection of the generic type, that every concrete instantiation of that generic type will be a subtype of that projection.
Kotlin provides so-called star-projection syntax for this:
- For Foo , where T is a covariant type parameter with the upper bound TUpper , Foo is equivalent to Foo . This means that when the T is unknown you can safely read values of TUpper from Foo .
- For Foo , where T is a contravariant type parameter, Foo is equivalent to Foo . This means there is nothing you can write to Foo in a safe way when T is unknown.
- For Foo , where T is an invariant type parameter with the upper bound TUpper , Foo is equivalent to Foo for reading values and to Foo for writing values.
If a generic type has several type parameters, each of them can be projected independently. For example, if the type is declared as interface Function you could use the following star-projections:
Star-projections are very much like Java’s raw types, but safe.
Generic functions
Classes aren’t the only declarations that can have type parameters. Functions can, too. Type parameters are placed before the name of the function:
fun
To call a generic function, specify the type arguments at the call site after the name of the function:
val l = singletonList
Type arguments can be omitted if they can be inferred from the context, so the following example works as well:
val l = singletonList(1)
Generic constraints
The set of all possible types that can be substituted for a given type parameter may be restricted by generic constraints.
Upper bounds
The most common type of constraint is an upper bound, which corresponds to Java’s extends keyword:
fun > sort(list: List)
The type specified after a colon is the upper bound, indicating that only a subtype of Comparable can be substituted for T . For example:
sort(listOf(1, 2, 3)) // OK. Int is a subtype of Comparable
The default upper bound (if there was none specified) is Any? . Only one upper bound can be specified inside the angle brackets. If the same type parameter needs more than one upper bound, you need a separate where-clause:
fun
The passed type must satisfy all conditions of the where clause simultaneously. In the above example, the T type must implement both CharSequence and Comparable .
Definitely non-nullable types
To make interoperability with generic Java classes and interfaces easier, Kotlin supports declaring a generic type parameter as definitely non-nullable.
To declare a generic type T as definitely non-nullable, declare the type with & Any . For example: T & Any .
A definitely non-nullable type must have a nullable upper bound.
The most common use case for declaring definitely non-nullable types is when you want to override a Java method that contains @NotNull as an argument. For example, consider the load() method:
import org.jetbrains.annotations.*; public interface Game
To override the load() method in Kotlin successfully, you need T1 to be declared as definitely non-nullable:
interface ArcadeGame
When working only with Kotlin, it’s unlikely that you will need to declare definitely non-nullable types explicitly because Kotlin’s type inference takes care of this for you.
Type erasure
The type safety checks that Kotlin performs for generic declaration usages are done at compile time. At runtime, the instances of generic types do not hold any information about their actual type arguments. The type information is said to be erased. For example, the instances of Foo and Foo are erased to just Foo .
Generics type checks and casts
Due to the type erasure, there is no general way to check whether an instance of a generic type was created with certain type arguments at runtime, and the compiler prohibits such is -checks such as ints is List or list is T (type parameter). However, you can check an instance against a star-projected type:
if (something is List) < something.forEach < println(it) >// The items are typed as `Any?` >*>
Similarly, when you already have the type arguments of an instance checked statically (at compile time), you can make an is -check or a cast that involves the non-generic part of the type. Note that angle brackets are omitted in this case:
fun handleStrings(list: MutableList
The same syntax but with the type arguments omitted can be used for casts that do not take type arguments into account: list as ArrayList .
The type arguments of generic function calls are also only checked at compile time. Inside the function bodies, the type parameters cannot be used for type checks, and type casts to type parameters ( foo as T ) are unchecked. The only exclusion is inline functions with reified type parameters, which have their actual type arguments inlined at each call site. This enables type checks and casts for the type parameters. However, the restrictions described above still apply for instances of generic types used inside checks or casts. For example, in the type check arg is T , if arg is an instance of a generic type itself, its type arguments are still erased.
Unchecked casts
Type casts to generic types with concrete type arguments such as foo as List cannot be checked at runtime.
These unchecked casts can be used when type safety is implied by the high-level program logic but cannot be inferred directly by the compiler. See the example below.
fun readDictionary(file: File): Map = file.inputStream().use < TODO("Read a mapping of strings to arbitrary elements.") >// We saved a map with `Int`s into this file val intsFile = File(«ints.dictionary») // Warning: Unchecked cast: `Map` to `Map` val intsDictionary: Map = readDictionary(intsFile) as Map
A warning appears for the cast in the last line. The compiler can’t fully check it at runtime and provides no guarantee that the values in the map are Int .
To avoid unchecked casts, you can redesign the program structure. In the example above, you could use the DictionaryReader and DictionaryWriter interfaces with type-safe implementations for different types. You can introduce reasonable abstractions to move unchecked casts from the call site to the implementation details. Proper use of generic variance can also help.
For generic functions, using reified type parameters makes casts like arg as T checked, unless arg ‘s type has its own type arguments that are erased.
An unchecked cast warning can be suppressed by annotating the statement or the declaration where it occurs with @Suppress(«UNCHECKED_CAST») :
inline fun
On the JVM: array types ( Array ) retain information about the erased type of their elements, and type casts to an array type are partially checked: the nullability and actual type arguments of the element type are still erased. For example, the cast foo as Array?> will succeed if foo is an array holding any List , whether it is nullable or not.
Underscore operator for type arguments
The underscore operator _ can be used for type arguments. Use it to automatically infer a type of the argument when other types are explicitly specified:
Generics в Kotlin vs. Generics в JAVA: cходства, различия, особенности

Эта статья про 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 fun makeSomething(someData : K) : K < var localData = someData //. return localData >
Такие ограничения обозначают, что вместо K может быть использован не любой класс, а только удовлетворяющий условию (в данном случае — Number или класс, его наследующий).
//корректно makeSomething(1) //некорректно makeSomething(“string”)
Ограничения могут быть и комплексными, например, показывающими, что передаваемый в метод объект должен наследовать какой-то класс и реализовывать какой-то интерфейс, например:
//Java public static SomeFragment newInstance(T interactor)
//Kotlin fun newInstance(interactor : T) : SomeFragment where T : Interaction, T : 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
- контравариантные Wildcards
Ограниченные “снизу” ковариантные Wildcards используются в случаях, когда ожидается generic-класс от какого-то класса или его потомков. Например:
// Java public interface Container < T getData(); void putData(T data); >static void hideView(Container viewContainer)
Здесь метод hideView ожидает объект, реализующий интерфейс Container, но не любой и не только содержащий View, а содержащий View или какой-либо другой класс, наследующий View. Это и называется ковариантностью.
В Kotlin такое поведение может быть реализовано похожим образом:
// Kotlin interface Container < fun getData() : T fun putData(data : T) >fun hideView (viewContainer : Container)
При этом на использование параметра, объявленного как 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); //а такой - некорректен, потому что на самом деле это может быть, например, ContainerviewContainer.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
И тогда его использование будет выглядеть следующим образом:
// 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)
В Kotlin аналогичный механизм называется star-projection. Во всех тривиальных случаях единственное его отличие от неограниченных Wildcards в Java — использование символа “*” вместо “?”:
// Kotlin interface Container < fun getData() : T fun putData(data : T); >fun isNull(container : Container) : Boolean
В Java неограниченные Wildcards используются по следующим правилам: положить в них можно только null, считывается всегда Object. В Kotlin положить внутрь нельзя ничего, а считывается объект класса Any.
При совместном использовании declaration-side variance и star-projection нужно учитывать, что ограничения суммируются. Так, при использовании контравариантного declaration-side variance (позволяющего положить внутрь что угодно, но считать только Any?) вместе со star-projection положить внутрь нельзя будет ничего (ограничение star-projection), а возвращаться будет все тот же Any? (в этом их ограничения совпадают).
- Блог компании red_mad_robot
- Java
- Разработка мобильных приложений
- Разработка под Android
- Kotlin
