Java

Tutorial Java: Generics

Java Generics: Aprende los fundamentos de clases e interfaces genéricas para mejorar la flexibilidad y seguridad de tus aplicaciones.

Aprende Java y certifícate

Fundamentos de generics: clases e interfaces genéricas

Los generics sirven para crear clases, interfaces y métodos que operan con tipos parametrizados. Fueron introducidos en Java 5 para dar seguridad de tipos en tiempo de compilación y eliminar la necesidad de realizar conversiones explícitas de tipos (casting).

La idea fundamental detrás de los generics es permitir que los tipos (clases e interfaces) sean parámetros al definir clases, interfaces y métodos. Usando generics, se puede crear una única clase que funcione automáticamente con diferentes tipos de datos.

Una clase genérica se define utilizando uno o más parámetros de tipo entre ángulos (<>). La convención de nomenclatura sugiere usar letras mayúsculas simples como:

  • T - Tipo
  • E - Elemento
  • K - Clave
  • V - Valor
  • N - Número

Por ejemplo, una clase genérica simple se podría definir así:

public class Contenedor<T> {
    private T valor;
    
    public Contenedor(T valor) {
        this.valor = valor;
    }
    
    public T obtener() {
        return valor;
    }
    
    public void establecer(T nuevoValor) {
        this.valor = nuevoValor;
    }
}

Al instanciar esta clase, se proporciona un tipo real que reemplaza el parámetro T:

// Contenedor para Strings
Contenedor<String> contenedorTexto = new Contenedor<>("Hola");
String texto = contenedorTexto.obtener(); // No se necesita casting

// Contenedor para Integers
Contenedor<Integer> contenedorNumero = new Contenedor<>(42);
int numero = contenedorNumero.obtener(); // No se necesita casting

Se pueden definir clases genéricas con múltiples parámetros de tipo:

public class Par<K, V> {
    private K clave;
    private V valor;
    
    public Par(K clave, V valor) {
        this.clave = clave;
        this.valor = valor;
    }
    
    public K getClave() {
        return clave;
    }
    
    public V getValor() {
        return valor;
    }
}

Los parámetros de tipo pueden ser acotados (bounded), lo que restringe los tipos que pueden usarse como argumentos:

// T debe ser Number o una subclase de Number
public class Calculadora<T extends Number> {
    private T valor;
    
    public Calculadora(T valor) {
        this.valor = valor;
    }
    
    public double obtenerValorDoble() {
        return valor.doubleValue();
    }
}

Este ejemplo restringe el parámetro T a Number o cualquiera de sus subclases (como Integer, Double, etc.).

También se pueden definir interfaces genéricas:

public interface Comparable<T> {
    int compareTo(T other);
}

Una clase que implementa una interfaz genérica debe especificar un tipo concreto o mantener el parámetro genérico:

// Implementación con tipo concreto
public class Persona implements Comparable<Persona> {
    private String nombre;
    
    public Persona(String nombre) {
        this.nombre = nombre;
    }
    
    @Override
    public int compareTo(Persona otra) {
        return this.nombre.compareTo(otra.nombre);
    }
}

// Implementación manteniendo el parámetro genérico
public class ListaOrdenada<E extends Comparable<E>> {
    // Implementación
}

Los generics también admiten limitaciones múltiples utilizando el operador &:

// T debe implementar Comparable e Iterable
public class Ejemplo<T extends Comparable<T> & Iterable<?>> {
    // Implementación
}

La herencia funciona de manera especial con las clases genéricas. Si Hijo extiende Padre, entonces Contenedor<Hijo> no es subclase de Contenedor<Padre>. Esta relación se conoce como invarianza de los generics.

// Esto NO compila
Contenedor<Number> numeros = new Contenedor<Integer>(10); // Error

// Esto sí compila
Contenedor<Integer> enteros = new Contenedor<>(10);
Contenedor<? extends Number> numeros = enteros; // Usando wildcard

Las clases genéricas anidadas pueden acceder a los parámetros de tipo de la clase contenedora:

public class Exterior<T> {
    private T dato;
    
    public class Interior {
        public T obtenerDatoExterior() {
            return dato; // Accede al parámetro T de la clase Exterior
        }
    }
}

Métodos genéricos y wildcard types (? extends, ? super)

Los métodos genéricos permiten definir parámetros de tipo a nivel de método, sin necesidad de que toda la clase sea genérica.

La sintaxis básica de un método genérico es la siguiente:

public <T> T metodoGenerico(T parametro) {
    return parametro;
}

El parámetro de tipo <T> se declara antes del tipo de retorno para poder utilizar el mismo tipo tanto para el parámetro como para el valor de retorno, o incluso para variables locales dentro del método.

Los métodos genéricos se pueden llamar de dos formas:

// Forma explícita (especificando el tipo)
String resultado = this.<String>metodoGenerico("Hola");

// Forma implícita (inferencia de tipo)
String resultado = metodoGenerico("Hola");

En la mayoría de los casos, el compilador de Java puede inferir el tipo a partir de los argumentos proporcionados.

Un caso de uso habitual de los métodos genéricos es crear métodos de utilidad que funcionen con diferentes tipos:

public static <E> void imprimirArray(E[] array) {
    for (E elemento : array) {
        System.out.print(elemento + " ");
    }
    System.out.println();
}

// Uso
Integer[] numeros = {1, 2, 3, 4, 5};
String[] palabras = {"Hola", "Mundo"};

imprimirArray(numeros); // 1 2 3 4 5
imprimirArray(palabras); // Hola Mundo

Los métodos genéricos también pueden tener múltiples parámetros de tipo:

public static <K, V> Map<K, V> crearMapa(K clave, V valor) {
    Map<K, V> mapa = new HashMap<>();
    mapa.put(clave, valor);
    return mapa;
}

Por otro lado, los wildcard types (tipos comodín) sirven para trabajar con generics de manera flexible.

Un wildcard type se representa con el símbolo de interrogación ? y puede aparecer en tres formas:

  1. Wildcard sin límites (?): Representa "cualquier tipo"
  2. Wildcard con límite superior (? extends T): Representa "T o cualquier subtipo de T"
  3. Wildcard con límite inferior (? super T): Representa "T o cualquier supertipo de T"

El wildcard sin límites se utiliza cuando el código no depende del tipo específico:

public void procesarLista(List<?> lista) {
    for (Object elemento : lista) {
        System.out.println(elemento);
    }
}

Este método puede recibir una lista de cualquier tipo, pero solo puede acceder a los elementos como Object.

El wildcard con límite superior permite acceder a los métodos del tipo límite o sus supertipos:

public double sumarNumeros(List<? extends Number> numeros) {
    double suma = 0.0;
    for (Number numero : numeros) {
        suma += numero.doubleValue(); // Acceso seguro a métodos de Number
    }
    return suma;
}

Este método puede recibir una lista de Number, Integer, Double, etc., y acceder a los métodos definidos en Number.

Sin embargo, con un wildcard con límite superior no se pueden agregar elementos a la colección (excepto null), ya que no se puede garantizar el tipo específico:

public void agregarALista(List<? extends Number> numeros) {
    // numeros.add(Integer.valueOf(42)); // Error de compilación
    numeros.add(null); // Esto sí está permitido
}

El wildcard con límite inferior permite agregar elementos del tipo límite o cualquiera de sus subtipos:

public void agregarEnteros(List<? super Integer> destino) {
    destino.add(Integer.valueOf(42)); // Correcto
    destino.add(Integer.valueOf(10)); // Correcto
    // Integer valor = destino.get(0); // Error de compilación
}

Con este wildcard, se pueden agregar elementos pero no se puede garantizar el tipo exacto al recuperarlos (solo se puede acceder a ellos como Object).

Estos dos tipos de wildcard se resumen en el principio PECS (Producer Extends, Consumer Super):

  • Usar ? extends T cuando se quiere extraer elementos de una estructura (Producer)
  • Usar ? super T cuando se quiere añadir elementos a una estructura (Consumer)
  • Usar T exacto cuando se quiere hacer ambas operaciones

Un ejemplo práctico que ilustra este principio:

public static <T> void copiar(List<? extends T> origen, List<? super T> destino) {
    for (T elemento : origen) {
        destino.add(elemento);
    }
}

Este método permite copiar elementos de una lista origen a una lista destino, donde:

  • La lista origen actúa como productora de elementos de tipo T
  • La lista destino actúa como consumidora de elementos de tipo T

Los wildcards son útiles al combinar generics con herencia. Por ejemplo, permiten trabajar con estructuras como:

List<Integer> enteros = new ArrayList<>();
List<? extends Number> numeros = enteros; // Correcto

List<Number> numeros = new ArrayList<>();
List<? super Integer> destino = numeros; // Correcto

Pero los wildcards tienen algunas limitaciones:

  • No se pueden usar wildcards como parámetros de tipo en instanciaciones de clases
  • No se pueden usar wildcards como parámetros de tipo en métodos genéricos
  • Se deben utilizar con cuidado para mantener la legibilidad del código
// Incorrecto
Map<?, ?> mapa = new HashMap<?, ?>(); // Error de compilación

// Correcto
Map<?, ?> mapa = new HashMap<>();

Borrado de tipos y limitaciones en tiempo de ejecución

El borrado de tipos (type erasure) es diferente con respecto a lenguajes como C#, que implementan generics a nivel de tiempo de ejecución. Java utiliza elimina la información de tipo genérico durante la compilación para mantener la compatibilidad hacia atrás con versiones anteriores de la JVM.

Cuando el compilador de Java procesa el código con generics, realiza las siguientes transformaciones:

1. Reemplaza todos los parámetros de tipo por su tipo límite o por Object si no tienen límite 2. Inserta conversiones (casts) automáticas donde sea necesario 3. Genera métodos puente para preservar la semántica de polimorfismo en clases genéricas heredadas

Por ejemplo, el siguiente código genérico:

public class Caja<T> {
    private T contenido;
    
    public void guardar(T objeto) {
        this.contenido = objeto;
    }
    
    public T obtener() {
        return contenido;
    }
}

Se transforma, aproximadamente, en este código después del borrado de tipos:

public class Caja {
    private Object contenido;
    
    public void guardar(Object objeto) {
        this.contenido = objeto;
    }
    
    public Object obtener() {
        return contenido;
    }
}

Si la clase se definiera con un límite, como Caja<T extends Number>, entonces T se reemplazaría por Number en lugar de Object.

Esta técnica de implementación tiene implicaciones y limitaciones:

  • La información de tipos genéricos no está disponible en tiempo de ejecución

Una de las consecuencias más importantes del borrado de tipos es que la información sobre los parámetros de tipo no se conserva en tiempo de ejecución. Esto significa que no se puede verificar o acceder a esta información mediante reflection.

List<String> listaStrings = new ArrayList<>();
List<Integer> listaEnteros = new ArrayList<>();

// En tiempo de ejecución, ambas expresiones evalúan a true
System.out.println(listaStrings.getClass() == listaEnteros.getClass()); // true
  • No se puede usar el operador instanceof con tipos genéricos

Debido al borrado de tipos, no se puede comprobar si un objeto es una instancia de un tipo genérico específico:

public static boolean esListaDeStrings(Object obj) {
    // Esto NO compila
    return obj instanceof List<String>;
    
    // Solo se puede comprobar si es una List, sin especificar el tipo
    return obj instanceof List;
}
  • No se pueden crear arrays de tipos genéricos

Java no permite crear arrays de tipos genéricos directamente:

// Esto NO compila
List<String>[] arrayDeListas = new List<String>[10];

// Se debe usar un enfoque sin tipos específicos
List<?>[] arrayDeListas = new List<?>[10];

Esta restricción existe porque los arrays en Java conocen y verifican sus tipos de componentes en tiempo de ejecución, lo que es incompatible con el borrado de tipos.

  • No se pueden usar tipos primitivos como parámetros de tipo

Los parámetros de tipo en Java solo pueden ser tipos de referencia, no tipos primitivos:

// Esto NO compila
List<int> listaEnteros = new ArrayList<int>();

// Se deben usar las clases wrapper
List<Integer> listaEnteros = new ArrayList<>();
  • No se pueden crear instancias de parámetros de tipo

No se puede utilizar el operador new con un parámetro de tipo:

public <T> T crearInstancia() {
    // Esto NO compila
    return new T();
}

Para solucionar esto, se suelen usar técnicas como:

public <T> T crearInstancia(Class<T> clase) throws ReflectiveOperationException {
    return clase.getDeclaredConstructor().newInstance();
}
  • No se pueden declarar excepciones genéricas

Java no permite crear o capturar excepciones con parámetros de tipo:

// Esto NO compila
class MiExcepcion<T> extends Exception {}

// Esto tampoco compila
public <T extends Exception> void metodo() throws T {}

A pesar de estas limitaciones, hay técnicas y patrones para mitigarlas:

  • TypeToken y Super Type Token: Para preservar la información de tipo genérico en tiempo de ejecución, se puede utilizar una técnica conocida como "type token". La biblioteca Guava de Google proporciona una implementación con su clase TypeToken.
// Versión simplificada de un TypeToken
public abstract class TypeToken<T> {
    private final Type type;
    
    protected TypeToken() {
        Type superclass = getClass().getGenericSuperclass();
        this.type = ((ParameterizedType) superclass).getActualTypeArguments()[0];
    }
    
    public Type getType() {
        return type;
    }
}

// Uso
TypeToken<List<String>> typeToken = new TypeToken<>() {};
Type type = typeToken.getType(); // Preserva la información List<String>
  • Contenedores seguros para tipos: Se pueden diseñar estructuras que mantengan información de tipo en tiempo de ejecución:
public class SafeMap {
    private final Map<Class<?>, Object> map = new HashMap<>();
    
    public <T> void put(Class<T> type, T value) {
        map.put(type, value);
    }
    
    public <T> T get(Class<T> type) {
        return type.cast(map.get(type));
    }
}
  • Metaprogramación con reflection: Para casos como la creación de instancias de tipos genéricos, se puede utilizar reflection:
public class Factory<T> {
    private final Class<T> type;
    
    public Factory(Class<T> type) {
        this.type = type;
    }
    
    public T create() throws ReflectiveOperationException {
        return type.getDeclaredConstructor().newInstance();
    }
}

Patrones de diseño con generics: Type Token, Builder genérico, Factory

Type Token

El patrón Type Token permite capturar y preservar información de tipos genéricos en tiempo de ejecución, superando una de las limitaciones del borrado de tipos en Java.

La implementación se basa en que cuando se extiende una clase genérica o se implementa una interfaz genérica con un parámetro de tipo concreto, esa información se conserva en la estructura de la clase como un tipo genérico.

public class TypeToken<T> {
    private final Type type;
    
    protected TypeToken() {
        // Obtiene la clase actual y su superclase genérica
        Type superclass = getClass().getGenericSuperclass();
        // Extrae el parámetro de tipo real
        this.type = ((ParameterizedType) superclass).getActualTypeArguments()[0];
    }
    
    public Type getType() {
        return type;
    }
}

Para utilizar este TypeToken, se crea una subclase anónima con el tipo específico:

// Captura el tipo List<String>
TypeToken<List<String>> listStringToken = new TypeToken<>() {};
Type type = listStringToken.getType();
System.out.println(type); // java.util.List<java.lang.String>

// Captura el tipo Map<String, Integer>
TypeToken<Map<String, Integer>> mapToken = new TypeToken<>() {};
Type mapType = mapToken.getType();
System.out.println(mapType); // java.util.Map<java.lang.String, java.lang.Integer>

Este patrón es útil para operaciones como:

  • Deserialización JSON/XML tipada
  • Registro de convertidores específicos de tipo
  • Implementación de inyección de dependencias tipada

Una versión más avanzada, conocida como Super Type Token, puede manejar incluso tipos genéricos anidados más complejos:

public class TypeReference<T> {
    private final Type type;
    
    protected TypeReference() {
        // Obtiene la clase actual
        Class<?> parameterizedTypeClass = getClass();
        // Obtiene la superclase genérica
        Type genericSuperclass = parameterizedTypeClass.getGenericSuperclass();
        // Extrae el parámetro de tipo
        this.type = ((ParameterizedType) genericSuperclass).getActualTypeArguments()[0];
    }
    
    public Type getType() {
        return type;
    }
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass().getSuperclass() != obj.getClass().getSuperclass()) return false;
        TypeReference<?> other = (TypeReference<?>) obj;
        return type.equals(other.type);
    }
    
    @Override
    public int hashCode() {
        return type.hashCode();
    }
}

Builder Genérico

El patrón Builder se utiliza para construir objetos complejos paso a paso.

Una implementación genérica del patrón Builder utiliza lo que se conoce como "Curiously Recurring Generic Pattern" (CRGP):

public abstract class GenericBuilder<T, B extends GenericBuilder<T, B>> {
    // El objeto que se está construyendo
    protected final T objeto;
    
    // Constructor protegido que recibe el objeto a construir
    protected GenericBuilder(T objeto) {
        this.objeto = objeto;
    }
    
    // Método para obtener la instancia del builder concreto
    @SuppressWarnings("unchecked")
    protected B self() {
        return (B) this;
    }
    
    // Método para construir el objeto final
    public T build() {
        return objeto;
    }
}

Para usar este builder genérico, se crea una implementación concreta:

public class Persona {
    private String nombre;
    private int edad;
    private String email;
    
    // Constructor package-private para uso del builder
    Persona() {}
    
    // Getters
    public String getNombre() { return nombre; }
    public int getEdad() { return edad; }
    public String getEmail() { return email; }
    
    // Builder concreto
    public static class Builder extends GenericBuilder<Persona, Builder> {
        public Builder() {
            super(new Persona());
        }
        
        public Builder nombre(String nombre) {
            objeto.nombre = nombre;
            return self();
        }
        
        public Builder edad(int edad) {
            objeto.edad = edad;
            return self();
        }
        
        public Builder email(String email) {
            objeto.email = email;
            return self();
        }
    }
}

El uso de este builder es seguro en cuanto a tipos:

Persona persona = new Persona.Builder()
    .nombre("Ana")
    .edad(30)
    .email("ana@ejemplo.com")
    .build();

La ventaja de este enfoque genérico es que permite la herencia de builders manteniendo la fluidez de la API:

public class Empleado extends Persona {
    private String departamento;
    private double salario;
    
    Empleado() {}
    
    public String getDepartamento() { return departamento; }
    public double getSalario() { return salario; }
    
    public static class Builder extends Persona.Builder {
        public Builder() {
            super();
        }
        
        public Builder departamento(String departamento) {
            ((Empleado)objeto).departamento = departamento;
            return self();
        }
        
        public Builder salario(double salario) {
            ((Empleado)objeto).salario = salario;
            return self();
        }
        
        @Override
        public Empleado build() {
            return (Empleado)objeto;
        }
    }
}

Factory Genérico

El patrón Factory se utiliza para crear objetos sin especificar la clase exacta del objeto que se creará.

Una implementación simple de una Factory genérica:

public interface Factory<T> {
    T create();
}

Esta interfaz se puede implementar para diferentes tipos:

// Factory para crear ArrayList
public class ArrayListFactory<E> implements Factory<ArrayList<E>> {
    @Override
    public ArrayList<E> create() {
        return new ArrayList<>();
    }
}

// Factory para crear HashSet
public class HashSetFactory<E> implements Factory<HashSet<E>> {
    @Override
    public HashSet<E> create() {
        return new HashSet<>();
    }
}

Un uso más avanzado es un ServiceFactory genérico que crea servicios específicos:

// Interfaz base para servicios
public interface Service {
    void ejecutar();
}

// Implementaciones concretas
public class ServicioA implements Service {
    @Override
    public void ejecutar() {
        System.out.println("Ejecutando Servicio A");
    }
}

public class ServicioB implements Service {
    @Override
    public void ejecutar() {
        System.out.println("Ejecutando Servicio B");
    }
}

// Factory genérica para servicios
public class ServiceFactory<T extends Service> implements Factory<T> {
    private final Class<T> tipoServicio;
    
    public ServiceFactory(Class<T> tipoServicio) {
        this.tipoServicio = tipoServicio;
    }
    
    @Override
    public T create() {
        try {
            return tipoServicio.getDeclaredConstructor().newInstance();
        } catch (ReflectiveOperationException e) {
            throw new RuntimeException("No se pudo crear el servicio", e);
        }
    }
}

Uso de estas factorías:

Factory<ArrayList<String>> listFactory = new ArrayListFactory<>();
ArrayList<String> lista = listFactory.create();

Factory<ServicioA> servicioAFactory = new ServiceFactory<>(ServicioA.class);
ServicioA servicioA = servicioAFactory.create();
servicioA.ejecutar(); // "Ejecutando Servicio A"

Otro enfoque es el Abstract Factory genérico:

public interface AbstractFactory<T> {
    T crear(String tipo);
}

public class ColeccionFactory<E> implements AbstractFactory<Collection<E>> {
    @Override
    public Collection<E> crear(String tipo) {
        return switch (tipo.toLowerCase()) {
            case "list", "arraylist" -> new ArrayList<E>();
            case "set", "hashset" -> new HashSet<E>();
            case "linkedlist" -> new LinkedList<E>();
            case "treeset" -> new TreeSet<E>();
            default -> throw new IllegalArgumentException("Tipo desconocido: " + tipo);
        };
    }
}

Uso:

AbstractFactory<Collection<String>> factory = new ColeccionFactory<>();
Collection<String> lista = factory.crear("list");
Collection<String> conjunto = factory.crear("set");

Otras aplicaciones de generics en patrones de diseño

Repository genérico: Un patrón común en aplicaciones con acceso a datos:

public interface Repository<T, ID> {
    Optional<T> findById(ID id);
    List<T> findAll();
    T save(T entity);
    void deleteById(ID id);
}

public class ProductoRepository implements Repository<Producto, Long> {
    // Implementación específica para Producto
}

Decorator genérico: Permite envolver objetos manteniendo su tipo:

public abstract class Decorator<T> implements Component {
    protected T componente;
    
    public Decorator(T componente) {
        this.componente = componente;
    }
}

Singleton con parameter injection:

public class Configuracion<T> {
    private static Configuracion<?> instancia;
    private final T valor;
    
    private Configuracion(T valor) {
        this.valor = valor;
    }
    
    @SuppressWarnings("unchecked")
    public static <T> Configuracion<T> getInstance(T valor) {
        if (instancia == null) {
            instancia = new Configuracion<>(valor);
        }
        return (Configuracion<T>) instancia;
    }
    
    public T getValor() {
        return valor;
    }
}

CONSTRUYE TU CARRERA EN IA Y PROGRAMACIÓN SOFTWARE

Accede a +1000 lecciones y cursos con certificado. Mejora tu portfolio con certificados de superación para tu CV.

Plan mensual

19.00 € /mes

Precio normal mensual: 19 €
47 % DE DESCUENTO

Plan anual

10.00 € /mes

Ahorras 108 € al año
Precio normal anual: 120 €
Aprende Java online

Ejercicios de esta lección Generics

Evalúa tus conocimientos de esta lección Generics con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

Clases abstractas

Test

Listas

Código

Métodos de la clase String

Código

Streams: reduce()

Test

Polimorfismo

Código

Pattern Matching

Código

Streams: flatMap()

Test

Llamada y sobrecarga de funciones

Puzzle

Métodos referenciados

Test

Métodos de la clase String

Código

Representación de Fecha

Puzzle

Operadores lógicos

Test

Inferencia de tipos con var

Código

Tipos de datos

Código

Estructuras de iteración

Puzzle

Streams: forEach()

Test

Objetos

Puzzle

Funciones lambda

Test

Uso de Scanner

Puzzle

CRUD en Java de modelo Customer sobre un ArrayList

Proyecto

Tipos de variables

Puzzle

Streams: collect()

Puzzle

Operadores aritméticos

Puzzle

Arrays y matrices

Código

Clases y objetos

Código

Interfaz funcional Consumer

Test

Interfaces

Código

Enumeraciones Enums

Código

API java.nio 2

Puzzle

API Optional

Test

Interfaz funcional Function

Test

Encapsulación

Test

Interfaces

Código

Uso de API Optional

Puzzle

Representación de Hora

Test

Herencia básica

Test

Clases y objetos

Código

Interfaz funcional Supplier

Puzzle

HashMap

Puzzle

Sobrecarga de métodos

Test

Polimorfismo de tiempo de ejecución

Puzzle

OOP en Java

Proyecto

Sobrecarga de métodos

Código

Clases sealed

Código

Creación de Streams

Test

Records

Código

Encapsulación

Código

Streams: min max

Puzzle

Métodos avanzados de la clase String

Puzzle

Funciones

Código

Polimorfismo de tiempo de compilación

Test

Reto sintaxis Java

Proyecto

Conjuntos

Código

Estructuras de control

Código

Recursión

Código

Excepciones

Puzzle

Herencia avanzada

Puzzle

Estructuras de selección

Test

Uso de interfaces

Test

Operadores

Código

Variables

Código

HashSet

Test

Objeto Scanner

Test

Streams: filter()

Puzzle

Operaciones de Streams

Puzzle

Interfaz funcional Predicate

Puzzle

Streams: sorted()

Test

Configuración de entorno

Test

CRUD en Java de modelo Customer sobre un HashMap

Proyecto

Uso de variables

Test

Clases

Test

Streams: distinct()

Puzzle

Streams: count()

Test

ArrayList

Test

Datos de referencia

Test

Interfaces funcionales

Puzzle

Métodos básicos de la clase String

Test

Tipos de datos

Código

Clases abstractas

Código

Instalación

Test

Funciones

Código

Excepciones

Código

Estructuras de control

Código

Herencia de clases

Código

La clase Scanner

Código

Generics

Código

Streams: map()

Puzzle

Funciones y encapsulamiento

Test

Streams: match

Test

Gestión de errores y excepciones

Código

Datos primitivos

Puzzle

Todas las lecciones de Java

Accede a todas las lecciones de Java y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Instalación De Java

Introducción Y Entorno

Configuración De Entorno Java

Introducción Y Entorno

Tipos De Datos

Sintaxis

Variables

Sintaxis

Operadores

Sintaxis

Estructuras De Control

Sintaxis

Funciones

Sintaxis

Recursión

Sintaxis

Excepciones

Programación Orientada A Objetos

Clases Y Objetos

Programación Orientada A Objetos

Encapsulación

Programación Orientada A Objetos

Herencia

Programación Orientada A Objetos

Clases Abstractas

Programación Orientada A Objetos

Interfaces

Programación Orientada A Objetos

Sobrecarga De Métodos

Programación Orientada A Objetos

Polimorfismo

Programación Orientada A Objetos

La Clase Scanner

Programación Orientada A Objetos

Métodos De La Clase String

Programación Orientada A Objetos

Records

Programación Orientada A Objetos

Pattern Matching

Programación Orientada A Objetos

Inferencia De Tipos Con Var

Programación Orientada A Objetos

Enumeraciones Enums

Programación Orientada A Objetos

Generics

Programación Orientada A Objetos

Clases Sealed

Programación Orientada A Objetos

Listas

Framework Collections

Conjuntos

Framework Collections

Mapas

Framework Collections

Funciones Lambda

Programación Funcional

Interfaz Funcional Consumer

Programación Funcional

Interfaz Funcional Predicate

Programación Funcional

Interfaz Funcional Supplier

Programación Funcional

Interfaz Funcional Function

Programación Funcional

Métodos Referenciados

Programación Funcional

Creación De Streams

Programación Funcional

Operaciones Intermedias Con Streams: Map()

Programación Funcional

Operaciones Intermedias Con Streams: Filter()

Programación Funcional

Operaciones Intermedias Con Streams: Distinct()

Programación Funcional

Operaciones Finales Con Streams: Collect()

Programación Funcional

Operaciones Finales Con Streams: Min Max

Programación Funcional

Operaciones Intermedias Con Streams: Flatmap()

Programación Funcional

Operaciones Intermedias Con Streams: Sorted()

Programación Funcional

Operaciones Finales Con Streams: Reduce()

Programación Funcional

Operaciones Finales Con Streams: Foreach()

Programación Funcional

Operaciones Finales Con Streams: Count()

Programación Funcional

Operaciones Finales Con Streams: Match

Programación Funcional

Api Optional

Programación Funcional

Api Java.nio 2

Entrada Y Salida (Io)

Api Java.time

Api Java.time

Ecosistema Jakarta Ee De Java

Frameworks Para Java

Accede GRATIS a Java y certifícate

Certificados de superación de Java

Supera todos los ejercicios de programación del curso de Java y obtén certificados de superación para mejorar tu currículum y tu empleabilidad.

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender el concepto de generics y su utilidad en Java.
  • Identificar las ventajas de usar generics para mejorar la flexibilidad y seguridad de los tipos.
  • Implementar clases e interfaces genéricas en aplicaciones.
  • Analizar limitaciones y beneficios del borrado de tipos.
  • Aplicar patrones de diseño utilizando generics: Type Token, Builder, Factory.