Skip to content

Java Core Interview Questions

Fundamentals: OOP concepts, memory management, concurrency basics, language features, and best practices.


1. Why is String immutable in Java?

View Answer

Direct Answer

String objects in Java are immutable (cannot be changed after creation). Once a String is created, any modification creates a new String object.

Why (Reasoning & Benefits)

  • Thread Safety: Immutability eliminates synchronization needs. Multiple threads can safely share the same String.
  • Cacheability: Strings can be pooled (String pool in heap). Same string literals reuse the same object.
  • HashMap Keys: Strings are hashCode-safe. If mutable, the hash would change, breaking HashMap lookups.
  • Security: Safe for sensitive data like file paths, database URLs, passwords. Cannot be altered mid-use.
  • Performance: String interning optimizes memory usage.

Best Practice

Use StringBuilder (not String) for string mutations in loops to avoid creating intermediate objects and garbage.

// ❌ Bad: Creates many String objects
String result = "";
for (int i = 0; i < 1000; i++) {
    result += i;  // New String each iteration
}

// ✅ Good: Reuses buffer
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    sb.append(i);
}
String result = sb.toString();

📚 Learn more: Strings & Primitives


2. What's the difference between == and .equals()?

View Answer

Quick Comparison

OperatorChecksUse Case
==Identity (object reference)Comparing null, singletons
.equals()Logical equality (content)Comparing values

Example

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

a == b          // false (different objects)
a.equals(b)     // true (same content)
a == c          // true (same reference)

Important: Override Both Together

If you override .equals(), always override hashCode() too. Objects equal by .equals() must have the same hashCode().

class Person {
    String name;
    int age;

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Person)) return false;
        Person other = (Person) obj;
        return this.name.equals(other.name) && this.age == other.age;
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);  // Same hash for equal objects
    }
}

📚 Learn more: OOP


3. What's the difference between HashMap and ConcurrentHashMap?

View Answer

Comparison Table

Feature HashMap ConcurrentHashMap
Thread-safe No Yes (bucket-level locking)
Null keys/values Allowed (1 null key) ❌ Not allowed
Performance Faster (no locking) Slower (with locking)
Use case Single-threaded Multi-threaded
Iterator Fail-fast (throws exception) Fail-safe (weakly consistent)

When to Use

  • HashMap: Single-threaded context or well-controlled synchronization
  • ConcurrentHashMap: Multi-threaded environment with concurrent reads/writes
  • Avoid: Synchronized collections like Collections.synchronizedMap() (single lock, poor performance)

Key Insight: Bucket-Level Locking

ConcurrentHashMap locks individual buckets, allowing multiple threads to access different buckets simultaneously. HashMap locks the entire map.

📚 Learn more: Collections


4. What happens when you overflow Integer.MAX_VALUE?

View Answer

Answer

Integer overflow wraps silently to Integer.MIN_VALUE. No exception is thrown.

Integer.MAX_VALUE       // 2147483647
Integer.MAX_VALUE + 1   // -2147483648 (wrapped to MIN_VALUE)
Integer.MAX_VALUE + 2   // -2147483647

Why This Happens

Java integers use two's complement representation. When the maximum positive value increments, it overflows to the minimum negative value.

Different Behavior by Type

Type MAX_VALUE + 1 Behavior
int Integer.MIN_VALUE Silently wraps
long Long.MIN_VALUE Silently wraps
double Infinity Returns Infinity

How to Prevent

  • Use long: For large numbers
  • Check bounds: Before arithmetic operations
  • Use BigInteger: For arbitrary precision
// Check before adding
if (a > Integer.MAX_VALUE - b) {
    // Handle overflow
}

// Or use Math.addExact (throws ArithmeticException)
int result = Math.addExact(a, b);

📚 Learn more: Java Basics


5. What's the output of a++ + ++a for a=5?

View Answer

Answer

int a = 5;
int result = a++ + ++a;
System.out.println("result: " + result + ", a: " + a);
// Output: result: 12, a: 7

Step-by-Step Trace

  1. a++: Returns 5 (current value), then increments a to 6 (post-increment)
  2. ++a: Increments a to 7, then returns 7 (pre-increment)
  3. 5 + 7 = 12
  4. a becomes 7

Why This Is Confusing

The order of evaluation for a++ + ++a is undefined behavior in many languages. The outcome depends on compiler optimization and evaluation order, making it unreliable.

Best Practice

❌ Avoid mixing increment operators in the same expression.

// ❌ Bad: Confusing, undefined behavior
int result = a++ + ++a;

// ✅ Good: Clear intent
int temp1 = a++;       // a is 6
int temp2 = ++a;       // a is 7
int result = temp1 + temp2;  // 5 + 7 = 12

📚 Learn more: Java Basics


6. What's the difference between 0.0/0.0 and 1/0 in Java?

View Answer

Answer

ExpressionResultType
0.0 / 0.0NaN (Not a Number)float/double
1.0 / 0.0Infinityfloat/double
1 / 0ArithmeticExceptionint/long

Key Difference: Floating-Point vs Integer

  • Floating-point division by zero: Returns Infinity or NaN (IEEE 754 standard)
  • Integer division by zero: Throws ArithmeticException

Example

// Floating-point (no exception)
System.out.println(1.0 / 0.0);      // Infinity
System.out.println(-1.0 / 0.0);     // -Infinity
System.out.println(0.0 / 0.0);      // NaN

// Integer (throws exception)
System.out.println(1 / 0);          // ArithmeticException: / by zero

// Check for NaN
double result = 0.0 / 0.0;
if (Double.isNaN(result)) {
    System.out.println("Result is NaN");
}

Handling NaN and Infinity

// NaN comparisons
double x = 0.0 / 0.0;
x == Double.NaN   // false (NaN != NaN always)
Double.isNaN(x)   // true (correct way)

// Infinity comparisons
double y = 1.0 / 0.0;
y == Double.POSITIVE_INFINITY  // true
Double.isInfinite(y)           // true

📚 Learn more: Java Basics


7. What's the difference between fail-fast and fail-safe iterators?

View Answer

Comparison Table

Property Fail-Fast Fail-Safe
Behavior on modification Throws ConcurrentModificationException Continues with copy
Examples ArrayList, HashMap, HashSet CopyOnWriteArrayList, ConcurrentHashMap
Performance Faster (no copying) Slower (creates copy)
Concurrency Not thread-safe for modifications Thread-safe
Memory O(1) O(n) - full copy

Fail-Fast Example

List<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));
Iterator<Integer> it = list.iterator();

it.next();      // 1
list.add(4);    // Modifying list during iteration

it.next();      // ❌ ConcurrentModificationException

Fail-Safe Example

List<Integer> list = new CopyOnWriteArrayList<>(Arrays.asList(1, 2, 3));
Iterator<Integer> it = list.iterator();

it.next();      // 1
list.add(4);    // Modifying list during iteration

it.next();      // ✅ Works fine (sees original copy)

When to Use

  • Fail-Fast: Single-threaded, want early detection of concurrent modifications
  • Fail-Safe: Multi-threaded environment, safe iteration with concurrent modifications

📚 Learn more: Collections


8. What's the difference between Comparable and Comparator?

View Answer

Quick Comparison

Feature Comparable Comparator
Purpose Natural ordering of class Custom ordering logic
Implementation Implement in class: class X implements Comparable<X> External class: class XComparator implements Comparator<X>
Method compareTo(Object obj) compare(Object a, Object b)
Orderings per class One natural order Multiple custom orders
Use with Arrays.sort(), Collections.sort() Arrays.sort(array, comparator)

Comparable Example

class Student implements Comparable<Student> {
    String name;
    int age;

    @Override
    public int compareTo(Student other) {
        return this.age - other.age;  // Natural: sort by age
    }
}

Student[] students = { ... };
Arrays.sort(students);  // Uses natural order (age)

Comparator Example

// Multiple sorting options
Comparator<Student> byName = (s1, s2) -> s1.name.compareTo(s2.name);
Comparator<Student> byAge = (s1, s2) -> s1.age - s2.age;
Comparator<Student> byNameThenAge = byName.thenComparing(byAge);

Student[] students = { ... };
Arrays.sort(students, byName);              // Sort by name
Arrays.sort(students, byAge);               // Sort by age
Arrays.sort(students, byNameThenAge);       // Sort by name, then age

When to Use

  • Comparable: When there's an obvious natural order (e.g., ID, age, timestamp)
  • Comparator: When you need multiple orderings or the class doesn't implement Comparable

📚 Learn more: OOP


9. Which Collections data structure stores unique employee objects, and what requirements must they fulfill?

View Answer

Direct Answer

Use HashSet for most cases; TreeSet for sorted order.

HashSet vs TreeSet Comparison

FeatureHashSetTreeSet
OrderNo guaranteed orderSorted (natural or custom)
PerformanceO(1) add/remove/lookupO(log n) add/remove/lookup
MemoryLowerHigher (maintains tree)
Null handlingAllows one null❌ No nulls allowed
ImplementationHash tableRed-Black tree
Use CaseFast uniqueness checkOrdered iteration needed

Entity Requirements for Set Uniqueness

public class Employee {
    private Long id;
    private String name;
    private String email;

    // ✅ MUST override equals()
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof Employee)) return false;
        Employee other = (Employee) obj;
        return this.id.equals(other.id);  // or email if email is unique
    }

    // ✅ MUST override hashCode() in the same way
    @Override
    public int hashCode() {
        return Objects.hash(id);  // Same fields as equals()
    }

    // For TreeSet: implement Comparable or provide Comparator
    @Override
    public int compareTo(Employee other) {
        return Long.compare(this.id, other.id);
    }
}

// Usage
Set<Employee> employees = new HashSet<>();
employees.add(new Employee(1L, "John", "john@example.com"));
employees.add(new Employee(1L, "Jane", "jane@example.com"));
// Set size: 1 (duplicates removed by equals/hashCode)

// For sorted access
Set<Employee> sorted = new TreeSet<>(employees);
// Iterates in order by ID

Critical Rules

  • ✅ Override equals() and hashCode() together — Violations break Set contracts
  • ✅ hashCode() must return same value for equal objects — Required by Set contract
  • ✅ For TreeSet: implement Comparable or provide Comparator — Needed for ordering
  • ❌ Don't use mutable fields in equals()/hashCode() — Changing them breaks Set lookups

📚 Learn more: Collections Framework


10. How do you efficiently sort and filter collections of objects using modern Java features?

View Answer

Modern Approach: Stream API (Java 8+)

Streams provide declarative, functional filtering and sorting.

List<Employee> employees = Arrays.asList(
    new Employee(1L, "John", 50000),
    new Employee(2L, "Jane", 60000),
    new Employee(3L, "Bob", 45000)
);

// ✅ Filter: Salary > 50000
List<Employee> highEarners = employees.stream()
    .filter(e -> e.getSalary() > 50000)
    .collect(Collectors.toList());

// ✅ Sort: By salary descending
List<Employee> sorted = employees.stream()
    .sorted((e1, e2) -> Double.compare(e2.getSalary(), e1.getSalary()))
    .collect(Collectors.toList());

// ✅ Chain: Filter + Sort + Limit
List<Employee> top2HighEarners = employees.stream()
    .filter(e -> e.getSalary() > 45000)
    .sorted(Comparator.comparingDouble(Employee::getSalary).reversed())
    .limit(2)
    .collect(Collectors.toList());

Comparison: Stream vs Traditional Loop

Traditional LoopStream API
List result = new ArrayList<>();
for (Employee e : employees) {
    if (e.getSalary() > 50000) {
        result.add(e);
    }
}
Collections.sort(result, 
    (e1, e2) -> Double.compare(
        e2.getSalary(),
        e1.getSalary()
    )
);
List result = employees.stream()
    .filter(e -> e.getSalary() > 50000)
    .sorted(Comparator
        .comparingDouble(
            Employee::getSalary
        ).reversed()
    )
    .collect(Collectors.toList());

Common Stream Operations

// Filter by multiple conditions
employees.stream()
    .filter(e -> e.getSalary() > 50000 && e.getName().startsWith("J"))
    .collect(Collectors.toList());

// Sort by multiple fields
employees.stream()
    .sorted(Comparator.comparingDouble(Employee::getSalary)
        .reversed()
        .thenComparingLong(Employee::getId))
    .collect(Collectors.toList());

// Transform (map)
List<String> names = employees.stream()
    .map(Employee::getName)
    .collect(Collectors.toList());

// Distinct by field
employees.stream()
    .distinct()  // Uses equals/hashCode
    .collect(Collectors.toList());

// Group by
Map<String, List<Employee>> byDept = employees.stream()
    .collect(Collectors.groupingBy(Employee::getDepartment));

// Reduce to single value
double totalSalary = employees.stream()
    .mapToDouble(Employee::getSalary)
    .sum();

Performance Considerations

  • Streams are lazy: Operations don't execute until a terminal operation (collect, forEach, count)
  • Single pass: Streams consume data once; can't reuse
  • Parallel streams: Use .parallelStream() for large datasets on multi-core systems
  • Memory: Streams can be memory-efficient with intermediate operations
// For large datasets, use parallelStream
List<Employee> result = employees.parallelStream()
    .filter(e -> e.getSalary() > 50000)
    .sorted(Comparator.comparingDouble(Employee::getSalary).reversed())
    .collect(Collectors.toList());

📚 Learn more: Collections & Stream API


Additional Topics

See Sample Topics for more areas to explore: - Explain the difference between class and object - What are the 4 pillars of OOP? - What's the difference between ArrayList and LinkedList? - Explain interfaces vs abstract classes - How does garbage collection work? - What causes a memory leak in Java? - Explain method overloading vs overriding - When should you use enum?


See also: Java Core Basics, OOP, Interview Questions Overview