- Interfaces - Desacoplamiento completo - "Herencia múltiple" en Java - Ampliación de la interfaz mediante herencia - Colisiones de nombres al combinar interfaces - Adaptación a una interfaz - Campos en las interfaces - Inicialización de campos en las interfaces - Anidamiento de interfaces - Interfaces y factorías
Las interfaces y las clases abstractas proporcionan una forma más estructurada de separar la interfaz de la implementación.
Estas características no son tan comunes en los lenguajes de programación. Existen palabras clave en Java para estos conceptos, por tanto, estas ideas fueron consideradas importantes para proporcionar soporte directo.
Una clase abstracta está entre una clase normal y una interfaz. Se trata de una herramienta importante y necesaria para construir clases que dispongan de algunos métodos no implementados.
CLASES ABSTRACTAS Y MÉTODOS ABSTRACTOS
Si recordamos los ejemplos del tema 8 de instrumentos musicales, los métodos de la clase base Instrument pueden considerarse ficticios. Normalmente, nunca deben ser invocados y el único sentido de la existencia de la clase Instrument es tener una interfaz común para las clases derivadas de ella.
Esta interfaz permite expresar todo aquello que es común para todas las clases derivadas. Otra manera de decir esto es que Instrument es una clase abstracta.
Si declaramos una clase Instrument abstracta, los objetos de dicha clase no tienen significado propio. Cuando creamos una clase abstracta es para manipular un conjunto de clases a través de su interfaz común. Por tanto, Instrument expresa la interfaz y no una implementación concreta, así, no tiene ningún sentido crear un objeto Instrument, es más, si tratamos de crear un objeto a partir de una clase abstracta obtendremos un error en tiempo de compilación.
Java proporciona un mecanismo para crear los denominados métodos abstractos. Son métodos incompletos que sólo tienen una declaración y no disponen de cuerpo. La declaración sería:
abstract void f();Una clase con uno o más métodos abstractos se denomina abstracta y debe declararse como tal.
Si definimos una clase derivada de una clase abstracta, se podrán crear objetos de esta nueva clase, pero deben definirse todos los métodos abstractos. Si no, la clase derivada también será abstracta y deberá declararse como tal.
Se pueden definir clases abstractas sin ningún método abstracto. Esto es útil si tenemos una clase en la que no tiene sentido declarar métodos abstractos pero no queremos que se generen instancias de esta clase.
No es necesario que todos los métodos de una clase abstracta sean abstractos. La clase Instrument del tema anterior quedaría así:
//: interfaces/music4/Music4.java // Abstract classes and methods. package interfaces.music4; import polymorphism.music.Note; import static net.mindview.util.Print.*; 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 { public void play(Note n) { print("Wind.play() " + n); } public String what() { return "Wind"; } public void adjust() {} } class Percussion extends Instrument { public void play(Note n) { print("Percussion.play() " + n); } public String what() { return "Percussion"; } public void adjust() {} } class Stringed extends Instrument { public void play(Note n) { print("Stringed.play() " + n); } public String what() { return "Stringed"; } public void adjust() {} } class Brass extends Wind { public void play(Note n) { print("Brass.play() " + n); } public void adjust() { print("Brass.adjust()"); } } class Woodwind extends Wind { public void play(Note n) { print("Woodwind.play() " + n); } public String what() { return "Woodwind"; } } public class Music4 { // 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); } } /* Output: Wind.play() MIDDLE_C Percussion.play() MIDDLE_C Stringed.play() MIDDLE_C Brass.play() MIDDLE_C Woodwind.play() MIDDLE_C *///:~Sólo se han realizado cambios en la clase base.
Las clases y métodos abstractos informan al usuario y al compilador de cómo se pretende que se utilice esa clase. También son útiles como herramienta de rediseño ya que permiten mover hacia arriba los métodos comunes en la jerarquía de herencia.
Ejercicio 1. Modifica el Ejercicio 9 del capítulo anterior de modo que Rodent sea una clase abstracta. Define los métodos de Rodent como abstractos siempre que sea posible.
Ejercicio 2. Crea una clase abstracta sin incluir ningún método abstracto y verifica que no pueden crearse instancias de la clase.
Ejercicio 3. Crea una clase base con un método print() abstracto que se sustituye en una clase derivada. La versión sustituida del método debe imprimir el valor de una variable int definida en la clase derivada. En el punto de definición de esta variable, proporciona un valor distinto de cero. En el constructor de la clase base, llama a este método. En main(), crea un objeto del tipo derivado y luego invoca su método print(). Explica los resultados.
Ejercicio 4. Crea una clase abstracta sin métodos. Define una clase derivada y añádele un método. Crea un método estático que tome una referencia a la clase base, especialízalo para que apunte a la clase derivada e invoque el método. En main(), demuestra que este mecanismo funciona. Ahora, incluye la declaración abstracta del método en la clase base, eliminando así la necesidad de la especialización.
INTERFACES
La palabra clave interface lleva el concepto de abstracción un paso más allá. Una interface es una clase completamente abstracta, ya que no se implementa ninguno de sus métodos, sólo tenemos los nombres de los métodos, las listas de argumentos y los tipos de retorno. La interfaz proporciona la forma.
Lo que las interfaces hacen es decir: "Todas las clases que implementen esta interfaz concreta tendrán este aspecto". La interfaz se utiliza para establecer un "protocolo" entre las clases.
Pero una interfaz es algo más que una clase totalmente abstracta ya que permite realizar una variante del mecanismo de "herencia múltiple", creando una clase que pueda generalizarse a más de un tipo base.
Cuando queramos crear una interfaz utilizaremos la palabra clave interface en lugar de class. Podemos incluir la palabra public antes de interface, sólo si la interfaz está definida en un archivo del mismo nombre. Si no se incluye la palabra public, se obtiene acceso de tipo paquete. La interfaz puede contener campos, éstos serán static y final.
Cuando queramos crear una clase que se adapte a una o varias interfaces concretas, debemos utilizar la palabra clave implements. Con la interfaz tenemos el aspecto, ahora vamos a decir cómo funciona. La definición de clase derivada es parecida a la herencia normal. Vamos a verlo con el ejemplo de los instrumentos musicales:
Las clases Woodwind y Brass heredan de la clase Wind que implementa la interfaz. La clase Wind es una clase normal que se puede ampliar de manera normal.
Los métodos de una interfaz son públicos aunque no lo especifiquemos. Así, al implementar una interfaz estos métodos deben definirse como públicos, si no tendrían acceso de paquete y reduciríamos la accesibilidad de los métodos durante la herencia, y esto no lo permite el compilador Java.
Vamos a ver el ejemplo de una interfaz con la clase Instrument:
//: interfaces/music5/Music5.java // Interfaces. package interfaces.music5; import polymorphism.music.Note; import static net.mindview.util.Print.*; interface Instrument { // Compile-time constant: int VALUE = 5; // static & final // Cannot have method definitions: void play(Note n); // Automatically public void adjust(); } class Wind implements Instrument { public void play(Note n) { print(this + ".play() " + n); } public String toString() { return "Wind"; } public void adjust() { print(this + ".adjust()"); } } class Percussion implements Instrument { public void play(Note n) { print(this + ".play() " + n); } public String toString() { return "Percussion"; } public void adjust() { print(this + ".adjust()"); } } class Stringed implements Instrument { public void play(Note n) { print(this + ".play() " + n); } public String toString() { return "Stringed"; } public void adjust() { print(this + ".adjust()"); } } class Brass extends Wind { public String toString() { return "Brass"; } } class Woodwind extends Wind { 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); } } /* Output: Wind.play() MIDDLE_C Percussion.play() MIDDLE_C Stringed.play() MIDDLE_C Brass.play() MIDDLE_C Woodwind.play() MIDDLE_C *///:~En esta versión hemos cambiado el método what() por toString() ya que what() se utilizaba para lo mismo que hace toString(). Este método forma parte de la raíz Object por eso no necesita aparecer en la interfaz.
El resto del código funciona de la misma manera. No importa si generalizamos a una clase "normal" llamada Instrument, a una clase abstracta llamada Instrument o a una interfaz llamada Instrument, el comportamiento siempre es el mismo. En el método tune() no sabemos si Instrument es una clase normal, abstracta o interfaz.
Ejercicio 5. Crea una interfaz que contenga tres métodos en su propio paquete. Implementa la interfaz en un paquete diferente.
Ejercicio 6. Demuestra que todos los métodos de una interfaz son automáticamente públicos.
Ejercicio 7. Modifica el Ejercicio 9 del Capítulo 8, Polimorfismo, para que Rodent sea una interfaz.
Ejercicio 8. En polymorphism.Sandwich.java, crea una interfaz denominada FastFood (con los métodos apropiados) y cambia Sandwich de modo que también implemente FastFood.
Ejercicio 9. Rediseña Music5.java moviendo los métodos comunes de Wind, Percussion y Stringed a una clase abstracta.
Ejercicio 10. Modifica Music5.java añadiendo una interfaz Playable. Mueve la declaración de play() de Instrument a Playable. Añade Playable a las clases derivadas incluyéndola en la lista implements. Modifica tune() de modo que acepte un objeto Playable en lugar de un objeto Instrument.
DESACOPLAMIENTO COMPLETO
Si tenemos un método en una clase particular en lugar de una interfaz, estamos limitados a utilizar esta clase o sus subclases. No podemos aplicar este método a una clase fuera de esta jerarquía. Las interfaces relajan esta restricción y permiten tener código más reutilizable.
Por ejemplo, tenemos una clase Processor con los métodos name() y process() que toman una entrada, la modifican y producen una salida. La clase base se puede ampliar para crear diferentes tipos de objetos Processor. En este caso, los subtipos de Processor modifican objetos de tipo String, (los tipos de retorno de los métodos pueden variar, pero no los tipos de los argumentos):
//: interfaces/classprocessor/Apply.java package interfaces.classprocessor; import java.util.*; import static net.mindview.util.Print.*; class Processor { public String name() { return getClass().getSimpleName(); } Object process(Object input) { return input; } } class Upcase extends Processor { String process(Object input) { // Covariant return return ((String)input).toUpperCase(); } } class Downcase extends Processor { String process(Object input) { return ((String)input).toLowerCase(); } } class Splitter extends Processor { String process(Object input) { // The split() argument divides a String into pieces: return Arrays.toString(((String)input).split(" ")); } } public class Apply { public static void process(Processor p, Object s) { print("Using Processor " + p.name()); print(p.process(s)); } public static String s = "Disagreement with beliefs is by definition incorrect"; public static void main(String[] args) { process(new Upcase(), s); process(new Downcase(), s); process(new Splitter(), s); } } /* Output: Using Processor Upcase DISAGREEMENT WITH BELIEFS IS BY DEFINITION INCORRECT Using Processor Downcase disagreement with beliefs is by definition incorrect Using Processor Splitter [Disagreement, with, beliefs, is, by, definition, incorrect] *///:~El método Apply.process() toma cualquier tipo de objeto Processor, lo aplica a un objeto Object e imprime los resultados. Un método que se comporta de manera diferente según el argumento que se le pase es lo que se denomina patrón de diseño basado en estrategias. El método contiene la parte fija del algoritmo, mientras que la estrategia, que es el objeto que pasamos, contiene la parte que varía, el código que hay que ejecutar. En este caso Processor es la estrategia, en main() vemos cómo se aplican las tres estrategias diferentes a la cadena de caracteres s.
Ahora descubrimos una serie de "filtros electrónicos" que pudieran encajar en nuestro método Apply.process():
//: interfaces/filters/Waveform.java package interfaces.filters; public class Waveform { private static long counter; private final long id = counter++; public String toString() { return "Waveform " + id; } } ///:~ //: interfaces/filters/Filter.java package interfaces.filters; public class Filter { public String name() { return getClass().getSimpleName(); } public Waveform process(Waveform input) { return input; } } ///:~ //: interfaces/filters/LowPass.java package interfaces.filters; public class LowPass extends Filter { double cutoff; public LowPass(double cutoff) { this.cutoff = cutoff; } public Waveform process(Waveform input) { return input; // Dummy processing } } ///:~ //: interfaces/filters/HighPass.java package interfaces.filters; public class HighPass extends Filter { double cutoff; public HighPass(double cutoff) { this.cutoff = cutoff; } public Waveform process(Waveform input) { return input; } } ///:~ //: interfaces/filters/BandPass.java package interfaces.filters; public class BandPass extends Filter { double lowCutoff, highCutoff; public BandPass(double lowCut, double highCut) { lowCutoff = lowCut; highCutoff = highCut; } public Waveform process(Waveform input) { return input; } } ///:~
Si nos fijamos, la clase Filter tiene los mismos elementos de interfaz que Processor, pero al no heredar de esta clase no podemos utilizar un objeto Filter con el método Apply.process(), a pesar de que funcionaría. El acoplamiento entre Apply.process() y Processor es más fuerte de lo necesario y esto impide que el código de Apply.process() pueda reutilizarse. Las entradas y salidas en el método process() de Filter son de tipo WaveForm.
Si la clase Processor fuera una interfaz, las restricciones se relajan los suficiente como para poder reutilizar un método Apply.process() que acepte dicha interfaz. Aquí tenemos las versiones modificadas de Processor y Apply:
//: interfaces/interfaceprocessor/Processor.java package interfaces.interfaceprocessor; public interface Processor { String name(); Object process(Object input); } ///:~ //: interfaces/interfaceprocessor/Apply.java package interfaces.interfaceprocessor; import static net.mindview.util.Print.*; public class Apply { public static void process(Processor p, Object s) { print("Using Processor " + p.name()); print(p.process(s)); } } ///:~La primera forma de reutilizar este código es escribir clases para que se adapten a la interfaz:
//: interfaces/interfaceprocessor/StringProcessor.java package interfaces.interfaceprocessor; import java.util.*; public abstract class StringProcessor implements Processor{ public String name() { return getClass().getSimpleName(); } public abstract String process(Object input); public static String s = "If she weighs the same as a duck, she's made of wood"; public static void main(String[] args) { Apply.process(new Upcase(), s); Apply.process(new Downcase(), s); Apply.process(new Splitter(), s); } } class Upcase extends StringProcessor { public String process(Object input) { // Covariant return return ((String)input).toUpperCase(); } } class Downcase extends StringProcessor { public String process(Object input) { return ((String)input).toLowerCase(); } } class Splitter extends StringProcessor { public String process(Object input) { return Arrays.toString(((String)input).split(" ")); } } /* Output: Using Processor Upcase IF SHE WEIGHS THE SAME AS A DUCK, SHE'S MADE OF WOOD Using Processor Downcase if she weighs the same as a duck, she's made of wood Using Processor Splitter [If, she, weighs, the, same, as, a, duck,, she's, made, of, wood] *///:~Sin embargo, en muchas ocasiones no podemos modificar las clases que queremos utilizar. En el caso de los filtros electrónicos, la correspondiente biblioteca la hemos descubierto, en lugar de desarrollarla. En estos casos podemos utilizar un patrón de diseño adaptador, escribiendo código para tomar la interfaz de la que disponemos y producir la que necesitamos:
//: interfaces/interfaceprocessor/FilterProcessor.java package interfaces.interfaceprocessor; import interfaces.filters.*; class FilterAdapter implements Processor { Filter filter; public FilterAdapter(Filter filter) { this.filter = filter; } public String name() { return filter.name(); } public Waveform process(Object input) { return filter.process((Waveform)input); } } public class FilterProcessor { public static void main(String[] args) { Waveform w = new Waveform(); Apply.process(new FilterAdapter(new LowPass(1.0)), w); Apply.process(new FilterAdapter(new HighPass(2.0)), w); Apply.process(new FilterAdapter(new BandPass(3.0, 4.0)), w); } } /* Output: Using Processor LowPass Waveform 0 Using Processor HighPass Waveform 0 Using Processor BandPass Waveform 0 *///:~En esta aplicación, el patrón de diseño de adaptación, el constructor FilterAdapter, toma la interfaz Filter y produce un objeto que tiene la interfaz Processor que necesitamos. Podemos ver también el mecanismo de delegación en la clase FilterAdapter.
Desacoplar la interfaz de la implementación permite aplicar las interfaces a múltiples implementaciones diferentes, así el código es más reutilizable.
Ejercicio 11. Crea una clase con un método que tome como argumento un objeto String y produzca un resultado en el que se intercambie cada pareja de caracteres contenida en el argumento. Adapta la clase para que funcione con interfaceprocessor.Apply.process().
"HERENCIA MÚLTIPLE" EN JAVA
Como una interfaz no está implementada, se pueden combinar varias interfaces. Esto es muy útil en algunas ocasiones, como cuando queremos implementar el concepto "una x es una a y una b y una c". En C++ se denomina herencia múltiple y se combinan varias interfaces y cada clase tiene una implementación. En Java, sólo una de las clases puede tener una implementación, así no aparecen los problemas de C++ al combinar varias interfaces:
Si realizamos la herencia de algo que no sea una interfaz, sólo podemos heredar de una clases, los restantes elementos deberán ser interfaces. Los nombres de las interfaces se colocan detrás de la palabra clave implements y se separan mediante comas. Podemos incluir las interfaces que queramos y realizar generalizaciones (upcast) a cada interfaz ya que cada interaz representa un tipo independiente. A continuación vemos una clase concreta que se combina con varias interfaces para producir una nueva clase:
//: interfaces/Adventure.java // Multiple interfaces. interface CanFight { void fight(); } interface CanSwim { void swim(); } interface CanFly { void fly(); } class ActionCharacter { public void fight() {} } class Hero extends ActionCharacter implements CanFight, CanSwim, CanFly { public void swim() {} public void fly() {} } public class Adventure { public static void t(CanFight x) { x.fight(); } public static void u(CanSwim x) { x.swim(); } public static void v(CanFly x) { x.fly(); } public static void w(ActionCharacter x) { x.fight(); } public static void main(String[] args) { Hero h = new Hero(); t(h); // Treat it as a CanFight u(h); // Treat it as a CanSwim v(h); // Treat it as a CanFly w(h); // Treat it as an ActionCharacter } } ///:~La clase Hero combina la clase ActionCharacter con las interfaces CanFight, CanSwim y CanFly. Como vemos se expresa primero la clase concreta ActionCharacter y las interfaces a continuación (si no el compilador dará error).
Si nos fijamos el método fight() es igual en la interfaz CanFight y en la clase ActionCharacter. En la clase Hero no se implementa fight(). Podemos ampliar una interfaz, pero lo que obtendremos será otra interfaz. Cuando queramos crear un objeto, todas las definiciones deben haber sido ya hechas.
La clase Hero no define el método fight() pero al estar definido en ActionCharacter, es posible crear objetos Hero.
En la clase Adventure hay cuatro métodos que toman argumentos de las distintas interfaces y de la clase concreta ActionCharacter. Al crear un objeto Hero, se le puede pasar cualquiera de estos métodos y se generalizará en cada caso a cada una de las interfaces. Este mecanismo funciona sin que el programador se preocupe de nada.
Una de las razones para usar interfaces es realizar generalizaciones a más de un tipo base, lo que proporciona gran flexibilidad. Una segunda razón es impedir que el programador de clientes cree un objeto de esta clase y para establecer que sólo se trata de una interfaz.
Ahora nos preguntamos, ¿debemos utilizar una interfaz o una clase abstracta? Si podemos crear nuestra clase base sin ninguna definición de método y sin ninguna variable miembro, son preferibles las interfaces. Si pensamos que una clase puede ser una clase base, debemos pensar en transformarla en una interfaz.
Ejercicio 12. En Adventure.java, añade una interfaz llamada CanClimb, siguiendo el patrón de las otras interfaces.
Ejercicio 13. Crea una interfaz y hereda de ella otras dos nuevas interfaces. Define, mediante herencia múltiple, una tercera interfaz a partir de otras dos. (Este ejemplo muestra cómo las interfaces evitan el denominado "problema del rambo", que se presenta el mecanismo de herencia múltiple de C++).
AMPLIACIÓN DE LA INTERFAZ MEDIANTE HERENCIA
Podemos añadir nuevas declaraciones de métodos a una interfaz mediante herencia y podemos combinar varias interfaces mediante herencia para crear una nueva interfaz. En los dos casos obtendremos una interfaz nueva:
//: interfaces/HorrorShow.java // Extending an interface with inheritance. interface Monster { void menace(); } interface DangerousMonster extends Monster { void destroy(); } class DragonZilla implements DangerousMonster { public void menace() {} public void destroy() {} } interface Lethal { void kill(); } interface Vampire extends DangerousMonster, Lethal { void drinkBlood(); } class VeryBadVampire implements Vampire { public void menace() {} public void destroy() {} public void kill() {} public void drinkBlood() {} } public class HorrorShow { static void u(Monster b) { b.menace(); } static void v(DangerousMonster d) { d.menace(); d.destroy(); } static void w(Lethal l) { l.kill(); } public static void main(String[] args) { DangerousMonster barney = new DragonZilla(); u(barney); v(barney); Vampire vlad = new VeryBadVampire(); u(vlad); v(vlad); w(vlad); } } ///:~DangerousMonster es una interfaz que es una extensión simple de Monster que produce una nueva interfaz. Ésta se implementa en DragonZilla.
La sintaxis empleada en Vampire sólo funciona cuando se heredan interfaces. Normalmente, sólo se puede utilizar extends con una única clase, pero extends puede hacer referencia a múltiples interfaces base a la hora de construir una nueva interfaz, vemos los nombres de interfaz separados por comas.
Ejercicio 14. Crea tres interfaces, cada una de ellas con dos métodos. Define mediante herencia una nueva interfaz que combine las tres, añadiendo un nuevo método. Crea una clase implementando la nueva interfaz y que también herede de una clase concreta. A continuación, escribe cuatro métodos, cada uno de los cuales tome una de las cuatro interfaces como argumento. En main(), crea un objeto de esa clase y pásalo a cada uno de los métodos.
Ejercicio 15. Modifica el ejercicio anterior creando una clase abstracta y haciendo que la clase derivada herede de ella.
Colisiones de nombres al combinar interfaces
En uno de los ejemplos anteriores las clases CanFight y ActionCharacter tienen métodos idénticos void fight(). Esto no resulta problemático, a menos que los métodos difieran en cuanto a signatura o tipo de retorno. Veamos un ejemplo:
//: interfaces/InterfaceCollision.java package interfaces; interface I1 { void f(); } interface I2 { int f(int i); } interface I3 { int f(); } class C { public int f() { return 1; } } class C2 implements I1, I2 { public void f() {} public int f(int i) { return 1; } // overloaded } class C3 extends C implements I2 { public int f(int i) { return 1; } // overloaded } class C4 extends C implements I3 { // Identical, no problem: public int f() { return 1; } } // Methods differ only by return type: //! class C5 extends C implements I1 {} //! interface I4 extends I1, I3 {} ///:~Los métodos sobrecargados no pueden diferir sólo en el tipo de retorno. Si nos fijamos vemos que los métodos sobrecargados si devuelven diferentes tipos de objeto, deben diferenciarse en el tipo de argumentos ya que si no obtendremos un error. Si quitamos los comentarios de las dos últimas líneas, tendríamos los siguientes mensajes de error:
InterfaceCollision.java:23:f() in C cannot implement f() in I1; attempting to use incompatible return type found : int required : void InterfaceCollision.java:24:Interfaces I3 and I1 are incompatible; bot define f(), but with different return typeUtilizar los mismos nombres de métodos en diferentes interfaces es algo confuso en cuanto a legibilidad del código, hay que evitar esto.
ADAPTACIÓN A UNA INTERFAZ
Una de las razones importantes para utilizar interfaces es disponer de varias implementaciones para una misma interfaz. En los casos más simples, lo llevamos a la práctica empleando un método que acepte una interfaz como argumento, así podemos implementar la interfaz a nuestro antojo y pasar nuestro objeto a dicho método.
Uno de los usos más comunes de las interfaces es el patrón de diseño basado en estrategia anteriormente visto: escribimos un método que realice ciertas operaciones y dicho método toma como argumento una interfaz. Lo que estamos diciendo es: "Puedes utilizar mi método con cualquier objeto que quieras siempre que se adapte a mi interfaz". De esta forma el método es más flexible, general y reutilizable.
Por ejemplo, el constructor para la clase Scanner de Java SE5 (hablaremos en el Tema 13 sobre ella), admite una interfaz Readable. Esta interfaz no es argumento de ningún otro método de la biblioteca de Java, se creo pensando en la clase Scanner, para que esta clase no tenga que restringir su argumento para que sea de una clase determinada. Así Scanner funciona con más tipos de datos. Si creamos una clase y queremos usarla con Scanner, basta con que la hagamos de tipo Readable, por ejemplo:
//: interfaces/RandomWords.java // Implementing an interface to conform to a method. import java.nio.*; import java.util.*; public class RandomWords implements Readable { private static Random rand = new Random(47); private static final char[] capitals = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray(); private static final char[] lowers = "abcdefghijklmnopqrstuvwxyz".toCharArray(); private static final char[] vowels = "aeiou".toCharArray(); private int count; public RandomWords(int count) { this.count = count; } public int read(CharBuffer cb) { if(count-- == 0) return -1; // Indicates end of input cb.append(capitals[rand.nextInt(capitals.length)]); for(int i = 0; i < 4; i++) { cb.append(vowels[rand.nextInt(vowels.length)]); cb.append(lowers[rand.nextInt(lowers.length)]); } cb.append(" "); return 10; // Number of characters appended } public static void main(String[] args) { Scanner s = new Scanner(new RandomWords(10)); while(s.hasNext()) System.out.println(s.next()); } } /* Output: Yazeruyac Fowenucor Goeazimom Raeuuacio Nuoadesiw Hageaikux Ruqicibui Numasetih Kuuuuozog Waqizeyoy *///:~La interfaz Readable sólo requiere que se implemente un método read(). Dentro de read(), añadimos la información al argumento CharBuffer (hay varias formas) o devolvemos -1 cuando ya no haya más datos de entrada.
Pero si tenemos una clase base que no implementa Readable, ¿cómo hacemos que funcione con Scanner? Veamos un ejemplo:
//: interfaces/RandomDoubles.java import java.util.*; public class RandomDoubles { private static Random rand = new Random(47); public double next() { return rand.nextDouble(); } public static void main(String[] args) { RandomDoubles rd = new RandomDoubles(); for(int i = 0; i < 7; i ++) System.out.print(rd.next() + " "); } } /* Output: 0.7271157860730044 0.5309454508634242 0.16020656493302599 0.18847866977771732 0.5166020801268457 0.2678662084200585 0.2613610344283964 *///:~Nuevamente podemos utilizar el patrón de diseño adaptador, en este caso la clase adaptada puede crearse heredando e implementando la interfaz Readable. Así, si utilizamos la herencia pseudo - múltiple proporcionada por la palabra clave interface, produciremos una nueva clase que será a la vez RandomDoubles y Readable:
//: interfaces/AdaptedRandomDoubles.java // Creating an adapter with inheritance. import java.nio.*; import java.util.*; public class AdaptedRandomDoubles extends RandomDoubles implements Readable { private int count; public AdaptedRandomDoubles(int count) { this.count = count; } public int read(CharBuffer cb) { if(count-- == 0) return -1; String result = Double.toString(next()) + " "; cb.append(result); return result.length(); } public static void main(String[] args) { Scanner s = new Scanner(new AdaptedRandomDoubles(7)); while(s.hasNextDouble()) System.out.print(s.nextDouble() + " "); } } /* Output: 0.7271157860730044 0.5309454508634242 0.16020656493302599 0.18847866977771732 0.5166020801268457 0.2678662084200585 0.2613610344283964 *///:~Como vemos se puede añadir una interfaz a cualquier clase existente, un método que tome como argumento una interfaz nos permitirá adaptar cualquier clase para que funcione con dicho método. Esta es la verdadera potencia de utilizar interfaces en lugar de clases.
Ejercicio 16. Crea una clase que genere una secuencia de caracteres. Adapta esta clase para que pueda utilizarse como entrada a un objeto Scanner.
CAMPOS EN LAS INTERFACES
Cualquier campo que incluyamos en una interfaz será automáticamente de tipo static y final, por tanto, una interfaz es una buena herramienta para crear grupos de valores constantes. Antes de JavaSE5, esta era la única forma de producir el mismo efecto que con la palabra clave enum en C o C++. Así, es posible encontrarse con código anterior a Java SE5 con el siguiente aspecto:
//: interfaces/Months.java // Using interfaces to create groups of constants. package interfaces; public interface Months { int JANUARY = 1, FEBRUARY = 2, MARCH = 3, APRIL = 4, MAY = 5, JUNE = 6, JULY = 7, AUGUST = 8, SEPTEMBER = 9, OCTOBER = 10, NOVEMBER = 11, DECEMBER = 12; } ///:~Si nos fijamos se utiliza el estilo Java: en caso de valores estáticos finales, todas las letras van en mayúscula, con guiones bajos para separar las palabras. Los campos de una interfaz son automáticamente públicos, por tanto, no es necesario especificarlo mediante el atributo public.
Con Java SE5 disponemos de la palabra clave enum, más potente y flexible, por lo que raramente tendrá sentido utilizar interfaces para definir constantes.
Ejercicio 17. Demuestra que los campos de una interfaz son implícitamente de tipo static y final.
Inicialización de campos en las interfaces
Los campos definidos en las interfaces no pueden ser valores "finales en blanco", pero pueden inicializarse con expresiones no constantes. Por ejemplo:
//: interfaces/RandVals.java // Initializing interface fields with // non-constant initializers. import java.util.*; public interface RandVals { Random RAND = new Random(47); int RANDOM_INT = RAND.nextInt(10); long RANDOM_LONG = RAND.nextLong() * 10; float RANDOM_FLOAT = RAND.nextLong() * 10; double RANDOM_DOUBLE = RAND.nextDouble() * 10; } ///:~Al ser los campos estáticos, se inicializan cuando se carga por primera vez la clase, es decir, cuando se accede por primera vez a cualquiera de sus campos. Veamos una prueba:
//: interfaces/TestRandVals.java import static net.mindview.util.Print.*; public class TestRandVals { public static void main(String[] args) { print(RandVals.RANDOM_INT); print(RandVals.RANDOM_LONG); print(RandVals.RANDOM_FLOAT); print(RandVals.RANDOM_DOUBLE); } } /* Output: 8 -32032247016559954 -8.5939291E18 5.779976127815049 *///:~Los campos no forman parte de la interfaz. Los valores se almacenan en el área de almacenamiento estático correspondiente a dicha interfaz.
ANIDAMIENTO DE INTERFACES
Las interfaces pueden anidarse dentro de clases y dentro de otras interfaces. Esto revela características interesantes como vemos en el siguiente ejemplo, fijaos en que hay interfaces y clases privadas.:
//: interfaces/nesting/NestingInterfaces.java package interfaces.nesting; class A { interface B { void f(); } public class BImp implements B { public void f() {} } private class BImp2 implements B { public void f() {} } public interface C { void f(); } class CImp implements C { public void f() {} } private class CImp2 implements C { public void f() {} } private interface D { void f(); } private class DImp implements D { public void f() {} } public class DImp2 implements D { public void f() {} } public D getD() { return new DImp2(); } private D dRef; public void receiveD(D d) { dRef = d; dRef.f(); } } interface E { interface G { void f(); } // Redundant "public": public interface H { void f(); } void g(); // Cannot be private within an interface: //! private interface I {} } public class NestingInterfaces { public class BImp implements A.B { public void f() {} } class CImp implements A.C { public void f() {} } // Cannot implement a private interface except // within that interface's defining class: //! class DImp implements A.D { //! public void f() {} //! } class EImp implements E { public void g() {} } class EGImp implements E.G { public void f() {} } class EImp2 implements E { public void g() {} class EG implements E.G { public void f() {} } } public static void main(String[] args) { A a = new A(); // Can't access A.D: //! A.D ad = a.getD(); // Doesn't return anything but A.D: //! A.DImp2 di2 = a.getD(); // Cannot access a member of the interface: //! a.getD().f(); // Only another A can do anything with getD(): A a2 = new A(); a2.receiveD(a.getD()); } } ///:~La sintaxis para anidar una interfaz dentro de una clase es amplia. Las interfaces anidadas pueden tener visibilidad pública o de paquete, pero también pueden ser privadas, como vemos en A.D (tenemos la misma sintaxis de cualificación para interfaces que para clases anidadas). ¿Pero para qué sirve una interfaz anidada privada? Se puede pensar que sólo puede implementarse como clase interna privada, como en DImp, sin embargo A.DImp2 muestra que se puede implementar como clase pública. Sin embargo, A.DImp2 sólo puede utilizarse como ella misma, no se nos permite mencionar el hecho de que implementa la interfaz privada D. Implementar interfaces privadas fuerza la definición de los métodos de dicha interfaz sin añadir ninguna información de tipos, es decir, sin permitir ninguna generalización.
El método getD() muestra que un método público puede devolver una referencia a una interfaz privada. Pero, ¿qué hacemos con el valor de retorno de este método? En main() vemos varios intentos de utilizar el valor de retorno, todos los cuales fallan. La única manera es entregar este valor de retorno a un objeto que tenga permiso para usarlo, en este caso es otro objeto A, a través del método receiveD().
En la interfaz E vemos que podemos anidar unas interfaces dentro de otras, por las reglas acerca de las interfaces, una interfaz anidada dentro de otra será automáticamente pública y no puede definirse como privada.
En NestingInterfaces vemos diferentes formas de implementar las interfaces anidadas. Vemos que cuando implementamos una interfaz no estamos obligados a implementar ninguna de las interfaces anidadas dentro de ella. Asimismo, las interfaces privadas no pueden implementarse fuera de sus clases definitorias.
Parece que estas características sólo se han añadido para garantizar la coherencia sintáctica, pero siempre se descubren ocasiones en las que pueden resultar útil.
INTERFACES Y FACTORÍAS
El objetivo principal de una interfaz es la existencia de múltiples implementaciones, una forma de producir objetos que encajen con una interfaz es el patrón de diseño de método factoría. En lugar de llamar a un constructor directamente, invocamos un método de creación en un objeto factoría que produce una implementación de la interfaz; así nuestro código estará aislado de la implementación de la interfaz, siendo posible intercambiar de forma transparente una implementación por otra. Veamos un ejemplo de la estructura del método factoría:
//: interfaces/Factories.java import static net.mindview.util.Print.*; interface Service { void method1(); void method2(); } interface ServiceFactory { Service getService(); // Método factoría } class Implementation1 implements Service { Implementation1() {} // Package access public void method1() {print("Implementation1 method1");} public void method2() {print("Implementation1 method2");} } class Implementation1Factory implements ServiceFactory { public Service getService() { return new Implementation1(); } } class Implementation2 implements Service { Implementation2() {} // Package access public void method1() {print("Implementation2 method1");} public void method2() {print("Implementation2 method2");} } class Implementation2Factory implements ServiceFactory { public Service getService() { return new Implementation2(); } } public class Factories { public static void serviceConsumer(ServiceFactory fact) { Service s = fact.getService(); s.method1(); s.method2(); } public static void main(String[] args) { serviceConsumer(new Implementation1Factory()); // Implementations are completely interchangeable: serviceConsumer(new Implementation2Factory()); } } /* Output: Implementation1 method1 Implementation1 method2 Implementation2 method1 Implementation2 method2 *///:~Sin el método factoría, nuestro código tendría que especificar en algún lugar el tipo exacto de objeto Service que se estuviera creando, para así invocar el constructor apropiado.
¿Pero para qué sirve añadir este nivel adicional de indirección? Una razón es crear un marco de trabajo para el desarrollo. Si suponemos que estamos creando un sistema para juegos que permita, por ejemplo, jugar al ajedrez y a las damas en el mismo tablero:
//: interfaces/Games.java // A Game framework using Factory Methods. import static net.mindview.util.Print.*; interface Game { boolean move(); } interface GameFactory { Game getGame(); } // Método factoría class Checkers implements Game { private int moves = 0; private static final int MOVES = 3; public boolean move() { print("Checkers move " + moves); return ++moves != MOVES; } } class CheckersFactory implements GameFactory { public Game getGame() { return new Checkers(); } } class Chess implements Game { private int moves = 0; private static final int MOVES = 4; public boolean move() { print("Chess move " + moves); return ++moves != MOVES; } } class ChessFactory implements GameFactory { public Game getGame() { return new Chess(); } } public class Games { public static void playGame(GameFactory factory) { Game s = factory.getGame(); while(s.move()); } public static void main(String[] args) { playGame(new CheckersFactory()); playGame(new ChessFactory()); } } /* Output: Checkers move 0 Checkers move 1 Checkers move 2 Chess move 0 Chess move 1 Chess move 2 Chess move 3 *///:~Si la clase Games representa un fragmento complejo de código, esta técnica permite reutilizar dicho código con diferentes tipos de juegos. En el siguiente tema veremos una forma más elegante de implementar las factorías usando las clases internas anónimas.
Ejercicio 18. Crea una interfaz Cycle, con implementaciones Unicycle, Bicycle y Tricycle. Crea factorías para cada tipo de Cycle y el código necesario que utilicen estas factorías.
Ejercicio 19. Crea un marco de trabajo utilizando métodos factoría que permita simular las operaciones de lanzar una moneda y lanzar un dado.
Se puede llegar a pensar que las interfaces son tan útiles que son preferibles a las clases concretas. Casi siempre que creemos una clase podemos crear en su lugar una interfaz y una factoría.
Mucha gente cae en la tentación de crear interfaces y factorías siempre que sea posible, pensando que pueden ser útiles en el futuro para una implementación diferente.
Las interfaces debemos utilizarlas cuando sean necesarias para optimizar el código, ya que si se incluyen en todas partes, hace que aumente la complejidad.
Una directriz apropiada señala que las clases resultan preferibles a las interfaces. Debemos comenzar con clases y si está claro que las interfaces son necesarias, rediseñaremos el código. Las interfaces son una muy buena herramienta pero debemos utilizarlas en su justa medida.
Que tal Arween17, por desgracia no leí el texto completo aunque se ve muy interesante, debido a que venía por una duda en particular que es mejor utilizar una interfaz o una clase abstracta en un caso en paticular, en tu articulo mencionas que una directriz apropiada señala que las clases resultan preferibles a las interfaces, sin embargo cuando se ocupan patrones de diseño lo más común es siempre preferir las interfaces sobre las clases, y preferir la composición sobre la herencia, más que nada debido a que en Java no existe herencia múltiple es decir si un programador necesita agregar una clase a tu factoría, por ejemplo necesitaria implementar una interfaz, haciendo esto el programador podría hacer lo siguiente.
ResponderEliminarclass Expresso extends Coffee implements Beverage {}
Nota: Beverage sería la interfaz a implementar para insertar en la factoria.
Sin embargo si lo haces como una clase abstracta entonces no podría heredar de dos clases por lo cual tendría que encerrar todo el comportamiento en la misma clase, obligarías al programador a heredar de tu clase y a lidiar con sus problemas de otras formas.
En diseño de software algo deseable es aplicar la inyección de dependencias es decir depender de abstracciones y no de clases concretas, disculpa el largo discurso no quiero sonar pedante, espero que tomes esto como una crítica constructiva y te de algo de curiosidad investigar estos temas.
Saludos y seguire visitando tu blog.
Hola kokubunji:
EliminarBuen comentario, tienes razón, me parece muy interesante lo que comentas y no tienes que pedir disculpas. Cualquier comentario constructivo es bienvenido.
Saludos.
Hola. gran trabajo en tu pagina. continuaste con los siguientes temas?
ResponderEliminarUn saludo.
Muchas gracias. La verdad es que tengo algún tema a medias que no he terminado, quiero seguir publicando, lo malo es la falta de tiempo pero mi intención es acabarlo.
EliminarUn saludo y gracias.
Muy buen aporte, creo que cubres temas muy importantes como lo es el desacoplamiento, lo de inyección de dependencias talves quedaría mejor en otro tema como por ejemplo si alguna vez llegas a cubrir el framework spring
ResponderEliminarSaludos