Skip to content
IRC-CodingIRC-Coding
OOPrelaciones de clasesasociaciónagregacióncomposiciónherenciagenericsmiembros estáticosexamen IHK

OOP Klassenbeziehungen: Assoziation y Composición

Aprende las relaciones entre clases en OOP: asociación, agregación, composición y herencia. Guía para examen IHK.

S

schutzgeist

14 min read
OOP Klassenbeziehungen: Assoziation y Composición

Relaciones entre clases OOP: Asociación, Agregación, Composición y Herencia

Los objetos individuales son útiles, pero la verdadera potencia de la POO surge cuando los objetos se relacionan entre sí y colaboran para resolver tareas complejas. Este documento explica cómo están construidas las clases y los diferentes tipos de relaciones que pueden existir entre ellas, ideal para preparar exámenes de certificación técnica.

1. Componentes de una clase (Su estructura interna)

Una clase es como un molde. Está formada por varios componentes bien definidos:

1. Nombre de clase

El nombre único de la clase (por ejemplo, Cliente, CuentaBancaria). Por convención comienza con mayúscula.

2. Atributos (Campos de datos)

Son las variables dentro de la clase. Describen el estado que tendrá un futuro objeto.

  • Cada atributo tiene un nombre (por ejemplo, saldo) y un tipo de dato (por ejemplo, double, String)
  • Generalmente se declaran como private (consulta encapsulación)

3. Constructor

Un método especial que se invoca automáticamente cuando se crea una nueva instancia de la clase usando new.

Propósito: Inicializa los nuevos objetos, es decir, asigna valores iniciales a los atributos.

  • El constructor tiene el mismo nombre que la clase y no retorna nada (ni siquiera void)
  • Puede haber varios constructores (sobrecarga), por ejemplo uno sin parámetros y otro que inicializa todos los atributos
// Ejemplo de constructor
public class CuentaBancaria {
    private double saldo;
    private String titular;
    
    // Constructor
    public CuentaBancaria(String nombreTitular, double saldoInicial) {
        titular = nombreTitular;
        saldo = saldoInicial;
    }
    
    // Constructor sin parámetros
    public CuentaBancaria() {
        this("Desconocido", 0.0);
    }
    
    // Constructor sobrecargado
    public CuentaBancaria(String nombreTitular) {
        this(nombreTitular, 0.0);
    }
}

// Creación de objetos:
CuentaBancaria miCuenta = new CuentaBancaria("Juan Pérez", 1000.0);
CuentaBancaria cuentaVacia = new CuentaBancaria();

4. Métodos (Funciones miembro)

Definen el comportamiento de los objetos. Operan sobre los atributos.

  • Getters/Setters: Métodos públicos para leer y escribir atributos private de forma controlada
  • Métodos de negocio: Implementan la funcionalidad principal (por ejemplo, depositar(double monto), retirar(double monto))

5. Modificadores de visibilidad (Access Modifiers)

Determinan desde dónde se puede acceder a los componentes de la clase. Son esenciales para la encapsulación.

  • private: Solo visible desde la propia clase
  • protected: Visible desde la propia clase y sus subclases (herencia)
  • public: Visible desde cualquier parte
  • (sin modificador/default): Solo visible dentro del mismo paquete

2. Relaciones entre clases: Cómo se relacionan

Las clases raramente existen en aislamiento. Trabajan juntas. Estas relaciones se modelan en diagramas de clases UML.

a) Asociación

Descripción: La relación más general. Describe una conexión semántica entre dos clases independientes. Una clase “conoce” a otra.

Representación UML: Línea continua.

Ejemplo: Un Profesor imparte clases a un Estudiante. El profesor y el estudiante existen independientemente. La relación suele ser bidireccional (ambos se “conocen”).

Implementación: Generalmente mediante atributos que contienen referencias.

// Ejemplo de asociación
public class Profesor {
    private String nombre;
    private String departamento;
    private List<Estudiante> estudiantes; // Profesor SE ASOCIA CON Estudiante
    
    public Profesor(String nombre, String departamento) {
        this.nombre = nombre;
        this.departamento = departamento;
        this.estudiantes = new ArrayList<>();
    }
    
    public void agregarEstudiante(Estudiante estudiante) {
        estudiantes.add(estudiante);
        estudiante.agregarProfesor(this); // Relación bidireccional
    }
    
    public void imparte() {
        System.out.println("Profesor " + nombre + " imparte clases a " + estudiantes.size() + " estudiantes");
    }
    
    // Getter
    public String getNombre() { return nombre; }
    public List<Estudiante> getEstudiantes() { return new ArrayList<>(estudiantes); }
}

public class Estudiante {
    private String nombre;
    private int matricula;
    private List<Profesor> profesores; // Estudiante SE ASOCIA CON Profesor
    
    public Estudiante(String nombre, int matricula) {
        this.nombre = nombre;
        this.matricula = matricula;
        this.profesores = new ArrayList<>();
    }
    
    public void agregarProfesor(Profesor profesor) {
        profesores.add(profesor);
    }
    
    public void estudia() {
        System.out.println("Estudiante " + nombre + " estudia con " + profesores.size() + " profesores");
    }
    
    // Getter
    public String getNombre() { return nombre; }
    public List<Profesor> getProfesores() { return new ArrayList<>(profesores); }
}

b) Agregación

Descripción: Una forma más específica de asociación. Describe una relación “es-parte-de” o “tiene-un” en la que las partes pueden existir sin el todo. Es una relación débil.

Representación UML: Línea continua con un rombo sin relleno en el lado del todo.

Ejemplo: Un Departamento tiene Empleados. Si el departamento se disuelve, los empleados no desaparecen, sino que pueden asignarse a otro departamento. La parte (empleado) existe independientemente del todo (departamento).

Implementación: Similar a la asociación, pero los ciclos de vida de los objetos no están acoplados.

// Ejemplo de agregación
public class Departamento {
    private String nombre;
    private String ubicacion;
    private List<Empleado> empleados; // Departamento TIENE Empleados (Agregación)
    
    public Departamento(String nombre, String ubicacion) {
        this.nombre = nombre;
        this.ubicacion = ubicacion;
        this.empleados = new ArrayList<>();
    }
    
    public void agregarEmpleado(Empleado empleado) {
        this.empleados.add(empleado);
        empleado.setDepartamento(this);
    }
    
    public void removerEmpleado(Empleado empleado) {
        this.empleados.remove(empleado);
        empleado.setDepartamento(null); // El empleado sigue existiendo
    }
    
    public void disolver() {
        // Los empleados no se destruyen, solo se desvinculan del departamento
        for (Empleado e : empleados) {
            e.setDepartamento(null);
        }
        empleados.clear();
        System.out.println("Departamento " + nombre + " disuelto, empleados siguen existiendo");
    }
    
    // Getter
    public String getNombre() { return nombre; }
    public List<Empleado> getEmpleados() { return new ArrayList<>(empleados); }
}

public class Empleado {
    private String nombre;
    private String cargo;
    private Departamento departamento; // Empleado puede existir sin departamento
    
    public Empleado(String nombre, String cargo) {
        this.nombre = nombre;
        this.cargo = cargo;
        this.departamento = null; // Empleado puede crearse sin departamento
    }
    
    public void setDepartamento(Departamento departamento) {
        this.departamento = departamento;
    }
    
    public void trabaja() {
        String nombreDepartamento = departamento != null ? departamento.getNombre() : "sin departamento";
        System.out.println(nombre + " trabaja como " + cargo + " en " + nombreDepartamento);
    }
    
    // Getter
    public String getNombre() { return nombre; }
    public Departamento getDepartamento() { return departamento; }
}

c) Composición

Descripción: Una forma aún más fuerte de la relación “es parte de”. Las partes no pueden existir sin el todo. El todo es responsable de la duración de vida de las partes. Relaciones de propiedad estrictas.

Representación UML: Línea sólida con un rombo relleno en el lado del todo.

Ejemplo: Un Auto está compuesto por un Motor. El motor no tiene existencia independiente sin el auto. Cuando el auto se desguaza, el motor también se destruye. La duración de vida está fuertemente acoplada.

Implementación: El todo crea las partes en su propio constructor.

// Ejemplo de Composición
public class Auto {
    private String marke;
    private String modell;
    private Motor motor; // Auto ESTÁ COMPUESTO POR Motor
    private List<Rad> raeder; // Auto ESTÁ COMPUESTO POR Ruedas
    
    public Auto(String marke, String modell) {
        this.marke = marke;
        this.modell = modell;
        
        // Las partes se crean con el Auto (acoplamiento estricto de duración de vida)
        this.motor = new Motor(2.0, 150); // Motor se crea con Auto
        this.raeder = new ArrayList<>();
        
        // Se crean 4 ruedas
        for (int i = 0; i < 4; i++) {
            raeder.add(new Rad("225/45R17"));
        }
    }
    
    public void starten() {
        motor.starten();
        System.out.println(marke + " " + modell + " wird gestartet");
    }
    
    public void verschrotten() {
        // Todas las partes se destruyen cuando el Auto se desguaza
        motor.zerstoeren();
        for (Rad rad : raeder) {
            rad.zerstoeren();
        }
        raeder.clear();
        System.out.println("Auto und alle Teile wurden verschrottet");
    }
    
    // Getter
    public String getMarke() { return marke; }
    public Motor getMotor() { return motor; }
}

public class Motor {
    private double hubraum;
    private int leistungPS;
    private boolean laeuft;
    
    public Motor(double hubraum, int leistungPS) {
        this.hubraum = hubraum;
        this.leistungPS = leistungPS;
        this.laeuft = false;
    }
    
    public void starten() {
        this.laeuft = true;
        System.out.println("Motor (" + hubraum + "L, " + leistungPS + "PS) gestartet");
    }
    
    public void zerstoeren() {
        System.out.println("Motor zerstört");
    }
    
    // Getter
    public double getHubraum() { return hubraum; }
    public int getLeistungPS() { return leistungPS; }
}

public class Rad {
    private String groesse;
    private double druck;
    
    public Rad(String groesse) {
        this.groesse = groesse;
        this.druck = 2.5;
    }
    
    public void zerstoeren() {
        System.out.println("Rad (" + groesse + ") zerstört");
    }
    
    // Getter
    public String getGroesse() { return groesse; }
}

d) Generalización y Especialización (Herencia)

Descripción: Esta es la relación “es un” y se implementa mediante herencia.

Generalización: Extraer características comunes de varias clases en una clase base más general (por ejemplo, Perro, GatoAnimal).

Especialización: Derivar una clase más específica (subclase) de una clase más general (clase base). La subclase hereda todas las propiedades y las refina o amplía (por ejemplo, AnimalPerro; el perro añade el método ladrar()).

Representación UML: Línea sólida con una flecha hueca que va de la subclase a la clase base.

Ejemplo: Gerente es un Empleado. Hereda todos los atributos (nombre, salario) y quizá añade un atributo bonus.

// Ejemplo de Herencia
public class Empleado {
    protected String nombre;
    protected double salarioBase;
    protected int idEmpleado;
    private static int proximoId = 1;
    
    public Empleado(String nombre, double salarioBase) {
        this.nombre = nombre;
        this.salarioBase = salarioBase;
        this.idEmpleado = proximoId++;
    }
    
    public void trabajar() {
        System.out.println(nombre + " trabaja con salario base de " + salarioBase + "€");
    }
    
    public double calcularSalario() {
        return salarioBase;
    }
    
    // Getter
    public String getNombre() { return nombre; }
    public int getIdEmpleado() { return idEmpleado; }
}

public class Gerente extends Empleado {
    private double bono;
    private List<Empleado> equipo;
    
    public Gerente(String nombre, double salarioBase, double bono) {
        super(nombre, salarioBase); // Llamar al constructor de la clase base
        this.bono = bono;
        this.equipo = new ArrayList<>();
    }
    
    public void agregarMiembroEquipo(Empleado empleado) {
        equipo.add(empleado);
    }
    
    @Override
    public void trabajar() {
        System.out.println(nombre + " gestiona equipo de " + equipo.size() + " empleados");
    }
    
    @Override
    public double calcularSalario() {
        return salarioBase + bono; // Cálculo adicional
    }
    
    public void reunionEquipo() {
        System.out.println(nombre + " realiza reunión de equipo");
        for (Empleado e : equipo) {
            System.out.println("- " + e.getNombre());
        }
    }
    
    // Getter
    public double getBono() { return bono; }
    public List<Empleado> getEquipo() { return new ArrayList<>(equipo); }
}

public class Desarrollador extends Empleado {
    private List<String> lenguajes;
    
    public Desarrollador(String nombre, double salarioBase, List<String> lenguajes) {
        super(nombre, salarioBase);
        this.lenguajes = new ArrayList<>(lenguajes);
    }
    
    @Override
    public void trabajar() {
        System.out.println(nombre + " programa en " + lenguajes);
    }
    
    public void aprenderLenguaje(String lenguaje) {
        lenguajes.add(lenguaje);
        System.out.println(nombre + " aprende " + lenguaje);
    }
    
    // Getter
    public List<String> getLenguajes() { return new ArrayList<>(lenguajes); }
}

3. Métodos y Atributos Estáticos vs. No Estáticos

Esta diferencia es fundamental y se refiere a si una característica pertenece a la clase o al objeto.

CaracterísticaNo estático (miembro de instancia)Estático (miembro de clase)
PertenenciaPertenece al objeto individual (instancia)Pertenece a la clase misma
Número en memoriaUna copia por objeto. 100 objetos = 100 copias del atributoUna sola copia para la clase completa. Compartida por todos los objetos
LlamadaA través del objeto: nombreObjeto.nombreMetodo()A través del nombre de clase: NombreClase.nombreMetodo()
Acceso a…Puede acceder a miembros estáticos y no estáticosPuede acceder SOLO a miembros estáticos. No puede acceder a miembros no estáticos (porque ¿cuál objeto se refiere?)
Uso típicoAtributos que varían de objeto a objeto (por ejemplo, saldo, nombre)Métodos de utilidad (por ejemplo, Math.sqrt()), constantes (por ejemplo, Math.PI), variables de contador (por ejemplo, numeroObjetosCreados)
// Ejemplo de Estático vs. No Estático
public class Estudiante {
    // Atributo no estático (una copia por objeto)
    private String nombre;
    private int matricula;
    
    // Atributo estático (una sola copia para la clase)
    private static int cantidadEstudiantes = 0;
    private static final String UNIVERSIDAD = "Universidad Técnica";
    
    public Estudiante(String nombre, int matricula) {
        this.nombre = nombre;
        this.matricula = matricula;
        cantidadEstudiantes++; // Acceso a atributo estático
    }
    
    // Método no estático
    public void estudiar() {
        System.out.println(nombre + " estudia en " + UNIVERSIDAD);
        System.out.println("Cantidad actual de estudiantes: " + cantidadEstudiantes);
    }
    
    // Método estático
    public static int getCantidadEstudiantes() {
        return cantidadEstudiantes;
        // return nombre; // ERROR: nombre no es estático y no se puede acceder
    }
    
    // Método estático de utilidad
    public static boolean esMatriculaValida(int numero) {
        return numero >= 100000 && numero <= 999999;
    }
    
    // Getter y Setter
    public String getNombre() { return nombre; }
    public int getMatricula() { return matricula; }
    
    public static String getUniversidad() { return UNIVERSIDAD; }
}

// Uso
public class Main {
    public static void main(String[] args) {
        // Crear objetos
        Estudiante est1 = new Estudiante("Max Mustermann", 123456);
        Estudiante est2 = new Estudiante("Erika Mustermann", 234567);
        
        // Llamar métodos no estáticos a través de objetos
        est1.estudiar();
        est2.estudiar();
        
        // Llamar métodos estáticos a través de la clase
        System.out.println("Cantidad estudiantes: " + Estudiante.getCantidadEstudiantes());
        System.out.println("Universidad: " + Estudiante.getUniversidad());
        
        // Método estático de utilidad
        boolean valida = Estudiante.esMatriculaValida(123456);
        System.out.println("Matrícula válida: " + valida);
        
        // Acceso a atributos no estáticos
        System.out.println("Nombre estudiante 1: " + est1.getNombre());
        
        // ERROR: No se puede llamar método estático en objeto
        // Estudiante.getNombre(); // Error de compilación
    }
}

4. Clases genéricas (Generics) - por ejemplo List<T>

¿Cuál es el problema?

Antes de los Generics, las colecciones (como ArrayList) se definían para el tipo Object. Se podía insertar cualquier cosa (Strings, Integers, etc.). Al extraer datos, había que verificar el tipo manualmente mediante casting y reconvertir (String s = (String) myList.get(0);). Los errores en tiempo de ejecución eran frecuentes.

¿Cuál es la solución?

Las clases genéricas. Se trata de plantillas de clase que utilizan uno o varios marcadores de posición para tipos de datos (frecuentemente T como “Type”, E como “Element”).

Propósito: Seguridad de tipos en tiempo de compilación. El compilador verifica que solo se inserten objetos del tipo correcto. Se elimina el casting y se previenen los errores en tiempo de ejecución.

// Ejemplo de Generics

// Sin Generics (anticuado, propenso a errores)
List myOldList = new ArrayList();
myOldList.add("Hola");
myOldList.add(123); // El compilador no dice nada, pero...
String s = (String) myOldList.get(1); // Error en tiempo de ejecución: ClassCastException!

// Con Generics (typsicher)
List<String> myList = new ArrayList<>(); // T se convierte en String
myList.add("Hola");
// myList.add(123); // ERROR DE COMPILACIÓN: 123 no es un String!
String s = myList.get(0); // Sin casting necesario, seguro.

// Clase genérica personalizada
public class Box<T> {
    private T inhalt;
    
    public void setInhalt(T inhalt) {
        this.inhalt = inhalt;
    }
    
    public T getInhalt() {
        return inhalt;
    }
    
    public boolean istLeer() {
        return inhalt == null;
    }
}

// Uso de la clase genérica
public class GenericsBeispiel {
    public static void main(String[] args) {
        // Box para Strings
        Box<String> stringBox = new Box<>();
        stringBox.setInhalt("Hola Mundo");
        String inhalt = stringBox.getInhalt(); // Sin casting necesario
        
        // Box para Integer
        Box<Integer> integerBox = new Box<>();
        integerBox.setInhalt(42);
        Integer zahl = integerBox.getInhalt(); // Sin casting necesario
        
        // Box para objetos propios
        Box<Student> studentBox = new Box<>();
        studentBox.setInhalt(new Student("Max", 123456));
        Student student = studentBox.getInhalt();
        
        System.out.println("String Box: " + inhalt);
        System.out.println("Integer Box: " + zahl);
        System.out.println("Student Box: " + student.getName());
    }
}

// Métodos genéricos
public class Utility {
    // Método genérico para intercambiar
    public static <T> void tausche(T[] array, int i, int j) {
        T temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }
    
    // Método genérico para máximo
    public static <T extends Comparable<T>> T maximum(T x, T y, T z) {
        T max = x;
        if (y.compareTo(max) > 0) max = y;
        if (z.compareTo(max) > 0) max = z;
        return max;
    }
}

5. Ventajas de los contenedores genéricos (Templates en C++) frente a arrays

CaracterísticaArraysContenedores genéricos (p. ej. ArrayList<T>, List<T>)
TamañoEstático/Fijo. El tamaño se define al crear el array y no puede cambiar despuésDinámico/Creciente. El tamaño se adapta automáticamente según la cantidad de elementos
Seguridad de tiposOfrecen seguridad de tipos básica, pero solo pueden almacenar un tipo fijoOfrecen seguridad de tipos completa en tiempo de compilación mediante Generics
FuncionalidadMuy limitada. Operaciones básicas (lectura, escritura en índice)Ofrecen muchos métodos útiles: .add(), .remove(), .contains(), .size(), etc.
RendimientoMuy rápido para acceso directo por índiceAlgo más lento debido al overhead de la gestión dinámica, pero generalmente insignificante
FlexibilidadBajaMuy alta. Existen diferentes contenedores para diferentes propósitos (listas, conjuntos, mapas, colas)
// Comparación: Array vs. ArrayList
public class ArrayVsContainer {
    public static void main(String[] args) {
        // Array - tamaño fijo
        String[] namenArray = new String[3];
        namenArray[0] = "Alice";
        namenArray[1] = "Bob";
        namenArray[2] = "Charlie";
        // namenArray[3] = "David"; // ERROR: ArrayIndexOutOfBoundsException!
        
        // ArrayList - tamaño dinámico
        ArrayList<String> namenList = new ArrayList<>();
        namenList.add("Alice");
        namenList.add("Bob");
        namenList.add("Charlie");
        namenList.add("David"); // Sin problema!
        namenList.add("Eve");   // Añade tantos elementos como sea necesario!
        
        // Comparación de funcionalidad
        System.out.println("Longitud del Array: " + namenArray.length);
        System.out.println("Tamaño del ArrayList: " + namenList.size());
        
        // ArrayList tiene más métodos
        namenList.remove("Bob"); // Eliminar elemento
        boolean enthaeltAlice = namenList.contains("Alice"); // Verificar
        Collections.sort(namenList); // Ordenar
        
        System.out.println("ArrayList después de eliminar y ordenar: " + namenList);
    }
}

Ventaja de síntesis: Los contenedores genéricos combinan la seguridad de tipos con la flexibilidad de una estructura de datos dinámica, lo que los hace superiores a los arrays en prácticamente cualquier caso de uso.

Resumen para la prueba IHK

  • Componentes de una clase: Nombre, atributos, constructor, métodos
  • Relaciones:
    • Asociación: Relación “conoce”
    • Agregación: “tiene-un” (suelta, la parte sigue existiendo)
    • Composición: “consta-de” (fuerte, la parte se destruye)
    • Herencia: “es-un” (Generalización/Especialización)
  • Estático vs. No-Estático: Clase vs. Objeto
  • Generics: Hacen las clases type-safe al utilizar marcadores de posición para tipos de datos
  • Contenedores vs. Arrays: Los contenedores son dinámicos, type-safe y más funcionales

Conceptos relevantes para la prueba

Distinciones importantes

ConceptoDescripciónSímbolo UMLCiclo de vida
AsociaciónRelación “conoce” entre clases independientesLíneaIndependiente
AgregaciónRelación “tiene-un”, acoplamiento débilLínea con rombo vacíoIndependiente
ComposiciónRelación “consta-de”, acoplamiento fuerteLínea con rombo llenoDependiente
HerenciaRelación “es-un”, reutilización de códigoLínea con flecha huecaHeredado

Tareas típicas de prueba

  1. Dibuja diagramas de clases UML con diferentes relaciones
  2. Implementa asociación, agregación y composición
  3. Explica la diferencia entre miembros estáticos y no-estáticos
  4. Utiliza Generics para contenedores type-safe
  5. Compara arrays con contenedores genéricos

Estos conceptos son fundamentales para comprender la arquitectura de software orientada a objetos y forman la base para diseños de sistemas complejos.

Continúa en la ruta de aprendizaje OOP

Todos los artículos OOP están completos. Regresa al primer artículo: Objektorientierte Programmierung OOP Grundlagen.

Volver al blog
Share:

Nächster Artikel in Programación Orientada a Objetos

Weiterlesen
Encapsulation OOP: Information Hiding y Visibilidad

Entradas relacionadas