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 unList<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
- Type Parameter: Marcador de posición
<T>para tipos concretos - Bounded Types:
<T extends Number>con restricciones - Wildcards:
<?>para tipos desconocidos - Generic Methods:
<T> T max(T a, T b) - Generic Classes:
class Box<T> - Type Safety: Verificación de tipos en tiempo de compilación
- Covarianza: Relación
Producer extends - 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
-
¿Qué es Type Erasure en Java? La información de tipos se elimina en tiempo de compilación, en runtime solo existen Raw Types.
-
¡Explica la regla PECS! Producer Extends (solo puede leer), Consumer Super (solo puede escribir).
-
¿Cuál es la diferencia entre
List<?>yList<Object>?List<?>es un tipo desconocido,List<Object>es específicamente para Object. -
¿Por qué se necesitan Bounded Type Parameters? Para definir constraints y poder utilizar métodos del tipo.
Fuentes principales
- https://docs.oracle.com/javase/tutorial/java/generics/
- https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/
- https://en.cppreference.com/w/cpp/language/templates



