Perguntas sobre a entrevista sobre genéricos Java (+ respostas)

Perguntas sobre a entrevista sobre genéricos Java (+ respostas)

1. Introdução

Neste artigo, veremos alguns exemplos de perguntas e respostas de entrevistas de genéricos Java.

Os genéricos são um conceito central em Java, introduzido pela primeira vez no Java 5. Por esse motivo, quase todas as bases de código Java as utilizam, quase garantindo que o desenvolvedor as encontre em algum momento. É por isso que é essencial entendê-los corretamente e é mais do que provável que eles sejam questionados durante um processo de entrevista.

2. Questões

Q1. O que é um parâmetro de tipo genérico?

Type é o nome de aclass ouinterface. Conforme implícito no nome,a generic type parameter is when a type can be used as a parameter in a class, method or interface declaration.

Vamos começar com um exemplo simples, sem genéricos, para demonstrar isso:

public interface Consumer {
    public void consume(String parameter)
}

Neste caso, o tipo de parâmetro do método do métodoconsume() éString. Não é parametrizado e não é configurável.

Agora vamos substituir nosso tipoString por um tipo genérico que chamaremos deT.. É nomeado assim por convenção:

public interface Consumer {
    public void consume(T parameter)
}

Quando implementamos nosso consumidor, podemos fornecer otype que queremos que ele consuma como argumento. Este é um parâmetro de tipo genérico:

public class IntegerConsumer implements Consumer {
    public void consume(Integer parameter)
}

Nesse caso, agora podemos consumir números inteiros. Podemos trocar estetype por tudo o que precisarmos.

Q2. Quais são algumas vantagens de usar tipos genéricos?

Uma vantagem do uso de genéricos é evitar custos e fornecer segurança de tipo. Isso é particularmente útil ao trabalhar com coleções. Vamos demonstrar isso:

List list = new ArrayList();
list.add("foo");
Object o = list.get(1);
String foo = (String) o;

Em nosso exemplo, o tipo de elemento em nossa lista é desconhecido para o compilador. Isso significa que a única coisa que pode ser garantida é que é um objeto. Então, quando recuperamos nosso elemento, um objeto é o que recuperamos. Como autores do código, sabemos que é umString,, mas temos que lançar nosso objeto em um para corrigir o problema explicitamente. Isso produz muito ruído e clichê.

Em seguida, se começarmos a pensar na sala para erros manuais, o problema de fundição se agravará. E se tivéssemos acidentalmente um número inteiro em nossa lista?

list.add(1)
Object o = list.get(1);
String foo = (String) o;

Nesse caso, obteríamos umClassCastException em tempo de execução, pois umInteger não pode ser convertido emString.

Agora, vamos tentar nos repetir, desta vez usando genéricos:

List list = new ArrayList<>();
list.add("foo");
String o = list.get(1);    // No cast
Integer foo = list.get(1); // Compilation error

Como podemos ver,by using generics we have a compile type check which prevents ClassCastExceptions and removes the need for casting.

The other advantage is to avoid code duplication. Sem genéricos, precisamos copiar e colar o mesmo código, mas para tipos diferentes. Com genéricos, não precisamos fazer isso. Podemos até implementar algoritmos que se aplicam a tipos genéricos.

Q3. O que é apagamento de tipo?

É importante perceber que as informações de tipo genérico estão disponíveis apenas para o compilador, não para o JVM. Em outras palavras, type erasure means that generic type information is not available to the JVM at runtime, only compile time.

O raciocínio por trás das principais opções de implementação é simples - preservando a compatibilidade com versões anteriores do Java. Quando um código genérico é compilado no bytecode, será como se o tipo genérico nunca existisse. Isso significa que a compilação irá:

  1. Substituir tipos genéricos por objetos

  2. Substitua tipos limitados (mais sobre isso em uma pergunta posterior) pela primeira classe vinculada

  3. Insira o equivalente de conversões ao recuperar objetos genéricos.

É importante entender o apagamento de tipo. Caso contrário, um desenvolvedor pode ficar confuso e pensar que seria capaz de obter o tipo em tempo de execução:

public foo(Consumer consumer) {
   Type type = consumer.getGenericTypeParameter()
}

O exemplo acima é um pseudo-código equivalente a como as coisas podem parecer sem o apagamento do tipo, mas, infelizmente, é impossível. Mais uma vez,the generic type information is not available at runtime.

Q4. Se um tipo genérico for omitido ao instanciar um objeto, o código ainda será compilado?

Como os genéricos não existiam antes do Java 5, é possível não usá-los. Por exemplo, os genéricos foram adaptados à maioria das classes Java padrão, como coleções. Se olharmos para a nossa lista da pergunta um, veremos que já temos um exemplo de omissão do tipo genérico:

List list = new ArrayList();

Apesar de ser capaz de compilar, ainda é provável que haja um aviso do compilador. Isso ocorre porque estamos perdendo a verificação do tempo de compilação extra que obtemos ao usar genéricos.

O ponto a lembrar é quewhile backward compatibility and type erasure make it possible to omit generic types, it is bad practice.

Q5. Como um método genérico difere de um tipo genérico?

A generic method is where a type parameter is introduced to a method,living within the scope of that method. Vamos tentar isso com um exemplo:

public static  T returnType(T argument) {
    return argument;
}

Nós usamos um método estático, mas também poderíamos ter usado um não estático se quiséssemos. Ao alavancar a inferência de tipo (abordada na próxima pergunta), podemos invocá-lo como qualquer método comum, sem precisar especificar nenhum argumento de tipo quando o fazemos.

Q6. O que é inferência de tipo?

Inferência de tipo é quando o compilador pode examinar o tipo de um argumento de método para inferir um tipo genérico. Por exemplo, se passamosT para um método que retornaT,, então o compilador pode descobrir o tipo de retorno. Vamos experimentar invocando nosso método genérico da pergunta anterior:

Integer inferredInteger = returnType(1);
String inferredString = returnType("String");

Como podemos ver, não há necessidade de um elenco e nem de passar nenhum argumento de tipo genérico. O tipo de argumento infere apenas o tipo de retorno.

Q7. O que é um parâmetro de tipo limitado?

Até agora, todas as nossas perguntas cobriram argumentos de tipos genéricos que são ilimitados. Isso significa que nossos argumentos de tipo genérico podem ser do tipo que queremos.

Quando usamos parâmetros limitados, estamos restringindo os tipos que podem ser usados ​​como argumentos de tipo genérico.

Por exemplo, digamos que queremos forçar nosso tipo genérico sempre a ser uma subclasse de animal:

public abstract class Cage {
    abstract void addAnimal(T animal)
}

Usando extends,, estamos forçandoT a ser uma subclasse de. animal. Poderíamos então ter uma gaiola de gatos:

Cage catCage;

Mas não poderíamos ter uma gaiola de objetos, pois um objeto não é uma subclasse de um animal:

Cage objectCage; // Compilation error


Uma vantagem disso é que todos os métodos de animal estão disponíveis para o compilador. Sabemos que nosso tipo o estende, para que pudéssemos escrever um algoritmo genérico que funcione em qualquer animal. Isso significa que não temos que reproduzir nosso método para diferentes subclasses de animais:

public void firstAnimalJump() {
    T animal = animals.get(0);
    animal.jump();
}

Q8. É possível declarar um parâmetro de tipo limitado múltiplo?

É possível declarar vários limites para nossos tipos genéricos. No nosso exemplo anterior, especificamos um único limite, mas também poderíamos especificar mais se desejar:

public abstract class Cage

No nosso exemplo, o animal é uma classe e comparável é uma interface. Agora, nosso tipo deve respeitar esses dois limites superiores. Se nosso tipo fosse uma subclasse de animal, mas não implementasse comparável, o código não seria compilado. It’s also worth remembering that if one of the upper bounds is a class, it must be the first argument.

Q9. O que é um tipo de curinga?

A wildcard type represents an unknown type. É detonado com um ponto de interrogação da seguinte forma:

public static consumeListOfWildcardType(List list)

Aqui, estamos especificando uma lista que pode ser de qualquertype. Poderíamos passar uma lista de qualquer coisa para esse método.

Q10. O que é um curinga de limite superior?

An upper bounded wildcard is when a wildcard type inherits from a concrete type. Isso é particularmente útil ao trabalhar com coleções e herança.

Vamos tentar demonstrar isso com uma classe de fazenda que armazenará animais, primeiro sem o tipo curinga:

public class Farm {
  private List animals;

  public void addAnimals(Collection newAnimals) {
    animals.addAll(newAnimals);
  }
}

Se tivéssemos várias subclasses de, animal, como gato e cachorro,, poderíamos fazer a suposição incorreta de que podemos adicionar todos eles à nossa fazenda:

farm.addAnimals(cats); // Compilation error
farm.addAnimals(dogs); // Compilation error

Isso ocorre porque o compilador espera uma coleção do tipo concreto animal,, não uma das subclasses.

Agora, vamos apresentar um caractere curinga de limite superior ao nosso método para adicionar animais:

public void addAnimals(Collection newAnimals)

Agora, se tentarmos novamente, nosso código será compilado. Isso ocorre porque agora estamos dizendo ao compilador para aceitar uma coleção de qualquer subtipo de animal.

Q11. O que é um caractere curinga ilimitado?

Um curinga ilimitado é um curinga sem limite superior ou inferior, que pode representar qualquer tipo.

Também é importante saber que o tipo curinga não é sinônimo de objeto. Isso ocorre porque um curinga pode ser de qualquer tipo, enquanto um tipo de objeto é especificamente um objeto (e não pode ser uma subclasse de um objeto). Vamos demonstrar isso com um exemplo:

List wildcardList = new ArrayList();
List objectList = new ArrayList(); // Compilation error


Novamente, a razão pela qual a segunda linha não é compilada é que é necessária uma lista de objetos, não uma lista de seqüências de caracteres. A primeira linha é compilada porque uma lista de qualquer tipo desconhecido é aceitável.

Q12. O que é um caractere curinga de limite inferior?

Um curinga de limite inferior é quando, em vez de fornecer um limite superior, fornecemos um limite inferior usando a palavra-chavesuper. Em outras palavras,a lower bounded wildcard means we are forcing the type to be a superclass of our bounded type. Vamos tentar isso com um exemplo:

public static void addDogs(List list) {
   list.add(new Dog("tom"))
}

Usandosuper,, podemos chamar addDogs em uma lista de objetos:

ArrayList objects = new ArrayList<>();
addDogs(objects);


Isso faz sentido, pois um objeto é uma superclasse de animal. Se não usássemos o curinga com limite inferior, o código não seria compilado, pois uma lista de objetos não é uma lista de animais.

Se pensarmos sobre isso, não poderíamos adicionar um cachorro a uma lista de nenhuma subclasse de animal, como gatos ou mesmo cães. Apenas uma superclasse de animal. Por exemplo, isso não seria compilado:

ArrayList objects = new ArrayList<>();
addDogs(objects);

Q13. Quando você escolheria usar um tipo de limite inferior vs. um tipo de limite superior?

Ao lidar com coleções, uma regra comum para selecionar entre curingas de limite superior ou inferior é o PECS. PECS significaproducer extends, consumer super.

Isso pode ser facilmente demonstrado através do uso de algumas interfaces e classes Java padrão.

Producer extends significa apenas que se você está criando um produtor de um tipo genérico, use a palavra-chaveextends. Vamos tentar aplicar este princípio a uma coleção, para ver por que faz sentido:

public static void makeLotsOfNoise(List animals) {
    animals.forEach(Animal::makeNoise);
}

Aqui, queremos chamarmakeNoise() em cada animal em nossa coleção. Isso significa que nossa coleção é um produtor,, pois tudo o que estamos fazendo com ela é fazer com que ela devolva animais para realizarmos nossa operação. Se nos livrarmos deextends, não poderíamos passar listas de cães, de gatos ou quaisquer outras subclasses de animais. Ao aplicar o princípio de extensão do produtor, temos a maior flexibilidade possível.

Consumer super significa o oposto deproducer extends. Significa apenas que, se estivermos lidando com algo cujos elementos consumidores, devemos usar a palavra-chavesuper. Podemos demonstrar isso repetindo nosso exemplo anterior:

public static void addCats(List animals) {
    animals.add(new Cat());
}

Estamos apenas adicionando à nossa lista de animais, portanto nossa lista de animais é um consumidor. É por isso que usamos a palavra-chavesuper. Isso significa que poderíamos passar uma lista de qualquer superclasse de animal, mas não uma subclasse. Por exemplo, se tentássemos passar uma lista de cães ou gatos, o código não seria compilado.

A última coisa a considerar é o que fazer se uma coleção for um consumidor e um produtor. Um exemplo disso pode ser uma coleção em que os elementos são adicionados e removidos. Nesse caso, um curinga ilimitado deve ser usado.

Q14. Há alguma situação em que as informações de tipo genérico estejam disponíveis em tempo de execução?

Há uma situação em que um tipo genérico está disponível em tempo de execução. É quando um tipo genérico faz parte da assinatura da classe assim:

public class CatCage implements Cage

Usando reflexão, obtemos este parâmetro de tipo:

(Class) ((ParameterizedType) getClass()
  .getGenericSuperclass()).getActualTypeArguments()[0];

Este código é um pouco quebradiço. Por exemplo, é dependente do parâmetro de tipo definido na superclasse imediata. Porém, demonstra que a JVM possui essas informações de tipo.