Table of Contents

Core Java Interview Questions for Freshers: 45 Must-Know Q&A

  • Last Updated: October 26, 2026
  • By: javahandson
  • Series
img

Core Java Interview Questions for Freshers: 45 Must-Know Q&A

Core Java interview questions for freshers: 45 beginner Q&A on OOP, collections, strings, exceptions, threads & the JVM. Your complete prep list.

Landing your first Java job starts with strong fundamentals. This guide brings together the most common core java interview questions for freshers, the ones that show up again and again in real interviews. You will find 45 questions here, grouped into ten topics, from language basics all the way to Java 8 streams.

I have written each answer the way you would actually say it to an interviewer. So you get enough depth to sound confident, without rambling on. Moreover, every topic group links to a deeper guide, so you can dig further wherever you feel shaky.

I have spent close to ten years building Java systems for banking and payments. So these answers lean on what interviewers actually test, not just dictionary definitions. Now let us start with the basics and build up from there.

Core Java interview topic map showing ten clusters: basics, OOP, strings, collections, maps, exceptions, generics, multithreading, JVM memory, and Java 8

1. Language Basics and Fundamentals

Interviewers almost always warm up with the basics. So nail these first, because a shaky answer here sets a weak tone for the rest.

Deep dive: see the full Java fundamentals guide for a closer look at each idea below.

1.1 What is Java, and what does “write once, run anywhere” mean?

Answer – Java is a high-level, object-oriented programming language that runs on top of the Java Virtual Machine (JVM).

When you compile Java, you do not get machine code directly. Instead, the compiler (javac) produces bytecode, a neutral, in-between format stored in .class files.

That bytecode then runs on any device that has a JVM, whether it is Windows, Linux, or macOS. The JVM translates it into instructions the local machine understands, so it quietly handles all the platform differences for you.

// Hello.java
public class Hello {
    public static void main(String[] args) {
        System.out.println("Hello, Java!");
    }
}

// Compile once:  javac Hello.java   -> produces Hello.class (bytecode)
// Run anywhere:  java Hello         -> Output: Hello, Java!

So “write once, run anywhere” means you compile your code just once, and the same .class file runs everywhere without changes. That portability is a big reason Java became so popular for large systems.

1.2 What is the difference between the JDK, JRE, and JVM?

Answer – This is one of the most common opening questions, so answer it layer by layer, from the inside out.

JVM (Java Virtual Machine) – the engine that actually runs your bytecode. It loads and verifies classes, executes them (interpreting at first, then JIT-compiling the hot code into fast machine code), and manages memory through garbage collection. The JVM is platform-specific, which is exactly what lets your bytecode stay platform-neutral.

JRE (Java Runtime Environment) – the JVM plus the core class libraries such as java.lang and java.util. It contains everything you need to run a Java program, but it cannot compile one. Since Java 11, Oracle no longer ships a separate JRE download; the runtime comes inside the JDK, or you build a trimmed one with jlink.

JDK (Java Development Kit) – the outermost layer and the full developer kit. On top of the runtime, it adds the tools you need to build software: javac (the compiler), jar, javadoc, jdb (the debugger), and more.

You can see the split on your own machine. A plain runtime can launch programs, but only a JDK has the compiler:

java -version     # the launcher that starts a JVM: present in every runtime
javac -version    # the compiler: present only in a JDK
jlink --add-modules java.base --output my-runtime   # build a small, JRE-like runtime

On a machine with only a runtime, java Hello works, but javac Hello.java fails with “command not found”.

In short, you build with the JDK, run with the JRE, and the JVM does the actual execution.

Nested diagram of JDK, JRE, and JVM in Java, with JDK as the outer box containing the JRE, which contains the JVM

Interview tip: if you can draw the three as nested boxes on a whiteboard, you instantly look more prepared than a candidate who only recites words.

1.3 What are the eight primitive types in Java?

Answer – Java gives you eight primitive types that hold simple values directly, not as objects. Here is each one, from the smallest whole number up:

  • byte – an 8-bit signed whole number, from -128 to 127. Handy for raw data such as file contents or network packets.
  • short – a 16-bit signed whole number, from -32,768 to 32,767. You rarely see it in modern code.
  • int – a 32-bit signed whole number, from -2,147,483,648 to 2,147,483,647 (about plus or minus 2.1 billion). This is the default choice for everyday whole numbers.
  • long – a 64-bit signed whole number, roughly plus or minus 9.2 quintillion. Use it for large counts, IDs, and timestamps in milliseconds. Its literals need an L suffix.
  • float – a 32-bit decimal number with about 6-7 significant digits. Its literals need an f suffix, like 3.14f.
  • double – a 64-bit decimal number with about 15-16 significant digits. This is the default type for decimals.
  • char – a single 16-bit Unicode character, written in single quotes like 'A'. Internally it is an unsigned number from 0 to 65,535.
  • boolean – holds only true or false. It is used for flags and conditions.
byte age = 25;
short year = 2026;
int population = 1_400_000_000;
long lightYearInMeters = 9_460_730_472_580_800L;  // needs the L suffix
float price = 99.99f;                             // needs the f suffix
double pi = 3.141592653589793;
char grade = 'A';
boolean isActive = true;

System.out.println(0.1 + 0.2);  // Output: 0.30000000000000004

Because these types are not objects, they stay fast and use very little memory. For everyday whole numbers, reach for int. For money, avoid float and double, since they can quietly lose precision (as the last line shows). Use BigDecimal instead.

1.4 What is the difference between == and .equals()?

Answer – This one trips up a lot of freshers, so it is worth being precise.

  • == checks identity. For objects, it asks whether two references point to the very same object in memory. For primitives, it simply compares the values.
  • equals() checks content. It asks whether two objects are meaningfully equal. Classes like String and Integer override it to compare their values.
String a = new String("Java");
String b = new String("Java");

System.out.println(a == b);      // Output: false (two different objects)
System.out.println(a.equals(b)); // Output: true  (same characters)

int x = 10, y = 10;
System.out.println(x == y);      // Output: true  (primitives compare values)

One detail interviewers like: the default equals() inherited from Object is just ==. So your own classes must override it if you want content comparison. The rule to remember is simple: use == for identity, and equals() for value.

1.5 What is the difference between final, finally, and finalize()?

Answer – These three look similar but do completely different jobs, and interviewers love that confusion.

  • final (a keyword) – marks something as unchangeable. A final variable can be assigned only once, a final method cannot be overridden, and a final class cannot be extended. String is a final class.
  • finally (a block) – belongs to exception handling. It runs after try and catch, whether or not an exception happened, which makes it perfect for cleanup.
  • finalize() (a method) – the garbage collector once called it before reclaiming an object. It has been deprecated since Java 9 and marked for removal since Java 18, so use try-with-resources or Cleaner instead.
final int MAX_RETRIES = 3;
// MAX_RETRIES = 5;   // compile error: cannot assign a value to final variable

try {
    System.out.println(10 / 0);
} catch (ArithmeticException e) {
    System.out.println("Caught: " + e.getMessage()); // Output: Caught: / by zero
} finally {
    System.out.println("Cleanup runs anyway");       // Output: Cleanup runs anyway
}

The final keyword also works on references, methods, and classes. And here is how finalize() used to look, so you can recognise it in old code:

final List<String> names = new ArrayList<>();
names.add("Asha");              // allowed: the object can still change
// names = new ArrayList<>();   // compile error: the reference itself is final

class Payment {
    final void audit() { }      // subclasses cannot override this method
}

final class Currency { }        // no class can extend this one
// class Rupee extends Currency { }   // compile error: cannot inherit from final Currency

class LegacyResource {
    @Override
    protected void finalize() throws Throwable {   // deprecated: don't write new code like this
        System.out.println("Cleaning up");
    }
}

A common follow-up asks about that first line. A final reference cannot point somewhere else, but the object it points to can still change.

So just remember: final is a keyword, finally is a block, and finalize was a method.

1.6 What is autoboxing and unboxing?

Answer – Autoboxing and unboxing are about moving between primitives and their wrapper classes, such as int and Integer, or double and Double.

  • Autoboxing – Java automatically wraps a primitive into its wrapper object, like turning an int into an Integer.
  • Unboxing – the reverse, where Java pulls the int back out of the Integer.

Java does this quietly, so you can use primitives in places that expect objects, such as collections.

List<Integer> scores = new ArrayList<>();
scores.add(95);             // autoboxing: int 95 -> Integer.valueOf(95)
int first = scores.get(0);  // unboxing: Integer -> int

Integer missing = null;
int value = missing;        // NullPointerException at runtime!

Integer p = 127, q = 127;
System.out.println(p == q); // Output: true  (cached values -128 to 127)
Integer r = 1000, s = 1000;
System.out.println(r == s); // Output: false (two separate objects)

Two traps are worth mentioning. First, unboxing a null throws a NullPointerException, which surprises many people. Second, Integer caches only -128 to 127, so always compare wrapper objects with equals().

2. Object-Oriented Programming

OOP is the heart of Java, so expect several questions here. Clear, example-led answers matter more than long definitions.

Deep dive: the Java OOPS interview questions guide covers the four pillars with runnable code.

2.1 What are the four pillars of OOP?

Answer – Java has four core OOP pillars, and a clean answer names each with a one-line meaning.

  • Encapsulation – keep data private and expose it only through methods, so the class controls how its state changes.
  • Inheritance – one class reuses the fields and behaviour of another with extends.
  • Polymorphism – the same method call behaves differently depending on the actual object.
  • Abstraction – show what an object does and hide how it does it, using abstract classes and interfaces.

In real code, these four work together nicely. Here a bank account shows all of them at once:

abstract class Account {                       // abstraction
    private double balance;                    // encapsulation: hidden state

    public void deposit(double amount) {
        if (amount > 0) balance += amount;     // controlled access
    }

    public double getBalance() { return balance; }

    public abstract double interestRate();
}

class SavingsAccount extends Account {         // inheritance
    @Override
    public double interestRate() { return 0.04; }
}

class CurrentAccount extends Account {
    @Override
    public double interestRate() { return 0.0; }
}

Account acc = new SavingsAccount();            // polymorphism
acc.deposit(1000);
System.out.println(acc.interestRate());        // Output: 0.04

2.2 What is the difference between a class and an object?

Answer – A class is a blueprint, and an object is a real thing built from that blueprint.

  • Class – defines which fields and methods exist, but holds no actual data by itself.
  • Object – a live instance created with new. It sits in memory and holds real values.
class Account {
    String holder;
    double balance;

    Account(String holder, double balance) {
        this.holder = holder;
        this.balance = balance;
    }
}

Account a1 = new Account("Asha", 5000);    // object 1
Account a2 = new Account("Ravi", 12000);   // object 2, same class
System.out.println(a1.holder + " " + a2.holder);  // Output: Asha Ravi

Here Account is the class that describes what an account looks like. Asha’s and Ravi’s accounts are two separate objects, each with its own holder name and balance. That one-to-many relationship is the key idea.

2.3 What is the difference between method overloading and overriding?

Answer – Both share a method name, but they solve different problems.

  • Overloading – several methods with the same name but different parameter lists. The compiler picks one from the arguments you pass, so it is decided at compile time. A different return type alone is not enough.
  • Overriding – a child class rewrites a parent method with the exact same signature. The JVM picks the child version at runtime, based on the real object.
class Calculator {
    int add(int a, int b) { return a + b; }              // overloaded
    double add(double a, double b) { return a + b; }     // overloaded
    int add(int a, int b, int c) { return a + b + c; }   // overloaded
}

class Animal {
    String sound() { return "..."; }
}

class Dog extends Animal {
    @Override
    String sound() { return "Woof"; }                    // overridden
}

Animal pet = new Dog();
System.out.println(pet.sound());                 // Output: Woof (chosen at runtime)
System.out.println(new Calculator().add(2, 3));  // Output: 5    (chosen at compile time)

So the short version is this: overloading is many versions chosen by parameters, and overriding is one version replaced by a subclass. One is decided early, the other late.

2.4 What is the difference between an abstract class and an interface?

Answer – Both let you define a contract, but they fit different situations.

  • Abstract class – can mix abstract methods, normal methods with code, instance fields, and constructors. A class can extend only one abstract class.
  • Interface – mainly declares methods a class promises to implement. Since Java 8 it can also have default and static methods, and since Java 9 private ones. Its fields are always public static final. A class can implement many interfaces.
abstract class Vehicle {
    protected String name;                       // state
    Vehicle(String name) { this.name = name; }   // constructor
    abstract int wheels();                       // subclasses must implement

    void describe() {                            // shared behaviour
        System.out.println(name + " has " + wheels() + " wheels");
    }
}

interface Electric {
    int batteryKwh();
    default String charge() { return "Charging..."; }   // Java 8+
}

class Car extends Vehicle implements Electric {
    Car() { super("Car"); }
    int wheels() { return 4; }
    public int batteryKwh() { return 60; }
}

new Car().describe();   // Output: Car has 4 wheels

My rule of thumb is simple. I reach for an abstract class to share common base state and behaviour, and an interface to add a capability, like Comparable or Runnable.

2.5 What is a constructor, and what is constructor chaining?

Answer – A constructor is a special method that runs the moment you create an object. Its job is to set up the object’s starting state, like filling in the initial field values. It has the same name as the class and no return type. If you write none, the compiler adds a default no-argument constructor.

Constructor chaining is when one constructor calls another to avoid repeating setup code. You use this(...) to call another constructor in the same class, and super(...) to call the parent’s constructor. The call goes at the start of the constructor body.

class Person {
    Person() {
        System.out.println("1. Person constructor");
    }
}

class Employee extends Person {
    String name;
    double salary;

    Employee(String name) {
        this(name, 30000);              // chains to the other constructor
        System.out.println("3. Employee(name)");
    }

    Employee(String name, double salary) {
        super();                        // calls Person() first
        this.name = name;
        this.salary = salary;
        System.out.println("2. Employee(name, salary)");
    }
}

new Employee("Asha");
// Output:
// 1. Person constructor
// 2. Employee(name, salary)
// 3. Employee(name)

Notice the order. The parent constructor always finishes first, before the child’s own body. So the object gets built from the top of the family tree downward.

2.6 What is the difference between this and super?

Answer – Both are reference keywords, but they point in different directions.

  • this – refers to the current object. Use it to reach the object’s own fields and methods, or to call another constructor with this(...).
  • super – refers to the parent class. Use it to reach the parent’s fields and methods, or to call the parent constructor with super(...).
class Account {
    void display() {
        System.out.println("Account details");
    }
}

class SavingsAccount extends Account {
    private double rate;

    SavingsAccount(double rate) {
        this.rate = rate;        // this.rate = the field, rate = the parameter
    }

    @Override
    void display() {
        super.display();         // run the parent's version first
        System.out.println("Interest rate: " + rate);
    }
}

new SavingsAccount(0.04).display();
// Output:
// Account details
// Interest rate: 0.04

The two most common uses are shown above. this.rate clears up a clash between a parameter and a field, and super.display() reuses the parent’s version of an overridden method. Neither keyword works inside a static method, because there is no current object there.

3. Strings and Text Handling

Strings feel simple, yet they hide some sharp interview traps. Immutability and comparison come up almost every time.

Deep dive: the Java String interview questions guide explains the string pool with memory diagrams.

3.1 Why are Strings immutable in Java?

Answer – In Java, once you create a String, it never changes. If you try to modify it, you actually get a brand new String, and the old one stays as it was.

String city = "Pune";
city.concat(" City");          // creates a new String, but the result is thrown away
System.out.println(city);      // Output: Pune

city = city.concat(" City");   // point the reference at the new object
System.out.println(city);      // Output: Pune City

There are a few solid reasons for this design:

  • Thread safety – immutable strings can be shared across many threads without any locking.
  • String pool – Java can safely reuse one literal for many references, which saves memory. That only works if nobody can change it.
  • Reliable HashMap keys – a String’s hash code never shifts, so it can be cached and the key always lands in the same bucket.
  • Security – file paths, URLs, and class names cannot change after a security check has approved them.

Interview tip: follow up by linking immutability to thread safety and HashMap keys. That connection is exactly what interviewers want to hear.

3.2 What is the difference between String, StringBuilder, and StringBuffer?

Answer – All three deal with text, but they differ in mutability and thread safety.

  • String – immutable, so every change creates a new object. Fine for fixed text, but wasteful inside a loop.
  • StringBuilder – mutable and not synchronized, so you keep appending to the same object, and it runs fast.
  • StringBuffer – also mutable, but synchronized, so it is thread-safe and a little slower. It is mostly a legacy class today.
// Wasteful: creates a new String object on every pass
String s = "";
for (int i = 0; i < 5; i++) {
    s += i;
}

// Efficient: one mutable buffer, reused each time
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 5; i++) {
    sb.append(i);
}

System.out.println(s);              // Output: 01234
System.out.println(sb.toString());  // Output: 01234

StringBuilder methods return the builder itself, so you can chain them:

StringBuilder sb = new StringBuilder("Java");
sb.append(" 21").insert(0, "Hello ");
System.out.println(sb);             // Output: Hello Java 21
System.out.println(sb.reverse());   // Output: 12 avaJ olleH

StringBuffer has the same methods. It only earns its place when several threads append to the same buffer:

StringBuffer log = new StringBuffer();
Runnable writer = () -> {
    for (int i = 0; i < 1000; i++) {
        log.append('x');            // synchronized: safe across threads
    }
};

Thread t1 = new Thread(writer);
Thread t2 = new Thread(writer);
t1.start(); t2.start();
t1.join();  t2.join();

System.out.println(log.length());   // Output: 2000
// With a shared StringBuilder, appends can be lost or an exception thrown

So my simple guidance is this. Use String for fixed text, pick StringBuilder when you build text in one thread, and keep StringBuffer only when several threads truly share one buffer.

3.3 What is the String constant pool?

Answer – The string pool is a special area in memory where Java stores unique string literals. Since Java 7 it lives inside the heap.

When you write a literal like "Pune", Java first checks the pool. If that text already exists there, Java reuses the same object instead of making a new one. We call this reuse interning, and it quietly saves a lot of memory.

The catch is that new String("Pune") skips the pool on purpose and forces a fresh object. You can pull the pooled copy back with intern().

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

System.out.println(a == b);           // Output: true  (same pooled object)
System.out.println(a == c);           // Output: false (c is a separate object)
System.out.println(a == c.intern());  // Output: true  (intern() returns the pooled copy)

So two strings with the same text can still be different objects, which is exactly why == can fool you.

3.4 Why is == a bug when comparing strings?

Answer – The == operator compares references, meaning it checks whether two variables point to the same object. It does not look at the characters inside the string at all.

Strings built at runtime, through new, concatenation, substring(), or user input, usually land in a different object from the matching literal. So == might return false even when the words clearly match.

String expected = "admin";
String typed = new StringBuilder("nimda").reverse().toString();  // built at runtime

System.out.println(typed == expected);                    // Output: false
System.out.println(typed.equals(expected));               // Output: true
System.out.println("ADMIN".equalsIgnoreCase(expected));   // Output: true

String role = null;
System.out.println("admin".equals(role));                 // Output: false (no NPE)

The safe habit is to always use equals() for string content, and equalsIgnoreCase() when case should not matter. Calling it on the known literal, as in the last line, also protects you from a NullPointerException.

4. Collections Framework

Almost every Java role touches collections daily. Freshers should know when to pick each class, not just name them.

Deep dive: the Java Collections interview questions guide adds Big-O trade-off tables and a fail-fast demo.

4.1 What are the core interfaces of the Collections Framework?

Answer – The Collections Framework centres on a few main interfaces, and naming them cleanly shows you understand the structure.

  • List – keeps elements in order and allows duplicates, with access by index. Common classes: ArrayList, LinkedList.
  • Set – stores only unique elements. HashSet has no order, LinkedHashSet keeps insertion order, and TreeSet keeps elements sorted.
  • Queue – holds items for processing in a set order, usually first in, first out. Common classes: ArrayDeque, PriorityQueue.
  • Map – stores key-value pairs rather than single values, so it does not extend Collection. Common classes: HashMap, LinkedHashMap, TreeMap.
List<String> names = new ArrayList<>(List.of("Asha", "Ravi", "Asha"));
Set<String> unique = new HashSet<>(names);
Queue<String> tasks = new ArrayDeque<>(List.of("build", "test", "deploy"));
Map<String, Integer> ages = new HashMap<>(Map.of("Asha", 24));

System.out.println(names);             // Output: [Asha, Ravi, Asha]
System.out.println(unique.size());     // Output: 2
System.out.println(tasks.poll());      // Output: build
System.out.println(ages.get("Asha"));  // Output: 24

So each interface describes a behaviour, and each one has concrete classes that implement it.

4.2 What is the difference between ArrayList and LinkedList?

Answer – Both are lists, but they store data in very different ways under the hood.

  • ArrayList – backed by a resizable array. Reading by index is O(1), and adding at the end is fast. Inserting or removing in the middle is O(n), because later elements must shift.
  • LinkedList – a doubly linked chain of nodes. Adding or removing at either end is O(1), but reading by index is O(n), because it walks node by node. Each node also carries two extra pointers, so it uses more memory.
List<Integer> arrayList = new ArrayList<>(List.of(10, 20, 30));
LinkedList<Integer> linkedList = new LinkedList<>(List.of(10, 20, 30));

arrayList.get(1);         // O(1): jumps straight to index 1
linkedList.get(1);        // O(n): walks from the first node

linkedList.addFirst(5);   // O(1): just relinks the head
arrayList.add(0, 5);      // O(n): shifts every element one place right

In practice, I use ArrayList for almost everything, since fast random access is the common need. Even for queue or stack work, ArrayDeque usually beats LinkedList.

4.3 What is the difference between a List and a Set?

Answer – The main difference comes down to order and duplicates.

  • List – keeps elements in the exact order you added them, and happily allows duplicates. Pick it when position or repeats matter, like a shopping cart.
  • Set – rejects duplicates, using equals() and hashCode() to decide. Most Set types make no promise about order. Pick it when you only care about unique items, like distinct user IDs.
List<String> cart = new ArrayList<>();
cart.add("apple");
cart.add("apple");
System.out.println(cart);                  // Output: [apple, apple]

Set<String> userIds = new HashSet<>();
userIds.add("u101");
System.out.println(userIds.add("u101"));   // Output: false (duplicate rejected)
System.out.println(userIds);               // Output: [u101]

That one decision, duplicates or no duplicates, usually tells you which to choose.

If you need a Set with a predictable order, pick the implementation to match:

List<String> input = List.of("banana", "apple", "cherry", "apple");

System.out.println(new HashSet<>(input));        // no guaranteed order
System.out.println(new LinkedHashSet<>(input));  // Output: [banana, apple, cherry] (insertion order)
System.out.println(new TreeSet<>(input));        // Output: [apple, banana, cherry] (sorted)

All three drop the duplicate “apple”. They only differ in the order you get back.

4.4 What is the difference between Iterator and ListIterator?

Answer – Both let you walk through a collection, but one is more capable than the other.

  • Iterator – works on any Collection, moves forward only, and can remove elements safely as it goes.
  • ListIterator – works on lists alone, moves both forward and backward, and can also add and replace elements while you iterate.
List<String> langs = new ArrayList<>(List.of("Java", "C", "Python"));

Iterator<String> it = langs.iterator();
while (it.hasNext()) {
    if (it.next().equals("C")) {
        it.remove();                     // safe removal while iterating
    }
}
System.out.println(langs);               // Output: [Java, Python]

ListIterator<String> li = langs.listIterator();
while (li.hasNext()) {
    li.set(li.next().toUpperCase());     // replace in place
}
while (li.hasPrevious()) {
    System.out.print(li.previous() + " ");   // Output: PYTHON JAVA
}

A related trap: calling list.remove() inside a for-each loop throws ConcurrentModificationException. Use iterator.remove() or removeIf() instead.

4.5 What is the difference between Comparable and Comparator?

Answer – Both are about sorting, but they answer different questions.

  • Comparable – defines a class’s natural ordering through a compareTo() method written inside the class itself. Use it when there is one obvious order.
  • Comparator – a separate object that defines a custom ordering through a compare() method. Use it when you need several different orderings, or cannot change the class.
class Account implements Comparable<Account> {
    int number;
    String holder;
    double balance;

    Account(int number, String holder, double balance) {
        this.number = number;
        this.holder = holder;
        this.balance = balance;
    }

    @Override
    public int compareTo(Account other) {        // natural order: by account number
        return Integer.compare(this.number, other.number);
    }

    double getBalance() { return balance; }
    String getHolder() { return holder; }
}

List<Account> accounts = new ArrayList<>(List.of(
        new Account(3, "Ravi", 500),
        new Account(1, "Asha", 9000),
        new Account(2, "Meera", 1500)));

Collections.sort(accounts);                                // by number:  1, 2, 3
accounts.sort(Comparator.comparing(Account::getBalance));  // by balance: Ravi, Meera, Asha
accounts.sort(Comparator.comparing(Account::getHolder));   // by name:    Asha, Meera, Ravi

Comparator deserves its own example. You can write it as a separate class, as a lambda, or with the Java 8 factory methods, and you never touch the Account class itself:

// 1. A separate Comparator class
class BalanceComparator implements Comparator<Account> {
    @Override
    public int compare(Account a, Account b) {
        return Double.compare(a.balance, b.balance);   // negative, zero, or positive
    }
}
accounts.sort(new BalanceComparator());                // Ravi, Meera, Asha

// 2. The same rule as a lambda
accounts.sort((a, b) -> Double.compare(a.balance, b.balance));

// 3. Factory methods: reverse order and tie-breakers
accounts.sort(Comparator.comparingDouble(Account::getBalance).reversed());
accounts.forEach(a -> System.out.print(a.getHolder() + " "));   // Output: Asha Meera Ravi

accounts.sort(Comparator.comparing(Account::getHolder)
                        .thenComparingDouble(Account::getBalance));   // name, then balance

// 4. Sorted collections accept a Comparator too
TreeSet<Account> byName = new TreeSet<>(Comparator.comparing(Account::getHolder));
byName.addAll(accounts);

Both compareTo() and compare() follow the same contract. Return a negative number if the first value comes first, zero if they are equal, and a positive number otherwise. Avoid the old shortcut return a.number - b.number;, because the subtraction can overflow for large values. Integer.compare() and Double.compare() are always safe.

So Comparable is the built-in default order, while Comparator lets you plug in any order you like from outside.

5. Maps and Hashing

HashMap is a fresher favourite for a reason. One strong answer on hashing often carries real weight.

Deep dive: the HashMap interview questions guide walks through buckets, treeify, and the equals/hashCode contract.

5.1 What is a HashMap, and how do you store and retrieve values?

Answer – A HashMap stores data as key-value pairs and gives you very fast lookups. You add an entry with put(), passing a key and a value, and read it back with get(), passing just the key.

Map<String, Double> balances = new HashMap<>();
balances.put("ACC101", 2500.0);     // store
balances.put("ACC102", 8000.0);
balances.put("ACC101", 3000.0);     // same key: the old value is replaced

System.out.println(balances.get("ACC101"));                // Output: 3000.0
System.out.println(balances.get("ACC999"));                // Output: null
System.out.println(balances.getOrDefault("ACC999", 0.0));  // Output: 0.0
System.out.println(balances.containsKey("ACC102"));        // Output: true

Behind the scenes, the map uses the key’s hashCode() to pick a bucket, then equals() to find the exact key inside it. Thanks to that hashing, put() and get() usually run in constant time, no matter how big the map grows.

Two details round out a strong answer. Keys are unique, so a second put() with the same key overwrites the value. And a HashMap makes no promise about iteration order; use LinkedHashMap if you need insertion order.

5.2 What is the difference between HashMap and Hashtable?

Answer – Both store key-value pairs, but they differ in age and in thread safety.

  • Synchronization – HashMap is not synchronized, so it is fast. Hashtable synchronizes every method, so it is thread-safe but slower.
  • Nulls – HashMap allows one null key and any number of null values. Hashtable allows no null keys or values at all.
  • Age – Hashtable is a legacy class from Java 1.0. HashMap arrived with the Collections Framework in Java 1.2.
Map<String, String> hm = new HashMap<>();
hm.put(null, "allowed");
System.out.println(hm.get(null));    // Output: allowed

Map<String, String> ht = new Hashtable<>();
ht.put(null, "boom");                // throws NullPointerException

Map<String, Integer> safe = new ConcurrentHashMap<>();   // the modern thread-safe choice

ConcurrentHashMap also rejects nulls, like Hashtable. But instead of locking the whole table, it offers atomic update methods, so shared counters stay correct:

Map<String, Integer> hits = new ConcurrentHashMap<>();
Runnable visitor = () -> {
    for (int i = 0; i < 1000; i++) {
        hits.merge("home", 1, Integer::sum);   // atomic read-modify-write
    }
};

Thread t1 = new Thread(visitor);
Thread t2 = new Thread(visitor);
t1.start(); t2.start();
t1.join();  t2.join();

System.out.println(hits.get("home"));   // Output: 2000

So in modern code, if I need a plain map I use HashMap, and if I need a thread-safe map I use ConcurrentHashMap. It gives far better performance than locking the whole table.

5.3 What is the role of hashCode() and equals() in a map?

Answer – A map leans on these two methods working together as a team:

  • hashCode() – the map calls it on the key to jump straight to the right bucket.
  • equals() – within that bucket, the map calls it to confirm the exact key you asked for.

Because of this teamwork, if you override one method, you must override the other. Look at what happens when a custom key class skips both:

class Emp {
    int id;
    Emp(int id) { this.id = id; }
    // equals() and hashCode() NOT overridden
}

Map<Emp, String> map = new HashMap<>();
map.put(new Emp(1), "Asha");
System.out.println(map.get(new Emp(1)));   // Output: null (lookup misses the entry)

The two Emp(1) objects have different hash codes, so the lookup misses. Overriding both methods fixes it:

class Emp {
    int id;
    Emp(int id) { this.id = id; }

    @Override
    public boolean equals(Object o) {
        return o instanceof Emp e && e.id == id;
    }

    @Override
    public int hashCode() {
        return Integer.hashCode(id);
    }
}

// Now map.get(new Emp(1)) returns "Asha"

Interview tip: say the contract out loud. Equal objects must return equal hash codes, but equal hash codes need not mean equal objects.

6. Exception Handling

Good exception handling separates careful coders from careless ones. Interviewers probe judgment here, not just syntax.

Deep dive: the exception handling interview questions in Java guide covers try-with-resources and scenario traps.

6.1 What is the difference between checked and unchecked exceptions?

Answer – The split comes down to whether the compiler forces you to deal with the exception.

  • Checked exceptions – must be either caught or declared with throws, or the code will not compile. They usually point to outside problems you can recover from, such as a missing file. Example: IOException.
  • Unchecked exceptions – extend RuntimeException, and the compiler does not force you to handle them. They usually point to bugs in your own code, such as a bad array index. Example: NullPointerException.
// Checked: the compiler forces you to handle or declare it
public String readConfig(String path) throws IOException {
    return Files.readString(Path.of(path));
}

// Unchecked: compiles fine, fails only at runtime
int[] nums = {1, 2, 3};
System.out.println(nums[5]);   // ArrayIndexOutOfBoundsException

Anyone who calls readConfig() must now choose. They can catch the checked exception, or pass it on with their own throws:

// Option 1: handle it here
try {
    String config = readConfig("app.properties");
} catch (IOException e) {
    System.out.println("Using defaults: " + e.getMessage());
}

// Option 2: pass it up to your own caller
public void start() throws IOException {
    readConfig("app.properties");
}

Unchecked exceptions need neither. The right fix for them is usually better code, such as a bounds check or a null check, rather than a catch block.

So the mental model is simple: checked for expected trouble, and unchecked for programming mistakes.

6.2 What is the difference between an exception and an error?

Answer – Both sit under Throwable, but they signal very different levels of trouble.

  • Exception – a problem your program can usually catch and recover from, like a file not found or bad user input.
  • Error – a serious failure, usually inside the JVM, that you normally cannot fix, like OutOfMemoryError or StackOverflowError.
// Exception: expected trouble, so catch it and recover
try {
    Integer.parseInt("12a");
} catch (NumberFormatException e) {
    System.out.println("Invalid number, using default 0");
}

// Error: a JVM-level failure, so don't try to catch it
void recurseForever() {
    recurseForever();   // no exit condition -> StackOverflowError
}

In practice, you handle exceptions carefully, but you let errors crash the program and then investigate the root cause. That difference in recoverability is the heart of the answer.

6.3 How do try, catch, and finally work together?

Answer – These three work as a team to handle failures in a controlled way.

  • try – holds the risky code that might throw an exception.
  • catch – steps in when a matching exception is thrown and handles it.
  • finally – runs no matter what, whether the code succeeded or failed, so it is the right place for cleanup.
FileReader reader = null;
try {
    reader = new FileReader("accounts.csv");
    // read the file...
} catch (FileNotFoundException e) {
    System.out.println("File missing: " + e.getMessage());
} finally {
    if (reader != null) {
        try { reader.close(); } catch (IOException ignored) { }
    }
}

// Since Java 7: try-with-resources closes the reader for you
try (BufferedReader br = new BufferedReader(new FileReader("accounts.csv"))) {
    System.out.println(br.readLine());
} catch (IOException e) {
    System.out.println("Could not read file: " + e.getMessage());
}

In short, try attempts, catch recovers, and finally cleans up. For anything that needs closing, try-with-resources is the modern, shorter way to get the same safety.

6.4 What is the difference between throw and throws?

Answer – They look almost identical, but they play different roles.

  • throw – a statement that actually raises an exception at a specific line. You use it with an exception object.
  • throws – a clause in the method signature that declares which checked exceptions the method may pass on to its caller.
class InsufficientFundsException extends Exception {
    InsufficientFundsException(String message) { super(message); }
}

public void withdraw(double amount) throws InsufficientFundsException {  // declares
    if (amount > balance) {
        throw new InsufficientFundsException("Balance too low");         // raises
    }
    balance -= amount;
}

Because withdraw() declares a checked exception, every caller has to deal with it. And throw works with unchecked exceptions too, in which case no throws clause is needed:

try {
    account.withdraw(5000);
} catch (InsufficientFundsException e) {
    System.out.println("Withdrawal failed: " + e.getMessage());
    // Output (when the balance is below 5000): Withdrawal failed: Balance too low
}

public void deposit(double amount) {           // no throws clause needed
    if (amount <= 0) {
        throw new IllegalArgumentException("Deposit must be positive");
    }
    balance += amount;
}

So throw is an action you perform, while throws is a promise you document. Keeping that action-versus-declaration difference clear usually satisfies the interviewer.

6.5 Does finally always execute?

Answer – In almost every normal situation, yes. The finally block runs even if the try block returns early or throws an exception. That reliability is exactly why we trust it for cleanup.

static int test() {
    try {
        return 1;
    } finally {
        System.out.println("finally runs before the return");
    }
}
// test() prints "finally runs before the return", then returns 1

try {
    System.exit(0);
} finally {
    System.out.println("Never printed");   // the JVM has already shut down
}

However, there are a few rare cases where finally does not run:

  • System.exit() – the JVM shuts down immediately and skips it.
  • JVM crash or forced kill – a fatal crash, a kill -9, or a power cut stops everything.
  • Never leaving try – an endless loop or a deadlock inside try means finally is never reached.

One more tip: never put a return inside finally. It silently swallows any exception thrown from the try block.

7. Generics and the Type System

Generics confuse many freshers, so a calm answer stands out. Focus on type safety and the problem they solve.

Deep dive: the Java generics interview questions guide goes deep on type erasure and PECS.

7.1 What are generics, and what problem do they solve?

Answer – Generics let you write classes and methods that work with a type you decide later. They solve two problems:

  • Type safety – type mistakes are caught at compile time instead of blowing up at runtime.
  • No casting – you no longer need risky casts when you read values back out.
// Before generics: a raw list, a cast, and an error only at runtime
List rawList = new ArrayList();
rawList.add("Java");
rawList.add(42);                       // the compiler allows it
String s = (String) rawList.get(1);    // ClassCastException at runtime

// With generics: checked at compile time, no cast needed
List<String> names = new ArrayList<>();
names.add("Java");
// names.add(42);                      // compile error
String first = names.get(0);

You can write your own generic classes too. The T is a placeholder that the caller fills in:

class Box<T> {
    private T value;
    void set(T value) { this.value = value; }
    T get() { return value; }
}

Box<Integer> box = new Box<>();
box.set(10);
int n = box.get();   // no cast needed

7.2 What is a bounded type parameter?

Answer – A bounded type parameter puts a limit on which types a generic will accept. You write it with extends, like T extends Number, which means T can be Number or any subclass of it.

The real benefit is that inside your generic code, you can safely call Number methods like doubleValue(). Without the bound, the compiler would only trust Object methods.

static <T extends Number> double sum(List<T> numbers) {
    double total = 0;
    for (T n : numbers) {
        total += n.doubleValue();   // allowed: T is guaranteed to be a Number
    }
    return total;
}

System.out.println(sum(List.of(1, 2, 3)));   // Output: 6.0
System.out.println(sum(List.of(1.5, 2.5)));  // Output: 4.0
// sum(List.of("a", "b"));                   // compile error: String is not a Number

So bounds give you the best of both worlds. You stay flexible across many types, yet you keep enough safety to use the methods you need. You can even combine bounds, as in T extends Number & Comparable<T>.

A close cousin is the bounded wildcard. You use it in method parameters when you do not need to name the type:

static double total(List<? extends Number> numbers) {   // read from it: any Number subtype
    double sum = 0;
    for (Number n : numbers) sum += n.doubleValue();
    return sum;
}

static void addDefaults(List<? super Integer> target) { // write into it: Integer or a supertype
    target.add(0);
    target.add(100);
}

List<Object> bucket = new ArrayList<>();
addDefaults(bucket);                          // allowed: Object is a supertype of Integer
System.out.println(bucket);                   // Output: [0, 100]
System.out.println(total(List.of(1, 2.5)));   // Output: 3.5

The memory aid is PECS: Producer Extends, Consumer Super. Use ? extends when you only read from a collection, and ? super when you only write into it.

7.3 Why can’t you create an array of a generic type?

Answer – Java does not let you write new T[10], and the reason sits in how arrays and generics work.

  • Arrays are reified – they know their exact element type at runtime and check it on every store.
  • Generics are erased – the compiler removes type parameters, so at runtime the JVM has no idea what T was.

You can see the array’s runtime check in action here:

Object[] objects = new String[2];
objects[0] = 42;    // compiles, but throws ArrayStoreException at runtime

class Container<T> {
    // private T[] items = new T[10];             // compile error: generic array creation
    private List<T> items = new ArrayList<>();    // the usual workaround
}

With a generic array, the JVM could not run that check, so unsafe values would slip in without any warning. To protect type safety, Java simply blocks generic array creation. The usual workaround is a List<T>, which plays nicely with generics.

Library code runs into this too. ArrayList, for example, keeps an Object[] internally and casts on the way out. If you truly need an array inside your own generic class, there are two common tricks:

// Trick 1: create an Object[] and cast it (safe while the array stays private)
class FixedStack<T> {
    private final T[] items;

    @SuppressWarnings("unchecked")
    FixedStack(int capacity) {
        items = (T[]) new Object[capacity];
    }
}

// Trick 2: pass in the Class object and create a real typed array
static <T> T[] newArray(Class<T> type, int size) {
    @SuppressWarnings("unchecked")
    T[] array = (T[]) java.lang.reflect.Array.newInstance(type, size);
    return array;
}

String[] names = newArray(String.class, 3);   // a genuine String[]

8. Multithreading and Concurrency

Concurrency is tricky, and freshers are not expected to master it. Still, clear answers on the basics impress interviewers.

Deep dive: the Java multithreading interview questions guide adds deadlock demos and Java 21 virtual threads.

8.1 What is the difference between a process and a thread?

Answer – A process is an independent program running with its own private memory space. A thread is a smaller unit of work that runs inside a process.

  • Memory – each process has its own memory, while threads in the same process share the heap. Sharing makes communication fast but also risky.
  • Isolation – one crashing process does not directly corrupt another, but one misbehaving thread can affect the whole process.
  • Cost – creating and switching threads is much cheaper than doing the same for processes.
public static void main(String[] args) {
    System.out.println(Thread.currentThread().getName());   // Output: main

    Thread worker = new Thread(() ->
            System.out.println("Running in " + Thread.currentThread().getName()));
    worker.start();                                         // Output: Running in Thread-0
}

Every Java program is one process that starts with a main thread and can create many more. That shared-memory point is usually what the interviewer wants to hear.

8.2 What are the ways to create a thread in Java?

Answer – There are two classic ways, plus modern approaches worth mentioning.

  • Extend Thread – subclass Thread and override run().
  • Implement Runnable – write the task as a Runnable (often a lambda) and pass it into a Thread. Preferred, because your class stays free to extend something else.
  • Use an ExecutorService – hand tasks to a thread pool, which manages the threads for you. A Callable can even return a result.
  • Virtual threads (Java 21+) – lightweight threads managed by the JVM, great for many blocking tasks.
// 1. Extend Thread
class Downloader extends Thread {
    @Override
    public void run() { System.out.println("Downloading..."); }
}
new Downloader().start();

// 2. Implement Runnable
Runnable task = () -> System.out.println("Processing...");
new Thread(task).start();

// 3. Thread pool with a Callable that returns a value
ExecutorService pool = Executors.newFixedThreadPool(2);
Future<Integer> result = pool.submit(() -> 6 * 7);
System.out.println(result.get());   // Output: 42
pool.shutdown();

// 4. Virtual thread (Java 21+)
Thread.startVirtualThread(() -> System.out.println("Lightweight!"));

These days we rarely create raw threads by hand in real projects. A thread pool or virtual threads are the usual choice.

8.3 What is the difference between start() and run()?

Answer – This is a classic trap, so it pays to be careful here.

  • start() – creates a brand new thread and runs your task on it. You get real concurrency.
  • run() – just executes the code on the current thread, like an ordinary method call. No new thread is created.
Thread t = new Thread(() ->
        System.out.println("Running on: " + Thread.currentThread().getName()));

t.run();     // Output: Running on: main      (plain method call)
t.start();   // Output: Running on: Thread-0  (a new thread)
// t.start(); // a second start() throws IllegalThreadStateException

Many freshers call run by mistake and then wonder why nothing runs in parallel. The clean way to say it is that start schedules a new thread, and run is only the body it executes.

Interview tip: whenever an interviewer asks this, mention that calling run() directly loses all the threading. That single line shows you truly understand it.

8.4 What does the synchronized keyword do?

Answer – The synchronized keyword makes sure only one thread at a time can enter a block or method for a given lock. This stops two threads from changing shared data at once, which prevents corruption. It also guarantees that changes made inside the lock are visible to the next thread that takes it.

class Counter {
    private int count = 0;

    public synchronized void increment() {   // one thread at a time
        count++;
    }

    public int getCount() { return count; }
}

Counter counter = new Counter();
Thread t1 = new Thread(() -> { for (int i = 0; i < 10_000; i++) counter.increment(); });
Thread t2 = new Thread(() -> { for (int i = 0; i < 10_000; i++) counter.increment(); });
t1.start(); t2.start();
t1.join();  t2.join();

System.out.println(counter.getCount());   // Output: 20000 (without synchronized, often less)

You can synchronize a whole method, as above, or just a smaller block with synchronized (this) { ... }. Safety is the big win, but other threads must wait for the lock. So synchronize only the smallest section that truly needs it.

Here is the block form in a slightly more realistic class. Notice the private lock object. It stops outside code from grabbing the same lock and blocking you:

class BankAccount {
    private final Object lock = new Object();
    private double balance;

    public void deposit(double amount) {
        if (amount <= 0) {                 // validation needs no lock
            throw new IllegalArgumentException("Invalid amount");
        }
        synchronized (lock) {              // only the shared update is locked
            balance += amount;
        }
    }

    public static synchronized void audit() {   // static method: locks on BankAccount.class
        System.out.println("Auditing all accounts");
    }
}

So an instance method locks on this, a static method locks on the class object, and a block locks on whatever object you name.

8.5 What is a deadlock?

Answer – A deadlock happens when two threads each hold a lock that the other one is waiting for. Neither thread can move forward, so both end up frozen forever. The usual cause is that the threads grab their locks in different orders.

Object lockA = new Object();
Object lockB = new Object();

Thread t1 = new Thread(() -> {
    synchronized (lockA) {
        pause(100);                        // helper that wraps Thread.sleep()
        synchronized (lockB) { System.out.println("t1 got both"); }
    }
});

Thread t2 = new Thread(() -> {
    synchronized (lockB) {                 // opposite order: deadlock!
        pause(100);
        synchronized (lockA) { System.out.println("t2 got both"); }
    }
});

t1.start();
t2.start();   // both threads freeze forever, nothing is printed

Here t1 holds A and waits for B, while t2 holds B and waits for A. You can avoid this in a few ways:

  • Consistent lock order – always acquire locks in the same global order everywhere. If t2 also took A first, there would be no deadlock.
  • Timeouts – ReentrantLock.tryLock() with a timeout lets a thread back off instead of waiting forever.
  • Fewer nested locks – hold one lock at a time where you can, or use higher-level concurrency tools.

Here is the earlier example fixed both ways:

// Fix 1: t2 now takes the locks in the same order as t1
Thread t2 = new Thread(() -> {
    synchronized (lockA) {
        pause(100);
        synchronized (lockB) { System.out.println("t2 got both"); }
    }
});

// Fix 2: back off instead of waiting forever
ReentrantLock first = new ReentrantLock();
ReentrantLock second = new ReentrantLock();

if (first.tryLock(1, TimeUnit.SECONDS)) {
    try {
        if (second.tryLock(1, TimeUnit.SECONDS)) {
            try {
                System.out.println("Got both locks");
            } finally {
                second.unlock();
            }
        } else {
            System.out.println("Could not get the second lock, will retry");
        }
    } finally {
        first.unlock();
    }
}

To spot a deadlock in a running application, take a thread dump with jstack <pid>. It prints “Found one Java-level deadlock” and names the threads and locks involved.

9. JVM, Memory, and Garbage Collection

A little JVM awareness goes a long way in interviews. You do not need deep internals, just a solid mental model.

Deep dive: the Java memory management interview questions guide diagrams the heap, GC, and modern collectors.

9.1 What are the main memory areas of the JVM?

Answer – The JVM divides its memory into a few important areas, and naming them shows real awareness.

  • Heap – the big shared space where all objects live. The garbage collector manages it.
  • Stack – one per thread. It holds a frame for each method call, with its local variables and references.
  • Metaspace – stores class metadata, like the structure of your classes and their methods. It replaced PermGen in Java 8 and sits in native memory, outside the heap.
  • PC register – one per thread, tracking the instruction currently running.
  • Native method stack – supports calls into native (non-Java) code.
public class Bank {
    static String bankName = "JHB";       // static field: kept with the Class object, on the heap

    public static void main(String[] args) {  // main's frame goes on the main thread's stack
        int branches = 12;                // local primitive: on the stack
        Account acc = new Account();      // 'acc' on the stack, the Account object on the heap
    }
}
// Bank's class structure (methods, field layout) lives in Metaspace

For most interviews, heap, stack, and metaspace are the three you really need to explain well.

9.2 What is garbage collection, and why doesn’t Java need manual freeing?

Answer – Garbage collection is the JVM’s way of automatically cleaning up memory you no longer use.

The JVM tracks which objects are still reachable from GC roots, such as local variables on thread stacks and static fields. When nothing can reach an object anymore, it becomes eligible for collection, and the JVM frees that memory.

Account a = new Account();   // the object is reachable through 'a'
a = null;                    // no references left -> eligible for GC
System.gc();                 // only a hint; the JVM may ignore it

static List<byte[]> cache = new ArrayList<>();
cache.add(new byte[1_000_000]);   // never removed -> still reachable, so a memory leak

Because of this, you do not manually free memory the way you would in C. That automation removes a whole class of nasty bugs, like dangling pointers and double frees.

Even so, the last two lines show the catch. Garbage collection frees only unreachable objects, so anything you keep referencing, like a static cache that only grows, causes a memory leak. Modern JVMs use G1 as the default collector, with ZGC available for very low pause times.

9.3 What is the difference between stack and heap memory?

Answer – The difference is about what each area stores and how long it lasts.

  • Stack – holds method frames, local variables, and references. It clears automatically as each method returns. Each thread owns its own stack, so it is very fast. Running out gives a StackOverflowError.
  • Heap – holds the actual objects, which live as long as something still points to them. All threads share one heap, and the garbage collector cleans it. Running out gives an OutOfMemoryError.
void transfer() {
    int amount = 500;                     // stack: primitive local
    Account from = new Account("Asha");   // stack: reference 'from'; heap: the Account object
    from.debit(amount);
}   // on return, 'amount' and 'from' vanish from the stack;
    // the Account object stays on the heap until GC finds it unreachable

A simple way to picture it is this: the reference sits on the stack, and the object it points to sits on the heap.

Each area fails in its own way, and naming both errors is a nice touch:

// Stack: every call adds a frame, so endless recursion overflows it
static void dive() {
    dive();
}
// dive() -> java.lang.StackOverflowError

// Heap: objects that stay reachable keep filling it
List<long[]> hog = new ArrayList<>();
while (true) {
    hog.add(new long[10_000_000]);
}
// -> java.lang.OutOfMemoryError: Java heap space

You can tune both sizes when you start the JVM: -Xss sets each thread’s stack size, and -Xmx sets the maximum heap.

9.4 What does the static keyword mean?

Answer – The static keyword ties a member to the class itself rather than to any single object. You can use it in four places:

  • Static variable – one copy shared by all objects of the class.
  • Static method – called on the class, without creating an object. Utility methods like Math.max() work this way.
  • Static block – runs once, when the class is first loaded.
  • Static nested class – a nested class that does not need an instance of the outer class.
class Account {
    static final double TAX_RATE = 0.18;   // constant shared by everyone
    static int totalAccounts = 0;          // one copy for the whole class
    String holder;                         // one copy per object

    static {
        System.out.println("Class loaded");   // runs once
    }

    Account(String holder) {
        this.holder = holder;
        totalAccounts++;
    }

    static int getTotal() {                // call it without an object
        return totalAccounts;
        // return holder;   // compile error: no instance inside a static method
    }
}

new Account("Asha");                       // Output: Class loaded
new Account("Ravi");
System.out.println(Account.getTotal());    // Output: 2

The fourth use, a static nested class, shows up a lot in the Builder pattern. You can create the builder without first creating an Order:

class Order {
    private final String item;
    private final int quantity;

    private Order(Builder b) {
        this.item = b.item;
        this.quantity = b.quantity;
    }

    static class Builder {                 // static: no Order instance needed
        private String item;
        private int quantity = 1;

        Builder item(String item) { this.item = item; return this; }
        Builder quantity(int quantity) { this.quantity = quantity; return this; }
        Order build() { return new Order(this); }
    }
}

Order order = new Order.Builder().item("Laptop").quantity(2).build();

The quick way to say it is that static belongs to the class, while non-static belongs to each instance.

10. Java 8 Features: Lambdas and Streams

Java 8 changed how we write everyday code. Freshers who show comfort with lambdas and streams stand out fast.

Deep dive: the Java 8 stream interview questions guide adds coding problems like group-by and map vs flatMap.

10.1 What is a lambda expression?

Answer – A lambda expression is a short, clean way to write a block of code you can pass around like data. It implements a functional interface without the heavy boilerplate of an anonymous class.

The syntax is (parameters) -> body. So (a, b) -> a + b is a tiny function that adds two numbers.

// Before Java 8: an anonymous inner class
Comparator<String> byLength = new Comparator<String>() {
    @Override
    public int compare(String a, String b) {
        return Integer.compare(a.length(), b.length());
    }
};

// With a lambda: the same behaviour in one line
Comparator<String> byLengthLambda = (a, b) -> Integer.compare(a.length(), b.length());

List<String> names = new ArrayList<>(List.of("Ravi", "Asha", "Meenakshi", "Om"));
names.sort(byLengthLambda);
names.forEach(n -> System.out.println(n));   // Output: Om, Ravi, Asha, Meenakshi (one per line)

Lambdas shine with collections and streams, where you pass behaviour into methods like forEach or filter. One rule to remember: a lambda can only use local variables that are final or effectively final.

10.2 What is a functional interface?

Answer – A functional interface is simply an interface with exactly one abstract method. That single method is the target that a lambda expression fills in. It can still have default and static methods.

Common examples include:

  • Runnable – takes nothing, returns nothing.
  • Comparator<T> – compares two values.
  • Function<T, R> – takes a T and returns an R.
  • Predicate<T> – takes a T and returns a boolean.
  • Consumer<T> and Supplier<T> – one only takes a value, the other only produces one.
@FunctionalInterface
interface TaxCalculator {
    double calculate(double amount);
    // double other(double x);   // adding this breaks the compile: two abstract methods
}

TaxCalculator gst = amount -> amount * 0.18;
System.out.println(gst.calculate(1000));    // Output: 180.0

Predicate<Integer> isEven = n -> n % 2 == 0;
Function<String, Integer> length = String::length;
System.out.println(isEven.test(4));         // Output: true
System.out.println(length.apply("Java"));   // Output: 4

Here are Consumer, Supplier, and a couple of their cousins. Functional interfaces can also be chained through their default methods, like and() on Predicate:

Consumer<String> greet = name -> System.out.println("Hello, " + name);
Supplier<Double> randomRate = () -> Math.random() * 10;
BiFunction<Integer, Integer, Integer> multiply = (a, b) -> a * b;
UnaryOperator<String> shout = s -> s.toUpperCase() + "!";

greet.accept("Asha");                       // Output: Hello, Asha
System.out.println(randomRate.get());       // Output: a random number below 10
System.out.println(multiply.apply(6, 7));   // Output: 42
System.out.println(shout.apply("java"));    // Output: JAVA!

Predicate<String> notEmpty = s -> !s.isEmpty();
Predicate<String> shortWord = s -> s.length() < 5;
System.out.println(notEmpty.and(shortWord).test("Java"));   // Output: true

The @FunctionalInterface annotation makes the compiler enforce the single-method rule. So in short, one abstract method makes an interface functional, and that is what powers lambdas.

10.3 What is the Stream API, and how does it differ from a collection?

Answer – The Stream API is a modern way to process data as a flowing pipeline rather than with manual loops. Here is how a stream differs from a collection:

  • Storage vs computation – a collection stores data, while a stream describes operations over data pulled from a source.
  • Lazy – intermediate steps like filter and map do nothing until a terminal operation like collect or count asks for a result.
  • Single use – a stream can be consumed only once. Reusing it throws IllegalStateException.
  • Non-destructive – a stream never modifies its source collection.
List<Double> balances = List.of(500.0, 12000.0, 75000.0, 300.0, 20000.0);

long bigAccounts = balances.stream()
        .filter(b -> b > 10_000)   // intermediate: lazy
        .count();                  // terminal: triggers the work
System.out.println(bigAccounts);   // Output: 3

List<String> labels = balances.stream()
        .filter(b -> b > 10_000)
        .map(b -> "Rs " + b)
        .collect(Collectors.toList());
System.out.println(labels);   // Output: [Rs 12000.0, Rs 75000.0, Rs 20000.0]

So with one readable chain, you can filter a list of accounts and count or relabel the big ones.

You can watch the laziness for yourself. Nothing runs until the terminal operation, and then elements flow through the pipeline one at a time:

Stream<String> pipeline = Stream.of("a", "bb", "ccc")
        .filter(s -> {
            System.out.println("filter " + s);
            return s.length() > 1;
        });

System.out.println("Nothing has run yet");
pipeline.forEach(s -> System.out.println("got " + s));
// Output:
// Nothing has run yet
// filter a
// filter bb
// got bb
// filter ccc
// got ccc

pipeline.count();   // IllegalStateException: stream has already been operated upon or closed

Collectors can also group results, which is a favourite coding question:

Map<Boolean, List<Integer>> evenOdd = Stream.of(1, 2, 3, 4, 5)
        .collect(Collectors.partitioningBy(n -> n % 2 == 0));
System.out.println(evenOdd);   // Output: {false=[1, 3, 5], true=[2, 4]}

Map<Integer, List<String>> byLength = Stream.of("Om", "Ravi", "Asha", "Meera")
        .collect(Collectors.groupingBy(String::length));
System.out.println(byLength);  // Output: {2=[Om], 4=[Ravi, Asha], 5=[Meera]}

10.4 What is Optional, and what problem does it solve?

Answer – Optional is a small container that may or may not hold a value. Its whole purpose is to make the possible absence of a value obvious in your code.

Instead of returning null and hoping callers check it, you return an Optional and bring the question into the open.

Optional<String> findEmail(int userId) {
    return userId == 1 ? Optional.of("asha@mail.com") : Optional.empty();
}

String email = findEmail(2).orElse("no-email@mail.com");
System.out.println(email);                  // Output: no-email@mail.com

findEmail(1)
        .map(String::toUpperCase)
        .ifPresent(System.out::println);    // Output: ASHA@MAIL.COM

The methods you will use most are:

  • of() / ofNullable() / empty() – create an Optional.
  • orElse() / orElseGet() / orElseThrow() – supply a default or fail clearly when it is empty.
  • map() / ifPresent() – work with the value only if it is there.

Two follow-up questions come up a lot: of() versus ofNullable(), and orElse() versus orElseGet():

String fromDb = null;
Optional<String> maybe = Optional.ofNullable(fromDb);   // empty, no exception
// Optional.of(fromDb);                                 // NullPointerException

String a = maybe.orElse(loadDefault());          // loadDefault() runs every time
String b = maybe.orElseGet(() -> loadDefault()); // runs only when the Optional is empty

String email = findEmail(1).orElseThrow(
        () -> new IllegalStateException("User has no email"));

Optional<String> domain = findEmail(1)
        .filter(e -> e.endsWith("@mail.com"))
        .map(e -> e.substring(e.indexOf('@') + 1));
System.out.println(domain.orElse("unknown"));    // Output: mail.com

So prefer orElseGet() when the default is expensive to build, such as a database call. With orElse(), the default is computed even when a value is present.

One caution worth mentioning: Optional is meant for return types, not for fields or method parameters. And avoid calling get() without a check, since that just moves the NullPointerException somewhere else.

11. Final Prep Tips

These 45 questions cover the ground most fresher interviews walk across. Revise them once the night before, then say each answer out loud in your own words. That small habit alone will make you sound far more confident in the room.

Next, pick the topics you find hardest and open their deep-dive guides below. Each one goes further with diagrams, runnable code, and the exact follow-up questions interviewers tend to ask.

12. Further Reading

Leave a Comment