8. Reutilizar (2)

Resumen del capítulo

  • Combinando composición y herencia
    • Garantizar una limpieza adecuada
    • nombre oculto
  • Elecciones entre composición y herencia
  • protegido
  • Transformación ascendente
    • Otra discusión sobre composición y herencia.

Combinando composición y herencia

A menudo utilizarás composición y herencia juntas. El siguiente ejemplo muestra la creación de clases usando herencia y composición, junto con la inicialización del constructor necesaria:

class Plate {
    
    
    Plate(int i) {
    
    
        System.out.println("Plate constructor");
    }
}

class DinnerPlate extends Plate {
    
    
    DinnerPlate(int i) {
    
    
        super(i);
        System.out.println("DinnerPlate constructor");
    }
}

class Utensil {
    
    
    Utensil(int i) {
    
    
        System.out.println("Utensil constructor");
    }
}

class Spoon extends Utensil {
    
    
    Spoon(int i) {
    
    
        super(i);
        System.out.println("Spoon constructor");
    }
}

class Fork extends Utensil {
    
    
    Fork(int i) {
    
    
        super(i);
        System.out.println("Fork constructor");
    }
}

class Knife extends Utensil {
    
    
    Knife(int i) {
    
    
        super(i);
        System.out.println("Knife constructor");
    }
}

// A cultural way of doing something:
class Custom {
    
    
    Custom(int i) {
    
    
        System.out.println("Custom constructor");
    }
}

public class PlaceSetting extends Custom {
    
    
    private Spoon sp;
    private Fork frk;
    private Knife kn;
    private DinnerPlate pl;

    public PlaceSetting(int i) {
    
    
        super(i + 1);
        sp = new Spoon(i + 2);
        frk = new Fork(i + 3);
        kn = new Knife(i + 4);
        pl = new DinnerPlate(i + 5);
        System.out.println("PlaceSetting constructor");
    }

    public static void main(String[] args) {
    
    
        PlaceSetting x = new PlaceSetting(9);
    }
}

Insertar descripción de la imagen aquí

Aunque el compilador lo obliga a inicializar la clase base y requiere que la inicialice al comienzo del constructor, no lo monitorea para asegurarse de que inicialice los objetos miembro. Observe cómo las clases están claramente separadas. Ni siquiera necesitas el código fuente del método para reutilizar el código. Sólo puedes importar como máximo un paquete. (Esto es válido tanto para la herencia como para la composición).

Garantizar una limpieza adecuada

Java no tiene el concepto de destructor en C++, que es un método que se llama automáticamente cuando se destruye un objeto. La razón puede ser que en Java, los objetos generalmente se olvidan en lugar de destruirse, lo que permite al recolector de basura recuperar memoria según sea necesario. Por lo general, esto está bien, pero a veces su clase puede realizar algunas actividades durante su ciclo de vida que requieren limpieza. Como se mencionó en el capítulo Inicialización y limpieza, no hay forma de saber cuándo se llamará al recolector de basura, ni siquiera si se llamará. Por lo tanto, si desea limpiar algo para una clase, debe escribir explícitamente un método especial para hacerlo y asegurarse de que el programador cliente sepa que tiene que llamar a este método. Lo más importante, como se describe en el capítulo "Excepciones", es que debe evitar excepciones colocando dicha limpieza en la cláusula final.

Considere el ejemplo de un sistema de diseño asistido por computadora que dibuja imágenes en una pantalla:

class Shape {
    
    
    Shape(int i) {
    
    
        System.out.println("Shape constructor");
    }

    void dispose() {
    
    
        System.out.println("Shape dispose");
    }
}

class Circle extends Shape {
    
    
    Circle(int i) {
    
    
        super(i);
        System.out.println("Drawing Circle");
    }

    @Override
    void dispose() {
    
    
        System.out.println("Erasing Circle");
        super.dispose();
    }
}

class Triangle extends Shape {
    
    
    Triangle(int i) {
    
    
        super(i);
        System.out.println("Drawing Triangle");
    }

    @Override
    void dispose() {
    
    
        System.out.println("Erasing Triangle");
        super.dispose();
    }
}

class Line extends Shape {
    
    
    private int start, end;

    Line(int start, int end) {
    
    
        super(start);
        this.start = start;
        this.end = end;
        System.out.println(
                "Drawing Line: " + start + ", " + end);
    }

    @Override
    void dispose() {
    
    
        System.out.println(
                "Erasing Line: " + start + ", " + end);
        super.dispose();
    }
}

public class CADSystem extends Shape {
    
    
    private Circle c;
    private Triangle t;
    private Line[] lines = new Line[3];

    public CADSystem(int i) {
    
    
        super(i + 1);
        for (int j = 0; j < lines.length; j++)
            lines[j] = new Line(j, j * j);
        c = new Circle(1);
        t = new Triangle(1);
        System.out.println("Combined constructor");
    }

    @Override
    public void dispose() {
    
    
        System.out.println("CADSystem.dispose()");
        // The order of cleanup is the reverse
        // of the order of initialization:
        t.dispose();
        c.dispose();
        for (int i = lines.length - 1; i >= 0; i--) {
    
    
            lines[i].dispose();
        }
        super.dispose();
    }

    public static void main(String[] args) {
    
    
        CADSystem x = new CADSystem(47);
        try {
    
    
            // Code and exception handling...
        } finally {
    
    
            x.dispose();
        }
    }
}

Insertar descripción de la imagen aquí

Todo en este sistema es una especie de Forma (que a su vez es una especie de Objeto porque se hereda implícitamente de la clase raíz). Además de usar super para llamar a la versión de clase base del método, cada clase también anula dispose()el método. Las clases de Forma específicas ( Círculo , Triángulo y Línea ) tienen constructores de "dibujo", aunque cualquier método llamado durante la vida útil del objeto puede ser responsable de hacer lo que sea necesario limpiar. Cada clase tiene su propio dispose()método para restaurar el contenido que no es memoria al estado anterior a la existencia del objeto.

En main(), hay dos palabras clave que no ha visto antes y que no se explicarán en detalle hasta el capítulo "Excepciones": intentar y finalmente . La palabra clave try indica que el siguiente bloque (delimitado por llaves) es una región protegida, lo que significa que recibe un tratamiento especial. Uno de los tratamientos especiales es que no importa cómo salga el bloque try , siempre se ejecuta el código en la cláusula final después de esta área de protección . (Con el manejo de excepciones, los bloques try se pueden omitir de muchas maneras inusuales ). Aquí, la cláusula finalmente significa: "Pase lo que pase, llame siempre x.dispose()".

En el método de limpieza (en este caso dispose()), también debe prestar atención al orden en que se llaman los métodos de limpieza de la clase base y los objetos miembro, en caso de que un subobjeto dependa de otro subobjeto. Primero, realice toda la limpieza específica de la clase en orden inverso al de creación. (Generalmente, esto requiere que el elemento de la clase base aún sea accesible). Luego se llama al método de limpieza de la clase base, como se muestra aquí.

En muchos casos, la limpieza no es un problema; simplemente dejas que el recolector de basura haga el trabajo. Sin embargo, cuando tienes que realizar una limpieza explícita, requiere un poco más de esfuerzo y cuidado porque no hay nada en qué confiar en términos de recolección de basura. Es posible que nunca se llame al recolector de basura. Si se le llama, puede reciclar objetos en el orden que desee. No puede confiar en la recolección de basura para nada excepto para la recuperación de memoria. Si desea limpiar, puede usar su propio método de limpieza, no lo use finalize().

nombre oculto

Si el nombre de un método de una clase base de Java se sobrecarga varias veces, redefinir el nombre del método en una clase derivada no oculta ninguna versión de la clase base. La sobrecarga funciona independientemente de si el método está definido en este nivel o en una clase base:

class Homer {
    
    
    char doh(char c) {
    
    
        System.out.println("doh(char)");
        return 'd';
    }

    float doh(float f) {
    
    
        System.out.println("doh(float)");
        return 1.0f;
    }
}

class Milhouse {
    
    
}

class Bart extends Homer {
    
    
    void doh(Milhouse m) {
    
    
        System.out.println("doh(Milhouse)");
    }
}

public class Hide {
    
    
    public static void main(String[] args) {
    
    
        Bart b = new Bart();
        b.doh(1);
        b.doh('x');
        b.doh(1.0f);
        b.doh(new Milhouse());
    }
}

Insertar descripción de la imagen aquí

Todos los métodos sobrecargados de Homer están disponibles en Bart , aunque Bart introduce un nuevo método sobrecargado. Como verá en el próximo capítulo, es más común que sobrecargar anular un método del mismo nombre, usando exactamente la misma firma de método y tipo de retorno que en la clase base. De lo contrario sería confuso.

Ha visto la anotación Java 5 **@Override**, no es una palabra clave, pero se puede usar como palabra clave. Opcionalmente, puede agregar esta anotación cuando planee anular un método. Si accidentalmente usa la sobrecarga en lugar de anular, el compilador generará un mensaje de error:

// reuse/Lisa.java
// (c)2017 MindView LLC: see Copyright.txt
// We make no guarantees that this code is fit for any purpose.
// Visit http://OnJava8.com for more book information.
// {WillNotCompile}

class Lisa extends Homer {
    
    
  @Override void doh(Milhouse m) {
    
    
    System.out.println("doh(Milhouse)");
  }
}

La etiqueta {WillNotCompile} excluye este archivo de la compilación Gradle de este libro, pero si lo compila manualmente verá: el método no anula un método de su superclase. **@Override ** La anotación evita que usted anule accidentalmente .

Elecciones entre composición y herencia

Tanto la composición como la herencia permiten colocar subobjetos en una nueva clase (la composición es explícita, mientras que la herencia es implícita). Quizás te preguntes cuál es la diferencia entre ambos y cómo elegir entre ellos.

Cuando desee incluir la funcionalidad de una clase existente en una nueva clase, utilice composición, no herencia. Es decir, se incrusta un objeto (normalmente privado) en la nueva clase para implementar su funcionalidad. Los usuarios de la nueva clase ven la interfaz de la nueva clase que usted define, no la interfaz del objeto incrustado.

A veces tiene sentido dar a los usuarios de una clase acceso directo a los componentes compuestos de la nueva clase. Simplemente declare el objeto miembro como público (piense en esto como un tipo de "semidelegación"). Los objetos miembro ocultan la implementación, por lo que es segura. Cuando el usuario sabe que está ensamblando un conjunto de piezas, la interfaz es más fácil de entender. El siguiente objeto automóvil es un buen ejemplo:

// reuse/Car.java
// Composition with public objects
class Engine {
    
    
    public void start() {
    
    }
    public void rev() {
    
    }
    public void stop() {
    
    }
}

class Wheel {
    
    
    public void inflate(int psi) {
    
    }
}

class Window {
    
    
    public void rollup() {
    
    }
    public void rolldown() {
    
    }
}

class Door {
    
    
    public Window window = new Window();
    
    public void open() {
    
    }
    public void close() {
    
    }
}

public class Car {
    
    
    public Engine engine = new Engine();
    public Wheel[] wheel = new Wheel[4];
    public Door left = new Door(), right = new Door(); // 2-door
    
    public Car() {
    
    
        for (int i = 0; i < 4; i++) {
    
    
            wheel[i] = new Wheel();
        }
    }
    
    public static void main(String[] args) {
    
    
        Car car = new Car();
        car.left.window.rollup();
        car.wheel[0].inflate(72);
    }
}

Debido a que la composición de car en este ejemplo también es parte del análisis del problema (no parte del diseño subyacente), declarar públicos a los miembros ayuda a los programadores del cliente a comprender cómo usar la clase y reduce la complejidad del código que enfrentan los creadores de clases. Sin embargo, recuerde que este es un caso especial. En términos generales, las propiedades aún deberían declararse privadas .

Cuando se utiliza la herencia, se utiliza una clase existente y se desarrolla una nueva versión de la misma. Por lo general, esto significa tomar una clase general y especializarla en una necesidad específica. Si lo piensas por un momento, encontrarás que no tiene sentido usar un objeto vehículo para formar un automóvil: el automóvil no contiene transporte, es transporte. Esta relación "es uno" se expresa por herencia, mientras que la relación "tiene uno" se expresa por composición.

protegido

Ahora que hemos tocado el tema de la herencia, la palabra clave protected tiene sentido. En un mundo ideal, la palabra clave privado por sí sola sería suficiente. En proyectos reales, a menudo queremos ocultar algo del mundo exterior tanto como sea posible y permitir que los miembros de clases derivadas accedan a él.

La palabra clave protegida sirve para este propósito. Dice "Esto es privado en lo que respecta al usuario de la clase . Pero es accesible para cualquier subclase que lo herede o clases en el mismo paquete". ( protegido también proporciona acceso al paquete)

Aunque es posible crear propiedades protegidas , el mejor enfoque es declarar la propiedad como privada para conservar siempre el derecho de cambiar la implementación subyacente. Luego controle los permisos de acceso de los herederos de la clase a través de protected .

// reuse/Orc.java
// The protected keyword
class Villain {
    
    
    private String name;
    
    protected void set(String nm) {
    
    
        name = nm;
    }
    
    Villain(String name) {
    
    
        this.name = name;
    }
    
    @Override
    public String toString() {
    
    
        return "I'm a Villain and my name is " + name;
    }
}

public class Orc extends Villain {
    
    
    private int orcNumber;
    
    public Orc(String name, int orcNumber) {
    
    
        super(name);
        this.orcNumber = orcNumber;
    }
    
    public void change(String name, int orcNumber) {
    
    
        set(name); // Available because it's protected
        this.orcNumber = orcNumber;
    }
    
    @Override
    public String toString() {
    
    
        return "Orc " + orcNumber + ": " + super.toString();
    }
    
    public static void main(String[] args) {
    
    
        Orc orc = new Orc("Limburger", 12);
        System.out.println(orc);
        orc.change("Bob", 19);
        System.out.println(orc);
    }
}

Producción:

Orc 12: I'm a Villain and my name is Limburger
Orc 19: I'm a Villain and my name is Bob

change()El método puede acceder set()al método porque set()el método está protegido . Observe que los métodos de la clase OrctoString() también usan la versión de la clase base.

Transformación ascendente

El aspecto más importante de la herencia es no proporcionar métodos para nuevas clases. Es una relación entre la nueva clase y la clase base. En resumen, esta relación se puede expresar como "la nueva clase es un tipo de la clase existente".

Esta descripción no es una forma elegante de explicar la herencia, está directamente respaldada por el lenguaje. Por ejemplo, supongamos que hay una clase base Instrument que representa un instrumento musical y una clase derivada Wind . Debido a que la herencia garantiza que todos los métodos de la clase base también estén disponibles en la clase derivada, cualquier mensaje enviado a la clase base también se puede enviar a la clase derivada. Si Instrument tiene un play()método, Wind también lo tiene. Esto significa que puedes decir con precisión que el objeto Viento también es un tipo de Instrumento . El siguiente ejemplo muestra cómo el compilador admite este concepto:

// reuse/Wind.java
// Inheritance & upcasting
class Instrument {
    
    
    public void play() {
    
    }
    
    static void tune(Instrument i) {
    
    
        // ...
        i.play();
    }
}

// Wind objects are instruments
// because they have the same interface:
public class Wind extends Instrument {
    
    
    public static void main(String[] args) {
    
    
        Wind flute = new Wind();
        Instrument.tune(flute); // Upcasting
    }
}

tune()El método acepta una referencia de tipo Instrumento . Sin embargo, en el método Windmain() , tune()el método pasa una referencia Wind . Dado que Java es muy estricto en cuanto a la verificación de tipos, parece extraño que un método que acepta un tipo acepte otro tipo, a menos que se dé cuenta de que el objeto Wind también es un objeto Instrument y que el método Instrumenttune debe existir en Wind . En tune(), el código funciona en Instrumento y en todas las clases derivadas de Instrumento . Este comportamiento de convertir una referencia de viento en una referencia de instrumento se llama _upcast_.

La terminología se basa en el diagrama de herencia de clases tradicional: la parte superior del diagrama es la raíz y luego se extiende hacia abajo. (Por supuesto, puede dibujar el diagrama de clases de cualquier forma que le resulte útil). Entonces, el diagrama de clases para Wind.java es:

Insertar descripción de la imagen aquí

En el diagrama de herencia, la transformación de una clase derivada a una clase base es ascendente, por lo que generalmente se la denomina "transformación ascendente". Debido a que está convirtiendo de una clase más específica a una clase más general, la conversión ascendente siempre es segura. En otras palabras, una clase derivada es un superconjunto de la clase base. Puede contener más métodos que la clase base, pero debe tener al menos los mismos métodos que la clase base. Durante la transformación ascendente, la interfaz de clase solo puede perder métodos pero no agregar métodos. Es por eso que el compilador todavía permite conversiones ascendentes sin conversiones explícitas u otros marcadores especiales.

También es posible realizar una transformación descendente en lugar de una transformación ascendente, pero habrá problemas, que se analizarán con más profundidad en el próximo capítulo y en el capítulo "Información de tipo".

Otra discusión sobre composición y herencia.

En la programación orientada a objetos, la forma más probable de crear y utilizar código es empaquetar datos y métodos en una clase y luego utilizar objetos de esa clase. También puede utilizar clases existentes para crear nuevas clases combinándolas. En realidad, la herencia no es muy común. Entonces, aunque muchas veces enfatizamos la herencia cuando enseñamos programación orientada a objetos, esto no significa que debamos usarla siempre que sea posible. Por el contrario, utilícelo con moderación a menos que la herencia sea realmente útil. Una de las formas más claras de decidir si usar composición o herencia es preguntarse si necesita convertir la nueva clase a una clase base. Si es necesaria una transformación ascendente, la herencia es necesaria, pero si no, se debe considerar más a fondo si se debe utilizar la herencia. El capítulo "Polimorfismo" presenta una de las razones más importantes para utilizar el upcasting, pero puedes elegir la mejor de las dos simplemente recordando preguntar "¿Necesito upcasting?".

Supongo que te gusta

Origin blog.csdn.net/GXL_1012/article/details/132216715
Recomendado
Clasificación