Programación Orientada a Objetos en la universidad: herencia, polimorfismo e interfaces con ejercicios de examen resueltos en Java

Programación Orientada a Objetos (POO) es la asignatura de primero o segundo de Ingeniería Informática en la que dejas de escribir funciones sueltas y empiezas a diseñar programas como un conjunto de clases que colaboran: encapsulación, herencia, polimorfismo, clases abstractas, interfaces, colecciones, genéricos y excepciones. Según la universidad se llama Programación II, Metodología de la Programación, Tecnología de la Programación o directamente POO, y casi siempre se da en Java, aunque en algunas es C++. Para aprobarla tienes que saber pasar de un enunciado a una jerarquía de clases, predecir qué imprime un programa con herencia y polimorfismo, ordenar objetos con Comparable y Comparator, escribir bien equals y hashCode y diseñar tus propias excepciones.
Es la asignatura donde más gente que aprobó Fundamentos de Programación se queda atascada. El motivo es que ya no basta con que el programa funcione: el examen corrige el diseño (qué clase hace qué, qué es abstracto, qué es privado) y tiene preguntas de "¿qué imprime este código?" pensadas para que caigas si no sabes exactamente cómo decide Java qué método se ejecuta. Aquí tienes qué entra, seis ejercicios de examen resueltos, las preguntas de teoría que más se repiten, los errores que más vemos en clase y cómo prepararla.
Qué entra en Programación Orientada a Objetos
El temario cambia poco de una universidad a otra. Lo que cambia es el lenguaje y el peso de la práctica frente al examen.
| Bloque | Qué te piden | Lo que más falla |
|---|---|---|
| Clases y objetos | Atributos, métodos, constructores, this, miembros static, encapsulación con private y métodos de acceso |
Atributos públicos; confundir lo que es de la clase (static) con lo que es de cada objeto |
| Herencia | extends, super, sobrescritura, protected, final, la clase Object |
Llamar a super(...) fuera de la primera línea; heredar para reutilizar dos líneas |
| Polimorfismo | Tipo estático y dinámico, enlace dinámico, casting, instanceof, sobrecarga frente a sobrescritura |
Las preguntas de "¿qué imprime?" |
| Clases abstractas e interfaces | abstract, interface, métodos default, implementar varias interfaces |
Elegir entre una y otra sin saber justificarlo |
| Colecciones y genéricos | List, Set, Map, ArrayList, HashMap, iteradores, Comparable, Comparator, tipos genéricos |
equals y hashCode; ordenar por varios criterios |
| Excepciones | try, catch, finally, throw, throws, comprobadas frente a no comprobadas, excepciones propias |
El orden de los catch; tragarse excepciones con un catch vacío |
| Diseño | Diagramas de clases UML, composición frente a herencia, a veces principios SOLID y algún patrón de diseño | Usar herencia donde había una relación "tiene un" |
Lo normal es que se evalúe con un examen escrito (en muchas universidades, código Java en papel), una o varias prácticas en grupo con una defensa oral en la que el profesor pregunta por tu código, y a veces un examen en el laboratorio delante del ordenador.
Sobrecarga y sobrescritura: la diferencia que siempre preguntan
Sobrecargar es tener varios métodos con el mismo nombre y distintos parámetros en la misma clase (o en una subclase): saludar(Animal a) y saludar(Perro p). Java decide cuál se llama al compilar, mirando el tipo declarado de los argumentos.
Sobrescribir es que una subclase vuelva a escribir un método que ha heredado, con el mismo nombre y los mismos parámetros, para cambiar lo que hace. Java decide qué versión se ejecuta en tiempo de ejecución, mirando el tipo real del objeto. Eso es el enlace dinámico, y es lo que hace funcionar el polimorfismo.
Detrás de casi todas las preguntas trampa está la misma idea: una variable tiene un tipo estático, el que pone en su declaración, y apunta a un objeto con un tipo dinámico, el que se creó con new. En Animal a = new Perro();, el tipo estático es Animal y el dinámico, Perro. El tipo estático decide qué puedes llamar y qué sobrecarga se elige; el dinámico decide qué versión sobrescrita se ejecuta.
Seis ejercicios típicos resueltos
El código es Java 17 o posterior, lo que se usa en la mayoría de universidades. Al final tienes qué cambia si tu asignatura es en C++.
1. De un enunciado a una jerarquía de clases
Enunciado: una empresa tiene empleados fijos, que cobran un sueldo mensual, y empleados por horas, que cobran un precio por hora; las horas por encima de 160 al mes se pagan un 50 % más caras. Los comerciales son empleados fijos que, además, cobran una comisión del 5 % de sus ventas. Diseña las clases y escribe un método que calcule el total de la nómina mensual. No debe poder crearse un empleado sin tipo.
"No debe poder crearse un empleado sin tipo" significa que Empleado es una clase abstracta. Todos los empleados tienen un salario, pero cada tipo lo calcula de una forma, así que calcularSalario() es un método abstracto:
public abstract class Empleado {
private final String nombre;
private final String dni;
protected Empleado(String nombre, String dni) {
this.nombre = nombre;
this.dni = dni;
}
public abstract double calcularSalario();
public String getNombre() {
return nombre;
}
public String getDni() {
return dni;
}
@Override
public String toString() {
return String.format("%s (%s): %.2f €", nombre, dni, calcularSalario());
}
}
public class EmpleadoFijo extends Empleado {
private final double sueldoMensual;
public EmpleadoFijo(String nombre, String dni, double sueldoMensual) {
super(nombre, dni);
if (sueldoMensual < 0) {
throw new IllegalArgumentException("El sueldo no puede ser negativo");
}
this.sueldoMensual = sueldoMensual;
}
@Override
public double calcularSalario() {
return sueldoMensual;
}
}
public class EmpleadoPorHoras extends Empleado {
private static final int HORAS_ORDINARIAS = 160;
private static final double RECARGO_HORAS_EXTRA = 1.5;
private final double precioHora;
private int horasTrabajadas;
public EmpleadoPorHoras(String nombre, String dni, double precioHora) {
super(nombre, dni);
this.precioHora = precioHora;
}
public void registrarHoras(int horas) {
if (horas <= 0) {
throw new IllegalArgumentException("Las horas deben ser positivas");
}
horasTrabajadas += horas;
}
@Override
public double calcularSalario() {
int ordinarias = Math.min(horasTrabajadas, HORAS_ORDINARIAS);
int extra = Math.max(0, horasTrabajadas - HORAS_ORDINARIAS);
return ordinarias * precioHora + extra * precioHora * RECARGO_HORAS_EXTRA;
}
}
public class Comercial extends EmpleadoFijo {
private static final double COMISION = 0.05;
private double ventas;
public Comercial(String nombre, String dni, double sueldoMensual) {
super(nombre, dni, sueldoMensual);
}
public void registrarVenta(double importe) {
if (importe <= 0) {
throw new IllegalArgumentException("El importe debe ser positivo");
}
ventas += importe;
}
@Override
public double calcularSalario() {
return super.calcularSalario() + ventas * COMISION;
}
}
import java.util.ArrayList;
import java.util.List;
public class Nomina {
private final List<Empleado> empleados = new ArrayList<>();
public void contratar(Empleado empleado) {
empleados.add(empleado);
}
public double totalMensual() {
double total = 0;
for (Empleado empleado : empleados) {
total += empleado.calcularSalario();
}
return total;
}
}
Con un fijo de 1.800 €, un empleado por horas a 15 €/h que ha trabajado 170 horas y un comercial con 1.500 € de sueldo y 20.000 € en ventas:
| Empleado | Cálculo | Salario |
|---|---|---|
| Fijo | 1.800 | 1.800 € |
| Por horas | 160 × 15 + 10 × 15 × 1,5 = 2.400 + 225 | 2.625 € |
| Comercial | 1.500 + 20.000 × 0,05 | 2.500 € |
| Total | 6.925 € |
Lo que hay que entender: totalMensual() no sabe qué tipos de empleado existen y no tiene ni un if ni un instanceof. Llama a calcularSalario() y cada objeto ejecuta su versión: eso es el polimorfismo. Si mañana aparece un nuevo tipo de empleado, se añade una clase y Nomina no se toca. Si en un examen escribes if (e instanceof EmpleadoFijo) ... else if (e instanceof EmpleadoPorHoras) ..., el programa funciona, pero te quitan buena parte de la nota de diseño. Fíjate también en que Comercial reutiliza el cálculo de su padre con super.calcularSalario() en vez de copiarlo, en que los atributos son private y no protected (las subclases no los necesitan) y en que super(...) es siempre la primera línea del constructor.
2. ¿Qué imprime? Polimorfismo, sobrecarga y atributos
Enunciado: indica qué imprime cada línea marcada del método main. Si alguna no compila, explica por qué.
class Animal {
String nombre = "animal";
void hablar() {
System.out.println("...");
}
void presentarse() {
System.out.print("Soy " + nombre + " y digo: ");
hablar();
}
static void tipo() {
System.out.println("Animal");
}
}
class Perro extends Animal {
String nombre = "perro";
@Override
void hablar() {
System.out.println("Guau");
}
void hablar(int veces) {
for (int i = 0; i < veces; i++) {
hablar();
}
}
static void tipo() {
System.out.println("Perro");
}
}
public class Main {
static void saludar(Animal a) {
System.out.println("Hola, animal");
}
static void saludar(Perro p) {
System.out.println("Hola, perro");
}
public static void main(String[] args) {
Animal a = new Perro();
a.hablar(); // 1
a.presentarse(); // 2
System.out.println(a.nombre); // 3
System.out.println(((Perro) a).nombre); // 4
a.tipo(); // 5
saludar(a); // 6
saludar((Perro) a); // 7
a.hablar(2); // 8
}
}
| Línea | Resultado | Por qué |
|---|---|---|
| 1 | Guau |
hablar() está sobrescrito: se ejecuta la versión del tipo dinámico, Perro |
| 2 | Soy animal y digo: Guau |
El código de presentarse() está en Animal, así que nombre es el atributo de Animal; pero la llamada a hablar() es dinámica y ejecuta la de Perro |
| 3 | animal |
Los atributos no son polimórficos: se elige por el tipo estático. Perro no sobrescribe nombre, lo oculta |
| 4 | perro |
Con el cast, el tipo estático pasa a ser Perro |
| 5 | Animal |
Los métodos static tampoco son polimórficos: se eligen por el tipo estático. Compila, con un aviso por llamar a un método estático a través de un objeto |
| 6 | Hola, animal |
La sobrecarga se resuelve al compilar con el tipo estático del argumento, Animal, aunque el objeto sea un perro |
| 7 | Hola, perro |
Con el cast, el tipo estático del argumento es Perro |
| 8 | No compila | hablar(int) solo existe en Perro, y el compilador solo deja llamar a lo que existe en el tipo estático, Animal. Habría que escribir ((Perro) a).hablar(2) |
Lo que hay que entender: solo hay una regla que memorizar. Los métodos de instancia sobrescritos se eligen por el tipo dinámico; todo lo demás (atributos, métodos static, qué sobrecarga se usa y qué se puede llamar) se decide por el tipo estático. La línea 2 es la que más se falla, porque mezcla las dos cosas en la misma instrucción. Y por eso un buen diseño nunca repite un atributo con el mismo nombre en la subclase: el código compila y confunde a cualquiera. Si el cast fuese a un tipo que el objeto no es, por ejemplo (Gato) a, compilaría igual pero lanzaría ClassCastException al ejecutarse; con if (a instanceof Perro p) compruebas el tipo y haces el cast en un solo paso.
3. Ordenar objetos con Comparable y Comparator
Enunciado: la clase Alumno tiene un nombre y una nota. Haz que los alumnos se ordenen de forma natural por nombre. Después, ordena una lista de mayor a menor nota y, a igualdad de nota, por nombre.
public class Alumno implements Comparable<Alumno> {
private final String nombre;
private final double nota;
public Alumno(String nombre, double nota) {
this.nombre = nombre;
this.nota = nota;
}
public String getNombre() {
return nombre;
}
public double getNota() {
return nota;
}
@Override
public int compareTo(Alumno otro) {
return this.nombre.compareTo(otro.nombre);
}
@Override
public String toString() {
return nombre + " (" + nota + ")";
}
}
List<Alumno> alumnos = new ArrayList<>(List.of(
new Alumno("Marta", 7.5),
new Alumno("Ana", 9.0),
new Alumno("Luis", 7.5)));
Collections.sort(alumnos);
System.out.println(alumnos); // [Ana (9.0), Luis (7.5), Marta (7.5)]
alumnos.sort(Comparator.comparingDouble(Alumno::getNota).reversed()
.thenComparing(Alumno::getNombre));
System.out.println(alumnos); // [Ana (9.0), Luis (7.5), Marta (7.5)]
Las dos salidas coinciden por casualidad: Ana es la primera por nombre y también la de mejor nota. Si Marta tuviese un 9,5, la primera ordenación no cambiaría y la segunda empezaría por Marta.
Lo que hay que entender: Comparable define el orden natural de una clase y se implementa dentro de ella, con un único criterio. Comparator define órdenes adicionales desde fuera, tantos como quieras, sin tocar la clase. compareTo devuelve un número negativo si this va antes, cero si son iguales y positivo si va después. Dos trampas típicas: escribir return (int) (this.nota - otro.nota);, que con 7,5 y 7,0 da (int) 0.5, es decir, 0, y considera iguales dos notas distintas (usa Double.compare); y poner el .reversed() al final de la cadena, que invierte también el desempate por nombre. Si en tu asignatura todavía no habéis visto expresiones lambda ni referencias a métodos (Alumno::getNota), el mismo Comparator se escribe como una clase anónima con un método compare.
4. equals y hashCode: por qué el HashSet no encuentra tu objeto
Enunciado: explica qué imprime este código y corrige la clase Punto para que dos puntos con las mismas coordenadas se consideren iguales.
public class Punto {
private final int x;
private final int y;
public Punto(int x, int y) {
this.x = x;
this.y = y;
}
}
Punto p1 = new Punto(1, 2);
Punto p2 = new Punto(1, 2);
System.out.println(p1 == p2); // false
System.out.println(p1.equals(p2)); // false
Set<Punto> puntos = new HashSet<>();
puntos.add(p1);
puntos.add(p2);
System.out.println(puntos.size()); // 2
System.out.println(puntos.contains(new Punto(1, 2))); // false
Sin sobrescribir nada, equals es el heredado de Object, que compara referencias igual que ==. Para Java son dos objetos distintos, así que el conjunto guarda los dos y no encuentra un tercer punto con las mismas coordenadas.
Corregido:
import java.util.Objects;
public class Punto {
private final int x;
private final int y;
public Punto(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Punto otro)) {
return false;
}
return x == otro.x && y == otro.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
}
Ahora p1.equals(p2) es true, el tamaño del conjunto es 1 y contains devuelve true. p1 == p2 sigue siendo false: son dos objetos distintos en memoria, aunque sean iguales.
Lo que hay que entender: la regla es que si dos objetos son iguales según equals, tienen que tener el mismo hashCode. HashSet y HashMap usan primero el hashCode para saber en qué cajón buscar y solo después comparan con equals dentro de ese cajón. Si sobrescribes equals y no hashCode, dos puntos iguales caerán casi siempre en cajones distintos y el conjunto seguirá teniendo tamaño 2. El otro error clásico es escribir public boolean equals(Punto o): eso no sobrescribe, sobrecarga, porque el parámetro de Object.equals es Object. Las colecciones llaman a equals(Object) y tu método no se usa nunca. Con @Override el compilador te avisa, y por eso conviene ponerlo siempre. En un TreeSet o un TreeMap, en cambio, lo que decide si dos elementos son iguales es compareTo o el Comparator, no equals.
5. Excepciones propias, finally y el orden de los catch
Enunciado: crea una excepción comprobada SaldoInsuficienteException que lance retirar cuando no haya saldo suficiente, y una no comprobada para cantidades no válidas. Indica qué imprime el programa.
public class SaldoInsuficienteException extends Exception {
public SaldoInsuficienteException(String mensaje) {
super(mensaje);
}
}
public class Cuenta {
private double saldo;
public Cuenta(double saldoInicial) {
this.saldo = saldoInicial;
}
public void retirar(double cantidad) throws SaldoInsuficienteException {
if (cantidad <= 0) {
throw new IllegalArgumentException("Cantidad no válida");
}
if (cantidad > saldo) {
throw new SaldoInsuficienteException("Saldo insuficiente: " + saldo);
}
saldo -= cantidad;
}
}
public static void main(String[] args) {
Cuenta cuenta = new Cuenta(100);
double[] retiradas = {30, 200, -5};
for (double cantidad : retiradas) {
try {
cuenta.retirar(cantidad);
System.out.println("Retirados " + cantidad);
} catch (SaldoInsuficienteException e) {
System.out.println("Error: " + e.getMessage());
} catch (IllegalArgumentException e) {
System.out.println("Dato incorrecto: " + e.getMessage());
} finally {
System.out.println("Fin de operación");
}
}
}
Salida:
Retirados 30.0
Fin de operación
Error: Saldo insuficiente: 70.0
Fin de operación
Dato incorrecto: Cantidad no válida
Fin de operación
Lo que hay que entender: cuando se lanza una excepción, el resto del try no se ejecuta; por eso tras intentar retirar 200 no aparece "Retirados". El finally se ejecuta siempre, haya excepción o no, y es donde se liberan recursos (aunque para ficheros y conexiones es mejor try-with-resources). Las excepciones que heredan de Exception son comprobadas: el compilador te obliga a capturarlas o a declararlas con throws, y se usan para situaciones que el programa debe prever y gestionar, como un saldo insuficiente. Las que heredan de RuntimeException, como IllegalArgumentException, son no comprobadas y suelen indicar un error de programación o un dato mal pasado. El orden de los catch importa: las más concretas van primero. Si pones catch (Exception e) antes de catch (SaldoInsuficienteException e), el programa no compila, porque el segundo bloque nunca se podría alcanzar. Y un catch vacío que se traga la excepción es lo primero que un profesor tacha en una práctica.
6. Métodos genéricos y por qué una lista de perros no es una lista de animales
Enunciado: escribe un método genérico maximo que devuelva el mayor elemento de una lista de cualquier tipo comparable. Después, explica por qué la última línea no compila.
public static <T extends Comparable<T>> T maximo(List<T> lista) {
if (lista.isEmpty()) {
throw new NoSuchElementException("La lista está vacía");
}
T mayor = lista.get(0);
for (T elemento : lista) {
if (elemento.compareTo(mayor) > 0) {
mayor = elemento;
}
}
return mayor;
}
System.out.println(maximo(List.of(3, 9, 4))); // 9
System.out.println(maximo(List.of("pera", "uva", "kiwi"))); // uva
System.out.println(maximo(alumnos)); // Marta (7.5)
List<Perro> perros = new ArrayList<>();
List<Animal> animales = perros; // no compila
Con los alumnos del ejercicio 3 devuelve Marta, porque el orden natural de Alumno es por nombre y Marta es la última alfabéticamente, aunque no tenga la mejor nota.
Lo que hay que entender: <T extends Comparable<T>> es un tipo acotado: el método acepta cualquier tipo, siempre que se pueda comparar consigo mismo, y gracias a eso puede llamar a compareTo. La última línea es la pregunta trampa de genéricos: aunque un Perro es un Animal, una List<Perro> no es una List<Animal>. Si lo fuese, podrías hacer animales.add(new Gato()) y meter un gato en una lista de perros. Cuando un método solo necesita leer de la lista, se declara con un comodín, List<? extends Animal>, y entonces acepta listas de perros y de gatos, pero no deja añadir elementos. Si tu asignatura llega más lejos, la firma completa que usa Java en sus propias clases es <T extends Comparable<? super T>>.
Preguntas de teoría típicas de POO
Son las preguntas cortas que más se repiten en los exámenes y en las defensas de las prácticas.
| Modificador | Misma clase | Mismo paquete | Subclase en otro paquete | Cualquier clase |
|---|---|---|---|---|
private |
Sí | No | No | No |
| Sin modificador (de paquete) | Sí | Sí | No | No |
protected |
Sí | Sí | Sí | No |
public |
Sí | Sí | Sí | Sí |
| Clase abstracta | Interfaz | |
|---|---|---|
| Se puede instanciar | No | No |
| Cuántas puede usar una clase | Una (extends) |
Varias (implements) |
| Atributos | De cualquier tipo, con estado | Solo constantes (public static final) |
| Constructores | Sí, los llaman las subclases con super(...) |
No |
| Métodos con código | Sí | Sí desde Java 8, como default o static |
| Cuándo usarla | Clases muy relacionadas que comparten estado y código ("es un") | Una capacidad que pueden tener clases sin relación ("puede hacer"): Comparable, Runnable |
- ¿Cuáles son los pilares de la POO? Abstracción (quedarse con lo relevante de un objeto para el problema), encapsulación (ocultar el estado y controlar el acceso con métodos), herencia (definir una clase a partir de otra) y polimorfismo (que un mismo mensaje produzca comportamientos distintos según el objeto que lo recibe).
- ¿Qué significa
static? Que el atributo o el método pertenece a la clase y no a cada objeto. Hay una sola copia de un atributostaticcompartida por todos los objetos, y un métodostaticno puede usarthisni los atributos de instancia sin un objeto. - ¿Qué hace
final? En una variable, no se puede reasignar (si es una referencia, el objeto sí puede cambiar por dentro). En un método, no se puede sobrescribir. En una clase, no se puede heredar de ella, como pasa conString. - ¿Qué es
thisy qué essuper?thises el objeto actual;this(...)llama a otro constructor de la misma clase.superda acceso a la versión del padre de un método sobrescrito, ysuper(...)llama al constructor del padre. Si no lo escribes, Java pone unsuper()sin argumentos, y si el padre no tiene ese constructor, la subclase no compila. - ¿Herencia o composición? Herencia si la relación es "es un" y lo será siempre: un comercial es un empleado. Composición si es "tiene un": un coche tiene un motor, una nómina tiene empleados. Ante la duda, composición, porque acopla menos: el principio se resume como "favorece la composición sobre la herencia".
- ¿Agregación o composición en UML? En las dos un objeto contiene a otros. En la composición (rombo relleno) las partes no existen sin el todo y se crean y destruyen con él. En la agregación (rombo vacío) las partes tienen vida propia: un equipo agrega jugadores, que siguen existiendo si el equipo desaparece.
- ¿Qué es el principio de sustitución de Liskov? Que cualquier objeto de una subclase se pueda usar donde se espera uno de la clase padre sin que el programa deje de funcionar correctamente. El ejemplo clásico de incumplirlo es
Cuadrado extends Rectangulo: un cuadrado no puede cambiar el ancho sin cambiar también el alto, y el código que esperaba un rectángulo se rompe. - ¿Para qué sirve
@Override? Para que el compilador compruebe que de verdad estás sobrescribiendo un método. Si te equivocas en el nombre o en los parámetros, no compila, en vez de crear en silencio un método nuevo que nadie llama. - ¿Qué hace
toStringsi no lo sobrescribes? Devuelve el nombre de la clase, una arroba y elhashCodeen hexadecimal, algo comoPunto@1b6d3586. Por eso, al imprimir una lista de objetos sintoString, sale ese texto.
Si tu asignatura es en C++
Las ideas son las mismas, pero hay diferencias que se preguntan mucho:
- Los métodos no son polimórficos por defecto. Sin la palabra
virtualen la clase base,animal->hablar()ejecuta la versión del tipo del puntero, no la del objeto. En el ejercicio 2, la línea 1 imprimiría...sihablarno fuesevirtual. Y el polimorfismo solo funciona a través de punteros o referencias: si copias unPerroen una variableAnimalpor valor, se pierde la parte de perro. - Una clase abstracta se declara con métodos virtuales puros:
virtual double calcularSalario() const = 0;. No existe la palabrainterface; se usa una clase con todos sus métodos virtuales puros. - El destructor de una clase base tiene que ser
virtual. Si no lo es, al hacerdeletede unPerroa través de un puntero aAnimal, solo se ejecuta el destructor deAnimal. - Hay herencia múltiple de clases, con el problema del diamante, que se resuelve con herencia virtual.
- La memoria es cosa tuya: cada
newnecesita sudelete, o mejor, punteros inteligentes comostd::unique_ptr.
Los errores que más suspenden en POO
Son los que más vemos cuando un alumno nos enseña un examen corregido o una práctica que no ha pasado la defensa.
Atributos públicos o protected por costumbre. Encapsular es la primera cosa que se corrige. Atributos private y solo los métodos de acceso que de verdad hacen falta.
Cadenas de instanceof en lugar de polimorfismo. Si tienes un if por cada subclase, te falta un método abstracto en la clase padre.
Confundir el tipo estático con el dinámico. Es la causa de casi todos los fallos en los ejercicios de "¿qué imprime?". Aplica la regla del ejercicio 2 línea a línea.
Sobrecargar creyendo que sobrescribes. equals(Punto o) en vez de equals(Object o), o un parámetro de distinto tipo en la subclase. @Override siempre.
Sobrescribir equals sin hashCode. El programa parece funcionar hasta que metes los objetos en un HashSet o los usas de clave en un HashMap.
Heredar para reutilizar código. Pila extends ArrayList te da una pila a la que se le pueden insertar elementos en medio. Si la relación no es "es un", usa composición.
Copiar el código del padre en vez de llamar a super. Si el cálculo del padre cambia, la subclase se queda desactualizada.
catch vacíos o que capturan Exception para todo. Esconden los errores y en la defensa de la práctica te preguntarán qué pasa si falla.
No saber explicar tu propia práctica. En las defensas orales se pregunta por qué una clase es abstracta o por qué un método está en una clase y no en otra. Si la práctica la escribió otra persona del grupo o una IA y no la entiendes, se nota en la primera pregunta.
Cómo prepararla
Haz los ejercicios de "¿qué imprime?" en papel y compruébalos en el ordenador. Escribe tu respuesta línea a línea antes de ejecutar. Cuando falles, busca qué regla de tipo estático o dinámico no has aplicado. Si no sabes por qué sale lo que sale, ejecútalo con el depurador entrando en cada llamada: tienes el método en cómo depurar código paso a paso.
Diseña antes de programar. Ante un enunciado, subraya los sustantivos (candidatos a clases y atributos) y los verbos (candidatos a métodos), dibuja el diagrama de clases y solo después escribe el código. Si las ideas de base todavía no están claras, empieza por la programación orientada a objetos explicada con un ejemplo.
Practica escribiendo Java sin IDE. Si el examen es en papel, no hay autocompletado ni subrayado rojo. Escribe las clases a mano, con sus import, y después pásalas al ordenador para ver cuántos errores de compilación tenías.
Reescribe tus prácticas sin mirar. Coge una clase de tu práctica, bórrala y vuelve a escribirla. Es la mejor preparación para la defensa y para el examen, porque los ejercicios suelen parecerse a lo que habéis hecho en el laboratorio.
No dejes que la asignatura siguiente te pille sin base. Estructuras de Datos se apoya en todo esto: listas, pilas, árboles y tablas hash se implementan como clases genéricas. Tienes los ejercicios típicos en la guía de Estructuras de Datos, y una visión general de las asignaturas que más se atascan en las asignaturas más difíciles de Ingeniería Informática.
Cuándo pedir ayuda
Si haces los ejercicios de "¿qué imprime?" sin ejecutar y aciertas, y sabes explicar por qué cada clase de tu práctica es como es, vas bien. Merece la pena buscar ayuda cuando:
- Te dan un enunciado y no sabes qué clases crear ni qué debe ser abstracto, o tu diseño acaba lleno de
instanceof. - Fallas las preguntas de polimorfismo y no entiendes por qué, ni siquiera viendo la solución.
- Tu práctica funciona pero no sabrías defenderla, o no funciona y llevas días sin encontrar el error.
- Fundamentos de Programación quedó cogido con pinzas y ahora los bucles y los arrays te frenan además de los objetos. Tienes un repaso en la guía de Fundamentos de Programación.
En nuestras clases de programación para universidad y en las clases de Java online trabajamos sobre tu temario, tus exámenes de otros años y tus prácticas: diseñas tú las clases, las revisamos juntos y repasamos cada pregunta trampa hasta que la regla sale sola. Son clases online uno a uno a 25 € la hora, sin matrícula ni permanencia.
Preguntas frecuentes
¿Qué se da en Programación Orientada a Objetos?
Clases y objetos, encapsulación, herencia, polimorfismo, clases abstractas e interfaces, colecciones (listas, conjuntos y mapas), genéricos, excepciones y diagramas de clases UML. En algunas universidades se añaden ficheros, interfaces gráficas, pruebas unitarias con JUnit, principios SOLID o patrones de diseño.
¿Es difícil Programación Orientada a Objetos?
Es de las asignaturas de primero y segundo que más se suspenden, sobre todo si Fundamentos de Programación no quedó sólido. Lo que cuesta no es la sintaxis, sino pensar en términos de responsabilidades (qué clase debe hacer qué) y dominar las reglas de tipo estático y dinámico que deciden qué método se ejecuta.
¿Qué diferencia hay entre una clase abstracta y una interfaz?
Una clase abstracta puede tener atributos con estado, constructores y métodos con código, y una clase solo puede heredar de una. Una interfaz define una capacidad que pueden tener clases sin relación entre sí, solo tiene constantes y una clase puede implementar varias. Clase abstracta para "es un", interfaz para "puede hacer".
¿Qué diferencia hay entre sobrecarga y sobrescritura?
La sobrecarga son varios métodos con el mismo nombre y distintos parámetros, y Java elige cuál llamar al compilar, por el tipo declarado de los argumentos. La sobrescritura es redefinir en una subclase un método heredado con la misma firma, y Java elige qué versión ejecutar en tiempo de ejecución, por el tipo real del objeto.
¿POO se da en Java o en C++?
En la mayoría de universidades españolas, en Java. Algunas usan C++, sobre todo cuando Fundamentos de Programación se dio en C, y unas pocas Python o C#. Los conceptos son los mismos; en C++ hay que añadir virtual, destructores virtuales y gestión de memoria.
¿Cómo apruebo el examen de POO si es en papel?
Practicando exactamente eso: escribir clases completas a mano, sin IDE, y resolver exámenes de otros años antes de mirar la solución. Las preguntas de "¿qué imprime?" se aprueban aplicando línea a línea la regla del tipo estático y dinámico, y las de diseño, justificando por qué cada clase es abstracta, concreta o una interfaz.