Ampliación: posibilidades al crear clases
En el tema anterior hemos aprendido a crear clases y objetos, utilizar constructores, trabajar con this y utilizar static.
Sin embargo, existen muchas posibilidades a la hora de diseñar una clase.
Por ejemplo:
- No todos los atributos tienen que recibirse mediante el constructor.
- Algunos atributos pueden tener valores iniciales.
- Podemos tener varios constructores.
- Los métodos pueden recibir parámetros o no.
- Los métodos pueden devolver valores o no.
- Podemos controlar quién puede acceder a los atributos.
- Podemos utilizar getters y setters para consultar y modificar los atributos.
Vamos a ver estas posibilidades paso a paso.
No todos los atributos tienen que ser parámetros
En el ejemplo anterior teníamos una clase Persona como esta:
public class Persona {
String nombre;
int edad;
public Persona(String nombre, int edad) {
this.nombre = nombre;
this.edad = edad;
}
}
En este caso, todos los atributos se reciben como parámetros del constructor:
new Persona("Ana", 25);
Pero esto no es obligatorio.
Una clase puede tener atributos que no necesitemos recibir al crear el objeto.
Por ejemplo, podemos añadir un atributo activo:
public class Persona {
String nombre;
int edad;
boolean activo;
public Persona(String nombre, int edad) {
this.nombre = nombre;
this.edad = edad;
}
}
Ahora nombre y edad son parámetros del constructor, pero activo no.
Esto es perfectamente válido. Cuando el problema o enunciado indique que solo el nombre y la edad se definen al crear cualquier Persona, será únicamente esos atributos los que pongamos en el contructor. El resto de atributos simplemente no tendran valor aún.
Valores iniciales
Si un atributo no se recibe mediante el constructor, podemos darle un valor inicial.
Por ejemplo:
public class Persona {
String nombre;
int edad;
boolean activo = true;
public Persona(String nombre, int edad) {
this.nombre = nombre;
this.edad = edad;
}
}
Ahora todas las personas que creemos comenzarán teniendo:
activo → true
Por ejemplo:
Persona persona1 = new Persona("Ana", 25);
Persona persona2 = new Persona("Luis", 30);
Los dos objetos tendrán:
persona1
nombre → Ana
edad → 25
activo → true
y:
persona2
nombre → Luis
edad → 30
activo → true
El valor true es un valor inicial del atributo.
Valores por defecto
En ocasiones queremos que determinados atributos tengan un valor automáticamente si no indicamos nada al crear el objeto.
Por ejemplo, podemos crear una clase Producto:
public class Producto {
String nombre;
double precio;
int stock = 0;
public Producto(String nombre, double precio) {
this.nombre = nombre;
this.precio = precio;
}
}
Podemos crear un producto:
Producto producto = new Producto("Teclado", 35.99);
No hemos indicado ningún valor para stock.
Sin embargo, el objeto tendrá:
nombre → Teclado
precio → 35.99
stock → 0
Esto resulta útil cuando existe un valor inicial lógico para un atributo.
Valores por defecto utilizando constructores
Otra posibilidad consiste en crear diferentes constructores.
Por ejemplo:
public class Producto {
String nombre;
double precio;
int stock;
public Producto(String nombre, double precio) {
this.nombre = nombre;
this.precio = precio;
this.stock = 0;
}
}
Aquí estamos indicando explícitamente que, cuando no proporcionemos el stock, comenzará en 0.
Podemos crear el producto:
Producto producto = new Producto("Teclado", 35.99);
Y automáticamente tendrá:
stock → 0
Varios constructores
Una clase puede tener más de un constructor.
Esto se conoce como sobrecarga de constructores.
Por ejemplo:
public class Producto {
String nombre;
double precio;
int stock;
public Producto(String nombre, double precio) {
this.nombre = nombre;
this.precio = precio;
this.stock = 0;
}
public Producto(String nombre, double precio, int stock) {
this.nombre = nombre;
this.precio = precio;
this.stock = stock;
}
}
Ahora podemos crear objetos de dos formas diferentes.
Sin indicar el stock:
Producto producto1 = new Producto("Teclado", 35.99);
En este caso:
stock → 0
O indicando el stock:
Producto producto2 = new Producto("Ratón", 20.50, 15);
En este caso:
stock → 15
La clase nos ofrece diferentes formas de crear el objeto.
Constructor vacío
También podemos crear un constructor sin parámetros.
public class Persona {
String nombre;
int edad;
public Persona() {
nombre = "Desconocido";
edad = 0;
}
}
Ahora podemos crear una persona sin proporcionar ningún dato:
Persona persona = new Persona();
El objeto tendrá:
nombre → Desconocido
edad → 0
Esto puede ser útil cuando queremos crear primero el objeto y completar sus datos posteriormente.
Métodos sin parámetros
No todos los métodos necesitan recibir información.
Por ejemplo:
public void saludar() {
System.out.println("Hola");
}
Podemos llamarlo así:
persona.saludar();
Este método no necesita ningún parámetro.
Métodos con parámetros
Un método también puede recibir información.
Por ejemplo:
public void saludarOtraPersona(String nombre) {
System.out.println("Hola " + nombre);
}
Podemos utilizarlo:
saludarOtraPersona("Esther");
Salida:
Hola Esther
En este caso:
String nombre
es el parámetro del método.
Métodos que devuelven valores
Un método puede realizar una operación y devolver un resultado.
Por ejemplo:
public int sumar(int numero1, int numero2) {
return numero1 + numero2;
}
Podemos utilizarlo:
int resultado = sumar(5, 3);
System.out.println(resultado);
Salida:
8
El método:
public int sumar(...)
indica que devuelve un int.
Métodos void
Si un método no necesita devolver ningún valor, utilizamos void.
Por ejemplo:
public void mostrarNombre() {
System.out.println(nombre);
}
Este método realiza una acción, pero no devuelve ningún resultado.
Podemos llamarlo:
persona.mostrarNombre();
Métodos con varios parámetros
Un método puede recibir tantos parámetros como necesitemos.
Por ejemplo:
public void mostrarProducto(String nombre, double precio, int stock) {
System.out.println("Nombre: " + nombre);
System.out.println("Precio: " + precio);
System.out.println("Stock: " + stock);
}
Podemos llamarlo:
mostrarProducto("Teclado", 35.99, 10);
Aunque debemos evitar crear métodos con demasiados parámetros si hacen que el código resulte difícil de entender.
Modificadores de acceso
Hasta ahora cuando definíamos los atributos al inicio de la clase lo hacíamos así:
public class Persona {
String nombre;
int edad;
}
De esta manera podíamos acceder luego desde el main a sus atributos utilizando el punto .:
Persona persona1 = new Persona("Maria", 26);
persona1.nombre;
Sin embargo esto es solo una simplificación. Lo normal será que la creación de atributos vaya precedido por su "acceso". Los más habituales serán:
private String nombre;
y:
public String nombre;
Estos son modificadores de acceso.
Permiten controlar desde dónde se puede acceder a una parte de una clase, en este caso, a los atributos.
private
Cuando un atributo es private, solo se puede acceder directamente desde la propia clase.
Por ejemplo:
public class Persona {
private String nombre;
}
Desde Main no podremos hacer directamente:
persona.nombre = "Ana";
El atributo está protegido por private.
Esto permite controlar cómo se modifica la información de los objetos. En este caso, prohibiendo que se modifique directamente desde el main. Más abajo veremos cómo hacemos entoces para acceder a los atributos de la clase.
public
Cuando algo es public, puede utilizarse desde otras clases.
Por ejemplo:
public void saludar() {
System.out.println("Hola");
}
Podemos utilizarlo desde Main:
persona.saludar();
Poner public viene a ser lo mismo que no poner nada.
Getters
Cuando utilizamos atributos private, normalmente necesitamos alguna forma de consultar sus valores.
Para ello utilizamos los getters.
Un getter es un método que devuelve el valor de un atributo privado, ya que como hemos visto no podremos utilizar el punto .directamente.
Los getters seguiran siempre la misma estructura:
public tipo getAtributo(){
return atributo;
}
Por ejemplo:
public class Persona {
private String nombre;
private int edad;
public String getNombre() {
return nombre;
}
public int getEdad() {
return edad;
}
}
Ahora podemos consultar los valores desde fuera de la clase a través del método getter:
Persona persona = new Persona("Ana", 25);
System.out.println(persona.getNombre());
System.out.println(persona.getEdad());
Salida:
Ana
25
El atributo continúa siendo private.
La única forma de acceder a él desde fuera es mediante el método público.
Setters
Los setters permiten modificar el valor de un atributo.
Su estructura es:
public void setAtributo(tipo atributo){
this.atributo = atributo;
}
Por ejemplo:
public void setNombre(String nombre) {
this.nombre = nombre;
}
Y:
public void setEdad(int edad) {
this.edad = edad;
}
Ahora podemos modificar los datos:
persona.setNombre("Luis");
persona.setEdad(30);
Getters y setters juntos
Una clase puede tener getters y setters para sus atributos.
public class Persona {
private String nombre;
private int edad;
public Persona(String nombre, int edad) {
this.nombre = nombre;
this.edad = edad;
}
public String getNombre() {
return nombre;
}
public void setNombre(String nombre) {
this.nombre = nombre;
}
public int getEdad() {
return edad;
}
public void setEdad(int edad) {
this.edad = edad;
}
}
Desde Main:
Persona persona = new Persona("Ana", 25);
System.out.println(persona.getNombre());
persona.setNombre("Luis");
System.out.println(persona.getNombre());
Salida:
Ana
Luis
¿Por qué utilizar getters y setters?
Podríamos pensar:
Si podemos acceder directamente a los atributos, ¿para qué necesitamos getters y setters?
La ventaja es que podemos controlar qué ocurre cuando consultamos o modificamos un atributo.
Por ejemplo, no queremos permitir edades negativas.
Podemos controlar el valor desde el setter:
public void setEdad(int edad) {
if (edad >= 0) {
this.edad = edad;
}
}
Ahora:
persona.setEdad(25);
funciona correctamente.
Pero:
persona.setEdad(-10);
no modificará el valor.
De esta forma, la clase puede proteger sus propios datos.
Getters sin setters
No todos los atributos necesitan tener getter y setter.
Por ejemplo, podemos querer que un dato pueda consultarse pero no modificarse directamente.
public class Producto {
private String codigo;
public String getCodigo() {
return codigo;
}
}
En este caso podemos hacer:
producto.getCodigo();
pero no existe:
producto.setCodigo(...);
Esto significa que el código puede consultarse, pero no modificarse mediante un setter.
Atributos que no deberían modificarse
En algunos casos queremos que un valor no cambie después de crear el objeto.
Para ello podemos utilizar final.
Por ejemplo:
public class Producto {
private final String codigo;
public Producto(String codigo) {
this.codigo = codigo;
}
}
Ahora codigo debe establecerse cuando se crea el objeto y no puede cambiar posteriormente.
Podemos consultar su valor:
public String getCodigo() {
return codigo;
}
Pero no tendría sentido crear un setter:
setCodigo(...)
porque el atributo es final.
Validar datos dentro de los setters
Los setters son especialmente útiles para validar información.
Por ejemplo, una cuenta bancaria podría tener un saldo:
public class Cuenta {
private double saldo;
public void setSaldo(double saldo) {
if (saldo >= 0) {
this.saldo = saldo;
}
}
}
De esta manera impedimos establecer directamente un saldo negativo.
También podemos utilizar métodos específicos para modificar el estado del objeto.
Por ejemplo:
public void ingresar(double cantidad) {
if (cantidad > 0) {
saldo += cantidad;
}
}
Y:
public void retirar(double cantidad) {
if (cantidad > 0 && cantidad <= saldo) {
saldo -= cantidad;
}
}
Esto puede ser incluso más apropiado que proporcionar un setter para saldo.
En lugar de:
cuenta.setSaldo(500);
podemos utilizar:
cuenta.ingresar(500);
La propia clase controla qué operaciones son válidas.
Encapsulación
Todo esto nos lleva a un concepto importante de la Programación Orientada a Objetos: la encapsulación.
La encapsulación consiste en proteger los datos internos de una clase y controlar cómo se accede a ellos.
Una forma habitual de hacerlo es:
Atributos
↓
private
↓
No se acceden directamente desde fuera
↓
Métodos públicos
↓
getters / setters / otros métodos
Por ejemplo:
public class Cuenta {
private double saldo;
public double getSaldo() {
return saldo;
}
public void ingresar(double cantidad) {
if (cantidad > 0) {
saldo += cantidad;
}
}
}
Desde fuera podemos consultar el saldo:
cuenta.getSaldo();
y realizar operaciones:
cuenta.ingresar(100);
Pero no podemos modificar directamente:
cuenta.saldo = -500;
Esto permite que la propia clase controle sus datos.