Skip to content

8.2 Composición

1. Sintaxis de la composición

Para indicar que una clase contiene objetos de otra clase no es necesaria ninguna sintaxis especial. Cada uno de esos objetos no es más que un atributo y, por tanto, debe ser declarado como tal:

1
2
3
4
5
6
class <nombreClase> {
  [modificadores] <NombreClase1> nombreAtributo1;
  [modificadores] <NombreClase2> nombreAtributo2;
  <NombreClase3>[] listado;
  ...    
}

En unidades anteriores has trabajado con la clase Punto, que definía las coordenadas de un punto en el plano, y con la clase Rectangulo, que definía una figura de tipo rectángulo también en el plano a partir de dos de sus vértices (inferior izquierdo y superior derecho). Tal y como hemos formalizado ahora los tipos de relaciones entre clases, parece bastante claro que aquí tendrías un caso de composición: "un rectángulo contiene puntos". Por tanto, podrías ahora redefinir los atributos de la clase Rectangulo (cuatro números reales) como dos objetos de tipo Punto:

1
2
3
4
5
class Rectangulo {
  private Punto vertice1;
  private Punto vertice2;
  ...
}

Ahora los métodos de esta clase deberán tener en cuenta que ya no hay cuatro atributos de tipo double, sino dos atributos de tipo Punto (cada uno de los cuales contendrá en su interior dos atributos de tipo double).

Ejemplo 2.01: Revisa con cuidado el siguiente ejemplo

Intenta reescribir los siguientes los métodos de la clase Rectangulo teniendo en cuenta ahora su nueva estructura de atributos (dos objetos de la clase Punto, en lugar de cuatro elementos de tipo double):

  • Método calcularSuperfice, que calcula y devuelve el área de la superficie encerrada por la figura.

  • Método calcularPerimetro, que calcula y devuelve la longitud del perímetro de la figura.

Solución
En ambos casos la interfaz no se ve modificada en absoluto (desde fuera su funcionamiento es el mismo), pero internamente deberás tener en cuenta que ya no existen los atributos x1, y1, x2, y2, de tipo double, sino los atributos vertice1 y vertice2 de tipo Punto.

public class Punto {
    private double x;
    private double y;

    public Punto(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public double getX() {
        return x;
    }

    public void setX(double x) {
        this.x = x;
    }

    public double getY() {
        return y;
    }

    public void setY(double y) {
        this.y = y;
    }
}

En la siguiente presentación puedes observar detalladamente el proceso completo de elaboración de la clase Rectangulo haciendo uso de la clase Punto:

1) Objetos de tipo Rectangulo compuesto por objetos de tipo Punto:

ut08_006
2) Clase Rectangulo y su método calcularSuperficie:
ut08_008

public class Rectangulo {
    // Atributos de objeto
    private Punto vertice1;  // Vértice inferior izquierdo
    private Punto vertice2;  // Vértice superior derecho

    public double calcularSuperficie (){
        // cálculo de la base
        double base = vertice2.obtenerX() - vertice1.obtenerX();
        // cálculo de la altura
        double altura = vertice2.obtenerY() - vertice1.obtenerY();
        // cálculo del área
        double area = base * altura;
        return area;   // valor de retorno
    }
}

2. Uso de la composición

2.1. Preservación de la ocultación

Como ya has observado, la relación de composición no tiene más misterio a la hora de implementarse que simplemente declarar atributos de las clases que necesites dentro de la clase que estés diseñando.

Ahora bien, cuando escribas clases que contienen objetos de otras clases (lo cual será lo más habitual) deberás tener un poco de precaución con aquellos métodos que devuelvan información acerca de los atributos de la clase (métodos consultores o de tipo get).

Como ya viste en la unidad dedicada a la creación de clases, lo normal suele ser declarar los atributos como privados (o protegidos, como veremos un poco más adelante) para ocultarlos a los posibles clientes de la clase (otros objetos que en el futuro harán uso de la clase). Para que otros objetos puedan acceder a la información contenida en los atributos, o al menos a una parte de ella, deberán hacerlo a través de métodos que sirvan de interfaz, de manera que sólo se podrá tener acceso a aquella información que el creador de la clase haya considerado oportuna. Del mismo modo, los atributos solamente serán modificados desde los métodos de la clase, que decidirán cómo y bajo qué circunstancias deben realizarse esas modificaciones. Con esa metodología de acceso se tenía perfectamente separada la parte de manipulación interna de los atributos de la interfaz con el exterior.

Hasta ahora los métodos de tipo get devolvían tipos primitivos, es decir, copias del contenido (a veces con algún tipo de modificación o de formato) que había almacenado en los atributos, pero los atributos seguían "a salvo" como elementos privados de la clase. Pero, a partir de este momento, al tener objetos dentro de las clases y no sólo tipos primitivos, es posible que en un determinado momento interese devolver un objeto completo.

Cuidado al devolver atributos de tipo objeto

Cuando vayas a devolver un objeto habrás de obrar con mucha precaución. Si en un método de la clase devuelve directamente un objeto que es un atributo, estarás ofreciendo directamente una referencia a un objeto atributo que probablemente has definido como privado.

¡De esta forma estás volviendo a hacer público un atributo que inicialmente era privado!

Para evitar ese tipo de situaciones (ofrecer al exterior referencias a objetos privados) puedes optar por diversas alternativas, procurando siempre evitar la devolución directa de un atributo que sea un objeto:

opción 1) Devolver siempre tipos primitivos.

opción 2) Dado que siempre no va a ser posible devolver tipos primitivos, otra posibilidad es crear un nuevo objeto que sea una copia del atributo que quieres devolver y utilizar ese objeto como valor de retorno. Es decir, crear una copia del objeto especialmente para devolverlo. De esta manera, el código cliente de ese método podrá manipular a su antojo ese nuevo objeto, pues no será una referencia al atributo original, sino un nuevo objeto con el mismo contenido.

opción 3) Debes tener en cuenta que es posible que en algunos casos sí se necesite realmente la referencia al atributo original (algo muy habitual en el caso de atributos estáticos). En tales casos, no habrá problema en devolver directamente el atributo para que el código llamante (cliente) haga el uso que estime oportuno de él.

Resumiendo: Debes evitar por todos los medios la devolución de un atributo que sea un objeto, pues estarías dando directamente una referencia al atributo, visible y manipulable desde fuera; salvo que se trate de un caso en el que deba ser así.

Para entender estas situaciones un poco mejor, podemos volver a la clase Rectangulo y observar sus nuevos métodos de tipo get.

Ejemplo 2.02: Revisa con cuidado el siguiente ejemplo

Dada la clase Rectangulo, escribe sus nuevos métodos getVertice1 y getVertice2 para que devuelvan los vértices inferior izquierdo y superior derecho del rectángulo (objetos de tipo Punto), teniendo en cuenta su nueva estructura de atributos (dos objetos de la clase Punto, en lugar de cuatro elementos de tipo double):

Solución incorrecta
Los métodos de obtención de vértices devolverán objetos de la clase Punto:

1
2
3
4
5
6
7
public Punto getVertice1 (){
    return vertice1;
}

public Punto getVertice2 (){
    return vertice2;
}

Esto funcionaría perfectamente, pero deberías tener cuidado con este tipo de métodos que devuelven directamente una referencia a un objeto atributo que probablemente has definido como privado.

Cuidado!! estás de alguna manera haciendo público un atributo que fue declarado como privado**.

Solución correcta
Para evitar que esto suceda bastaría con crear un nuevo objeto que fuera una copia del atributo que se desea devolver (en este caso un objeto de la clase Punto).

Aquí tienes la solución para la nueva clase Rectangulo:

class Rectangulo {
    private Punto vertice1;
    private Punto vertice2;

    public double calcularSuperficie() {
        double area, base, altura; // Variables locales
        base = vertice2.getX() - vertice1.getX(); // Antes era x2 - x1
        altura = vertice2.getY() - vertice1.getY(); // Antes era y2 - y1
        area = base * altura;
        return area;
    }

    public double calcularPerimetro() {
        double perimetro, base, altura; // Variables locales
        base = vertice2.getX() - vertice1.getX(); // Antes era x2 - x1
        altura = vertice2.getY() - vertice1.getY(); // Antes era y2 - y1
        perimetro = 2 * base + 2 * altura;
        return perimetro;
    }

    /*
    * ASÍ NO!!
    *
    *public Punto getVertice1mal() {
    *    return vertice1;
    *}
    *
    *public Punto getVertice2mal() {
    *    return vertice2;
    *}  
    */

    // Método 1:
    public Punto getVertice1() {
        // Creación de un nuevo punto extrayendo sus atributos:
        double x, y;
        Punto p;
        x = this.vertice1.getX();
        y = this.vertice1.getY();
        p = new Punto(x, y);
        return p;
    }

    // Método 2 (mucho mejor este método):
    public Punto getVertice2() {
        // Utilizando el constructor copia de Punto (si es que está definido)

        // Punto p;
        // p = new Punto(this.vertice2);
        // return p;

        // o más corto:
        // Uso del constructor copia
        return new Punto(this.vertice2);
    }

    public Rectangulo(Punto vertice1, Punto vertice2) {
        // para evitar aliasing (compartir referencia)
        this.vertice1 = new Punto(vertice1); // copia del punto
        this.vertice2 = new Punto(vertice2); // copia del punto
    }

    public static void main(String[] args) {
        Punto puntoA = new Punto(0, 0);
        Punto puntoB = new Punto(5, 5);

        Rectangulo rectA = new Rectangulo(puntoA, puntoB);
        System.out.println("Perímetro del rectanculo A: " + rectA.calcularPerimetro());  //20

        puntoA.setX(4);
        puntoA.setY(4);

        Rectangulo rectB = new Rectangulo(puntoA, puntoB);
        System.out.println("Creo un nuevo rectangulo, pero cambia el Perímetro del anterior");
        System.out.println("Perímetro del rectanculo A: " + rectA.calcularPerimetro());  //20
        System.out.println("Perímetro del rectanculo B: " + rectB.calcularPerimetro());  //4
    }
}

De esta manera, se devuelve un punto totalmente nuevo que podrá ser manipulado sin ningún temor por parte del código cliente de la clase pues es una copia para él.

2.2. Llamadas a constructores

Otro factor que debes considerar, a la hora de escribir clases que contengan como atributos objetos de otras clases, es su comportamiento a la hora de instanciarse. Durante el proceso de creación de un objeto (constructor) de la clase contenedora habrá que tener en cuenta también la creación (llamadas a constructores) de aquellos objetos que son contenidos.

A tener en cuenta

El constructor de la clase contenedora debe invocar a los constructores de las clases de los objetos contenidos.

En este caso hay que tener cuidado con las referencias a objetos que se pasan como parámetros para rellenar el contenido de los atributos. Es conveniente hacer una copia de esos objetos y utilizar esas copias para los atributos pues si se utiliza la referencia que se ha pasado como parámetro, el código cliente de la clase podría tener acceso a ella sin necesidad de pasar por la interfaz de la clase (volveríamos a dejar abierta una puerta pública a algo que quizá sea privado).

Además, si el objeto parámetro que se pasó al constructor formaba parte de otro objeto, esto podría ocasionar un desagradable efecto colateral si esos objetos son modificados en el futuro desde el código cliente de la clase, ya que no sabes de dónde provienen esos objetos, si fueron creados especialmente para ser usados por el nuevo objeto creado o si pertenecen a otro objeto que podría modificarlos más tarde. Es decir, correrías el riesgo de estar "compartiendo" esos objetos con otras partes del código, sin ningún tipo de control de acceso y con las nefastas consecuencias que eso podría tener: cualquier cambio de ese objeto afectaría a partes del programa supuestamente independientes, que entienden ese objeto como suyo.

A tener en cuenta

En el fondo, los objetos no son más que variables de tipo referencia a la zona de memoria en la que se encuentra toda la información del objeto en sí mismo. Esto es, puedes tener un único objeto y múltiples referencias a él. Pero sólo se trata de un objeto, y cualquier modificación desde una de sus referencias afectaría a todas las demás, pues estamos hablando del mismo objeto.

Recuerda también que sólo se crean objetos cuando se llama a un constructor (uso de new). Si realizas asignaciones o pasos de parámetros, no se están copiando o pasando copias de los objetos, sino simplemente de las referencias, y por tanto se tratará siempre del mismo objeto.

Se trata de un efecto similar al que sucedía en los métodos de tipo get, pero en este caso en sentido contrario (en lugar de que nuestra clase "regale" al exterior uno de sus atributos objeto mediante una referencia, en esta ocasión se "adueña" de un parámetro objeto que probablemente pertenezca a otro objeto y que es posible que en el futuro haga uso de él).

Para entender mejor estos posibles efectos podemos continuar con el ejemplo de la clase Rectangulo que contiene en su interior dos objetos de la clase Punto. En los constructores del rectángulo habrá que incluir todo lo necesario para crear dos instancias de la clase Punto evitando las referencias a parámetros (haciendo copias).

Ejemplo 2.03: Revisa con cuidado el siguiente ejemplo

Intenta reescribir los constructores de la clase Rectangulo teniendo en cuenta ahora su nueva estructura de atributos (dos objetos de la clase Punto, en lugar de cuatro elementos de tipo double):

) Un constructor sin parámetros (para sustituir al constructor por defecto) que haga que los valores iniciales de las esquinas del rectángulo sean (0,0) y (1,1).

) Un constructor con cuatro parámetros, x1, y1, x2, y2, que cree un rectángulo con los vértices (x1, y1) y (x2, y2).

) Un constructor con dos parámetros, punto1, punto2, que rellene los valores iniciales de los atributos del rectángulo con los valores proporcionados a través de los parámetros.

) Un constructor con dos parámetros, base y altura, que cree un rectángulo donde el vértice inferior derecho esté ubicado en la posición (0,0) y que tenga una base y una altura tal y como indican los dos parámetros proporcionados.

) Un constructor copia.


Solución
Durante el proceso de creación de un objeto (constructor) de la clase contenedora (en este caso Rectangulo) hay que tener en cuenta también la creación (llamada a constructores) de aquellos objetos que son contenidos (en este caso objetos de la clase Punto).

) En el caso del primer constructor, habrá que crear dos puntos con las coordenadas (0,0) y (1,1) y asignarlos a los atributos correspondientes (vertice1 y vertice2):

1
2
3
4
public Rectangulo (){
    this.vertice1 = new Punto (0,0);
    this.vertice2 = new Punto (1,1);
}

) Para el segundo constructor habrá que crear dos puntos con las coordenadas x1, y1, x2, y2 que han sido pasadas como parámetros:

1
2
3
4
public Rectangulo (double x1, double y1, double x2, double y2){
    this.vertice1 = new Punto (x1, y1);
    this.vertice2 = new Punto (x2, y2);
}

) En el caso del tercer constructor puedes utilizar directamente los dos puntos que se pasan como parámetros para construir los vértices del rectángulo.

1
2
3
4
public Rectangulo (Punto vertice1, Punto vertice2) {
    this.vertice1 = vertice1;
    this.vertice2 = vertice2;
}

Ahora bien, esto podría ocasionar un efecto colateral no deseado si esos objetos de tipo Punto son modificados en el futuro desde el código cliente del constructor (no sabes si esos puntos fueron creados especialmente para ser usados por el rectángulo o si pertenecen a otro objeto que podría modificarlos más tarde).

Por tanto, para este caso quizá fuera recomendable crear dos nuevos puntos a imagen y semejanza de los puntos que se han pasado como parámetros. Para ello tendrías dos opciones:

a) Llamar al constructor de la clase Punto con los valores de los atributos (x, y).

1
2
3
4
public Rectangulo(Punto vertice1, Punto vertice2) {
    this.vertice1 = new Punto(vertice1.getX(), vertice1.getY());
    this.vertice2 = new Punto(vertice2.getX(), vertice2.getY());
}

b) Llamar al constructor copia de la clase Punto, si es que se dispone de él.

1
2
3
4
public Rectangulo (Punto vertice1, Punto vertice2) {
    this.vertice1 = new Punto (vertice1);
    this.vertice2 = new Punto (vertice2);
}

) Para el cuarto caso, el caso del constructor que recibe como parámetros la base y la altura, habrá que crear sendos vértices con valores (0,0) y (0 + base, 0 + altura), o lo que es lo mismo: (0,0) y (base, altura).

1
2
3
4
public Rectangulo(double base, double altura) {
    this.vertice1 = new Punto(0,0);
    this.vertice2 = new Punto(base, altura);
}

) Quedaría finalmente por implementar el constructor copia, quinto caso:

1
2
3
4
public Rectangulo (Rectangulo r) {
    this.vertice1 = new Punto (r.getVertice1());
    this.vertice2 = new Punto (r.getVertice2());
}

En este caso nuevamente volvemos a clonar los atributos vertice1 y vertice2 del objeto r que se ha pasado como parámetro para evitar tener que compartir esos atributos en los dos rectángulos.

Así ahora el método main que comprueba la clase Rectangulo funciona correctamente:

public static void main(String[] args) {
    Punto puntoA = new Punto(0, 0);
    Punto puntoB = new Punto(5, 5);

    Rectangulo rectA = new Rectangulo(puntoA, puntoB);
    System.out.println("Perímetro del rectanculo A: " + rectA.calcularPerimetro());//20

    puntoA.setX(4);
    puntoA.setY(4);

    Rectangulo rectB = new Rectangulo(puntoA, puntoB);
    System.out.println("Creo un nuevo rectangulo, pero cambia el Perímetro del anterior");
    System.out.println("Perímetro del rectanculo A: " + rectA.calcularPerimetro());//20
    System.out.println("Perímetro del rectanculo B: " + rectB.calcularPerimetro());//4
}

3. Clases anidadas o internas

En algunos lenguajes, es posible definir una clase dentro de otra clase (clases internas):

1
2
3
4
5
6
7
8
class ClaseContenedora {
  // Cuerpo de la clase
  ...
  class ClaseInterna {
    // Cuerpo de la clase interna
    ...
  }
}

Se pueden distinguir varios tipos de clases internas:

  • Clases internas estáticas (o clases anidadas), declaradas con el modificador static.

    Estas clases anidadas, como miembros de una clase que son (miembros de ClaseContenedora), pueden ser declaradas con los modificadores public, protected, private o de paquete, como el resto de miembros.

  • Clases internas miembro, conocidas habitualmente como clases internas. Declaradas al máximo nivel de la clase contenedora y no estáticas.

    Estas clases internas (no estáticas) tienen acceso a otros miembros de la clase dentro de la que está definida aunque sean privados (se trata en cierto modo de un miembro más de la clase), mientras que las anidadas (estáticas) no.

    Las clases internas se utilizan en algunos casos para:
    - Agrupar clases que sólo tiene sentido que existan en el entorno de la clase en la que han sido definidas, de manera que se oculta su existencia al resto del código.
    - Incrementar el nivel de encapsulación y ocultamiento.
    - Proporcionar un código fuente más legible y fácil de mantener (el código de las clases internas y anidadas está más cerca de donde es usado).

  • Clases internas locales, que se declaran en el interior de un bloque de código (normalmente dentro de un método).

  • Clases anónimas, similares a las internas locales, pero sin nombre (sólo existirá un objeto de ellas y, al no tener nombre, no tendrán constructores). Se suelen usar en la gestión de eventos en los interfaces gráficos.

1
2
3
4
5
6
7
8
9
class ClaseContenedora {
  ...
  static class ClaseAnidadaEstatica {
     ...
  }
  class ClaseInterna {
    ...
  }
}

Nota

En Java es posible definir clases internas y anidadas, permitiendo todas esas posibilidades. Aunque para los ejemplos con los que vas a trabajar no las vas a necesitar por ahora.

Ejemplo 2.04: ClaseContenedora y ClaseInterna

public class ClaseContenedora {
    private int valorExterno;

    // Constructor de la clase contenedora
    public ClaseContenedora(int valorExterno) {
        this.valorExterno = valorExterno;
    }

    // Método de la clase contenedora para imprimir el valor externo
    public void imprimirValorExterno() {
        System.out.println("Valor externo: " + valorExterno);
    }

    // Clase interna dentro de la clase contenedora
    public class ClaseInterna {
        private int valorInterno;

        // Constructor de la clase interna
        public ClaseInterna(int valorInterno) {
            this.valorInterno = valorInterno;
        }

        // Método de la clase interna para imprimir el valor interno y externo
        public void imprimirValores() {
            System.out.println("Valor externo: " + valorExterno);
            System.out.println("Valor interno: " + valorInterno);
        }
    }
}
public class Ejemplo {
    public static void main(String[] args) {
        // Crear una instancia de la clase contenedora
        ClaseContenedora contenedor = new ClaseContenedora(5);

        // Crear una instancia de la clase interna utilizando la instancia de la clase contenedora
        ClaseContenedora.ClaseInterna interna = contenedor.new ClaseInterna(10);

        // Llamar al método para imprimir el valor interno y externo desde la clase interna
        interna.imprimirValores();
    }
}