Saltar al contenido principal

Capítulo 6: Trampas Fundamentales de Java

Objetivo

Este capítulo cubre las preguntas puras de Java más frecuentes en entrevistas. Son las bases que todo desarrollador Java debe dominar. Un entrevistador las plantea para verificar que usted comprende cómo funciona Java en memoria.


1. == vs .equals() — LA pregunta más frecuente​

La regla de oro​

Operador¿Qué compara?Usar para
==Las direcciones de memoria (referencias)Tipos primitivos (int, boolean, char...)
.equals()El contenido (valores)Objetos (String, Integer, List...)

Ejemplos con String​

String a = "hello";
String b = "hello";
String c = new String("hello");

System.out.println(a == b); // true ← misma referencia (String Pool)
System.out.println(a == c); // false ← referencias diferentes (new crea un nuevo objeto)
System.out.println(a.equals(c)); // true ← mismo contenido

¿Por qué a == b es true?​

Java mantiene un String Pool (caché) en memoria. Cuando usted escribe "hello" directamente en el código, Java reutiliza el mismo objeto:

Memoria:
┌──────────────────────────────────┐
│ String Pool │
│ ┌─────────┐ │
│ │ "hello" │ ← a y b apuntan aquí
│ └─────────┘ │
└──────────────────────────────────┘

┌──────────────────────────────────┐
│ Heap (montón) │
│ ┌─────────┐ │
│ │ "hello" │ ← c apunta aquí (new = siempre un nuevo objeto)
│ └─────────┘ │
└──────────────────────────────────┘

Ejemplos con Integer (¡trampa favorita!)​

Integer x = 127;
Integer y = 127;
System.out.println(x == y); // true ← caché Integer [-128, 127]

Integer a = 128;
Integer b = 128;
System.out.println(a == b); // false ← fuera del caché
System.out.println(a.equals(b)); // true ← mismo valor
Trampa de entrevista

Java almacena en caché los Integer de -128 a 127. Más allá de ese rango, == devuelve false aunque los valores sean idénticos. Por eso hay que usar siempre .equals() para comparar objetos.

La buena práctica: "CONSTANT".equals(variable)​

String status = getStatus(); // puede retornar null

// PELIGROSO — NullPointerException si status es null
if (status.equals("ACTIVE")) { ... }

// SEGURO — nunca lanza NPE
if ("ACTIVE".equals(status)) { ... }
Frase de entrevista

"== compara referencias (direcciones de memoria), .equals() compara contenido. Siempre uso .equals() para objetos. Para strings, pongo la constante a la izquierda para evitar NullPointerException."


2. hashCode() — ¿Qué es y por qué es importante?​

Definición simple​

hashCode() devuelve un número entero que representa un objeto. Es como una huella digital: rápido de comparar, pero no 100% único.

El contrato hashCode/equals​

Regla absoluta

Si dos objetos son .equals(), DEBEN tener el mismo hashCode().

Si usted redefine .equals(), debe también redefinir hashCode().

// MALO — redefine equals sin hashCode
public class User {
private String email;

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User other)) return false;
return Objects.equals(email, other.email);
}
// NO hay hashCode() → BUG con HashMap, HashSet
}

// BUENO — redefine ambos
public class User {
private String email;

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User other)) return false;
return Objects.equals(email, other.email);
}

@Override
public int hashCode() {
return Objects.hash(email);
}
}

¿Por qué es crítico? ¡HashMap y HashSet dependen de esto!​

User user1 = new User("alice@mail.com");
User user2 = new User("alice@mail.com");

// Sin hashCode correcto:
Set<User> set = new HashSet<>();
set.add(user1);
set.add(user2);
System.out.println(set.size()); // 2 ← ¡MALO! Debería ser 1

// Con hashCode correcto:
set.add(user1);
set.add(user2);
System.out.println(set.size()); // 1 ← CORRECTO

Cómo HashMap usa hashCode​

hashCode("alice@mail.com") → 12345

HashMap interno:
┌──────┬──────────────────┐
│ slot │ contenido │
├──────┼──────────────────┤
│ ... │ │
│ 12345│ → User("alice") │ ← hashCode determina el slot
│ ... │ │
└──────┴──────────────────┘

1. hashCode() → encuentra el slot correcto (rápido, O(1))
2. equals() → verifica que es el mismo objeto

Con Lombok (lo más sencillo)​

@Data  // genera equals(), hashCode(), toString(), getters, setters
public class User {
private String email;
private String name;
}

// O para controlar qué campos usar:
@EqualsAndHashCode(of = {"email"})
public class User {
private String email;
private String name;
}

Con Java Records (lo más moderno)​

public record User(String email, String name) {}
// equals(), hashCode(), toString() se generan automáticamente
Frase de entrevista

"hashCode es como una huella digital para los objetos. HashMap lo usa para encontrar el bucket correcto en O(1). El contrato dice: si dos objetos son iguales, deben tener el mismo hashCode. Por eso siempre sobrescribo ambos juntos — o uso Lombok @Data o Java Records."


3. toString() — Para depuración​

Sin toString​

User user = new User("Alice", "alice@mail.com");
System.out.println(user);
// → com.example.User@1a2b3c4d ← ¡inútil!

Con toString​

@Override
public String toString() {
return "User{name='%s', email='%s'}".formatted(name, email);
}

System.out.println(user);
// → User{name='Alice', email='alice@mail.com'} ← ¡útil!

Con Lombok o Record​

@Data       // genera toString() automáticamente
public class User { ... }

// O
public record User(String name, String email) {}
// toString() → User[name=Alice, email=alice@mail.com]

4. Tipos primitivos vs Clases Wrapper​

PrimitivoWrapperTamañoValor por defecto
intInteger4 bytes0
longLong8 bytes0L
doubleDouble8 bytes0.0
booleanBoolean1 bitfalse
charCharacter2 bytes'\u0000'

¿Cuándo usar cuál?​

SituaciónUsarPor qué
Variables locales, cálculosint, doubleMás rápido, sin boxing
Atributos de entidades JPALong, IntegerPuede ser null (importante para id aún no persistido)
Colecciones (List, Map)Integer, LongLas colecciones no aceptan primitivos
Parámetros opcionalesIntegerPuede ser null para indicar "no proporcionado"

Autoboxing / Unboxing​

// Autoboxing : int → Integer (automático)
Integer a = 42;

// Unboxing : Integer → int (automático)
int b = a;

// TRAMPA : unboxing de null → NullPointerException
Integer c = null;
int d = c; // 💥 NullPointerException
Trampa de entrevista
Integer a = null;
if (a == 0) { ... } // 💥 NullPointerException (unboxing de null)

Siempre verifique null antes de comparar un Wrapper con un primitivo.

Frase de entrevista

"Uso primitivos para variables locales y cálculos, y wrappers para IDs de entidades y colecciones. La trampa clave es hacer unboxing de null, lo que lanza NullPointerException."


5. final — Tres usos diferentes​

ContextoSignificadoEjemplo
VariableLa referencia no puede cambiarfinal List<String> list = new ArrayList<>()
MétodoNo puede ser sobrescrito (override)public final void process()
ClaseNo puede ser heredadapublic final class String

Trampa: final NO hace inmutable el objeto​

final List<String> list = new ArrayList<>();
list.add("hello"); // ✅ OK — se modifica el CONTENIDO
list = new ArrayList<>(); // ❌ Error — se intenta cambiar la REFERENCIA
Memoria:
┌─────────────────┐
list (final) ──→ │ ArrayList │
(referencia │ ["hello"] │ ← el contenido puede cambiar
no cambia) └─────────────────┘
Frase de entrevista

"final significa que la referencia no puede ser reasignada, pero el objeto en sí puede ser mutado. Para inmutabilidad real, uso List.of() o Collections.unmodifiableList()."


6. static — Pertenece a la clase, no a la instancia​

public class Counter {
static int totalCount = 0; // compartido entre TODAS las instancias
int instanceCount = 0; // propio de cada instancia

public Counter() {
totalCount++;
instanceCount++;
}
}

Counter c1 = new Counter();
Counter c2 = new Counter();
Counter c3 = new Counter();

System.out.println(Counter.totalCount); // 3 (compartido)
System.out.println(c3.instanceCount); // 1 (propio de c3)

static en la práctica​

UsoEjemplo¿Buena práctica?
Constantesstatic final int MAX = 100Sí
Métodos utilitariosMath.max(), Collections.sort()Sí
Variables compartidasstatic int countPrecaución (thread safety)
Clases internasstatic class BuilderSí (Builder pattern)
En Spring Boot

Evite variables static mutables en los beans de Spring. Los beans ya son singleton por defecto — use campos de instancia normales.


7. String — Inmutable por diseño​

Los String son inmutables​

String s = "hello";
s.toUpperCase();
System.out.println(s); // "hello" ← ¡NO modificado!

s = s.toUpperCase();
System.out.println(s); // "HELLO" ← nueva String asignada

Cada modificación crea un nuevo objeto String.

Concatenación en bucle: StringBuilder​

// MALO — crea N objetos String intermedios → O(n²)
String result = "";
for (int i = 0; i < 10000; i++) {
result += i + ",";
}

// BUENO — un solo objeto, modificaciones en el lugar → O(n)
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append(i).append(",");
}
String result = sb.toString();

String Pool (caché)​

String a = "hello";          // va al String Pool
String b = "hello"; // reutiliza el mismo objeto del Pool
String c = new String("hello"); // crea un NUEVO objeto en el Heap

System.out.println(a == b); // true (misma referencia en el Pool)
System.out.println(a == c); // false (c está en el Heap, no en el Pool)

Comparación de Strings​

MétodoUso
.equals()Comparación sensible a mayúsculas/minúsculas
.equalsIgnoreCase()Comparación insensible a mayúsculas/minúsculas
.compareTo()Orden alfabético (devuelve int)
.contains()Verificar si contiene una subcadena
.startsWith() / .endsWith()Verificar inicio/fin
.isBlank()Vacío o solo espacios (Java 11+)
.isEmpty()Vacío (longitud 0)
Frase de entrevista

"Los Strings son inmutables en Java. Cada modificación crea un nuevo objeto. Para concatenación en bucles, uso StringBuilder. Para comparación, siempre uso .equals(), nunca ==."


8. Optional — Evitar los NullPointerException​

El problema​

// NullPointerException si findById retorna null
User user = userRepository.findById(1L);
String email = user.getEmail(); // 💥 NPE si user es null

La solución: Optional​

Optional<User> optional = userRepository.findById(1L);

// Enfoque 1: isPresent
if (optional.isPresent()) {
User user = optional.get();
}

// Enfoque 2: orElse (valor por defecto)
User user = optional.orElse(new User("anonymous"));

// Enfoque 3: orElseThrow (excepción)
User user = optional.orElseThrow(() ->
new ResourceNotFoundException("User not found with id: 1"));

// Enfoque 4: map (transformar)
String email = optional.map(User::getEmail).orElse("N/A");

Los anti-patrones a evitar​

// ❌ MALO — equivale a un null check
if (optional.isPresent()) {
return optional.get();
} else {
return null;
}

// ✅ BUENO
return optional.orElse(null);

// ❌ MALO — Optional como parámetro
public void process(Optional<String> name) { ... }

// ✅ BUENO — Optional solo como retorno
public Optional<User> findByEmail(String email) { ... }
Frase de entrevista

"Optional representa un valor que puede o no estar presente. Lo uso como tipo de retorno, nunca como parámetro. Prefiero orElseThrow() para valores obligatorios y map() para transformaciones."


9. Paso por valor vs paso por referencia​

Java pasa TODO por valor​

// Primitivos: el VALOR se copia
void increment(int x) {
x++; // modifica la copia local
}
int a = 5;
increment(a);
System.out.println(a); // 5 ← no modificado

// Objetos: la REFERENCIA se copia (no el objeto)
void addItem(List<String> list) {
list.add("new"); // modifica el objeto apuntado por la copia de referencia
}
List<String> myList = new ArrayList<>();
addItem(myList);
System.out.println(myList); // [new] ← ¡modificado!

// Pero reasignar la referencia no cambia nada
void replace(List<String> list) {
list = new ArrayList<>(); // cambia la copia local, no el original
}
replace(myList);
System.out.println(myList); // [new] ← sigue siendo la lista original

Esquema de memoria​

Llamada: addItem(myList)

main() addItem()
┌──────┐ ┌──────┐
│myList │──┐ ┌────│ list │ ← copia de la referencia
└──────┘ │ │ └──────┘
↓ ↓
┌──────────────┐
│ ArrayList │ ← ambos apuntan al MISMO objeto
│ ["new"] │
└──────────────┘
Frase de entrevista

"Java siempre es pass-by-value. Para primitivos, se copia el valor. Para objetos, se copia la referencia — así que se puede modificar el objeto a través de la copia, pero no se puede hacer que la variable original apunte a otro objeto."


10. Interfaces vs Clases abstractas​

AspectoInterfaceClase abstracta
Palabra claveimplementsextends
Herencia múltipleSí (varias interfaces)No (una sola clase)
ConstructorNoSí
Campos de instanciaNo (solo static final)Sí
Métodos concretosSí (desde Java 8, default)Sí
Cuándo usarContrato / capacidad ("puede hacer")Base común ("es un")
// Interface = contrato (lo que el objeto PUEDE hacer)
interface Flyable {
void fly();
}
interface Swimmable {
void swim();
}

class Duck implements Flyable, Swimmable {
public void fly() { ... }
public void swim() { ... }
}

// Clase abstracta = base común (lo que el objeto ES)
abstract class Animal {
protected String name;

Animal(String name) { this.name = name; }

abstract void makeSound();

void breathe() { System.out.println(name + " breathes"); }
}

class Dog extends Animal {
Dog(String name) { super(name); }
void makeSound() { System.out.println("Woof!"); }
}
Frase de entrevista

"Uso interfaces para definir contratos (lo que un objeto puede hacer) y clases abstractas para compartir comportamiento común (lo que un objeto es). Desde Java 8, las interfaces pueden tener métodos default, lo que difumina la línea — pero sigo prefiriendo interfaces por flexibilidad y herencia múltiple."


Resumen rápido para la entrevista​

PreguntaRespuesta en 1 frase
== vs .equals()== compara referencias, .equals() compara contenido
hashCode()Huella digital numérica de un objeto, usada por HashMap/HashSet
Contrato equals/hashCodeSi a.equals(b) entonces a.hashCode() == b.hashCode()
finalReferencia no reasignable (no inmutabilidad del objeto)
staticPertenece a la clase, no a la instancia
String inmutableCada modificación crea un nuevo objeto
OptionalContenedor que puede estar vacío, evita null checks
Paso por valorJava copia el valor (o la referencia para objetos)
Interface vs AbstractaInterface = contrato, Abstracta = base común

⬅️ Capítulo 5: Depuración | Capítulo 7: Colecciones Java →