Skip to main content

Top 50 Java Interview Questions for Freshers (2026) with Simple Answers

Fifty Java interview questions for freshers answered in plain language, each with real code and the exact output it printed on JDK 25: OOP, strings, collections, exceptions, Java 8 to 25 features and threads, with the traps interviewers love.

Fresher interviews ask roughly the same fifty questions every year, in different words: why is Java platform independent, what is the difference between == and equals(), how does HashMap work, what happens in finally. This page answers fifty of them in the order a first-time reader needs them, from how Java runs, through object-oriented programming, strings, collections and exceptions, to the Java 8 to 25 features that campus interviews now expect. Each answer has the same three parts: a short answer you can say out loud in under a minute, real code, and the exact output that code printed. Nothing is typed from memory. Every snippet is cut from a file in the companion repository, and a test fails the build if a line the article quotes stops being true. Deeper material sits in collapsed sections, so you can open it when you want it and ignore it when you do not.
Versions this was built and run against. JDK 25.0.5+8 (Temurin, LTS) with --release 25, and JUnit 6.1.3 for the tests. No other library is used: every demo is plain Java. Where an answer depends on the Java version (Q8 and Q11), it says so. All code and every transcript is in the fresher-qa module of the java-interview-lab repository. Timings are absent on purpose; the evidence is exact output, which does not vary from machine to machine.

Start with how Java runs, then the basics every interviewer checks

These nine questions decide whether an interviewer relaxes. They are short, and most of them hide a trap: a value that overflows, a comparison that flips at 128, a method that cannot change the caller’s variable.

Q1. Why is Java called platform independent?

Because you compile once to bytecode, not to machine code. javac turns .java into a .class file that any Java Virtual Machine (JVM) can run, and each operating system has its own JVM. The class file even announces its format: it starts with the magic number CAFEBABE and carries a version, here 69, which is the number for Java 25.
Hello.javasource codejavaccompile onceHello.classbytecode, CAFEBABEJVM on WindowsJVM on LinuxJVM on macOSOne class file, many machines: the platform-specific part is the JVM, not your program.
The picture to remember for Q1: your source is compiled once, and the thing that differs per machine is the JVM. The class file in the middle is what the snippet below opens and reads.
try (InputStream in = BasicsQa.class.getResourceAsStream("BasicsQa.class");
     DataInputStream data = new DataInputStream(in)) {
    int magic = data.readInt();            // first four bytes of every .class file
    data.readUnsignedShort();              // minor version
    int major = data.readUnsignedShort();  // class-file format version
    System.out.println("magic = " + Integer.toHexString(magic).toUpperCase());
    System.out.println("class file major version = " + major);
}
magic = CAFEBABE
class file major version = 69

Code: BasicsQa.java · output: 01-basics.txt

Q2. What is the difference between JDK, JRE and JVM?

The JVM runs bytecode. The JRE is the JVM plus the standard library. The JDK is the JRE plus developer tools such as javac. Today most distributions ship only the JDK, and you can build a smaller runtime with jlink. The output shows a JVM and a compiler in the same install, which is what makes it a JDK.
System.out.println("running on Java " + Runtime.version().feature());
System.out.println("JVM: " + System.getProperty("java.vm.name"));
System.out.println("compiler available: " + (javax.tools.ToolProvider.getSystemJavaCompiler() != null));
running on Java 25
JVM: OpenJDK 64-Bit Server VM
compiler available: true

Code: BasicsQa.java · output: 01-basics.txt

Q3. What are the primitive data types in Java and how big are they?

Eight: byte, short, int, long, float, double, char and boolean. Sizes for the first seven are fixed by the language, so they do not change between machines. boolean has no BYTES constant because its storage size is left to the JVM. Fields get default values (0, false, null); local variables do not, and the compiler refuses to read one before you assign it.
System.out.println("byte   " + Byte.BYTES + " byte,  " + Byte.MIN_VALUE + " to " + Byte.MAX_VALUE);
System.out.println("short  " + Short.BYTES + " bytes, " + Short.MIN_VALUE + " to " + Short.MAX_VALUE);
System.out.println("int    " + Integer.BYTES + " bytes, " + Integer.MIN_VALUE + " to " + Integer.MAX_VALUE);
System.out.println("long   " + Long.BYTES + " bytes, " + Long.MIN_VALUE + " to " + Long.MAX_VALUE);
System.out.println("float  " + Float.BYTES + " bytes, double " + Double.BYTES + " bytes, char " + Character.BYTES + " bytes");
System.out.println("field defaults: " + defaultInt + ", " + defaultBoolean + ", " + defaultString);
byte   1 byte,  -128 to 127
short  2 bytes, -32768 to 32767
int    4 bytes, -2147483648 to 2147483647
long   8 bytes, -9223372036854775808 to 9223372036854775807
float  4 bytes, double 8 bytes, char 2 bytes
field defaults: 0, false, null

Code: BasicsQa.java · output: 01-basics.txt

Q4. What is type casting? What happens on overflow?

Widening (int to long) is automatic. Narrowing (double to int, int to byte) needs an explicit cast and can lose data. Integer arithmetic wraps around silently on overflow; Math.addExact throws instead. Integer division drops the fraction, and 0.1 + 0.2 is not exactly 0.3 because binary floating point cannot represent it.
System.out.println("7 / 2 = " + (7 / 2) + ", (double) 7 / 2 = " + ((double) 7 / 2));
System.out.println("(byte) 130 = " + (byte) 130);
System.out.println("Integer.MAX_VALUE + 1 = " + (Integer.MAX_VALUE + 1));
char c = 'A';
System.out.println("c + 1 = " + (c + 1) + ", (char) (c + 1) = " + (char) (c + 1));
System.out.println("0.1 + 0.2 = " + (0.1 + 0.2));
try {
    Math.addExact(Integer.MAX_VALUE, 1);
} catch (ArithmeticException e) {
    System.out.println("Math.addExact: " + e.getMessage());
}
7 / 2 = 3, (double) 7 / 2 = 3.5
(byte) 130 = -126
Integer.MAX_VALUE + 1 = -2147483648
c + 1 = 66, (char) (c + 1) = B
0.1 + 0.2 = 0.30000000000000004
Math.addExact: integer overflow

Code: BasicsQa.java · output: 01-basics.txt

The compiler stops lossy conversions that you do not cast Without a cast, the compiler refuses narrowing. These are the real messages:
class LossyCast {
    public static void main(String[] args) {
        int x = 3.5;
        byte b = 200;
    }
}
LossyCast.java:3: error: incompatible types: possible lossy conversion from double to int
        int x = 3.5;
                ^
LossyCast.java:4: error: incompatible types: possible lossy conversion from int to byte
        byte b = 200;
                 ^
2 errors

Code and message: LossyCast.java · 07-compile-errors.txt, produced by scripts/compile-errors.sh.

Q5. What is autoboxing, and why is 127 == 127 true but 128 == 128 false?

Autoboxing converts int to Integer automatically, using Integer.valueOf. That method caches objects for -128 to 127, so two boxed 127s are the same object and == works. Boxed 128s are two different objects. The rule for objects never changes: use equals(), not ==. A boxed Long is never equal to a boxed Integer even with the same number.
Integer a = 127, b = 127;
Integer c = 128, d = 128;
System.out.println("127 == 127 -> " + (a == b));
System.out.println("128 == 128 -> " + (c == d));
System.out.println("128 equals 128 -> " + c.equals(d));
Long boxed = 127L;
System.out.println("Long 127 equals Integer 127 -> " + boxed.equals(127));
127 == 127 -> true
128 == 128 -> false
128 equals 128 -> true
Long 127 equals Integer 127 -> false

Code: BasicsQa.java · output: 01-basics.txt

Q6. Is Java pass by value or pass by reference?

Always pass by value. For objects, the value that is copied is the reference. So a method can change the object the caller sees (append), but it cannot make the caller’s variable point somewhere else (sb = new ...). This is the single most common trick question on this list.
static void reassign(StringBuilder sb) {
    sb = new StringBuilder("new object");   // changes only the local copy of the reference
}

static void mutate(StringBuilder sb) {
    sb.append(" + mutated");                // follows the reference to the same object
}

StringBuilder sb = new StringBuilder("original");
reassign(sb);
System.out.println("after reassign: " + sb);
mutate(sb);
System.out.println("after mutate:   " + sb);
after reassign: original
after mutate:   original + mutated

Code: BasicsQa.java · output: 01-basics.txt

Q7. What is a static block, and in what order does initialisation happen?

static members belong to the class, not to any object. A static block runs once, when the class is first loaded. An instance initialiser block runs before the constructor body on every new. In the output the static line appears once and the instance line twice.
static class Order {
    static int created;
    static { System.out.println("static block runs once, when the class loads"); }
    { System.out.println("instance block runs before each constructor body"); }

    Order() {
        created++;
        System.out.println("constructor #" + created);
    }
}

new Order();
new Order();
static block runs once, when the class loads
instance block runs before each constructor body
constructor #1
instance block runs before each constructor body
constructor #2

Code: BasicsQa.java · output: 01-basics.txt

Q8. What is the difference between final, finally and finalize?

final is a keyword: a final variable cannot be reassigned, a final method cannot be overridden, a final class cannot be extended. finally is a block that runs after try. finalize() was a cleanup hook called by the garbage collector; do not use it. The last output line is read from the JDK itself: it is deprecated and marked for removal.
final int[] numbers = {1, 2, 3};
numbers[0] = 99;                         // legal: final fixes the reference, not the contents
System.out.println("final array contents changed: " + numbers[0]);
try {
    System.out.println("in try");
} finally {
    System.out.println("finally always runs");
}
Deprecated d = Object.class.getDeclaredMethod("finalize").getAnnotation(Deprecated.class);
System.out.println("Object.finalize forRemoval = " + d.forRemoval() + ", since " + d.since());
final array contents changed: 99
in try
finally always runs
Object.finalize forRemoval = true, since 9

Code: BasicsQa.java · output: 01-basics.txt

Q9. What is the difference between stack and heap memory?

Each thread has its own stack holding method frames, local variables and references. All objects live on the shared heap, which the garbage collector cleans. Too many nested calls exhaust the stack and throw StackOverflowError; too many live objects exhaust the heap and throw OutOfMemoryError.
static int depth;

static void recurseForever() {
    depth++;
    recurseForever();
}

int[] onHeap = new int[1_000_000];       // the array lives on the heap; only the reference sits on the stack
System.out.println("heap array length = " + onHeap.length);
try {
    recurseForever();
} catch (StackOverflowError e) {
    System.out.println("StackOverflowError after many frames: " + (depth > 1000));
}
heap array length = 1000000
StackOverflowError after many frames: true

Code: BasicsQa.java · output: 01-basics.txt

Going deeper: more on this section

Object-oriented programming: eleven questions, and the traps inside them

OOP questions are about vocabulary plus one surprising behaviour each. Learn the one-line definition, then the surprise.

Q10. What are the four pillars of OOP?

Encapsulation hides data behind methods that enforce rules. Abstraction shows what something does, not how (Shape.area()). Inheritance reuses a parent’s behaviour. Polymorphism lets one call do different things depending on the real object. The snippet uses all four in about thirty lines.
interface Shape {                                   // abstraction: callers only know "area()"
    double area();
}

record Circle(double radius) implements Shape {
    public double area() { return Math.PI * radius * radius; }
}

record Rectangle(double w, double h) implements Shape {
    public double area() { return w * h; }
}

static class Account {
    private double balance;                         // encapsulation: no outside code can touch this directly

    void deposit(double amount) {
        if (amount <= 0) throw new IllegalArgumentException("amount must be positive");
        balance += amount;
    }

    double balance() { return balance; }
}

List<Shape> shapes = List.of(new Circle(1), new Rectangle(2, 3));
for (Shape s : shapes) {                        // polymorphism: one call, different behaviour
    System.out.printf("%s area = %.2f%n", s.getClass().getSimpleName(), s.area());
}
Account account = new Account();
account.deposit(500);
try {
    account.deposit(-1);
} catch (IllegalArgumentException e) {
    System.out.println("rejected: " + e.getMessage() + ", balance still " + account.balance());
}
Circle area = 3.14
Rectangle area = 6.00
rejected: amount must be positive, balance still 500.0

Code: OopQa.java · output: 02-oop.txt

Q11. How does inheritance work, and in what order do constructors run?

A child class gets the parent’s accessible members with extends. Constructors run top-down: the parent constructor finishes before the child’s body starts, because the first thing a constructor does is call super(...), explicitly or implicitly. Java supports single inheritance of classes, but a class can implement many interfaces.
static class Animal {
    Animal() { System.out.println("Animal constructor"); }
}

static class Dog extends Animal {
    Dog() {
        super();                                    // implicit if you leave it out
        System.out.println("Dog constructor");
    }
}

static class Puppy extends Animal {
    Puppy(String name) {
        if (name.isBlank()) throw new IllegalArgumentException("name required");  // before super(): legal since Java 25
        super();
        System.out.println("Puppy constructor for " + name);
    }
}

new Dog();
new Puppy("Bruno");
Animal constructor
Dog constructor
Animal constructor
Puppy constructor for Bruno

Code: OopQa.java · output: 02-oop.txt

Interview trap that changed in Java 25 For years the textbook answer was “super() must be the first statement”. Java 25 finalised flexible constructor bodies (JEP 513): you may run statements before super(), for example to validate an argument, as long as they do not touch this. The Puppy class above compiled and ran on JDK 25. This is the same file under both language levels:
class Animal {
    Animal() { }
}

class Puppy extends Animal {
    Puppy(String name) {
        if (name.isBlank()) throw new IllegalArgumentException("name required");
        super();
    }
}
PreSuper.java:8: error: flexible constructors is not supported in -source 21
        super();
             ^
  (use -source 25 or higher to enable flexible constructors)
1 error

Code and messages: PreSuper.java · 07-compile-errors.txt, produced by scripts/compile-errors.sh.

compiled without errors

Both messages: 07-compile-errors.txt.

If your syllabus or interviewer says “first statement”, give that answer and add that Java 25 relaxed it.

Q12. What is the difference between method overloading and overriding?

Overloading: same name, different parameters, resolved at compile time from the declared types. Overriding: a subclass replaces an inherited method with the same signature, resolved at run time from the real object. In the output, show(text) with an Object-typed variable picks the Object overload even though the value is a String, and then the override in FancyPrinter runs.
static class Printer {
    String show(Object o) { return "show(Object)"; }
    String show(String s) { return "show(String)"; }
}

static class FancyPrinter extends Printer {
    @Override
    String show(Object o) { return "FancyPrinter.show(Object)"; }
}

Printer p = new FancyPrinter();
Object text = "hello";
System.out.println(p.show(text));               // compile time picks show(Object) from the declared type; run time picks FancyPrinter's override
System.out.println(p.show("hello"));            // compile time picks show(String); FancyPrinter never overrode it
System.out.println(p.show((Object) 42));        // same as the first line: overload by declared type, body by real class
FancyPrinter.show(Object)
show(String)
FancyPrinter.show(Object)

Code: OopQa.java · output: 02-oop.txt

Q13. What is the difference between an abstract class and an interface?

An abstract class can hold state and constructors and gives a partial implementation to its subclasses; a class extends only one. An interface describes a capability; a class can implement many, and since Java 8 an interface may carry default, static and (Java 9) private methods. Rule of thumb: interface for “can do”, abstract class for “is a, with shared code”.
abstract static class Vehicle {                     // abstract class: state + constructor + partial code
    protected final String name;
    Vehicle(String name) { this.name = name; }
    abstract int wheels();
    String describe() { return name + " has " + wheels() + " wheels"; }
}

interface Electric {                                // interface: a capability, many allowed
    default String charge() { return "charging"; }
    static String standard() { return "Type 2"; }
}

static class Scooter extends Vehicle implements Electric {
    Scooter() { super("Scooter"); }
    int wheels() { return 2; }
}

Scooter s = new Scooter();
System.out.println(s.describe() + ", " + s.charge() + ", plug " + Electric.standard());
Scooter has 2 wheels, charging, plug Type 2

Code: OopQa.java · output: 02-oop.txt

Q14. What is polymorphism, and what is NOT polymorphic in Java?

Instance methods are polymorphic: the real object decides. Fields and static methods are not: they are looked up by the declared type. With Parent p = new Child(), p.who() runs Child.who(), but p.name reads Parent’s field. A child’s static method with the same signature hides the parent’s; it does not override it.
static class Parent {
    String name = "Parent.field";
    static String kind() { return "Parent.static"; }
    String who() { return "Parent.who()"; }
}

static class Child extends Parent {
    String name = "Child.field";                    // hides, does not override
    static String kind() { return "Child.static"; } // hides, does not override
    @Override String who() { return "Child.who()"; }
}

Parent p = new Child();
System.out.println(p.who() + " | " + p.name + " | " + Parent.kind() + " | " + Child.kind());
Child.who() | Parent.field | Parent.static | Child.static

Code: OopQa.java · output: 02-oop.txt

Q15. Can you override static, private or final methods?

No to all three, for different reasons. A static method is hidden, not overridden. A private method is invisible to the subclass, so the child’s same-named method is a brand new method; the output shows the parent’s callSecret() still running the parent’s secret(). A final method is rejected by the compiler.
static class Base {
    static String hello() { return "Base.hello"; }
    private String secret() { return "Base.secret"; }
    final String locked() { return "Base.locked"; }
    String callSecret() { return secret(); }
}

static class Derived extends Base {
    static String hello() { return "Derived.hello"; }   // legal: hides
    private String secret() { return "Derived.secret"; } // legal: a new, unrelated method
    // final String locked() { ... }                     // compile error: cannot override final
}

Base b = new Derived();
System.out.println(b.callSecret() + " | " + b.locked());
Base.secret | Base.locked

Code: OopQa.java · output: 02-oop.txt

The compiler’s own message for overriding a final method
class Base {
    final String locked() { return "Base.locked"; }
}

class Derived extends Base {
    String locked() { return "Derived.locked"; }
}
FinalOverride.java:6: error: locked() in Derived cannot override locked() in Base
    String locked() { return "Derived.locked"; }
           ^
  overridden method is final
1 error

Code and message: FinalOverride.java · 07-compile-errors.txt, produced by scripts/compile-errors.sh.

Q16. How do you create an immutable class?

Make the class final, make every field private final, set them only in the constructor, provide no setters, and copy any mutable input or output (a defensive copy). List.copyOf does the copy and returns an unmodifiable list. For simple data carriers, a record (Q47) does most of this for you.
static final class Team {                           // final class: nobody can subclass and add mutators
    private final String name;
    private final List<String> members;

    Team(String name, List<String> members) {
        this.name = name;
        this.members = List.copyOf(members);        // defensive copy in
    }

    String name() { return name; }
    List<String> members() { return members; }      // already unmodifiable, safe to return
}

List<String> input = new ArrayList<>(List.of("Asha", "Ravi"));
Team team = new Team("Core", input);
input.add("Intruder");                          // does not leak into the Team
System.out.println("members: " + team.members());
try {
    team.members().add("Hacker");
} catch (UnsupportedOperationException e) {
    System.out.println("add blocked: UnsupportedOperationException");
}
members: [Asha, Ravi]
add blocked: UnsupportedOperationException

Code: OopQa.java · output: 02-oop.txt

Q17. Composition vs inheritance: which should you prefer?

Prefer composition (“has-a”) unless the relationship is truly “is-a”. Inheritance couples the child to everything the parent exposes. The JDK’s own Stack extends Vector, so it inherits add(index, element), and the output shows a “stack” whose last-in-first-out rule has been broken by one legal call. Use ArrayDeque as your stack instead.
Stack<Integer> stack = new Stack<>();           // java.util.Stack EXTENDS Vector, so it inherits all of Vector
stack.push(1);
stack.push(2);
stack.add(0, 99);                               // legal, and it breaks "last in, first out"
System.out.println("stack = " + stack + ", pop gives " + stack.pop());
stack = [99, 1, 2], pop gives 2

Code: OopQa.java · output: 02-oop.txt

Q18. What are the access modifiers in Java?

public: everywhere. protected: same package plus subclasses. No keyword (package-private): same package only. private: same class only. Top-level classes can be only public or package-private. The snippet reads the real modifiers back through reflection.
static class Modifiers {
    public int a;
    protected int b;
    int c;                                          // package-private: no keyword at all
    private int d;
}

for (Field f : Modifiers.class.getDeclaredFields()) {
    String m = Modifier.toString(f.getModifiers());
    System.out.println(f.getName() + " -> " + (m.isEmpty() ? "(package-private)" : m));
}
a -> public
b -> protected
c -> (package-private)
d -> private

Code: OopQa.java · output: 02-oop.txt

Q19. What are equals(), hashCode() and toString() in the Object class?

Every class inherits them from Object. Default equals compares references; default toString prints ClassName@hex. The contract: objects that are equals must return the same hashCode. The Point below overrides equals only, so two equal points are not found in a HashSet.
static class Point {
    final int x, y;
    Point(int x, int y) { this.x = x; this.y = y; }
    @Override public boolean equals(Object o) {
        return o instanceof Point p && p.x == x && p.y == y;
    }
    // hashCode() deliberately NOT overridden
}

Object plain = new Object();
System.out.println("default toString looks like ClassName@hex: " + plain.toString().matches("java\\.lang\\.Object@[0-9a-f]+"));
Point p1 = new Point(1, 2), p2 = new Point(1, 2);
System.out.println("p1.equals(p2) = " + p1.equals(p2));
Set<Point> set = new HashSet<>(List.of(p1));
System.out.println("set contains an equal point: " + set.contains(p2));
default toString looks like ClassName@hex: true
p1.equals(p2) = true
set contains an equal point: false

Code: OopQa.java · output: 02-oop.txt

Q20. What are constructors? Can a constructor be private?

A constructor builds an object and has the class name with no return type. If you write none, the compiler adds a no-argument one; as soon as you write any constructor, that free one is gone. this(...) calls another constructor of the same class. A private constructor is legal and is how singletons and utility classes stop outsiders calling new.
static class Config {
    final String env;
    final int port;

    Config() { this("dev", 8080); }                 // this(...) chains to another constructor
    Config(String env, int port) { this.env = env; this.port = port; }
    // The moment you write ANY constructor, the free no-argument one disappears.
    // So Config() above had to be written by hand.
}

static class Registry {
    private static final Registry INSTANCE = new Registry();
    private Registry() {}                           // private constructor: nobody else can call new
    static Registry get() { return INSTANCE; }
}

Config c = new Config();
System.out.println("default config: " + c.env + ":" + c.port);
System.out.println("same Registry instance: " + (Registry.get() == Registry.get()));
default config: dev:8080
same Registry instance: true

Code: OopQa.java · output: 02-oop.txt

Going deeper: more on this section

Strings: the most asked topic in fresher interviews

Strings are objects with special treatment. Almost every question here comes back to two facts: strings are immutable, and literals are pooled.

Q21. What is the difference between == and equals() for Strings?

== asks “same object?”; equals() asks “same characters?”. Literals are shared in the string pool, so a == b is true. new String(...) always builds a separate heap object, so a == c is false while a.equals(c) is true. Always compare strings with equals().
Stack (variables)HeapabcString pool“java”“java” (new)== compares the arrows (same object?); equals() compares the characters. a == b is true, a == c is false, a.equals(c) is true.
Reading the picture for Q21: a and b hold the same arrow into the pool, c holds an arrow to a different object. That is the whole reason == and equals() disagree.
String a = "java";
String b = "java";
String c = new String("java");
System.out.println("a == b      -> " + (a == b));      // two literals share one pooled object
System.out.println("a == c      -> " + (a == c));      // new always builds a separate object
System.out.println("a.equals(c) -> " + a.equals(c));   // equals compares the characters
a == b      -> true
a == c      -> false
a.equals(c) -> true

Code: StringsQa.java · output: 03-strings.txt

Q22. Why is String immutable in Java?

Once created, a String never changes; methods like toUpperCase() return a new one. That makes pooling safe (many variables can share one object), makes strings safe as HashMap keys because the hash cannot change, makes them thread-safe without locks, and stops a validated file path or URL from being altered afterwards.
String s = "java";
String upper = s.toUpperCase();
System.out.println("s     = " + s);
System.out.println("upper = " + upper);
System.out.println("same object returned when nothing changes: " + (s.toLowerCase() == s));
s     = java
upper = JAVA
same object returned when nothing changes: true

Code: StringsQa.java · output: 03-strings.txt

Q23. String vs StringBuilder vs StringBuffer?

String is immutable. StringBuilder is a mutable buffer for building text in a loop, and it is not thread-safe. StringBuffer is the older synchronised version. The output proves the last difference with reflection: append is synchronized on StringBuffer only. Use StringBuilder unless several threads share the buffer, which is rare.
StringBuilder sb = new StringBuilder("a");
sb.append("b").append("c").insert(0, "-").reverse();
System.out.println("builder result: " + sb);
boolean buf = Modifier.isSynchronized(StringBuffer.class.getMethod("append", String.class).getModifiers());
boolean bld = Modifier.isSynchronized(StringBuilder.class.getMethod("append", String.class).getModifiers());
System.out.println("append is synchronized: StringBuffer=" + buf + ", StringBuilder=" + bld);
builder result: cba-
append is synchronized: StringBuffer=true, StringBuilder=false

Code: StringsQa.java · output: 03-strings.txt

Q24. How does the string pool treat concatenation, and what does intern() do?

A concatenation of compile-time constants is folded by the compiler into one literal, so it lands in the pool. A concatenation that involves an ordinary variable happens at run time and creates a new object. A final variable initialised with a constant counts as a constant. intern() returns the pooled instance, which is why the last line is true.
String lo = "lo";
final String finalLo = "lo";
System.out.println("\"hel\" + \"lo\" == \"hello\"      -> " + ("hel" + "lo" == "hello"));
System.out.println("\"hel\" + lo == \"hello\"        -> " + ("hel" + lo == "hello"));
System.out.println("\"hel\" + finalLo == \"hello\"   -> " + ("hel" + finalLo == "hello"));
System.out.println("(\"hel\" + lo).intern() == \"hello\" -> " + (("hel" + lo).intern() == "hello"));
"hel" + "lo" == "hello"      -> true
"hel" + lo == "hello"        -> false
"hel" + finalLo == "hello"   -> true
("hel" + lo).intern() == "hello" -> true

Code: StringsQa.java · output: 03-strings.txt

Q25. How do you reverse a string and check for a palindrome?

Reverse with new StringBuilder(s).reverse(). For a palindrome, clean the text first (lower case, letters and digits only) and compare it with its reverse. Interviewers often ask you to do it with two pointers and no extra buffer; the StringBuilder form is the one to write when you only need it to work.
static String reverse(String s) {
    return new StringBuilder(s).reverse().toString();
}

static boolean isPalindrome(String s) {
    String cleaned = s.toLowerCase().replaceAll("[^a-z0-9]", "");
    return cleaned.equals(reverse(cleaned));
}

System.out.println(reverse("interview"));
System.out.println(isPalindrome("A man, a plan, a canal: Panama"));
System.out.println(isPalindrome("hello"));
weivretni
true
false

Code: StringsQa.java · output: 03-strings.txt

Q26. Which String methods should every fresher know? What surprises people about split()?

length, charAt, substring, indexOf, contains, equalsIgnoreCase, replace, split, join, repeat, isBlank, strip. Two traps show in the output. trim() only removes characters up to the space, so it left the em spaces on and the length is 6, while strip() understands Unicode whitespace and gives 2. split drops trailing empty strings unless you pass a negative limit.
String spaced = "\u2003 hi \u2003";                       // an em space on each side
System.out.println("trim().length()  = " + spaced.trim().length());
System.out.println("strip().length() = " + spaced.strip().length());
System.out.println("\"   \".isBlank() = " + "   ".isBlank() + ", \"ab\".repeat(3) = " + "ab".repeat(3));
System.out.println("\"a,b,,\".split(\",\").length     = " + "a,b,,".split(",").length);
System.out.println("\"a,b,,\".split(\",\", -1).length = " + "a,b,,".split(",", -1).length);
System.out.println("String.join: " + String.join("-", "2026", "11", "11"));
trim().length()  = 6
strip().length() = 2
"   ".isBlank() = true, "ab".repeat(3) = ababab
"a,b,,".split(",").length     = 2
"a,b,,".split(",", -1).length = 4
String.join: 2026-11-11

Code: StringsQa.java · output: 03-strings.txt

Q27. How do you count character frequency, find the first non-repeating character and check an anagram?

Use a LinkedHashMap to count, because it remembers insertion order, so the first entry with count 1 is the first non-repeating character. Two words are anagrams if their sorted characters are equal. These three problems cover most string-and-map rounds for freshers.
static Map<Character, Integer> frequency(String s) {
    Map<Character, Integer> counts = new LinkedHashMap<>();   // keeps first-seen order
    for (char ch : s.toCharArray()) {
        counts.merge(ch, 1, Integer::sum);
    }
    return counts;
}

static boolean areAnagrams(String x, String y) {
    char[] a = x.toCharArray(), b = y.toCharArray();
    Arrays.sort(a);
    Arrays.sort(b);
    return Arrays.equals(a, b);
}

Map<Character, Integer> counts = frequency("swiss");
System.out.println("frequency: " + counts);
char first = counts.entrySet().stream().filter(e -> e.getValue() == 1).findFirst().get().getKey();
System.out.println("first non-repeating: " + first);
System.out.println("listen/silent anagrams: " + areAnagrams("listen", "silent"));
frequency: {s=3, w=1, i=1}
first non-repeating: w
listen/silent anagrams: true

Code: StringsQa.java · output: 03-strings.txt

Going deeper: more on this section

Collections: choose the right container and explain why

Interviewers rarely ask you to list classes. They ask why you would pick one over another, so each answer below ends in a reason you can say out loud.

Q28. What is the Java Collections Framework? Draw the hierarchy.

A set of interfaces and classes for storing groups of objects. List keeps order and allows duplicates, Set rejects duplicates, Queue/Deque handle first-in-first-out and both-ends access, and Map stores key/value pairs and is not part of Collection. The output lists the real parent types of seven common classes, read from the JDK.
IterableCollectionListArrayList, LinkedListSetHashSet, TreeSetQueuePriorityQueueDequeMapHashMap, TreeMapnot a CollectionMap holds key/value pairs, so it sits outside the Collection tree. LinkedList and ArrayDeque are both Deques.
Reading the picture for Q28: everything under Collection is a group of elements; Map is its own tree. Queue and Deque are the ones people forget.
for (Class<?> c : List.of(ArrayList.class, LinkedList.class, HashSet.class, TreeSet.class, ArrayDeque.class, HashMap.class, TreeMap.class)) {
    String parents = Arrays.stream(c.getInterfaces()).map(Class::getSimpleName).toList().toString();
    System.out.println(c.getSimpleName() + " extends " + c.getSuperclass().getSimpleName() + " implements " + parents);
}
ArrayList extends AbstractList implements [List, RandomAccess, Cloneable, Serializable]
LinkedList extends AbstractSequentialList implements [List, Deque, Cloneable, Serializable]
HashSet extends AbstractSet implements [Set, Cloneable, Serializable]
TreeSet extends AbstractSet implements [NavigableSet, Cloneable, Serializable]
ArrayDeque extends AbstractCollection implements [Deque, Cloneable, Serializable]
HashMap extends AbstractMap implements [Map, Cloneable, Serializable]
TreeMap extends AbstractMap implements [NavigableMap, Cloneable, Serializable]

Code: CollectionsQa.java · output: 04-collections.txt

Q29. ArrayList vs LinkedList: which one and when?

ArrayList is backed by an array: fast get(i), slow insertion at the front because later elements shift. LinkedList is a chain of nodes: cheap add/remove at the ends, slow get(i) because it must walk. Both of those are from how they are built, and the marker interface RandomAccess in the output is the JDK’s own way of saying which is which. In practice ArrayList is the default; reach for ArrayDeque before LinkedList when you need a queue.
System.out.println("ArrayList is RandomAccess:  " + (new ArrayList<>() instanceof RandomAccess));
System.out.println("LinkedList is RandomAccess: " + (new LinkedList<>() instanceof RandomAccess));
System.out.println("LinkedList is also a Deque: " + (new LinkedList<>() instanceof Deque));
List<Integer> list = new ArrayList<>(List.of(10, 20, 30));
list.add(0, 5);                                  // ArrayList shifts every later element one slot right
System.out.println("after add(0, 5): " + list + ", get(2) = " + list.get(2));
ArrayList is RandomAccess:  true
LinkedList is RandomAccess: false
LinkedList is also a Deque: true
after add(0, 5): [5, 10, 20, 30], get(2) = 20

Code: CollectionsQa.java · output: 04-collections.txt

Q30. How does HashMap work internally?

A HashMap keeps an array of buckets. To store a key it calls hashCode(), mixes the high bits into the low bits, and uses the result to pick a bucket. Keys in the same bucket are told apart with equals(). Two different keys can share a hash code, as "Aa" and "BB" do (both 2112), and both are still found.
table (16 buckets)0123456789101112131415AaBBAnkurBoth “Aa” and “BB” have hashCode 2112, so they share bucket 0 and are told apart by equals().Lookup = compute the bucket from the hash, then walk that bucket comparing with equals(). A bad hashCode makes the chain long.Bucket numbers come from Q30’s output: Aa and BB land in 0, Ankur in 12.
The picture is Q30’s output drawn out: Aa and BB share bucket 0, Ankur sits alone in bucket 12, and finding a key means going to its bucket and comparing with equals().
// Same formula as java.util.HashMap.hash() followed by the (table.length - 1) mask
for (String key : new String[] {"Aa", "BB", "Ankur"}) {
    int h = key.hashCode();
    int spread = h ^ (h >>> 16);
    System.out.println(key + ": hashCode=" + h + ", bucket (16 slots)=" + (spread & 15));
}
Map<String, String> map = new HashMap<>();
map.put("Aa", "first");
map.put("BB", "second");                         // same hashCode, so same bucket, but not equals()
System.out.println("both survive a collision: " + map.get("Aa") + ", " + map.get("BB") + ", size " + map.size());
Aa: hashCode=2112, bucket (16 slots)=0
BB: hashCode=2112, bucket (16 slots)=0
Ankur: hashCode=63412443, bucket (16 slots)=12
both survive a collision: first, second, size 2

Code: CollectionsQa.java · output: 04-collections.txt

Deeper: resize, treeification and the source of the formula
  • The mixing step h ^ (h >>> 16) is copied from HashMap.hash(), which I read in the JDK 25 src.zip while writing this. The default table has 16 buckets and doubles when it is 75% full.
  • A bucket that grows past 8 entries (in a table of at least 64) turns into a tree, so worst-case lookup is O(log n) rather than O(n). The companion post on HashMap internal working shows this on a real map, and HashMap vs ConcurrentHashMap covers the thread-safety side.

Q31. HashMap vs Hashtable vs ConcurrentHashMap vs TreeMap?

HashMap: not thread-safe, one null key allowed, no ordering. Hashtable: legacy, every method synchronised, no nulls. ConcurrentHashMap: built for concurrent use without locking the whole map, no nulls. TreeMap: sorted by key, so it needs to compare keys and cannot take a null. The null-key test in the output is the quickest way to remember the differences.
Map<String, Map<String, Integer>> maps = new LinkedHashMap<>();
maps.put("HashMap", new HashMap<>());
maps.put("Hashtable", new Hashtable<>());
maps.put("ConcurrentHashMap", new ConcurrentHashMap<>());
maps.put("TreeMap", new TreeMap<>());
for (var e : maps.entrySet()) {
    try {
        e.getValue().put(null, 1);
        System.out.println(e.getKey() + ": null key accepted");
    } catch (NullPointerException ex) {
        System.out.println(e.getKey() + ": null key throws NullPointerException");
    }
}
HashMap: null key accepted
Hashtable: null key throws NullPointerException
ConcurrentHashMap: null key throws NullPointerException
TreeMap: null key throws NullPointerException

Code: CollectionsQa.java · output: 04-collections.txt

Q32. HashSet vs LinkedHashSet vs TreeSet?

All three reject duplicates. HashSet has no promised order (the order you see is an accident of the hash codes). LinkedHashSet keeps insertion order. TreeSet keeps sorted order and is slower, O(log n) per operation, because it is a balanced tree.
List<String> fruit = List.of("pear", "apple", "mango", "banana", "cherry");
System.out.println("HashSet       " + new HashSet<>(fruit));
System.out.println("LinkedHashSet " + new LinkedHashSet<>(fruit));
System.out.println("TreeSet       " + new TreeSet<>(fruit));
HashSet       [banana, apple, cherry, pear, mango]
LinkedHashSet [pear, apple, mango, banana, cherry]
TreeSet       [apple, banana, cherry, mango, pear]

Code: CollectionsQa.java · output: 04-collections.txt

Q33. Comparable vs Comparator?

Comparable is the one natural order a class defines for itself (compareTo). Comparator is an outside rule you pass in, so you can sort the same objects several ways without touching the class. Chain comparators with thenComparing.
record Student(String name, int marks) implements Comparable<Student> {
    @Override
    public int compareTo(Student other) {            // the "natural" order: by name
        return name.compareTo(other.name);
    }
}

List<Student> students = new ArrayList<>(List.of(new Student("Ravi", 90), new Student("Asha", 88), new Student("Meena", 88)));
Collections.sort(students);
System.out.println("natural (Comparable):  " + students);
students.sort(Comparator.comparingInt(Student::marks).reversed().thenComparing(Student::name));
System.out.println("marks desc, then name: " + students);
natural (Comparable):  [Student[name=Asha, marks=88], Student[name=Meena, marks=88], Student[name=Ravi, marks=90]]
marks desc, then name: [Student[name=Ravi, marks=90], Student[name=Asha, marks=88], Student[name=Meena, marks=88]]

Code: CollectionsQa.java · output: 04-collections.txt

Q34. What is a fail-fast iterator, and what is ConcurrentModificationException?

If a list is structurally changed while a for-each loop is running, the iterator notices and throws ConcurrentModificationException. It is a best-effort check, not a guarantee: the second experiment removes the second-to-last element and nothing is thrown, but the loop quietly skips the last item. Remove with Iterator.remove() or removeIf.
List<Integer> list = new ArrayList<>(List.of(1, 2, 3, 4, 5));
try {
    for (Integer n : list) {
        if (n == 2) list.remove(n);              // structural change while the for-each iterator is live
    }
} catch (java.util.ConcurrentModificationException e) {
    System.out.println("removing 2 inside for-each -> ConcurrentModificationException");
}
List<Integer> second = new ArrayList<>(List.of(1, 2, 3, 4));
for (Integer n : second) {
    if (n == 3) second.remove(n);                // removing the second-to-last element: no exception, loop just stops
}
System.out.println("removing the second-to-last element threw nothing, list = " + second);
list.removeIf(n -> n % 2 == 0);                  // the safe way
System.out.println("removeIf even -> " + list);
removing 2 inside for-each -> ConcurrentModificationException
removing the second-to-last element threw nothing, list = [1, 2, 4]
removeIf even -> [1, 3, 5]

Code: CollectionsQa.java · output: 04-collections.txt

Q35. What are the pitfalls of Arrays.asList() and List.of()?

Arrays.asList(array) is a fixed-size view over the array: you can set, and a write goes through to the array, but not add or remove. Passing a primitive array like int[] gives a list with one element, the array itself. List.of is immutable and refuses nulls.
String[] array = {"x", "y"};
List<String> view = Arrays.asList(array);
view.set(0, "changed");
System.out.println("write-through to the array: " + array[0]);
try {
    view.add("z");
} catch (UnsupportedOperationException e) {
    System.out.println("Arrays.asList().add -> UnsupportedOperationException");
}
System.out.println("Arrays.asList(int[]) has " + Arrays.asList(new int[] {1, 2, 3}).size() + " element");
try {
    List.of("a", null);
} catch (NullPointerException e) {
    System.out.println("List.of(.., null) -> NullPointerException");
}
write-through to the array: changed
Arrays.asList().add -> UnsupportedOperationException
Arrays.asList(int[]) has 1 element
List.of(.., null) -> NullPointerException

Code: CollectionsQa.java · output: 04-collections.txt

Q36. What are Queue, Deque and PriorityQueue?

Queue is first-in-first-out. Deque lets you add and remove at both ends, so it also works as a stack (ArrayDeque is the modern replacement for Stack). PriorityQueue always hands out the smallest element first, not the oldest.
Queue<Integer> pq = new PriorityQueue<>(List.of(5, 1, 4, 2, 3));
StringBuilder order = new StringBuilder();
while (!pq.isEmpty()) order.append(pq.poll()).append(' ');
System.out.println("PriorityQueue polls: " + order.toString().trim());
Deque<String> stack = new ArrayDeque<>();
stack.push("a"); stack.push("b"); stack.push("c");
System.out.println("ArrayDeque as a stack pops: " + stack.pop() + stack.pop() + stack.pop());
PriorityQueue polls: 1 2 3 4 5
ArrayDeque as a stack pops: cba

Code: CollectionsQa.java · output: 04-collections.txt

Q37. What goes wrong if you use a mutable object as a HashMap key?

The entry is filed in a bucket chosen by the hash at insertion time. Change a field that feeds hashCode() and the key now hashes to a different bucket, so lookups miss it, even with the same object. In the output both lookups return null while the map still holds the entry: it is unreachable. Use immutable keys such as String, Integer or records.
static class Key {
    String id;
    Key(String id) { this.id = id; }
    @Override public boolean equals(Object o) { return o instanceof Key k && k.id.equals(id); }
    @Override public int hashCode() { return id.hashCode(); }
}

Map<Key, String> map = new HashMap<>();
Key k = new Key("A1");
map.put(k, "value");
k.id = "B2";                                     // the hashCode changes after the entry was filed
System.out.println("get with the mutated key: " + map.get(k));
System.out.println("get with a fresh \"A1\" key: " + map.get(new Key("A1")));
System.out.println("size is still " + map.size());
get with the mutated key: null
get with a fresh "A1" key: null
size is still 1

Code: CollectionsQa.java · output: 04-collections.txt

Q38. Collections.unmodifiableList() vs List.copyOf(): what is the difference?

unmodifiableList is a read-only view: you cannot change it, but it shows later changes to the original. List.copyOf is an immutable snapshot. Returning a view from a getter does not protect your internal list.
List<String> original = new ArrayList<>(List.of("a", "b"));
List<String> view = Collections.unmodifiableList(original);
List<String> copy = List.copyOf(original);
original.add("c");
System.out.println("view sees the change: " + view);
System.out.println("copy does not:        " + copy);
view sees the change: [a, b, c]
copy does not:        [a, b]

Code: CollectionsQa.java · output: 04-collections.txt

Going deeper: more on this section

Exceptions: what the compiler forces and what it does not

Exception questions test whether you know which kind the compiler checks, and what finally really does with a return.

Q39. What is the difference between checked and unchecked exceptions?

Checked exceptions (IOException and other subclasses of Exception that are not RuntimeException) must be caught or declared with throws; the compiler enforces it. Unchecked exceptions (RuntimeException and its children such as NullPointerException) signal programming mistakes and need no handling.
ThrowableErrorOutOfMemoryError, StackOverflowErrorExceptionRuntimeExceptionunchecked: NPE, ArithmeticExceptionIOException and friendschecked: you must handleThe compiler enforces handling only for Exception subclasses that are not RuntimeException. Errors are not meant to be caught.
Reading the picture for Q39: only the branch through Exception that does not pass through RuntimeException is checked. Errors are a separate branch you should not catch.
try {
    Files.readString(Path.of("missing.txt"));    // checked: the compiler forces you to deal with IOException
} catch (IOException e) {
    System.out.println("checked:   " + e.getClass().getSimpleName() + " (is an IOException: " + (e instanceof IOException) + ")");
}
try {
    int zero = 0;
    System.out.println(10 / zero);               // unchecked: no try/catch required
} catch (ArithmeticException e) {
    System.out.println("unchecked: ArithmeticException: " + e.getMessage());
}
System.out.println("NoSuchFileException is checked: " + !RuntimeException.class.isAssignableFrom(NoSuchFileException.class));
checked:   NoSuchFileException (is an IOException: true)
unchecked: ArithmeticException: / by zero
NoSuchFileException is checked: true

Code: ExceptionsQa.java · output: 05-exceptions.txt

The compiler’s own message for an unhandled checked exception
import java.nio.file.Files;
import java.nio.file.Path;

class CheckedNotHandled {
    public static void main(String[] args) {
        String s = Files.readString(Path.of("missing.txt"));
    }
}
CheckedNotHandled.java:6: error: unreported exception IOException; must be caught or declared to be thrown
        String s = Files.readString(Path.of("missing.txt"));
                                   ^
1 error

Code and message: CheckedNotHandled.java · 07-compile-errors.txt, produced by scripts/compile-errors.sh.

Q40. Does finally always run? What if there is a return in try?

Almost always: it runs after try or catch, even when they return or throw. It does not run if the JVM exits first (System.exit) or crashes. A return value is computed before finally runs, so changing the local variable in finally does not change what is returned (primitiveSnapshot still gives 10). A return inside finally replaces an in-flight exception and hides it; never write that.
@SuppressWarnings("finally")
static int tryOrFinally() {
    try {
        return 1;
    } finally {
        System.out.println("finally runs before the method actually returns");
    }
}

static int primitiveSnapshot() {
    int x = 10;
    try {
        return x;                                    // value 10 is copied aside here
    } finally {
        x = 99;                                      // too late to change what is returned
    }
}

@SuppressWarnings("finally")
static int finallyOverrides() {
    try {
        throw new IllegalStateException("lost");
    } finally {
        return 7;                                    // swallows the exception: never do this
    }
}

System.out.println("returned " + tryOrFinally());
System.out.println("primitiveSnapshot = " + primitiveSnapshot());
System.out.println("finallyOverrides = " + finallyOverrides());
finally runs before the method actually returns
returned 1
primitiveSnapshot = 10
finallyOverrides = 7

Code: ExceptionsQa.java · output: 05-exceptions.txt

Q41. What is the difference between throw and throws? How do you write a custom exception?

throw is the statement that throws one exception object. throws is the clause on a method signature that declares which checked exceptions callers must handle. For a custom exception extend Exception (checked) or RuntimeException (unchecked) and pass the original exception as the cause, so the stack trace is not lost.
static class InsufficientFundsException extends Exception {
    InsufficientFundsException(String message, Throwable cause) { super(message, cause); }
}

static void withdraw(int balance, int amount) throws InsufficientFundsException {
    try {
        if (amount > balance) throw new IllegalArgumentException("balance " + balance + " < " + amount);
    } catch (IllegalArgumentException e) {
        throw new InsufficientFundsException("withdrawal refused", e);   // wrap, keep the cause
    }
}

try {
    withdraw(100, 500);
} catch (InsufficientFundsException e) {
    System.out.println(e.getMessage() + " <- cause: " + e.getCause().getMessage());
}
withdrawal refused <- cause: balance 100 < 500

Code: ExceptionsQa.java · output: 05-exceptions.txt

Q42. What is try-with-resources?

Any object that implements AutoCloseable declared in the parentheses is closed automatically, in reverse order, whether the body succeeds or fails. If the body throws and close() also throws, the close exception is attached as suppressed instead of replacing the real error. The output shows both facts: second closes first, and its failure is under getSuppressed().
static class Res implements AutoCloseable {
    private final String name;
    private final boolean failOnClose;

    Res(String name, boolean failOnClose) {
        this.name = name;
        this.failOnClose = failOnClose;
        System.out.println("open " + name);
    }

    @Override
    public void close() {
        System.out.println("close " + name);
        if (failOnClose) throw new IllegalStateException("close failed: " + name);
    }
}

try (Res first = new Res("first", false); Res second = new Res("second", true)) {
    throw new RuntimeException("body failed");
} catch (RuntimeException e) {
    System.out.println("caught: " + e.getMessage());
    System.out.println("suppressed: " + e.getSuppressed()[0].getMessage());
}
open first
open second
close second
close first
caught: body failed
suppressed: close failed: second

Code: ExceptionsQa.java · output: 05-exceptions.txt

Q43. What is the difference between Error and Exception? What is multi-catch?

Error means the JVM is in trouble (OutOfMemoryError, StackOverflowError); applications normally should not catch it. Exception is what your code handles. Multi-catch, catch (A | B e), handles unrelated exception types with one block. The first experiment asks for an array bigger than the JVM can ever allocate, so the error comes back instantly with its real message.
try {
    long[] tooBig = new long[Integer.MAX_VALUE];
    System.out.println(tooBig.length);
} catch (OutOfMemoryError e) {
    System.out.println("Error, not Exception: " + e.getMessage());
}
try {
    Object o = "text";
    Integer n = (Integer) o;
} catch (ClassCastException | NullPointerException e) {      // multi-catch: one handler, several types
    System.out.println("multi-catch caught " + e.getClass().getSimpleName());
}
Error, not Exception: Requested array size exceeds VM limit
multi-catch caught ClassCastException

Code: ExceptionsQa.java · output: 05-exceptions.txt

Going deeper: more on this section

Modern Java: lambdas, streams, records and threads

Campus interviews in 2026 assume you know Java 8 features, and increasingly ask about records, text blocks and virtual threads. Each is a few lines of real code below.

Q44. What is a lambda expression and a functional interface?

A lambda is a short anonymous function: price -> price * 0.9. It can be used wherever the target type is a functional interface, an interface with exactly one abstract method. @FunctionalInterface makes the compiler check that. Predicate, Function, Supplier and BiFunction in java.util.function cover most needs. A lambda can use local variables only if they are effectively final.
@FunctionalInterface
interface Discount {
    double apply(double price);
}

Discount tenOff = price -> price * 0.9;
Predicate<String> isEmpty = String::isEmpty;
Function<String, Integer> length = String::length;
Supplier<String> greeting = () -> "hello";
BiFunction<Integer, Integer, Integer> add = (x, y) -> x + y;
System.out.println(tenOff.apply(200) + " " + isEmpty.test("") + " " + length.apply("lambda")
        + " " + greeting.get() + " " + add.apply(2, 3));
180.0 true 6 hello 5

Code: ModernJavaQa.java · output: 06-modern-java.txt

Q45. What is the Stream API? What does lazy mean?

A stream is a pipeline over a source: intermediate steps (filter, map) describe the work and a terminal step (collect, count, findFirst) runs it. Nothing happens until the terminal step, and findFirst stops after one element: only “visited asha” is printed. A stream can be consumed once; the second count() throws.
List<String> names = List.of("asha", "ravi", "meena", "arjun");
System.out.println(names.stream().filter(n -> n.startsWith("a")).map(String::toUpperCase).collect(Collectors.toList()));
System.out.println(names.stream().collect(Collectors.groupingBy(String::length, Collectors.counting())));
Stream<String> lazy = names.stream().peek(n -> System.out.println("visited " + n));
System.out.println("nothing visited yet: no terminal operation");
System.out.println("first = " + lazy.findFirst().get());   // stops after the first element
Stream<String> used = names.stream();
used.count();
try {
    used.count();
} catch (IllegalStateException e) {
    System.out.println("reuse -> " + e.getMessage());
}
[ASHA, ARJUN]
{4=2, 5=2}
nothing visited yet: no terminal operation
visited asha
first = asha
reuse -> stream has already been operated upon or closed

Code: ModernJavaQa.java · output: 06-modern-java.txt

Q46. What is Optional, and orElse vs orElseGet?

Optional is a container that may or may not hold a value, used as a return type to avoid returning null. orElse(x) always evaluates x, even when a value is present, as the “expensive() called” line proves; orElseGet(supplier) only calls the supplier when needed. Do not use Optional for fields or method parameters.
Optional<String> present = Optional.of("value");
System.out.println("orElse    : " + present.orElse(expensive()));       // argument is evaluated even though unused
System.out.println("orElseGet : " + present.orElseGet(() -> expensive())); // lambda is never run
Optional<String> empty = Optional.empty();
System.out.println(empty.map(String::toUpperCase).orElse("nothing here"));
expensive() called
orElse    : value
orElseGet : value
nothing here

Code: ModernJavaQa.java · output: 06-modern-java.txt

Q47. What is a record?

A short way to declare an immutable data carrier: record Point(int x, int y) generates the constructor, accessors (x()), equals, hashCode and toString. A compact constructor adds validation. Records are final and cannot extend another class, but they can implement interfaces.
record Point(int x, int y) {
    Point {                                          // compact constructor: validation
        if (x < 0 || y < 0) throw new IllegalArgumentException("negative coordinate");
    }
}

Point p = new Point(1, 2);
System.out.println(p + " equals " + new Point(1, 2) + ": " + p.equals(new Point(1, 2)) + ", x() = " + p.x());
try {
    new Point(-1, 0);
} catch (IllegalArgumentException e) {
    System.out.println("rejected: " + e.getMessage());
}
Point[x=1, y=2] equals Point[x=1, y=2]: true, x() = 1
rejected: negative coordinate

Code: ModernJavaQa.java · output: 06-modern-java.txt

Q48. What are var, switch expressions and text blocks?

var (Java 10) lets the compiler infer a local variable’s type; the variable is still statically typed. A switch expression (Java 14) returns a value and has no fall-through. Text blocks (Java 15) are multi-line string literals in triple quotes, with the indentation trimmed.
var scores = Map.of("asha", 91);
int marks = scores.get("asha");
String grade = switch (marks / 10) {
    case 10, 9 -> "A";
    case 8 -> "B";
    default -> "C";
};
String json = """
        {"name": "asha",
         "grade": "%s"}
        """.formatted(grade);
System.out.print(json);
{"name": "asha",
 "grade": "A"}

Code: ModernJavaQa.java · output: 06-modern-java.txt

Q49. What are method references and default methods?

A method reference is a shorthand lambda: String::compareTo and System.out::print. A default method gives an interface a body so new methods can be added without breaking implementers. If two interfaces supply the same default, the class must override and choose with Walker.super.move().
interface Walker { default String move() { return "walk"; } }
interface Swimmer { default String move() { return "swim"; } }

static class Duck implements Walker, Swimmer {
    @Override public String move() { return Walker.super.move() + " and " + Swimmer.super.move(); }
}

List<String> items = List.of("b", "a", "c");
items.stream().sorted(String::compareTo).forEach(System.out::print);   // static-style and bound references
System.out.println();
System.out.println(new Duck().move());
abc
walk and swim

Code: ModernJavaQa.java · output: 06-modern-java.txt

Q50. What is the difference between start() and run()? What are virtual threads?

start() creates a new thread that calls run(). Calling run() directly is an ordinary method call on the current thread, so the output shows “main” for the first line. A virtual thread (Java 21, final) is a very cheap thread scheduled by the JVM instead of the operating system, made for large numbers of blocking tasks.
Runnable task = () -> System.out.println("running on: " + Thread.currentThread().getName());
Thread t = new Thread(task, "worker-1");
t.run();                                         // plain method call, still on main
t.start();                                       // new thread
t.join();
Thread v = Thread.ofVirtual().name("virtual-1").start(() ->
        System.out.println("virtual: " + Thread.currentThread().isVirtual()));
v.join();
running on: main
running on: worker-1
virtual: true

Code: ModernJavaQa.java · output: 06-modern-java.txt

Deeper: what to read next on threads
Going deeper: more on this section

Should you memorise these fifty answers?

No, and interviewers can tell. A rehearsed definition falls apart on the first follow-up (“so what does hashCode return after you change the field?”). The reliable way to prepare is the one this page uses: say the answer, run the code, and read the output until it stops surprising you. Pick the ten questions whose output surprised you most (for most readers that is Q5, Q24, Q34, Q37 and Q40) and re-run those demos yourself. After that, move to a deeper set such as Java Interview Questions for 5 Years Experience.

Further reading

No Comments yet!

This site uses Akismet to reduce spam. Learn how your comment data is processed.