image

Access unlimited bootcamps and 650+ courses forever

60
%OFF
Article image

LL

Luiz Lopes23/10/2025 23:45
Share

O Poder da Verbosiade: Generics e Reflection na Prática

  • #Java
  • #Spring
  • #JPA

O Pesadelo dos iniciantes e estudantes: 😱

Não é segredo para ninguém que Java tem fama de ser a linguagem mais "chata", verbosa e complicada. Quando um dev migra de uma linguagem com pouca tipagem ou até mesmo quando um dev é inserido no mundo da programação pela linguagem Java (acontece muito nas faculdades 😂), é evidente a frustração desse dev em ter que escrever aquele famoso template public static void main(String[] args) + System.out.println("Hello World") apenas para imprimir uma linha no terminal, enquanto em linguagens como Python, você só escreve print("Hello World") e a mágica está feita. ✨

Mas veja bem, acredite em mim quando digo que essa "chatice" pode ser sua melhor amiga. Depois que seu código tem dezenas de milhares de linhas e você precisa chamar aquela função de validação que recebe 3 parâmetros diferentes, você vai agradecer por saber exatamente que tipo de valor colocar nesses parâmetros.

Esse é o poder da Tipagem Estática (Static Typing), onde os tipos são verificados em tempo de compilação, ao contrário da Tipagem Dinâmica (Dynamic Typing) do Python ou JavaScript, onde os erros só aparecem em tempo de execução. Em um sistema dinâmico, um TypeError: 'NoneType' object is not callable ou undefined is not a function pode explodir em produção, às 3 da manhã, apenas porque um path de código obscuro foi ativado e passou um null para uma função que esperava um objeto (Alguém vai se identificar, pode ter certeza).

Type Safety: O Pilar da Confiança

Type Safety, esse é o termo que nós estamos procurando! 🚀

Type Safety é o termo utilizado para descrever um sistema de linguagem que garante a tipagem do seu código, evitando problemas de tipagem. Em resumo, Type Safety é quando a linguagem te impede de fazer 1 + "1" e quebrar todo o seu sistema (ou, pior, te dar um resultado todo esquisito como "11"). 💥

Vou dar um exemplo da utilidade disso. Imagine que você criou uma função cadastrarCliente(String nome, String cpf), e depois de 2 meses você precisa utilizar essa função novamente; você faz uma chamada dela e escreve cadastrarCliente("John", 12345678900). A primeira coisa que irá acontecer, provavelmente, é que sua IDE nem vai deixar você fazer isso, visto que você passou um número no lugar que esperava uma String, e logo vai avisar que tem algo errado. Caso você não use uma IDE que avise disso, o erro irá aparecer na compilação do código. No fim das contas, você é impedido de cometer um erro que, em tese, é meio bobo.

image

O Mundo Pré-Java 5: O Caos dos Object

Para realmente apreciar o que vem a seguir, precisamos de uma rápida lição de história. Antes do Java 5 (lançado em 2004), Generics não existiam. Como fazíamos para ter coleções flexíveis? Nós usávamos a classe-mãe de tudo: Object.

O código era assim:

// O pesadelo do Java 1.4
List minhaLista = new ArrayList();
minhaLista.add("Uma String");
minhaLista.add("Outra String");
minhaLista.add(new Integer(123)); // O compilador acha isso OK!

// ... em outra parte do sistema ...
for (int i = 0; i < minhaLista.size(); i++) {
  // 1. Cast explícito e feio
  // 2. Risco de explosão em runtime
  try {
      String item = (String) minhaLista.get(i); // <-- Aqui ta o monstro!
      System.out.println(item.toUpperCase());
  } catch (ClassCastException e) {
      System.err.println("ERRO: Um não-String foi encontrado!");
  }
}

Esse código era um campo minado. A responsabilidade de lembrar o que estava dentro da lista era 100% do desenvolvedor, não do compilador (atenção desenvolvedores JavaScript).

Agora, como manter o contrato rígido e robusto do Type Safety sem sacrificar a flexibilidade de coleções e componentes? A resposta começa com a ferramenta mais poderosa e subestimada dos fundamentos Java: Generics. Vamos ir mais fundo, estamos apenas começando! 🏊‍♂️

Generics na prática:

Aqui vamos começar a nos aprofundar na linguagem, vamos ver o que o seu instrutor não mostrou no curso de SpringBoot (caso ele não seja da DIO obviamente). 😉

Generics é a ferramenta que vai nos permitir escrever código que opera mesmo com diferentes tipos de dados, ou seja, você não vai precisar saber exatamente que tipo que você vai receber para poder garantir a segurança dos tipos de dados no sistema.

Com Generics, nosso exemplo anterior fica assim: List<String> minhaLista = new ArrayList<>(); minhaLista.add(new Integer(123)); // ERRO DE COMPILAÇÃO!

O erro mudou de um ClassCastException em produção para um erro sublinhado em vermelho na sua IDE. Isso é uma evolução gigantesca.

Não entendeu nada? Ok, compreensível, é realmente meio esquisito para quem nunca viu isso sendo utilizado de verdade, então vamos partir para a prática, porque, afinal de contas, é isso que interessa: 💻

Cenário Real: Padronizando a Camada de Serviço

Imagine um e-commerce. Você tem UserService, ProductService, OrderService. Todos eles precisam de métodos básicos: findById, save, delete.

A primeira coisa que você pensa para resolver isso é simplesmente copiar e colar essa lógica em cada classe de serviço. Já se você for um pouco mais "malandro" e souber usar Generics, você pode optar por uma abordagem mais flexível para criar um contrato: 👇

// 1. O Contrato Genérico
// Define as operações que TODOS os serviços terão.
// T = O tipo da Entidade (User, Product)
// ID = O tipo do ID da Entidade (Long, String, UUID)
public interface GenericService<T, ID> {
Optional<T> findById(ID id);
List<T> findAll();
T save(T entity);
void deleteById(ID id);
}

// 2. A Implementação Base Abstrata
// Centraliza a lógica comum (validações, logs, acesso ao repositório)
public abstract class AbstractGenericService<T, ID> implements GenericService<T, ID> {

// Usamos um repositório genérico (que veremos depois)
private final JpaRepository<T, ID> repository;

public AbstractGenericService(JpaRepository<T, ID> repository) {
  this.repository = repository;
}

@Override
public Optional<T> findById(ID id) {
  if (id == null) {
    throw new IllegalArgumentException("ID não pode ser nulo.");
  }
  return repository.findById(id);
}
  
@Override
public T save(T entity) {
  // Lógica de validação ou log centralizada aqui
  return repository.save(entity);
}
  
// ... outras implementações ...
}

// 3. O Serviço Concreto (limpo e focado)
@Service
public class UserService extends AbstractGenericService<User, Long> {
  
public UserService(UserRepository userRepository) {
  // A mágica: passamos o repositório CONCRETO para o construtor ABSTRATO
  super(userRepository);
}

// A classe UserService agora foca APENAS em regras de negócio
// específicas de usuário (ex: trocarSenha, validarEmail)
}

Pronto! Você eliminou um bocado de redundância! 🎉 Imagina só se você tivesse que repetir a bendita função findById() em cada classe de serviço que você fosse criar? Nada legal, né? Agora, quem sabe o que está fazendo, vai querer usar Generics da forma correta e criar esse contrato robusto para não ter que ficar jogando código fora.

Expandindo: A Abstração do Controller

Outra coisa que você pode fazer, se você tem um Controller que chama a camada de serviço você pode abstrair isso também, olha que top isso aqui: 🤩

/**
 * Controlador abstrato genérico que fornece endpoints CRUD básicos.
 *
 * @param <T>  O tipo da Entidade (ex: User)
 * @param <ID> O tipo do ID da Entidade (ex: Long)
 * @param <S>  O tipo do Serviço, que DEVE implementar GenericService<T, ID>
 */
public abstract class AbstractGenericController<T, ID, S extends GenericService<T, ID>> {

// O serviço específico (ex: UserService) será injetado
protected final S service;

/**
 * Injeta o serviço genérico específico via construtor.
 */
public AbstractGenericController(S service) {
  this.service = service;
}

/**
 * Endpoint GET para buscar todos os registros.
 * Mapeamento: GET /recurso
 */
@GetMapping
public ResponseEntity<List<T>> findAll() {
  return ResponseEntity.ok(service.findAll());
}

@GetMapping("/{id}")
public ResponseEntity<T> findById(@PathVariable ID id) {
    return service.findById(id)
            .map(ResponseEntity::ok)
            .orElse(ResponseEntity.notFound().build());
}

// ... outros métodos CRUD ...
}

Massa, né? Você pode fazer uma Classe Genérica para o seu Controller que vai usar a abstração da classe genérica de Serviço e no final tudo se conecta e você economiza 1000 horas de código repetitivo que no final, não ia te agregar em nada ficar reescrevendo.

Note a declaração: <S extends GenericService<T, ID>>. Isso é chamado de Bounded Type Parameter (Parâmetro de Tipo Limitado). Estamos dizendo ao Java: "Eu aceito qualquer tipo S, desde que S seja um subtipo de GenericService". Isso nos dá o melhor dos dois mundos: flexibilidade (aceita UserService, ProductService, etc.) e segurança (o compilador garante que qualquer S terá os métodos findById, save, etc.).

Generics Avançados: O Poder dos Wildcards (PECS)

Para realmente dominar Generics, você precisa entender os wildcards (caracteres curinga), como ? extends T e ? super T. Eles são usados para criar APIs flexíveis que lidam com hierarquias de tipos.

Existe um mnemônico famoso: PECS: Producer Extends, Consumer Super.

  1. ? extends T (Producer / Covariância): "Qualquer tipo que estende T". Use isso quando sua coleção for um produtor (você só vai ler dela).
  • Exemplo: double calcularTotal(List<? extends Produto> produtos)
  • Isso permite que seu método aceite List<ProdutoDigital> e List<ProdutoFisico>, já que você só vai ler (.getPreco()) de cada item.
  1. ? super T (Consumer / Contravariância): "Qualquer tipo que é supertipo de T". Use isso quando sua coleção for um consumidor (você só vai adicionar nela).
  • Exemplo: void adicionarNovosProdutos(List<? super ProdutoFisico> lista)
  • Isso permite que você passe uma List<ProdutoFisico>, uma List<Produto> ou até uma List<Object> para este método, pois todos eles podem consumir (aceitar via .add()) um ProdutoFisico.

Dominar isso é o que separa um usuário de Generics de um mestre em APIs.

Generics e Design Patterns: O Casamento Perfeito

Mas não paramos por aí. Você, como um programador esperto, pode juntar esse conhecimento do uso prático de Generics com o entendimento sobre Design Patterns e criar uma abstração muito massa para, por exemplo, enviar notificações para o seu usuário! 📲 Isso, é claro, sem ter que ficar repetindo um monte de código chato para gerar uma notificação diferente para cada funcionalidade que você tem.

Você pode usar os padrões Strategy e Factory para isso. A ideia central é implementar o Princípio Aberto/Fechado (Open/Closed Principle) do SOLID: seu código deve ser aberto para extensão (você pode adicionar novos tipos de notificação) mas fechado para modificação (sem mexer no serviço principal que envia a notificação).

Cara, mas aí você bate o olho nisso e pensa: "não é só fazer um if/else?". Na verdade é, mas quando seu cliente pedir para seu sistema enviar 10 tipos de notificações com 10 diferentes mensagens que vão precisar de diferentes dados de várias partes do sistema, vai por mim, ter um padrão vai deixar seu código mil vezes mais legível (experiência própria). 💡

Veja uma implementação robusta:

// 1. O Contrato da Estratégia
public interface NotificationContentStrategy {
  NotificationContent generate(Map<String, Object> context);
  NotificationType getType();
}

public enum NotificationType {
  WELCOME_EMAIL,
  PASSWORD_RESET;
}

// 2. Estratégias Concretas (os algoritmos)
@Component
public class WelcomeNotificationStrategy implements NotificationContentStrategy {
  @Override
  public NotificationContent generate(Map<String, Object> context) {
      String userName = (String) context.get("userName");
      String subject = "Bem-vindo(a)!";
      String body = "Olá, " + userName + "! Seu cadastro foi um sucesso.";
      return new NotificationContent(subject, body);
  }
  
  @Override
  public NotificationType getType() { return NotificationType.WELCOME_EMAIL; }
}

@Component
public class PasswordResetStrategy implements NotificationContentStrategy {
  @Override
  public NotificationContent generate(Map<String, Object> context) {
      String resetLink = (String) context.get("link");
      String subject = "Redefinição de Senha";
      String body = "Clique aqui para redefinir sua senha: " + resetLink;
      return new NotificationContent(subject, body);
  }
  
  @Override
  public NotificationType getType() { return NotificationType.PASSWORD_RESET; }
}

// 3. A Factory (O Seletor de Estratégias)
@Component
public class NotificationContentFactory {
  private final Map<NotificationType, NotificationContentStrategy> strategyMap;

  // Mágica do Spring: injeta TODAS as implementações de
  // NotificationContentStrategy em uma Lista.
  @Autowired
  public NotificationContentFactory(List<NotificationContentStrategy> strategies) {
      strategyMap = strategies.stream()
          .collect(Collectors.toMap(NotificationContentStrategy::getType, s -> s));
  }

  public NotificationContentStrategy getStrategy(NotificationType type) {
      NotificationContentStrategy strategy = strategyMap.get(type);
      if (strategy == null) {
          throw new IllegalArgumentException("Tipo de notificação não suportado: " + type);
      }
      return strategy;
  }
}

// 4. O Serviço Orquestrador (quem usa a Factory)
@Service
public class NotificationBuilderService {
  
  private final NotificationContentFactory factory;

  @Autowired
  public NotificationBuilderService(NotificationContentFactory factory) {
      this.factory = factory;
  }

  public NotificationContent build(NotificationType type, Map<String, Object> context) {
      // 1. Pega a estratégia certa na Factory
      NotificationContentStrategy strategy = factory.getStrategy(type);
      // 2. Executa a estratégia
      return strategy.generate(context);
  }
}

Se amanhã você precisar de uma OrderConfirmationStrategy, você só precisa criar a classe. O NotificationBuilderService nem saberá que ela existe. Isso é arquitetura de software de verdade. (Caso queira ver mais é só ir no website do Refactoring Guru https://refactoring.guru/, caso não tenha notado eu coloquei os links como âncoras nos nomes dos Patterns)

Bom, você já viu como construir classes super poderosas utilizando Generics e Design Patterns na sua linguagem preferida, que obviamente é Java. Agora, vamos cavar um pouco mais fundo, vamos ver o Java de gente grande... 🕵️‍♂️

Beans, Reflection API e seu framework por trás dos panos:

Essa é a parte mais profunda: como seu framework (vamos pensar no Spring, visto que é o mais famoso) é tão poderoso e como ele atua por baixo dos panos para achar seus serviços, beans, fazer a injeção das suas dependências e tudo mais. Para entender legal o que vou explicar aqui, talvez seja necessário entender o papel da Reflection API, então aqui vai um artigo para você ler caso sinta a necessidade: https://www.baeldung.com/java-reflection 📖

Vou dividir essa explicação em 3 atos:

  1. Busca pelos Beans (Scan)
  2. A análise (Parsing)
  3. A montagem (Injection)

Ato 1: A Caça aos Beans com Reflection

Primeiramente, o seu framework não vai conhecer nada sobre suas classes. Ele usa o ClassLoader para varrer os pacotes (.class files) que você mandou ele escanear. E como o framework faz isso? A resposta é Reflection API.

O processo é mais ou menos assim:

  1. O que vou procurar? Beans (São declarados com as anotações @Component, @Service, @Repository, @Configuration, etc.)
  2. O framework encontra seu UserService.class.
  3. Usa Reflection para inspecioná-la: UserService.class.getAnnotations().
  4. Ele encontra @Service. A partir disso, vê que sua classe UserService é um candidato a Bean e a marca, guardando sua "receita" (chamada de BeanDefinition).

Até aí, não é criado nenhum objeto, apenas são marcadas suas classes que podem se tornar Beans, mas vamos continuar... 🏃‍♂️

Ato 2: A Análise e o "Drible" no Type Erasure

Nessa etapa, seu framework (o Contêiner de Injeção de Dependência IoC) tem uma lista de BeanDefinitions (as receitas) e vai ter que ver como elas se encaixam. É aqui que enfrentamos um problema: o Type Erasure.

Veja o construtor:

public UserService(UserRepository userRepository) {
// ...
}

E a interface do repositório:

public interface UserRepository extends JpaRepository<User, Long> { }

Em tempo de execução, a JVM "apaga" os tipos genéricos, ou seja, um List<String> vira somente List. Se isso fosse 100% verdade, o Spring olharia para UserRepository e veria apenas extends JpaRepository. Como ele saberia que deve gerenciar User e não Product?

É aqui que a Reflection vai entrar novamente. O Type Erasure apaga os tipos das instâncias e variáveis, mas a assinatura da classe (os metadados no .class) é preservada!

Para burlar o Type Erasure, o framework utiliza Reflection avançada: 🛠️

  1. O framework analisa a interface UserRepository.class.
  2. Ele usa um método de Reflection como getGenericInterfaces(). Isso retorna as interfaces que UserRepository estende, mas com a informação genérica intacta!
  3. O retorno não é um Class, mas sim um objeto do tipo Type, especificamente um ParameterizedType.
  4. O framework então usa esse ParameterizedType e chama seu método getActualTypeArguments().
  5. Bingo! esse método retorna um array de Type com [User.class, Long.class].

Com essa informação, o Spring Data JPA pode, em tempo de execução, criar dinamicamente uma implementação concreta de UserRepository que sabe exatamente como fazer SELECT * FROM user WHERE id = ?.

Ato 3: A Montagem (Injeção de Dependência)

Com o mapa de dependências citado anteriormente em mãos, o framework vai começar a construir os objetos (Beans).

  1. Ele vê que UserService precisa de UserRepository. Ele primeiro cria a implementação do UserRepository (usando as informações que conseguiu com Reflection).
  2. Em seguida, ele vai criar o UserService. É aqui que entram os tipos de injeção. O Spring vai usar Reflection para invocar o construtor UserService(UserRepository userRepository). Como ele já tem o Bean do UserRepository pronto, ele o passa como argumento. (Isso é Constructor Injection, a forma recomendada, e não aquele seu @Autowired feio haha).
  3. O objeto UserService recém-criado, já com sua dependência injetada, é guardado no contêiner (numa área chamada "singleton cache") e está pronto para ser usado.

Bônus: Proxies e a Mágica do @Transactional

Quando o Spring injeta o UserRepository, ele raramente te dá o objeto real. Ele te dá um Proxy Dinâmico. É um objeto "falso" criado em tempo de execução (usando java.lang.reflect.Proxy ou bibliotecas como CGLIB) que se parece com um UserRepository.

Quando você chama userRepository.save(user), você na verdade está chamando o Proxy. O Proxy, então, pode fazer coisas antes e depois de chamar o método real. Por exemplo, se seu método de serviço está anotado com @Transactional, o Proxy vai:

  1. Receber a chamada.
  2. Abrir uma transação com o banco de dados.
  3. Chamar o método real (o seu código).
  4. Se o método terminar sem erro, ele commita a transação.
  5. Se o método lançar uma exceção, ele dá rollback na transação.

É por isso que a Injeção de Dependência é tão poderosa. Ela permite que o framework gerencie o ciclo de vida dos seus objetos e insira comportamentos (segurança, transações, logs) de forma declarativa, sem poluir seu código de negócio.

E, por fim, essa é a orquestração inteligente que seu framework vai fazer por ti, para você poder escrever classes de Services, Controllers, Repositories, Components e até Configurations de forma dinâmica e fácil! Caso queira ver mais dessas coisas mirabolantes é só fuçar na documentação do Spring 📚: https://docs.spring.io/spring-framework/reference/core/beans.html

Conclusão

Ufa! Muita coisa para absorver de uma vez... 🤯 Eu concordo, mas esses são conhecimentos para pessoas realmente curiosas, que não se contentam em só aceitar como as coisas são feitas, e querem realmente saber como construir e inovar. São conhecimentos que devem ser construídos como base, para que toda a estrutura construída posteriormente fique firme e não venha a desmoronar (estou falando do seu sistema). 🏗️

Para os que leram até aqui (leram mesmo, não só pularam para o final), meus parabéns. De verdade, recomendo guardar os links e documentações citados nesse artigo para poder ler mais tarde, pois é fácil esquecer, então é sempre válido relembrar!

Para finalizar, peço a todos que, se possível e se quiserem, visitem meus perfis nas redes sociais (linkedin e github) para se conectarem, dessa forma talvez, futuramente possamos trocar ideias e fazer um network produtivo. Obrigado! 👋

https://www.linkedin.com/in/luiz-guilherme-zanella-lopes-b929791b0/

https://github.com/Lugui14

Share
Recommended for you
PcD Tech Bradesco - Java & QA Developer
Riachuelo - Primeiros Passos com Java
GFT Start #7 - Java
Comments (0)