Documentos de Académico
Documentos de Profesional
Documentos de Cultura
6
Clases
L
as clases son el núcleo de Java. Es la construcción lógica sobre la que se basa el lenguaje
Java porque define la forma y naturaleza de un objeto. De tal forma que son la base de la
programación orientada a objetos en Java. Cualquier concepto que se quiera implementar en
Java debe estar encapsulado dentro de una clase.
Dada la importancia que tienen las clases en Java, este capítulo y los próximos se dedican a este
tema. Aquí introduciremos los elementos básicos de una clase y aprenderemos cómo se usan las
clases para crear objetos. También veremos los métodos, constructores y la palabra clave this.
Fundamentos de clases
Las clases se han utilizado desde el comienzo de este libro. Sin embargo, hasta ahora se habían
utilizado sólo de una forma muy rudimentaria. Las clases creadas en los capítulos anteriores existían
simplemente para encapsular el método main( ), que ha permitido mostrar los fundamentos de la
sintaxis de Java. Como veremos, las clases son sustancialmente más potentes que las presentadas
hasta el momento.
Probablemente la característica más importante de una clase es que define un nuevo tipo de
dato. Una vez definido, este nuevo tipo de dato se puede utilizar para crear objetos de ese tipo o
clase. De este modo, una clase es un template (un modelo) para un objeto, y un objeto es una instancia
de una clase. Debido a que un objeto es una instancia de una clase, a menudo las dos palabras objeto
e instancia se usan indistintamente.
105
106 Parte I: El lenguaje Java
// ...
tipo variable_de_instanciaN;
tipo nombre_de_método1 (parámetros) {
// cuerpo del método
}
tipo nombre_de_método2 (parámetros) {
// cuerpo del método
}
// ...
tipo nombre_de_metodoN (parámetros) {
// cuerpo del método
}
}
Los datos, o variables, definidos en una clase se denominan variables de instancia. El código está
contenido en los métodos. El conjunto de los métodos y las variables definidos dentro de una
clase se denominan miembros de la clase. En la mayor parte de las clases, los métodos definidos
acceden y actúan sobre las variables de instancia, es decir, los métodos determinan cómo se
deben utilizar los datos de una clase.
Las variables definidas en una clase se llaman variables de instancia porque cada instancia
de la clase (esto es, cada objeto de la clase), contiene su propia copia de estas variables. Así, los
datos de un objeto son distintos y únicos de los de otros. Éste es un concepto importante sobre
el que volveremos más adelante.
Todos los métodos tienen el mismo formato general, similar al del método main( ) que
hemos estado utilizando hasta el momento. Sin embargo, la mayor parte de los métodos no se
especifican como static o public. Observe que la forma general de una clase no especifica un
método main( ). Las clases de Java no tienen necesariamente un método main( ). Solamente
se requiere un método main( ) si esa clase es el punto de inicio del programa. Los applets no
requieren un método main( ).
NOTA Si usted está familiarizado con C++, observará que en Java, la declaración de una clase y la
implementación de los métodos se almacenan en el mismo sitio y no se definen separadamente.
Esto, en ocasiones, da lugar a archivos .java muy largos, ya que cualquier clase debe estar
definida completamente en un solo archivo. Esta característica de diseño se estableció en Java,
ya que se supuso que, a largo plazo, tener en un sólo sitio las especificaciones, declaraciones e
implementación daría como resultado un código más fácil de mantener.
double largo;
}
PARTE I
Como se ha dicho anteriormente, una clase define un nuevo tipo de dato. En este caso, el nuevo
tipo se llama Caja. Utilizaremos este nombre para declarar objetos de tipo Caja. Es importante
recordar que la declaración de una clase solamente crea un modelo o patrón y no un objeto real.
Así que el código anterior no crea ningún objeto de la clase Caja.
Para crear un objeto de tipo Caja habrá que utilizar una sentencia como la siguiente:
Caja miCaja = new Caja(); // crea un objeto de la clase Caja llamado miCaja
Cuando se ejecute esta sentencia, miCaja será una referencia a una instancia de Caja. Además,
será una realidad “física”. De momento no nos preocuparemos por los detalles de esta sentencia.
Cada vez que creemos una instancia de una clase, estaremos creando un objeto que
contiene su propia copia de cada variable de instancia definida por la clase. Por lo tanto, cada
objeto Caja contendrá sus propias copias de las variables de instancia ancho, alto y largo. Para
acceder a estas variables, utilizaremos el operador punto (.). El operador punto liga el nombre del
objeto con el nombre de una de sus variables de instancia. Por ejemplo, la siguiente sentencia
sirve para asignar a la variable ancho del objeto miCaja el valor l00.
miCaja.ancho = 100;
Esta sentencia indica al compilador que debe asignar a la copia de ancho que está contenida
en el objeto miCaja el valor 100. En general, el operador punto se usa para acceder tanto a las
variables como a los métodos de un objeto. El siguiente es un programa completo que utiliza la
clase Caja:
/* Un programa que utiliza la clase Caja.
El nombre de este archivo es CajaDemo.java
*/
class Caja {
double ancho;
double alto;
double largo;
}
// Esta clase declara un objeto de la clase Caja.
class CajaDemo {
public static void main (String args[]) {
Caja miCaja = new Caja();
double vol;
// asignación de valores a las variables del objeto miCaja
miCaja.ancho = 10;
miCaja.alto = 20;
miCaja.largo = 15;
Al archivo que contiene este programa se le debe llamar CajaDemo.java, ya que el método
main( ) está dentro de la clase denominada CajaDemo, no en la clase denominada Caja.
Cuando se compila este programa, se generan dos archivos .class, uno para Caja y otro para
CajaDemo. El compilador Java crea automáticamente para cada clase su propio archivo .class.
No es necesario que las clases Caja y CajaDemo estén en el mismo archivo fuente. Se puede
escribir cada clase en su propio archivo, es decir, en los archivos Caja.java y CajaDemo.java,
respectivamente.
Para ejecutar este programa, debemos ejecutar CajaDemo.class, y obtendremos la siguiente
salida:
El volumen es 3000.0
Tal y como se ha visto anteriormente, cada objeto tiene sus propias copias de las variables
de instancia. Esto significa que si tenemos dos objetos Caja, cada uno tiene sus propias copias de
largo, ancho y alto. Es importante tener en cuenta que los cambios en las variables de instancia
de un objeto no afectan a las variables de otro. Por ejemplo, el siguiente programa declara dos
objetos Caja.
// Este programa declara dos objetos Caja.
class Caja {
double ancho;
double alto;
double largo;
}
class CajaDemo2{
public static void main (String args[]) {
Caja miCajal = new Caja();
Caja miCaja2 = new Caja();
double vol;
// asignación de valores a las variables de la instancia miCaja1
miCaja1.ancho = 10;
miCaja1.alto = 20;
miCaja1.largo = 15;
/* asignación de valores diferentes a las variables de la instancia miCaja2
*/
miCaja2.ancho = 3;
miCaja2.alto = 6;
miCaja2.1argo = 9;
// calcula el volumen de la primera caja
vol = miCaja1.ancho * miCaja1.alto * miCaja1.largo;
System.out.println("E1 volumen es " + vol);
// calcula el volumen de la segunda caja
vol = miCaja2.ancho * miCaja2.alto * miCaja2.largo;
System.out.println("E1 volumen es " + vol);
}
}
El volumen es 3000.0
El volumen es 162.0
PARTE I
Como se puede comprobar, los datos de miCajal son completamente independientes de los
datos contenidos en miCaja2.
Declaración de objetos
Tal y como se acaba de explicar, cuando se crea una clase, se está creando un nuevo tipo de datos
que se utilizará para declarar objetos de ese tipo. Sin embargo, la obtención de objetos de una
clase es un proceso que consta de dos etapas. En primer lugar, se debe declarar una variable del
tipo de la clase. Esta variable no define un objeto, sino que simplemente es una referencia a un
objeto. En segundo lugar, se debe obtener una copia física del objeto y asignarla a esa variable.
Para ello se utiliza el operador new que asigna dinámicamente, durante el tiempo de ejecución,
memoria a un objeto y devuelve una referencia al mismo. Esta referencia es algo así como la
dirección en memoria del objeto creado por la operación new. Luego se almacena esta referencia
en la variable. Todos los objetos de una clase en Java se asignan dinámicamente. Veamos con más
detalle este procedimiento.
En los ejemplos anteriores se utilizó una línea similar a la siguiente para declarar un objeto
de la clase Caja:
Caja miCaja = new Caja();
Esta sentencia combina las dos etapas descritas anteriormente y, para mostrar más claramente
cada una de ellas, dicha sentencia se puede volver a escribir del siguiente modo:
Caja miCaja; // declara la referencia a un objeto
miCaja = new Caja(); // reserva espacio en memoria para el objeto
La primera línea declara miCaja como una referencia a un objeto de la clase Caja. Después
de que se ejecute esta línea, miCaja contiene el valor null, que indica que todavía no apunta a
un objeto real. Cualquier intento de utilizar miCaja en esta situación dará lugar a un error de
compilación. En la siguiente línea se reserva memoria para un objeto real y se asigna miCaja
como la referencia a dicho objeto. Una vez que se ejecute la segunda línea, ya se puede utilizar
miCaja como si fuera un objeto de la clase Caja. En realidad, miCaja simplemente contiene la
dirección de memoria del objeto real. El efecto de estas dos líneas se describe en la Figura 6.1.
NOTA Los lectores familiarizados con C/C++ habrán observado, probablemente, que las referencias
a objetos son muy semejantes a los apuntadores. Básicamente, esto es correcto. Una referencia
a objeto es semejante a un apuntador a memoria. La principal diferencia –y la clave para la
seguridad de Java– es que no se pueden manipular las referencias tal y como se hace con los
apuntadores. Por lo tanto, una referencia no puede apuntar a una dirección arbitraria de memoria
ni se puede manipular como si fuese entero.
El operador new
Como se explicó, el operador new reserva memoria dinámicamente para un objeto. Su forma
general es:
variable = new nombre_de_clase ();
110 Parte I: El lenguaje Java
Aquí, variable es una variable cuyo tipo es la clase creada, y el nombre_de_clase es el nombre de
la clase que está siendo instanciada. El nombre de la clase seguido de paréntesis está especificando
una llamada al método constructor de la clase. Un constructor define lo que ocurre cuando se crea
un objeto de una clase. Los constructores son una parte importante de todas las clases y tienen
muchos atributos significativos. En la práctica, la mayoría de las clases definen explícitamente
sus propios constructores en la definición de la clase. Cuando no se definen explícitamente, Java
suministra automáticamente el constructor por omisión. Esto es lo que ha ocurrido con la clase
Caja. Por ahora seguiremos utilizando el constructor por omisión, aunque pronto veremos cómo
definir nuestros propios constructores.
En este momento nos podríamos plantear la siguiente pregunta: ¿Por qué no es necesario
utilizar el operador new en el caso de los enteros o de los caracteres? La respuesta es que los
tipos primitivos no se implementan como objetos sino como variables “normales”. Esto se
hace así con el objeto de lograr una mayor eficiencia. Los objetos tienen muchas características
y atributos que obligan a Java a tratarlos de forma diferente a la que utiliza con los tipos
básicos. Al no aplicar la misma sobrecarga a los tipos primitivos que a los objetos, Java puede
implementar a los tipos básicos más eficientemente. Más adelante se verán versiones con
objetos de los tipos primitivos, las cuales están disponibles para su uso en situaciones en las que
se necesitan objetos completos para trabajar con valores primitivos.
Es importante tener en cuenta que el operador new reserva memoria para un objeto durante
el tiempo de ejecución. La ventaja de hacerlo así es que el programa crea exactamente los objetos
que necesita durante su ejecución. Sin embargo, dado que la memoria disponible es finita, puede
ocurrir que ese operador new no sea capaz de reservar memoria para un objeto porque no exista
ya memoria disponible. Si esto ocurre, se producirá una excepción en tiempo de ejecución. (En el
Capítulo 10 se verá la gestión de ésta y otras excepciones). En los ejemplos que se presentan en
este libro no es necesario que nos preocupemos por el hecho de quedamos sin memoria, pero sí
es preciso considerar esta posibilidad en los programas reales.
Volvamos de nuevo a la distinción entre clase y objeto. Una clase crea un nuevo tipo de
dato que se utilizará para crear objetos, es decir, una clase crea un marco lógico que define las
relaciones entre sus miembros. Cuando se declara un objeto de una clase, se está creando una
instancia de esa clase. Por lo tanto, una clase es una construcción lógica, mientras que un objeto
Capítulo 6: Clases 111
tiene una realidad física, esto es, un objeto ocupa un espacio de memoria. Es importante tener en
cuenta esta distinción.
PARTE I
Asignación de variables de referencia a objetos
Las variables de referencia a objetos actúan de una forma diferente a la que se podría esperar
cuando tiene lugar una asignación. Por ejemplo, ¿qué hace el siguiente fragmento de código?
Caja bl = new Caja();
Caja b2 = bl;
Podríamos pensar que a b2 se le asigna una referencia a una copia del objeto que se referencia
mediante bl, es decir, que bl y b2 se refieren a objetos distintos. Sin embargo, esto no es así.
Cuando este fragmento de código se ejecute, bl y b2 se referirán al mismo objeto. La asignación
de bl a b2 no reserva memoria ni copia parte alguna del objeto original. Simplemente hace que
b2 se refiera al mismo objeto que bl. Por lo tanto, cualquier cambio que se haga en el objeto a
través de b2 afectará al objeto al que se refiere bl, ya que, en definitiva, se trata del mismo objeto.
Esta situación se representa gráficamente a continuación.
Ancho
b1
Alto objeto Caja
Largo
b2
Aunque bl y b2 se refieren al mismo objeto, no están relacionados de ninguna otra forma. Por
ejemplo, una asignación posterior a bl simplemente desenganchará bl del objeto original sin
afectar al objeto o a b2. Por ejemplo:
Caja bl = new Caja();
Caja b2 = bl;
// ...
bl = null;
En este caso, bl ha sido asignado a null, pero b2 todavía apunta al objeto original.
RECUERDE Cuando se asigna una variable de referencia a objeto a otra variable de referencia a
objeto, no se crea una copia del objeto, sino que sólo se hace una copia de la referencia.
Métodos
Como se mencionó al comienzo de este capítulo, las clases están formadas por variables de
instancia y métodos. El concepto de método es muy amplio ya que Java les concede una gran
potencia y flexibilidad. La mayor parte del siguiente capítulo se dedica a los métodos. Sin
112 Parte I: El lenguaje Java
embargo, es preciso introducir en este momento algunas nociones básicas para empezar a
incorporar métodos a las clases.
La forma general de un método es la siguiente:
tipo nombre_de_método (parámetros) {
// cuerpo del método
}
Donde tipo especifica el tipo de dato que devuelve el método, el cual puede ser cualquier tipo
válido, incluyendo los tipos definidos mediante clases creadas por el programador. Cuando el
método no devuelve ningún valor, el tipo devuelto debe ser void. El nombre del método se
especifica en nombre_de_método, que puede ser cualquier identificador válido que sea distinto
de los que ya están siendo utilizados por otros elementos del programa. Los parámetros son
una sucesión de pares de tipo e identificador separados por comas. Los parámetros son,
esencialmente, variables que reciben los valores de los argumentos que se pasa a los métodos
cuando se les llama. Si el método no tiene parámetros, la lista de parámetros estará vacía.
Los métodos que devuelven un tipo diferente del tipo void devuelven el valor a la rutina
llamante mediante la siguiente forma de la sentencia return:
return valor;
Donde valor es el valor que el método retorna.
En los siguientes apartados se verá cómo crear distintos tipos de métodos, incluyendo los
que tienen parámetros y los que devuelven valores.
class CajaDemo3 {
public static void main (String args[]) {
Caja miCaja1 = new Caja();
PARTE I
Caja miCaja2 = new Caja();
Este programa genera la siguiente salida, que es la misma que se obtuvo en la versión
anterior.
El volumen es 3000.0
El volumen es 162.0
Por lo tanto, en un método no es necesario especificar el objeto por segunda ocasión. Esto
significa que ancho, alto y largo dentro de volumen( ) se refieren implícitamente a las copias de
esas variables que están en el objeto que llama a volumen( ).
Revisando, cuando se accede a una variable de instancia por un código que no forma parte
de la clase en la que está definida la variable de instancia, se debe hacer mediante un objeto
utilizando el operador punto. Sin embargo, cuando el código forma parte de la misma clase en
la que se define la variable de instancia a la que accede dicho código, la referencia a esa variable
puede ser directa. Esto se aplica de la misma forma a los métodos.
Devolución de un valor
La implementación del método volumen( ) realiza el cálculo del volumen de una caja dentro de
la clase Caja a la que pertenece, sin embargo esta implementación no es la mejor. Por ejemplo,
puede ser un problema si en otra parte del programa se necesita el valor del volumen de la caja,
pero sin que sea necesario presentar dicho valor. Una mejor forma de implementar el método
volumen( ) es realizar el cálculo del volumen y devolver el resultado a la parte del programa que
llama al método. En el siguiente ejemplo, que es una versión mejorada del programa anterior, se
hace eso.
// Ahora volumen() devuelve el volumen de una caja.
class Caja {
double ancho;
double alto;
double largo;
class CajaDemo4 {
public static void main (String args[]) {
Caja miCajal = new Caja();
Caja miCaja2 = new Caja();
double vol;
PARTE I
}
}
Efectivamente este método devuelve el cuadrado de 10, pero su utilización es muy limitada.
Sin embargo, si se modifica de forma que tome un parámetro, como se muestra a continuación,
entonces se consigue que cuadrado( ) tenga una mayor utilidad.
int cuadrado(int i)
{
return i * i;
}
116 Parte I: El lenguaje Java
Este código funciona, pero presenta problemas por dos razones. En primer lugar, resulta torpe y
propenso a errores; por ejemplo, fácilmente se puede olvidar dar valor a una de las dimensiones.
En segundo lugar, en los programas de Java correctamente diseñados, sólo se puede acceder a
las variables de instancia por medio de métodos definidos por sus clases. De ahora en adelante,
permitiremos alterar el comportamiento de un método, pero no el de una variable de instancia
accesible desde el exterior de la clase.
Una mejor solución es crear un método que tome las dimensiones de la caja dentro de sus
parámetros y establezca las variables de instancia apropiadamente. En el siguiente programa se
implementa este concepto:
// Este programa usa un método parametrizado.
class Caja {
double ancho;
double alto;
double largo;
// cálculo y devolución del volumen
double volumen () {
return ancho * alto * largo;
}
// establece las dimensiones de la caja
void setDim (double w, double h, double d) {
ancho = w;
Capítulo 6: Clases 117
alto = h;
largo = d;
}
PARTE I
}
class CajaDemo5 {
public static void main (String args[]) {
Caja miCajal = new Caja();
Caja miCaja2 = new Caja();
double vol;
// inicializa cada caja
miCaja1.setDim (10, 20, 15);
miCaja2.setDim (3, 6, 9);
// calcula el volumen de la primera caja
vol = miCaja1.volumen ();
System.out.println ("El volumen es " + vol);
// calcula el volumen de la segunda caja
vol = miCaja2.volumen ();
System.out.println ("El volumen es " + vol);
}
}
El método setDim( ) se utiliza para establecer las dimensiones de cada caja. Por ejemplo,
cuando se ejecuta:
miCaja1.setDim(10, 20, 15);
Constructores
El proceso de inicializar todas las variables en una clase cada vez que se crea una instancia puede
resultar tedioso, incluso cuando se añaden métodos como setDim( ). Puede resultar más simple
y más conciso realizar todas las inicializaciones cuando el objeto se crea por primera vez. El
proceso de inicialización es tan común que Java permite que los objetos se inicialicen cuando
son creados. Esta inicialización automática se lleva a cabo mediante el uso de un constructor.
Un constructor inicializa un objeto inmediatamente después de su creación. Tiene el
mismo nombre que la clase en la que reside y, sintácticamente, es similar a un método. Una
vez definido, se llama automáticamente al constructor después de crear el objeto y antes de
que termine el operador new. Los constructores resultan un poco diferentes, a los métodos
convencionales, porque no devuelven ningún tipo, ni siquiera void. Esto se debe a que el
tipo implícito que devuelve un constructor de clase es el propio tipo de la clase. La tarea del
constructor es inicializar el estado interno de un objeto de forma que el código que crea a la
118 Parte I: El lenguaje Java
instancia pueda contar con un objeto completamente inicializado que pueda ser utilizado
inmediatamente.
Se puede modificar el ejemplo anterior de forma que las dimensiones de la caja se inicialicen
automáticamente cuando se construye el objeto. Para ello se sustituye el método setDim( ) por
un constructor. Comencemos definiendo un constructor sencillo que simplemente asigne los
mismos valores a las dimensiones de cada caja.
/* La clase Caja usa un constructor para inicializar
las dimensiones de las caja.
*/
class Caja {
double ancho;
double alto;
double largo;
// Este es el constructor para Caja.
Caja() {
System.out.println("Constructor de Caja");
ancho = 10;
alto = 10;
largo = 10;
}
// calcula y devuelve el volumen
doub1e volumen () {
return ancho * alto * largo;
}
}
c1ass CajaDemo6 {
pub1ic static void main (String args[]) {
// declara, reserva memoria, e inicial iza objetos de tipo Caja
Caja miCajal = new Caja();
Caja miCaja2 = new Caja();
doub1e vol;
// obtiene el volumen de la primera caja
vol = miCajal.volumen () ;
System.out.println ("E1 volumen es " + vol);
// obtiene el volumen de la segunda caja
vol = miCaja2.vo1umen ();
System.out.println ("El volumen es " + vol);
}
}
Como puede observarse, miCajal y miCaja2 han sido inicializados por el constructor
de Caja( ) en el momento de su creación. Como el constructor asigna el mismo valor, 10, a
Capítulo 6: Clases 119
todas las dimensiones de la caja, miCajal y miCaja2 tienen el mismo volumen. La sentencia
println( ) dentro de Caja( ) sólo sirve para mostrar cómo funciona el constructor. La mayoría
PARTE I
de los constructores no presentan alguna salida, sino que simplemente inicializan un objeto.
Antes de seguir, examinemos de nuevo el operador new. Cuando se reserva espacio de
memoria para un objeto, se hace de la siguiente forma:
variable = new nombre_de_clase ();
Ahora resulta más evidente la necesidad de los paréntesis después del nombre de clase. Lo que
ocurre realmente es que se está llamando al constructor de la clase. Por lo tanto, en la línea:
Caja miCajal = new Caja();
Como se puede ver, cada objeto es inicializado como se especifica en los parámetros de su
constructor. Por ejemplo, en la siguiente línea:
Caja miCajal = new Caja(l0, 20, 15);
Los valores 10, 20 y 15 se pasan al constructor de Caja( ) cuando new crea el objeto. Así las
copias de ancho, alto y largo de miCajal contendrán los valores 10, 20 y 15, respectivamente.
Esta versión de Caja( ) opera exactamente igual que la versión anterior. El uso de this es
redundante pero correcto. Dentro de Caja( ), this se refiere siempre al objeto llamante. Aunque
en este caso es redundante, en otros contextos this es útil; uno de esos contextos se explica en la
siguiente sección.
una variable tiene el mismo nombre que una variable de instancia, la variable local esconde a
la variable de instancia. Por esta razón, ancho, alto y largo no se utilizaron como los nombres
PARTE I
de los parámetros en el constructor Caja( ) dentro de la clase Caja. Si se hubieran utilizado,
entonces ancho se hubiera referido al parámetro formal, ocultando la variable de instancia
ancho. Si bien normalmente será más sencillo utilizar nombres diferentes, this permite hacer
referencia directamente al objeto y resolver de esta forma cualquier colisión entre nombres,
que pudiera darse entre las variables de instancia y las variables locales. La siguiente versión de
Caja( ) utiliza ancho, alto, y largo como nombres de parámetros y, después, this para acceder a
variables de instancia que tienen los mismos nombres.
// Uso de this para resolver colisiones en el espacio de nombres
Caja (double ancho, double alto, double largo) {
this.ancho = ancho;
this.alto = alto;
this.largo = largo;
}
NOTA El uso de this en este contexto puede ser confuso, y algunos programadores tienen la
precaución de no utilizar nombres de variables locales y parámetros formales que puedan ocultar
variables de instancia. Otros programadores creen precisamente lo contrario, es decir, que puede
resultar conveniente, para una mayor claridad, utilizar los mismos nombres, y usan this para
superar el ocultamiento de la variable de instancia. Adoptar una tendencia u otra es una cuestión
de preferencias.
El método finalize( )
En algunas ocasiones es necesario realizar alguna acción cuando se destruye un objeto. Por
ejemplo, si un objeto sustenta algún recurso que no pertenece a Java, como un descriptor de
archivo o un tipo de letra del sistema de ventanas, entonces es necesario liberar estos recursos
antes de destruir el objeto.