Currículo: esta unidad cubre parte de los saberes básicos del Bloque A – Programación (PRYC.2.A.2) correspondiente a 2º Bachillerato. Además, se evalúan los criterios que puedes encontrar al final de esta página.
Tabla de contenidos
- 1. ¿Qué es la programación orientada a objetos?
- 2. Clases, objetos y constructores
- 3. Herencia, superclases y subclases
- 4. Práctica guiada.
En este tema vamos a dar el salto desde la forma tradicional de programar en Java (programación estructurada) hacia un nuevo paradigma: la Programación Orientada a Objetos (POO).
La finalidad es que comprendas qué aporta este paradigma, cómo se construyen programas basados en objetos y cómo estos objetos colaboran entre sí para resolver problemas.
1. ¿Qué es la programación orientada a objetos?
La programación orientada a objetos es una forma de organizar el código basada en objetos, que son entidades que tienen datos y realizan acciones.
Mientras que en la programación estructurada escribimos código paso a paso, en la POO modelamos el problema creando «cosas» parecidas a las de la vida real.
Por ejemplo, si estuviéramos programando un videojuego, podríamos tener objetos como Jugador, Enemigo, Arma, Mapa… Cada uno con sus datos y comportamientos.
2. Clases, objetos y constructores
2.1. Las clases
Una clase es un molde o plantilla que describe cómo serán los objetos que creemos.
En una clase definimos:
- Atributos → datos que representan el estado del objeto.
- Métodos → acciones que ese objeto puede realizar.
Por ejemplo: clase Coche
public class Coche {
String marca;
int velocidad;
void acelerar() {
velocidad += 10;
}
}Esta clase no es un coche real: es la receta para construir coches.
Ejercicio 3.1
Escribe una clase para modelar a una persona, un animal, un comercio y un centro educativo. Cada clase debe tener al menos tres atributos y dos métodos.
2.2. Los objetos
Un objeto es una instancia real creada a partir de una clase.
Cuando escribimos:
Coche c1 = new Coche();
estamos creando un coche concreto con su propia marca y velocidad.
Así creamos y usamos un objeto:
Coche c1 = new Coche(); c1.marca = "Toyota"; c1.acelerar(); System.out.println(c1.velocidad); // 10
Como puedes ver, cada objeto tiene su propio estado, independiente del resto de objetos de la aplicación.
Ejercicio 3.2
Escribe un programa que cree un objeto de cada clase y haga uso de sus métodos.
2.3. Constructores
Un constructor es un método especial que se llama igual que la clase y que se ejecuta automáticamente al crear un objeto. Sirve para inicializar los atributos.
Por ejemplo:
public class Coche {
String marca;
int velocidad;
public Coche() {
marca = "Toyota";
velocidad = 0;
}
}Cuando hacemos:
Coche c1 = new Coche();
automáticamente se ejecuta el constructor de la clase y se inicializa la marca del objeto a “Toyota”.
Ejercicio 3.3
Escribe el constructor de cada una de las clases creadas en el Ejercicio 3.1.
2.4. Sobrecarga de constructores
Podemos crear varios constructores con distintos parámetros, lo cual nos permite crear objetos de diferentes formas en función de las necesidades que tengamos en cada momento.
// Constructor sin parámetros
public Coche() {
marca = "Desconocida";
velocidad = 0;
}
// Constructor con un parámetro
public Coche(String marca) {
this.marca = marca;
velocidad = 0;
}Ejercicio 3.4
Elige dos clases de las que escribiste en el Ejercicio 3.1 y escribe al menos tres constructores para cada una de las dos.
2.5. Encapsulamiento y ocultación
Uno de los principios fundamentales de la POO es proteger los datos internos de un objeto.
Para ello usamos:
- private → oculta los atributos.
- getters y setters → acceso y modificación de los datos respectivamente.
Por ejemplo:
public class CuentaBancaria {
private double saldo;
public void ingresar(double cantidad) {
saldo += cantidad;
}
public double getSaldo() {
return saldo;
}
}¿Por qué encapsular?
- Evita errores.
- Permite controlar qué se puede modificar y qué no.
- Hace el código más robusto.
Ejercicio 3.5
Elige una de las dos clases que no elegiste en el ejercicio anterior y modifícala para ocultar todos sus atributos y acceder a ellos con getters y setters.
3. Herencia, superclases y subclases
La herencia permite crear clases nuevas basadas en otras ya existentes.
Por ejemplo: si Vehiculo es una clase general, Coche y Moto pueden heredar de ella.
public class Vehiculo {
int velocidad;
void acelerar() { velocidad += 10; }
}
public class Coche extends Vehiculo {
int numeroPuertas;
}La subclase hereda atributos y métodos de la superclase, y puede aportar atributos y/o métodos propios.
Cuando se escriben relaciones de herencia en nuestros programas debemos dominar el lenguaje propio utilizado: la clase desde la que se hereda –la clase madre– se llama superclase, y la clase nueva –la clase hija– se llama subclase.
De esta manera podemos construir una jerarquía de clases tan compleja como deseemos:

3.1. Sobreescritura (override)
Una subclase, puede:
- Simplemente usar lo que hereda.
- Añadir atributos y/o métodos nuevos.
- Modificar métodos heredados. A esto es a lo que llamamos sobreescritura (override).
Por ejemplo: la clase Coche puede sobreescribir el método acelerar de la superclase Vehículo.
public class Vehiculo {
int velocidad;
void acelerar() { velocidad += 10; }
}
public class Coche extends Vehiculo {
int numeroPuertas;
@Override
void acelerar() {
velocidad += 20; // Los coches aceleran más rápido
}
}Ejercicio 3.6
Crea una clase Moto con los atributos y métodos que consideres, que herede de Vehículo y que sobreescriba alguno de los métodos de la superclase. Crea un método main que haga uso de la clase Moto y demuestre el funcionamiento del método sobreescrito. (Para ello deberás crear un objeto de tipo Vehículo, otro objeto de tipo Moto y llamar al mismo método de ambos objetos)
3.2. Interfaces
Una interface define un conjunto de métodos sin implementar. Las clases que la implementan se comprometen a escribir esos métodos.
La existencia de las interfaces está plenamente justificada porque nos permiten crear código más flexible, al mismo tiempo que permiten que clases distintas compartan comportamientos sin heredar, lo cual es muy útil en muchas ocasiones.
Por ejemplo:
// Definición de la interface
public interface Conducible {
void arrancar();
void parar();
}
// Uso de la interface
public class Coche implements Conducible {
public void arrancar() {
// código del método
}
public void parar() {
// código del método
}
}Una interface no representa qué es un objeto, sino qué puede hacer. Este matiz es fundamental y es importante que se entienda.
Por ejemplo:
- Un mando de consola, un teclado y un ratón son objetos muy distintos, pero todos permiten introducir acciones.
- Una tarjeta bancaria, un móvil y un reloj inteligente pueden pagar, aunque internamente funcionen de forma distinta (sus atributos y métodos son muy distintos).
En programación, una interfaz funciona igual: define una capacidad, no una identidad.
3.3. Polimorfismo
El polimorfismo permite que diferentes objetos respondan de forma distinta al mismo mensaje.
Piensa en un programa en el que tenemos distintos tipos de vehículos y queremos ponerlos en marcha. Una solución típica de un programador novato sería algo así:
- Si es un coche, arranca de una forma.
- Si es una moto, arranca de otra.
- Si es un camión, arranca de otra.
Eso nos llevaría a:
- Muchos
ifoswitch. - Código difícil de leer.
- Código muy difícil de ampliar (cada nuevo tipo obliga a tocar el código existente).
Aquí aparece la pregunta clave:
👉 ¿no sería mejor decir simplemente “arranca” y que cada objeto se encargue de hacerlo a su manera?
Esa es exactamente la razón de ser del polimorfismo.
Por ejemplo:
public interface Conducible {
void arrancar();
}
public class Coche implements Conducible {
@Override
public void arrancar() {
System.out.println("El coche arranca girando la llave y soltando el embrague");
}
}
public class Moto implements Conducible {
@Override
public void arrancar() {
System.out.println("La moto arranca pulsando el botón de arranque");
}
}
Conducible v1 = new Coche();
Conducible v2 = new Moto();Aquí ocurre algo muy importante:
- El tipo de la referencia es el mismo (
Conducible). - El tipo real del objeto es distinto (
CocheyMoto).
Esto significa que, desde el punto de vista del programa, ambos objetos se tratan igual… hasta que reciben un mensaje.
v1.arrancar(); v2.arrancar();
Desde el punto de vista del código:
- Se llama dos veces al mismo método.
- No hay
if. - No hay comprobaciones de tipo.
- No hay decisiones explícitas.
Sin embargo, lo que ocurre en ejecución es distinto:
v1.arrancar()ejecuta el código deCoche.v2.arrancar()ejecuta el código deMoto.
Esto es polimorfismo puro:
👉 el programa no decide qué hacer; decide el objeto.
4. Práctica guiada.
En este ejercicio vamos a ver, paso a paso, cómo podemos usar la programación orientada a objetos para modelar un pequeño videojuego con varios tipos de personajes: hechicero, guerrero y campesino, todos ellos derivados de un personaje básico.
Queremos crear un mini videojuego muy simple, donde:
- Existe una clase base
Personajecon atributos y métodos comunes. - Hay tres subclases:
Hechicero,GuerreroyCampesino, que heredan dePersonaje. - Tendremos una interfaz que represente una habilidad especial (por ejemplo,
HabilidadEspecial). - Usaremos constructores, sobrecarga, encapsulamiento (atributos privados + getters/setters), herencia, interfaces y polimorfismo.
- En un
mainsimularemos una dinámica básica de juego con unos pocos turnos.
La idea es que puedas seguir el razonamiento línea a línea y luego adaptar el ejemplo a tus propias ideas de videojuegos.
4.1. Diseño de la jerarquía de clases
Vamos a planificar mentalmente la estructura POO antes de escribir código. Pensemos un momento en el diseño.
Vamos a definir:
- Una superclase
Personaje, con:- Atributos:
nombre,vida,ataqueBase. - Métodos:
atacar(Personaje objetivo),recibirDanio(int puntos),estaVivo(), getters y setters.
- Atributos:
- Tres subclases:
Guerrero→ pega fuerte con ataques físicos.Hechicero→ lanza hechizos.Campesino→ hace poco daño, pero puede tener una habilidad útil.
- Una interfaz
HabilidadEspecialcon un método:void usarHabilidadEspecial(Personaje objetivo);
De esta forma, podremos crear una lista de personajes (todos vistos como Personaje) y que cada uno se comporte de forma distinta al atacar o usar su habilidad.
4.2. Implementación
Vamos a ir escribiendo el código y comentando las decisiones.
Empezamos con la clase general de la que heredarán todos:
public class Personaje {
// Atributos privados -> encapsulación
private String nombre;
private int vida;
private int ataqueBase;
// Constructor principal
public Personaje(String nombre, int vida, int ataqueBase) {
this.nombre = nombre;
this.vida = vida;
this.ataqueBase = ataqueBase;
}
// Constructor sobrecargado: vida por defecto
public Personaje(String nombre) {
this(nombre, 100, 10); // llamamos al otro constructor
}
// Getters (solo lectura desde fuera)
public String getNombre() {
return nombre;
}
public int getVida() {
return vida;
}
public int getAtaqueBase() {
return ataqueBase;
}
// Setter controlado de vida (no usamos setVida público a lo loco)
protected void setVida(int vida) {
this.vida = Math.max(0, vida); // nunca menos de 0
}
// Método común para atacar (se podrá sobreescribir)
public void atacar(Personaje objetivo) {
System.out.println(this.nombre + " ataca a " + objetivo.getNombre() +
" y causa " + ataqueBase + " puntos de daño.");
objetivo.recibirDanio(ataqueBase);
}
// Método para recibir daño
public void recibirDanio(int puntos) {
setVida(this.vida - puntos);
System.out.println(nombre + " recibe " + puntos + " puntos de daño. Vida restante: " + vida);
}
// Método para saber si sigue vivo
public boolean estaVivo() {
return vida > 0;
}
}Ahora definimos una interfaz que represente una acción especial que algunos personajes pueden realizar.
public interface HabilidadEspecial {
void usarHabilidadEspecial(Personaje objetivo);
}Esta interfaz nos ayudará a practicar el polimorfismo con tipos de referencia de interfaz.
Vamos a crear un guerrero que hereda de Personaje e implementa HabilidadEspecial.
public class Guerrero extends Personaje implements HabilidadEspecial {
private int bonificacionEspada;
// Constructor
public Guerrero(String nombre, int vida, int ataqueBase, int bonificacionEspada) {
super(nombre, vida, ataqueBase); // llamamos al constructor de la superclase
this.bonificacionEspada = bonificacionEspada;
}
// Constructor sobrecargado con valores por defecto
public Guerrero(String nombre) {
this(nombre, 120, 15, 5);
}
// El guerrero ataca con ataque base + bonificación
@Override
public void atacar(Personaje objetivo) {
int danio = getAtaqueBase() + bonificacionEspada;
System.out.println(getNombre() + " golpea con su espada a " + objetivo.getNombre() +
" causando " + danio + " puntos de daño.");
objetivo.recibirDanio(danio);
}
// Habilidad especial: golpe poderoso (más daño que un ataque normal)
@Override
public void usarHabilidadEspecial(Personaje objetivo) {
int danio = getAtaqueBase() + bonificacionEspada + 10;
System.out.println(getNombre() + " usa GOLPE PODEROSO sobre " + objetivo.getNombre() +
" causando " + danio + " puntos de daño devastador.");
objetivo.recibirDanio(danio);
}
}Ahora creamos un hechicero que lanza hechizos. También tendrá habilidad especial.
public class Hechicero extends Personaje implements HabilidadEspecial {
private int poderMagico;
public Hechicero(String nombre, int vida, int ataqueBase, int poderMagico) {
super(nombre, vida, ataqueBase);
this.poderMagico = poderMagico;
}
public Hechicero(String nombre) {
this(nombre, 80, 8, 20);
}
@Override
public void atacar(Personaje objetivo) {
int danio = getAtaqueBase() + poderMagico / 4;
System.out.println(getNombre() + " lanza un hechizo básico a " + objetivo.getNombre() +
" causando " + danio + " puntos de daño mágico.");
objetivo.recibirDanio(danio);
}
// Habilidad especial: bola de fuego
@Override
public void usarHabilidadEspecial(Personaje objetivo) {
int danio = getAtaqueBase() + poderMagico;
System.out.println(getNombre() + " lanza una BOLA DE FUEGO sobre " + objetivo.getNombre() +
" causando " + danio + " puntos de daño mágico.");
objetivo.recibirDanio(danio);
}
}Observa cómo, usando la misma interfaz HabilidadEspecial, cada clase implementa la habilidad de forma totalmente diferente: esto es polimorfismo.
Por último, vamos a crear un personaje débil en combate, pero con una habilidad de apoyo.
public class Campesino extends Personaje implements HabilidadEspecial {
private int capacidadCuracion;
public Campesino(String nombre, int vida, int ataqueBase, int capacidadCuracion) {
super(nombre, vida, ataqueBase);
this.capacidadCuracion = capacidadCuracion;
}
public Campesino(String nombre) {
this(nombre, 90, 5, 15);
}
@Override
public void atacar(Personaje objetivo) {
System.out.println(getNombre() + " golpea torpemente a " + objetivo.getNombre() +
" causando " + getAtaqueBase() + " puntos de daño.");
objetivo.recibirDanio(getAtaqueBase());
}
// Habilidad especial: curar a un aliado (en este ejemplo, se cura a sí mismo)
@Override
public void usarHabilidadEspecial(Personaje objetivo) {
// En este caso ignoramos 'objetivo' y se cura a sí mismo
int nuevaVida = getVida() + capacidadCuracion;
System.out.println(getNombre() + " se cura a sí mismo " + capacidadCuracion +
" puntos de vida. Vida actual: " + nuevaVida);
// usamos el setVida protegido de la superclase
setVida(nuevaVida);
}
// Necesitamos poder llamar a setVida desde aquí, así que en Personaje lo pusimos como protected
@Override
protected void setVida(int vida) {
super.setVida(vida);
}
}En esta clase vemos que:
- Sigue existiendo herencia y polimorfismo (otra implementación distinta de
usarHabilidadEspecial). - La habilidad ya no es ofensiva, sino de curación, para que veas que la interfaz no obliga a que todas las habilidades sean ataques.
4.3. El programa principal
Ahora reunimos todo en una clase con main para simular una pequeña partida.
Aquí es donde verás el polimorfismo de forma clara, usando referencias de tipo Personaje y HabilidadEspecial.
public class Juego {
public static void main(String[] args) {
// Creamos los personajes
Personaje guerrero = new Guerrero("Arthas");
Personaje hechicero = new Hechicero("Jaina");
Personaje campesino = new Campesino("Paco el Campesino");
// También podemos verlos como HabilidadEspecial si queremos usar sus habilidades
HabilidadEspecial hGuerrero = (HabilidadEspecial) guerrero;
HabilidadEspecial hHechicero = (HabilidadEspecial) hechicero;
HabilidadEspecial hCampesino = (HabilidadEspecial) campesino;
System.out.println("=== COMIENZA LA PARTIDA ===");
System.out.println();
// Turno 1: el guerrero ataca al hechicero
guerrero.atacar(hechicero);
System.out.println();
// Turno 2: el hechicero responde al guerrero
hechicero.atacar(guerrero);
System.out.println();
// Turno 3: el campesino intenta atacar al hechicero
campesino.atacar(hechicero);
System.out.println();
// Turno 4: el guerrero usa su habilidad especial contra el hechicero
hGuerrero.usarHabilidadEspecial(hechicero);
System.out.println();
// Turno 5: el campesino usa su habilidad especial para curarse
hCampesino.usarHabilidadEspecial(campesino);
System.out.println();
// Turno 6: el hechicero lanza su habilidad especial contra el guerrero
hHechicero.usarHabilidadEspecial(guerrero);
System.out.println();
// Mostramos el estado final de cada personaje
System.out.println("=== ESTADO FINAL DE LOS PERSONAJES ===");
mostrarEstado(guerrero);
mostrarEstado(hechicero);
mostrarEstado(campesino);
}
public static void mostrarEstado(Personaje p) {
System.out.println(p.getNombre() + " - Vida: " + p.getVida() +
(p.estaVivo() ? " (VIVO)" : " (MUERTO)"));
}
}En este main estamos usando:
- Polimorfismo con la superclase: variables de tipo
Personajeque apuntan a objetosGuerrero,HechiceroyCampesino.
Cuando llamamos aatacar, se ejecuta la versión correspondiente a la clase real del objeto. - Polimorfismo con la interfaz: variables de tipo
HabilidadEspecial(hGuerrero,hHechicero,hCampesino).
El mismo métodousarHabilidadEspecialse comporta de forma distinta según el tipo real del objeto. - Una dinámica básica de juego: varios turnos donde cada personaje realiza acciones, y al final consultamos sus vidas.