JAVA3
En
los métodos Java, los argumentos son pasados por valor. Cuando se le llama, el
método recibe el valor de la variable pasada. Cuando el argumento es de un tipo
primitivo, pasar por valor significa que el método no puede cambiar el valor.
Cuando el argumento es del tipo de referencia, pasar por valor significa que el
método no puede cambiar el objeto referenciado, pero si puede invocar a los métodos
del objeto y puede modificar las variables accesibles dentro del objeto.
Consideremos
esta serie de sentencias Java que intentan recuperar el color actual de un
objeto Pen en una aplicación gráfica.
.
. .
int
r = -1, g = -1, b = -1;
pen.getRGBColor(r,
g, b);
System.out.println("red
= " + r + ", green = " + g + ", blue = " + b);
.
. .
En
el momento que se llama al método getRGBColor(), las variables r,
g, y b tienen un valor de -1. El llamador espera que el método getRGBColor()
le devuelva los valores de rojo, verde y azul para el color actual en las
variables r, g, y b.
Sin
embargo, el sistema Java pasa los valores de las variables(-1) al método
getRGBColor(); no una referencia a las variables r, g,
y b.
Con
esto se podría visualizar la llamada a getRGBColor() de esta forma.
getRGBColor(-1,
-1, -1)
Cuando
el control pasa dentro del método getRGBColor(), los
argumentos entran dentro del ámbito (se les asigna espacio) y son
inicializados a los valores pasados al método.
class
Pen {
int
valorRojo, valorVerde, valorAzul;
void
getRGBColor(int rojo, int verde, int azul) {
//
rojo, verde y azul han sido creados y sus valores son -1
.
. .
}
}
Con
esto getRGBColor() obtiene acceso a los valores de r, g, y b
del llamador a tavés de sus argumentos rojo, verde, y azul,
respectivamente.
El
método obtiene su propia copia de los valores para utilizarlos dentro del ámbito
del método. Cualquier cambio realizado en estas copias locales no sera
reflejado en las variables originales del llamador.
Ahora
veremos la implementación de getRGBColor() dentro de la clase Pen que
implicaba la firma de método anterior.
class
Pen {
int
valorRojo, valorVerde, valorAzul;
.
. .
//
Este método no trabaja como se espera
void
getRGBColor(int rojo, int verde, int azul) {
rojo
= valorRojo;
verde=valorVerde;
azul=valorAzul;
}
}
Este
método no trabajará como se espera. Cuando el control llega a la sentencia println()
en el siguiente fragmento de código, los argumentos rojo, verde y
azul de getRGBColor() ya no existen. Por lo tanto las
asignaciones
realizadas dentro del método no tendrán efecto; r, g, y b seguiran
siendo igual a -1.
.
. .
int
r = -1, g = -1, b = -1;
pen.getRGBColor(r,
g, b);
System.out.println("rojo
= " + r + ", verde = " + g + ", azul = " + b);
.
. .
El
paso de las varibales por valor le ofrece alguna seguridad a los programadores:
los métodos no puede modificar de forma no intencionada una variable que está
fuera de su ámbito. Sin embargo, alguna vez se querrá que un método modifique
alguno de sus argumentos. El metodo getRGBColor() es un caso apropiado.
El llamador quiere que el método devuelva tres valores a través de sus
argumentos. Sin embargo, el método no puede modificar sus argumentos, y, además,
un método sólo puede devolver un valor a través de su valor de retorno.
Entonces, ¿cómo puede un método devolver más de un valor, o tener algún
efecto (modificar algún valor) fuera de su ámbito?
Para
que un método modifique un argumento, debe ser un tipo de
referencia como un objeto o un array. Los objetos y arrays también son
pasados por valor, pero el valor de un objeto es una referencia. Entonces el
efecto es que los argumentos de tipos de referencia son pasados por referencia.
De aquí el nombre. Una referencia a un objeto es la dirección del objeto en la
memoria. Ahora, el argumento en el método se refiere a la misma posición de
memoria que el llamador.
Reescribamos
el método getRGBColor() para que haga lo que
quye se quiere. Primero introduzcamos un nuevo objeto RGBColor, que puede
contener los valores de rojo, verde y azul de un color en formato RGB.
class
RGBColor {
public
int rojo, verde, azul;;
}
Ahora
podemos reescribir getRGBColor() para que acepte un objeto RGBColor como
argumento. El método getRGBColor() devuelve el color actual de lápiz,
en los valores de las variables miembro rojo, verde y azul de
su argumento RGBColor.
class
Pen {
int
valorRojo, valorVerde, valorAzul;
void
getRGBColor(RGBColor unColor) {
unColor.rojo
= valorRojo;
unColor.verde
= valorVerde;
unColor.azul
= valorAzul;
}
}
Y
finalmente, reescribimos la secuencia de llamada.
.
. .
RGBColor
penColor = new RGBColor();
pen.getRGBColor(penColor);
System.out.println("ojo
= " + penColor.rojo + ", verde = " + penColor.verde + ",
azul = " + penColor.azul);
.
. .
Las
modificaciones realizadas al objeto RGBColor dentro del método getRGBColor()
afectan al objeto creado en la secuencia de llamada porque los nombres penColor
(en la secuencia de llamada) y unColor (en el método getRGBColor())
se refieren al mismo objeto.
En
el siguiente ejemplo, el cuerpo de método para los métodos estaVacio() y
poner() están en negrita.
class
Stack {
static
final int PILA_VACIA = -1;
Object[]
elementosPila;
int
elementoSuperior = PILA_VACIA;
.
. .
boolean
estaVacio() {
if
(elementoSuperior == PILA_VACIA)
return
true;
else
return
false;
}
Object
poner() {
if
(elementoSuperior == PILA_VACIA)
return
null;
else
{
return
elementosPila[elementoSuperior--];
}
}
}
Junto
a los elementos normales del lenguaje Java, se puede utilizar this en el
cuerpo del método para referirse a los miembros del objeto actual.
El
objeto actual es el objeto del que uno de cuyos miembros está siendo llamado.
También se puede utilizar super para referirse a los miembros de la
superclase que el objeto actual haya ocultado mediante la sobreescritura. Un
cuerpo de método también puede contener declaraciones de variables que son
locales de ese método.
Normalmente,
dentro del cuerpo de un método de un objeto se puede referir directamente a las
variables miembros del objeto. Sin embargo, algunas veces no se querrá tener
ambigüedad sobre el nombre de la variable miembro y uno de los argumentos del método
que tengan el mismo nombre.
This
Por
ejemplo, el siguiente constructor de la clase HSBColor inicializa alguna
variable miembro de un objeto de acuerdo a los argumentos pasados al
constructor. Cada argumento del constructor tiene el mismo nombre que la
variable del objeto cuyo valor contiene el argumento.
class
HSBColor {
int
hue, saturacion, brillo;
HSBColor
(int luminosidad, int saturacion, int brillo) {
this.luminosidad
= luminosidad;
this.saturacion
= saturacion;
this.brillo
= brillo;
}
}
Se
debe utilizar this en este constructor para evitar la ambigüedad entre
el argumento luminosidad y la variable miembro luminosidad (y así
con el resto de los argumentos). Escribir luminosidad = luminosidad; no
tendría sentido. Los nombres de argumentos tienen mayor precedencia y ocultan a
los nombres de las variables miembro con el mismo nombre. Para referirise a la
variable miembro se debe hacer explicitamente a través del objeto actual--this.
También
se puede utilizar this para llamar a uno de los métodos del objeto
actual. Esto sólo es necesario si existe alguna ambigüedad con el nombre del método
y se utiliza para intentar hacer el código más claro.
super
Si
el método oculta una de las variables miembro de la superclase, se puede
referir a la variable oculta utilizando super. De igual forma, si el método
sobreescribe uno de los métodos de la superclase, se puede llamar al método
sobreescrito a través de super.
Consideremos
esta clase.
class
MiClase {
boolean
unaVariable;
void
unMetodo() {
unaVariable
= true;
}
}
y
una subclase que oculta unaVariable y sobreescribe unMetodo().
class
OtraClase extends MiClase {
boolean
unaVariable;
void
unMetodo() {
unaVariable
= false;
super.unMetodo();
System.out.println(unaVariable);
System.out.println(super.unaVariable);
}
}
Primero
unMetodo() selecciona unaVariable (una declarada en OtraClase que
oculta a la declarada en MiClase) a false. Luego unMetodo() llama
a su método sobreescrito con esta sentencia.
super.unMetodo();
Esto
selecciona la versión oculta de unaVariable (la declarada en MiClase) a true.
Luego
unMetodo muestra las dos versiones de unaVariable con diferentes
valores.
False
y True
Dentro
del cuerpo de un método se puede declarar más variables para usarlas dentro
del método.
Estas
variables son variables locales y viven sólo mientras el control permanezca
dentro del método. Este método declara un variable local i y la utiliza
para operar sobre los elementos del array.
Object
encontrarObjetoEnArray(Object o, Object[] arrayDeObjetos) {
int
i; // variable local
for
(i = 0; i < arrayDeObjetos.length; i++) {
if
(arrayDeObjetos[i] == o)
return
o;
}
return
null;
}
Después
de que este método retorne, i ya no existirá más.
Cuando
se declara una variable miembro como unFloat en MiClase.
class
MiClase {
float
unFloat;
}
declara
una variable de ejemplar. Cada vez que se crea un ejemplar de la clase, el
sistema crea una copia de todas las variables de ejemplar de la clase.
Las
variables de ejemplar están en constraste con las variables de clase (que se
declaran utilizando el modificador static). El sistema asigna espacio
para las variables de clase una vez por clase, sin importar el número de
ejemplares creados de la clase. Todos los objetos creados de esta clase
comparten la misma copia de las variables de clase de la clase, se puede acceder
a las variables de clase a través de un ejemplar o a través de la propia
clase.
Los
métodos son similares: una clase puede tener métodos de ejemplar y métodos de
clase. Los métodos de ejemplar operan sobre las variables de ejemplar del
objeto actual pero también pueden acceder a las variables de clase. Por otro
lado, los métodos de clase no pueden acceder a las variables del ejemplar
declarados dentro de la clase (a menos que se cree un objeto nuevo y acceda a
ellos a través del objeto). Los métodos de clase también pueden ser invocados
desde la clase, no se necesita un ejemplar para llamar a los métodos de la
clase. Por defecto, a menos que se especifique de otra forma, un miembro
declarado dentro de una clase es un miembro del ejemplar. La clase definida
abajo tiene una variable de ejemplar -- un entero llamado x -- y dos métodos
de ejemplar -- x() y setX() -- que permite que otros objetos
pregunten por el valor de x.
class
UnEnteroLlamadoX {
int
x;
public
int x() {
return
x;
}
public
void setX(int newX) {
x
= newX;
}
}
Cada
vez que se ejemplariza un objeto nuevo desde una clase, se obtiene una copia de
cada una de las variables de ejemplar de la clase. Estas copias están asociadas
con el objeto nuevo. Por eso, cada vez que se ejemplariza un nuevo objeto
UnEnteroLlamadoX de la clase, se obtiene una copia de x que está
asociada con el nuevo objeto UnEnteroLlamadoX.
Todos
los ejemplares de una clase comparten la misma implementación de un método de
ejemplar; todos los ejemplares de UnEnteroLlamadoX comparten la misma
implementación de x() y setX(). Observa que estos métodos se
refieren a la variable de ejemplar del objeto x por su nombre.
"Pero, ¿si todos los ejemplares de UnEnteroLlamadoX comparten la misma
implementación de x() y setX() esto no es ambigüo?" La
respuesta es no. Dentro de un método de ejemplar, el nombre de una variable de
ejemplar se refiere a la variable de ejemplar del objeto actual (asumiendo que
la variable de ejemplar no está ocultada por un parámetro del método). Ya
que, dentro de x() y setX(), x es equivalente a this.x.
Los
objetos externos a UnEnteroLlamadoX que deseen acceder a x deben hacerlo
a través de un ejemplar particular de UnEnteroLlamadoX. Supongamos que este código
estuviera en otro método del objeto. Crea dos objetos diferentes del tipo
UnEnteroLlamadoX, y selecciona sus valores de x a diferente valores y
luego lo muestra:
.
. .
UnEnteroLlamadoX
miX = new UnEnteroLlamadoX();
UnEnteroLlamadoX
otroX = new UnEnteroLlamadoX();
miX.setX(1);
otroX.x
= 2;
System.out.println("miX.x
= " + miX.x());
System.out.println("otroX.x
= " + otroX.x());
Observese
que el código utilizado en setX() para seleccionar el valor de x para
miX pero sólo asignando el valor otroX.x directamente. De otra
forma, el código manipula dos copias diferentes de x: una contenida en
el objeto miX y la otra en el objeto otroX. La salida producida
por este código es.
miX.x
= 1
otroX.x
= 2
mostrando
que cada ejemplar de la clase UnEnteroLlamadoX tiene su propia copia de la
variable de ejemplar x y que cada x tiene un valor diferente.
Cuando
se declara una variable miembro se puede especificar que la variable es una
variable de clase en vez de una variable de ejemplar. Similarmente, se puede
especificar que un método es un método de clase en vez de un método de
ejemplar. El sistema crea una sola copia de una variable de clase la primera vez
que encuentra la clase en la que está definida la variable. Todos los
ejemplares de esta clase comparten la misma copia de las variables de clase. Los
métodos de clase sólo pueden operar con variables de clase -- no pueden
acceder a variables de ejemplar definidas en la clase.
Para
especificar que una variable miembro es una variable de clase, se utiliza la
palabra clave static. Por ejemplo, cambiemos la clase UnEnteroLlamadoX
para que su variable x sea ahora una variable de clase.
class
UnEnteroLlamadoX {
static
int x;
public
int x() {
return
x;
}
public
void setX(int newX) {
x
= newX;
}
}
Ahora
veamos el mismo código mostrado anteriormente que crea dos ejemplares de
UnEnteroLlamadoX, selecciona sus valores de x, y muestra esta salida
diferente.
miX.x
= 2
otroX.x
= 2
La
salida es diferente porque x ahora es una variable de clase por lo que sólo
hay una copia de la variable y es compartida por todos los ejemplares de
UnEnteroLlamadoX incluyendo miX y otroX.
Cuando
se llama a setX() en cualquier ejemplar, cambia el valor de x para
todos los ejemplares de UnEnteroLlamadoX.
Las
variables de clase se utilizan para aquellos puntos en los que se necesite una
sola copia que debe estar accesible para todos los objetos heredados por la
clase en la que la variable fue declarada. Por ejemplo, las variables de clase
se utilizan frecuentemente con final para definir constantes (esto es más
eficiente en el consumo de memoria, ya que las constantes no pueden cambiar y sólo
se necesita una copia).
Similarmente,
cuando se declare un método, se puede especificar que el método es un método
de clase en vez de un método de ejemplar. Los métodos de clase sólo pueden
operar con variables de clase y no pueden
acceder
a las variables de ejemplar definidas en la clase.
Para
especificar que un método es un método de clase, se utiliza la palabra clave static
en la declaración de método. Cambiemos la clase UnEnteroLlamadoX para que
su variable miembro x sea de nuevo una variable de ejemplar, y sus dos métodos
sean ahora métodos de clase.
class
UnEnteroLlamadoX {
private
int x;
static
public int x() {
return
x;
}
static
public void setX(int newX) {
x
= newX;
}
}
Cuando
se intente compilar esta versión de UnEnteroLlamadoX, se obtendrán errores de
compilación.
Esto
es porque los métodos de la clase no pueden acceder a variables de ejemplar a
menos que el método haya creado un ejemplar de UnEnteroLlamadoX primero y luego
acceda a la variable a través de él.
Construyamos
de nuevo UnEnteroLlamadoX para hacer que su variable x sea una variable
de clase.
class
UnEnteroLlamadoX {
static
private int x;
static
public int x() {
return
x;
}
static
public void setX(int newX) {
x
= newX;
}
}
Ahora
la clase se compilará y el código anterior que crea dos ejemplares de
UnEnteroLlamadoX, selecciona sus valores x, y muestra en su salida los
valores de x.
miX.x
= 2
otroX.x
= 2
De
nuevo, cambiar x a través de miX también lo cambia para los
otros ejemplares de UnEnteroLlamadoX. Otra diferencia entre miembros del
ejemplar y de la clase es que los miembros de la clase son accesibles desde la
propia clase. No se necesita ejemplarizar la clase para acceder a los miembros
de clase.
Reescribamos
el código anterior para acceder a x() y setX() directamente desde
la clase UnEnteroLlamadoX.
.
. .
UnEnteroLlamadoX.setX(1);
System.out.println("UnEnteroLlamadoX.x
= " + UnEnteroLlamadoX.x());
.
. .
Observa
que ya no se tendrá que crear miX u otroX. Se puede seleccionar x
y recuperarlo directamente desde la clase UnEnteroLlamadoX. No se puede
hacer esto con miembros del ejemplar. Solo se puede invocar métodos de ejemplar
a través de un objeto y sólo puede acceder a las variables de ejemplar desde
un objeto.
Se
puede acceder a las variables y métodos de clase desde un ejemplar de la clase
o desde la clase misma.
Uno
de los beneficos de las clases es que pueden proteger sus variables y métodos
miembros frente al acceso de otros objetos. ¿Por qué es esto importante? Bien,
consideremos esto. Se ha escrito una clase que
representa
una petición a una base de datos que contiene toda clase de información
secreta, es decir, registros de empleados o proyectos secretos de la compañia.
Ciertas
informaciones y peticiones contenidas en la clase, las soportadas por los métodos
y variables accesibles públicamente en su objeto son correctas para el consumo
de cualquier otro objeto del sistema.
Otras
peticiones contenidas en la clase son sólo para el uso personal de la clase.
Estas otras soportadas por la operación de la clase no deberían ser utilizadas
por objetos de otros tipos. Se querría proteger esas variables y métodos
personales a nivel del lenguaje y prohibir el acceso desde objetos de otros
tipos.
En
Java se pueden utilizar los especificadores de acceso para proteger tanto las
variables como los métodos de la clase cuando se declaran. El lenguaje Java
soporta cuatro niveles de acceso para las variables y
métodos
miembros: private, protected, public, y, todavía no especificado, acceso de
paquete.
La
siguiente tabla le muestra los niveles de acceso pemitidos por cada
especificador.
Especificador |
clase |
subclase |
paquete |
mundo |
private |
X |
|
|
|
protected |
X |
X* |
X |
|
Public |
X |
X |
X |
X |
Package |
X |
|
X |
|
La
primera columna indica si la propia clase tiene acceso al miembro definido por
el especificador de acceso.
La
segunda columna indica si las subclases de la clase (sin importar dentro de que
paquete se encuentren estas) tienen acceso a los miembros. La tercera columna
indica si las clases del mismo paquete que la clase (sin importar su parentesco)
tienen acceso a los miembros. La cuarta columna indica si todas las clases
tienen acceso a los miembros.
Observa
que la intersección entre protected y subclase tiene un '*' - este caso de
acceso particular tiene una explicación en más detalle más adelante.
Echemos
un vistazo a cada uno de los niveles de acceso más detalladamente.
El
nivel de acceso más restringido es private. Un miembro privado es accesible sólo
para la clase en la que está definido. Se utiliza este acceso para declarar
miembros que sólo deben ser utilizados por la clase. Esto incluye las variables
que contienen información que si se accede a ella desde el exterior podría
colocar al objeto en un estado de inconsistencia, o los métodos que llamados
desde el exterior pueden poner en peligro el estado del objeto o del programa
donde se está ejecutando. Los miembros privados son como secretos, nunca deben
contarsele a nadie.
Para
declarar un miembro privado se utiliza la palabra clave private en su
declaración. La clase siguiente contiene una variable miembro y un método
privados.
class
Alpha {
private
int soyPrivado;
private
void metodoPrivado() {
System.out.println("metodoPrivado");
}
}
Los
objetos del tipo Alpha pueden inspeccionar y modificar la variable soyPrivado
y pueden invocar el método metodoPrivado(), pero los objetos de
otros tipos no pueden acceder. Por ejemplo, la clase Beta
definida
aquí.
class
Beta {
void
metodoAccesor() {
Alpha
a = new Alpha();
a.soyPrivado
= 10; // ilegal
a.metodoPrivado();
// ilegal
}
}
no
puede acceder a la variable soyPrivado ni al método metodoPrivado() de
un objeto del tipo Alpha porque Beta no es del tipo Alpha.
Si
una clase está intentando acceder a una variable miembro a la que no tiene
acceso--el compilador mostrará un mensaje de error.
a.iamprivate
= 10; // ilegal
Y
si un programa intenta acceder a un método al que no tiene acceso, generará un
error de compilación.
a.privateMethod();
// ilegal
El
siguiente especificador de nivel de acceso es 'protected' que permite a la
propia clase, las subclases (con la excepción a la que nos referimos
anteriormente), y todas las clases dentro del mismo paquete que accedan a los
miembros. Este nivel de acceso se utiliza cuando es apropiado para una subclase
de la clase tener acceso a los miembros, pero no las clases no relacionadas. Los
miembros protegidos son como secretos familiares - no importa que toda la
familia lo sepa, incluso algunos amigos allegados pero no se quiere que los
extraños lo sepan.
Para
declarar un miembro protegido, se utiliza la palabra clave protected.
Primero echemos un vistazo a cómo afecta este especificador de acceso a las
clases del mismo paquete.
Consideremos
esta versión de la clase Alpha que ahora se declara para estar incluida en el
paquete Griego y que tiene una variable y un método que son miembros
protegidos.
package
Griego;
class
Alpha {
protected
int estoyProtegido;
protected
void metodoProtegido() {
System.out.println("metodoProtegido");
}
}
Ahora,
supongamos que la clase Gamma, también está declarada como miembro del paquete
Griego (y no es una subclase de Alpha). La Clase Gamma puede acceder legalmente
al miembro estoyProtegido del objeto Alpha y puede llamar legalmente a su
método metodoProtegido().
package
Griego;
class
Gamma {
void
metodoAccesor() {
Alpha
a = new Alpha();
a.estoyProtegido
= 10; // legal
a.metodoProtegido();
// legal
}
}
Esto
es muy sencillo. Ahora, investiguemos cómo afecta el especificador protected a
una subclase de Alpha.
Introduzcamos
una nueva clase, Delta, que desciende de la clase Alpha pero reside en un
paquete diferente - Latin. La clase Delta puede acceder tanto a estoyProtegido
como a metodoProtegido(), pero solo en objetos del tipo Delta o sus
subclases. La clase Delta no puede acceder a estoyProtegido o metodoProtegido()
en objetos del tipo Alpha. metodoAccesor() en el siguiente ejemplo
intenta acceder a la variable miembro estoyProtegido de un objeto del
tipo Alpha, que es ilegal, y en un objeto del tipo Delta que es legal.
Similarmente,
metodoAccesor() intenta invocar a metodoProtegido() en un objeto
del tipo Alpha, que también es ilegal.
import
Griego.*;
package
Latin;
class
Delta extends Alpha {
void
metodoAccesor(Alpha a, Delta d) {
a.estoyProtegido
= 10; // ilegal
d.estoyProtegido
= 10; // legal
a.metodoProtegido();
// ilegal
d.metodoProtegido();
// legal
}
}
Si
una clase es una subclase o se cuentra en el mismo paquete de la clase con el
miembro protegido, la clase tiene acceso al miembro protegido.
El
especificador de acceso más sencillo es 'public'. Todas las clases, en todos
los paquetes tienen acceso a los miembros públicos de la clase. Los miembros públicos
se declaran sólo si su acceso no produce resultados indeseados si un extraño
los utiliza. Aquí no hay secretos familiares; no importa que lo sepa todo el
mundo.
Para
declarar un miembro público se utiliza la palabra clave public. Por
ejemplo:
package
Griego;
class
Alpha {
public
int soyPublico;
public
void metodoPublico() {
System.out.println("metodoPublico");
}
}
Reescribamos
nuestra clase Beta una vez más y la ponemos en un
paquete diferente que la clase Alpha y nos aseguramos que no están
relacionadas (no es una subclase) de Alpha.
import
Griego.*;
package
Romano;
class
Beta {
void
metodoAccesor() {
Alpha
a = new Alpha();
a.soyPublico
= 10; // legal
a.metodoPublico();
// legal
}
}
Como
se puede ver en el ejemplo anterior, Beta puede inspeccionar y modificar
legalmente la variable soyPublico en la clase Alpha y puede llamar
legalmente al método metodoPublico().