HOWTO · Java

Extender dos clases en Java

Descubre por qué una clase de Java solo puede extender una clase y cómo las interfaces y la composición combinan el comportamiento de varios tipos.

En esta página

Una clase de Java no puede extender dos clases. Cada clase tiene una sola superclase directa, por lo que una declaración como class Child extends First, Second no es válida en Java. Para combinar el comportamiento de varios tipos, extiende una clase e implementa tantas interfaces como necesites, o conserva otros objetos y delega en ellos el trabajo mediante composición.

Por qué una clase de Java no puede extender dos clases

Java utiliza herencia simple para las clases: cada clase puede indicar como máximo una superclase con extends. Así, un objeto tiene una sola cadena de herencia de clases y se evita la ambigüedad sobre qué implementación o estado de la superclase debe aplicarse cuando dos clases padre definen el mismo miembro. El compilador rechaza una declaración de clase que enumere dos superclases; es una restricción de compilación, no un error que se produzca mientras se ejecuta el programa.

La siguiente sintaxis de Java es deliberadamente inválida y no compilará: class Child extends First, Second {}. Usa una sola superclase cuando la nueva clase sea realmente una especialización de ese tipo. En la sección siguiente se muestra cómo los contratos de interfaz pueden describir capacidades adicionales.

Usa una superclase e implementa varias interfaces

Una clase puede extender otra clase e implementar varias interfaces. La superclase puede proporcionar estado o implementación compartidos, mientras que cada interfaz declara una capacidad que la clase se compromete a ofrecer. Es importante usar las palabras clave de Java correctamente: una clase usa extends para su superclase y implements para las interfaces. Una interfaz puede, a su vez, extender varias interfaces, pero eso no da a una clase varias superclases.

Este ejemplo completo define dos contratos de interfaz y los implementa junto con una superclase:

public class CatalogItem extends Record implements Printable, Identifiable {
    CatalogItem(String name) {
        super(name);
    }

    @Override
    public void printLabel() {
        System.out.println(name());
    }

    @Override
    public String id() {
        return "item-" + name().toLowerCase();
    }

    public static void main(String[] args) {
        CatalogItem item = new CatalogItem("Book");
        item.printLabel();
        System.out.println(item.id());
    }

}

interface Printable {
    void printLabel();
}

interface Identifiable {
    String id();
}

class Record {
    private final String name;

    Record(String name) {
        this.name = name;
    }

    String name() {
        return name;
    }
}
Book
item-book

CatalogItem hereda de Record la implementación de name() y proporciona los métodos que requieren ambas interfaces. Las interfaces son útiles cuando se quiere que clases no relacionadas compartan un tipo o un contrato de capacidades. No proporcionan automáticamente estado ni una implementación de clase como lo haría una segunda superclase.

Combina el comportamiento de varias clases mediante composición

La composición combina objetos mediante una relación de tipo «tiene un»: una clase conserva referencias a otros objetos colaboradores y llama a sus métodos. Es útil cuando los comportamientos proceden de clases que también tienen utilidad por separado, o cuando conviene poder sustituir el colaborador. La clase que los contiene controla qué operaciones ofrece y puede delegar solo el trabajo que necesitan quienes la usan.

En este ejemplo, un SmartDevice contiene un PowerController y un MessageSender, y delega en ellos las operaciones correspondientes:

public class SmartDevice {
    private final PowerController power = new PowerController();
    private final MessageSender sender = new MessageSender();

    void start() {
        power.powerOn();
    }

    void sendMessage(String message) {
        sender.send(message);
    }

    public static void main(String[] args) {
        SmartDevice device = new SmartDevice();
        device.start();
        device.sendMessage("Ready");
    }

}

class PowerController {
    void powerOn() {
        System.out.println("Power on");
    }
}

class MessageSender {
    void send(String message) {
        System.out.println("Sending: " + message);
    }
}
Power on
Sending: Ready

SmartDevice no se convierte en un PowerController ni en un MessageSender; utiliza ambos. En un programa más grande, los colaboradores pueden proporcionarse mediante un constructor o representarse con interfaces, lo que facilita sustituir implementaciones y probar por separado la clase que los contiene.

Elige el patrón de reutilización adecuado

Elige la herencia de clases cuando el nuevo tipo sea realmente una especialización de una única superclase y deba heredar su implementación. Añade interfaces cuando un tipo deba comprometerse a ofrecer capacidades que comparta con otras clases. Usa composición cuando el comportamiento deba ensamblarse a partir de componentes, delegarse o modificarse de forma independiente.

Procura que cada clase tenga una responsabilidad clara y exponga solo las operaciones que necesitan quienes la usan. Prefiere contratos de interfaz pequeños en lugar de amplios y evita duplicar comportamientos que pertenecen a un colaborador compartido. Estas decisiones facilitan comprender, probar y adaptar las relaciones cuando cambian los requisitos. En resumen, Java no permite que una clase extienda dos clases; una superclase, varios contratos de interfaz y colaboradores compuestos cubren las necesidades habituales de reutilización.