Unidad 5. Programación Orientada a Objetos (POO)
Objetivos de la Unidad
- Comprender el paradigma orientado a objetos y sus diferencias con la programación estructurada.
- Diseñar y crear clases con atributos, métodos y constructores apropiados.
- Aplicar el principio de encapsulamiento mediante modificadores de acceso y getters/setters.
- Implementar múltiples constructores aprovechando la sobrecarga y la llamada a
this(...). - Utilizar miembros estáticos (
static) y constantes (static final) correctamente. - Usar el puntero
thispara referenciar el objeto actual y resolver el shadowing.
1. Introducción: Pensando en Objetos
La Programación Orientada a Objetos (POO) no es solo una sintaxis nueva; es un paradigma de programación. Un paradigma es un modelo o estilo fundamental de desarrollo de software.
Hasta ahora, has programado bajo el paradigma estructurado (o procedimental). En este enfoque, los datos (variables) y las acciones (funciones) están separados. El programador es como un cocinero que sigue una receta paso a paso: "coge estos huevos, bátelos, añade sal". Si el programa crece, terminamos con miles de variables sueltas y funciones que las modifican sin control, generando un código difícil de mantener ("código espagueti").
La POO propone un cambio radical: modelar el software basándonos en las cosas (objetos) que existen en el problema real. En lugar de separar datos y lógica, los empaquetamos juntos en unidades llamadas Objetos.
Documentación oficial
Java pertenece a la conocida empresa Oracle. Puedes consultar la documentación oficial de Oracle para ampliar tu conocimiento sobre Programación Orientada a Objetos.
1.1. Conceptos Fundamentales: La Tríada del Objeto
Para que algo sea considerado un objeto en este paradigma, debe cumplir con tres características esenciales que le dan sentido y autonomía:
- Identidad: Es la propiedad que permite distinguir un objeto de otro, incluso si sus contenidos son idénticos. En la vida real, dos monedas de un euro son iguales, pero son dos objetos distintos (tienen identidad propia). En Java, esta identidad la gestiona la referencia de memoria.
- Estado (Atributos): Son los datos que caracterizan al objeto en un momento dado. Representan las propiedades o características (ej: color, tamaño, velocidad actual).
- Comportamiento (Métodos): Son las acciones que el objeto puede realizar o las respuestas que puede dar ante mensajes de otros objetos (ej: frenar, cambiar de color, calcular total).
1.2. Clase vs. Objeto: La Abstracción
Es vital distinguir entre el concepto abstracto y la realidad concreta.
- La Clase (El plano): Es la definición abstracta, el plano o la plantilla. Define qué atributos y métodos tendrán los objetos, pero no contiene datos concretos ni ocupa memoria de ejecución para almacenarlos.
- El Objeto (La Instancia): Es la concreción de la clase. Es un elemento tangible en la memoria del ordenador que se ha creado a partir de la clase.
classDiagram
%% Definición de la Clase (El plano)
class Coche {
String color
String marca
int velocidad
arrancar()
frenar()
}
note for Coche "CLASE (Definición abstracta)"
%% Objetos (Instancias concretas)
class CocheDeJuan {
color: "Rojo"
marca: "Seat"
velocidad: 0
}
class CocheDeMaria {
color: "Azul"
marca: "Ford"
velocidad: 120
}
Coche <|-- CocheDeJuan : Instancia (new)
Coche <|-- CocheDeMaria : Instancia (new)
1.3. Ejemplos de Abstracción en la Vida Real
El proceso de diseñar clases se llama abstracción: consiste en observar un objeto real, ignorar los detalles irrelevantes y quedarse solo con las características importantes para nuestro programa.
Veamos cómo modelaríamos diferentes objetos cotidianos en clases de Java:
A. Una Lámpara
Si queremos programar un sistema de domótica, no nos importa de qué material es la bombilla, solo si está encendida o no.
- Estado (Atributos):
estaEncendida(boolean),intensidad(int). - Comportamiento (Métodos):
encender(),apagar(),regularIntensidad(int nivel).
B. Una Cuenta Bancaria
Para un banco, tu cuenta es un objeto digital.
- Estado:
numeroCuenta(String),saldo(double),titular(String). - Comportamiento:
ingresar(double cantidad),retirar(double cantidad),consultarSaldo().
C. Un Enemigo de Videojuego
Para un juego de rol.
- Estado:
puntosVida(int),fuerzaAtaque(int),tipoArma(String). - Comportamiento:
atacar(Personaje objetivo),recibirDano(int cantidad),gritar().
2. Clases y Objetos: El Plano y la Casa
Para entender la diferencia fundamental entre estos dos conceptos, usaremos una analogía arquitectónica:
-
La Clase es el Plano (Blueprints):
- El arquitecto dibuja un plano detallado. En él especifica dónde van las ventanas, de qué material son las paredes y cuántas habitaciones hay.
- Pero nadie puede vivir en un plano. El plano es solo papel (o un archivo digital). No tiene paredes físicas, ni techo, ni ocupa un lugar en la calle. Es pura información.
-
El Objeto es la Casa Construida:
- Usando ese mismo plano, una constructora puede edificar 50 casas en una urbanización.
- Cada casa es una instancia del plano.
- Aunque todas se hicieron con el mismo plano, cada casa es independiente:
- En la Casa Nº 1 pueden pintar las paredes de azul.
- En la Casa Nº 2 pueden tener las luces encendidas.
- Si se rompe una ventana en la Casa Nº 3, no se rompe en la Casa Nº 4.
En programación:
- La Clase define la estructura.
- El Objeto ocupa memoria y tiene sus propios valores.
2.1. Definición de una Clase en Java
Una clase se define con la palabra reservada class. Por convención, los nombres de las clases siempre empiezan por Mayúscula.
Vamos a crear nuestro hilo conductor: un Personaje de Videojuego.
🔎 Comprobación
Comprueba el funcionamiento del programa. Copia el código y ejecútalo en tu entorno para comprender cómo funciona.
Representación Visual (Diagrama de Clases UML):
Así es como representamos esta clase en un diagrama profesional:
classDiagram
class Personaje {
- String nombre
- int vida
- int nivel
+ void saludar()
}
2.2. Instanciación: Creando Objetos
Para crear un objeto (una "casa") a partir de la clase, usamos el operador new.
Diagrama de Instancias:
Aquí podemos ver cómo, a partir de una única clase Personaje, hemos creado dos objetos independientes en la memoria, cada uno con sus propios valores.
classDiagram
%% Definición de la Clase (El plano)
class Personaje {
- String nombre
- int vida
- int nivel
+ void saludar()
}
class Heroe {
- nombre: "Aragorn"
- vida: 100
- nivel: 1
}
class Villano {
- nombre: "Orco Feo"
- vida: 50
- nivel: 1
}
Personaje <|-- Heroe : Instancia (new)
Personaje <|-- Villano : Instancia (new)
💻 Momento de Práctica: Tu primer Objeto
- Crea una clase llamada
Cocheen un archivoCoche.java. - Añádele atributos:
marca(String),modelo(String) yvelocidad(int). - Crea un método
void infoCoche()que muestre un mensaje con la información del modelo y la marca. - En el
main, crea dos coches distintos (ej: un Ferrari y un Seat, con sus modelos) y muestra su información.
3. Miembros de una Clase
Una clase se compone de dos tipos de miembros: Atributos y Métodos.
3.1. Atributos (Variables de Instancia)
Son las variables que guardan el estado de cada objeto individualmente. Cada objeto tiene su propia copia de estas variables.
- Si cambias la vida del
heroe, la vida delvillanoNO cambia. Son independientes.
3.2. Métodos
Son las funciones que definen qué puede hacer el objeto. Pueden leer y modificar los atributos del propio objeto.
Vamos a ampliar nuestro Personaje con una nueva habilidad: recibirDano(int puntos).
💻 Momento de Práctica: ¡A la batalla!
Ahora te toca a ti probar este nuevo método. Vuelve a tu clase principal (JuegoMain) y modifica el código para simular una pelea:
- Copia el código nuevo del método
recibirDanodentro de tu clasePersonaje. - En el
main, haz que elvillanoreciba 30 puntos de daño. - Luego, haz que reciba otros 50 puntos.
- ¿Qué pasa si recibe un golpe fatal de 100 puntos? Comprueba que sale el mensaje de derrota.
3.3. El Puntero this: ¿Quién soy yo?
En Java, cuando escribes código dentro de una clase, a veces necesitas referirte al propio objeto que está ejecutando el código, es decir, "MI atributo" o "MI método". Para eso usamos la palabra clave this.
thises una referencia al objeto actual que está ejecutando el código.
Es muy útil para evitar ambigüedades cuando un parámetro se llama igual que un atributo. Esto se llama Shadowing (Sombreado).
Aplicándolo a nuestro Personaje
Vamos a añadir un método para cambiar el nombre del personaje, usando this para diferenciar el atributo del parámetro.
💻 Momento de Práctica: Ampliando el Coche
Vamos a mejorar tu clase Coche.
- Añade un método
void frenar(int cantidad)que reduzca la velocidad en la cantidad correspondiente. - Añade un método
void acelerar(int cantidad)que acelere la velocidad en la cantidad dada. - Lógica: El coche no puede tener velocidad negativa. Si al frenar baja de 0, la velocidad debe quedarse en 0 y mostrar un mensaje "¡Coche detenido!".
- Lógica: El coche no puede sobrepasar la velocidad máxima (por ejemplo 200). Si al acelerar pasa de 200, la velocidad debe quedarse en 200 y mostrar un mensaje "¡Velocidad máxima alcanzada!".
- Prueba a acelerar y frenar en el
main.
4. Encapsulamiento y Visibilidad (La Caja Negra)
El Encapsulamiento es el principio fundamental de la POO que nos dice: "Oculta los detalles internos y muestra solo lo que sea seguro usar".
4.1 ¿Por qué necesitamos proteger nuestros datos?
Imagina que estás conduciendo tu coche. Tienes acceso al volante, los pedales y la palanca de cambios (Interfaz Pública). Pero el fabricante del coche ha protegido el motor bajo un capó cerrado.
¿Qué pasaría si pudieras tocar los pistones del motor directamente mientras conduces?
- Integridad: Podrías romper el motor sin querer.
- Seguridad: Podrías hacerte daño.
- Complejidad: Tendrías que saber mecánica avanzada solo para ir a comprar pan.
En programación ocurre lo mismo. Si dejas que cualquiera modifique la vida de tu personaje directamente (heroe.vida = -5000), tu programa podría romperse o comportarse de forma absurda (¡personajes zombies con vida negativa!).
En Java, usamos los Modificadores de Acceso para controlar esto:
public: Acceso total. Es la "puerta abierta" de la clase.private: Acceso restringido. Solo la propia clase puede ver y tocar estos miembros.
Regla de Oro: Por defecto, los atributos deben ser siempre private.
4.2 Getters y Setters
Si hacemos los atributos privados, ¿cómo trabajamos con ellos? Usando métodos públicos controlados, conocidos como Accessors.
1. El Setter (Método de Modificación)
Se usa para asignar un valor a un atributo privado.
- Ventaja: Nos permite validar el dato antes de guardarlo. Si el dato es incorrecto, podemos rechazarlo o corregirlo.
- Analogía: Es como el guardia de seguridad de una discoteca. No deja entrar a cualquiera.
2. El Getter (Método de Lectura)
Se usa para recuperar el valor de un atributo privado.
- Ventaja: Nos permite controlar cómo se muestra el dato o incluso devolver un dato calculado.
- Analogía: Es como mirar a través de un cristal blindado. Puedes ver lo que hay, pero no tocarlo.
4.3. Métodos Ocultos (Privados)
A veces, una clase necesita realizar tareas internas complejas que no deben ser vistas ni usadas desde fuera. A estos métodos se les llama Helpers o métodos auxiliares, y se declaran como private.
Imagina un método que calcula si un golpe recibido es crítico. Solo la clase Personaje debe usarlo cuando recibe daño; nadie desde fuera debería poder llamar a esGolpeCritico() directamente.
Si el Personaje recibe un golpe crítico (tiene un 5% de posibilidades de recibirlo), al daño recibido se le añade 10 puntos. Nuestro método recibirDano puede llamar internamente al nuevo método:
4.4. Código Final de la Clase Personaje
Aquí tienes la versión profesional de nuestra clase Personaje, aplicando encapsulamiento, constructores y métodos.
🔎 Comprobación
Comprueba el funcionamiento del programa. Copia el código y ejecútalo en tu entorno para comprender cómo funciona.
4.5. Diagrama de Clases UML (Encapsulado)
Observa cómo cambian los símbolos:
-significa private (candado cerrado).+significa public (puerta abierta).
classDiagram
class Personaje {
- String nombre
- int vida
- int nivel
+ void saludar()
+ void recibirDano(int)
+ String getNombre()
+ void setNombre(String)
+ int getVida()
+ void setVida(int)
+ int getNivel()
+ void setNivel(int)
- boolean esGolpeCritico()
}
4.6. Ejemplo de Uso en el Main
Veamos cómo usamos esta clase segura. Fíjate que ya no podemos hacer p.vida = -100, el compilador no nos dejará.
💻 Momento de Práctica: Protegiendo tu Coche
Vuelve a tu clase Coche.
- Cambia la visibilidad de todos los atributos de la clase a
private. - Intenta acceder directamente desde el main (
miCoche.velocidad = 200). Verás que da error. - Crea un método
setVelocidad(int v)que impida poner una velocidad negativa. - Usa el setter en el main para probarlo.
5. Constructores: Naciendo con Estado
Hasta este momento, hemos creado nuestros objetos en dos pasos:
- Los creábamos vacíos con
new Personaje(). - Les dábamos valores línea a línea usando los setters.
¿El problema? Es un proceso propenso a errores humanos. ¿Qué pasa si creas un Personaje pero se te olvida ponerle nombre o vida? Tendrías un objeto "zombie" o incompleto circulando por tu programa, lo cual podría causar errores graves más adelante.
El Constructor es un método especial diseñado para solucionar esto. Se ejecuta automáticamente en el momento exacto de la creación del objeto (al usar new). Su misión es garantizar que el objeto nazca en un estado válido.
5.1. Características Técnicas
Para que un método sea un constructor, debe cumplir dos reglas estrictas:
- Tener exactamente el mismo nombre que la clase.
- No tener tipo de retorno (ni siquiera
void). No devuelve nada, devuelve el objeto mismo.
5.2. Tipos de Constructores
A. El Constructor por Defecto (El Invisible)
Si tú no escribes ningún constructor en tu clase, Java te regala uno invisible y vacío: public Personaje() {}. Este es el que nos permitía hacer new Personaje() hasta ahora.
Importante
En el momento en que escribas TÚ un constructor (con parámetros), Java retira el constructor por defecto. Si quieres seguir usándolo, tendrás que escribirlo explícitamente.
B. Constructores con Parámetros
Son los más útiles. Nos obligan a pasar los datos necesarios para que el objeto exista.
Ahora, es imposible crear un personaje vacío. El compilador nos obligará a hacer: new Personaje("Gandalf", 150, 5).
C. El Constructor de Copia
A veces necesitamos crear un objeto que sea una copia exacta de otro objeto que ya existe (un clon). Para eso usamos un constructor que recibe como parámetro un objeto de su misma clase.
5.3. Sobrecarga de Constructores (Overloading)
A veces queremos flexibilidad. Quizás queremos crear un personaje dando todos los datos, o quizás solo dando el nombre y asumiendo valores por defecto para el resto.
Podemos tener múltiples constructores siempre que sus parámetros sean diferentes (en cantidad o tipo).
5.4. Resultado Final: La Clase Personaje Profesional
Aquí tienes cómo queda nuestra clase integrando Encapsulamiento (atributos privados y getters/setters) y Constructores.
🔎 Comprobación
Comprueba el funcionamiento del programa. Copia el código y ejecútalo en tu entorno para comprender cómo funciona.
5.5. Uso en el Programa Principal
Fíjate cómo la creación de objetos ahora es mucho más robusta y limpia.
💻 Momento de Práctica: La Fábrica de Coches
Vamos a profesionalizar tu clase Coche.
- Elimina cualquier inicialización manual que tuvieras en el
main. - Crea un Constructor Completo que pida
marca,modeloyvelocidad(los datos los pedirá al usuario antes de llamar al constructor). - Crea un Constructor Parcial que solo pida
marcaymodelo(la velocidad debe iniciarse a 0). - Crea un Constructor Copia.
- En el
main, intenta crear un coche connew Coche(). ¿Qué ocurre? - Crea dos coches usando los dos constructores nuevos que has programado y muestra sus datos.
- Copia en un tercer coche, el coche segundo.
6. Miembros Estáticos (static)
Hasta ahora, cada objeto tenía su propia copia de los atributos (cada Personaje tenía su propio nombre). Pero, ¿y si queremos un dato que sea compartido por todos?
Imagina un contador de "Total de Personajes Creados".
- Si se lo damos a "Aragorn", solo él sabría el dato.
- Si se lo damos a "Legolas", tendríamos el dato duplicado.
- Ese dato no pertenece a un personaje concreto, pertenece al JUEGO (a la Clase).
Para eso usamos la palabra clave static.
- Atributo Estático: Existe una única copia en memoria para toda la aplicación. Si un objeto lo modifica, cambia para todos.
- Método Estático: Se puede llamar sin crear ningún objeto (como
Math.sqrt()). No puede usarthisporque no está ligado a ninguna instancia concreta.
Podríamos encapsular el atributo contadorPersonajes para que solo pueda ser modificado por el constructor. Si además, necesitamos acceder a su valor, podemos crear un Getter.
Uso:
6.1 Constantes estáticas
En la UD1 estudiamos el uso de constantes, que son aquellas que se identifican con la palabra final.
Este concepto está muy relacionado con los atributos estáticos. Por ejemplo el método Math.PI tiene la siguiente forma, donde puedes observar el uso de la palabra static:
Math. Excepcionalmente este tipo de atributos se crean públicos, por lo que no incluye getter.
Nuestra Clase Personaje también puede mejorarse utilizando esta técnica. Vamos a imaginar que la vida de nuestro personaje está limitada a 500. Para ello definimos la siguiente constante estática:
Esto nos permitiría hacer mejoras a nuestro código del siguiente tipo:
6.2. Código Final de la Clase Personaje
Aquí tienes la clase definitiva, integrando Encapsulamiento, Constructores, Lógica y Miembros Estáticos.
🔎 Comprobación
Comprueba el funcionamiento del programa. Copia el código y ejecútalo en tu entorno para comprender cómo funciona.
Los miembros estáticos suelen representarse subrayados. En este diagrama, el contador es estático.
classDiagram
class Personaje {
- String nombre
- int vida
- int nivel
- static int contadorPersonajes$
- static final int VIDA_MAX$
+ Personaje(String, int, int)
+ Personaje(String)
+ Personaje(Personaje)
+ void saludar()
+ void recibirDano(int)
+ String getNombre()
+ void setNombre(String)
+ int getVida()
+ void setVida(int)
+ int getNivel()
+ void setNivel(int)
- boolean esGolpeCritico()
+ static int getTotalPersonajes()$
}
6.3. Ejemplo de Uso en el Main
Observa cómo accedemos a la información estática usando el Nombre de la Clase (Personaje.algo), no el nombre de un objeto.
💻 Momento de Práctica: La Fábrica de Coches (Final)
Para terminar tu clase Coche, vamos a añadirle control de producción.
- Añade un atributo privado y estático
contadorCoches. - Modifica tus constructores para que aumenten este contador cada vez que se cree un coche nuevo.
- Crea un método público y estático
getCochesFabricados()que devuelva ese número. - Añade también una Constante Estática (public static final) llamada
RUEDAS = 4(todos los coches tienen 4 ruedas). - En el
main:- Imprime cuántas ruedas tiene un coche usando la constante.
- Crea 3 coches.
- Imprime el total de coches fabricados llamando al método estático.