Java

Tutorial Java: Clases y objetos

Java clases y objetos: creación y uso. Aprende a crear y usar clases y objetos

Aprende Java y certifícate

Introducción a la programación orientada a objetos: qué es y para qué sirve

La programación orientada a objetos (POO) revolucionó la forma en que se diseñan y construyen aplicaciones. A diferencia de la programación procedural, donde el código se organiza en procedimientos y funciones que manipulan datos, la POO se centra en los objetos como elementos principales.

En este enfoque, se modela el software en términos de objetos que interactúan entre sí, similar a cómo percibimos el mundo real. Cada objeto representa una entidad con características (atributos) y comportamientos (métodos), permitiendo crear sistemas más cercanos a la realidad.

Fundamentos de la POO

La programación orientada a objetos se sustenta en cuatro pilares fundamentales:

  • Encapsulación: Se refiere a la capacidad de ocultar los detalles internos de un objeto y exponer solo lo necesario. Esto se logra mediante el control de acceso a los atributos y métodos, utilizando modificadores como public, private y protected.
public class CuentaBancaria {
    private double saldo; // Atributo encapsulado
    
    public double obtenerSaldo() {
        return saldo;
    }
    
    public void depositar(double cantidad) {
        if (cantidad > 0) {
            saldo += cantidad;
        }
    }
}
  • Herencia: Permite que una clase (subclase) adquiera las propiedades y comportamientos de otra clase (superclase). Esto facilita la reutilización de código y establece relaciones jerárquicas entre clases.
public class Vehículo {
    protected String marca;
    
    public void arrancar() {
        System.out.println("Vehículo arrancado");
    }
}

public class Automóvil extends Vehículo {
    private int numeroPuertas;
    
    @Override
    public void arrancar() {
        System.out.println("Automóvil arrancado");
    }
}
  • Polimorfismo: Permite que objetos de diferentes clases respondan al mismo mensaje o método de diferentes maneras. Esto proporciona flexibilidad al código.
Vehículo miVehículo = new Automóvil();
miVehículo.arrancar(); // Ejecuta el método de Automóvil
  • Abstracción: Consiste en identificar las características esenciales de un objeto, ignorando los detalles irrelevantes. Se implementa mediante clases abstractas e interfaces que definen comportamientos sin especificar su implementación completa.

Ventajas de la POO

La programación orientada a objetos ofrece beneficios:

  • Modularidad: El código se organiza en unidades independientes (clases) que pueden desarrollarse, probarse y mantenerse por separado.
  • Reutilización: A través de la herencia y la composición, se pueden reutilizar componentes existentes para crear nuevas funcionalidades, reduciendo la duplicación de código.
  • Mantenibilidad: La estructura organizada y la encapsulación hacen que el código sea más fácil de entender y modificar. Los cambios en una parte del sistema tienen un impacto limitado en otras partes.
  • Escalabilidad: Los sistemas orientados a objetos pueden crecer y evolucionar con más facilidad, permitiendo agregar nuevas funcionalidades sin alterar el código existente.

POO en Java

Java es un lenguaje fundamentalmente orientado a objetos que implementa todos los conceptos de la POO de manera rigurosa. En Java, casi todo se trata como un objeto (excepto los tipos primitivos), y cada objeto es una instancia de una clase.

// Definición de una clase en Java
public class Persona {
    // Atributos
    private String nombre;
    private int edad;
    
    // Constructor
    public Persona(String nombre, int edad) {
        this.nombre = nombre;
        this.edad = edad;
    }
    
    // Métodos
    public void saludar() {
        System.out.println("Hola, me llamo " + nombre);
    }
    
    public int obtenerEdad() {
        return edad;
    }
}

// Creación y uso de objetos
public class Aplicación {
    public static void main(String[] args) {
        Persona persona1 = new Persona("Ana", 25);
        persona1.saludar();
        
        Persona persona2 = new Persona("Carlos", 30);
        System.out.println("La edad de Carlos es: " + persona2.obtenerEdad());
    }
}

Aplicaciones prácticas

La POO se utiliza en diversos contextos de desarrollo:

  • Aplicaciones empresariales: Sistemas de gestión, aplicaciones financieras y plataformas de comercio electrónico.
  • Desarrollo de interfaces gráficas: Los frameworks de UI como JavaFX o Swing utilizan objetos para representar componentes visuales y gestionar eventos.
  • Videojuegos: Los personajes, escenarios y mecánicas de juego se modelan como objetos con propiedades y comportamientos específicos.
  • Simulaciones: Sistemas que modelan fenómenos del mundo real aprovechan la capacidad de la POO para representar entidades complejas y sus interacciones.

Diseño orientado a objetos

Para aprovechar al máximo la POO, se han desarrollado principios de diseño que guían la creación de sistemas robustos y flexibles:

  • Principio de responsabilidad única: Una clase debe tener una sola razón para cambiar, es decir, debe tener una única responsabilidad.
// Mal diseño: clase con múltiples responsabilidades
public class Usuario {
    private String nombre;
    
    public void guardarEnBaseDeDatos() { /* ... */ }
    public void enviarEmail() { /* ... */ }
    public void generarInforme() { /* ... */ }
}

// Mejor diseño: separación de responsabilidades
public class Usuario {
    private String nombre;
    // Solo métodos relacionados con el usuario
}

public class RepositorioUsuarios {
    public void guardar(Usuario usuario) { /* ... */ }
}

public class ServicioEmail {
    public void enviarA(Usuario usuario) { /* ... */ }
}
  • Principio abierto/cerrado: Las entidades de software deben estar abiertas para extensión pero cerradas para modificación.
  • Principio de sustitución de Liskov: Los objetos de una subclase deben poder sustituir a los objetos de la superclase sin afectar la corrección del programa.
  • Principio de segregación de interfaces: Es mejor tener muchas interfaces específicas que una interfaz general.
  • Principio de inversión de dependencias: Se debe depender de abstracciones, no de implementaciones concretas.

Patrones de diseño

Los patrones de diseño son soluciones probadas a problemas comunes en el desarrollo de software orientado a objetos. Algunos patrones populares incluyen:

  • Singleton: Garantiza que una clase tenga una única instancia y proporciona un punto de acceso global a ella.
public class ConfiguraciónSistema {
    private static ConfiguraciónSistema instancia;
    
    private ConfiguraciónSistema() {
        // Constructor privado
    }
    
    public static ConfiguraciónSistema obtenerInstancia() {
        if (instancia == null) {
            instancia = new ConfiguraciónSistema();
        }
        return instancia;
    }
}
  • Factory: Proporciona una interfaz para crear objetos en una superclase, pero permite a las subclases alterar el tipo de objetos que se crearán.
  • Observer: Define una dependencia uno-a-muchos entre objetos, de modo que cuando un objeto cambia de estado, todos sus dependientes son notificados automáticamente.
  • Strategy: Define una familia de algoritmos, encapsula cada uno y los hace intercambiables, permitiendo que el algoritmo varíe independientemente de los clientes que lo utilizan.

Clases en Java

Las clases constituyen el elemento fundamental de la programación orientada a objetos. Una clase se puede entender como un plano o plantilla que define la estructura y comportamiento que tendrán los objetos creados a partir de ella. En esencia, una clase encapsula datos (atributos) y comportamientos (métodos) relacionados en una única unidad lógica.

En Java, se define una clase utilizando la palabra clave class seguida del nombre de la clase y un bloque de código delimitado por llaves. El nombre de la clase debe seguir las convenciones de nomenclatura de Java, comenzando con letra mayúscula y utilizando notación CamelCase.

public class Coche {
    // Atributos (variables de instancia)
    private String marca;
    private String modelo;
    private int año;
    private double velocidad;
    
    // Métodos
    public void acelerar(double incremento) {
        velocidad += incremento;
    }
    
    public void frenar(double decremento) {
        if (velocidad >= decremento) {
            velocidad -= decremento;
        } else {
            velocidad = 0;
        }
    }
}

Estructura de una clase

Una clase en Java típicamente contiene los siguientes elementos:

  • Modificadores de acceso: Determinan la visibilidad de la clase. Los más comunes son:
    • public: Accesible desde cualquier parte del programa
    • default (sin modificador): Accesible solo dentro del mismo paquete
    • final: No puede ser extendida por otras clases
    • abstract: No puede ser instanciada directamente
  • Atributos: Son las variables que almacenan el estado de los objetos. Se recomienda declararlos como private para mantener la encapsulación.
public class Estudiante {
    private String nombre;
    private int edad;
    private double promedio;
    private boolean aprobado;
}
  • Métodos: Definen el comportamiento de los objetos. Pueden ser:
    • Métodos de acceso (getters): Permiten obtener el valor de los atributos
    • Métodos modificadores (setters): Permiten modificar el valor de los atributos
    • Métodos de comportamiento: Implementan la funcionalidad específica de la clase
public class CuentaBancaria {
    private double saldo;
    
    // Getter
    public double getSaldo() {
        return saldo;
    }
    
    // Método de comportamiento
    public void depositar(double monto) {
        if (monto > 0) {
            saldo += monto;
        }
    }
    
    public boolean retirar(double monto) {
        if (monto > 0 && saldo >= monto) {
            saldo -= monto;
            return true;
        }
        return false;
    }
}
  • Constructores: Métodos especiales que se ejecutan cuando se crea un objeto. Se utilizan para inicializar los atributos.
public class Producto {
    private String nombre;
    private double precio;
    
    // Constructor por defecto
    public Producto() {
        nombre = "Sin nombre";
        precio = 0.0;
    }
    
    // Constructor con parámetros
    public Producto(String nombre, double precio) {
        this.nombre = nombre;
        this.precio = precio;
    }
}

Miembros estáticos

Las clases en Java pueden contener miembros estáticos (atributos y métodos) que pertenecen a la clase en sí, no a instancias específicas. Se declaran con la palabra clave static.

  • Atributos estáticos: Son compartidos por todas las instancias de la clase. Se utilizan para representar propiedades comunes a todos los objetos.
public class Contador {
    private static int totalInstancias = 0;
    private int id;
    
    public Contador() {
        totalInstancias++;
        id = totalInstancias;
    }
    
    public static int getTotalInstancias() {
        return totalInstancias;
    }
    
    public int getId() {
        return id;
    }
}
  • Métodos estáticos: Se pueden invocar directamente desde la clase, sin necesidad de crear un objeto. No pueden acceder a miembros no estáticos.
public class Matemáticas {
    public static double calcularÁreaCirculo(double radio) {
        return Math.PI * radio * radio;
    }
    
    public static int máximo(int a, int b) {
        return (a > b) ? a : b;
    }
}

// Uso:
double área = Matemáticas.calcularÁreaCirculo(5.0);
int max = Matemáticas.máximo(10, 20);

Clases anidadas

Java permite definir clases dentro de otras clases, lo que se conoce como clases anidadas. Estas pueden ser:

  • Clases internas: Definidas dentro de otra clase y asociadas a una instancia de la clase externa.
public class Externa {
    private int valor = 10;
    
    public class Interna {
        public void mostrar() {
            System.out.println("Valor desde clase interna: " + valor);
        }
    }
    
    public void crearInterna() {
        Interna interna = new Interna();
        interna.mostrar();
    }
}

// Uso:
Externa externa = new Externa();
Externa.Interna interna = externa.new Interna();
  • Clases estáticas anidadas: Similar a las clases internas, pero declaradas como static. No tienen acceso a los miembros no estáticos de la clase externa.
public class Exterior {
    private static int dato = 100;
    
    public static class Anidada {
        public void imprimir() {
            System.out.println("Dato desde clase anidada: " + dato);
        }
    }
}

// Uso:
Exterior.Anidada anidada = new Exterior.Anidada();

Clases abstractas

Una clase abstracta se define con la palabra clave abstract y no puede ser instanciada directamente. Se utiliza como base para otras clases que la extenderán.

public abstract class Figura {
    protected String color;
    
    public Figura(String color) {
        this.color = color;
    }
    
    // Método abstracto (sin implementación)
    public abstract double calcularÁrea();
    
    // Método concreto (con implementación)
    public String getColor() {
        return color;
    }
}

// Clase concreta que extiende la clase abstracta
public class Círculo extends Figura {
    private double radio;
    
    public Círculo(String color, double radio) {
        super(color);
        this.radio = radio;
    }
    
    @Override
    public double calcularÁrea() {
        return Math.PI * radio * radio;
    }
}

Clases finales

Una clase final se declara con la palabra clave final y no puede ser extendida por otras clases. Se utiliza cuando se quiere evitar que una clase sea modificada.

public final class Constantes {
    public static final double PI = 3.14159265359;
    public static final double E = 2.71828182846;
    
    // Métodos utilitarios
    public static double convertirRadianesAGrados(double radianes) {
        return radianes * 180 / PI;
    }
}

Clases de utilidad

Las clases de utilidad contienen métodos estáticos que proporcionan funcionalidades comunes. Por convención, estas clases suelen tener un constructor privado para evitar su instanciación.

public class StringUtils {
    // Constructor privado para evitar instanciación
    private StringUtils() {
        throw new AssertionError("Esta clase no debe ser instanciada");
    }
    
    public static boolean esVacíoONulo(String str) {
        return str == null || str.trim().isEmpty();
    }
    
    public static String invertir(String str) {
        if (str == null) return null;
        return new StringBuilder(str).reverse().toString();
    }
}

Buenas prácticas al definir clases

Al diseñar clases en Java, se recomienda seguir estas prácticas:

  • Encapsulación: Declarar los atributos como private y proporcionar métodos de acceso controlado.
  • Responsabilidad única: Cada clase debe tener una única responsabilidad bien definida.
  • Cohesión: Los miembros de una clase deben estar relacionados entre sí.
  • Inmutabilidad: Cuando sea posible, diseñar clases inmutables (cuyos objetos no pueden cambiar después de crearse).
// Ejemplo de clase inmutable
public final class Punto {
    private final int x;
    private final int y;
    
    public Punto(int x, int y) {
        this.x = x;
        this.y = y;
    }
    
    public int getX() {
        return x;
    }
    
    public int getY() {
        return y;
    }
    
    // En lugar de modificar, crea un nuevo objeto
    public Punto trasladar(int deltaX, int deltaY) {
        return new Punto(x + deltaX, y + deltaY);
    }
}

Organización de clases en paquetes

En Java, las clases se organizan en paquetes que actúan como espacios de nombres. Los paquetes ayudan a evitar conflictos de nombres y a organizar el código.

// Declaración del paquete
package com.miempresa.aplicacion.modelo;

// Importaciones
import java.util.ArrayList;
import java.util.List;

// Definición de la clase
public class Cliente {
    // Contenido de la clase
}

Para utilizar una clase de otro paquete, se debe importar explícitamente:

// Importar una clase específica
import com.miempresa.aplicacion.modelo.Cliente;

// Importar todas las clases de un paquete
import com.miempresa.aplicacion.modelo.*;

El método constructor

El constructor es un método especial dentro de una clase Java que se ejecuta automáticamente cuando se crea un nuevo objeto. Su principal función es inicializar los atributos del objeto y prepararlo para su uso.

Un constructor se distingue por tener el mismo nombre que la clase y no especificar ningún tipo de retorno, ni siquiera void. Cuando se utiliza el operador new para crear un objeto, se invoca automáticamente al constructor correspondiente.

public class Persona {
    private String nombre;
    private int edad;
    
    // Constructor
    public Persona(String nombre, int edad) {
        this.nombre = nombre;
        this.edad = edad;
    }
}

// Creación de un objeto usando el constructor
Persona persona = new Persona("Ana", 25);

Tipos de constructores

En Java se pueden definir diferentes tipos de constructores según las necesidades:

  • Constructor por defecto: Si no se define ningún constructor en la clase, Java proporciona automáticamente un constructor sin parámetros (constructor por defecto). Este constructor inicializa los atributos con valores predeterminados (0 para tipos numéricos, false para booleanos, null para referencias).
public class Producto {
    private String nombre;
    private double precio;
    
    // Java crea implícitamente:
    // public Producto() {}
}

// Uso del constructor por defecto
Producto p = new Producto(); // nombre será null, precio será 0.0
  • Constructor parametrizado: Permite inicializar los atributos con valores específicos proporcionados al crear el objeto.
public class Rectángulo {
    private double ancho;
    private double alto;
    
    public Rectángulo(double ancho, double alto) {
        this.ancho = ancho;
        this.alto = alto;
    }
}

// Uso
Rectángulo r = new Rectángulo(5.0, 3.0);
  • Constructor de copia: Se utiliza para crear un nuevo objeto como copia de otro existente.
public class Punto {
    private int x;
    private int y;
    
    public Punto(int x, int y) {
        this.x = x;
        this.y = y;
    }
    
    // Constructor de copia
    public Punto(Punto otro) {
        this.x = otro.x;
        this.y = otro.y;
    }
}

// Uso
Punto p1 = new Punto(10, 20);
Punto p2 = new Punto(p1); // Crea una copia de p1

Sobrecarga de constructores

La sobrecarga de constructores permite definir múltiples constructores con diferentes parámetros en la misma clase. Esto permite diferentes formas de inicialización.

public class Empleado {
    private String nombre;
    private String departamento;
    private double salario;
    
    // Constructor completo
    public Empleado(String nombre, String departamento, double salario) {
        this.nombre = nombre;
        this.departamento = departamento;
        this.salario = salario;
    }
    
    // Constructor con valores predeterminados para departamento
    public Empleado(String nombre, double salario) {
        this(nombre, "General", salario); // Llama al constructor completo
    }
    
    // Constructor mínimo
    public Empleado(String nombre) {
        this(nombre, "General", 1000.0); // Valores predeterminados
    }
}

En el ejemplo anterior, se utiliza this() para llamar a otro constructor dentro de la misma clase. Esta técnica es conocida como encadenamiento de constructores y evita la duplicación de código.

La palabra clave this

Dentro de un constructor, la palabra clave this tiene dos usos principales:

  • Referencia a los atributos de la instancia: Distingue entre los parámetros del constructor y los atributos de la clase cuando tienen el mismo nombre.
public class Cliente {
    private String nombre;
    private String email;
    
    public Cliente(String nombre, String email) {
        this.nombre = nombre; // 'this.nombre' se refiere al atributo
        this.email = email;   // 'email' se refiere al atributo
    }
}
  • Llamada a otro constructor: this() permite invocar a otro constructor de la misma clase.
public class Tiempo {
    private int horas;
    private int minutos;
    private int segundos;
    
    public Tiempo(int horas, int minutos, int segundos) {
        this.horas = horas;
        this.minutos = minutos;
        this.segundos = segundos;
    }
    
    public Tiempo(int horas, int minutos) {
        this(horas, minutos, 0); // Segundos = 0
    }
    
    public Tiempo(int horas) {
        this(horas, 0, 0); // Minutos = 0, Segundos = 0
    }
}

Inicialización de atributos

Los constructores permiten diferentes estrategias para inicializar los atributos:

  • Inicialización directa: Asignar valores directamente en el constructor.
public class Usuario {
    private String nombre;
    private boolean activo;
    
    public Usuario(String nombre) {
        this.nombre = nombre;
        this.activo = true; // Todos los usuarios comienzan activos
    }
}
  • Inicialización con validación: Verificar la validez de los valores antes de asignarlos.
public class CuentaBancaria {
    private String número;
    private double saldo;
    
    public CuentaBancaria(String número, double saldoInicial) {
        if (número == null || número.length() != 10) {
            throw new IllegalArgumentException("Número de cuenta inválido");
        }
        
        if (saldoInicial < 0) {
            throw new IllegalArgumentException("El saldo inicial no puede ser negativo");
        }
        
        this.número = número;
        this.saldo = saldoInicial;
    }
}
  • Inicialización con valores predeterminados: Establecer valores por defecto para ciertos atributos.
public class Configuración {
    private String idioma;
    private boolean modoOscuro;
    private int tamaño;
    
    public Configuración() {
        // Valores predeterminados
        this.idioma = "es";
        this.modoOscuro = false;
        this.tamaño = 12;
    }
    
    public Configuración(String idioma) {
        this(); // Llama al constructor sin parámetros
        this.idioma = idioma; // Sobrescribe solo el idioma
    }
}

Constructores en herencia

Cuando se trabaja con herencia, los constructores tienen comportamientos específicos:

  • El constructor de la subclase debe llamar a un constructor de la superclase, ya sea explícitamente mediante super() o implícitamente.
  • Si no se especifica una llamada a super(), Java inserta automáticamente una llamada al constructor sin parámetros de la superclase.
public class Vehículo {
    protected String marca;
    protected String modelo;
    
    public Vehículo(String marca, String modelo) {
        this.marca = marca;
        this.modelo = modelo;
    }
}

public class Automóvil extends Vehículo {
    private int numeroPuertas;
    
    public Automóvil(String marca, String modelo, int numeroPuertas) {
        super(marca, modelo); // Llama al constructor de Vehículo
        this.numeroPuertas = numeroPuertas;
    }
}

Si la superclase no tiene un constructor sin parámetros, las subclases deben llamar explícitamente a uno de los constructores disponibles mediante super().

Bloques de inicialización

Además de los constructores, Java proporciona bloques de inicialización que se ejecutan cuando se crea un objeto. Existen dos tipos:

  • Bloques de inicialización de instancia: Se ejecutan antes del constructor, cada vez que se crea un objeto.
public class Ejemplo {
    private int[] números;
    
    // Bloque de inicialización de instancia
    {
        System.out.println("Inicializando objeto...");
        números = new int[10];
        for (int i = 0; i < números.length; i++) {
            números[i] = i * 2;
        }
    }
    
    public Ejemplo() {
        System.out.println("Constructor ejecutado");
    }
}
  • Bloques de inicialización estáticos: Se ejecutan cuando la clase se carga en memoria, antes de crear cualquier objeto.
public class BaseDatos {
    private static String url;
    
    // Bloque de inicialización estático
    static {
        System.out.println("Configurando conexión...");
        url = "jdbc:mysql://localhost:3306/midb";
        // Podría cargar configuración desde un archivo
    }
    
    public BaseDatos() {
        System.out.println("Conectando a: " + url);
    }
}

Buenas prácticas con constructores

Para utilizar constructores de manera adecuada, se recomienda seguir estas prácticas:

  • Validar parámetros: Verificar que los valores recibidos sean válidos antes de asignarlos a los atributos.
public class Círculo {
    private double radio;
    
    public Círculo(double radio) {
        if (radio <= 0) {
            throw new IllegalArgumentException("El radio debe ser positivo");
        }
        this.radio = radio;
    }
}
  • Minimizar la lógica: Los constructores deben centrarse en la inicialización. La lógica compleja debe delegarse a métodos auxiliares.
public class Pedido {
    private List<Producto> productos;
    private double total;
    
    public Pedido(List<Producto> productos) {
        if (productos == null || productos.isEmpty()) {
            throw new IllegalArgumentException("La lista de productos no puede estar vacía");
        }
        
        this.productos = new ArrayList<>(productos); // Copia defensiva
        this.total = calcularTotal(); // Delega el cálculo a un método
    }
    
    private double calcularTotal() {
        return productos.stream()
                .mapToDouble(Producto::getPrecio)
                .sum();
    }
}
  • Usar constructores privados para patrones de diseño específicos como Singleton o Factory.
public class Conexión {
    private static Conexión instancia;
    
    // Constructor privado
    private Conexión() {
        // Inicialización
    }
    
    // Método estático para obtener la instancia
    public static Conexión obtenerInstancia() {
        if (instancia == null) {
            instancia = new Conexión();
        }
        return instancia;
    }
}
  • Considerar el uso de patrones de construcción para clases con muchos parámetros opcionales.
public class Email {
    private final String destinatario;
    private final String asunto;
    private final String cuerpo;
    private final String remitente;
    private final List<String> cc;
    
    // Constructor privado usado por el Builder
    private Email(Builder builder) {
        this.destinatario = builder.destinatario;
        this.asunto = builder.asunto;
        this.cuerpo = builder.cuerpo;
        this.remitente = builder.remitente;
        this.cc = builder.cc;
    }
    
    // Clase Builder
    public static class Builder {
        private final String destinatario; // Obligatorio
        private String asunto = "";        // Opcional
        private String cuerpo = "";        // Opcional
        private String remitente = "no-reply@ejemplo.com"; // Valor predeterminado
        private List<String> cc = new ArrayList<>();       // Valor predeterminado
        
        public Builder(String destinatario) {
            this.destinatario = destinatario;
        }
        
        public Builder asunto(String asunto) {
            this.asunto = asunto;
            return this;
        }
        
        public Builder cuerpo(String cuerpo) {
            this.cuerpo = cuerpo;
            return this;
        }
        
        public Builder remitente(String remitente) {
            this.remitente = remitente;
            return this;
        }
        
        public Builder cc(String cc) {
            this.cc.add(cc);
            return this;
        }
        
        public Email build() {
            return new Email(this);
        }
    }
}

// Uso del patrón Builder
Email email = new Email.Builder("destinatario@ejemplo.com")
                .asunto("Prueba")
                .cuerpo("Este es un mensaje de prueba")
                .remitente("yo@ejemplo.com")
                .cc("copia@ejemplo.com")
                .build();

Objetos en Java

Los objetos son la materialización de las clases en la programación orientada a objetos. Si una clase es el plano o plantilla, un objeto es la instancia concreta creada a partir de esa clase. Cada objeto posee su propio estado (valores de atributos) y comportamiento (métodos) definidos por su clase.

En Java, los objetos se crean mediante el operador new, que reserva memoria para el objeto y llama al constructor de la clase para inicializarlo. A diferencia de los tipos primitivos que se almacenan directamente en la memoria, los objetos se almacenan en el heap (montículo) y se accede a ellos mediante referencias.

// Declaración de una referencia a un objeto
Persona persona;

// Creación del objeto e inicialización de la referencia
persona = new Persona("Juan", 30);

// Declaración y creación en una sola línea
Libro miLibro = new Libro("Cien años de soledad", "Gabriel García Márquez");

Anatomía de un objeto

Un objeto en Java consta de:

  • Estado interno: Representado por los valores de sus atributos, que pueden variar durante la vida del objeto.
  • Identidad única: Cada objeto tiene una identidad distinta, incluso si contiene exactamente los mismos valores que otro objeto.
  • Comportamiento: Definido por los métodos que puede ejecutar.
public class CuentaBancaria {
    private String número;
    private double saldo;
    private String titular;
    
    // Constructor
    public CuentaBancaria(String número, String titular) {
        this.número = número;
        this.titular = titular;
        this.saldo = 0.0;
    }
    
    // Métodos que definen el comportamiento
    public void depositar(double cantidad) {
        if (cantidad > 0) {
            saldo += cantidad;
        }
    }
    
    public boolean retirar(double cantidad) {
        if (cantidad > 0 && saldo >= cantidad) {
            saldo -= cantidad;
            return true;
        }
        return false;
    }
    
    public double consultarSaldo() {
        return saldo;
    }
}

// Creación y uso de objetos
CuentaBancaria cuenta1 = new CuentaBancaria("1001", "Ana López");
cuenta1.depositar(1000);
cuenta1.retirar(200);
System.out.println("Saldo actual: " + cuenta1.consultarSaldo());

CuentaBancaria cuenta2 = new CuentaBancaria("1002", "Carlos Ruiz");
cuenta2.depositar(500);

Ciclo de vida de un objeto

Los objetos en Java pasan por varias etapas durante su existencia:

  • Creación: Se reserva memoria y se inicializa el objeto mediante un constructor.
  • Uso: Se accede a sus atributos y se invocan sus métodos.
  • Destrucción: Cuando no hay referencias al objeto, se marca para ser eliminado por el recolector de basura.
public void ejemploCicloVida() {
    // Creación
    Producto p = new Producto("Laptop", 999.99);
    
    // Uso
    p.aplicarDescuento(10);
    System.out.println("Precio final: " + p.getPrecio());
    
    // Destrucción (implícita)
    p = null; // Elimina la referencia, permitiendo que el GC libere la memoria
}

A diferencia de otros lenguajes, en Java no se necesita liberar manualmente la memoria, ya que el recolector de basura (Garbage Collector) se encarga automáticamente de recuperar la memoria de objetos que ya no son accesibles.

Referencias a objetos

En Java, las variables de tipo objeto no contienen directamente el objeto, sino una referencia (similar a un puntero) que indica dónde se encuentra el objeto en memoria. Esto tiene importantes implicaciones:

  • Asignación de referencias: Al asignar una variable de objeto a otra, se copia la referencia, no el objeto.
  • Comparación de objetos: El operador == compara referencias (si apuntan al mismo objeto), mientras que el método equals() compara el contenido de los objetos.
Persona p1 = new Persona("Elena", 25);
Persona p2 = new Persona("Elena", 25); // Mismo contenido pero diferente objeto
Persona p3 = p1; // p3 y p1 apuntan al mismo objeto

// Comparación de referencias
System.out.println(p1 == p2); // false (diferentes objetos)
System.out.println(p1 == p3); // true (misma referencia)

// Comparación de contenido (asumiendo que Persona tiene un método equals() adecuado)
System.out.println(p1.equals(p2)); // true (mismo contenido)

Paso de objetos a métodos

Cuando se pasa un objeto como argumento a un método, se pasa la referencia al objeto, no una copia del objeto. Esto significa que los cambios realizados al objeto dentro del método afectan al objeto original.

public class Ejemplo {
    public static void main(String[] args) {
        Contador c = new Contador(5);
        System.out.println("Valor inicial: " + c.getValor()); // 5
        
        incrementar(c);
        System.out.println("Después de incrementar: " + c.getValor()); // 6
        
        // Crear nueva referencia no afecta al objeto original
        reasignar(c);
        System.out.println("Después de reasignar: " + c.getValor()); // 6
    }
    
    public static void incrementar(Contador c) {
        c.incrementar(); // Modifica el objeto original
    }
    
    public static void reasignar(Contador c) {
        c = new Contador(10); // Solo afecta a la referencia local
    }
}

class Contador {
    private int valor;
    
    public Contador(int valor) {
        this.valor = valor;
    }
    
    public void incrementar() {
        valor++;
    }
    
    public int getValor() {
        return valor;
    }
}

Arreglos de objetos

Se pueden crear arreglos que contengan referencias a objetos, lo que permite manejar colecciones de objetos.

// Crear un arreglo de referencias a Estudiante
Estudiante[] grupo = new Estudiante[3];

// Inicializar cada elemento con un nuevo objeto
grupo[0] = new Estudiante("Ana", 20, 9.5);
grupo[1] = new Estudiante("Luis", 22, 8.7);
grupo[2] = new Estudiante("Marta", 21, 9.2);

// Recorrer y usar los objetos
for (Estudiante e : grupo) {
    System.out.println(e.getNombre() + ": " + e.getPromedio());
}

Al crear un arreglo de objetos, inicialmente contiene referencias null. Se deben crear e inicializar los objetos individualmente antes de usarlos.

Objetos anónimos

En ocasiones, se pueden crear objetos anónimos (sin asignarlos a una variable) cuando solo se necesitan temporalmente o para una única operación.

// Objeto anónimo como argumento
procesarProducto(new Producto("Teclado", 49.99));

// Objeto anónimo en una expresión
double área = new Rectángulo(5, 10).calcularÁrea();

// Encadenamiento de métodos con objeto anónimo
String resultado = new StringBuilder("Hola")
    .append(" ")
    .append("Mundo")
    .toString();

Métodos de la clase Object

Todos los objetos en Java heredan de la clase Object, que proporciona métodos fundamentales:

  • equals(Object obj): Determina si dos objetos son iguales en contenido. Por defecto compara referencias, pero se puede sobrescribir.
  • hashCode(): Devuelve un código hash del objeto, utilizado en estructuras como HashMap.
  • toString(): Devuelve una representación en cadena del objeto. Por defecto muestra la clase y dirección de memoria.
  • getClass(): Devuelve un objeto Class que representa la clase del objeto.
public class Empleado {
    private int id;
    private String nombre;
    
    public Empleado(int id, String nombre) {
        this.id = id;
        this.nombre = nombre;
    }
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        
        Empleado otro = (Empleado) obj;
        return id == otro.id && 
               Objects.equals(nombre, otro.nombre);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(id, nombre);
    }
    
    @Override
    public String toString() {
        return "Empleado{id=" + id + ", nombre='" + nombre + "'}";
    }
}

Objetos inmutables

Los objetos inmutables son aquellos cuyo estado no puede cambiar después de su creación.

public final class Coordenada {
    private final double x;
    private final double y;
    
    public Coordenada(double x, double y) {
        this.x = x;
        this.y = y;
    }
    
    public double getX() {
        return x;
    }
    
    public double getY() {
        return y;
    }
    
    // En lugar de modificar, crea un nuevo objeto
    public Coordenada trasladar(double deltaX, double deltaY) {
        return new Coordenada(x + deltaX, y + deltaY);
    }
    
    public double distanciaA(Coordenada otra) {
        double dx = this.x - otra.x;
        double dy = this.y - otra.y;
        return Math.sqrt(dx*dx + dy*dy);
    }
}

Para crear objetos inmutables:

  • Declarar la clase como final para evitar subclases
  • Hacer todos los campos private y final
  • No proporcionar métodos que modifiquen el estado
  • Asegurar que los campos mutables no sean expuestos directamente

Identidad vs. igualdad

En Java, es importante distinguir entre identidad e igualdad de objetos:

  • Identidad: Dos referencias apuntan al mismo objeto en memoria (comparación con ==)
  • Igualdad: Dos objetos tienen el mismo estado o contenido (comparación con equals())
String s1 = new String("Hola");
String s2 = new String("Hola");
String s3 = s1;

// Identidad
System.out.println(s1 == s2); // false (diferentes objetos)
System.out.println(s1 == s3); // true (misma referencia)

// Igualdad
System.out.println(s1.equals(s2)); // true (mismo contenido)

Objetos y memoria

La gestión de objetos en Java está estrechamente relacionada con la gestión de memoria:

  • Los objetos se almacenan en el heap (montículo), un área de memoria gestionada por la JVM
  • Las referencias a objetos se almacenan en la pila (stack) o en otros objetos
  • El recolector de basura libera la memoria de objetos sin referencias
public void ejemploMemoria() {
    // La referencia 'local' se almacena en la pila
    // El objeto Persona se almacena en el heap
    Persona local = new Persona("Juan", 30);
    
    // Al salir del método, 'local' se destruye
    // Si no hay otras referencias, el objeto queda disponible para el GC
}

Buenas prácticas con objetos

Para trabajar eficientemente con objetos en Java:

  • Inicializar objetos correctamente: Usar constructores adecuados y validar parámetros.
  • Implementar equals() y hashCode(): Sobrescribir estos métodos cuando se necesite comparar objetos por contenido.
  • Preferir inmutabilidad: Cuando sea posible, diseñar clases inmutables.
  • Liberar recursos: Usar bloques try-with-resources para objetos que manejan recursos externos.
// Uso de try-with-resources para objetos que implementan AutoCloseable
try (Scanner scanner = new Scanner(new File("datos.txt"))) {
    while (scanner.hasNextLine()) {
        System.out.println(scanner.nextLine());
    }
} catch (IOException e) {
    System.err.println("Error al leer el archivo: " + e.getMessage());
}
// El scanner se cierra automáticamente al salir del bloque try

Serialización de objetos

La serialización permite convertir objetos en secuencias de bytes para almacenarlos o transmitirlos, y luego reconstruirlos.

public class Cliente implements Serializable {
    private static final long serialVersionUID = 1L;
    private String nombre;
    private String email;
    private transient String contraseña; // No se serializa
    
    // Constructor y métodos
}

// Serializar
try (ObjectOutputStream out = new ObjectOutputStream(
        new FileOutputStream("cliente.dat"))) {
    Cliente c = new Cliente("Ana", "ana@ejemplo.com");
    out.writeObject(c);
}

// Deserializar
try (ObjectInputStream in = new ObjectInputStream(
        new FileInputStream("cliente.dat"))) {
    Cliente c = (Cliente) in.readObject();
    System.out.println("Cliente recuperado: " + c.getNombre());
}

Para que un objeto sea serializable:

  • La clase debe implementar la interfaz Serializable
  • Todos sus atributos deben ser serializables o marcados como transient
  • Se recomienda definir un serialVersionUID para control de versiones

CONSTRUYE TU CARRERA EN IA Y PROGRAMACIÓN SOFTWARE

Accede a +1000 lecciones y cursos con certificado. Mejora tu portfolio con certificados de superación para tu CV.

30 % DE DESCUENTO

Plan mensual

19.00 /mes

13.30 € /mes

Precio normal mensual: 19 €
63 % DE DESCUENTO

Plan anual

10.00 /mes

7.00 € /mes

Ahorras 144 € al año
Precio normal anual: 120 €
Aprende Java online

Ejercicios de esta lección Clases y objetos

Evalúa tus conocimientos de esta lección Clases y objetos con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

Clases abstractas

Test

Streams: reduce()

Test

Streams: flatMap()

Test

Llamada y sobrecarga de funciones

Puzzle

Métodos referenciados

Test

Métodos de la clase String

Código

Representación de Fecha

Puzzle

Operadores lógicos

Test

Tipos de datos

Código

Estructuras de iteración

Puzzle

Streams: forEach()

Test

Objetos

Puzzle

Funciones lambda

Test

Uso de Scanner

Puzzle

CRUD en Java de modelo Customer sobre un ArrayList

Proyecto

Tipos de variables

Puzzle

Streams: collect()

Puzzle

Operadores aritméticos

Puzzle

Interfaz funcional Consumer

Test

API java.nio 2

Puzzle

API Optional

Test

Interfaz funcional Function

Test

Encapsulación

Test

Interfaces

Código

Uso de API Optional

Puzzle

Representación de Hora

Test

Herencia básica

Test

Clases y objetos

Código

Interfaz funcional Supplier

Puzzle

HashMap

Puzzle

Sobrecarga de métodos

Test

Polimorfismo de tiempo de ejecución

Puzzle

OOP en Java

Proyecto

Creación de Streams

Test

Streams: min max

Puzzle

Métodos avanzados de la clase String

Puzzle

Polimorfismo de tiempo de compilación

Test

Excepciones

Puzzle

Herencia avanzada

Puzzle

Estructuras de selección

Test

Uso de interfaces

Test

HashSet

Test

Objeto Scanner

Test

Streams: filter()

Puzzle

Operaciones de Streams

Puzzle

Interfaz funcional Predicate

Puzzle

Streams: sorted()

Test

Configuración de entorno

Test

CRUD en Java de modelo Customer sobre un HashMap

Proyecto

Uso de variables

Test

Clases

Test

Streams: distinct()

Puzzle

Streams: count()

Test

ArrayList

Test

Datos de referencia

Test

Interfaces funcionales

Puzzle

Métodos básicos de la clase String

Test

Instalación

Test

Funciones

Código

Estructuras de control

Código

Herencia de clases

Código

Streams: map()

Puzzle

Funciones y encapsulamiento

Test

Streams: match

Test

Gestión de errores y excepciones

Código

Datos primitivos

Puzzle

Todas las lecciones de Java

Accede a todas las lecciones de Java y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Instalación De Java

Introducción Y Entorno

Configuración De Entorno Java

Introducción Y Entorno

Ecosistema Jakarta Ee De Java

Introducción Y Entorno

Tipos De Datos

Sintaxis

Variables

Sintaxis

Operadores

Sintaxis

Estructuras De Control

Sintaxis

Funciones

Sintaxis

Excepciones

Programación Orientada A Objetos

Clases Y Objetos

Programación Orientada A Objetos

Encapsulación

Programación Orientada A Objetos

Herencia

Programación Orientada A Objetos

Clases Abstractas

Programación Orientada A Objetos

Interfaces

Programación Orientada A Objetos

Sobrecarga De Métodos

Programación Orientada A Objetos

Polimorfismo

Programación Orientada A Objetos

La Clase Scanner

Programación Orientada A Objetos

Métodos De La Clase String

Programación Orientada A Objetos

Listas

Framework Collections

Conjuntos

Framework Collections

Mapas

Framework Collections

Funciones Lambda

Programación Funcional

Interfaz Funcional Consumer

Programación Funcional

Interfaz Funcional Predicate

Programación Funcional

Interfaz Funcional Supplier

Programación Funcional

Interfaz Funcional Function

Programación Funcional

Métodos Referenciados

Programación Funcional

Creación De Streams

Programación Funcional

Operaciones Intermedias Con Streams: Map()

Programación Funcional

Operaciones Intermedias Con Streams: Filter()

Programación Funcional

Operaciones Intermedias Con Streams: Distinct()

Programación Funcional

Operaciones Finales Con Streams: Collect()

Programación Funcional

Operaciones Finales Con Streams: Min Max

Programación Funcional

Operaciones Intermedias Con Streams: Flatmap()

Programación Funcional

Operaciones Intermedias Con Streams: Sorted()

Programación Funcional

Operaciones Finales Con Streams: Reduce()

Programación Funcional

Operaciones Finales Con Streams: Foreach()

Programación Funcional

Operaciones Finales Con Streams: Count()

Programación Funcional

Operaciones Finales Con Streams: Match

Programación Funcional

Api Optional

Programación Funcional

Api Java.nio 2

Entrada Y Salida (Io)

Api Java.time

Api Java.time

Accede GRATIS a Java y certifícate

Certificados de superación de Java

Supera todos los ejercicios de programación del curso de Java y obtén certificados de superación para mejorar tu currículum y tu empleabilidad.

En esta lección

Objetivos de aprendizaje de esta lección

  • Entender la diferencia entre una clase y un objeto en Java
  • Saber cómo definir una clase en Java
  • Aprender a crear un objeto a partir de una clase
  • Entender la importancia de la encapsulación en la programación orientada a objetos
  • Saber cómo implementar la encapsulación en una clase a través de atributos privados y métodos públicos
  • Conocer cómo interactuar con los atributos y métodos de un objeto