La programación orientada a objetos en Java va mucho más allá de las clases, los objetos y la herencia. Java presenta una serie de capacidades que enriquecen el modelo de objetos que se puede representar en un programa Java. En este capítulo entraremos en ellos.
Vamos a ver cómo programar conceptos avanzados de la herencia, polimorfismo y composición, como conceptos que se pueden programar en Java.
B. Operaciones avanzadas en la herencia a.) IntroducciónEn el capítulo anterior ya se han estudiado los fundamentos de la herencia en Java. Sin embargo, el lenguaje tiene muchas más posibilidades en este aspecto, como estudiaremos a continuación.
Conviene recordar que estamos utilizando el código de la clase MiPunto, cuyo código se puede encontrar en el apartado "II.5. Clases y Objetos" de este tutorial.
b.) Los elementos globales: staticA veces se desea crear un método o una variable que se utiliza fuera del contexto de cualquier instancia, es decir, de una manera global a un programa. Todo lo que se tiene que hacer es declarar estos elementos como static.
Esta es la manera que tiene Java de implementar funciones y variables globales.
Por ejemplo:
static int a = 3; static void metodoGlobal() { // implementación del método }
No se puede hacer referencia a this o a super dentro de una método static.
Mediante atributos estáticos, todas las instancias de una clase además del espacio propio para variables de instancia, comparten un espacio común. Esto es útil para modelizar casos de la vida real.
Otro aspecto en el que es útil static es en la creación de métodos a los que se puede llamar directamente diciendo el nombre de la clase en la que están declarados. Se puede llamar a cualquier método static, o referirse a cualquier variable static, utilizando el operador punto con el nombre de la clase, sin necesidad de crear un objeto de ese tipo:
class ClaseStatic { int atribNoStatic = 42; static int atribStatic = 99; static void metodoStatic() { System.out.println("Met. static = " + atribStatic); } static void metodoNoStatic() { System.out.println("Met. no static = " + atribNoStatic); } }
El siguiente código es capaz de llamar a metodoStatic y atribStatic nombrando directamente la clase (sin objeto, sin new), por haber sido declarados static.
System.out.println("At. static = " + ClaseStatic.atribStatic); ClaseStatic.metodoStatic(); // Sin instancia new ClaseStatic().metodoNoStatic(); // Hace falta instanciaSi ejecutamos este programa obtendríamos:
At. static = 99 Met. static = 99 Met. no static = 42Debe tenerse en cuenta que en un método estático tan sólo puede hacerse refernecia a variables estáticas.
c.) Las clases y métodos abstractos: abstractHay situaciones en las que se necesita definir una clase que represente un concepto abstracto, y por lo tanto no se pueda proporcionar una implementación completa de algunos de sus métodos.
Se puede declarar que ciertos métodos han de ser sobrescritos en las subclases, utilizando el modificador de tipo abstract. A estos métodos también se les llama responsabilidad de subclase. Cualquier subclase de una clase abstract debe implementar todos los métodos abstract de la superclase o bien ser declarada también como abstract.
Cualquier clase que contenga métodos declarados como abstract también se tiene que declarar como abstract, y no se podrán crear instancias de dicha clase (operador new).
Por último se pueden declarar constructores abstract o métodos abstract static.
Veamos un ejemplo de clases abstractas:
abstract class claseA { abstract void metodoAbstracto(); void metodoConcreto() { System.out.println("En el metodo concreto de claseA"); } } class claseB extends claseA { void metodoAbstracto(){ System.out.println("En el metodo abstracto de claseB"); } }
La clase abstracta claseA ha implementado el método concreto metodoConcreto(), pero el método metodoAbstracto() era abstracto y por eso ha tenido que ser redefinido en la clase hija claseB.
claseA referenciaA = new claseB(); referenciaA.metodoAbstracto(); referenciaA.metodoConcreto();La salida de la ejecución del programa es:
En el metodo abstracto de claseB En el metodo concreto de claseA C. El polimorfismo a.) Selección dinámica de métodoLas dos clases implementadas a continuación tienen una relación subclase/superclase simple con un único método que se sobrescribe en la subclase:
class claseAA { void metodoDinamico() { System.out.println("En el metodo dinamico de claseAA"); } } class claseBB extends claseAA { void metodoDinamico() { System.out.println("En el metodo dinamico de claseBB"); } }
Por lo tanto si ejecutamos:
claseAA referenciaAA = new claseBB(); referenciaAA.metodoDinamico();La salida de este programa es:
En el metodo dinamico de claseBBSe declara la variable de tipo claseA, y después se almacena una referencia a una instancia de la clase claseB en ella. Al llamar al método metodoDinamico() de claseA, el compilador de Java verifica que claseA tiene un método llamado metodoDinamico(), pero el intérprete de Java observa que la referencia es realmente una instancia de claseB, por lo que llama al método metodoDinamico() de claseB en vez de al de claseA.
Esta forma de polimorfismo dinámico en tiempo de ejecución es uno de los mecanismos más poderosos que ofrece el diseño orientado a objetos para soportar la reutilización del código y la robustez.
b.) Sobrescritura de un métodoDurante una jerarquía de herencia puede interesar volver a escribir el cuerpo de un método, para realizar una funcionalidad de diferente manera dependiendo del nivel de abstracción en que nos encontremos. A esta modificación de funcionalidad se le llama sobrescritura de un método.
Por ejemplo, en una herencia entre una clase SerVivo y una clase hija Persona; si la clase SerVivo tuviese un método alimentarse(), debería volver a escribirse en el nivel de Persona, puesto que una persona no se alimenta ni como un Animal, ni como una Planta...
La mejor manera de observar la diferencia entre sobrescritura y sobrecarga es mediante un ejemplo. A continuación se puede observar la implementación de la sobrecarga de la distancia en 3D y la sobrescritura de la distancia en 2D.
class MiPunto3D extends MiPunto { int x,y,z; double distancia(int pX, int pY) { // Sobrescritura int retorno=0; retorno += ((x/z)-pX)*((x/z)-pX); retorno += ((y/z)-pY)*((y/z)-pY); return Math.sqrt( retorno ); } }
Se inician los objetos mediante las sentencias:
MiPunto p3 = new MiPunto(1,1); MiPunto p4 = new MiPunto3D(2,2);Y llamando a los métodos de la siguiente forma:
p3.distancia(3,3); //Método MiPunto.distancia(pX,pY) p4.distancia(4,4); //Método MiPunto3D.distancia(pX,pY)Los métodos se seleccionan en función del tipo de la instancia en tiempo de ejecución, no a la clase en la cual se está ejecutando el método actual. A esto se le llama selección dinámica de método.
c.) Sobrecarga de métodoEs posible que necesitemos crear más de un método con el mismo nombre, pero con listas de parámetros distintas. A esto se le llama sobrecarga del método. La sobrecarga de método se utiliza para proporcionar a Java un comportamiento polimórfico.
Un ejemplo de uso de la sobrecarga es por ejemplo, el crear constructores alternativos en función de las coordenadas, tal y como se hacía en la clase MiPunto:
MiPunto( ) { //Constructor por defecto inicia( -1, -1 ); } MiPunto( int paramX, int paramY ) { // Parametrizado this.x = paramX; y = paramY; }
Se llama a los constructores basándose en el número y tipo de parámetros que se les pase. Al número de parámetros con tipo de una secuencia específica se le llama signatura de tipo. Java utiliza estas signaturas de tipo para decidir a qué método llamar. Para distinguir entre dos métodos, no se consideran los nombres de los parámetros formales sino sus tipos:
MiPunto p1 = new MiPunto(); // Constructor por defecto MiPunto p2 = new MiPunto( 5, 6 ); // Constructor parametrizado d.) Limitación de la sobreescritura: finalTodos los métodos y las variables de instancia se pueden sobrescribir por defecto. Si se desea declarar que ya no se quiere permitir que las subclases sobrescriban las variables o métodos, éstos se pueden declarar como final. Esto se utiliza a menudo para crear el equivalente de una constante de C++.
Es un convenio de codificación habitual elegir identificadores en mayúsculas para las variables que sean final, por ejemplo:
final int NUEVO_ARCHIVO = 1; d. las referencias polimórficas: this y super a.) Acceso a la propia clase: thisAunque ya se explicó en el apartado "II.5. Clases y objetos" de este tutorial el uso de la referencia this como modificador de ámbito, también se la puede nombrar como ejemplo de polimorfismo
Además de hacer continua referencia a la clase en la que se invoque, también vale para sustituir a sus constructores, utilizándola como método:
this(); // Constructor por defecto this( int paramX, int paramY ); // Constructor parametrizado b.) Acceso a la superclase: superYa hemos visto el funcionamiento de la referencia this como referencia de un objeto hacia sí mismo. En Java existe otra referencia llamada super, que se refiere directamente a la superclase.
La referencia super usa para acceder a métodos o atributos de la superclase.
Podíamos haber implementado el constructor de la clase MiPunto3D (hija de MiPunto) de la siguiente forma:
MiPunto3D( int x, int y, int z ) { super( x, y ); // Aquí se llama al constructor de MiPunto this.z = super.metodoSuma( x, y ); // Método de la superclase }
Con una sentencia super.metodoSuma(x, y) se llamaría al método metodoSuma() de la superclase de la instancia this. Por el contrario con super() llamamos al constructor de la superclase.
E. la composiciónOtro tipo de relación muy habitual en los diseños de los programas es la composición. Los objetos suelen estar compuestos de conjuntos de objetos más pequeños; un coche es un conjunto de motor y carrocería, un motor es un conjunto de piezas, y así sucesivamente. Este concepto es lo que se conoce como composición.
La forma de implementar una relación de composición en Java es incluyendo una referencia a objeto de la clase componedora en la clase compuesta.
Por ejemplo, una clase AreaRectangular, quedaría definida por dos objetos de la clase MiPunto, que representasen dos puntas contrarias de un rectángulo:
class AreaRectangular { MiPunto extremo1; //extremo inferior izquierdo MiPunto extremo2; //extremo superior derecho AreaRectangular() { extremo1=new MiPunto(); extremo2=new MiPunto(); } boolean estaEnElArea( MiPunto p ){ if ( ( p.x>=extremo1.x && p.x<=extremo2.x ) && ( p.y>=extremo1.y && p.y<=extremo2.y ) ) return true; else return false; } }
Puede observarse que las referencias a objeto (extremo1 y extremo2) son iniciadas, instanciando un objeto para cada una en el constructor. Así esta clase mediante dos puntos, referenciados por extremo1 y extremo2, establece unos límites de su área, que serán utilizados para comprobar si un punto está en su área en el método estaEnElArea().