Los métodos y atributos que habitualmente utilizamos en clase están asociados a las instancias de las clases, esto es, a los objetos. Por ejemplo, dada la clase siguiente:
public class Complejo { private double parteReal; private double parteImaginaria; public Complejo(double parteReal, double parteImaginaria) { this.parteReal = parteReal; this.parteImaginaria = parteImaginaria; } public Complejo(double parteReal) { this(parteReal, 0.0); } public Complejo() { this(0.0, 0.0); } public double getParteReal() { return parteReal; } public double getParteImaginaria() { return parteImaginaria; } public void sumar(Complejo numero) { parteReal = parteReal + numero.getParteReal(); parteImaginaria = parteImaginaria + numero.getParteImaginaria(); } public void sumar(double real) { parteReal = parteReal + real; } public String toString() { String cadena = "" + parteReal; if (parteImaginaria >= 0.0) { cadena = cadena + "+"; } cadena = cadena + parteImaginaria + "i"; return cadena; } }
los atributos parteReal
y
parteImaginaria
pertenecen a cada objeto de la
clase. De hecho, cada objeto poseerá su propia copia de los
atributos. Estos podrán tomar un valor independientemente de los
valores de los atributos del resto de las instancias de la clase
Complejo
. Gracias a ello, podemos representar a la
vez varios números complejos en nuestro programa, cada uno con
su propia parte real y parte imaginaria.
Lo mismo ocurre con los métodos. Cada objeto tiene su propio
método toString
. Lo que diferencia al método
toString
de una instancia de Complejo
del método toString
de otra instancia de
Complejo
es que cada uno accede a los atributos de
su propia instancia, aunque ambos métodos tengan el mismo
código.
Estos atributos y métodos se llaman atributos de instancia y métodos de instancia, debido precisamente al hecho que hemos visto de que están asociados a las instancias de las clases.
Existen otro tipo de atributos y métodos que no están asociados a las instancias, sino a las clases. Debido a ello, se llaman atributos de clase y métodos de clase.
De los atributos de clase existe una copia única para toda la clase, al contrario de lo que ocurre con los de instancia, para los cuales hay una copia por cada una de las instancias de la clase. De hecho, el atributo de clase existe por el hecho de existir la clase, incluso aunque no se haya creado ninguna instancia de esa clase.
Un ejemplo habitual de uso de los atributos de clase son las
constantes. Por ejemplo, la clase Math
,
que forma parte de las bibliotecas estándar de Java, define las
constantes E
y PI
como atributos de
clase. Se definen de esta forma porque, dado que el valor de
pi y e es universal,
se malgastarían recursos si se guardase una copia de cada uno de
ellos en cada instancia, siempre con el mismo valor.
Otro nombre que se usa habitualmente para referirse a atributos
de clase es el de atributo estático. Los
atributos estáticos (de clase) se declaran con la palabra
reservada static
. A continuación se muestra un
ejemplo (aunque le mejoraremos un par de detalles un poco más
adelante):
public class Complejo { private double parteReal; private double parteImaginaria; private static Complejo i = new Complejo(0.0, 1.0); (...)
En el ejemplo se declara un atributo de clase llamado
i
, que representa la unidad imaginaria. El código
es correcto, pero hay unos cuantos detalles que se pueden
mejorar:
Complejo
. Por ejemplo, PI
y
E
son públicos en Math
por este
mismo motivo.
i
. Por ello, conviene que lo declaremos como
final
. De hecho, es habitual que este tipo de
constantes se declaren con static
y
final
a la vez.
PROPORCION_AUREA
).
Si corregimos estos detalles, nos queda el siguiente código:
public class Complejo { private double parteReal; private double parteImaginaria; public static final Complejo I = new Complejo(0.0, 1.0); (...)
Dado que un atributo de clase está asociado a la clase, es recomendable que se acceda a él (para leer o escribir su valor) mediante el nombre de la clase, y no mediante una referencia a un objeto, que es lo que se haría si fuese un atributo de instancia:
cero = new Complejo(-1.0, 0.0); cero.sumar(Complejo.I);
En el ejemplo se está accediendo a I
mediante la
notación Complejo.I
(nombre de clase, un punto, y
nombre de atributo). De la misma forma se accede a
Math.PI
y Math.E
.
Los métodos de clase están también asociados a una clase y no a una instancia. Una consecuencia importante de esto es que desde el código de un método de clase no se puede acceder a atributos ni métodos de instancia de la misma clase directamente. El motivo es que, dado que el método no está asociado a ninguna instancia, ¿cómo podría saber la máquina virtual en cuáles de los objetos de la clase están los atributos o métodos a los que acceder? Eso sí, es posible acceder a atributos de instancia a través de una referencia a un objeto, con la notación habitual (referencia, punto, nombre de atributo o método). El motivo de que se pueda a través de una referencia es que la propia referencia indica ya claramente a qué instancia pertenecen los atributos o métodos a los que queremos acceder. Otra cosa que es posible hacer es acceder desde un método de clase a otros métodos o atributos de clase de dicha clase.
Un método que siempre se define como de clase es el método
especial main
. Por eso siempre lo declaramos con la
palabra clase static
. Añadamos a la clase
Complejo
un método main
:
public class Complejo { private double parteReal; private double parteImaginaria; public static final Complejo I = new Complejo(0.0, 1.0); (...) public String toString() {...} (...) public static void main(String[] args) { // Sentencias correctas: System.out.println(I); System.out.println(Complejo.I); Complejo c = new Complejo(1.0, -2.0); System.out.println(c.toString()); // Sentencias incorrectas (no compilan): System.out.println(toString()); // ¿sobre qué instancia? System.out.println(parteReal); // ¿de qué instancia? } }
En el ejemplo se muestran sentencias que serían correctas, como
invocar el método de instancia toString
mediante
una referencia a un objeto, y sentencias incorrectas, como
intentar acceder directamente, sin una referencia, al mismo
método toString
o al atributo
parteReal
.
Otro ejemplo de método de clase es sqrt
de la clase Math
, que recibe un double
y devuelve otro con su raíz cuadrada. Su invocación se realiza
también mediante el nombre de la clase:
double raiz = Math.sqrt(16.4);
¿Cuándo es más adecuado declarar un atributo o método de instancia, y cuándo es más adecuado declararlo como de clase? Esto requiere un poco de experiencia, que iremos adquiriendo con la práctica. A nuestro nivel actual, nos podemos quedar con un consejo simple: salvo que tengamos un buen motivo para declarar un atributo o método como de clase, declarémoslo siempre como de instancia. ¿Cuál sería un buen motivo? Con lo que ya sabemos se nos pueden ocurrir unos cuantos:
I
.
main
.
sqrt
del cual
hemos hablado anteriormente.
Hay otras situaciones en que se deben declarar atributos y métodos de clase, pero las dejaremos para cuando tengamos un poco más de experiencia programando. De momento no es probable que nos surjan estos casos.