Olá! Continuamos estudando o tema dos genéricos. Você já tem bastante conhecimento sobre eles em palestras anteriores (sobre o uso de varargs ao trabalhar com genéricos e sobre apagamento de tipos ), mas ainda não abordamos um tópico importante: curingas . Esta é uma característica muito importante dos genéricos. Tanto é que dedicamos uma palestra separada para isso! Porém, não há nada de complicado em curingas, você verá isso agora :) Vejamos um exemplo:
public class Main {
public static void main(String[] args) {
String str = new String("Test!");
// ниHowих проблем
Object obj = str;
List<String> strings = new ArrayList<String>();
// ошибка компиляции!
List<Object> objects = strings;
}
}
O que está acontecendo aqui? Vemos duas situações muito semelhantes. No primeiro deles, estamos tentando converter um objeto String
para type Object
. Não há problemas com isso, tudo funciona como deveria. Mas na segunda situação, o compilador gera um erro. Embora pareça que estamos fazendo a mesma coisa. Só que agora estamos usando uma coleção de vários objetos. Mas por que o erro ocorre? Qual é a diferença, essencialmente, entre convertermos um objeto String
em um tipo Object
ou 20 objetos? Há uma diferença importante entre um objeto e uma coleção de objetos . Se uma classe é sucessora de uma classe , então ela não é sucessora . É por esta razão que não pudemos trazer o nosso . é herdeiro , mas não é herdeiro . Intuitivamente, isso não parece muito lógico. Por que os criadores da linguagem foram guiados por este princípio? Vamos imaginar que o compilador não nos daria erro aqui: B
А
Collection<B>
Collection<A>
List<String>
List<Object>
String
Object
List<String>
List<Object>
List<String> strings = new ArrayList<String>();
List<Object> objects = strings;
Neste caso, poderíamos, por exemplo, fazer o seguinte:
objects.add(new Object());
String s = strings.get(0);
Como o compilador não nos deu erros e nos permitiu criar uma referência List<Object> object
a uma coleção de strings strings
, podemos adicionar strings
não uma string, mas simplesmente qualquer objeto Object
! Assim, perdemos a garantia de que nossa coleção contém apenas os objetos especificados no genéricoString
. Ou seja, perdemos a principal vantagem dos genéricos – tipo segurança. E como o compilador nos permitiu fazer tudo isso, significa que só teremos um erro durante a execução do programa, o que é sempre muito pior que um erro de compilação. Para evitar tais situações, o compilador nos dá um erro:
// ошибка компиляции
List<Object> objects = strings;
...e lembra a ele que List<String>
ele não é um herdeiro List<Object>
. Esta é uma regra rígida para a operação de genéricos e deve ser lembrada ao usá-los. Vamos continuar. Digamos que temos uma pequena hierarquia de classes:
public class Animal {
public void feed() {
System.out.println("Animal.feed()");
}
}
public class Pet extends Animal {
public void call() {
System.out.println("Pet.call()");
}
}
public class Cat extends Pet {
public void meow() {
System.out.println("Cat.meow()");
}
}
No topo da hierarquia estão simplesmente os animais: os animais de estimação são herdados deles. Os animais de estimação são divididos em 2 tipos – Cães e Gatos. Agora imagine que precisamos criar um método simples iterateAnimals()
. O método deve aceitar uma coleção de quaisquer animais ( Animal
, Pet
, Cat
, Dog
), iterar por todos os elementos e gerar algo no console a cada vez. Vamos tentar escrever este método:
public static void iterateAnimals(Collection<Animal> animals) {
for(Animal animal: animals) {
System.out.println("Еще один шаг в цикле пройден!");
}
}
Parece que o problema está resolvido! No entanto, como descobrimos recentemente, List<Cat>
ou não List<Dog>
somos List<Pet>
herdeiros List<Animal>
! Portanto, quando tentarmos chamar um método iterateAnimals()
com uma lista de gatos, receberemos um erro do compilador:
import java.util.*;
public class Main3 {
public static void iterateAnimals(Collection<Animal> animals) {
for(Animal animal: animals) {
System.out.println("Еще один шаг в цикле пройден!");
}
}
public static void main(String[] args) {
List<Cat> cats = new ArrayList<>();
cats.add(new Cat());
cats.add(new Cat());
cats.add(new Cat());
cats.add(new Cat());
//ошибка компилятора!
iterateAnimals(cats);
}
}
A situação não parece boa para nós! Acontece que teremos que escrever métodos separados para enumerar todos os tipos de animais? Na verdade, não, você não precisa :) E os curingas nos ajudarão com isso ! Resolveremos o problema com um método simples, usando a seguinte construção:
public static void iterateAnimals(Collection<? extends Animal> animals) {
for(Animal animal: animals) {
System.out.println("Еще один шаг в цикле пройден!");
}
}
Este é o curinga. Mais precisamente, este é o primeiro de vários tipos de curinga - “ estende ” (outro nome é Upper Bounded Wildcards ). O que esse design nos diz? Isso significa que o método recebe como entrada uma coleção de objetos de classe Animal
ou objetos de qualquer classe descendente Animal (? extends Animal)
. Animal
Em outras palavras, o método pode aceitar a coleção , Pet
, Dog
ou como entrada Cat
- não faz diferença. Vamos ter certeza de que isso funciona:
public static void main(String[] args) {
List<Animal> animals = new ArrayList<>();
animals.add(new Animal());
animals.add(new Animal());
List<Pet> pets = new ArrayList<>();
pets.add(new Pet());
pets.add(new Pet());
List<Cat> cats = new ArrayList<>();
cats.add(new Cat());
cats.add(new Cat());
List<Dog> dogs = new ArrayList<>();
dogs.add(new Dog());
dogs.add(new Dog());
iterateAnimals(animals);
iterateAnimals(pets);
iterateAnimals(cats);
iterateAnimals(dogs);
}
Saída do console:
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Еще один шаг в цикле пройден!
Criamos um total de 4 coleções e 8 objetos, e há exatamente 8 entradas no console. Tudo funciona muito bem! :) O curinga nos permitiu ajustar facilmente a lógica necessária com ligação a tipos específicos em um método. Eliminamos a necessidade de escrever um método separado para cada tipo de animal. Imagine quantos métodos teríamos se nosso aplicativo fosse usado em um zoológico ou clínica veterinária :) Agora vamos ver uma situação diferente. Nossa hierarquia de herança permanecerá a mesma: a classe de nível superior é Animal
, logo abaixo está a classe Pets Pet
e no próximo nível está Cat
e Dog
. Agora você precisa reescrever o método iretateAnimals()
para que ele funcione com qualquer tipo de animal, exceto cães . Collection<Animal>
Ou seja, deveria aceitar , Collection<Pet>
ou como entrada Collection<Cat>
, mas não deveria funcionar com Collection<Dog>
. Como podemos conseguir isso? Parece que a perspectiva de escrever um método separado para cada tipo está surgindo diante de nós novamente:/ De que outra forma podemos explicar nossa lógica ao compilador? E isso pode ser feito de forma muito simples! Aqui, os curingas virão em nosso auxílio novamente. Mas desta vez usaremos um tipo diferente - “ super ” (outro nome é Lower Bounded Wildcards ).
public static void iterateAnimals(Collection<? super Cat> animals) {
for(int i = 0; i < animals.size(); i++) {
System.out.println("Еще один шаг в цикле пройден!");
}
}
O princípio é semelhante aqui. A construção <? super Cat>
informa ao compilador que o método iterateAnimals()
pode tomar como entrada uma coleção de objetos da classe Cat
ou de qualquer outra classe ancestral Cat
. Cat
No nosso caso, a própria classe , seu ancestral - Pets
e o ancestral do ancestral - se enquadram nessa descrição Animal
. A classe Dog
não se enquadra nesta restrição e, portanto, tentar usar um método com uma lista List<Dog>
resultará em um erro de compilação:
public static void main(String[] args) {
List<Animal> animals = new ArrayList<>();
animals.add(new Animal());
animals.add(new Animal());
List<Pet> pets = new ArrayList<>();
pets.add(new Pet());
pets.add(new Pet());
List<Cat> cats = new ArrayList<>();
cats.add(new Cat());
cats.add(new Cat());
List<Dog> dogs = new ArrayList<>();
dogs.add(new Dog());
dogs.add(new Dog());
iterateAnimals(animals);
iterateAnimals(pets);
iterateAnimals(cats);
//ошибка компиляции!
iterateAnimals(dogs);
}
Nosso problema foi resolvido e, novamente, os curingas revelaram-se extremamente úteis :) Isso conclui a palestra. Agora você vê como o tópico genérico é importante ao aprender Java - passamos 4 palestras sobre isso! Mas agora você tem um bom domínio do assunto e poderá provar seu valor na entrevista :) E agora é a hora de voltar às tarefas! Boa sorte em seus estudos! :)
GO TO FULL VERSION