El lenguaje Java
Los conceptos de la orientación a objetos tal y como los aplica Java, el ciclo de vida y la comparación de objetos, los modificadores y su visibilidad, los tipos de datos, y los operadores y estructuras de control del lenguaje.
Orientación a objetos en Java
Java es un lenguaje orientado a objetos, y ese paradigma se apoya en un puñado de conceptos que se preguntan una y otra vez: clase, objeto, encapsulación, herencia, sobrecarga y polimorfismo con ligadura dinámica.
- Clase: la plantilla. Describe qué atributos y qué métodos van a tener los objetos de ese tipo. Se corresponde con la idea de tipo abstracto de datos y por convenio empieza por mayúscula.
- Objeto: una instancia concreta de una clase, ya creada en memoria y con sus propios valores.
- Encapsulación: ocultar el estado interno y obligar a llegar a él a través de los métodos. Nadie toca los atributos desde fuera de la clase. Cuanto menos sepan dos partes del programa la una de la otra, más fácil es mantenerlas por separado.
- Herencia: una clase toma los atributos y métodos de otra y los amplía o los redefine. En Java la herencia entre clases es simple: solo se puede tener un padre.
- Sobrecarga: varios métodos con el mismo nombre y distinta lista de parámetros dentro de la misma clase. Se resuelve al compilar.
- Polimorfismo con ligadura dinámica: una referencia del tipo padre puede apuntar a un objeto del tipo hijo, y la versión del método que se ejecuta es la del objeto real, decidida en tiempo de ejecución.
Las relaciones entre tipos usan dos palabras reservadas y hay que tenerlas bien separadas: extends se usa entre clases y también entre interfaces; implements se usa entre una clase y una interfaz. Una clase puede implementar varias interfaces aunque solo pueda extender una clase, y ese es el mecanismo con el que Java suple la herencia múltiple.
// una clase hereda de otra: extends
public class SimulacroOficial extends Test { ... }
// una interfaz hereda de otra: también extends
public interface Corregible extends Puntuable { ... }
// una clase cumple un contrato: implements, y admite varios
public class IntentoDeAlumno implements Corregible, Comparable<IntentoDeAlumno> { ... }Entre clases y entre interfaces, extends. Entre clase e interfaz, implements. Herencia simple de clases, herencia múltiple de interfaces.
Para el examen
extends: entre clases y entre interfaces
implements: entre clase e interfaz
Herencia de clases: simple
Herencia de interfaces: múltiple
Object, constructores, referencias e igualdad
Toda clase de Java hereda, directa o indirectamente, de la clase Object. Es la raíz del árbol de tipos, y por eso cualquier objeto tiene siempre métodos como equals, hashCode o toString aunque su autor no los haya escrito. Dentro de un objeto, this es la referencia a la instancia actual y super la referencia a la parte heredada del padre.
El constructor es el método que inicializa el objeto en el momento de crearlo. Al construir un hijo se ejecutan antes los constructores de sus padres. Java proporciona un constructor por defecto sin parámetros, pero en cuanto se escribe un constructor propio ese constructor por defecto deja de existir, y ese es un fallo de compilación muy típico.
La memoria de los objetos la libera el recolector de basura cuando ya no queda ninguna referencia apuntando a ellos. El método finalize() no libera nada: era solo un aviso que la máquina virtual daba antes de reclamar el objeto. Está obsoleto desde Java 9, desactivado por defecto desde Java 18 y no debe usarse; para cerrar recursos se usa try-with-resources.
En Java todo lo que no sea un tipo primitivo se maneja por referencia. De ahí sale la distinción que más preguntas genera del tema: con objetos, el operador == compara si dos referencias apuntan al mismo objeto, no si su contenido coincide; para comparar contenido se sobrescribe equals. Con tipos primitivos, en cambio, == sí compara valores.
Tema a = new Tema("java-ee-y-plataforma-net");
Tema b = new Tema("java-ee-y-plataforma-net");
a == b // false: son dos objetos distintos en memoria
a.equals(b) // true, si Tema sobrescribe equals comparando el slug
int x = 5, y = 5;
x == y // true: con primitivos, == compara valoresEl casting o conversión de tipos consiste en tratar un objeto de una clase como si fuera de otra dentro de su jerarquía. Subir hacia el padre es seguro y automático; bajar hacia el hijo hay que escribirlo y puede fallar en ejecución si el objeto no era realmente de ese tipo.
Para el examen
Raíz de la jerarquía: toda clase hereda de Object
== con objetos: compara referencias
equals: compara contenido
Constructor propio: elimina el constructor por defecto
finalize(): obsoleto
Modificadores y visibilidad de los miembros
Los modificadores son palabras reservadas que se colocan delante de una clase, un atributo o un método y cambian su comportamiento. Se dividen en dos familias: los que controlan la herencia y el ámbito, y los que controlan quién puede ver el miembro.
- final: en una clase impide heredar de ella, en un método impide sobrescribirlo y en un atributo lo convierte en constante.
- static: el miembro pertenece a la clase y no a cada objeto. Existe una sola copia compartida y se puede usar sin crear ninguna instancia. Lo estático no se hereda para redefinirse: se hereda para usarse.
- abstract: el método se declara pero no se codifica, y obliga a las clases hijas a implementarlo. Una clase con algún método abstracto es abstracta y no se puede instanciar; sirve de base para heredar de ella.
- native: el método no se implementa en Java sino en otro lenguaje, normalmente C o C++, y se conecta a través de la interfaz nativa JNI.
- volatile: obliga a leer y escribir el atributo siempre en la memoria principal y no en la caché de cada hilo, para que ninguno trabaje con un valor caducado.
- transient: marca un atributo para que no se guarde cuando el objeto se serializa. Por defecto se serializa todo.
- synchronized: garantiza que solo un hilo a la vez recorra ese método o ese bloque; los demás esperan en cola.
| Desde dónde se accede | private | default (sin modificador) | protected | public |
|---|---|---|---|---|
| Desde la propia clase | Sí | Sí | Sí | Sí |
| Desde una subclase del mismo paquete | No | Sí | Sí | Sí |
| Desde otra clase del mismo paquete | No | Sí | Sí | Sí |
| Desde una subclase de otro paquete | No | No | Sí | Sí |
| Desde otra clase de otro paquete | No | No | No | Sí |
La única fila que separa default de protected es la de otro paquete heredando. Ahí está el 90 % de las preguntas de visibilidad: protected añade a default el acceso desde una subclase de otro paquete, y nada más.
Para el examen
final: impide heredar o sobrescribir
static: el miembro pertenece a la clase
abstract: obliga a implementar
transient y volatile: excluye de la serialización y fuerza a leer de memoria principal
Visibilidad de menor a mayor: private, default (paquete), protected y public
Paquetes, módulos, interfaces, enum, record y anotaciones
- Paquete: agrupación de clases. Se corresponde con una estructura de carpetas dentro del .jar. Una clase ve por defecto a las de su mismo paquete, salvo las de java.lang, que están disponibles siempre sin importar nada.
- Módulo: agrupación por encima de los paquetes, incorporada en Java 9. Un módulo declara qué paquetes exporta, qué servicios ofrece y de qué otros módulos depende, en un fichero module-info.java.
- Interfaz: un contrato funcional puro. Hasta Java 7 era un esqueleto de métodos sin código, parecido a una clase abstracta pero sin atributos de instancia. Obliga a implementar el 100 % de lo que declara.
- Enum: un tipo con un conjunto cerrado de valores con nombre. Sirve para no manejar cadenas sueltas cuando las opciones posibles son conocidas y limitadas.
- Record: una forma corta de declarar una clase que solo transporta datos. Apareció como vista previa en Java 14 y es estable desde Java 16.
- Anotación: metainformación que se coloca sobre una clase, un método o un atributo para configurar el comportamiento. Es lo que ha sustituido en buena medida a los ficheros XML de configuración.
public enum Modalidad { LARGO, MEDIANO, EXPRESS }
// un record: los accesores, equals, hashCode y toString los pone el compilador
public record ResultadoIntento(String slugSubtema, int aciertos, int total) { }
Modalidad m = Modalidad.EXPRESS;
ResultadoIntento r = new ResultadoIntento("jakarta-ee", 7, 10);Una anotación propia se declara con @interface. Dos anotaciones sobre anotaciones se preguntan mucho: @Target indica en qué elementos se puede colocar (una clase, un método, un atributo) y @Retention indica hasta cuándo sobrevive, con tres políticas posibles: SOURCE (solo en el fuente), CLASS (llega al .class pero no a la ejecución) y RUNTIME (disponible en ejecución, que es lo que hace falta para que la lean por reflexión frameworks como los de persistencia o inyección).
Para el examen
Módulos: desde Java 9, con module-info.java
enum: conjuntos cerrados de valores
record: desde Java 16, para transportar datos
Anotaciones: metainformación; la retención RUNTIME es la que permite leerlas por reflexión
Los tipos de datos
Los tipos de Java se reparten en tres grupos: los primitivos, que guardan el valor directamente; los estructurados, que son clases y por tanto se manejan por referencia; y los envoltorios, que son clases que empaquetan un primitivo para poder tratarlo como objeto.
| Grupo | Tipos | Cómo se manejan |
|---|---|---|
| Enteros | byte, short, int, long | Primitivos: guardan el valor y se pasan por valor |
| Coma flotante | float, double | Primitivos |
| Booleano | boolean | Primitivo, solo true o false |
| Carácter | char | Primitivo, un carácter Unicode |
| Estructurados | String, arrays, clases de la biblioteca estándar, de terceros y propias | Objetos: se manejan por referencia |
| Envoltorios (wrapper) | Byte, Short, Integer, Long, Float, Double, Boolean, Character | Objetos que envuelven un primitivo, y por eso admiten null y caben en las colecciones |
El envoltorio importa por una razón práctica: las colecciones de Java solo guardan objetos, así que una lista no puede contener int, tiene que contener Integer. La conversión entre uno y otro es automática desde Java 5 y se llama autoboxing.
En Java el primitivo booleano se llama boolean, no bool. bool es de C#, y es una confusión que se cuela con facilidad en un tema que compara las dos plataformas.
Para el examen
Los ocho primitivos: byte, short, int, long, float, double, boolean y char
Estructurados: van por referencia
Envoltorios: permiten meter primitivos en colecciones
Autoboxing: el paso automático entre primitivo y envoltorio
Aviso: en Java es boolean, no bool
Operadores y estructuras de control
El operador de resto (%) devuelve lo que sobra de una división entera, y es la base de los cálculos de paridad y de reparto cíclico. Los operadores incrementales ++ y -- suman o restan uno, y su posición cambia el resultado: escrito delante, primero se modifica la variable y luego se usa su valor; escrito detrás, primero se usa el valor y luego se modifica.
int pendientes = 5;
int x = pendientes++; // x vale 5, pendientes queda en 6 (post-incremento)
int pendientes2 = 5;
int y = ++pendientes2; // y vale 6, pendientes2 queda en 6 (pre-incremento)
// operadores combinados: la forma corta de «a = a op b»
aciertos += 1; // equivale a aciertos = aciertos + 1
tiempo *= 2; // equivale a tiempo = tiempo * 2
resto %= 10; // equivale a resto = resto % 10Entre los operadores lógicos, la distinción que decide preguntas es la del cortocircuito. Los operadores simples & y | evalúan siempre los dos operandos; los dobles && y || no: si el primero ya determina el resultado, el segundo ni se mira. Se completa la familia con ! (negación) y ^ (o exclusivo). El operador condicional o ternario ?: elige entre dos valores según una condición y es la forma compacta de un if-else.
// con && el segundo operando no se evalúa si el primero es falso:
// esto es seguro aunque intento sea null
if (intento != null && intento.getAciertos() > 0) { ... }
// con & se evaluarían los dos y saltaría NullPointerException
// operador condicional (ternario)
String veredicto = aciertos >= 5 ? "superado" : "pendiente";Las estructuras de control son las habituales. Para decidir: if, con sus else if y else encadenados, y switch, que compara una expresión contra varios case, ejecuta el bloque que coincida y sigue cayendo a los siguientes si no se corta con break, con un default como salida por defecto. Para repetir: while, que comprueba la condición antes de entrar y puede no ejecutarse nunca; do-while, que ejecuta el cuerpo al menos una vez porque comprueba al final; y for, cuando se sabe cuántas vueltas hay que dar.
El for encierra en una sola línea tres cosas que en un while quedarían separadas, y ese es el punto que hay que entender. Su orden de ejecución es: primero la inicialización, una única vez; después la condición; si es cierta se ejecuta el cuerpo y a continuación la actualización, y se vuelve a comprobar la condición; si es falsa se sale. Por eso todo for se puede reescribir como un while poniendo la inicialización delante y la actualización al final del cuerpo.
// las dos formas recorren lo mismo
for (int i = 0; i < preguntasFalladas.size(); i++) {
repasar(preguntasFalladas.get(i));
}
int i = 0;
while (i < preguntasFalladas.size()) {
repasar(preguntasFalladas.get(i));
i++;
}Dentro de un bucle hay dos sentencias de ramificación: break sale del bucle o del bloque del switch, y continue abandona la vuelta actual y salta directamente a la siguiente iteración.
Para el examen
Lógicos dobles: cortocircuitan: el segundo operando puede no evaluarse
i++ frente a ++i: usa el valor y luego incrementa, frente a incrementar antes
while frente a do-while: el while puede no ejecutarse; el do-while ejecuta al menos una vez
break y continue: salir del bucle y saltar a la siguiente vuelta
Genéricos, operadores de bit y métodos de Object
La genericidad permite escribir una clase que agrupa elementos sin decidir de qué tipo son hasta el momento de usarla. Quien la usa indica el tipo entre ángulos, y a cambio el compilador le garantiza que dentro no se colará nada de otra clase, sin necesidad de conversiones.
// clase genérica: T se decide al usarla
public class Cola<T> {
public void encolar(T elemento) { ... }
public T desencolar() { ... }
}
Cola<Pregunta> repaso = new Cola<>(); // solo admite Pregunta
// genérico acotado (bounded): T tiene que heredar de Number
public class Media<T extends Number> { ... }Los operadores de bit trabajan sobre la representación binaria de un número. Los de negación, o, o exclusivo y y (~, |, ^, &) actúan bit a bit. Los de desplazamiento son tres y se distinguen por lo que entra por el hueco: << desplaza a la izquierda y mete ceros; >> desplaza a la derecha conservando el bit de signo; >>> desplaza a la derecha sin signo, metiendo siempre ceros.
| Método de Object | Qué hace |
|---|---|
| clone() | Crea una copia del objeto |
| equals(Object) | Indica si otro objeto es igual a este. Por defecto compara referencias, y se sobrescribe para comparar contenido |
| hashCode() | Devuelve un entero asociado al objeto, que usan las tablas hash. Se sobrescribe siempre junto a equals |
| getClass() | Obtiene la clase del objeto en tiempo de ejecución |
| toString() | Devuelve una cadena que describe el objeto |
| wait() | Deja al hilo actual esperando sobre este objeto |
| notify() y notifyAll() | Reanudan a uno, o a todos, los hilos que esperaban sobre este objeto |
| finalize() | Aviso previo a la reclamación del objeto. Obsoleto y desaconsejado |
Para el examen
Genéricos: fijan el tipo al usar la clase y evitan conversiones
Desplazamiento con signo: conserva el bit de signo
Desplazamiento de tres símbolos: mete siempre ceros
Regla de oro: equals y hashCode se sobrescriben siempre juntos