CodeGym /Blogue Java /Random-PT /Java Generics: como usar colchetes angulares na prática
John Squirrels
Nível 41
San Francisco

Java Generics: como usar colchetes angulares na prática

Publicado no grupo Random-PT

Introdução

A partir do JSE 5.0, os genéricos foram adicionados ao arsenal da linguagem Java.

O que são genéricos em java?

Os genéricos são o mecanismo especial de Java para implementar a programação genérica — uma maneira de descrever dados e algoritmos que permite trabalhar com diferentes tipos de dados sem alterar a descrição dos algoritmos. O site da Oracle possui um tutorial separado dedicado aos genéricos: " Lesson ". Para entender os genéricos, primeiro você precisa descobrir por que eles são necessários e o que eles fornecem. A seção " Por que usar genéricos? " do tutorial diz que alguns propósitos são verificação de tipo mais forte em tempo de compilação e eliminação da necessidade de conversões explícitas. Genéricos em Java: como usar colchetes angulares na prática - 1Vamos nos preparar para alguns testes em nosso amado compilador java online Tutorialspoint . Suponha que você tenha o seguinte código:

import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List list = new ArrayList();
		list.add("Hello");
		String text = list.get(0) + ", world!";
		System.out.print(text);
	}
}
Este código funcionará perfeitamente bem. Mas e se o chefe vier até nós e disser que "Olá, mundo!" é uma frase muito usada e que você deve retornar apenas "Olá"? Vamos remover o código que concatena ", world!" Isso parece inofensivo o suficiente, certo? Mas, na verdade, obtemos um erro AT COMPILE TIME:

error: incompatible types: Object cannot be converted to String
O problema é que em nossa List armazena Objects. String é descendente de Object (já que todas as classes Java herdam implicitamente Object ), o que significa que precisamos de uma conversão explícita, mas não adicionamos uma. Durante a operação de concatenação, o método estático String.valueOf(obj) será chamado usando o objeto. Eventualmente, ele chamará o método toString da classe Object . Em outras palavras, nossa List contém um Object . Isso significa que sempre que precisarmos de um tipo específico (não Object ), teremos que fazer a conversão de tipo nós mesmos:

import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List list = new ArrayList();
		list.add("Hello!");
		list.add(123);
		for (Object str : list) {
		    System.out.println("-" + (String)str);
		}
	}
}
No entanto, neste caso, como List recebe objetos, ele pode armazenar não apenas String s, mas também Integer s. Mas o pior é que o compilador não vê nada de errado aqui. E agora obteremos um erro AT RUN TIME (conhecido como "erro de tempo de execução"). O erro será:

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
Você deve concordar que isso não é muito bom. E tudo isso porque o compilador não é uma inteligência artificial capaz de sempre adivinhar corretamente a intenção do programador. O Java SE 5 introduziu genéricos para nos permitir informar ao compilador sobre nossas intenções — sobre quais tipos vamos usar. Corrigimos nosso código informando ao compilador o que queremos:

import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List<String> list = new ArrayList<>();
		list.add("Hello!");
		list.add(123);
		for (Object str : list) {
		    System.out.println("-" + str);
		}
	}
}
Como você pode ver, não precisamos mais de uma conversão para uma String . Além disso, temos colchetes em torno do argumento de tipo. Agora o compilador não nos permite compilar a classe até removermos a linha que adiciona 123 à lista, já que este é um Integer . E isso nos dirá. Muitas pessoas chamam os genéricos de "açúcar sintático". E eles estão certos, pois depois que os genéricos são compilados, eles realmente se tornam as mesmas conversões de tipo. Vejamos o bytecode das classes compiladas: uma que usa cast explícito e outra que usa genéricos: Genéricos em Java: como usar colchetes angulares na prática - 2Após a compilação, todos os genéricos são apagados. Isso é chamado de " apagamento de tipo". O apagamento de tipo e os genéricos são projetados para serem compatíveis com versões anteriores do JDK, permitindo simultaneamente que o compilador ajude com as definições de tipo em novas versões do Java.

Tipos brutos

Falando em genéricos, sempre temos duas categorias: tipos parametrizados e tipos brutos. Tipos brutos são tipos que omitem o "esclarecimento de tipo" entre colchetes angulares: Genéricos em Java: como usar colchetes angulares na prática - 3Tipos parametrizados, por outro lado, incluem um "esclarecimento": Genéricos em Java: como usar colchetes angulares na prática - 4Como você pode ver, usamos uma construção incomum, marcada por uma seta na captura de tela. Esta é uma sintaxe especial que foi adicionada ao Java SE 7. É chamada de " diamante ". Por que? Os colchetes angulares formam um losango: <> . Você também deve saber que a sintaxe do diamante está associada ao conceito de " inferência de tipo ". Afinal, o compilador, vendo <>à direita, olha para o lado esquerdo do operador de atribuição, onde encontra o tipo da variável cujo valor está sendo atribuído. Com base no que encontra nesta parte, ele entende o tipo do valor à direita. De fato, se um tipo genérico for fornecido à esquerda, mas não à direita, o compilador poderá inferir o tipo:

import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List<String> list = new ArrayList();
		list.add("Hello, World");
		String data = list.get(0);
		System.out.println(data);
	}
}
Mas isso mistura o novo estilo com genéricos e o antigo sem eles. E isso é altamente indesejável. Ao compilar o código acima, obtemos a seguinte mensagem:

Note: HelloWorld.java uses unchecked or unsafe operations
Na verdade, a razão pela qual você precisa adicionar um diamante aqui parece incompreensível. Mas aqui vai um exemplo:

import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List<String> list = Arrays.asList("Hello", "World");
		List<Integer> data = new ArrayList(list);
		Integer intNumber = data.get(0);
		System.out.println(data);
	}
}
Você deve se lembrar que ArrayList tem um segundo construtor que recebe uma coleção como argumento. E é aqui que algo sinistro se esconde. Sem a sintaxe do diamante, o compilador não entende que está sendo enganado. Com a sintaxe do diamante, sim. Portanto, a regra nº 1 é: sempre use a sintaxe do diamante com tipos parametrizados. Caso contrário, corremos o risco de perder onde estamos usando tipos brutos. Para eliminar os avisos "usa operações não verificadas ou inseguras", podemos usar a anotação @SuppressWarnings("unchecked") em um método ou classe. Mas pense por que você decidiu usá-lo. Lembre-se da regra número um. Talvez você precise adicionar um argumento de tipo.

Métodos genéricos Java

Os genéricos permitem criar métodos cujos tipos de parâmetro e tipo de retorno são parametrizados. Uma seção separada é dedicada a esse recurso no tutorial do Oracle: " Métodos genéricos ". É importante lembrar a sintaxe ensinada neste tutorial:
  • inclui uma lista de parâmetros de tipo entre colchetes angulares;
  • a lista de parâmetros de tipo vai antes do tipo de retorno do método.
Vejamos um exemplo:

import java.util.*;
public class HelloWorld {
	
    public static class Util {
        public static <T> T getValue(Object obj, Class<T> clazz) {
            return (T) obj;
        }
        public static <T> T getValue(Object obj) {
            return (T) obj;
        }
    }

    public static void main(String []args) {
		List list = Arrays.asList("Author", "Book");
		for (Object element : list) {
		    String data = Util.getValue(element, String.class);
		    System.out.println(data);
		    System.out.println(Util.<String>getValue(element));
		}
    }
}
Se você observar a classe Util , verá que ela possui dois métodos genéricos. Graças à possibilidade de inferência de tipo, podemos indicar o tipo diretamente ao compilador ou podemos especificá-lo nós mesmos. Ambas as opções são apresentadas no exemplo. A propósito, a sintaxe faz muito sentido se você pensar bem. Ao declarar um método genérico, especificamos o parâmetro de tipo ANTES do método, porque se declararmos o parâmetro de tipo após o método, a JVM não conseguiria descobrir qual tipo usar. Assim, primeiro declaramos que usaremos o parâmetro de tipo T e, em seguida, dizemos que retornaremos esse tipo. Naturalmente, Util.<Integer>getValue(element, String.class) falhará com um erro:tipos incompatíveis: Class<String> não pode ser convertido em Class<Integer> . Ao usar métodos genéricos, você deve sempre se lembrar do apagamento de tipo. Vejamos um exemplo:

import java.util.*;
public class HelloWorld {
	
    public static class Util {
        public static <T> T getValue(Object obj) {
            return (T) obj;
        }
    }

    public static void main(String []args) {
		List list = Arrays.asList(2, 3);
		for (Object element : list) {
		    System.out.println(Util.<Integer>getValue(element) + 1);
		}
    }
}
Isso vai funcionar muito bem. Mas apenas enquanto o compilador entender que o tipo de retorno do método que está sendo chamado é Integer . Substitua a instrução de saída do console pela seguinte linha:

System.out.println(Util.getValue(element) + 1);
Obtemos um erro:

bad operand types for binary operator '+', first type: Object, second type: int.
Em outras palavras, ocorreu o apagamento de tipo. O compilador vê que ninguém especificou o tipo, então o tipo é indicado como Object e o método falha com um erro.

classes genéricas

Não apenas os métodos podem ser parametrizados. As aulas também podem. A seção "Tipos genéricos" do tutorial da Oracle é dedicada a isso. Vamos considerar um exemplo:

public static class SomeType<T> {
	public <E> void test(Collection<E> collection) {
		for (E element : collection) {
			System.out.println(element);
		}
	}
	public void test(List<Integer> collection) {
		for (Integer element : collection) {
			System.out.println(element);
		}
	}
}
Tudo é simples aqui. Se usarmos a classe genérica, o parâmetro de tipo é indicado após o nome da classe. Agora vamos criar uma instância desta classe no método main :

public static void main(String []args) {
	SomeType<String> st = new SomeType<>();
	List<String> list = Arrays.asList("test");
	st.test(list);
}
Este código funcionará bem. O compilador vê que existe uma Lista de números e uma Coleção de Strings . Mas e se eliminarmos o parâmetro de tipo e fizermos isso:

SomeType st = new SomeType();
List<String> list = Arrays.asList("test");
st.test(list);
Obtemos um erro:

java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
Novamente, este é o apagamento de tipo. Como a classe não usa mais um parâmetro de tipo, o compilador decide que, como passamos uma List , o método com List<Integer> é o mais adequado. E falhamos com um erro. Portanto, temos a Regra nº 2: Se você tiver uma classe genérica, sempre especifique os parâmetros de tipo.

Restrições

Podemos restringir os tipos especificados em métodos genéricos e classes. Por exemplo, suponha que queremos que um contêiner aceite apenas um número como argumento de tipo. Esse recurso é descrito na seção Bounded Type Parameters do tutorial da Oracle. Vejamos um exemplo:

import java.util.*;
public class HelloWorld {
	
    public static class NumberContainer<T extends Number> {
        private T number;
    
        public NumberContainer(T number) { this.number = number; }
    
        public void print() {
            System.out.println(number);
        }
    }

    public static void main(String []args) {
		NumberContainer number1 = new NumberContainer(2L);
		NumberContainer number2 = new NumberContainer(1);
		NumberContainer number3 = new NumberContainer("f");
    }
}
Como você pode ver, restringimos o parâmetro de tipo à classe/interface Number ou seus descendentes. Observe que você pode especificar não apenas uma classe, mas também interfaces. Por exemplo:

public static class NumberContainer<T extends Number & Comparable> {
Os genéricos também suportam curingas Eles são divididos em três tipos: O uso de curingas deve seguir o princípio Get-Put . Pode ser expresso da seguinte forma:
  • Use um curinga de extensão quando você obtiver apenas valores de uma estrutura.
  • Use um super curinga quando você apenas colocar valores em uma estrutura.
  • E não use um curinga quando você deseja obter e colocar de/para uma estrutura.
Este princípio também é chamado de princípio Produtor Estende Consumidor Super (PECS). Aqui está um pequeno exemplo do código fonte para o método Collections.copy do Java : Genéricos em Java: como usar colchetes angulares na prática - 5E aqui está um pequeno exemplo do que NÃO VAI funcionar:

public static class TestClass {
	public static void print(List<? extends String> list) {
		list.add("Hello, World!");
		System.out.println(list.get(0));
	}
}

public static void main(String []args) {
	List<String> list = new ArrayList<>();
	TestClass.print(list);
}
Mas se você substituir extends por super , então está tudo bem. Como populamos a lista com um valor antes de exibir seu conteúdo, ela é um consumer . Assim, usamos super.

Herança

Os genéricos têm outra característica interessante: herança. A maneira como a herança funciona para genéricos é descrita em " Genéricos, Herança e Subtipos " no tutorial da Oracle. O importante é lembrar e reconhecer o seguinte. Não podemos fazer isso:

List<CharSequence> list1 = new ArrayList<String>();
Porque a herança funciona de maneira diferente com os genéricos: Genéricos em Java: como usar colchetes angulares na prática - 6E aqui está outro bom exemplo que falhará com um erro:

List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
Novamente, tudo é simples aqui. List<String> não é descendente de List<Object> , embora String seja descendente de Object . Para reforçar o que você aprendeu, sugerimos que você assista a uma vídeo aula do nosso Curso de Java

Conclusão

Assim, refrescamos nossa memória sobre os genéricos. Se você raramente tira o máximo proveito de suas capacidades, alguns dos detalhes ficam confusos. Espero que esta breve revisão tenha ajudado a refrescar sua memória. Para resultados ainda melhores, recomendo fortemente que você se familiarize com o seguinte material:
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION