Skip to content
IRC-CodingIRC-Coding
GenericsType SafetyType ParametersWildcardsBounded TypesAlgoritmosFundamentos

Clases Genéricas: Generics, Type Safety y Parameters

Clases genéricas para reutilización type-safe. Generics, Type Erasure, Wildcards, Bounded Types con ejemplos en Java, C# y C++.

S

schutzgeist

8 min read
Clases Genéricas: Generics, Type Safety y Parameters

Clases Genéricas: Generics, Seguridad de Tipos y Parámetros de Tipo

Este artículo es una explicación exhaustiva de las clases genéricas incluyendo Generics, seguridad de tipos, wildcards y parámetros de tipo acotados con ejemplos prácticos.

En Resumen

Las clases genéricas son plantillas de tipo que proporcionan estructuras de datos y algoritmos reutilizables y seguros en cuanto a tipos mediante parámetros de tipo. Ejemplos incluyen List<T>, Map<K,V> o C++ Templates.

Descripción Técnica Compacta

Las clases genéricas encapsulan el comportamiento independientemente del tipo de dato concreto e se instancian mediante argumentos de tipo. Permiten reutilización segura de tipos sin duplicación de código.

Implementación en diferentes lenguajes:

Java Generics

  • Type Erasure: La información de tipos se elimina en tiempo de compilación
  • Ejecución: Solo tipos sin procesar, sin tipos genéricos
  • Wildcards: ? extends Number, ? super Number
  • Tipos Acotados: <T extends Number>

C# Generics

  • Reified Generics: La información de tipos se preserva en tiempo de ejecución
  • Constraints: where T : class, new()
  • Covarianza/Contravarianza: out T, in T
  • Rendimiento: Sin overhead de boxing-unboxing

C++ Templates

  • Tiempo de Compilación: Se genera una versión especializada para cada tipo
  • Zero Cost: Sin overhead en tiempo de ejecución
  • Template Metaprogramming: Turing-completo
  • SFINAE: Substitution Failure Is Not An Error

Conceptos Centrales:

  • Invarianza: List<String> no es un List<Object>
  • Covarianza: Producer extends (puede suministrar)
  • Contravarianza: Consumer super (puede consumir)

Puntos Clave para Evaluación

  • Generics: Plantillas de tipo para código reutilizable
  • Type Safety: Verificación de tipos en tiempo de compilación en lugar de errores en tiempo de ejecución
  • Type Parameters: Marcadores de posición para tipos concretos
  • Bounded Types: Restricciones en parámetros de tipo
  • Wildcards: Tipos desconocidos con restricciones
  • Type Erasure: Implementación de Java sin tipos en tiempo de ejecución
  • PECS Rule: Producer Extends, Consumer Super
  • Relevancia en exámenes: Importante para programación segura en cuanto a tipos

Componentes Principales

  1. Type Parameter: Marcador de posición <T> para tipos concretos
  2. Bounded Types: <T extends Number> con restricciones
  3. Wildcards: <?> para tipos desconocidos
  4. Generic Methods: <T> T max(T a, T b)
  5. Generic Classes: class Box<T>
  6. Type Safety: Verificación de tipos en tiempo de compilación
  7. Covarianza: Relación Producer extends
  8. Contravarianza: Relación Consumer super

Ejemplos Prácticos

1. Clase genérica simple en Java

// Clase Box genérica
public class Box<T> {
    private T inhalt;
    
    public Box() {
        this.inhalt = null;
    }
    
    public Box(T inhalt) {
        this.inhalt = inhalt;
    }
    
    public void setInhalt(T inhalt) {
        this.inhalt = inhalt;
    }
    
    public T getInhalt() {
        return inhalt;
    }
    
    public boolean istLeer() {
        return inhalt == null;
    }
    
    @Override
    public String toString() {
        return "Box[" + (inhalt != null ? inhalt.toString() : "leer") + "]";
    }
}

// Uso de la clase genérica
public class BoxDemo {
    public static void main(String[] args) {
        // Box para Strings
        Box<String> stringBox = new Box<>("Hallo Welt");
        System.out.println(stringBox); // Box[Hallo Welt]
        
        // Box para Integer
        Box<Integer> integerBox = new Box<>(42);
        System.out.println(integerBox); // Box[42]
        
        // Box para objetos propios
        class Person {
            String name;
            Person(String name) { this.name = name; }
            @Override public String toString() { return name; }
        }
        
        Box<Person> personBox = new Box<>(new Person("Max"));
        System.out.println(personBox); // Box[Max]
        
        // Seguridad de tipos en tiempo de compilación
        // stringBox.setInhalt(123); // Error: no se puede asignar Integer a String
    }
}

2. Métodos Genéricos

public class GenericMethods {
    
    // Método genérico para intercambiar elementos
    public static <T> void swap(T[] array, int i, int j) {
        T temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
    
    // Método genérico con Bounded Type Parameter
    public static <T extends Comparable<T>> T max(T a, T b) {
        return a.compareTo(b) > 0 ? a : b;
    }
    
    // Método genérico con Wildcard
    public static void printList(List<?> list) {
        for (Object element : list) {
            System.out.print(element + " ");
        }
        System.out.println();
    }
    
    // PECS: Producer Extends
    public static double sum(List<? extends Number> list) {
        double sum = 0.0;
        for (Number n : list) {
            sum += n.doubleValue();
        }
        return sum;
    }
    
    // PECS: Consumer Super
    public static void addNumbers(List<? super Integer> list) {
        list.add(1);
        list.add(2);
        list.add(3);
    }
    
    public static void main(String[] args) {
        // Método swap
        String[] names = {"Alice", "Bob", "Charlie"};
        swap(names, 0, 2);
        System.out.println(Arrays.toString(names)); // [Charlie, Bob, Alice]
        
        // Método max
        System.out.println(max(5, 10)); // 10
        System.out.println(max("Apple", "Banana")); // Banana
        
        // Wildcard
        List<String> strings = Arrays.asList("A", "B", "C");
        List<Integer> numbers = Arrays.asList(1, 2, 3);
        
        printList(strings); // A B C
        printList(numbers); // 1 2 3
        
        // Ejemplos PECS
        List<Integer> ints = Arrays.asList(1, 2, 3);
        System.out.println(sum(ints)); // 6.0
        
        List<Object> objects = new ArrayList<>();
        addNumbers(objects);
        System.out.println(objects); // [1, 2, 3]
    }
}

3. Clase genérica con Bounded Types

// Clase genérica con múltiples Bounded Type Parameters
public class Paar<T extends Comparable<T>, U> {
    private T erster;
    private U zweiter;
    
    public Paar(T erster, U zweiter) {
        this.erster = erster;
        this.zweiter = zweiter;
    }
    
    public T getErster() {
        return erster;
    }
    
    public U getZweiter() {
        return zweiter;
    }
    
    public void setErster(T erster) {
        this.erster = erster;
    }
    
    public void setZweiter(U zweiter) {
        this.zweiter = zweiter;
    }
    
    // Método que aprovecha el Bounded Type
    public int vergleicheErsten(T anderer) {
        return this.erster.compareTo(anderer);
    }
    
    @Override
    public String toString() {
        return "Paar[" + erster + ", " + zweiter + "]";
    }
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        
        Paar<T, U> paar = (Paar<T, U>) obj;
        return erster.equals(paar.erster) && zweiter.equals(paar.zweiter);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(erster, zweiter);
    }
}

// Uso
public class PaarDemo {
    public static void main(String[] args) {
        // Válido: String implementa Comparable
        Paar<String, Integer> nameAlter = new Paar<>("Alice", 25);
        System.out.println(nameAlter); // Paar[Alice, 25]
        
        // Error: StringBuilder no implementa Comparable
        // Paar<StringBuilder, Integer> invalid = new Paar<>(new StringBuilder(), 25);
        
        // Usar la comparación
        Paar<String, Integer> alice = new Paar<>("Alice", 25);
        Paar<String, Integer> bob = new Paar<>("Bob", 30);
        
        int vergleich = alice.vergleicheErsten(bob.getErster());
        System.out.println("Vergleich: " + vergleich); // negativo (Alice < Bob)
    }
}

4. Genéricos en C# con Constraints

using System;

// Clase genérica con constraints
public class Repository<T> where T : class, new()
{
    private List<T> items = new List<T>();
    
    public void Add(T item)
    {
        items.Add(item);
    }
    
    public T GetById(int id)
    {
        return items.FirstOrDefault(item => GetId(item) == id);
    }
    
    public IEnumerable<T> GetAll()
    {
        return items;
    }
    
    // Método que aprovecha el constraint new()
    public T CreateNew()
    {
        return new T(); // Solo posible gracias al constraint new()
    }
    
    private int GetId(T item)
    {
        // Suposición: T tiene una propiedad Id
        var property = typeof(T).GetProperty("Id");
        if (property != null && property.PropertyType == typeof(int))
        {
            return (int)property.GetValue(item);
        }
        return 0;
    }
}

// Clase de entidad
public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    
    public Person() { }
    
    public Person(int id, string name)
    {
        Id = id;
        Name = name;
    }
}

// Uso
public class RepositoryDemo
{
    public static void Main(string[] args)
    {
        var personRepo = new Repository<Person>();
        
        // Crear una nueva persona (usa el constraint new())
        var person1 = personRepo.CreateNew();
        person1.Id = 1;
        person1.Name = "Alice";
        
        var person2 = new Person(2, "Bob");
        
        personRepo.Add(person1);
        personRepo.Add(person2);
        
        // Mostrar todas las personas
        foreach (var person in personRepo.GetAll())
        {
            Console.WriteLine($"ID: {person.Id}, Name: {person.Name}");
        }
        
        // Buscar persona por ID
        var foundPerson = personRepo.GetById(1);
        Console.WriteLine($"Encontrado: {foundPerson?.Name}");
    }
}

5. Templates en C++

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>

// Clase genérica (Template)
template<typename T>
class Stack {
private:
    std::vector<T> elements;
    
public:
    void push(const T& element) {
        elements.push_back(element);
    }
    
    T pop() {
        if (elements.empty()) {
            throw std::runtime_error("Stack está vacío");
        }
        
        T element = elements.back();
        elements.pop_back();
        return element;
    }
    
    bool empty() const {
        return elements.empty();
    }
    
    size_t size() const {
        return elements.size();
    }
    
    // Método template dentro de la clase
    void print() const {
        std::cout << "Stack [";
        for (size_t i = 0; i < elements.size(); ++i) {
            std::cout << elements[i];
            if (i < elements.size() - 1) {
                std::cout << ", ";
            }
        }
        std::cout << "]" << std::endl;
    }
};

// Función template
template<typename T>
T max(T a, T b) {
    return (a > b) ? a : b;
}

// Template con parámetro de tipo acotado (Concept en C++20)
template<typename T>
requires std::is_arithmetic_v<T>
T calculateAverage(const std::vector<T>& numbers) {
    if (numbers.empty()) {
        return T{};
    }
    
    T sum = T{};
    for (const T& num : numbers) {
        sum += num;
    }
    
    return sum / static_cast<T>(numbers.size());
}

int main() {
    // Stack para diferentes tipos
    Stack<int> intStack;
    intStack.push(1);
    intStack.push(2);
    intStack.push(3);
    
    std::cout << "Int-Stack: ";
    intStack.print();
    
    Stack<std::string> stringStack;
    stringStack.push("Hola");
    stringStack.push("Mundo");
    
    std::cout << "String-Stack: ";
    stringStack.print();
    
    // Función template
    std::cout << "Max(5, 10): " << max(5, 10) << std::endl;
    std::cout << "Max(3.14, 2.71): " << max(3.14, 2.71) << std::endl;
    
    // Template acotado
    std::vector<int> numbers = {1, 2, 3, 4, 5};
    std::cout << "Promedio: " << calculateAverage(numbers) << std::endl;
    
    return 0;
}

Regla PECS: Producer Extends, Consumer Super

Producer (suministra datos)

// Producer: lee desde la lista, devuelve datos
public static void processList(List<? extends Number> list) {
    for (Number num : list) {
        System.out.println(num.doubleValue()); // Puede llamar métodos de Number
    }
    // list.add(42); // Error: No puede escribir en Producer
}

Consumer (consume datos)

// Consumer: escribe en la lista, consume datos
public static void fillList(List<? super Integer> list) {
    list.add(1);    // Integer puede agregarse
    list.add(2);    // Number puede agregarse
    list.add(3);    // Object puede agregarse
}

Type Erasure en Java

Qué ocurre en tiempo de compilación

// Código fuente
public class Box<T> {
    private T content;
    
    public void setContent(T content) {
        this.content = content;
    }
    
    public T getContent() {
        return content;
    }
}

// Después de Type Erasure (lo que ve la JVM)
public class Box {
    private Object content;
    
    public void setContent(Object content) {
        this.content = content;
    }
    
    public Object getContent() {
        return content;
    }
}

Casts de tipo en tiempo de ejecución

Box<String> stringBox = new Box<>();
stringBox.setContent("Hola");

// Cast implícito al devolver
String content = stringBox.getContent(); // Cast de Object a String

// Seguridad de tipos en tiempo de compilación
// stringBox.setContent(123); // Error en compilación

Ventajas y desventajas

Ventajas de los genéricos

  • Seguridad de tipos: Errores en compilación en lugar de en runtime
  • Reutilización: Un código para varios tipos
  • Legibilidad: Información de tipos explícita
  • Mantenibilidad: Menos duplicación de código
  • Rendimiento: Sin boxing/unboxing en tipos de valor (C#)

Desventajas

  • Complejidad: Curva de aprendizaje con wildcards y bounds
  • Type Erasure: Java pierde información de tipos en runtime
  • Overhead de compilación: Tiempos más largos (C++ Templates)
  • Compatibilidad binaria: Los cambios pueden romper código existente

Preguntas frecuentes de examen

  1. ¿Qué es Type Erasure en Java? La información de tipos se elimina en tiempo de compilación, en runtime solo existen Raw Types.

  2. ¡Explica la regla PECS! Producer Extends (solo puede leer), Consumer Super (solo puede escribir).

  3. ¿Cuál es la diferencia entre List<?> y List<Object>? List<?> es un tipo desconocido, List<Object> es específicamente para Object.

  4. ¿Por qué se necesitan Bounded Type Parameters? Para definir constraints y poder utilizar métodos del tipo.

Fuentes principales

  1. https://docs.oracle.com/javase/tutorial/java/generics/
  2. https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/
  3. https://en.cppreference.com/w/cpp/language/templates
Volver al blog
Share:

Nächster Artikel in Desarrollo de Software

Weiterlesen
Clases OOP: métodos Static vs Instance

Entradas relacionadas