Saltar a contenido

Semana 11 - Modificadores y Palabras Clave en POO en Java

Modificadores y Palabras Clave en POO en Java

En Java, los modificadores y palabras clave se dividen en dos categorías principales: 1. Modificadores de acceso: Controlan la visibilidad de clases, métodos y atributos. 2. Modificadores no relacionados con acceso: Afectan el comportamiento o las características de los elementos.

Además, hay palabras clave específicas que se usan frecuentemente en el contexto de POO para gestionar instancias, referencias y verificaciones de tipo. A continuación, se detalla cada uno de estos aspectos.


Modificadores de Acceso

Los modificadores de acceso determinan quién puede acceder a una clase, método, atributo o constructor. Java tiene cuatro niveles de acceso:

1. public

  • Descripción: El elemento es accesible desde cualquier clase, sin restricciones.
  • Uso en POO: Se usa para exponer métodos y atributos que deben ser accesibles universalmente, como métodos de una API pública o clases principales.
  • Ejemplo:
    public class Persona {
        public String nombre; // Atributo accesible desde cualquier clase
    
        public void saludar() { // Método accesible desde cualquier clase
            System.out.println("Hola, soy " + nombre);
        }
    }
    
    class Main {
        public static void main(String[] args) {
            Persona persona = new Persona();
            persona.nombre = "Juan"; // Acceso directo al atributo público
            persona.saludar(); // Salida: Hola, soy Juan
        }
    }
    

2. protected

  • Descripción: El elemento es accesible dentro del mismo paquete y también en las subclases, incluso si están en diferentes paquetes.
  • Uso en POO: Útil en herencia para permitir que las subclases accedan a atributos o métodos de la superclase, manteniendo cierto nivel de encapsulamiento.
  • Ejemplo:
    package paquete1;
    
    public class Animal {
        protected String especie; // Accesible en el paquete y en subclases
    
        protected void hacerSonido() {
            System.out.println("El animal hace un sonido");
        }
    }
    
    package paquete2;
    
    import paquete1.Animal;
    
    public class Perro extends Animal {
        public void setEspecie() {
            especie = "Canino"; // Acceso permitido porque es protected
            hacerSonido(); // Acceso permitido
        }
    }
    
    class Main {
        public static void main(String[] args) {
            Perro perro = new Perro();
            perro.setEspecie(); // Salida: El animal hace un sonido
        }
    }
    

3. Acceso por defecto (package-private)

  • Descripción: Si no se especifica un modificador de acceso, el elemento es accesible solo dentro del mismo paquete.
  • Uso en POO: Se usa para limitar el acceso a clases, métodos o atributos a un grupo específico de clases dentro del mismo paquete, promoviendo el encapsulamiento.
  • Ejemplo:
    package paquete1;
    
    class Coche { // Acceso por defecto (solo en paquete1)
        String marca = "Toyota";
    
        void mostrarMarca() {
            System.out.println("Marca: " + marca);
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Coche coche = new Coche();
            coche.mostrarMarca(); // Salida: Marca: Toyota
            // Esto funciona porque Main está en el mismo paquete
        }
    }
    
    Nota: Si intentas acceder a Coche desde otro paquete, obtendrás un error de compilación.

4. private

  • Descripción: El elemento solo es accesible dentro de la misma clase.
  • Uso en POO: Es clave para el encapsulamiento, ya que protege los datos internos de una clase y fuerza el acceso a través de métodos públicos (como getters y setters).
  • Ejemplo:
    public class CuentaBancaria {
        private double saldo; // Solo accesible dentro de la clase
    
        public void depositar(double monto) {
            if (monto > 0) {
                saldo += monto;
            }
        }
    
        public double getSaldo() {
            return saldo; // Acceso controlado al atributo privado
        }
    }
    
    class Main {
        public static void main(String[] args) {
            CuentaBancaria cuenta = new CuentaBancaria();
            cuenta.depositar(100.0);
            System.out.println("Saldo: " + cuenta.getSaldo()); // Salida: Saldo: 100.0
            // cuenta.saldo = 200; // Error: saldo es private
        }
    }
    

Modificadores No Relacionados con Acceso

Estos modificadores afectan el comportamiento o las propiedades de clases, métodos o atributos, y son fundamentales en POO para definir cómo se usan los elementos.

1. static

  • Descripción: Indica que un atributo o método pertenece a la clase, no a las instancias. Solo existe una copia de un elemento static, compartida por todos los objetos.
  • Uso en POO:
  • Atributos static: Variables compartidas, como contadores o configuraciones globales.
  • Métodos static: Métodos utilitarios que no dependen del estado de un objeto (ejemplo: Math.sqrt()).
  • Ejemplo:
    public class Contador {
        private static int instanciasCreadas = 0; // Atributo estático
    
        public Contador() {
            instanciasCreadas++; // Incrementa el contador al crear un objeto
        }
    
        public static int getInstanciasCreadas() { // Método estático
            return instanciasCreadas;
        }
    }
    
    class Main {
        public static void main(String[] args) {
            Contador c1 = new Contador();
            Contador c2 = new Contador();
            System.out.println("Instancias creadas: " + Contador.getInstanciasCreadas());
            // Salida: Instancias creadas: 2
        }
    }
    

2. final

  • Descripción: Indica que un elemento no puede ser modificado después de su inicialización.
  • Variables final: Son constantes (no se pueden reasignar).
  • Métodos final: No pueden ser sobrescritos por subclases.
  • Clases final: No pueden ser heredadas.
  • Uso en POO: Garantiza inmutabilidad, protege la lógica de métodos críticos y evita la extensión no deseada de clases.
  • Ejemplo:
    public final class Configuracion { // Clase no heredable
        public final int MAX_USUARIOS = 100; // Constante
    
        public final void iniciarSistema() { // Método no sobrescribible
            System.out.println("Sistema iniciado con máximo " + MAX_USUARIOS + " usuarios");
        }
    }
    
    class Main {
        public static void main(String[] args) {
            Configuracion config = new Configuracion();
            config.iniciarSistema(); // Salida: Sistema iniciado con máximo 100 usuarios
            // config.MAX_USUARIOS = hotspot:6
            200; // Error: MAX_USUARIOS es final
        }
    }
    
    Nota: Intentar heredar de Configuracion o sobrescribir iniciarSistema generará un error de compilación.

3. abstract

  • Descripción: Se usa en clases y métodos para indicar que no están completamente implementados.
  • Clases abstract: No pueden instanciarse y pueden contener métodos abstractos o concretos.
  • Métodos abstract: No tienen implementación y deben ser implementados por las subclases.
  • Uso en POO: Permite definir un esqueleto común para un grupo de clases, promoviendo la abstracción y la herencia.
  • Ejemplo:
    public abstract class Vehiculo {
        protected String marca;
    
        public Vehiculo(String marca) {
            this.marca = marca;
        }
    
        public abstract void acelerar(); // Método abstracto
    
        public void mostrarMarca() { // Método concreto
            System.out.println("Marca: " + marca);
        }
    }
    
    public class Coche extends Vehiculo {
        public Coche(String marca) {
            super(marca);
        }
    
        @Override
        public void acelerar() {
            System.out.println("El coche está acelerando");
        }
    }
    
    class Main {
        public static void main(String[] args) {
            // Vehiculo v = new Vehiculo("Toyota"); // Error: Vehiculo es abstracto
            Coche coche = new Coche("Toyota");
            coche.mostrarMarca(); // Salida: Marca: Toyota
            coche.acelerar(); // Salida: El coche está acelerando
        }
    }
    

4. synchronized

  • Descripción: Se usa en métodos o bloques para garantizar que solo un hilo acceda a un recurso a la vez en entornos multihilo.
  • Uso en POO: Protege el estado de objetos compartidos en aplicaciones concurrentes, asegurando consistencia de datos.
  • Ejemplo:
    public class ContadorSincronizado {
        private int valor = 0;
    
        public synchronized void incrementar() {
            valor++;
        }
    
        public int getValor() {
            return valor;
        }
    }
    
    class Main {
        public static void main(String[] args) throws InterruptedException {
            ContadorSincronizado contador = new ContadorSincronizado();
    
            // Crear dos hilos que incrementan el contador
            Runnable tarea = () -> {
                for (int i = 0; i < 1000; i++) {
                    contador.incrementar();
                }
            };
    
            Thread hilo1 = new Thread(tarea);
            Thread hilo2 = new Thread(tarea);
            hilo1.start();
            hilo2.start();
            hilo1.join();
            hilo2.join();
    
            System.out.println("Valor final: " + contador.getValor());
            // Salida: Valor final: 2000 (sin synchronized, podría ser menor)
        }
    }
    

5. transient

  • Descripción: Indica que un atributo no debe ser serializado cuando un objeto se convierte en un flujo de bytes (usado con Serializable).
  • Uso en POO: Útil para excluir datos sensibles o temporales (como contraseñas o cachés) durante la serialización.
  • Ejemplo:
    import java.io.*;
    
    public class Usuario implements Serializable {
        private String nombre;
        private transient String contraseña; // No se serializa
    
        public Usuario(String nombre, String contraseña) {
            this.nombre = nombre;
            this.contraseña = contraseña;
        }
    
        @Override
        public String toString() {
            return "Usuario{nombre='" + nombre + "', contraseña='" + contraseña + "'}";
        }
    }
    
    class Main {
        public static void main(String[] args) throws IOException, ClassNotFoundException {
            Usuario usuario = new Usuario("Ana", "secreto");
    
            // Serializar
            ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("usuario.ser"));
            out.writeObject(usuario);
            out.close();
    
            // Deserializar
            ObjectInputStream in = new ObjectInputStream(new FileInputStream("usuario.ser"));
            Usuario usuarioDeserializado = (Usuario) in.readObject();
            in.close();
    
            System.out.println(usuarioDeserializado);
            // Salida: Usuario{nombre='Ana', contraseña='null'}
        }
    }
    

6. volatile

  • Descripción: Indica que una variable puede ser modificada por múltiples hilos, asegurando que los cambios sean visibles inmediatamente para todos los hilos.
  • Uso en POO: Útil en concurrencia para evitar problemas de caché en variables compartidas.
  • Ejemplo:
    public class Controlador {
        private volatile boolean activo = true;
    
        public void detener() {
            activo = false;
        }
    
        public void ejecutar() {
            while (activo) {
                System.out.println("Ejecutando...");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("Detenido");
        }
    }
    
    class Main {
        public static void main(String[] args) throws InterruptedException {
            Controlador controlador = new Controlador();
            Thread hilo = new Thread(controlador::ejecutar);
            hilo.start();
    
            Thread.sleep(2000); // Esperar 2 segundos
            controlador.detener(); // Detener el bucle
            hilo.join();
        }
    }
    

Palabras Clave Relacionadas con POO

Estas palabras clave son esenciales para trabajar con objetos, herencia y verificaciones de tipo en Java.

1. this

  • Descripción: Hace referencia a la instancia actual de la clase. Se usa para:
  • Diferenciar entre atributos de instancia y parámetros/métodos locales.
  • Invocar otros constructores de la misma clase.
  • Uso en POO: Mejora la claridad del código y permite la sobrecarga de constructores.
  • Ejemplo:
    public class Estudiante {
        private String nombre;
        private int edad;
    
        public Estudiante(String nombre) {
            this(nombre, 0); // Llama al constructor con dos parámetros
        }
    
        public Estudiante(String nombre, int edad) {
            this.nombre = nombre; // Diferencia atributo de parámetro
            this.edad = edad;
        }
    
        public void mostrarInfo() {
            System.out.println("Nombre: " + this.nombre + ", Edad: " + this.edad);
        }
    }
    
    class Main {
        public static void main(String[] args) {
            Estudiante estudiante = new Estudiante("Luis");
            estudiante.mostrarInfo(); // Salida: Nombre: Luis, Edad: 0
        }
    }
    

2. super

  • Descripción: Hace referencia a la superclase inmediata. Se usa para:
  • Acceder a constructores, métodos o atributos de la superclase.
  • Llamar al constructor de la superclase desde una subclase.
  • Uso en POO: Esencial en herencia para reutilizar o extender el comportamiento de la superclase.
  • Ejemplo:
    public class Animal {
        protected String nombre;
    
        public Animal(String nombre) {
            this.nombre = nombre;
        }
    
        public void hacerSonido() {
            System.out.println("Sonido genérico");
        }
    }
    
    public class Gato extends Animal {
        public Gato(String nombre) {
            super(nombre); // Llama al constructor de Animal
        }
    
        @Override
        public void hacerSonido() {
            super.hacerSonido(); // Llama al método de la superclase
            System.out.println("Miau!");
        }
    }
    
    class Main {
        public static void main(String[] args) {
            Gato gato = new Gato("Luna");
            gato.hacerSonido();
            // Salida: Sonido genérico
            //         Miau!
        }
    }
    

3. instanceof

  • Descripción: Verifica si un objeto es una instancia de una clase, interfaz o subclase específica.
  • Uso en POO: Útil para realizar conversiones de tipo seguras (downcasting) o para implementar lógica condicional basada en tipos.
  • Ejemplo:
    public interface Volador {
        void volar();
    }
    
    public class Pajaro implements Volador {
        @Override
        public void volar() {
            System.out.println("El pájaro vuela");
        }
    }
    
    public class Coche {
        public void conducir() {
            System.out.println("El coche conduce");
        }
    }
    
    class Main {
        public static void main(String[] args) {
            Object obj = new Pajaro();
    
            if (obj instanceof Pajaro) {
                Pajaro pajaro = (Pajaro) obj; // Downcasting seguro
                pajaro.volar(); // Salida: El pájaro vuela
            }
    
            if (obj instanceof Volador) {
                Volador volador = (Volador) obj;
                volador.volar(); // Salida: El pájaro vuela
            }
    
            if (!(obj instanceof Coche)) {
                System.out.println("No es un coche"); // Salida: No es un coche
            }
        }
    }
    

Resumen de Modificadores y Palabras Clave

Categoría Elemento Descripción
Modificadores de Acceso public Accesible desde cualquier clase.
protected Accesible en el mismo paquete y en subclases.
(por defecto) Accesible solo en el mismo paquete.
private Accesible solo dentro de la misma clase.
Modificadores No de Acceso static Pertenece a la clase, no a instancias.
final Impide modificación (variables), sobrescritura (métodos) o herencia (clases).
abstract Define clases/métodos sin implementación completa.
synchronized Controla acceso en entornos multihilo.
transient Excluye atributos de la serialización.
volatile Garantiza visibilidad de cambios en variables entre hilos.
Palabras Clave Relacionadas this Referencia a la instancia actual.
super Referencia a la superclase.
instanceof Verifica el tipo de un objeto.

Buenas Prácticas con Modificadores y Palabras Clave

  1. Usa private por defecto: Protege los datos internos y usa getters/setters para acceso controlado.
  2. Minimiza el uso de public: Solo expón lo necesario para mantener el encapsulamiento.
  3. Usa final para constantes y clases inmutables: Mejora la seguridad y claridad.
  4. Evita abusar de static: Los elementos estáticos pueden romper el diseño orientado a objetos si se usan como variables globales.
  5. Usa synchronized con cuidado: Considera alternativas modernas como Lock o clases concurrentes.
  6. Verifica tipos con instanceof antes de downcasting: Evita excepciones de tipo ClassCastException.
  7. Documenta el uso de super y this: Mejora la legibilidad del código.