Saltearse al contenido

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

Clases en Java

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] class Nombre [extends clase] [implements interfaz, ...] {
// Atributos // Métodos
}
  • 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.

Caja.java
public class Caja {
// 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;
}
}

Definición de objeto

Objetos en Java

Elemento identificable que contiene características declarativas (que determinan su estado) y características procedimentales (que modelan su comportamiento).

[Clase] Nombre = new Constructor();

Para crear un objeto se usa el operador new junto con el constructor (Constructor()) de la clase ([Clase]).

Caja x = new Caja(); // Un objeto 'x' de clase 'Caja' con 'valor = O'
Caja y = new Caja(5); // Un objeto 'y' de clase 'Caja' con 'valor = 5'
Caja z = new Caja(5); // Un objeto 'z' de clase 'Caja' con 'valor = 5'

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 ==.

Caja x = new Caja();
x.setValor(7);
Caja y = x;
y.setValor(11);
Cargando diagrama...
Código diagrama
flowchart LR
x
y
subgraph Caja
valor[Valor: 11]
end
x & y --> Caja

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);
Cargando diagrama...
Código diagrama
flowchart LR
x
y
subgraph Caja1[Caja]
valor1[Valor: 11]
end
subgraph Caja2[Caja]
valor2[Valor: 11]
end
x --> Caja1
y --> 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 que y.equals(x).
  • Transitividad: Si x.equals(y) devuelve true y y.equals(z) devuelve true entonces x.equals(z) debe devolver true.
  • 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 devolver false para cualquier valor x no nulo.

Vamos a redefinir mediante @Override (this es el objeto que realiza la comparación):

Caja.java
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.

CajaExtendida.java
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.

Tabla explicando las características explicadas en la anterior descripción

* 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”.

Especificadores de acceso explicado como capas

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 primitivo int y privado. 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.
Cuenta.java
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 tipo Cuenta.

  • 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.

Cliente.java
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;
}
}
Uso de clones
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);
}
}
Objetos inmutables
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 valor
    public final String DNI;
    // El valor debe asignarse en el constructor
    public Persona(String identificador) {
    DNI = identificador;
    }
    }
  • Otros atributos:
    • transient o volatile 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 Java
public class Math {
public static int max(int a, int b) { /* ... */ }
// ...
}
// Nuestro código
System.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.

Caja.java
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.

Calificacion.java
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:

Registros en Java
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.

Registro.java
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.

Registro.java
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
}
}
Pablo Portas López © 2025 licensed under CC BY 4.0