Saltar al contenido

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.

java
// 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.

java
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 valores

El 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 accedeprivatedefault (sin modificador)protectedpublic
Desde la propia clase
Desde una subclase del mismo paqueteNo
Desde otra clase del mismo paqueteNo
Desde una subclase de otro paqueteNoNo
Desde otra clase de otro paqueteNoNoNo

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.
java
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.

GrupoTiposCómo se manejan
Enterosbyte, short, int, longPrimitivos: guardan el valor y se pasan por valor
Coma flotantefloat, doublePrimitivos
BooleanobooleanPrimitivo, solo true o false
CaráctercharPrimitivo, un carácter Unicode
EstructuradosString, arrays, clases de la biblioteca estándar, de terceros y propiasObjetos: se manejan por referencia
Envoltorios (wrapper)Byte, Short, Integer, Long, Float, Double, Boolean, CharacterObjetos 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.

java
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 % 10

Entre 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.

java
// 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.

java
// 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.

java
// 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 ObjectQué 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