10. Interfaz (1)

Resumen del capítulo

  • Clases y métodos abstractos.
  • Creación de interfaz
    • Método predeterminado
    • herencia múltiple
    • métodos estáticos en la interfaz
    • Instrumento como interfaz

Las interfaces y las clases abstractas proporcionan una forma más estructurada de separar la interfaz de la implementación.

Este mecanismo es poco común en los lenguajes de programación; C++, por ejemplo, sólo tiene soporte indirecto para este concepto. La existencia de estas palabras clave en Java muestra que estas ideas son importantes y Java les proporciona soporte directo.

Primero, veremos las clases abstractas, un medio entre clases e interfaces ordinarias. Aunque su primer pensamiento es crear interfaces, las clases abstractas también son una herramienta importante y necesaria para crear clases con propiedades y métodos no implementados. No siempre se pueden utilizar interfaces puras.

Clases y métodos abstractos.

En el ejemplo de instrumento del capítulo anterior, los métodos de la clase base Instrumento suelen ser métodos "tontos". Si se llaman estos métodos, se producirán algunos errores. Esto se debe a que el propósito de una interfaz es crear una interfaz común para sus clases derivadas.

En esos ejemplos, la única razón para crear esta interfaz común es que diferentes subclases puedan representar esta interfaz de diferentes maneras. Una interfaz común establece una forma básica para expresar partes comunes de todas las clases derivadas. Otra forma de decirlo es que Instrument se denomina clase base abstracta, o simplemente clase abstracta.

Para una clase abstracta como Instrument , sus objetos casi siempre no tienen sentido. Se crea una clase abstracta para manipular una serie de clases a través de una interfaz común. Por lo tanto, Instrument solo representa la interfaz, no la implementación específica, por lo que crear un objeto Instrument no tiene sentido y es posible que deseemos evitar que los usuarios lo hagan. Esto se puede lograr haciendo que todos los métodos del Instrumento generen errores, pero hacerlo retrasa los mensajes de error hasta el tiempo de ejecución y requiere pruebas confiables y exhaustivas por parte del usuario. Es mejor detectar problemas en tiempo de compilación.

Java proporciona un mecanismo llamado método abstracto. Este método está incompleto: solo tiene una declaración sin un cuerpo de método. A continuación se muestra la sintaxis de declaración de un método abstracto:

abstract void f();

Una clase que contiene métodos abstractos se llama _clase abstracta_. Si una clase contiene uno o más métodos abstractos, la clase en sí también debe calificarse como abstracta; de lo contrario, el compilador informará un error.

// interface/Basic.java
abstract class Basic {
    
    
    abstract void unimplemented();
}

Si una clase abstracta está incompleta, ¿qué hará Java al intentar crear un objeto de esta clase? No crea un objeto de la clase abstracta, por lo que solo obtenemos errores del compilador. Esto garantiza la pureza de la clase abstracta y no tenemos que preocuparnos por el mal uso.

// interfaces/AttemptToUseBasic.java
// {WillNotCompile}
public class AttemptToUseBasic {
    
    
    Basic b = new Basic();
    // error: Basic is abstract; cannot be instantiated
}

Si crea una nueva clase que hereda una clase abstracta y crea objetos para ella, debe proporcionar definiciones de métodos para todos los métodos abstractos de la clase base. Si no hace esto (puede optar por no hacerlo), la nueva clase seguirá siendo una clase abstracta y el compilador nos obligará a agregar la palabra clave abstracta a la nueva clase.

// interfaces/Basic2.java
abstract class Basic2 extends Basic {
    
    
    int f() {
    
    
        return 111;
    }
    
    abstract void g() {
    
    
        // unimplemented() still not implemented
    }
}

Una clase que no contiene ningún método abstracto se puede designar como abstracta . Esto es útil cuando los métodos abstractos de la clase no tienen sentido pero se desea evitar la creación de objetos de la clase.

// interfaces/AbstractWithoutAbstracts.java
abstract class Basic3 {
    
    
    int f() {
    
    
        return 111;
    }
    
    // No abstract methods
}

public class AbstractWithoutAbstracts {
    
    
    // Basic3 b3 = new Basic3();
    // error: Basic3 is abstract; cannot be instantiated
}

Para crear una clase inicializable, extienda la clase abstracta y proporcione definiciones para todos los métodos abstractos:

// interfaces/Instantiable.java
abstract class Uninstantiable {
    
    
    abstract void f();
    abstract int g();
}

public class Instantiable extends Uninstantiable {
    
    
    @Override
    void f() {
    
    
        System.out.println("f()");
    }
    
    @Override
    int g() {
    
    
        return 22;
    }
    
    public static void main(String[] args) {
    
    
        Uninstantiable ui = new Instantiable();
    }
}

Preste atención @Overrideal uso de . Sin esta anotación, si no define el mismo nombre de método o firma, el mecanismo de abstracción pensará que no ha implementado el método abstracto y generará un error en tiempo de compilación. Por lo tanto, podría pensar que aquí @Overridees redundante . Sin embargo, @Overridetambién indica que este método está anulado; creo que es útil, así que lo usaré @Override, sobre todo porque el compilador me indicará un error cuando esta anotación no esté presente.

Recuerde, el nivel de acceso de facto es "amigable". Pronto verás que las interfaces designan automáticamente sus métodos como públicos . De hecho, las interfaces solo permiten métodos públicos , si no se agrega ningún modificador de acceso, los métodos de la interfaz no son amigables sino públicos . Sin embargo, las clases abstractas permiten todo:

// interfaces/AbstractAccess.java
abstract class AbstractAccess {
    
    
    private void m1() {
    
    }
    
    // private abstract void m1a(); // illegal
    
    protected void m2() {
    
    }
    
    protected abstract void m2a();
    
    void m3() {
    
    }
    
    abstract void m3a();
    
    public void m4() {
    
    }
    
    public abstract void m4a();
}

Tiene sentido que el resumen privado esté prohibido porque no se puede definir legalmente en ninguna subclase de AbstractAccess .

La clase Instrumento del capítulo anterior se puede convertir fácilmente en una clase abstracta. Sólo algunos métodos necesitan ser abstractos . Designar una clase como abstracta no obliga a que todos los métodos de la clase sean métodos abstractos. Como se muestra abajo:

Insertar descripción de la imagen aquí

A continuación se muestra un ejemplo de un instrumento orquestal modificado para utilizar clases y métodos abstractos:

// interfaces/music4/Music4.java
// Abstract classes and methods
// {java interfaces.music4.Music4}
package interfaces.music4;
import polymorphism.music.Note;

abstract class Instrument {
    
    
    private int i; // Storage allocated for each
    
    public abstract void play(Note n);
    
    public String what() {
    
    
        return "Instrument";
    }
    
    public abstract void adjust();
}

class Wind extends Instrument {
    
    
    @Override
    public void play(Note n) {
    
    
        System.out.println("Wind.play() " + n);
    }
    
    @Override
    public String what() {
    
    
        return "Wind";
    }
    
    @Override
    public void adjust() {
    
    
        System.out.println("Adjusting Wind");
    }
}

class Percussion extends Instrument {
    
    
    @Override
    public void play(Note n) {
    
    
        System.out.println("Percussion.play() " + n);
    }
    
    @Override
    public String what() {
    
    
        return "Percussion";
    }
    
    @Override
    public void adjust() {
    
    
        System.out.println("Adjusting Percussion");
    }
}

class Stringed extends Instrument {
    
    
    @Override
    public void play(Note n) {
    
    
        System.out.println("Stringed.play() " + n);
    }
    
    @Override
    public String what() {
    
    
        return "Stringed";
    }
    
    @Override
    public void adjust() {
    
    
        System.out.println("Adjusting Stringed");
    }
}

class Brass extends Wind {
    
    
    @Override
    public void play(Note n) {
    
    
        System.out.println("Brass.play() " + n);
    }
    
    @Override
    public void adjust() {
    
    
        System.out.println("Adjusting Brass");
    }
}

class Woodwind extends Wind {
    
    
    @Override
    public void play(Note n) {
    
    
        System.out.println("Woodwind.play() " + n);
    }
    
    @Override
    public String what() {
    
    
        return "Woodwind";
    }
}

public class Music4 {
    
    
    // Doesn't care about type, so new types
    // added to system still work right:
    static void tune(Instrument i) {
    
    
        // ...
        i.play(Note.MIDDLE_C);
    }
    
    static void tuneAll(Instrument[] e) {
    
    
        for (Instrument i: e) {
    
    
            tune(i);
        }
    }
    
    public static void main(String[] args) {
    
    
        // Upcasting during addition to the array:
        Instrument[] orchestra = {
    
    
            new Wind(),
            new Percussion(),
            new Stringed(),
            new Brass(),
            new Woodwind()
        };
        tuneAll(orchestra);
    }
}

Producción:

Wind.play() MIDDLE_C
Percussion.play() MIDDLE_C
Stringed.play() MIDDLE_C
Brass.play() MIDDLE_C
Woodwind.play() MIDDLE_C

A excepción de Instrument , básicamente no hay diferencia.

La creación de clases abstractas y métodos abstractos es útil porque hacen explícita la abstracción de la clase e informan a los usuarios y al compilador sobre la intención de uso. Las clases abstractas también son una herramienta de refactorización útil, ya que se utilizan para mover fácilmente métodos públicos hacia arriba en la jerarquía de herencia.

Creación de interfaz

Las interfaces se crean utilizando la palabra clave interface. En este libro, la interfaz es tan común como la clase. A menos que se mencione específicamente la palabra clave interfaz , en otros casos se utilizan fuentes normales para escribir interfaz.

Describir las interfaces anteriores a Java 8 era más fácil porque solo permitían métodos abstractos. Como esto:

// interfaces/PureInterface.java
// Interface only looked like this before Java 8
public interface PureInterface {
    
    
    int m1(); 
    void m2();
    double m3();
}

Ni siquiera necesitamos agregar la palabra clave abstracta al método porque el método está en la interfaz. Java sabe que estos métodos no pueden tener un cuerpo de método (aún puede agregar la palabra clave abstracta al método, pero parecerá que no comprende la interfaz, lo cual es más vergonzoso).

Por lo tanto, antes de Java 8 podríamos decir esto: La palabra clave interfaz produce una clase completamente abstracta sin proporcionar ninguna implementación. Solo podemos describir cómo debería verse la clase, qué hacer, pero no cómo hacerlo, es decir, solo podemos determinar el nombre del método, la lista de parámetros y el tipo de retorno, pero no podemos determinar el cuerpo del método. Las interfaces sólo proporcionan forma y generalmente no tienen implementaciones, aunque pueden tener implementaciones bajo ciertas circunstancias restringidas.

Una interfaz representa: Todas las clases que implementan la interfaz tienen este aspecto. Por lo tanto, cualquier código que utilice una interfaz particular sabe qué métodos de esa interfaz se pueden llamar, y sólo eso. Por tanto, las interfaces se utilizan para establecer protocolos entre clases. (En algunos lenguajes de programación orientados a objetos, la palabra clave protocolo se utiliza para realizar la misma función).

Las interfaces cambian ligeramente en Java 8 porque Java 8 permite que las interfaces contengan métodos predeterminados y métodos estáticos, por algunas razones importantes que comprenderá más adelante. El concepto básico de interfaces sigue siendo el mismo, por encima de los tipos y debajo de las implementaciones. Quizás la diferencia más obvia entre interfaces y clases abstractas es la forma idiomática de uso. Un uso típico de una interfaz es representar un tipo de clase o un adjetivo, como Runnable o Serializable, mientras que una clase abstracta suele ser parte de una jerarquía de clases o el tipo de algo, como String o ActionHero.

Utilice la palabra clave interfaz en lugar de clase para crear una interfaz. Al igual que una clase, debe agregar la palabra clave pública antes de la palabra clave interfaz (pero solo cuando el nombre de la interfaz es el mismo que el nombre del archivo), de lo contrario, la interfaz solo tiene derechos de acceso al paquete y solo se puede usar bajo el paquete. con la misma interfaz.

Las interfaces también pueden contener propiedades, que implícitamente se designan como estáticas y finales .

Use la palabra clave implements para hacer que una clase siga una interfaz específica (o un conjunto de interfaces). Esto significa: La interfaz es solo la apariencia. Ahora quiero explicar cómo funciona. Aparte de eso, parece una herencia.

// interfaces/ImplementingAnInterface.java
interface Concept {
    
     // Package access
    void idea1();
    void idea2();
}

class Implementation implements Concept {
    
    
    @Override
    public void idea1() {
    
    
        System.out.println("idea1");
    }
    
    @Override
    public void idea2() {
    
    
        System.out.println("idea2");
    }
}

Puede optar por declarar explícitamente métodos en una interfaz como públicos , pero incluso si no lo hace, son públicos . Entonces, al implementar una interfaz, los métodos de la interfaz deben definirse como públicos . De lo contrario, solo tienen acceso a paquetes, por lo que al heredar, su accesibilidad se reduce, lo cual no está permitido por el compilador de Java.

Método predeterminado

Java 8 agrega un nuevo uso para la palabra clave default (anteriormente solo se usaba en declaraciones de cambio y anotaciones). Cuando se usa en una interfaz, cualquier cuerpo de método que implemente la interfaz pero no defina un método puede usar el cuerpo del método creado por defecto . Los métodos predeterminados son más restringidos que los métodos de clases abstractas, pero son muy útiles, como veremos en el capítulo "Programación de flujo". Ahora veamos cómo usarlo:

// interfaces/AnInterface.java
interface AnInterface {
    
    
    void firstMethod();
    void secondMethod();
}

Podemos implementar la interfaz de esta manera:

// interfaces/AnImplementation.java
public class AnImplementation implements AnInterface {
    
    
    public void firstMethod() {
    
    
        System.out.println("firstMethod");
    }
    
    public void secondMethod() {
    
    
        System.out.println("secondMethod");
    }
    
    public static void main(String[] args) {
    
    
        AnInterface i = new AnImplementation();
        i.firstMethod();
        i.secondMethod();
    }
}

Producción:

firstMethod
secondMethod

Si agregamos un nuevo método a AnInterfacenewMethod() pero no lo implementamos en AnImplementation , el compilador informará un error:

AnImplementation.java:3:error: AnImplementation is not abstract and does not override abstract method newMethod() in AnInterface
public class AnImplementation implements AnInterface {
^
1 error

Si usamos la palabra clave defaultnewMethod() para proporcionar una implementación predeterminada para el método, entonces todo el código relacionado con la interfaz puede funcionar normalmente sin verse afectado, y estos códigos también pueden llamar a nuevos métodos newMethod():

// interfaces/InterfaceWithDefault.java
interface InterfaceWithDefault {
    
    
    void firstMethod();
    void secondMethod();
    
    default void newMethod() {
    
    
        System.out.println("newMethod");
    }
}

La palabra clave default permite proporcionar implementaciones de métodos en interfaces; esto estaba prohibido antes de Java 8.

// interfaces/Implementation2.java
public class Implementation2 implements InterfaceWithDefault {
    
    
    @Override
    public void firstMethod() {
    
    
        System.out.println("firstMethod");
    }

    @Override
    public void secondMethod() {
    
    
        System.out.println("secondMethod");
    }

    public static void main(String[] args) {
    
    
        InterfaceWithDefault i = new Implementation2();
        i.firstMethod();
        i.secondMethod();
        i.newMethod();
    }
}

Producción:

firstMethod
secondMethod
newMethod

Aunque no está definido en Implementación2newMethod() , se puede utilizar newMethod().

Una razón convincente para agregar métodos predeterminados es que permite agregar nuevos métodos a una interfaz sin romper el código que ya usa la interfaz. Los métodos predeterminados a veces se denominan métodos de protección o métodos de extensión virtual.

herencia múltiple

La herencia múltiple significa que una clase puede heredar rasgos y características de múltiples tipos principales.

Cuando se diseñó Java por primera vez, se criticó el mecanismo de herencia múltiple de C++. Java solía ser un lenguaje que requería estrictamente una herencia única: se podía heredar de una sola clase (o clase abstracta), pero se podía implementar cualquier cantidad de interfaces. Antes de Java 8, las interfaces no tenían equipaje: eran solo una descripción de cómo debería verse un método.

Ahora, muchos años después, Java tiene algún tipo de característica de herencia múltiple a través de métodos predeterminados. Combinar una interfaz con métodos predeterminados significa combinar el comportamiento de múltiples clases base. Debido a que las propiedades todavía no están permitidas en las interfaces (solo propiedades estáticas, no aplicables), las propiedades seguirán viniendo solo de una única clase base o abstracta, es decir, no habrá herencia múltiple de estado. Como sigue:

// interfaces/MultipleInheritance.java
import java.util.*;

interface One {
    
    
    default void first() {
    
    
        System.out.println("first");
    }
}

interface Two {
    
    
    default void second() {
    
    
        System.out.println("second");
    }
}

interface Three {
    
    
    default void third() {
    
    
        System.out.println("third");
    }
}

class MI implements One, Two, Three {
    
    }

public class MultipleInheritance {
    
    
    public static void main(String[] args) {
    
    
        MI mi = new MI();
        mi.first();
        mi.second();
        mi.third();
    }
}

Producción:

first
second
third

Ahora hacemos algo que era imposible antes de Java 8: combinar implementaciones de múltiples fuentes. Esto funciona bien siempre que el nombre del método y la lista de parámetros sean diferentes en el método de la clase base; de ​​lo contrario, obtendrá un error del compilador:

// interface/MICollision.java
import java.util.*;

interface Bob1 {
    
    
    default void bob() {
    
    
        System.out.println("Bob1::bob");
    }
}

interface Bob2 {
    
    
    default void bob() {
    
    
        System.out.println("Bob2::bob");
    }
}

// class Bob implements Bob1, Bob2 {}
/* Produces:
error: class Bob inherits unrelated defaults
for bob() from types Bob1 and Bob2
class Bob implements Bob1, Bob2 {}
^
1 error
*/

interface Sam1 {
    
    
    default void sam() {
    
    
        System.out.println("Sam1::sam");
    }
}

interface Sam2 {
    
    
    default void sam(int i) {
    
    
        System.out.println(i * 2);
    }
}

// This works because the argument lists are distinct:
class Sam implements Sam1, Sam2 {
    
    }

interface Max1 {
    
    
    default void max() {
    
    
        System.out.println("Max1::max");
    }
}

interface Max2 {
    
    
    default int max() {
    
    
        return 47;
    }
}

// class Max implements Max1, Max2 {}
/* Produces:
error: types Max2 and Max1 are imcompatible;
both define max(), but with unrelated return types
class Max implements Max1, Max2 {}
^
1 error
*/

Dos métodos en la clase Samsam() tienen el mismo nombre de método pero firmas diferentes: la firma del método incluye el nombre del método y los tipos de parámetros, y el compilador también la utiliza para distinguir métodos. Pero como puede verse en la clase Max , el tipo de retorno no forma parte de la firma del método y, por lo tanto, no se puede utilizar para distinguir métodos. Para resolver este problema, es necesario anular el método conflictivo:

// interfaces/Jim.java
import java.util.*;

interface Jim1 {
    
    
    default void jim() {
    
    
        System.out.println("Jim1::jim");
    }
}

interface Jim2 {
    
    
    default void jim() {
    
    
        System.out.println("Jim2::jim");
    }
}

public class Jim implements Jim1, Jim2 {
    
    
    @Override
    public void jim() {
    
    
        Jim2.super.jim();
    }
    
    public static void main(String[] args) {
    
    
        new Jim().jim();
    }
}

Producción:

Jim2::jim

Por supuesto, puede redefinir el método, pero también puede usar la palabra clave superjim() para seleccionar una de las implementaciones de la clase base como en el ejemplo anterior .

métodos estáticos en la interfaz

Java 8 permite agregar métodos estáticos en las interfaces. Hacerlo puede colocar correctamente la funcionalidad de la herramienta en la interfaz, manipulando así la interfaz o convirtiéndose en una herramienta de propósito general:

// onjava/Operations.java
package onjava;
import java.util.*;

public interface Operations {
    
    
    void execute();
    
    static void runOps(Operations... ops) {
    
    
        for (Operations op: ops) {
    
    
            op.execute();
        }
    }
    
    static void show(String msg) {
    
    
        System.out.println(msg);
    }
}

Esta es una versión del patrón de diseño Método de plantilla (descrito en detalle en el capítulo "Patrones de diseño"), que runOps()es un método de plantilla. runOps()Usando una lista de argumentos variada, podemos pasar tantos argumentos de Operación como queramos y ejecutarlos en orden:

// interface/Machine.java
import java.util.*;
import onjava.Operations;

class Bing implements Operations {
    
    
    @Override
    public void execute() {
    
    
        Operations.show("Bing");
    }
}

class Crack implements Operations {
    
    
    @Override
    public void execute() {
    
    
        Operations.show("Crack");
    }
}

class Twist implements Operations {
    
    
    @Override
    public void execute() {
    
    
        Operations.show("Twist");
    }
}

public class Machine {
    
    
    public static void main(String[] args) {
    
    
        Operations.runOps(
        	new Bing(), new Crack(), new Twist());
    }
}

Producción:

Bing
Crack
Twist

Estas son las diferentes formas de crear operaciones : una clase externa (Bing), una clase anónima, una referencia de método y una expresión lambda, que es sin duda la mejor solución aquí.

Esta característica es una mejora porque permite colocar métodos estáticos en lugares más apropiados.

Instrumento como interfaz

Volviendo al ejemplo de un instrumento, usando una interfaz:

Insertar descripción de la imagen aquí

Las clases Woodwind y Brass indican que una vez que se implementa una interfaz, su implementación se convierte en una clase normal que se puede ampliar de la forma habitual.

La forma en que funcionan las interfaces es que no necesitamos declarar explícitamente los métodos que contienen como públicos , son automáticamente públicos . play()y defina la implementación adjust()utilizando la palabra clave predeterminada . Antes de Java 8, estas definiciones eran redundantes y molestaba que se repitieran en cada implementación:

enum Note {
    
    
  MIDDLE_C, C_SHARP, B_FLAT; // Etc.
}

interface Instrument {
    
    
    // Compile-time constant:
    int VALUE = 5; // static & final

    default void play(Note n) {
    
      // Automatically public
        System.out.println(this + ".play() " + n);
    }

    default void adjust() {
    
    
        System.out.println("Adjusting " + this);
    }
}

class Wind implements Instrument {
    
    
    @Override
    public String toString() {
    
    
        return "Wind";
    }
}

class Percussion implements Instrument {
    
    
    @Override
    public String toString() {
    
    
        return "Percussion";
    }
}

class Stringed implements Instrument {
    
    
    @Override
    public String toString() {
    
    
        return "Stringed";
    }
}

class Brass extends Wind {
    
    
    @Override
    public String toString() {
    
    
        return "Brass";
    }
}

class Woodwind extends Wind {
    
    
    @Override
    public String toString() {
    
    
        return "Woodwind";
    }
}

public class Music5 {
    
    
    // Doesn't care about type, so new types
    // added to the system still work right:
    static void tune(Instrument i) {
    
    
        // ...
        i.play(Note.MIDDLE_C);
    }

    static void tuneAll(Instrument[] e) {
    
    
        for (Instrument i : e) {
    
    
            tune(i);
        }
    }

    public static void main(String[] args) {
    
    
        // Upcasting during addition to the array:
        Instrument[] orchestra = {
    
    
                new Wind(),
                new Percussion(),
                new Stringed(),
                new Brass(),
                new Woodwind()
        };
        tuneAll(orchestra);
    }
}

Producción:

Wind.play() MIDDLE_C
Percussion.play() MIDDLE_C
Stringed.play() MIDDLE_C
Brass.play() MIDDLE_C
Woodwind.play() MIDDLE_C

Otro cambio en esta versión del ejemplo es que what()se modifica a toString()método, porque toString()implementa what()la lógica que se supone que debe implementar el método. Debido a toString()que es un método de la clase base Object , no es necesario que aparezca en la interfaz.

Tenga en cuenta que ya sea que se convierta en una clase normal llamada Instrument , una clase abstracta llamada Instrument o una interfaz llamada Instrument , su comportamiento es el mismo. De hecho, tune()del método no queda claro si Instrumento es una clase ordinaria, una clase abstracta o una interfaz.

Supongo que te gusta

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