Tema 2: Elementos básicos
Análisis de los elementos fundamentales de la orientación a objetos, incluyendo clases, objetos, identidad, estado y comportamiento, con un enfoque práctico en la implementación en Java mediante el uso de atributos, métodos y constructores.
1. Clases y Objetos
Definición de clase
Una clase es una plantilla que describe la estructura y el comportamiento de un tipo de objeto y permite la creación de los mismos.
- Modificadores:
public
: Permite que la clase sea accesible desde otro paquete (no confundir con los especificadores de visibilidad de atributos).abstract
: Define clases que no pueden instanciarse.final
: Define clases que no pueden extenderse con subclases.
- Cláusula
extends
:- Define la superclase de la clase actual (por defecto Object).
- Cláusula
implements
:- Define los interfaces que implementa la clase.
Los elementos abstract
, final
, extends
e implements
se explican en el Tema 3.
A lo largo del tema se utilizará esta clase Caja
como ejemplo.
Definición de objeto
Elemento identificable que contiene características declarativas (que determinan su estado) y características procedimentales (que modelan su comportamiento).
Para crear un objeto se usa el operador new
junto con el constructor (Constructor()
) de la clase ([Clase]
).
2. Identidad de Objetos
La identidad es la propiedad de un objeto que lo distingue de todos los demás de forma independiente al valor de sus atributos.
Identidad vs. Igualdad
Identidad: Dos objetos son idénticos si y solo si son el mismo objeto (es decir, tienen un mismo OID).
Comparación de identidad: mediante el operador
==
.
Igualdad: Dos objetos se consideran iguales si, a pesar de ser objetos distintos (con distinto OID), sus atributos son idénticos.
Comparación de igualdad: mediante el método
equals
.
Caja x = new Caja();x.setValor(11);Caja y = new Caja();y.setValor(11);
Código diagrama
flowchart LRxysubgraph Caja1[Caja] valor1[Valor: 11]endsubgraph Caja2[Caja] valor2[Valor: 11]endx --> Caja1y --> Caja2
Equals
Para crear un método equals correcto debemos cumplir el siguiente contrato:
- Reflexividad:
x.equals(x)
debe devolver true. - Simetría:
x.equals(y)
debe devolver lo mismo quey.equals(x)
. - Transitividad: Si
x.equals(y)
devuelve true yy.equals(z)
devuelve true entoncesx.equals(z)
debe devolvertrue
. - Consistencia: Si no se modifica el estado de los objetos que se usa para la comparación repetidas llamadas a
x.equals(y)
deberían responder siempre lo mismo. - Uso de nulos:
x.equals(null)
debe devolverfalse
para cualquier valor x no nulo.
Vamos a redefinir mediante @Override
(this
es el objeto que realiza la comparación):
public class Caja {19 collapsed lines
// Atributos - Estado private int valor;
// Métodos - Comportamiento public void setValor(int v) { valor = v; } public int getValor() { return valor; }
// Constructores public Caja() { valor = 0; } public Caja(int v) { valor = v; }
@Override public boolean equals(Object obj) {
if (this == obj) { return true; }
if (obj == null) { return false; }
if (getClass() != obj.getClass()) { return false; }
Caja caja = (Caja) obj; return this.valor == caja.valor; }10 collapsed lines
public static void main(String[] args) { Caja x = new Caja(); x.setValor(11); Caja y = new Caja(); y.setValor(11); System.out.println(x == y); // Resultado esperado: false System.out.println(x.equals(y)); // Resultado esperado: true }
}
hashCode
Es una función que mapea datos de un tamaño arbitrario en datos de un tamaño fijo.
Si dos objetos son iguales, de acuerdo con el método equals(Object)
, entonces llamar al método hashCode()
sobre ambos objetos debe producir el mismo resultado.
A partir de Java 7 existe un método en Objects
llamado hash()
, capaz de calcular de forma consistente valores hashcode con los datos que le pasemos.
En el siguiente ejemplo, acordamos que dos Cajas Extendidas serán iguales, si y solo si, todas sus variables internas son iguales. Por lo tanto redefinimos tanto equals
como hashCode
.
public class CajaExtendida { private int valorEntero; private char valorChar; private double valorDouble; private boolean valorBoolean; private String valorString;
@Override public boolean equals(Object obj) {6 collapsed lines
if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; }
CajaExtendida caja = (CajaExtendida) obj; return (this.valorEntero == caja.valorEntero) && (this.valorChar == caja.valorChar) && (this.valorDouble == caja.valorDouble) && (this.valorBoolean == caja.valorBoolean)
&& (Objects.equals(this.valorString, caja.valorString)); }
@Override public int hashCode() { return Objects.hash(valorEntero, valorChar, valorDouble, valorBoolean, valorString); }}
3. Estado de Objetos
Definición de Atributos
[EspecificadorAcceso][Modificadores] tipo nombreAtributo [= valorInicial];
- Especificador de Acceso: Definen desde dónde podemos acceder a ese atributo.
- Modificadores: Permiten definir atributos de clase y atributos constantes.
- Valor inicial:
- Todos los atributos se inicializan automáticamente al valor cero (si es numérico), false (si es booleano) o null (si es un objeto).
- En la definición se puede definir, si se considera necesario, un valor inicial distinto a dicho atributo.
Especificadores de Acceso
- Público (
public
): Los atributos son visibles para todas las clases. - Paquete (no se indica especificador)
- Es el que se aplica si no se especifica ningún especificador
- Permite que el atributo sea visible a todas las clases que se sitúan en el mismo paquete que la clase original.
- Protegido (
protected
)- Permite que el atributo sea visible a las subclases de la clase original
- También permite el acceso a las clases que se encuentren en el mismo paquete (a diferencia de otros leng. de programación).
- Privado (
private
): Los atributos sólo son visibles dentro de la propia clase.
* En protected
se puede acceder desde una subclase (otro paquete) solo desde objetos declarados del tipo de la subclase no de la superclase.
¿Porque fue diseñado así? Probablemente para hacerlos contenidos unos en otros. Lo cual provoca que la visibilidad “protegida” sea la más permisiva después de “pública”.
Ortodoxia de la Orientación a Objetos: Los atributos deben declararse privados y su acceso sólo debe ser posible a través de métodos públicos de lectura/escritura.
class Clase { private int valor; // La propiedad se define privada
public int getValor() { // Método de lectura return valor; }
public void setValor(int valor) { // Método de escritura this.valor=valor; }}
Acceso a objetos privados mutables
Se crea una clase Cuenta, para guardar y interactuar con una cuenta bancaria.
- Se define un atributo
balance
de tipo primitivoint
yprivado
. Por lo tanto no es accesible desde fuera de su clase - Se define un constructor y métodos de lectura y escritura para modificar el balance.
class Cuenta { // Atributos private int balance = 0;
// Métodos constructores public Cuenta(int cantidad) { balance = cantidad; }
// Métodos de lectura y escritura public int getBalance() { return balance; } public void retirada(int cantidad) { balance = balance - cantidad; } public void ingreso(int cantidad) { balance = balance + cantidad; }}
-
La clase
Cliente
se define con dos atributos privados, uno de ellos de tipoCuenta
. -
Definimos un constructor y métodos de lectura pero no permitimos el acceso al atributo privado de tipo
Cuenta
desde fuera de esos métodos.
class Cliente { // Atributos private String nombre; private Cuenta cuenta;
// Métodos constructores public Cliente(String n, int c) { nombre = n; cuenta = new Cuenta(c); }
// Métodos de acceso public String getNombre() { return nombre; } public Cuenta getCuenta() { return cuenta; }}
class Cuenta {3 collapsed lines
// Atributos private int balance = 0;
// Métodos constructor public Cuenta(int cantidad) { balance = cantidad; }
// Método constructor de copia public Cuenta (Cuenta c) { this.balance = c.balance; }13 collapsed lines
// Métodos de lectura y escritura public int getBalance() { return balance; }
public void retirada(int cantidad) { balance = balance - cantidad; }
public void ingreso(int cantidad) { balance = balance + cantidad; }}
class Cliente {15 collapsed lines
// Atributos private String nombre; private Cuenta cuenta;
// Métodos constructores public Cliente(String n, int c) { nombre = n; cuenta = new Cuenta(c); }
// Métodos de acceso public String getNombre() { return nombre; }
public Cuenta getCuenta() { return cuenta;
return new Cuenta (cuenta); }}
class Cuenta { // Atributos private int balance = 0;
// Métodos constructores public Cuenta(int cantidad) { balance = cantidad; }
// Métodos de lectura y escritura public int getBalance() { return balance; }
public void retirada(int cantidad) { balance = balance - cantidad; } public void ingreso(int cantidad) { balance = balance + cantidad; }}
Modificadores de Atributos
static
:- Los atributos pertenecen a la clase y no a una instancia en particular.
- Pueden ser modificados sin que exista una instancia creada de la clase.
Persona.java public class Persona {public int edad;public static int edadVoto = 18;public static void main (String[] args) {Persona p1 = new Persona();System.out.println("Edad de voto = " + Persona.edadVoto);System.out.println("Edad de voto = " + p1.edadVoto);}}final
:- Atributos constantes.
- Una vez que le hemos asignado un valor no es posible cambiarlo.
Constante de clase public class Circulo {public int radio;public static final double PI = 3.1416;public Circulo(int r) {radio = r;}public double areaCirculo() {return PI * radio * radio;}}Blank Finals (constantes de instancia) public class Persona {// Definimos la constante, pero no le damos valorpublic final String DNI;// El valor debe asignarse en el constructorpublic Persona(String identificador) {DNI = identificador;}}- Otros atributos:
transient
ovolatile
son particulares de Java y menos usados.
4. Comportamiento de Objetos
Definición de Métodos
[EspecificadorAcceso][Modificadores] tipoRetorno nombreMetodo ([parametro1, parametro2, ...]) { cuerpoMetodo }
- Especificadores de Acesso: Iguales que los atributos
- Modificadores: Permiten definir métodos de clase (
static
), métodos diseñados para ser sobrescritos (abstract
), métodos que no pueden sobrescribirse (final
), etc.
En Java los atributos ensombrecen a los atributos dentro de los métodos. Para evitar conflictos se emplea el puntero this
:
public class Parametros { private int valor; public void setValor(int valor) { this.valor = valor; }}
Modificadores de Métodos
static
: Métodos de clase.abstract
:- Métodos no definidos destinados a ser implementados por una subclase.
- Se tratarán con más detalle al hablar de la herencia: Tema 3.
final
:- Evita que un método sea sobreescrito
- Si una clase es final todos sus métodos son implícitamente final.
- Se tratarán con ms ádetalle al hablar del polimorfismo: Tema 3.
Métodos de clase (static
)
Són útiles cuando no se necesita acceder a al estado del objetos. Un ejemplo es en la API de Java, en la libería Math
el método max
.
// API de Javapublic class Math { public static int max(int a, int b) { /* ... */ } // ...}// Nuestro códigoSystem.out.println("El mayor de " + a + " y " + b + ": " + Math.max(a,b));
Métodos Constructores
Son los métodos que se utilizan para crear e inicializar instancias.
public class Caja { private int valor; public void setValor(int v) {valor = v;} public int getValor() { return valor; } // Método constructor public Caja(int valor) { this.valor = valor; } public static void main(String[] args) { Caja c = new Caja(5); System.out.println("Valor = " + c.getValor()); }}
En la creación de un objeto hay dos fases diferentes:
new
: Creación del objeto en el montículo.Caja(5)
: Inicialización del objeto usando un constructor.
Caja c = new Caja(5);
¿Tiene sentido utilizar constructores private
?
Pues si. Un constructor privado permite controlar la creación de instancias. Nadie podrá crear objetos usando new
, fuera de tu clase.
Tiene utilidad a la hora de usar distintos patrones como enumerados, singleton, etc.
Tipos de Enumerados
Es un tipo de datos que consiste en un conjunto de valores con nombre llamados elementos que se comportan como constantes en el lenguaje.
Podemos simplificar el enum (Cumpliendo el TypeSafe enum) de la siguiente manera.
enum Calificacion { MATRICULA(10), SOBRESALIENTE(9), NOTABLE(7), APROBADO(5), SUSPENSO(0), NO_PRESENTADO(0);
private int valor; public int getValor() { return valor; }
Calificacion(int valor) { this.valor = valor; }}
Registros
Un registro es un tipo de clase restringida que se crea de forma sencilla y eficiente para actuar como portadora de datos.
Los registros añaden automáticamente automáticamente varios métodos:
public record Caja(int valor) { }// ...public class UsaCaja { public static void main(String[] args) { Caja c1 = new Caja(5); // llamadas al constructor canónico Caja c2 = new Caja(5); System.out.println(c1.valor()); // llamada a "valor()" System.out.println(c1); // llamada a "toString()" System.out.println(c1.equals(c2)); // llamada a "equals(...)" // También se implementan los métodos equals y hashCode }}
Registros y constructores
Podemos modificar el constructor canónico mediante el constructor compacto (obtienes los parámetros de la declaración del registro, en este caso solamente valor
).
Podemos añadir otros constructores pero tendremos que hacer obligatoriamente referencia al canónico.
public record Registro(int valor) { public Registro { // Constructor compacto if (valor < 0) // Cambia el constructor canónico throw new IllegalArgumentException(); } public Registro(String numero) { this(Integer.decode(numero)); // Llamada al constructor canónico } public static void main(String[] args) { Registro r1 = new Registro("1"); System.out.println("r1 = " + r1); // r1 = Registro[valor=1]
Registro r2 = new Registro(-3); // throws IllegalArgumentException }}
Registros y métodos
Es posible añadir a los registros atributos estáticos (no de instancia) y métodos de instancia o estáticos si fuera necesario. También podemos darle una nueva implementación a los métodos de lectura y/o escritura.
public record Registro(int valor) {
public static final int sentidoVida = 69;
public int valorAlCuadrado() { return valor * valor; }
public static int getSentidoVida() { return sentidoVida; }
public int valor() { return valor * 2; } public static void main(String[] args) { Registro c = new Registro(5); System.out.println(r.valorAlCuadrado()); // 25 System.out.println(Registro.getSentidoVida()); // 69 System.out.println(r.valor()); // 10 }}