O paradigma no Kotlin é semelhante ao do Java. Você pode consultar o meu artigo sobre a introdução do paradigma Java:
Paradigma Java essas coisas (1)
Paradigma Java essas coisas (2)
Paradigma Java essas coisas (3)
Java Paradigma Essas Coisas (4)
No post acima, ele explica por que o Java deve introduzir o paradigma na versão 1.5 e alguns pontos de conhecimento básico sobre o paradigma Java.
Se você dividir um objeto em uma declaração e usá-lo em duas partes. Os genéricos concentram-se principalmente na reutilização de código de declarações de tipo e os curingas se concentram na reutilização de código em uso. Os genéricos são usados para definir a parametrização dos tipos de dados internos e os curingas são usados para definir a parametrização dos tipos de objetos utilizados.
O uso de genéricos e curingas aprimora a reutilização de código. Ao mesmo tempo, o tipo do objeto é verificado quanto à segurança do tipo, reduzindo erros durante o processo de conversão do tipo.
Alterações de paradigma e tipo de matriz
Matrizes em Java são covariantes
O código a seguir pode ser compilado e executado corretamente:
Integer[] ints = new Integer[3];
ints[0] = 0;
ints[1] = 1;
ints[2] = 2;
Number[] numbers = new Number[3];
numbers = ints;
for (Number n : numbers) {
System.out.println(n);
}
Em Java, como Integer é um subtipo de Number, o tipo de matriz Integer [] também é um subtipo de Number [], portanto, um valor Inteiro [] pode ser fornecido sempre que o valor Number [] for necessário.
Java não é covariante para listar genéricos <T>
Em outras palavras, a lista <número> não é um subtipo da lista <número> e tentar fornecer a lista <número> onde a lista <número> é necessária é um erro de tipo. O código a seguir, o compilador relatará diretamente um erro:
Mesmo se usarmos caracteres curinga, escreva:
Ainda é um erro.
Por que o objeto Number pode ser instanciado por Inteiro, mas o objeto ArrayList <Number> não pode ser instanciado por ArrayList <Integer>? O <? Extends Number> na lista declara que seu elemento é uma classe derivada de Number ou Number.Por que não pode adicionar Inteiro? Para resolver esses problemas, você precisa entender a inversão e covariância em Java e o uso de curingas em genéricos.
Inversão, covariância e invariância são usadas para descrever o relacionamento de herança após a transformação do tipo Sua definição: se A e B representam tipos, f (⋅) representa a conversão de tipos e ≤ representa o relacionamento de herança (por exemplo, A ≤B significa A é uma subclasse derivada de B)
- Quando A≤B, f (A) ≤f (B) se mantém, então f (⋅) é covariante
- Quando A≤B, f (B) ≤f (A) se mantém, então f (⋅) é contravariante
- Quando A≤B, as duas fórmulas acima não são verdadeiras, ou seja, f (A) ef (B) não têm relação de herança entre si ef (⋅) é invariável
Tanto a covariância como a covariância inversa são de tipo seguro.
A matriz de Kotlin não é covariante
abstract class Animal(val size: Int)
class Dog(val cuteness: Int): Animal(100)
class Cat(val terrorFactor: Int): Animal(1)
O seguinte erro de compilação da matriz:
val dogArr: Array<Dog> = arrayOf(Dog(1), Dog(2))
val animalArr: Array<Animal> = dogArr
Como objetos comuns em Java, o seguinte código pode ser compilado:
val dog: Dog = Dog(10)
var animal: Animal = dog
No Kotlin, uma classe de paradigma é definida da seguinte maneira e, em seguida, será compilada com erros quando usada:
class ReadableList<T>{
}
val dogReadable: ReadableList<Dog> = ReadableList()
//提示报错,需要ReadableList<Animal>,但却传了ReadableList<Dog>
val animalReadable: ReadableList<Animal> = dogReadable
Kotlin é covariante para listar genéricos <T>
Ou seja, o código a seguir pode ser compilado através
val dogList: List<Dog> = listOf(Dog(10), Dog(20))
playAnimal(dogList)
fun playAnimal(animalList: List<Animal>) {
...
}
Como fazer com que Java e Kotlin adicionem suporte a covariantes e inversores
Os genéricos em Java permanecem inalterados, mas às vezes você precisa implementar inversão e covariância, o que você deve fazer? No momento, precisamos usar os curingas de que falamos antes ?
.
No Java e Kotlin, você pode adicionar suporte para tipos de parâmetros que não suportam covariância por padrão. Mas as duas linguagens, Java e Kotlin, são tratadas de maneira diferente:
- Java: variação do site de uso
- Kotlin: variação do local da declaração
Pode-se observar que Java usa alterações de tipo de terminal, enquanto o Kotlin usa tipos de terminal declarativos. Qual é a diferença entre os dois?
O entendimento pessoal é que o uso da variação do site final (variação do site de uso) é covariável ao usar especificamente (inicializar) um objeto de Classe.
Java <? extends T>
implementa covariância genérica
List<? extends Number> list = new ArrayList<>();
A ? extends Number
representação aqui é a classe Number ou suas subclasses, que abreviamos como C.
Aqui C <= Number
, esta relação mantém List<C> <= List< Number >
verdadeiro: . Existem:
List<? extends Number> list1 = new ArrayList<Integer>();
List<? extends Number> list2 = new ArrayList<Float>();
Outro exemplo, conforme mostrado no código a seguir:
List<Cat> catList = new ArrayList<>();
List<? extends Animal> animalList = catList;
Como você pode ver, quando declaramos animalList, fizemos uma pequena modificação no tipo genérico.Depois de usar ? Extends Animal para modificar, o código acima pode ser compilado e executado com êxito. Mesmo podemos definir um método para aceitar esse tipo de parâmetro da seguinte maneira:
List<Cat> cats = new ArrayList<>();
playAnimal(cats);
public static void playAnimal(List<? extends Animal> animal) {
...
}
A compilação pode ser passada sem problemas, portanto, nosso código será mais escalável!
⚠️ Nota: Neste momento, para além do nulo, o objecto não pode ser adicionado a qualquer animal subclasse animalList, isto é, tendo em conta o seguinte código:
如果可以添加的话,List<? extends Number>
Ele conterá objetos de vários subtipos de números (Byte, Inteiro, Flutuante, Duplo, etc.). Para proteger sua consistência de tipo, o Java proíbe adicionar objetos à Lista <? Extends Number>, mas nulo pode ser adicionado.
Java <? super T>
implementa inversão genérica
List<? super Number> list = new ArrayList<>();
? super Number
Os curingas indicam que o limite inferior do tipo é Número. Ou seja, o tipo pai F está aqui ? super Number
e o tipo filho C é Number. Ou seja, quando F <= C, existe f (C) <= f (F), essa é a inversão. Exemplo de código:
List<? super Number> list3 = new ArrayList<Number>();
List<? super Number> list4 = new ArrayList<Object>();
list3.add(new Integer(3));
list4.add(new Integer(4));
Em outras palavras, não podemos List<? super Number >
adicionar nenhum objeto pai de Number a ele. Mas você pode adicionar Number e seus objetos de subclasse à List <? Super Number>.
PECS: Quando usar estende? Quando usar super?
Joshua Bloch se refere a objetos nos quais você só pode ler como produtores e objetos nos quais você pode escrever apenas como consumidores . Ele sugeriu: " Para maximizar a flexibilidade, use tipos curinga em parâmetros de entrada que representam produtores ou consumidores " e propôs as seguintes mnemônicas:
PECS significa Producer-Extens, Consumer-Super (Produtor-Extends, Consumer-Super).
Nota : Se você usar um objeto produtor, por exemplo List<? extends Foo>
, a chamada add()
ou o objeto não será permitido set()
. Mas isso não significa que o objeto seja imutável : por exemplo, nada impede que você chame clear()
para excluir todos os itens da lista, porque clear()
nenhum parâmetro é necessário. A única coisa garantida pelos curingas (ou outros tipos de alterações de tipo) é a segurança do tipo . A imutabilidade é outra questão inteiramente.
Declarar alteração de tipo
Suponha que exista uma interface genérica Source<T>
, não há T
método como parâmetro na interface , mas o método retorna o T
valor do tipo:
// Java
interface Source<T> {
T nextT();
}
Portanto, é extremamente seguro Source <Object>
armazenar Source <String>
a referência da instância em uma variável do tipo -no consumer-method pode ser chamado. Mas Java não sabe disso e ainda proíbe tais operações:
// Java
void demo(Source<String> strs) {
Source<Object> objects = strs; // !!!在 Java 中不允许
// ……
}
Para corrigir isso, devemos declarar o tipo do objeto como Source<? extends Object>
, o que não faz sentido, porque podemos chamar os mesmos métodos no objeto como antes, para que tipos mais complexos não tragam valor. Mas o compilador não sabe.
No Kotlin, há uma maneira de explicar isso ao compilador. Isso é chamado de alteração do tipo de declaração : podemos marcar Source
o parâmetro type T
para garantir que ele seja retornado (produzido) apenas pelos Source<T>
membros e nunca consumido. Para isso, fornecemos o modificador out :
interface Source<out T> {
fun nextT(): T
}
fun demo(strs: Source<String>) {
val objects: Source<Any> = strs // 这个没问题,因为 T 是一个 out-参数
// ……
}
O princípio geral é: quando C
o parâmetro de tipo de uma classe T
é declarado como desativado , ele pode aparecer apenas na posição de saídaC
do membro , mas o retorno é uma superclasse que pode ser usada com segurança .C<Base>
C<Derived>
Em resumo, eles dizem que a classe C
está no parâmetro T
em um covariante ou T
uma covariante parâmetro de tipo. Você pode pensar C
é T
o produtor , não T
o consumidor .
O modificador de saída é chamado de anotação de alteração de tipo e , como é fornecido na declaração de parâmetro de tipo, falaremos sobre a declaração de alteração de tipo . Isso é o oposto do uso de alteração de tipo por Java , cujo tipo de uso de curingas cria covariância de tipo.
Além de sair , Kotlin adicionou um comentário de alteração de tipo: in . Faz uma inversão de parâmetro de tipo : só pode ser consumida, mas não produzida. Um bom exemplo do tipo de inversor é Comparable
:
interface Comparable<in T> {
operator fun compareTo(other: T): Int
}
fun demo(x: Comparable<Number>) {
x.compareTo(1.0) // 1.0 拥有类型 Double,它是 Number 的子类型
// 因此,我们可以将 x 赋给类型为 Comparable <Double> 的变量
val y: Comparable<Double> = x // OK!
}
Acreditamos que as palavras dentro e fora são auto-explicativas (porque elas foram usadas com sucesso em C # por um longo tempo), portanto as mnemônicas mencionadas acima não são realmente necessárias e podem ser reescritas para um objetivo maior:
A transformação existencial : consumidor entra , produtor sai! :-)
Identificador ao definir uma classe, da seguinte maneira:
// 使用out关键字
class ReadableList<out T>{
}
val dogReadable: ReadableList<Dog> = ReadableList()
val animalReadable: ReadableList<Animal> = dogReadable
O código acima única diferença agora é que definimos antes ReadableList é adicionar um pouco restrições genéricas OUT , então você pode ser bem sucedida atribuição dogReadable para animalReadable objeto. Devemos ver isso antes Kotlin API Por que pode adivinhar o A lista <generic> suporta covariância.
Nota: Mas depois de modificado com a palavra-chave out, não pode haver métodos com T como o tipo de parâmetro dentro da classe ReadableList
Referência:
- https://github.com/EasyKotlin/chapter6_generics
- https://huanglizhuo.gitbooks.io/kotlin-in-chinese/content/ClassesAndObjects/Generics.html
- Profundo entendimento do tipo genérico e variância de Java e Kotlin
- Site chinês da Kotlin: https://www.kotlincn.net/docs/reference/generics.html