Métodos y Clases en Java: Parte III
Concluimos en esta tercera entrega todo lo que he considerado de interés para conocer cómo es la POO
en Java.
Las clases internas
Una
clase interna es una clase definida dentro de otra clase, conocida como
clase contenedora, en
alguna variante de la siguiente forma general:
class ClaseContenedora {
...
class ClaseInterna {
...
}
...
}
El alcance de una clase interna está limitado por el alcance de la clase que la contiene. Así, si una clase
B está englobada dentro de una clase A, entonces B es vista por A, pero no fuera de esta última. Una clase interna
tiene acceso a los miembros (variables o métodos), incluyendo los privados, de la clase que la contiene. No obstante,
la clase contenedora no tiene acceso a los miembros de la clase interna.
//Ejemplo de acceso a una variable miembro de la clase contenedora.
public class Contenedora {
private int a = 2;
public Contenedora() {
System.out.println("a dentro de Contenedora vale: " + a);
Inner inner = new Inner();
}
class Inner {
public Inner() {
a = a*a;
System.out.println("a en inner class: " + a);
}
}
public static void main(String args[]) {
Contenedora cont = new Contenedora();
}
}
Además de su utilidad en sí, las clases internas se utilizan mucho en el nuevo modelo de eventos introducido en
la versión de Java 1.1 y que veremos en el siguiente capítulo.
Hay que señalar que la máquina virtual de java no sabe nada de la existencia de clases internas.
Por ello, el compilador convierte estas clases en clases globales, contenidas en ficheros .class cuyo nombre se del
estilo ClaseContenedora$ClaseInterna.class. Esta conversión inserta variables ocultas, métodos y
argumentos en los constructores.
Hay cuatro tipos de clases internas:
- Clases internas static
- Clases internas miembro
- Clases internas locales
- Clases anónomas
Clases internas static
Se conocen también con el nombre de
clases anidadas (nested classes). Las clases internas static
sólo pueden ser creadas dentro de otra clase al máximo nivel, es decir, directamente en el bloque
de definición de la clase contenedora y no en un bloque más interno. Este tipo de clases internas se definen
utilizando la palabra static.
Debido a su caracter "estatico", estas clases no pueden acceder a los miembro de su clase contenedora
directamente sino a través de un objeto. En cierta forma, las clases internas static se comportan como
clases normales en un package. Para utilizar su nombre desde fuera de la clase contenedora hay que precederlo
por el nombre de la clase contenedora y el operador (.). Este tipo de relación entre clases se puede utilizar para
agrupar varias clases dentro de una clase más general.
Las clases internas static pueden ver y utlizar los miembros static de la clase contenedora. No
se necesitan objetos de la clase contenedora para crear objetos de la clase interna static. Los métodos
de la clase interna static no pueden acceder directamente a los objetos de la clase contenedora, caso de
que los haya: deben disponer de una referencia a dichos objetos, como cualquier otra clase.
El hecho de que las clases contenedoras no puedan acceder directamente a sus clases internas static hace que
estas últimas se empleen rara vez.
Clases internas miembro (no static)
Llamadas también simplemente
clases internas, son clases definidas al máximo nivel de la clase contenedora,
sin la palabra static. Este tipo de clase interna no puede tener variables miembro static. Tienen una nueva sintáxis para
las palabras reservadas this, new y super, que se verá un poco más adelante.
La característica principal de estas clases internas es que cada objeto de la clase interna existe
siempre dentro de uno y sólo un objeto de la clase contenedora. Un objeto de la clase contenedora puede
estar relacionado con uno o más objetos de la clase interna. Tener esto presente es muy importante para
entender las características que se explican a continuación:
- Los métodos de la clase interna ven directamente las variables miembro del objeto de la clase contenedora,
sin necesidad de cualificarlos, cosa que no ocurre al revés. Esto ocurre porque existe una relación "uno a varios" que
existe entre los objetos de la clase contenedora y los de la clase interna.
- Otras clases diferentes de las clases contenedora e interna pueden utilizar directamente los objetos
de la clase interna, sin cualificarlos con el objeto o el nombre de la clase contenedora. De hecho, se puede seguir
accediendo a los objetos de la clase interna aunque se pierda la referencia al objeto de la clase contenedora
con el que están asociados.
- Una clase interna miembro puede contener otra clase interna miembr, hasta el nivel que se desee (
aunque no se considera buena técnica de programación utilizar muchos niveles).
- En la clase interna, la palabra this se refiere al objeto de la propia clase interna. Para
acceder al objeto de la clase contenedora se utiliza ClaseContenedora.this.
- Para crear un nuevo objeto de la clase intertna se puede utilizar new, precedido por la
referencia al objeto de la clase contenedora que contendrá el nuevo objeto: unObjeto.new().
El tipo del objeto es el nombre de la clase contenedora seguido del nombre de la clase interna, como
por ejemplo:
Contenedora.Interna objetoInterna = objetoContenedora.new Interna(...);
//Imaginemos ahora que B es una clase interna de A
//y que C es una clase interna de B. La creación de
//objetos de las tres clases se puede hacer del
//siguiente modo:
A a = new A(); //se crea un objeto de la clase A
A.B b = a.new B(); //b es un objeto de la clase interna B
//dentro de a
A.B.C c = b.new C(); //c es un objeto de la clase interna C
//dentro de b
- Nunca se puede crear un objeto de la clase interna sin una referencia a un objeto de la clase
contenedora. Los constructores de la clase interna tienen como argumento oculto una referencia al objeto
de la clase contenedora.
- El nuevo significado de la palabra super es un poco complicado: Si una clase deriva de una
clase interna, su constructor no puede llamar a super() directamente. Ello hace que el compilador no
pueda crear un constructor por defecto. Al constructor hay que pasarle una referencia a la clase contenedora
de la clase interna super-clase, y con esa referencia llamar a ref.super().
Las clases internas pueden derivar de otras clases diferentes de la clase contenedora. En este caso,
conviene tener en cuenta lo siguiente:
- Las clases internas constituyen como una segunda jerarquía de clases en Java: por una parte están
en la clase contenedora y ven sus variables; por otra parte pueden derivar de otra clase que no tenga
nada que ver con la clase contenedora. Es muy importante evitar conflictos con los nombres. En caso de
conflicto entre un nombre heredado y un nombre en la clase contenedora, el nombre heredado debe tener
prioridad.
- En caso de conflicto de nombres, Java obliga a utilizar la referencia this con un nuevo significado
: para referirse a la variable o método miembro heredado se utiliza this.name, mientras que se utiliza
NombreClaseCont.this.name para el miembro de la clase contenedora.
- Si una clase contenedora deriva de una super-clase que tiene una clase interna, la clase interna
de la subclase puede a su vez derivar de la clase interna de la super-clase y redefinir todos los
métodos que necesite.
Todo esto expuesto aquí son todas las posibilidades que puede tener el uso de una clase interna, pero,
como en todo, haz las cosas sencillas y no te compliques la vida ;). Esto, como culturilla general ;).
Como restricciones de estas clases internas miembro tenemos:
No pueden tener el mismo nombre que la clase contenedora o package.
Tampoco pueden tener miembros static: variables, métodos o clases.
Ejemplo:
//clase contenedora
class A {
int i=1; //variable miembro
public A(int i) {this.i=i;} //constructor
//los métodos de la clase contenedora necesitan una
//referencia a los objetos de la clase interna
public void printA(B objetoB) {
System.out.println("i="+i+" objetoB.j="+objetoB.j);//sí lo acepta
}
//la clase interna puede tener cualquier visibilidad. Con private
//da error porque main() no puede acceder a la clase interna
protected class B {
int j=2;
public B(int j) {this.j=j;} //constructor
public void printB() {
System.out.println("i=" +i+ " j" + j); //sí sabe que es j
}
}//fin clase B
}//fin clase contenedora A
class ClaseInterna {
public static void main(String args[]) {
A a1 = new A(11);
A a2 = new A(12);
println("a1.i=" +a1.i + " a2.i=" + a2.i);
//forma de crear objetos de la clase interna
//asociados a un objeto de la clase contenedora
A.B b1 = a1.new B(-10), b2 = a1.new B(-20);
//referencia directa a los objetos b1 y b2 (sin cualificar).
println("b1.j=" +b1.j + " b2.j=" + b2.j);
//los métodos de la clase interna pueden acceder directamente
//a las variables miembro del objeto de la clase contenedora
b1.printB(); //escribe: i=11 j=-10
b2.printB(); //escribe: i=11 j=-20
//los métodos de la clase contenedora deben recibir referencias
//a los propios objetos de la clase interna, para que puedan
//identificarlos.
a1.printA(b1); a1.printA(b2);
A a3 = new A(13);
A.B b3 = a3.new B(-30);
println("b3.j=" + b3.j);
a3 = null;
b3.printB(); //escribe: i=13 j=-30
a3 = new A(14); //se crea un nuevo objeto asociado a la
//referencia a3
//b3 sigue asociado al anterior objeto de la clase contenedora
b3.printB(); //escribe: i=13 j=-30
}//fin main()
public static void println(String str) {System.out.println(str);}
}//fin clase ClaseInterna
Clases internas locales
Estas clases no se declaran dentro de otra clase al máximo nivel, sino dentro de un bloque de código,
normalmente en un método, aunque también se pueden crear en un inicializador static o de objeto.
Las principales características de las clases locales son las siguientes:
- Como las variables locales, las clases locales sólo son visibles y utilizables en el bloque
de código en el que están definidas. Los objetos de la clase local deben ser creados en el mismo
bloque en que dicha clase ha sido definida. De esta forma se puede acercar la definición al uso de
la clase.
- Las clases internas locales tienen acceso a todas las variables miembro y métodos de la clase
contenedora. Pueden ver también los miembros heredados, tanto por la clase interna local como por la clase
contenedora.
- Las clases locales pueden utilizar las variables locales y argumentos de métodos visibles en
ese bloque de código, pero sólo si son final (en realidad la clase local trabaja con sus copias
de las variables locales y por eso se exige que sean final y no puedan cambiar).
- Un objeto de una clase interna local sólo puede existir en relación con un objeto de la clase
contenedora, que debe existir previamente.
- La palabra this se puede utilizar en la misma forma que en las clases internas miembro,
pero no las palabras new y super.
Clases anónimas
Las clases anónimas son muy similares a las clases internas locales, pero sin nombre. En las clases
internas locales primero se define la clase y luego se crean uno o más objetos. En las clases anónimas
se unen estos dos pasos: Como la clase no tiene nombre sólo se puede crear un único objeto, ya que
las clases anónimas no pueden definir constructores. Estas clases se utilizan con mucha frecuencia
para definir clases y objetos que gestionen los eventos de los distintos componentes de la interface de
usuario como veremos en el siguiente artículo.
Existen 3 maneras de definir una clase anónima:
- Requieren una extensión de la palabra clave new. Se definen en una expresión de Java, incluida
en una asignación o en la llamada a un método. Se incluye la palabra new seguida de la definición de
la clase anónima, entre llaves {...}.
- Otra forma de definirlas es mediante la palabra new seguida del nombre de la clase de la que
hereda (sin extends) y la definición de la clase anónima entre llaves {...}. El nombre de la super-clase
puede ir seguido de argumentos para su constructor (entre paréntesis, que con mucha frecuencia estarán
vacíos pues se utilizará un constructor por defecto).
- Una tercera forma de definirlas es con la palabra new seguida del nombre de la interface que implementa
(sin implements) y la definición de la clase anónima entre llaves {...}. En este caso la clase anónima
deriva de Object. El nombre de la interface va seguido por paréntesis vacíos, pues el constructor
de Object no tiene argumentos.
Para las clases anónimas compiladas el compilador produce ficheros con un nombre del tipo
ClaseContenedora$1.class, asignando un número correlativo a cada una de las clases anónimas.
En este tipo de clase se debe respetar los aspectos tipográficos, pues al no tener nombre dichas
clases, suelen resultar dificiles de leer e interpretar. Mi consejo es que cuando las uses (aunque ya
veremos en el siguiente artículo cómo hacerlo) sigas lo siguiente:
- Poner la palabra new en la misma línea que el resto de la expresión.
- Las llaves se abren en la misma línea que new, después del cierre del paréntesis de los argumentos
del constructor.
- El cuerpo de la clase anónima se debe sangrar respecto a las líneas anteriores de código para que
resulte claramente distinguible.
- El cierre de las llaves va seguido por el resto de la expresión en la que se ha definido la clase anónima.
//Ejemplo de definición de clase anónima
objeto.addActionListener (new ActionListener() {
public void actionPerformed (ActionEvent ae) {
...
}
});
La herencia en Java
Esta es otra de las piedras angulares de la POO ya que permite la creación de una clasificación jerárquica.
Utilizando la herencia, puedes crear una clase general que defina los rasgos comunes de un conjunto de
términos relacionados. Esta clase puede entonces ser heredada por otras clases más especificas, añadiendo
cada una solo aquellas cosas que sean particulares a ellas pero conservando las propiedades generales
determinadas por la clase general de la que se hereda. En Java, la clase de la que se hereda se denomina
superclase y la clase heredera
subclase y esta herencia se transmite a través de la palabra
reservada
extends. Por tanto, una subclase es una versión especializada de su superclase. Esta hereda
todas las variables y métodos definidos por su superclase. Luego, añadirá sus propias miembros y/o podrá redefinir
las variables y métodos heredados.
Todas las clases de Java creadas por el programador tienen una superclase. Cuando no se indica explícitamente
una supercase con la palabra extends, la clase deriva de java.lang.Object, que es la clase raíz de toda
la jerarquía de clases de Java. Como consecuencia, todas las clases tienen algunos métodos que han heredado
de Object.
NOTA: No debemos confundir la sobrecarga de los métodos con la redefinición de
éstos. En el primer caso, como vimos en artículo anterior, sobrecargar un método consiste en definir
dos o más métodos dentro de la misma clase con el mismo nombre mientras que redefinir (override)
un método es darle una nueva definición. En este caso el método debe tener exactamente los mismos argumentos
en tipo y número que el método redefinido.
//Ejemplo básico de herencia.
//Nota: imprimir() es uan abreviación de System.out.println()
//Superclase
class Madre {
int i, j;
private int k;
void showij() {
imprimir("i y j: " + i + " " + j);
}
}
//Heredera
class Hija extends Madre {
int x;
void showx() {imprimir("x: " + x);}
void sum() {imprimir("i+j+x": " + (i+j+x))};
k = i+j+x;//quitar esta línea si quieres compilar...por qué?
//pues la respuesta es que a lo único que las clases
//herederas no pueden acceder de su superclase es a
//los miembros privados de ésta.
}
class EjemploSimple {
public static void main (string args[]) {
Madre superOb = new Madre();
Hija subOb = new Hija();
//La superclase se puede emplear por si misma
superOb.i = 10;
superOb.j = 20;
superOb.showij(); //i y j: 10 20
//La subclase tiene acceso a todos los miembros públicos de
//su superclase
subOb.i= 7;//redefinición de la variable miembro de la superclase
subOb.j= 8;
subOb.x= 9;
subOb.showij(); // i y j: 7 8 || hereda de Madre el método showij()
subOb.showx(); //x: 9
subOb.sum(); //i+j+x: 24
}
}
NOTA: No debemos olvidar que Java no permite herencia múltiple, es decir, que una clase hija
herede de varias madres pero sí permite una jerarquía de herencias multinivel como ya se apuntó en el
capítulo 2.
Misión de la palabra reservada super en la herencia
Hay veces que tu quieres crear una superclase que mantenga el detalle de su implementación para ella
sola, es decir, quieres que sus datos miembros sean privados. En este caso, no habría manera de que una
subclase accediera o inicializara directamente estas variables por su cuenta. Puesto que la encapsulación
es un atributo primordial de la POO, Java ha tenido en cuenta una manera de proporcionar una solución a este
problema.
Cuando quiera que sea que una clase necesite referirse a su inmediata superclase, lo podrá hacer
mediante el empleo de super.
super, en este contexto, se puede emplear de dos formas:
- Llamar al constructor de su superclase
- Acceder a un miembro de la superclase que haya sido escondido por un miembro de una subclase
- Llamada al constructor de su superclase
- Ya se comentó que un constructor de una clase puede llamar por medio de la palabra this
a otro constructor previamente definido en la misma clase. En este contexto, la palabra this sólo puede
aparecer en la primera sentencia de un constructor.
De forma análoga, el constructor de una clase derivada puede llamar al constructor de su superclase
por medio de la palabra super(). Entre paréntesis van los argumentos apropiados para uno de los constructores
de la superclase. De esta forma, un constructor solo tiene que inicializar directamente las variables
no heredadas.
La llamada al constructor de la superclase debe ser la primera sentencia del constructor, excepto si
se llama a otro constructor de la misma clase con this(). Si el programador no la incluye, Java incluye automáticamentre una llamada al constructor
por defecto de la superclase, super(). Esta llamada en cadena a los constructores de las superclases
llega hasta el origen de la jerarquía de clases, esto es, al constructor de Object.
- Acceso a los miembros de su superclase
- En este caso, super() actua algo así como this excepto que siempre se refiere a la superclase
de la subclase en la que se usa. En este caso, su emplea se hustifica para solucionar el problema
de que la clase derivada "pise" un miembro (variable o método) de su superclase por llamarlo de la misma manera.
Veámos un ejemplo:
class Madre {
int i;
}
class Hija extends Madre {
int i; //esta variable esconde la de su superclase
Hija(int a, int b) {
super.i = a; //es la variable de la clase Madre
i = b; //variable de Hija
}
void show() {
imprimir("i en Madre: " + super.i);
imprimir("i en Hija: " + i);
}
}
class UsoDeSuper {
public static void main (String args[]) {
Hija subOb = new Hija (1,2);
subOb.show(); //muestra: i en Madre: 1
//i en Hija: 2
}
}
//Uso de super con los métodos
class Madre {
int i,j;
Madre(int a, int b) {
i=a;
j=b;
}
void show() {
imprimir("i y j: " + i + " " + j);
}
}
class Hija extends Madre {
int k;
Hija(int a, int b, int c) {
super(a,b);
k=c;
}
void show() {
super.show(); //i y j: 1 2
//llamada al método show() de Madre
//esto, como veremos luego, es la manera
//de diferenciar entre un método de una
//superclase y uno redefinido por su hija
imprimir("k: " + k); //k: 3
}
}
class Redefine {
public static void main(String args[]) {
Hija subOb = new Hija(1,2,3);
subOb.show;//redefine show() de Madre
}
}
Por último, para concluir este apartado queda aclarar cuándo se llaman los construtores cuando tenemos
una jerarquía de clases derivadas.
La respuesta es que los constructores se llaman en orden de derivación, desde la superclase hacia
la subclase.
Redefinición de métodos
En una jerarquía de clases, cuando un método en una subclase tiene el mismo nombre y tipo de signatura que
un método en su superclase, entonces se dice que el método de la superclase está redefinido por su
subclase.
Cuando un método redefinido se llama dentro de la subclase, siempre se refiere a la versión definida
por esta subclase. Los métodos redefinidos de la superclase siguen siendo accesibles por medio de super, como
vimos anteriormente, aunque con este sistema sólo se puede subir un nivel en la jerarquía de clases.
Los métodos redefinidos pueden ampliar los derechos de acceso de la superclase (por ejemplo, ser public en vez de protected), pero nunca restringirlos.
Asimismo, los métodos de clase o static no pueden ser redefinidos en las clases derivadas.
Bien, hemos visto que se puede redefinir un método, pero qué ventaja tiene esto? Realmente, a priori, lo primero
que a uno se le viene a la cabeza es que no tienes porqué conocer la superclase de la que hereda tu clase, ya que
los límites quedan perfectamente definidos, pero, esto no acaba de parecer una ventaja sustanciosa. Entonces, dónde se aprecia el potencial de
la redefinición de un método?
Pues en algo que se viene a llamar algo así como despacho de métodos dinámicos y que constituye uno de los conceptos más
potentes de la programación en Java. Este es el mecanismo por el cual una llamada a una función redefinida
se resuelve en tiempo de ejecución en vez de hacerlo en tiempo de compilación. El despacho de métodos dinámicos
es importante porque es la manera con la que java implementa el polimorfismo en tiempo de ejecución.
Para entender esto, retomemos uno principio importante: una variable de la superclase puede referenciar
a un objeto de su subclase. Java emplea este hecho para resolver las llamadas a los métodos redefinidos en
tiempo de ejecución.
Cómo? Cuando se llama a un método redefinido a trvés de la referencia de la superclase, Java determina la versión
de ese método que tiene que ejecutar basándose en el tipo de objeto al que se está haciendo referencia al mismo
tiempo que la llamada ocurre. Así, esta determinación se hace en tiempo de ejecución. Cuando se hace referencia
a diferentes tipos de objetos, se llaman las diferentes versiones de un método redefinido. Por tanto, si una superclase contiene
un método que está redefinido por su subclase, entonces cuando diferentes tipos de objetos son referidos a
través de una varible referencia de la superclase, las diferentes versiones del método son ejecutadas.
Vamos a ver en un ejemplo este rollete que acabo de soltaros más claro:
//Despacho de métodos dinámicos
class madre {
void callme() {
imprimir("método callme() de Madre");
}
}
class Hija1 extends Madre {
//redefine callme()
void callme() {
imprimir("método callme() de Hija1");
}
}
class Hija2 extends Madre {
//redefine callme()
void callme() {
imprimir("método callme() de Hija2");
}
}
class Despachadora {
public static void main(String args[]) {
Madre madre = new Madre();
Hija1 hija1 = new Hija1();
Hija2 hija2 = new Hija2();
Madre referencia;
referencia = madre ;//REferencia al objeto Madre
referencia.callme();//llama al método de Madre
referencia = hija1 ;//REferencia al objeto HIja1
referencia.callme();//llama al método de Hija1
referencia = hija2 ;//REferencia al objeto Hija2
referencia.callme();//llama al método de Hija2
}
}
Misión del operador final con la herencia
Las dos formas que hay de emplearlo en este contexto, a parte del ya visto de hacer una variable comportarse
como una constante son:
- Prevenir la redefinición
- Prevenir la herencia
- Uso de final para prevenir la redefinición
- Para deshabilitar la capacidad de nu método de ser redefinido, debes especificar final como un modificador
al comienzo de la declaración del método. Por ejemplo:
class Madre {
final void meth() {
imprimir("método final");
}
}
class Hija extends Madre {
void meth() {//ERROR: CanŽt override }
}
- Uso de final para prevenir la herencia
- En este caso lo que hace es preceder la declaración de la clase del operador final. Hacer esto,
implícitamente declara todos sus métodos como final también.
final class madre {
...
}
class Hija extends Madre {//ERROR! CanŽt subclass Madre }
Pues ya está. Creo que con todo lo que hemos visto a lo largo de estos cinco artículos tenemos los suficientes
conocimientos para entender y comprender la POO en Java. A partir del próximo artículo empezaré otra
serie de ellos relacionados con aspectos más particulares de la programación en Java comenzando con la
explicación de cómo dar "vida" a nuestros programas: los manejadores de eventos.
Leo Suarez es un Ingeniero Superior de Telecomunicaciones por la Universidad de Las Palmas de Gran Canaria con todas sus letras, que no son pocas, y trabaja como tal en Santa Cruz de Tenerife. Cuando no está saltando de isla en isla o escribiendo para javaHispano aprovecha para disfrutar con la novia y los amigos del estupendo clima de las islas afortunadas.
Para cualquier duda o tirón de orejas, e-mail a: leo_ARROBA_javahispano.com
|
|