|
| Volver a índice |
El elemento básico de la programación orientada a objetos en Java es un clase. Una clase define la forma y el comportamiento de un objeto. Cualquier concepto que desee representar en su programa en Java está encapsulado en una clase. En este capítulo abordaremos en profundidad los conceptos teóricos de la programación orientada a objetos: encapsulado, herencia, y polimorfismo. Aprenderemos a crear, ampliar y crear instancias de nuestras propias clases y comenzaremos a utilizar la potencia real del estilo orientado a objetos de Java. Para crear una clase sólo se necesita un archivo fuente que contenga la palabra clave class seguida de un identificador legal y un par de llaves para el cuerpo.
class Point {
}
Las clases de Java típicas incluirán variables y métodos de
instancia. Los programas en Java completos constarán por lo
general de varias clases de Java de distintos archivos
fuente.
Una clase define la estructura de un objeto y su interfaz
funcional, conocida como métodos. Cuando se ejecuta un
programa en Java, el sistema utiliza definiciones de clase
para crear instancias de las clases, que son objetos
reales. La forma general de una clase se muestra a
continuación.
class nombre_de_clase extends nombre_de_superclase {
type variable_de_instancia1;
type variable_de_instancia2;
type variable_de_instanciaN;
type nombre_de_método1 (lista_de_parámetros) {
cuerpo_del_método;
}
type nombre_de_método2 (lista_de_parámetros) {
cuerpo_del_método;
}
type nombre_de_métodoN (lista_de_parámetros) {
cuerpo_del_método;
}
}
Aquí, nombre_de_clase y nombre_de_superclase son identificadores. La palabra clave extends se utiliza para indicar que nombre_de_clase será una subclase de nombre_de_superclase. Hay una clase incorporada, llamada Object (objeto), que está en la raíz de la jerarquía de clases de Java. Si se desea realizar una subclase de Object directamente, se puede omitir la cláusula extends.
Cada nueva clase que se crea aņade otro tipo que se puede
utilizar igual que los tipos simples. Por lo tanto, cuando
se declara una nueva variable, se puede utilizar un nombre
de clase como tipo. A estas variables se las conoce como
referencias a objeto.
A cada instancia se le puede llamar también objeto. Cuando
se declara que el tipo de una variable es una clase, tiene
como valor por omisión el null, que es una referencia al
tipo Object, y, por lo tanto, es compatible en tipo con
todas las otras clases. El objeto null no tiene valor; es
distinto del entero 0, igual que el false de boolean. Este
ejemplo declara una variable p cuyo tipo es de la clase
Point.
Point p; Aquí la variable p tiene un valor null.
Los datos se encapsulan dentro de una clase declarando las variables dentro de las llaves de apertura y cierre de la declaración de la clase. A las variables que se declaran en este ámbito y fuera del ámbito de un método concreto se las conoce como variables de instancia. Este ejemplo declara una clase de nombre Point, con dos variables de instancia enteras llamadas x e y.
class Point {
int x, y;
}
El operador new crea una única instancia de una clase y devuelve una referencia a ese objeto. Aquí se crea una nueva instancia de Point y se almacena en una variable p.
Point p = new Point();
Aquí p referencia a una instancia de Point, pero realmente no lo contiene. Se pueden crear múltiples referencias al mismo objeto.
Point p = new Point();
Point p2 = p;
Cualquier cambio realizado en el objeto refernciado por p2 afectará al mismo objeto al cual se refiere p. La asignación de p a p2 no asignó memoria ni copió nada en el objeto original. De hecho las asignaciones posteriores a p simplemente desengancharán p del objeto original sin afectar al propio objeto, como se muestra a continuación.
Point p = new Point();
Point p2 = p;
p = null;
Aunque se haya asignado p a null, p2 todavía apunta al objeto creado por el operador new.
El operador punto se utiliza para acceder a las variables de instancia y los métodos contenidos en un objeto. Esta es la forma general de acceder a las variables de instancia utilizando el operador punto.
referencia_a_objeto.nombre_de_variable
Aquí referencia_a_objeto es una referencia a un objeto y nombre_de_variable es el nombre de la variable de instancia contenida en el objeto al que se desea acceder. El siguiente fragmento de código muestra cómo se puede utilizar el operador punto para almacenar valores en variables de instancia y para referirnos a estos.
p.x = 10;
p.y = 20;
System.out.println("x = " + p.x + " y = " + p.y);
Los métodos son subrutinas unidas a una definición de una
clase específica. Se declaran dentro de una definición de
clase al mismo nivel que las variables de instancia. Se
debe llamar a los métodos en el contexto de una instancia
concreta de esa clase.
En la declaración de los métodos se define que devuelve un
valor de un tipo concreto y que tiene un conjunto de
parámetros de entrada.
tipo nombre_de_método ( lista_formal_de_parámetros ) {
cuerpo_del_método;
}
El nombre_de_método es cualquier identificador legal
distinto de los ya utilizados en el ámbito actual. La
lista_forma_de_parámetros es una secuencia de parejas de
tipo e identificador separadas por comas. Si no se desean
parámetros, la declaración del método deberá incluir un par
de paréntesis vacío.
Podríamos crear un método en nuestra clase Point que
inicialice las variables de instancia que quedaría de la
forma siguiente:
clase Point {
int x, y;
void init(int x, int y) {
this.x = x;
this.y = y;
}
}
En Java es ilegal declarar dos variables locales con el mismo nombre dentro del mismo ámbito o uno que lo incluya. Observará que hemos utilizado x e y como parámetros para el método init y en el interior del método hemos utilizado un valor de referencia especial llamado this para referirnos directamente a las variables de instancia. Si no hubiéramos utilizado this entonces x e y se hubieran referido al parámetro formal y no a las variables de instancia lo que se conoce como ocultar variables de instancia.
Dado que no hay funciones globales en Java, se debía idear
alguna manera de iniciar un programa, de ahí el sentido del
método main. Puesto que en otros lenguajes (p.e. C y C++)
main se utilizaba a menudo para pasar parámetros desde la
línea de órdenes, un concepto perdido en los usuarios de
interfaces de usuario gráficas, el main de Java también
pasa esos argumentos.
El compilador de Java compilará clases que no tengan el
método main. El intérprete Java, sin embargo, no tiene
ningún modo de ejecutar esas clases. El método main es
simplemente un lugar de inicio para que el intérprete
comience. Un programa complejo tendrá docenas de clases, y
sólo una de ellas necesitará tener un método main. Para los
applets (programas Java que están incrustados en los
visualizadores de red) no se utiliza el método main, ya que
los visualizadores (o navegadores) de red siguen un
convenio distinto para inicializar applets.
Se llama a los métodos dentro de una instancia de un clase utilizando el operador punto (.). La forma general de una llamada:
referencia_a_objeto . nombre_de_método ( lista_de_parámetros );
Aquí, referencia_a_objeto es cualquier variable que se
refiere a un objeto, nombre_de_método es el nombre de un
método de la clase con la que se declaró
referencia_a_objeto y lista_de_parámetros es una lista de
valores o expresiones separados por comas que coinciden en
número y tipo con cualquiera de los métodos declarados como
nombre_de_método en la clase.
En este caso, podríamos llamar al método init sobre
cualquier objeto Point.
Point p = new Point();
p.init (10, 20);
Las clases pueden implementar un método especial llamado
constructor. Un constructor es un método que inicializa un
objeto inmediatamente después de su creación. Tienen
exactamente el mismo nombre de la clase en la que residen;
de hecho no se puede tener ningún otro método que comparta
su nombre con su clase. Una vez definido, se llama
automáticamente al constructor después de crear el objeto,
antes de que termine el operador new.
Como extensión a nuestro ejemplo Point, podemos inicializar
las dos variables de instancia cuando construimos el
objeto.
class Point {
int x, y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
}
class PointCreate {
public static void main (String args[]) {
Point p = new Point(10, 20);
System.out.println("x = " + p.x + " y = " + p.y);
}
}
Se llama al método del constructor justo después de crear la instancia y antes de que new vuelva al punto de la llamada.
Es posible y a menudo deseable crear más de un método con el mismo nombre, pero con listas de parámetros distintas. A esto se le llama sobrecarga de método. Se sobrecarga un método siempre que se crea un método en una clase que ya tiene un método con el mismo nombre. Aquí presentamos una versión de la clase Point que utiliza sobrecarga de método para crear un constructor alternativo.
class Point {
int x, y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
Point() {
x = -1;
y = -1;
}
}
class PointCreateAlt {
public static void main (String args[]) {
Point p = new Point();
System.out.println("x = " + p.x + " y = " + p.y);
}
}
Este ejemplo crea un objeto Point que llama al segundo constructor sin parámetros en vez de al primero.
Un refinamiento adicional es que un constructor llame a otro para construir la instancia correctamente. A menudo es una buena idea crear constructores relacionados para que las clases puedan evolucionar con más suavidad a lo largo del ciclo de desarrollo. Por ejemplo.
class Point {
int x, y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
Point() {
this(-1, -1);
}
}
La herencia es un concepto que relaciona clases una encima de otra de un manera jerárquica. Esto permite que los descendientes de una clase hereden todas las variables y métodos de sus ascendientes, además de crear los suyos propios. A estos descendientes se les llama subclases. Al padre inmediato de una clase se le llama su superclase. En este ejemplo, extendemos la clase Point para que incluya un tercer componente llamado z.
class Point3D extends Point {
int z;
Point3D(int x, int y, int z) {
this.x = x;
this.y = y;
this.z = z;
}
Point3D() {
this(-1, -1, -1);
}
}
La palabra clave extends se utiliza para indicar que se desea crear una subclase de Point. No se necesita declarar las variables x e y en Point3D porque se habían heredado de Point. Todas las variables x, y, z están en el mismo ámbito desde la perspectiva de una instancia de Point3D.
Hay una variable especial en Java llamada super, que se refiere directamente a los constructores de la superclase. Este ejemplo define una nueva versión de Point3D que utiliza el constructor super para inicializar x e y.
class Point3D extends Point {
int z;
Point3D(int x, int y, int z) {
super(x, y); // aquí se llama al constructor Point(x, y)
this.z = z;
}
}
Durante la ejecución, la referencia a un objeto se puede referir a una instancia de alguna subclase del tipo de referencia declarado. En estos casos, Java utiliza la instancia real para decidir a qué método llamar en el caso que la subclase sobrescriba el método al que se llama. Por ejemplo, estas dos clases tienen una relación subclase/superclase simple con un único método que se sobrescribe en la subclase.
class A {
void callme() {
System.out.println("En el método callme de A");
}
}
class B extends A {
void callme() {
System.out.println("En el método callme de B");
}
}
class Dispatch {
public static void main(String args[]) {
A a = new B();
a.callme();
}
}
Hemos declarado que la variable es de tipo A y después hemos almacenado ella una referencia a una instancia de la clase B. Cuando llamamos al método callme de a, el compilador de Java verifica que realmente A tiene un método llamado callme, pero el interprete de Java observa que la referencia es realmente una instancia de B, por lo que llama al método de B, en vez de al método de A. Esta forma de polimorfismo dinámico durante la ejecución es uno de los mecanismos más poderosos que ofrece el diseņo orientado a objetos para soportar la reutilización de código y la robustez.
Todos 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.
El modificador de tipo final implica que todas las
referencias futuras a este elemento se basarán en esta
definición.
Se puede utilizar final como modificador en declaraciones
de método cuando se desea no permitir que las subclases
sobrescriban un método concreto.
Por lo general, en Java, no hay que preocuparse por liberar
memoria. Sin embargo, hay circunstancias en las que se
podría desear ejecutar algún código especial cuando el
sistema de recogida de basura reclama un objeto.
Se aņade un método a cualquier clase con el nombre finalize
y el intérprete de Java llama al método cuando vaya a
reclamar el espacio de ese objeto.
A veces se desea crear un método que se utiliza fuera del contexto de cualquier instancia. Todo lo que se tiene que hacer es declarar estos métodos como static (estático). Los métodos estáticos sólo pueden llamar a otros métodos static directamente, y no se pueden referir a this o super de ninguna manera. Las variables también se pueden declarar como static, pero debe ser consciente que es equivalente a declararlas como variables globales, que son accesibles desde cualquier fragmento de código. Se puede declarar un bloque static que se ejecuta una sola vez si se necesitan realizar cálculos para inializar las variables static. El ejemplo siguiente muestra una clase que tiene un método static, algunas variables static y un bloque de inicialización static.
class Static {
static int a = 3;
static int b;
static void method(int x) {
System.out.println("x = " + x);
System.out.println("a = " + a);
System.out.println("b = " + b);
}
static {
System.out.println("bloque static inicializado");
b = a * 4;
}
public static void main(String args[]) {
method(42);
}
}
Esta es la salida del programa.
bloque static inicializado
x = 42
a = 3
b = 12
Hay situaciones que se necesita definir una clase que
declara la estructura de una abstracción dada sin
promocionar una implementación completa de cada método. Se
puede indicar que se necesita que ciertos métodos se
sobrescriban en subclases utilizando el modificador
abstract. A estos métodos se les llama a veces
responsabilidad de subclase. Cualquier clase que contenga
métodos declarados como abstract también se tiene que
declarar como abstract. No se pueden crear instancias de
dichas clases directamente con el operador new, dado que su
implementación completa no está definida. No se pueden
declarar constructores abstract o métodos abstract static.
Cualquier subclase de una clase abstract debe implementar
todos los métodos abstract de la superclase o ser declarada
también como abstract.