Skip to main content

Chapter 6: Fundamental Java Traps

Objective

This chapter covers the most frequent pure Java questions in interviews. These are the basics that every Java developer must master. An interviewer asks them to verify that you understand how Java works in memory.


1. == vs .equals() — THE most asked question​

The golden rule​

OperatorCompares what?Use for
==Memory addresses (references)Primitive types (int, boolean, char...)
.equals()Content (values)Objects (String, Integer, List...)

Examples with String​

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

System.out.println(a == b); // true ← same reference (String Pool)
System.out.println(a == c); // false ← different references (new creates a new object)
System.out.println(a.equals(c)); // true ← same content

Why is a == b true?​

Java maintains a String Pool (cache) in memory. When you write "hello" directly in the code, Java reuses the same object:

Memory:
┌──────────────────────────────────┐
│ String Pool │
│ ┌─────────┐ │
│ │ "hello" │ ← a and b point here
│ └─────────┘ │
└──────────────────────────────────┘

┌──────────────────────────────────┐
│ Heap │
│ ┌─────────┐ │
│ │ "hello" │ ← c points here (new = always a new object)
│ └─────────┘ │
└──────────────────────────────────┘

Examples with Integer (favorite trap!)​

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

Integer a = 128;
Integer b = 128;
System.out.println(a == b); // false ← out of cache!
System.out.println(a.equals(b)); // true ← same value
Interview trap

Java caches Integer from -128 to 127. Beyond that, == returns false even if the values are identical. That's why you should always use .equals() to compare objects.

Best practice: "CONSTANT".equals(variable)​

String status = getStatus(); // may return null

// DANGEROUS — NullPointerException if status is null
if (status.equals("ACTIVE")) { ... }

// SAFE — never throws NPE
if ("ACTIVE".equals(status)) { ... }
Interview phrase

"== compares references (memory addresses), .equals() compares content. I always use .equals() for objects. For strings, I put the constant on the left to avoid NullPointerException."


2. hashCode() — What is it and why is it important?​

Simple definition​

hashCode() returns an integer that represents an object. It's like a fingerprint: quick to compare, but not 100% unique.

The hashCode/equals contract​

Absolute rule

If two objects are .equals(), they MUST have the same hashCode().

If you override .equals(), you must also override hashCode().

// BAD — overrides equals without 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 hashCode() → BUG with HashMap, HashSet!
}

// GOOD — overrides both
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);
}
}

Why is it critical? HashMap and HashSet depend on it!​

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

// Without correct hashCode:
Set<User> set = new HashSet<>();
set.add(user1);
set.add(user2);
System.out.println(set.size()); // 2 ← BAD! Should be 1

// With correct hashCode:
set.add(user1);
set.add(user2);
System.out.println(set.size()); // 1 ← CORRECT

How HashMap uses hashCode​

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

Internal HashMap:
┌──────┬──────────────────┐
│ slot │ content │
├──────┼──────────────────┤
│ ... │ │
│ 12345│ → User("alice") │ ← hashCode determines the slot
│ ... │ │
└──────┴──────────────────┘

1. hashCode() → finds the right slot (fast, O(1))
2. equals() → checks it's the same object

With Lombok (the simplest)​

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

// Or to control which fields to use:
@EqualsAndHashCode(of = {"email"})
public class User {
private String email;
private String name;
}

With Java Records (the most modern)​

public record User(String email, String name) {}
// equals(), hashCode(), toString() are generated automatically!
Interview phrase

"hashCode is like a fingerprint for objects. HashMap uses it to find the right bucket in O(1). The contract says: if two objects are equal, they must have the same hashCode. That's why I always override both together — or I use Lombok @Data or Java Records."


3. toString() — For debugging​

Without toString​

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

With 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'} ← useful!

With Lombok or Record​

@Data       // generates toString() automatically
public class User { ... }

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

4. Primitive types vs Wrapper classes​

PrimitiveWrapperSizeDefault value
intInteger4 bytes0
longLong8 bytes0L
doubleDouble8 bytes0.0
booleanBoolean1 bitfalse
charCharacter2 bytes'\u0000'

When to use what?​

SituationUseWhy
Local variables, calculationsint, doubleFaster, no boxing
JPA entity attributesLong, IntegerCan be null (important for id not yet persisted)
Collections (List, Map)Integer, LongCollections do not accept primitives
Optional parametersIntegerCan be null to indicate "not provided"

Autoboxing / Unboxing​

// Autoboxing: int → Integer (automatic)
Integer a = 42;

// Unboxing: Integer → int (automatic)
int b = a;

// TRAP: unboxing null → NullPointerException!
Integer c = null;
int d = c; // 💥 NullPointerException
Interview trap
Integer a = null;
if (a == 0) { ... } // 💥 NullPointerException (unboxing null)

Always check null before comparing a Wrapper with a primitive.

Interview phrase

"I use primitives for local variables and calculations, and wrappers for entity IDs and collections. The key pitfall is unboxing null, which throws NullPointerException."


5. final — Three different uses​

ContextMeaningExample
VariableThe reference cannot changefinal List<String> list = new ArrayList<>()
MethodCannot be overriddenpublic final void process()
ClassCannot be inheritedpublic final class String

Trap: final does NOT make the object immutable​

final List<String> list = new ArrayList<>();
list.add("hello"); // ✅ OK — modifying the CONTENT
list = new ArrayList<>(); // ❌ Error — trying to change the REFERENCE
Memory:
┌─────────────────┐
list (final) ──→ │ ArrayList │
(reference │ ["hello"] │ ← content can change
does not move) └─────────────────┘
Interview phrase

"final means the reference can't be reassigned, but the object itself can still be mutated. For true immutability, I use List.of() or Collections.unmodifiableList()."


6. static — Belongs to the class, not the instance​

public class Counter {
static int totalCount = 0; // shared among ALL instances
int instanceCount = 0; // unique to each instance

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

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

System.out.println(Counter.totalCount); // 3 (shared)
System.out.println(c3.instanceCount); // 1 (unique to c3)

static in practice​

UseExampleGood practice?
Constantsstatic final int MAX = 100Yes
Utility methodsMath.max(), Collections.sort()Yes
Shared variablesstatic int countCaution (thread safety)
Inner classesstatic class BuilderYes (Builder pattern)
In Spring Boot

Avoid mutable static variables in Spring beans. Beans are already singleton by default — use normal instance fields.


7. String — Immutable by design​

Strings are immutable​

String s = "hello";
s.toUpperCase();
System.out.println(s); // "hello" ← NOT modified!

s = s.toUpperCase();
System.out.println(s); // "HELLO" ← new String assigned

Each modification creates a new String object.

Loop concatenation: StringBuilder​

// BAD — creates N intermediate String objects → O(n²)
String result = "";
for (int i = 0; i < 10000; i++) {
result += i + ",";
}

// GOOD — one object, in-place modifications → O(n)
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) {
sb.append(i).append(",");
}
String result = sb.toString();

String Pool (cache)​

String a = "hello";          // goes into the String Pool
String b = "hello"; // reuses the same object from the Pool
String c = new String("hello"); // creates a NEW object in the Heap

System.out.println(a == b); // true (same reference in the Pool)
System.out.println(a == c); // false (c is in the Heap, not the Pool)

String comparison​

MethodUsage
.equals()Case-sensitive comparison
.equalsIgnoreCase()Case-insensitive comparison
.compareTo()Alphabetical order (returns int)
.contains()Check if contains a substring
.startsWith() / .endsWith()Check start/end
.isBlank()Empty or only spaces (Java 11+)
.isEmpty()Empty (length 0)
Interview phrase

"Strings are immutable in Java. Every modification creates a new object. For concatenation in loops, I use StringBuilder. For comparison, I always use .equals(), never ==."


8. Optional — Avoid NullPointerException​

The problem​

// NullPointerException if findById returns null
User user = userRepository.findById(1L);
String email = user.getEmail(); // 💥 NPE if user is null

The solution: Optional​

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

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

// Approach 2: orElse (default value)
User user = optional.orElse(new User("anonymous"));

// Approach 3: orElseThrow (exception)
User user = optional.orElseThrow(() ->
new ResourceNotFoundException("User not found with id: 1"));

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

Anti-patterns to avoid​

// ❌ BAD — same as null check
if (optional.isPresent()) {
return optional.get();
} else {
return null;
}

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

// ❌ BAD — Optional as parameter
public void process(Optional<String> name) { ... }

// ✅ GOOD — Optional only as return
public Optional<User> findByEmail(String email) { ... }
Interview phrase

"Optional represents a value that may or may not be present. I use it as a return type, never as a parameter. I prefer orElseThrow() for required values and map() for transformations."


9. Pass by value vs pass by reference​

Java passes EVERYTHING by value​

// Primitives: the VALUE is copied
void increment(int x) {
x++; // modifies the local copy
}
int a = 5;
increment(a);
System.out.println(a); // 5 ← not modified

// Objects: the REFERENCE is copied (not the object)
void addItem(List<String> list) {
list.add("new"); // modifies the object pointed by the reference copy
}
List<String> myList = new ArrayList<>();
addItem(myList);
System.out.println(myList); // [new] ← modified!

// But reassigning the reference changes nothing
void replace(List<String> list) {
list = new ArrayList<>(); // changes the local copy, not the original
}
replace(myList);
System.out.println(myList); // [new] ← still the old list

Memory diagram​

Call: addItem(myList)

main() addItem()
┌──────┐ ┌──────┐
│myList │──┐ ┌────│ list │ ← copy of the reference
└──────┘ │ │ └──────┘
↓ ↓
┌──────────────┐
│ ArrayList │ ← both point to the SAME object
│ ["new"] │
└──────────────┘
Interview phrase

"Java is always pass-by-value. For primitives, the value is copied. For objects, the reference is copied — so you can modify the object through the copy, but you can't make the original variable point to a different object."


10. Interfaces vs Abstract classes​

AspectInterfaceAbstract class
Keywordimplementsextends
Multiple inheritanceYes (multiple interfaces)No (one class)
ConstructorNoYes
Instance fieldsNo (only static final)Yes
Concrete methodsYes (since Java 8, default)Yes
When to useContract / capability ("can do")Common base ("is a")
// Interface = contract (what the object CAN do)
interface Flyable {
void fly();
}
interface Swimmable {
void swim();
}

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

// Abstract class = common base (what the object IS)
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!"); }
}
Interview phrase

"I use interfaces to define contracts (what an object can do) and abstract classes to share common behavior (what an object is). Since Java 8, interfaces can have default methods, which blurred the line — but I still prefer interfaces for flexibility and multiple inheritance."


Quick interview summary​

Question1-sentence answer
== vs .equals()== compares references, .equals() compares content
hashCode()Digital fingerprint of an object, used by HashMap/HashSet
equals/hashCode contractIf a.equals(b) then a.hashCode() == b.hashCode()
finalNon-reassignable reference (not object immutability)
staticBelongs to the class, not the instance
Immutable StringEach modification creates a new object
OptionalContainer that can be empty, avoids null checks
Pass by valueJava copies the value (or the reference for objects)
Interface vs AbstractInterface = contract, Abstract = common base

⬅️ Chapter 5: Debugging | Chapter 7: Java Collections →