08 - POO II: Visibilidad
Visibilidad
Hasta ahora, la mayoría nuestro métodos, propiedades y clases tenían asociada la palabra public en su declaración. Ésto hace referencia a la visibilidad (o modificadores de acceso) de los elementos. En la POO, normalmente existen 3 tipos de visibilidad:
- public: El elemento es accesible desde cualquier clase.
- private: El elemento es accesible solo desde la propia la clase.
- protected: El elemento es accesible desde la propia clase y sus clases derivadas (cuando veamos herencia lo entenderéis mejor).
En Java, existe un cuarto tipo de visibilidad, además de cambiar un poco la visibilidad de protected:
- protected en Java: Igual que antes, el elemento es accesible desde la propia clase y sus clases derivadas, pero además, también es accesible desde las clases que están en el mismo paquete (package).
- package private: El elemento es accesible solo desde las clases definidas en el mismo package. Si no se indica ningún tipo de visibilidad, este es el comportamiento por defecto.
Por ejemplo, vamos a modificar los atributos de la clase Coche creada anteriormente haciéndolos privados:
public class Coche { private String marca, modelo, color; private int numeroBastidor, velocidad = 0; public void acelerar(int aumento){ velocidad += aumento; } }
Intenta ahora acceder a los atributos de Coche desde la clase Main:
public class Main { public static void main(String[] args) { Coche coche1 = new Coche(); coche1.marca = "Kia"; System.out.println("La marca de mi coche es " + coche1.marca); } }
El compilador ya debería mostrarte un error diciendo que el atributo no es accesible. Si ejecutamos el archivo, se nos mostrará el mismo error:
The field Coche.marca is not visible
Modifica ahora los atributos de Coche y hazlos públicos:
public class Coche { public String marca, modelo, color; public int numeroBastidor, velocidad = 0; public void acelerar(int aumento){ velocidad += aumento; } }
Ahora ya no tenemos ningún problema a la hora de acceder a ellos desde otra clase:
public class Main { public static void main(String[] args) { Coche coche1 = new Coche(); coche1.marca = "Kia"; System.out.println("La marca de mi coche es " + coche1.marca); } }
La marca de mi coche es Kia
¿Qué visibilidad elegir para cada elemento? Obviamente, depende de la situación, pero por regla general, los atributos siempre serán privados, los métodos que no vayamos a utilizar fuera de la clase también serán privados, mientras que los métodos que queramos publicar para interactuar con nuestra clase serán públicos.
Los atributos los hacemos privados para poder chequear condiciones en sus valores. Por ejemplo, en el caso de Coche, podríamos hacer algo así desde la clase Main:
coche1.velocidad = -300000;
Obviamente, esa velocidad no tiene ningún sentido, con lo que si queremos modificar o leer el valor de los atributos, mejor hacer métodos públicos específicos para ello (setters y getters) donde podamos controlar los valores.
En cuanto a los métodos, solo aquellos que queramos que sean accesibles desde fuera de nuestra clase deberían ser públicos. Si tenemos métodos internos que solo vamos a gastar en esa clase, éstos deberían ser siempre privados.
Getters y Setters
Como hemos visto, la mayoría de veces (en realidad, debería ser casi siempre) nuestros atributos van a ser privados. Entonces, ¿Cómo podemos leer y darles valor? Para eso, normalmente se crear métodos especiales que se encargan de ello: los getters y setters.
La mayoría de IDEs son capaces de crear estos métodos de forma automática. Por ejemplo, en VSC si usamos Ctrl + . sobre un atributo, nos saldrá un desplegable donde podemos elegir Generate Getter and Setter for nombre_atributo:
Si pinchamos en la opción, veremos como nos crea automáticamente dos métodos públicos: getNombreAtributo() y setNombreAtributo():
public String getMarca() { return marca; } public void setMarca(String marca) { this.marca = marca; }
Estos dos métodos son, en principio, muy sencillos. Lo único que hacen es devolver o asignar un valor al atributo. Obviamente, podemos modificarlos a nuestro gusto, haciendo las comprobaciones que queramos antes de asignar el valor o devolviendo cualquier otra cosa.
También podemos generar los getters y setters de todos los atributos si pinchamos sobre la clase y elegimos Generate Getters and Setters:
¿Debemos generar getters y setters para todos nuestros atributos? No, de hecho, muchas veces nos interesan crear clases inmutables (que no cambien sus atributos a lo largo de la ejecución del programa), con lo que no queremos poder modificar el valor de los atributos, lo que quiere decir que no existirán setters. Además, hay veces que tendremos atributos internos que solo tienen sentido en nuestra clase, con lo que no queremos que puedan ser accesible desde fuera, con lo que podemos eliminar su método get() o hacerlo privado si lo queremos usar dentro de nuestra clase.
private String marca; public String marca(){ return marca; }
Si no generamos los setters de los atributos, ¿Cómo podemos asignarles valores? Aunque existen varias opciones, la más habitual es hacerlo en el momento en que creamos un objeto de la clase con un método especial que veremos a continuación: el constructor.
Constructores
Vamos a partir de las siguientes dos clases:
public class Coche { private String marca, modelo, color; private int numeroBastidor, velocidad; public String getMarca(){ return marca; } }
public class Main { public static void main(String[] args) { Coche coche1 = new Coche(); System.out.println("La marca del coche es " + coche1.getMarca()); } }
Como ves, la clase Coche tiene todos sus atributos privados, el método getMarca() y el método que devuelve el valor del atributo marca getMarca(). En la clase Main se crea un nuevo objeto de tipo Coche y se muestra por pantalla lo que devuelve el método getMarca(). Si lo ejecutamos, vemos que el valor del atributo es null:
La marca del coche es null
En esta situación, ¿Cómo podemos darle un valor inicial a los atributos de coche1? Crea un nuevo método dentro de la clase Coche que se llame exactamente como la clase (Coche) con el siguiente código:
public class Coche { private String marca, modelo, color; private int numeroBastidor, velocidad; public Coche() { System.out.println("Creando un objeto de la clase Coche"); } public String getMarca(){ return marca; } }
Vuelve a ejecutar la aplicación:
Creando un objeto de la clase Coche La marca del coche es null
¿Por qué el método recién creado se ejecuta automáticamente? Precisamente esa es la labor de los constructores. Son métodos especiales que se ejecutan al crear un objeto. Los constructores en Java tienen que seguir una serie de reglas:
- El nombre tiene que ser el mismo que el de la clase.
- No llevan valor de devolución explícito (ni siquiera hace falta añadir void delante del método).
- No suelen llevar visibilidad (aunque hay ocasiones que es útil añadírsela).
Normalmente, se utilizan los constructores para dar valores iniciales a las propiedades del objeto cuando se crea. Por ejemplo, modifica el constructor de Coche de la siguiente forma:
public Coche(String marca) { System.out.println("Creando un objeto de la clase Coche"); this.marca = marca; }
Si intentas ejecutar la aplicación, verás que te el compilador te muestra un error. ¿Por qué? Porque ahora es obligatorio darle un valor inicial al atributo marca cuando creamos el objeto. Para solucionarlo, cambia la creación del objeto coche1 en la clase Main añadiendo el valor de la marca:
public class Main { public static void main(String[] args) { Coche coche1 = new Coche("Kia"); System.out.println("La marca del coche es " + coche1.getMarca()); } }
Si ejecutamos la aplicación, vemos que ahora sí que muestra el valor de la marca:
Creando un objeto de la clase Coche La marca del coche es Kia
De esta forma, podemos dar valores iniciales a las propiedades de nuestros objetos sin necesidad de usar setters, con lo que podemos hacer que nuestros atributos no cambien a lo largo de la ejecución del programa.
Igual que con los setters y getters, la mayoría de IDEs son capaces de crear los constructores de forma automática. Por ejemplo, en VSC podemos usar Ctrl + . sobre el nombre de la clase y elegir Generate Constructors:
A continuación, nos pedirá que seleccionemos los atributos que queremos incluir en el constructor y VSC creará el método de forma automática.
La palabra reservada this
Fíjate en el código del constructor que acabamos de crear (podemos borrar la línea que muestra por pantalla la frase “Creando un objeto de la clase coche”):
public Coche(String marca) { this.marca = marca; }
¿Qué crees que hace exactamente la línea resaltada en el código anterior: this.marca = marca? ¿Por qué la primera parte de la instrucción es this.marca? Analiza el código: al constructor le pasamos una variable de tipo String que se llama marca, pero esa variable no es la propiedad marca de la clase. De hecho, podríamos cambiarle el nombre y llamarla como nosotros queramos:
public Coche(String marcaDelObjeto) { this.marca = marcaDelObjeto; }
En cualquier caso, lo habitual es crear los constructores con los nombres de las variables iguales a las propiedades de clase. ¿Recuerdas el ámbito de las variables? Esa variable solo existe en esa función y, aunque tenga el mismo nombre que la propiedad marca de la clase Coche no es la misma variable.
Precisamente para poder acceder a la propiedad marca de la clase, usamos la palabra reservada this. this hace referencia a la propia clase, con lo que la variables this.marca estará haciendo referencia a la propiedad de la clase, mientras que marca hará referencia a la variable que le pasamos al constructor.
Si no hay conflicto de nombres, podemos ahorrarnos el uso de this, aunque una buena práctica es usarlo siempre para saber en todo momento a qué estamos haciendo referencia.
this.acelerar(30);
Ejercicios
Ejercicio 1
Haz que las propiedades de las clases sean privadas y crea los getters y setters necesarios para que funcione la aplicación.
Ejercicio 2
Crea constructores en las clases que creas convenientes para poder pasar las propiedades a través de ellos cuando se crea un objeto.
Ejercicio 3
Piensa alguna forma de combinar ambas estrategias anteriores. Las clases donde has creado un constructor con las propiedades, se deben poder crear con sin valores en esas propiedades y utilizar los setters para darles valor (ten en cuenta que se tienen que poder seguir creando pasándoles los valores al constructor).