Skip to content

04. Programación Orientada a Objetos

Melissa Diaz edited this page Jan 23, 2025 · 1 revision

Programación Orientada a Objetos (POO)

La programación orientada a objetos (POO) es un paradigma de programación que se basa en el uso de objetos y sus interacciones para diseñar aplicaciones y programas de software.

Los objetos son instancias de clases, las cuales pueden incluir variables de instancia, métodos y otros datos necesarios para su funcionamiento.

Clases e instancias

  • Una clase representan un modelo o plantilla que define las características (atributos) y comportamientos (métodos) que deben tener los objetos de un tipo específico.
  • Una instancia es un objeto que se ha creado utilizando la plantilla de esa clase, mediante el operador new. Cada instancia puede acceder a los métodos que se han definido en la clase.

Note

Una clase consiste en un conjunto de instrucciones que especifican cómo deben construirse las instancias de dicha clase.

Ejemplo:

  • Clase:
public class Perro {
	// Atributos
	private String nombre;
	private String raza;
	private int edad;
	// Constructor
	public Perro(String nombre, String raza, int edad) {
	this.nombre = nombre;
	this.raza = raza;
	this.edad = edad;
}
}
  • Instancia:
public class Main {
	public static void main(String[] args) {
	// Crear una instancia de la clase Perro
	Perro miPerro = new Perro("Max", "Labrador", 3);
}
}

this y método constructor

Palabra clave this

La palabra clave this en Java se utiliza para referirse al objeto actual dentro de una clase.

Usos:

  • Referenciar propiedades del objeto desde métodos o constructores.
  • Invocar a otro constructor dentro de la misma clase usando this(...), lo que se llama delegación de constructores.

Constructores

  • Un constructor es un método especial diseñado para inicializar objetos de una clase. Tiene el mismo nombre que la clase y no devuelve ningún valor (ni siquiera void).
  • Tiene como propósito establecer los valores iniciales de los atributos de un objeto cuando se crea.
  • Si no se define un constructor, Java proporciona uno sin parámetros conocido como constructor por defecto.
  • El constructor predeterminado (o por defecto) crea una instancia de la clase con los valores predeterminados de sus campos:
  • 0 para tipos numéricos
  • false para boolean
  • null para referencias de objetos

Ejemplo:

public class Persona {
    String nombre; // propiedad de los objetos de la clase
    public Persona() {} // constructor por defecto, no hace falta declararlo
}

Sobrecarga de Constructores

La sobrecarga de constructores permite definir varios constructores con diferentes listas de parámetros. Esto permite inicializar objetos de distintas maneras.

Ejemplo de sobrecarga:

public class Persona {
    private String nombre;
    private Integer edad;

    // Constructor por defecto
    public Persona() {
        this("Desconocido", 0); // Se conoce c omo Llamada al constructor o delegación al constructor
    }

    // Constructor con parámetros
    public Persona(String nombre, int edad) {
        this.nombre = nombre;
        this.edad = edad;
    }
}
  • El constructor por defecto utiliza this(...) para invocar al constructor que toma nombre y edad como parámetros, evitando valores null en las propiedades.
  • La llamada this(...) debe ser la primera línea del constructor.

Modificador static

Una variable estática pertenece a la clase y no a las instancias individuales de esa clase. Esto significa que:

  • Solo existe una copia de la variable estática, sin importar cuántas instancias de la clase se creen.
  • Todas las instancias de la misma clase comparten el mismo valor de la variable estática.
  • Es útil para almacenar valores que deben ser comunes y persistentes para todas las instancias.

Note

Dentro de los métodos que pertenecen a las instancias (métodos no estáticos), podemos acceder a variables estáticas y variables de instancia. Sin embargo, en los métodos estáticos, no se pueden acceder directamente a las variables de instancia, ya que estas requieren una instancia de la clase. Los métodos estáticos solo pueden acceder a variables estáticas. Si no es estático no se puede referenciar dentro de un contexto estático.

Métodos estáticos

Un método estático pertenece a la clase y no a una instancia específica, lo cual implica que:

  • No puede acceder a variables o métodos de instancia, ya que estos dependen de una instancia de la clase.
  • Suelen usarse para operaciones generales que no dependen de los datos de un objeto específico.

Ejemplo de variables y métodos estáticos:

public class Clase {
    Integer variableNoEstatica;        // Variable de instancia
    static Integer variableEstatica;   // Variable estática

    public static void metodoEstatico() {
        // Método estático
    }

    public void metodoNoEstatico() {
        // Método no estático
    }
}

Note

  • Las variables estáticas se declaran dentro de la clase, pero fuera de métodos, constructores o bloques.
  • No se pueden declarar variables estáticas dentro de métodos, a menos que estén en un bloque estático, donde su alcance se limita a ese bloque.
  • Para invocar métodos y variables estáticas, se usa el nombre de la clase directamente, como Clase.metodoEstatico() o Clase.variableEstatica.

Métodos Getters y Setters en Java

Los métodos getters y setters son esenciales para la encapsulación y ocultación de datos en Java. Estos métodos permiten obtener (getters) y establecer (setters) el valor de un atributo privado de una clase, controlando el acceso y modificación de los datos.

Ejemplo:

public class Persona {
    // Atributos privados
    private String nombre;
    private int edad;

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

    // Getter para nombre
    public String getNombre() {
        return nombre;
    }

    // Setter para nombre
    public void setNombre(String nombre) {
        this.nombre = nombre;
    }

    // Getter para edad
    public int getEdad() {
        return edad;
    }

    // Setter para edad
    public void setEdad(int edad) {
        this.edad = edad;
    }
}

Los atributos nombre y edad son privados, lo que significa que solo pueden ser accedidos y modificados dentro de la clase.

Ventajas:

  • Encapsulación: Los datos privados solo pueden ser modificados a través de métodos controlados.
  • Modularidad y Mantenibilidad: Permiten acceder y modificar los valores de forma segura, contribuyendo a un diseño de código más modular y fácil de mantener.
  • Mejora la seguridad y robustez del código al evitar el acceso directo a los atributos privados desde fuera de la clase.

Buenas Prácticas de POO

Práctica Descripción
Entiende los Objetos Visualiza cada objeto como un pequeño programa con sus propias características y funcionalidades. Esta mentalidad te permitirá descomponer problemas complejos en unidades más manejables.
Uso de Clases y Objetos Aprende a emplear clases como modelos para crear objetos. Cada instancia de objeto que creas es una representación única de una clase.
Importancia de los Atributos Los atributos actúan como las "etiquetas" que definen las propiedades de tus objetos. Por ejemplo, en una clase "Perro", los atributos podrían ser su raza, color y edad.
Métodos para Acciones Los métodos representan las "acciones" que pueden realizar tus objetos. Si tienes un objeto "Automóvil", puede tener métodos como "arrancar" o "frenar".
Práctica Constante La práctica es fundamental. Cuantos más ejemplos y ejercicios realices, más familiarizado estarás con los conceptos de la POO y más habilidades desarrollarás.

Artículo: https://www.baeldung.com/java-static#:~:text=In%20the%20Java%20programming%20language,an%20instance%20of%20that%20type.

Modificadores de Acceso

Los modificadores de acceso son etiquetas que se les agregan a los atributos y métodos de una clase para controlar el acceso. Pueden ser:

  • private: La clase, el método o el atributo es accesible solamente dentro de la clase.

  • public: La clase, el método o el atributo es accesible desde cualquier lugar.

  • protected: La clase, el método o el atributo es accesible dentro del mismo paquete y también por subclases de cualquier paquete.

  • default (sin modificador): La clase, el método o el atributo es solo accesible dentro del mismo paquete. Es el comportamiento por defecto.

  • Encapsulamiento: Es el proceso de agrupar los datos (atributos) y los métodos que los manipulan dentro de una clase, formando una única unidad lógica. De esta manera, la clase puede gestionar sus datos y comportamientos de forma coherente y controlada.

  • Ocultamiento de Información: Es una técnica dentro del encapsulamiento que restringe el acceso directo a los atributos de una clase. Al marcar los atributos como private, estos no pueden ser accedidos o modificados directamente desde fuera de la clase; en su lugar, se accede a ellos mediante métodos públicos (como getters y setters), permitiendo así un control seguro y definido de cómo se manipulan los datos internos de la clase.

Beneficios del Encapsulación y Ocultación

  • Control de Acceso: Se obtiene un control completo sobre cómo y cuándo se accede o modifica la información de un objeto.

  • Flexibilidad y Mantenimiento: Al ocultar los detalles internos de la implementación de una clase, es posible cambiar esa implementación sin afectar otras partes del código que usan la clase.

  • Seguridad de los Datos: Al proteger los atributos de acceso directo, se asegura la integridad y coherencia de los datos, evitando que sean modificados de formas no deseadas o inesperadas.

Asociación de Objetos

La asociación implica que una clase sea "consciente" de otra y mantenga una referencia de ella.

La asociación implica el uso de propiedades que son instancias de clases personalizadas.

Agregación

La agregación es una forma de asociación en la que el ciclo de vida de un objeto no depende del otro. Es el comportamiento por defecto.

Composición

La composición es otra forma de asociación en la que el ciclo de vida de una clase depende del otro. En este caso, los objetos contenidos no pueden existir independientemente del objeto contenedor. Si el objeto contenedor se destruye, los objetos contenidos también se destruyen.

Note

Todas las asociaciones por defecto son de agregación, a menos que implementemos una lógica específica que haga que sea de composición.

Relación entre Clases en Java

En Programación Orientada a Objetos, las relaciones entre clases pueden representarse como composición o agregación. Estas relaciones pueden ser:

  • Uno a Uno: Relación con un único objeto.
  • Uno a Muchos: Relación con múltiples objetos.
  • Cero a Uno o Cero a Muchos: Menos comunes, reflejan relaciones opcionales.

Relación Uno a Uno

Una relación donde un objeto está vinculado a otro único objeto.
Ejemplo: Un curso tiene un único profesor.

public class Profesor {
    private String nombre;
    private String apellido;
}

public class Curso {
    private String nombre;
    private char division;
    private Profesor profesor;

    public Profesor getProfesor() {
        return profesor;
    }

    public void setProfesor(Profesor p) {
        this.profesor = p;
    }
}

Relación Uno a Muchos

Una relación donde un objeto está asociado a múltiples objetos de otra clase.

Ejemplo: Un curso tiene muchos estudiantes.

public class Estudiante {
    private String nombre;
    private String apellido;
}

public class Curso {
    private String nombre;
    private char division;
    private Estudiante[] estudiantes;

    public Estudiante[] getEstudiantes() {
        return estudiantes;
    }

    public void setEstudiantes(Estudiante[] e) {
        this.estudiantes = e;
    }
}

Herencia

  • Permite reutilizar el código y organizar clases de forma jerárquica.
  • Permite que una clase adquiera propiedades y comportamientos de otra clase (conocida como la clase padre o superclase) y a la vez añadir sus propios atributos y métodos específicos.
  • Permite una organización más clara y estructurada del código.
  • Java admite la herencia simple (una clase puede heredar SOLO de una clase padre).

Crear una herencia

  1. La clase padre o superclase se declara como se haría normalmente.
  2. Las propiedades, en lugar de ser privadas (private), deben declararse como protegidas (protected), lo que permité que las propiedades SÓLO puedan ser utilizadas en la clase padre y en las hijas.
  3. En las subclases se utiliza la palabra clave extends para indicar que la clase debe heredar todos los atributos y métodos de otra clase:
public class Estudiante extends Persona {
	private String grado;
}

La clase Estudiante hereda las propiedades de la clase Persona.

Palabra clave super

super se utiliza en Java para hacer referencia a los métodos y constructores de la clase padre (superclase). Se usa de manera similar a la palabra clave this, pero en este caso se refiere a la clase superior.

Ejemplo:

// Superclase o clase padre
public class Persona {
    protected String nombre;
    protected int edad;
    
    public Persona() {
    }

    public Persona(String nombre, int edad) {
        this.nombre = nombre;
        this.edad = edad;
    }
}

// Subclase o clase hija
public class Estudiante extends Persona {
    private String grado;

    public Estudiante() {
        super(); // Llama al constructor de la clase padre
        this.edad = 14;
        this.nombre = "Elias";
        this.grado = "segundo";
    }

    public Estudiante(String nombre, int edad, String grado) {
        super(nombre, edad); // Llama al constructor de la clase padre con parámetros
        this.grado = grado;
    }
}
  • La subclase Estudiante utiliza super() para llamar al constructor de la clase Persona. Esto permite que la clase hija acceda a los constructores de la clase padre, lo que es útil cuando la clase padre tiene un constructor con parámetros.

  • Las propiedades nombre y edad de la clase Persona son accesibles en la clase Estudiante porque están declaradas con el modificador de acceso protected.

Sobrescribir métodos usando @Override

Los métodos de la clase padre pueden ser sobrescritos en la clase hija utilizando la anotación @Override.

public class Persona {
    public void hablar() {
        System.out.println("Soy una persona");
    }
}

public class Estudiante extends Persona {
    @Override
    public void hablar() {
        System.out.println("Soy un estudiante");
    }
}

Si un objeto de la clase Estudiante invoca el método hablar(), se imprimirá "Soy un estudiante". Si el método no se sobrescribe, se ejecutará el método de la clase Persona, imprimiendo "Soy una persona".

La clase Object en Java

En Java, todos los objetos heredan de una clase base común llamada Object. Esta herencia es implícita, lo que significa que no es necesario declarar extends Object para que ocurra. La clase Object proporciona varios métodos que pueden ser sobrescritos en las clases derivadas para personalizar su comportamiento. Algunos de estos métodos son:

  • equals(): Compara dos objetos para determinar si son iguales. Puede ser sobrescrito en clases personalizadas para comparar los atributos de los objetos en lugar de la referencia en memoria.

  • hashCode(): Devuelve un código hash único para un objeto, útil en estructuras de datos como tablas hash. Es recomendable sobrescribirlo si se sobrescribe equals() para asegurar la coherencia.

  • toString(): Devuelve una representación en cadena de texto del objeto, útil para imprimir información legible, especialmente para depuración o visualización.

  • clone(): Crea una copia superficial del objeto. Este método ha caído en desuso, y se recomienda implementar métodos de copia personalizados.

  • finalize(): Se llama antes de que el recolector de basura elimine un objeto, permitiendo la limpieza de recursos. Este método está obsoleto desde Java 9 y no se recomienda su uso.

  • getClass(): Obtiene la clase en tiempo de ejecución del objeto. No puede ser sobrescrito. Es útil en operaciones de reflexión.

  • notify(), notifyAll(), wait(): Métodos relacionados con la sincronización de hilos.

Importancia de equals(), hashCode() y toString()

  • equals() y hashCode(): Sobrescribir estos métodos es crucial cuando se crean objetos personalizados. Permiten comparaciones basadas en los valores de los atributos, en lugar de en la referencia en memoria.

  • toString(): Sobrescribir este método permite devolver una representación legible de los atributos del objeto, lo que es útil para su impresión o depuración.

Polimorfismo en Java

El polimorfismo es un concepto fundamental en la programación orientada a objetos que se refiere a la capacidad de un método u objeto de tomar múltiples formas.

1. Polimorfismo de Métodos

Se produce mediante la sobrecarga y sobrescritura de métodos en una clase. Esto permite que un método tenga múltiples implementaciones con el mismo nombre, pero con diferentes comportamientos según el contexto.

  • Sobrecarga de Métodos: Permite que un método tenga el mismo nombre pero diferente número o tipo de parámetros.
  • Sobrescritura de Métodos: En una subclase, se redefine un método de la superclase para cambiar su comportamiento.

2. Polimorfismo de Objetos

Se produce cuando una superclase se utiliza para referirse a un objeto de una subclase. También ocurre cuando una clase implementa interfaces. En este caso, un objeto de una subclase puede ser tratado como un objeto de la superclase.

  • Uso de instanceof: Permite verificar si un objeto es una instancia de una clase o interfaz.
  • Implementación de Interfaces: Las clases que implementan interfaces permiten que objetos de diferentes clases sean tratados de manera similar si comparten la misma interfaz.

Tipos de Herencia

  • Herencia única: Una subclase hereda de una sola superclase.
  • Herencia jerárquica: Una superclase sirve de base para varias subclases.
  • Herencia multinivel: Una clase derivada hereda de una clase base y actúa como base para otra clase.
  • Herencia múltiple: No compatible en Java con clases, pero se puede lograr mediante interfaces.

Operador instanceof

El operador instanceof se usa para verificar si un objeto es una instancia de una clase, subclase o interfaz.

class Animal {}
class Perro extends Animal {}
public class Instanceof {
    public static void main(String[] args) {
        Animal animal = new Animal();
        Animal perro = new Perro();
        System.out.println(perro instanceof Animal); // true
        System.out.println(perro instanceof Perro); // true
        System.out.println(animal instanceof Perro); // false
    }
}

Pattern Matching

El Pattern Matching es una técnica que permite verificar si un valor coincide con un patrón específico.

Pattern Matching para instanceof (Introducido en Java 16)

Permite declarar la variable directamente al usar instanceof, eliminando la necesidad de conversión explícita.

public class Main {
    public static void main(String[] args) {
        Object obj = "Hola mundo";
        if (obj instanceof String s) {
            System.out.println(s.toLowerCase());
        }
    }
}

Pattern Matching en switch

(En revisión en Java 20)

Permite usar patrones complejos en declaraciones switch, mejorando su flexibilidad.

class Animal {}
class Perro extends Animal {}

public class Main {
    public static void main(String[] args) {
        Object[] objects = { "Hola", 10, 20.0, true, new Animal(), new Perro() };
        for (Object obj : objects) {
            switch (obj) {
                case String s -> System.out.println("String: " + s);
                case Integer i -> System.out.println("Integer: " + i);
                case Double d -> System.out.println("Double: " + d);
                case Boolean b -> System.out.println("Boolean: " + b);
                case Perro p -> System.out.println("Es un perro");
                case Animal a -> System.out.println("Es un animal");
                default -> System.out.println("Tipo desconocido");
            }
        }
    }
}

Clases abstractas

  • En Java, una clase abstracta es una clase que no se puede instanciar directamente (no se pueden crear objetos directamente).
  • Se utilizan como plantillas para derivar subclases concretas que heredan sus atributos y métodos.
  • Pueden contener métodos abstractos.

Son útiles porque:

  • Proveen una forma de definir una estructura común para todas las subclases.
  • Facilitan la reutilización de código y promueven un diseño más limpio y mantenible.
  • Ayudan a desarrollar programas más flexibles y escalables.

Métodos abstractos

  • Son métodos sin una implementación concreta.
  • Los metodos abstractos (y las clases abstractas) ofrecen un mecanismo para definir estructuras y comportamientos genéricos que deben ser implementados por las clases hijas.

Características principales:

  1. Declaración sin cuerpo: Solo se especifica el nombre, parámetros y tipo de retorno.

    public abstract void hacerSonido();
  2. Presente en clases abstractas: Las clases que contienen métodos abstractos deben ser declaradas como abstract.

    public abstract class Animal {
        public abstract void hacerSonido();
    }
  3. Implementación obligatoria en subclases: Las subclases concretas deben proporcionar la implementación de todos los métodos abstractos.

Ejemplo:

// Clase abstracta
public abstract class Animal {
    public abstract void hacerSonido();
}

// Subclase concreta: Perro
public class Perro extends Animal {
    @Override
    public void hacerSonido() {
        System.out.println("Guau Guau");
    }
}

// Subclase concreta: Gato
public class Gato extends Animal {
    @Override
    public void hacerSonido() {
        System.out.println("Miau Miau");
    }
}
  • La clase abstracta Animal define el método abstracto hacerSonido().
  • Las subclases Perro y Gato implementan el método con comportamientos específicos: - Perro imprime "Guau Guau". - Gato imprime "Miau Miau".

Beneficios:

  • Consistencia: Define un contrato común para las subclases.
  • Flexibilidad: Permite implementar detalles específicos según las necesidades de cada subclase.
  • Diseño modular: Facilita la extensión y mantenimiento del código.

Interfaces

  • Proporciona un medio para definir contratos de comportamiento que pueden ser implementados por cualquier clase.
  • Es una colección de métodos abstractos que puede implementar la clase.
  • Se pueden implementar múltiples interfaces en la clase.
  • Permiten definir métodos a implementar en la clase.
  • También permiten definir constantes.

  • Propósito: Definen un conjunto de métodos que deben ser implementados por las clases que las utilicen.
  • Implementación: Especifican qué hacer, pero no cómo hacerlo.

public interface Interfaz {
    public final int CONSTANTE = 10;
    public void metodo();
    public int sumar();
}

La interfaz Interfaz incluye: - Una constante CONSTANTE. - Dos métodos abstractos: metodo() y sumar().

Implementación de la interfaz en una clase:

public class MiClase implements Interfaz {
    @Override
    public void metodo() {
        System.out.println("Ejecutando método en MiClase");
    }

    @Override
    public int sumar() {
        int resultado = 10 + 20;
        System.out.println("La suma es: " + resultado);
        return resultado;
    }
}

La clase MiClase: - Utiliza implements para indicar que implementa la interfaz. - Proporciona implementaciones específicas para los métodos metodo() y sumar(). - Usa @Override para asegurar que sobrescribe correctamente los métodos.


Ventajas de las Interfaces

  • Flexibilidad: Una interfaz puede ser implementada por múltiples clases, lo que permite compartir comportamientos sin usar herencia directa.
  • Versatilidad: Ideal para diseñar sistemas modulares y desacoplados.

Convenciones y Buenas Prácticas

  • Nombres descriptivos: Las interfaces deben tener nombres en PascalCase que indiquen su propósito, como Serializable o Comparable.
  • Variables constantes: Las variables en una interfaz son public static final por defecto, útiles para definir constantes compartidas.

[!NOTE] Material Extra


En Java, una clase puede:

  • Extender de una clase padre.
  • Heredar y proporcionar implementaciones para métodos abstractos de una clase
  • Implementar una o más interfaces, asegurando la implementación de los métodos definidos en ellas.
  • Esta capacidad permite combinar herencia y contratos de comportamiento.

Ejemplo:

// Definición de la interfaz  
public interface MiClaseInter {  
    void metodoInterfaz();  
}  

// Definición de la clase abstracta  
public abstract class MiClaseAbstracta {  
    public abstract void metodoAbstracto();  
}  

// Clase que extiende la clase abstracta e implementa la interfaz  
class MiClase extends MiClaseAbstracta implements MiClaseInter {  
    @Override  
    public void metodoAbstracto() {  
        System.out.println("Implementación del método abstracto");  
    }  

    @Override  
    public void metodoInterfaz() {  
        System.out.println("Implementación del método de la interfaz");  
    }  
}  

Con este enfoque, se puede aprovechar la herencia para atributos y métodos, mientras se cumple con el contrato definido por las interfaces.

[!NOTE] Documentación oficial

Clase Anónima

Una clase anónima es una clase que no tiene un nombre específico y se utiliza para crear una implementación única y concreta de una clase abstracta o una interfaz, generalmente en el momento en que se necesita.

Estas clases son especialmente útiles cuando queremos sobrescribir métodos de una clase abstracta o una interfaz sin crear una clase separada.

Características de una Clase Anónima:

  • No tiene nombre: Se declara e implementa directamente en el lugar donde se utiliza.
  • Implementación específica: Proporciona una implementación concreta para los métodos de una clase abstracta o de una interfaz.
  • Uso limitado: Normalmente, se utiliza solo una vez o en un contexto específico.

Ejemplo con Clases Abstractas

En este ejemplo, implementamos una clase abstracta llamada Abuelo mediante una clase anónima:

abstract class Abuelo {
    abstract void saludar();
}

public class Main {
    public static void main(String[] args) {
        Abuelo abueloAnonimo = new Abuelo() {
            @Override
            void saludar() {
                System.out.println("Hola, soy una clase anónima que implementa Abuelo.");
            }
        };
        abueloAnonimo.saludar();
    }
}

Ejemplo con Interfaces

Dado que las interfaces son abstractas, también podemos crear una clase anónima que las implemente:

interface MiInterfaz {
    void metodo();
}

public class Main {
    public static void main(String[] args) {
        MiInterfaz interfazAnonima = new MiInterfaz() {
            @Override
            public void metodo() {
                System.out.println("Hola, soy una clase anónima que implementa una interfaz.");
            }
        };
        interfazAnonima.metodo();
    }
}

Ventajas de las Clases Anónimas:

  • Reducen la cantidad de código cuando la implementación solo se necesita una vez.
  • Útiles para sobrescribir rápidamente métodos de clases o interfaces sin crear clases adicionales.

Limitaciones:

  • Debido a que no tienen nombre, no se pueden reutilizar directamente.
  • Solo pueden implementar una interfaz o extender una clase abstracta, ya que Java no admite la herencia múltiple directa.

Otros modificadores de clase

Modificador final en Java

El modificador final se utiliza para garantizar control y seguridad en el diseño de clases y métodos. Cierra la jerarquía de herencia.

  • Clases final: Prohíben la herencia, cerrando la jerarquía y asegurando la integridad del diseño.

    public final class Animal {
      // ...
    }
  • Métodos final: Impiden la sobrescritura, preservando la implementación original.

    public class Animal {
      public final void hacerSonido() {
        // ...
      }
    }

El uso de final asegura una estructura más robusta y confiable en el código.

Clases Selladas

Las clases selladas permite restringir la herencia al especificar qué subclases pueden extender una clase sellada. Esto mejora la claridad y seguridad del código, evitando extensiones no deseadas.

Diferencias con Clases Finales:

  • Clases finales: No permiten ninguna extensión.
  • Clases selladas: Permiten herencia, pero solo a subclases específicas.

Tipos de Clases que Extienden Clases Selladas:

  1. Final: No puede ser extendida posteriormente.
  2. Sealed: Permite herencia limitada a subclases explícitas.
  3. Non-sealed: Permite herencia sin restricciones.

Ejemplo de Declaración:

public sealed class FiguraGeometrica permits Triangulo, Circulo, Paralelogramo {
    // ...
}

Las clases selladas promueven una jerarquía bien definida y un mantenimiento más sencillo del código.

Enums

  • Son una manera de representar un conjunto limitado de opciones posibles.
  • Son un tipo de datos especial que permite definir una colección de constantes nombradas.

Declaración

La palabra clave enum se utiliza para declarar un enumerador.

Ejemplo:

public enum DiaSemana {LUNES, MARTES, MIERCOLES, JUEVES, VIERNES, SABADO, DOMINGO }
  • Convención:
    • Los nombres de las constantes en un enum deben escribirse en mayúsculas.
    • Es buen práctica que los Enum estén alojados en una carpeta independiente.
  • Inmutabilidad: Los valores definidos en un enum son constantes y no pueden cambiarse.
  • Uso: Un enum puede declararse dentro o fuera de una clase, pero no dentro de un método.

enums

Enums con propiedades

En Java, los enums son tratados como clases especiales que pueden incluir campos, métodos y constructores. Esto permite asociar valores adicionales a cada constante y definir comportamientos específicos.

public enum DiaSemana {
    LUNES("Primer día de la semana"),
    MARTES("Segundo día de la semana"),
    MIERCOLES("Tercer día de la semana"),
    JUEVES("Cuarto día de la semana"),
    VIERNES("Quinto día de la semana"),
    SABADO("Sexto día de la semana"),
    DOMINGO("Séptimo día de la semana");

    private final String descripcion;

    // Constructor privado para inicializar el campo 'descripcion'
    private DiaSemana(String descripcion) {
        this.descripcion = descripcion;
    }

    // Método para obtener la descripción
    public String getDescripcion() {
        return descripcion;
    }
}
  • Cada constante del enum tiene un valor adicional asociado (en este caso, descripcion).
  • El constructor de un enum siempre es privado o tiene un modificador de acceso implícito, ya que no se pueden crear instancias manualmente.
  • Las constantes de un enum son finales e inmutables. Por este motivo, no se define un método setDescripcion().
  • Todas las constantes y sus propiedades se cargan durante la inicialización de la clase. No es posible modificar las constantes ni sus valores en tiempo de ejecución.

Ejemplo del uso:

public class Main {
    public static void main(String[] args) {
        for (DiaSemana dia : DiaSemana.values()) {
            System.out.println(dia + ": " + dia.getDescripcion());
        }
    }
}

// SALIDA
// LUNES: Primer día de la semana
// MARTES: Segundo día de la semana
// ...
// DOMINGO: Séptimo día de la semana

DiaSemana.values() devuelve un arreglo (DiaSemana[]) que contiene todas las constantes del enum, en el mismo orden en el que fueron declaradas.

Métodos útiles de java.lang.Enum

Enum en Java es una clase final extendida implícitamente por el compilador, por lo que hereda métodos de la clase java.lang.Enum.

  1. values()
    Devuelve un arreglo con todas las constantes en el orden declarado.
  2. valueOf(String name)
    Busca una constante del enum por su nombre.
  3. ordinal()
    Devuelve la posición (índice) de la constante en el enum, comenzando desde 0.
  4. name()
    Retorna el nombre de la constante como una cadena.
public class App {
  public static void main(String[] args) {
    DiaSemana dia = DiaSemana.LUNES;
    // Acceder a las constantes del enum
    System.out.println(dia);
    // Obtener todos los valores del enum
    DiaSemana[] dias = DiaSemana.values();
    for (DiaSemana d : dias) {
      System.out.println(d);
    }

    // Comparar valores de enum
    if (dia == DiaSemana.LUNES) {
      System.out.println("Es LUNES");
    }
    // Utilizar enums en estructuras de control
    switch (dia) {
      case LUNES, MARTES, MIERCOLES, JUEVES, VIERNES ->
        System.out.println("Es un día laboral");
      case SABADO, DOMINGO -> System.out.println("Es fin de semana");
    }
  }
}

Excepciones

Una excepción es un evento que ocurre durante la ejecución de un programa y que interrumpe el flujo normal de las instrucciones del mismo.

En Java, existen dos tipos principales de excepciones:

  • Marcadas
  • No marcadas.

Excepciones Marcadas

Las excepciones marcadas son aquellas que deben ser manejadas explícitamente en el código mediante una estructura try-catch o declaradas en la firma del método con throws. Estas excepciones son previsibles y representan situaciones que pueden ocurrir durante la ejecución normal del programa.

El compilador de Java exige que se manejen o declaren, asegurando así que el programador considere su posible impacto y mitigue las interrupciones en el flujo del programa.

Ejemplos:

  1. IOException

    • Descripción: Se lanza durante operaciones fallidas de entrada/salida, como al leer o escribir archivos.

    • Ejemplo:

      try {
          BufferedReader lector = new BufferedReader(new FileReader("ruta/al/archivo"));
      } catch (IOException e) {
          System.out.println("Ocurrió un error al leer el archivo");
      }
  2. ClassNotFoundException

    • Descripción: Ocurre cuando se intenta cargar una clase que no existe o no está en el classpath.

    • Ejemplo:

      try {
          Class.forName("ClaseNoExistente");
      } catch (ClassNotFoundException e) {
          System.out.println("La clase no existe");
      }

Note

El classpath le dice a la JVM y a las herramientas de Java dónde buscar las clases y recursos requeridos por el programa. Si una clase o recurso no se encuentra en el classpath, se producirá un error como ClassNotFoundException o NoClassDefFoundError.

  1. FileNotFoundException

    • Descripción: Se lanza cuando se intenta acceder a un archivo que no existe en la ruta especificada.

    • Ejemplo:

      try {
          new FileInputStream("archivo_no_existente.txt");
      } catch (FileNotFoundException e) {
          System.out.println("Archivo no encontrado");
      }
  2. SQLException

    • Descripción: Se lanza cuando ocurre un error relacionado con una operación de base de datos, como consultas o conexión.

    • Ejemplo:

      try {
          // Supongamos que tienes un objeto de conexión "conexion"
          conexion.executeQuery("SELECT * FROM tabla_no_existente");
      } catch (SQLException e) {
          System.out.println("Error en la consulta SQL");
      }

Las excepciones marcadas:

  • Obligan a manejar errores previsibles: Esto permite al desarrollador implementar soluciones específicas o alertar adecuadamente al usuario.
  • Incrementan la robustez del código: Evitan que el programa falle inesperadamente.
  • Mejoran la claridad del código: Declaran explícitamente posibles fallos, ayudando a otros desarrolladores a entender mejor el flujo.

Excepciones No Marcadas

Las excepciones no marcadas (Runtime Exceptions) son problemas generados por errores en el código y se consideran bugs. Java no exige que se capturen, ya que asume que el programador evitará estas situaciones con un código adecuado. Las excepciones relacionadas con la lógica de negocio deben ser anticipadas y manejadas por el programador.

Ejemplo:

  1. NullPointerException

    • Descripción: Se lanza al intentar acceder a un miembro de un objeto nulo.

    • Ejemplo:

      String str = null;
      try {
          System.out.println(str.length());
      } catch (NullPointerException e) {
          System.out.println("El objeto es nulo");
      }
  2. ArrayIndexOutOfBoundsException

    • Descripción: Se lanza al intentar acceder a un índice inexistente de un arreglo.

    • Ejemplo:

      int[] arr = new int[5];
      try {
          int numero = arr[10];
      } catch (ArrayIndexOutOfBoundsException e) {
          System.out.println("El índice está fuera de los límites del arreglo");
      }
  3. ArithmeticException

    • Descripción: Ocurre al realizar operaciones aritméticas ilegales, como dividir por cero.

    • Ejemplo:

      try {
          int resultado = 10 / 0;
      } catch (ArithmeticException e) {
          System.out.println("División por cero");
      }
  4. IllegalArgumentException

    • Descripción: Se lanza al pasar un argumento inapropiado a un método.

    • Ejemplo:

      try {
          Thread.sleep(-1000);
      } catch (IllegalArgumentException e) {
          System.out.println("Argumento ilegal");
      }
  5. NumberFormatException

    • Descripción: Se lanza al intentar convertir un String a un número con formato inválido.

    • Ejemplo:

      try {
          int num = Integer.parseInt("no_es_un_numero");
      } catch (NumberFormatException e) {
          System.out.println("Formato de número inválido");
      }
  6. ClassCastException

    • Descripción: Ocurre al intentar hacer un casting a un tipo incompatible.

    • Ejemplo:

      Object x = new Integer(0);
      try {
          System.out.println((String) x);
      } catch (ClassCastException e) {
          System.out.println("No se puede convertir a String");
      }
  7. NegativeArraySizeException

    • Descripción: Se lanza al intentar crear un arreglo con tamaño negativo.

    • Ejemplo:

      try {
          int[] arr = new int[-1];
      } catch (NegativeArraySizeException e) {
          System.out.println("El tamaño del arreglo no puede ser negativo");
      }
  8. StringIndexOutOfBoundsException

    • Descripción: Se lanza al intentar acceder a un índice inexistente en un String.

    • Ejemplo:

      String str = "Hola";
      try {
          char ch = str.charAt(10);
      } catch (StringIndexOutOfBoundsException e) {
          System.out.println("Índice fuera de los límites del String");
      }

Excepciones Personalizadas

Es posible crear excepciones personalizadas extendiendo alguna clase de la jerarquía de excepciones. Esto permite un manejo de errores más específico y significativo.

Ejemplo:

public class MiExcepcionPersonalizada extends Exception {
    // Constructor sin argumentos
    public MiExcepcionPersonalizada() {
        super();
    }

    // Constructor con mensaje personalizado
    public MiExcepcionPersonalizada(String mensaje) {
        super(mensaje);
    }

    // Constructor con mensaje y causa
    public MiExcepcionPersonalizada(String mensaje, Throwable causa) {
        super(mensaje, causa);
    }
}

Ventajas:

  1. Facilitan la identificación de errores específicos en el código.
  2. Ofrecen mensajes personalizados que mejoran la claridad al depurar errores.
  3. Permiten incluir información adicional relevante al contexto del error.

Bloque finally y try-with-resources

El bloque finally se utiliza para ejecutar código importante como la limpieza de recursos. Este bloque siempre se ejecuta, independientemente de si una excepción fue lanzada o no.

Ejemplo:

public class Main {
    public static void main(String[] args) {
        try {
            // Código que puede lanzar una excepción
        } catch (Exception e) {
            // Manejar la excepción
        } finally {
            // Limpiar recursos
        }
    }
}

Palabras clave throw y throws

throw

  • Lanza una excepción explícitamente dentro del código.
  • Se usa con el operador new para crear una instancia de la excepción.
public void metodoA() throws MiExcepcion {
    if (condicion) {
        throw new MiExcepcion("Mensaje de error");
    }
}

throws

  • Declara en la firma de un método que este puede lanzar una excepción.
  • Indica que el método puede propagar la excepción al método que lo llama, por lo que el método invocador debe manejarla o declararla a su vez.
public void metodoB() throws MiExcepcion {
    // Código que puede lanzar MiExcepcion
}

Note

Diferencia:

  • throw: Lanza una excepción en el cuerpo del método.
  • throws: Declara qué excepciones puede lanzar un método.

Excepciones y Herencia en Java

Clase Throwable

La clase base para todas las excepciones en Java.

  • Atributos:
    • detailMessage: Mensaje que describe el motivo de la excepción.
  • Métodos:
    • getMessage(): Retorna el mensaje de error.
    • printStackTrace(): Imprime la pila de llamadas para depuración.
    • getCause(): Retorna la causa de la excepción, si existe.
    • getStackTrace(): Retorna un array con los elementos de la pila de llamadas.

Clase Error

Indica errores graves controlados por la JVM que generalmente no deben ser manejados por la aplicación.

Ejemplos comunes:

  1. OutOfMemoryError: La JVM no puede asignar memoria suficiente.

    int[] matriz = new int[Integer.MAX_VALUE];
  2. StackOverflowError: Se produce por una recursión infinita.

    void funcionRecursiva() {
        funcionRecursiva();
    }

Clase Exception

Utilizada para manejar errores en el código.

  • Puede ser de dos tipos:
    • Excepciones Marcadas (Checked): Deben ser manejadas o declaradas.
    • Excepciones No Marcadas (Unchecked): Relacionadas con errores de programación.

Esta jerarquía asegura un manejo estructurado de los errores, separando aquellos controlables por el programador de los errores críticos del sistema.

Clone this wiki locally