== 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 thefresher-qamodule 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.
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 asjavac. 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 convertsint 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 throwStackOverflowError; 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
- Free practice: the Top 60 Java Viva Questions post covers the oral-exam angle of the same topics.
- Oracle Java Tutorials: Interfaces and Inheritance for the official wording.
- Roadmap: Java Roadmap 2026 shows where these basics lead.
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 withextends. 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.
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 carrydefault, 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 andstatic 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. Astatic 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 classfinal, 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 ownStack 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 fromObject. 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
- SOLID Principles in Java with Examples is the natural next step after composition vs inheritance.
- Top 30 Design Patterns Interview Questions builds on abstract classes and interfaces.
- JLS on constructors and flexible bodies: JEP 513.
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().
== 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, aString 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. Afinal 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 withnew 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 aLinkedHashMap 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
StringAPI documentation, Java 25.- Two classic algorithm problems that use these tools: Sliding Window and Two Pointers and Dynamic Programming for Beginners.
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.
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?
AHashMap 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.
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 fromHashMap.hash(), which I read in the JDK 25src.zipwhile 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 throwsConcurrentModificationException. 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 feedshashCode() 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
- Java Collections Framework: Choosing the Right Data Structure.
- HashMap internal working in Java and LRU Cache in Java for the next level of the same topic.
HashMapAPI documentation, Java 25.
Exceptions: what the compiler forces and what it does not
Exception questions test whether you know which kind the compiler checks, and whatfinally 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.
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 aftertry 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 implementsAutoCloseable 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
- Java Language Specification, chapter 14: Blocks, Statements and Patterns defines exactly how
try,finallyandreturninteract. - Testing exceptions: Top 30 JUnit 6 and Mockito Interview Questions.
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
- Java Multithreading Programs for Interviews covers the odd-even, ping-pong and deadlock programs with runnable code.
- From Raw Threads to Virtual Threads walks from
Threadto executors to virtual threads.
Going deeper: more on this section
- Java Streams API: The Complete Reference Guide.
- JMH Tutorial if you want to measure instead of guess when someone claims one version is faster.
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
- Companion code: the
fresher-qamodule (six classes, six transcripts, five files that deliberately do not compile). - Top 60 Java Viva Questions and Answers for the oral-exam version of these topics.
- Top 40 Hibernate and JPA Interview Questions and Top 30 JUnit 6 and Mockito Interview Questions for the next rounds.
- Java Roadmap 2026: Beginner to Advanced.
- java.util package documentation, Java 25 and JEP 513: Flexible Constructor Bodies.
No Comments yet!