Java 21 OCP - Questions & Answers
Collections & Generics
Q1: What is the READ-WRITE rule for bounded wildcards?
Answer: Read Extends, Write Super
? extends T
: READ-ONLY - Can read items as type T, cannot add anything (except null)? super T
: WRITE-ONLY - Can write T and subtypes, can only read as Object
Example:
List<? extends Number> readList = List.of(1, 2.0, 3L);
Number n = readList.get(0); // ✅ Can read as Number
// readList.add(4); // ❌ Cannot write
List<? super Integer> writeList = new ArrayList<Number>();
writeList.add(42); // ✅ Can write Integer
Object obj = writeList.get(0); // ✅ Can only read as Object
Q2: How do Deque stack and queue operations work together?
Answer: Deque can act as both Stack (LIFO) and Queue (FIFO) simultaneously:
- Stack operations:
push()
andpop()
work on the head - Queue operations:
offer()
/add()
work on tail,poll()
/remove()
work on head - You can mix operations on the same Deque
Example:
Deque<String> deque = new ArrayDeque<>();
deque.push("First"); // [First] (head)
deque.offerLast("Last"); // [First, Last] (tail)
deque.push("New First"); // [New First, First, Last] (head)
System.out.println(deque.poll()); // "New First" (removes from head)
Q3: What are the differences between HashSet, LinkedHashSet, and TreeSet?
Answer:
- HashSet: No ordering, O(1) operations, allows null
- LinkedHashSet: Insertion order, O(1) operations, allows null
- TreeSet: Natural/comparator ordering, O(log n) operations, no null
Memory tip: “HASH-LINKED-TREE” = No order → Insertion order → Sorted order
Q4: How does Map.merge() work when the key exists vs doesn’t exist?
Answer:
- Key exists: Combines existing value with new value using the merge function
- Key doesn’t exist: Simply inserts the new value (merge function not called)
Map<String, Integer> map = new HashMap<>();
map.put("Alice", 85);
map.merge("Alice", 10, Integer::sum); // 85 + 10 = 95 (key exists)
map.merge("Bob", 88, Integer::sum); // Just inserts 88 (key doesn't exist)
Enums
Q5: Can enum constructors be called directly with the new keyword?
Answer: No – enum constructors are implicitly private and can only be called when declaring enum constants.
public enum Planet {
EARTH(5.976e+24, 6.37814e6), // ✅ Constructor called here
MARS(6.421e+23, 3.3972e6);
private final double mass;
private final double radius;
Planet(double mass, double radius) { // Implicitly private
this.mass = mass;
this.radius = radius;
}
}
// Planet p = new Planet(1.0, 2.0); // ❌ Compile error
Key insight: Enum constants are the only instances that can ever exist.
Q6: How can enum constants override methods?
Answer: Each enum constant can provide its own implementation of methods by using an anonymous class body:
public enum Operation {
PLUS("+") {
public double apply(double x, double y) { return x + y; }
},
MINUS("-") {
public double apply(double x, double y) { return x - y; }
};
private final String symbol;
Operation(String symbol) { this.symbol = symbol; }
public abstract double apply(double x, double y); // Must be implemented by each constant
}
Usage: Operation.PLUS.apply(2, 3)
returns 5.0
I/O and NIO
Q7: What does Files.mismatch() return and when should you use it?
Answer: Files.mismatch()
compares two files byte by byte:
- Returns index of first mismatching byte (0-based)
- Returns -1 if files are identical
- Throws
IOException
if paths are invalid
Path file1 = Path.of("doc1.txt"); // Content: "Hello World"
Path file2 = Path.of("doc2.txt"); // Content: "Hello Mars"
long result = Files.mismatch(file1, file2); // Returns 6 (index of 'W' vs 'M')
Use case: Efficient file comparison without loading entire files into memory.
Q8: When should you use Files.readString() vs BufferedReader?
Answer:
- Files.readString(): Small files when you need the entire content at once
- BufferedReader: Large files or line-by-line processing to avoid memory issues
// For small files:
String content = Files.readString(Path.of("small.txt"));
// For large files:
try (BufferedReader reader = Files.newBufferedReader(Path.of("large.txt"))) {
reader.lines()
.filter(line -> line.contains("important"))
.forEach(System.out::println);
}
Q9: What’s the difference between Path.resolve() with relative vs absolute paths?
Answer:
- Relative path: Appended to the base path
- Absolute path: The absolute path is returned unchanged (absolute paths “win”)
Path base = Path.of("/home/user");
Path resolved1 = base.resolve("documents/file.txt"); // /home/user/documents/file.txt
Path resolved2 = base.resolve("/etc/config"); // /etc/config (absolute wins)
Q10: When should you use InputStreamReader vs FileReader?
Answer:
- InputStreamReader: When you need explicit encoding control or wrapping a byte stream
- FileReader: Convenience class that uses platform default encoding
// Explicit encoding control:
try (FileInputStream fis = new FileInputStream("file.txt");
InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8)) {
// Process with UTF-8 encoding
}
// Platform default encoding:
try (FileReader fr = new FileReader("file.txt")) {
// Uses system default encoding
}
Best practice: Use InputStreamReader with explicit encoding for cross-platform compatibility.
Streams & Functional Programming
Q11: What happens if you chain multiple intermediate operations without a terminal operation?
Answer: Nothing executes – intermediate operations are lazy. The pipeline is built but remains dormant until a terminal operation triggers evaluation.
Stream<String> pipeline = Stream.of("a", "bb", "ccc")
.filter(s -> {
System.out.println("Filtering: " + s); // This WON'T print!
return s.length() > 1;
})
.map(String::toUpperCase);
System.out.println("Pipeline created"); // This prints
// Only when terminal operation is called:
List<String> result = pipeline.collect(Collectors.toList()); // NOW filtering prints
Q12: What’s the difference between Collectors.partitioningBy() and groupingBy()?
Answer:
- partitioningBy(): Always creates exactly 2 groups based on boolean predicate
- groupingBy(): Creates multiple groups based on classifier function
List<String> words = List.of("a", "bb", "ccc", "dddd");
// Partition - exactly 2 groups:
Map<Boolean, List<String>> partitioned = words.stream()
.collect(Collectors.partitioningBy(w -> w.length() > 2));
// {false=[a, bb], true=[ccc, dddd]}
// Group - multiple groups:
Map<Integer, List<String>> grouped = words.stream()
.collect(Collectors.groupingBy(String::length));
// {1=[a], 2=[bb], 3=[ccc], 4=[dddd]}
Q13: What determines whether a lambda implements Runnable or Callable?
Answer: The target type of the assignment context:
- Runnable: Lambda returns no value (
void run()
) - **Callable
**: Lambda returns a value (`T call()`)
// Same lambda, different target types:
Runnable task1 = () -> System.out.println("Hello"); // void return - Runnable
Callable<String> task2 = () -> "Hello World"; // String return - Callable<String>
Q14: What’s the difference between ExecutorService submit(Runnable) and submit(Callable) return types?
Answer:
submit(Runnable)
→Future<?>
(result is always null)submit(Callable<T>)
→Future<T>
(result is of type T)
ExecutorService executor = Executors.newFixedThreadPool(2);
Future<?> future1 = executor.submit(() -> System.out.println("Task")); // Future<?> - null result
Future<String> future2 = executor.submit(() -> "Completed"); // Future<String> - String result
Q15: Why can’t you call Optional.get() safely without checking?
Answer: Optional.get()
throws NoSuchElementException if the Optional is empty. Always use safe alternatives:
Optional<String> empty = Optional.empty();
// ❌ Dangerous:
// String value = empty.get(); // NoSuchElementException!
// ✅ Safe alternatives:
String safe1 = empty.orElse("default"); // Returns "default"
String safe2 = empty.orElseGet(() -> "computed"); // Returns "computed"
empty.ifPresent(System.out::println); // Does nothing if empty
Date, Time, and Localization
Q16: Why are LocalDate/LocalTime objects immutable and how does this affect method calls?
Answer: LocalDate/LocalTime/LocalDateTime are immutable - all methods return new instances. The original object never changes.
LocalDate today = LocalDate.now();
today.plusDays(1); // ❌ Result ignored - today unchanged
LocalDate tomorrow = today.plusDays(1); // ✅ Correct - assigns new instance
Impact: You must always assign the result of date/time operations, or they have no effect.
Q17: How does DST “fall back” affect time calculations?
Answer: When DST ends, one hour (typically 1:00-2:00 AM) occurs twice, creating a 25-hour day. This affects calculations crossing the repeated hour.
Example: From 3:00 AM to 1:00 AM on DST end day:
- Normal day: -2 hours
- DST fall back day: Still -2 hours (ZonedDateTime picks the later 1:00 AM occurrence)
Key insight: The repeated hour makes some time differences longer than expected.
Q18: How does ResourceBundle fallback mechanism work?
Answer: ResourceBundle searches from most specific to most general:
For Locale(“fr”, “CA”):
messages_fr_CA.properties
messages_fr.properties
messages.properties
(default)
Uses the most specific match found. If only messages_fr.properties
and messages.properties
exist, it uses the French version.
Q19: What are the key differences between DateTimeFormatter patterns?
Answer: Common pattern symbols:
- y - year (yyyy = 2024, yy = 24)
- M - month (MM = 03, MMM = Mar, MMMM = March)
- d - day (dd = 15)
- H - hour 24-hour (HH = 14)
- h - hour 12-hour (hh = 02)
- a - AM/PM marker
- E - day of week (EEE = Fri, EEEE = Friday)
Thread safety: DateTimeFormatter is immutable and thread-safe - reuse instances for performance.
Exception Handling
Q20: In try-with-resources, which exception takes priority when both try block and close() throw exceptions?
Answer: The try block exception is primary. Exceptions from close()
are suppressed and attached to the primary exception.
try (MyResource res = new MyResource()) {
throw new RuntimeException("Primary"); // This becomes the main exception
// close() throws RuntimeException("Close") - this gets suppressed
} catch (Exception e) {
// e.getMessage() returns "Primary"
// e.getSuppressed()[0].getMessage() returns "Close"
}
Q21: Why can’t catch blocks catch exceptions thrown by other catch blocks in the same try-catch?
Answer: Only exceptions thrown in the try block can be caught by catch blocks. Exceptions from catch blocks propagate up uncaught.
try {
throw new IOException(); // ✅ Can be caught
} catch (IOException e) {
throw new SQLException(); // ❌ Cannot be caught by next catch
} catch (SQLException e) { // Never executes - SQLException from catch block
System.out.println("Won't run");
}
Solution: Use nested try-catch to handle exceptions from catch blocks.
Q22: What’s the difference between printing an exception and calling printStackTrace()?
Answer:
- System.out.println(exception): Shows only exception class name and message
- exception.printStackTrace(): Shows complete method call chain with line numbers
// Output of println(exception): java.lang.RuntimeException: Error message
// Output of printStackTrace():
// java.lang.RuntimeException: Error message
// at Method.name(File.java:line)
// at Caller.method(File.java:line)
// at Main.main(File.java:line)
Java 21 Features
Q23: How do guarded patterns work in switch expressions?
Answer: Guarded patterns use when
to add conditions. Patterns are checked in order until one matches.
return switch (obj) {
case String s when s.length() > 5 -> "Long string";
case String s when s.isEmpty() -> "Empty string";
case String s -> "Short string: " + s; // Catches remaining strings
default -> "Not a string";
};
Critical: Always provide a fallback case or default
to avoid MatchException
.
Q24: What are the key rules for sealed classes?
Answer: Sealed classes restrict inheritance through the permits
clause:
- Permitted subclasses must be:
final
,sealed
, ornon-sealed
- All permitted classes must extend/implement the sealed type
- Compiler knows all possibilities - enables exhaustive pattern matching
public sealed class Shape permits Circle, Rectangle, Triangle {}
final class Circle extends Shape {} // Cannot be extended further
sealed class Rectangle extends Shape permits Square {} // Can only be extended by Square
non-sealed class Triangle extends Shape {} // Can be extended by anyone
Q25: What do records automatically generate and what are the validation options?
Answer: Records automatically generate:
- Constructor with all fields as parameters
- Accessor methods (not getters - just field names)
- equals(), hashCode(), toString()
Validation: Use compact constructor syntax:
public record Person(String name, int age) {
public Person { // Compact constructor
if (age < 0) throw new IllegalArgumentException("Age cannot be negative");
if (name == null || name.isBlank()) throw new IllegalArgumentException("Name required");
}
}
Q26: When should you use text blocks and what are the syntax rules?
Answer: Use text blocks for multi-line strings like HTML, JSON, SQL:
Syntax rules:
- Start with
"""
followed by line terminator - Content preserves formatting and indentation
- End with
"""
(can be on same line as content or separate line)
String html = """
<html>
<body>
<h1>Hello World</h1>
</body>
</html>
""";
Benefits: Preserves formatting, reduces concatenation, improves readability.
Math, Arrays, Wrappers
Q27: How does Arrays.binarySearch() indicate when an element is not found?
Answer: Returns -(insertion point) - 1
when element not found.
Formula: If result is negative: insertion point = -result - 1
int[] sorted = {10, 20, 30, 40, 50};
int result = Arrays.binarySearch(sorted, 25); // Returns -3
int insertionPoint = -result - 1; // -(-3) - 1 = 2
// 25 would be inserted at index 2 (between 20 and 30)
Q28: What are the wrapper class caching rules and why do they matter?
Answer: Wrapper classes cache small values for performance:
- Integer: -128 to 127
- Boolean: TRUE and FALSE
- Character: 0 to 127
Impact: Cached values return same object reference (== is true), non-cached create new objects.
Integer a = 127, b = 127; // Same cached object
Integer c = 128, d = 128; // Different objects
System.out.println(a == b); // true (cached)
System.out.println(c == d); // false (not cached)
System.out.println(c.equals(d)); // true (value comparison)
Best practice: Always use .equals()
for wrapper comparisons, never ==
.
Q29: What’s the difference between Arrays.equals() and Arrays.deepEquals()?
Answer:
- Arrays.equals(): Shallow comparison - one level only
- Arrays.deepEquals(): Deep comparison - recursively compares multi-dimensional arrays
int[][] array1 = {{1, 2}, {3, 4}};
int[][] array2 = {{1, 2}, {3, 4}};
Arrays.equals(array1, array2); // false (compares references of sub-arrays)
Arrays.deepEquals(array1, array2); // true (compares content recursively)
Rule: Use deepEquals()
and deepToString()
for multi-dimensional arrays.
Modules & Migration
Q30: What’s the difference between named modules, automatic modules, and the unnamed module?
Answer:
- Named module: Has
module-info.java
, explicit declarations - Automatic module: JAR on module path without
module-info.java
, gets automatic name from JAR filename - Unnamed module: Code on classpath (not module path), can read all modules but cannot be required
Example automatic module naming: jackson-core-2.13.jar
→ jackson.core
Q31: What’s the difference between bottom-up and top-down module migration?
Answer:
- Bottom-up: Convert leaf dependencies first, work up to main app
- ✅ Guaranteed to work, clear dependencies
- ❌ Slower benefits, need to wait for third parties
- Top-down: Convert main app first, dependencies become automatic modules
- ✅ Quick wins, immediate benefits, independent of third parties
- ❌ Automatic module names can change, less predictable
Recommendation: Most projects should use top-down for practicality.
Q32: What’s the difference between exports and opens in module declarations?
Answer:
- exports: Makes packages visible for normal access (public API)
- opens: Allows deep reflection access to private members (for frameworks)
module com.myapp {
exports com.myapp.api; // Public API - normal access
opens com.myapp.model; // For Spring/Hibernate reflection
opens com.myapp.config to // Qualified opens - specific modules only
com.fasterxml.jackson.databind;
}
Without opens: Frameworks cannot access private fields/constructors via reflection.
OOP and Encapsulation
Q33: What’s the difference between method hiding and method overriding?
Answer:
- Instance methods: Overridden - resolved at runtime based on object type
- Static methods: Hidden - resolved at compile time based on reference type
- Fields: Always hidden - resolved at compile time based on reference type
Parent ref = new Child();
ref.instanceMethod(); // Child's version (overridden - runtime)
ref.staticMethod(); // Parent's version (hidden - compile time)
ref.field; // Parent's field (hidden - compile time)
Q33: When Java encounters overloaded methods, what determines which method is called at compile-time vs runtime?**
Answer: Two-Phase Resolution Process
Compile-Time (Method Overloading)
- What’s decided: Which overloaded method signature to use
- Based on: Reference type of the variable (not object type)
- Rule: Exact match first, then compatible types (widening/inheritance)
Runtime (Method Overriding)
- What’s decided: Which implementation of the chosen method to execute
- Based on: Actual object type (polymorphism)
- Rule: Most specific implementation in inheritance hierarchy
interface Person { default void speak() { System.out.println("Person speaking"); } }
class Father implements Person { public void speak() { System.out.println("Father speaking"); } }
class Mother implements Person { public void speak() { System.out.println("Mother speaking"); } }
static void speak(Person p) { System.out.print("Person: "); p.speak(); }
static void speak(Father f) { System.out.print("Father: "); f.speak(); }
public static void main(String[] args) {
Person p = new Father();
Father f = new Father();
Mother m = new Mother();
speak(p); // Compile-time: speak(Person) - Runtime: Father.speak() → "Person: Father speaking"
speak(f); // Compile-time: speak(Father) - Runtime: Father.speak() → "Father: Father speaking"
speak(m); // Compile-time: speak(Person) - Runtime: Mother.speak() → "Person: Mother speaking"
}
Q34: When is super() automatically inserted by the compiler?
Answer: The compiler inserts super()
only if:
- Constructor doesn’t explicitly call
super()
orthis()
- AND the superclass has a no-argument constructor
If parent has only parameterized constructors: You must explicitly call super(args)
.
class Parent {
Parent(String msg) { /* ... */ } // No no-arg constructor
}
class Child extends Parent {
Child() {
super("required"); // ✅ Must explicitly call super with args
}
// Child() {} // ❌ Compile error - implicit super() fails
}
Q35: How does protected access work across packages?
Answer: Protected access across packages has special rules:
- Same package: Accessible everywhere
- Different package: Only accessible from subclass, and only via subclass reference
// Different package
class Child extends Parent {
void test() {
this.protectedMethod(); // ✅ Via subclass reference (this)
new Child().protectedMethod(); // ✅ Via subclass reference
new Parent().protectedMethod(); // ❌ Via parent reference - compile error
}
}
Q36: What’s the key difference between static nested classes and inner classes?
Answer:
- Static nested class: Cannot access non-static outer members, no outer instance needed
- Inner class: Can access all outer members, requires outer instance
class Outer {
private String field = "outer";
static class StaticNested {
// Cannot access 'field' - no outer instance
}
class Inner {
void method() {
System.out.println(field); // ✅ Can access outer field
}
}
}
// Instantiation:
Outer.StaticNested sn = new Outer.StaticNested(); // No outer instance
Outer.Inner inner = new Outer().new Inner(); // Requires outer instance
Special note: Classes in interfaces, enums, and records are implicitly static (not inner classes).
Advanced Collections Topics
Q37: What happens when you try to add null to different Collection types?
Answer:
- ArrayList, LinkedList, HashSet, LinkedHashSet: Allow null values
- TreeSet: Throws NullPointerException (cannot compare null)
- HashMap, LinkedHashMap: Allow null keys and values
- TreeMap: Throws NullPointerException for null keys
- ConcurrentHashMap: Throws NullPointerException for null keys/values
List<String> list = new ArrayList<>();
list.add(null); // ✅ OK
Set<String> hashSet = new HashSet<>();
hashSet.add(null); // ✅ OK
Set<String> treeSet = new TreeSet<>();
// treeSet.add(null); // ❌ NullPointerException
Q38: What’s the difference between fail-fast and fail-safe iterators?
Answer:
- Fail-fast: Throws ConcurrentModificationException if collection is modified during iteration
- Fail-safe: Works on a copy/snapshot, doesn’t throw exceptions
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
// Fail-fast iterator:
for (String s : list) {
list.add("d"); // ❌ ConcurrentModificationException
}
// Safe approach:
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
it.remove(); // ✅ OK - using iterator's remove method
}
Fail-safe collections: ConcurrentHashMap, CopyOnWriteArrayList, etc.
Q39: What’s the difference between Comparable and Comparator?
Answer:
- Comparable: Single, natural ordering defined by the class itself
- Comparator: Multiple custom orderings defined externally
// Comparable - natural ordering
class Person implements Comparable<Person> {
String name;
int age;
@Override
public int compareTo(Person other) {
return this.name.compareTo(other.name); // Natural: by name
}
}
// Comparator - custom orderings
Comparator<Person> byAge = Comparator.comparing(p -> p.age);
Comparator<Person> byNameDesc = Comparator.comparing((Person p) -> p.name).reversed();
Collections.sort(people); // Uses Comparable (by name)
Collections.sort(people, byAge); // Uses Comparator (by age)
Advanced Stream Operations
Q40: What’s the difference between findFirst() and findAny()?
Answer:
- findFirst(): Returns the first element in encounter order
- findAny(): Returns any element (optimized for parallel streams)
List<String> words = List.of("apple", "banana", "cherry");
Optional<String> first = words.stream().findFirst(); // "apple"
Optional<String> any = words.stream().findAny(); // "apple" (sequential)
// In parallel streams:
Optional<String> parallelAny = words.parallelStream().findAny(); // Could be any element
Use findAny() for better performance when you don’t care about which element.
Q41: What’s the difference between reduce() with and without identity?
Answer:
- With identity: Returns T (never empty)
- Without identity: Returns **Optional
** (could be empty)
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// With identity:
int sum = numbers.stream().reduce(0, Integer::sum); // 15 (int)
// Without identity:
Optional<Integer> sum2 = numbers.stream().reduce(Integer::sum); // Optional[15]
// Empty stream:
List<Integer> empty = List.of();
int emptySum = empty.stream().reduce(0, Integer::sum); // 0 (identity)
Optional<Integer> emptySum2 = empty.stream().reduce(Integer::sum); // Optional.empty()
Q42: What’s the difference between map() and flatMap()?
Answer:
- map(): One-to-one transformation (T → R)
- flatMap(): One-to-many transformation, then flattens (T → Stream
)
List<String> sentences = List.of("Hello world", "Java streams");
// map() - transforms each sentence to its length:
List<Integer> lengths = sentences.stream()
.map(String::length) // Stream<Integer>
.collect(Collectors.toList()); // [11, 12]
// flatMap() - splits sentences into words and flattens:
List<String> words = sentences.stream()
.flatMap(s -> Arrays.stream(s.split(" "))) // Stream<String> of words
.collect(Collectors.toList()); // [Hello, world, Java, streams]
Concurrency Fundamentals
Q43: What’s the difference between Runnable and Callable interfaces?
Answer:
- Runnable:
void run()
- no return value, no checked exceptions - Callable:
V call() throws Exception
- returns value, can throw checked exceptions
// Runnable:
Runnable task1 = () -> System.out.println("Running");
// Callable:
Callable<String> task2 = () -> {
Thread.sleep(1000); // Can throw InterruptedException
return "Done";
};
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<?> future1 = executor.submit(task1); // Future<?> - no meaningful result
Future<String> future2 = executor.submit(task2); // Future<String> - String result
Q44: What happens when you submit more tasks than thread pool capacity?
Answer: Tasks are queued and executed when threads become available. The behavior depends on the queue type:
// Fixed thread pool with queue:
ExecutorService executor = Executors.newFixedThreadPool(2); // Only 2 threads
// Submit 5 tasks:
for (int i = 0; i < 5; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("Task " + taskId + " executing");
Thread.sleep(2000); // Simulate work
});
}
// First 2 tasks execute immediately, remaining 3 wait in queue
Queue types:
- Unbounded queue: Tasks wait indefinitely (can cause memory issues)
- Bounded queue: Rejects tasks when full (throws RejectedExecutionException)
Q45: What’s the difference between execute() and submit()?
Answer:
- execute(): Fire-and-forget, no return value, for Runnable only
- submit(): Returns Future for result/status tracking, works with Runnable and Callable
ExecutorService executor = Executors.newSingleThreadExecutor();
// execute() - no feedback:
executor.execute(() -> System.out.println("Fire and forget"));
// submit() - returns Future:
Future<?> future = executor.submit(() -> System.out.println("Trackable"));
future.get(); // Wait for completion and handle exceptions
Pattern Matching and Switch
Q46: What’s the difference between traditional switch and pattern matching switch?
Answer:
- Traditional switch: Only works with constants (int, String, enum)
- Pattern matching switch: Works with types, patterns, and guards
// Traditional switch:
switch (day) {
case "MONDAY":
case "TUESDAY":
return "Weekday";
default:
return "Other";
}
// Pattern matching switch:
return switch (obj) {
case String s when s.length() > 5 -> "Long string: " + s;
case String s -> "Short string: " + s;
case Integer i when i > 100 -> "Big number: " + i;
case Integer i -> "Small number: " + i;
case null -> "Null value";
default -> "Unknown type";
};
Q47: What is the significance of the “exhaustiveness” requirement in pattern matching?
Answer: The compiler must verify that all possible values are handled to prevent runtime MatchException. This is especially important with sealed classes:
public sealed class Result permits Success, Error {}
final class Success extends Result { String data; }
final class Error extends Result { String message; }
// ✅ Exhaustive - compiler knows all possibilities:
String handle(Result result) {
return switch (result) {
case Success s -> "Got: " + s.data;
case Error e -> "Error: " + e.message;
// No default needed - all cases covered
};
}
// ❌ Non-exhaustive without default:
String handleBad(Object obj) {
return switch (obj) {
case String s -> "String: " + s;
case Integer i -> "Number: " + i;
// Missing default - compile error!
};
}
Key insight: Sealed classes enable exhaustive checking without default cases.
Advanced Exception Handling
Q48: What are the rules for multi-catch exception handling?
Answer: Multi-catch allows handling multiple exception types in one catch block with restrictions:
Rules:
- Exception types must not be subclasses of each other
- The variable is implicitly final
- Common operations only (intersection of all types)
try {
riskyOperation();
} catch (IOException | SQLException e) { // ✅ No inheritance relationship
logger.error("Operation failed", e); // Common method
// e = new IOException(); // ❌ e is implicitly final
}
// ❌ Invalid - IOException is parent of FileNotFoundException:
// catch (IOException | FileNotFoundException e) {}
Q49: What happens with exception handling in method overriding?
Answer: Overriding methods have stricter exception rules:
- Checked exceptions: Can only throw same or subclasses of parent’s exceptions
- Unchecked exceptions: No restrictions
- Cannot add new checked exceptions
class Parent {
void method() throws IOException { }
}
class Child extends Parent {
@Override
void method() throws FileNotFoundException { } // ✅ Subclass of IOException
// @Override
// void method() throws SQLException { } // ❌ New checked exception
// @Override
// void method() throws RuntimeException { } // ✅ Unchecked - allowed
}
Q50: What’s the difference between throw and throws?
Answer:
- throw: Actually throws an exception (executable statement)
- throws: Declares that a method might throw exceptions (method signature)
public void processFile(String filename) throws IOException { // declares possible exception
if (filename == null) {
throw new IllegalArgumentException("Filename cannot be null"); // actually throws
}
// Code that might throw IOException
Files.readString(Path.of(filename)); // IOException propagates up
}
Advanced Generics
Q51: What is type erasure and how does it affect generics at runtime?
Answer: Type erasure removes generic type information at runtime for backward compatibility:
List<String> strings = new ArrayList<>();
List<Integer> numbers = new ArrayList<>();
// At runtime, both are just List:
System.out.println(strings.getClass() == numbers.getClass()); // true
// Cannot check generic type at runtime:
// if (list instanceof List<String>) {} // ❌ Compile error
// But can check raw type:
if (list instanceof List) { } // ✅ OK
// Generic information lost:
Method method = List.class.getMethod("add", Object.class); // Parameter is Object, not T
Implications:
- No generic type checking at runtime
- Cannot create arrays of generic types
- Cannot use generics with instanceof (except wildcards)
Q52: What’s the difference between <?> and <? extends Object>?
Answer: They are functionally equivalent but have different semantics:
- <?>: “Unknown type” (preferred for readability)
- <? extends Object>: “Some type that extends Object” (verbose)
List<?> list1 = new ArrayList<String>(); // ✅ More idiomatic
List<? extends Object> list2 = new ArrayList<String>(); // ✅ Same meaning, verbose
// Both have same limitations:
// list1.add("hello"); // ❌ Cannot add anything except null
// list2.add("hello"); // ❌ Cannot add anything except null
Object obj1 = list1.get(0); // ✅ Can read as Object
Object obj2 = list2.get(0); // ✅ Can read as Object
Recommendation: Use <?> for cleaner code.
Q53: Can you create a generic array? Why or why not?
Answer: No, you cannot create generic arrays due to type erasure and array covariance:
// ❌ Compile error:
// List<String>[] array = new List<String>[10];
// ❌ Would be unsafe:
// Object[] objArray = array;
// objArray[0] = new ArrayList<Integer>(); // Runtime: fine, Compile: disaster
// ✅ Workarounds:
List<String>[] array = new List[10]; // Raw type array
List<String>[] array2 = (List<String>[]) new List[10]; // Unchecked cast
// ✅ Better alternative:
List<List<String>> listOfLists = new ArrayList<>();
Reason: Arrays are covariant (String[] is a Object[]) but generics are invariant (List
Advanced Date/Time
Q54: What’s the difference between Instant and ZonedDateTime?
Answer:
- Instant: Point in time on UTC timeline (machine time)
- ZonedDateTime: Point in time with timezone context (human time)
// Same moment in time, different representations:
Instant instant = Instant.now(); // 2024-03-15T14:30:45.123Z
ZonedDateTime tokyo = instant.atZone(ZoneId.of("Asia/Tokyo")); // 2024-03-15T23:30:45.123+09:00
ZonedDateTime ny = instant.atZone(ZoneId.of("America/New_York")); // 2024-03-15T10:30:45.123-04:00
System.out.println(instant.equals(tokyo.toInstant())); // true - same moment
System.out.println(instant.equals(ny.toInstant())); // true - same moment
Use cases:
- Instant: Database timestamps, logging, calculations
- ZonedDateTime: User interfaces, scheduling, display
Q55: How do you handle timezone-aware date arithmetic correctly?
Answer: Always use ZonedDateTime for timezone-aware calculations to handle DST transitions:
// ❌ Wrong - loses timezone information:
LocalDateTime local = LocalDateTime.of(2024, 3, 10, 1, 30); // DST spring forward day
LocalDateTime later = local.plusHours(2); // Just adds 2 hours mechanically
// ✅ Correct - respects timezone rules:
ZonedDateTime zoned = ZonedDateTime.of(2024, 3, 10, 1, 30, 0, 0,
ZoneId.of("America/New_York"));
ZonedDateTime zonedLater = zoned.plusHours(2); // Handles DST transition correctly
// On DST spring forward, 2:00 AM becomes 3:00 AM, so adding 1 hour to 1:30 AM gives 3:30 AM
Q56: What’s the difference between Period and Duration?
Answer:
- Period: Date-based amount (years, months, days)
- Duration: Time-based amount (hours, minutes, seconds)
// Period - for dates:
Period period = Period.of(1, 2, 15); // 1 year, 2 months, 15 days
LocalDate date = LocalDate.of(2024, 1, 1);
LocalDate later = date.plus(period); // 2025-03-16
// Duration - for time:
Duration duration = Duration.ofHours(2).plusMinutes(30);
Instant instant = Instant.now();
Instant laterInstant = instant.plus(duration);
// ❌ Wrong combinations:
// LocalDate.now().plus(Duration.ofHours(1)); // Compile error
// LocalTime.now().plus(Period.ofDays(1)); // Compile error
Rule: Use Period with date-based types, Duration with time-based types.
Advanced I/O Operations
Q57: What’s the difference between character encoding in FileReader vs InputStreamReader?
Answer:
- FileReader: Always uses platform default encoding (can’t be changed)
- InputStreamReader: Allows explicit encoding specification
// FileReader - platform dependent:
try (FileReader fr = new FileReader("file.txt")) {
// Uses system default encoding (UTF-8 on Linux/Mac, Windows-1252 on Windows)
}
// InputStreamReader - explicit encoding:
try (FileInputStream fis = new FileInputStream("file.txt");
InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8)) {
// Guaranteed UTF-8 regardless of platform
}
// Modern alternative - Files with explicit encoding:
String content = Files.readString(Path.of("file.txt"), StandardCharsets.UTF_8);
Best practice: Always specify encoding explicitly for cross-platform compatibility.
Q58: What’s the difference between Files.move() vs Files.copy() followed by Files.delete()?
Answer:
- Files.move(): Atomic operation when source and target are on same filesystem
- copy() + delete(): Two separate operations, not atomic
Path source = Path.of("source.txt");
Path target = Path.of("target.txt");
// Atomic move (same filesystem):
Files.move(source, target, StandardCopyOption.REPLACE_EXISTING);
// Non-atomic alternative:
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
Files.delete(source); // If this fails, file exists in both locations!
When to use each:
- move(): When you need atomicity, renaming files
- copy() + delete(): When you need the original to remain temporarily, or need different error handling
Q59: What’s the difference between Path.toAbsolutePath() and Path.toRealPath()?
Answer:
- toAbsolutePath(): Resolves to absolute path textually (doesn’t check filesystem)
- toRealPath(): Resolves to canonical path (checks filesystem, resolves links)
// Create symbolic link: link.txt -> target.txt
Path link = Path.of("link.txt");
Path relative = Path.of("../docs/file.txt");
// toAbsolutePath() - textual resolution:
Path abs1 = link.toAbsolutePath(); // /current/dir/link.txt
Path abs2 = relative.toAbsolutePath(); // /current/dir/../docs/file.txt
// toRealPath() - filesystem resolution:
Path real1 = link.toRealPath(); // /current/dir/target.txt (follows symlink)
Path real2 = relative.toRealPath(); // /current/docs/file.txt (normalizes ..)
toRealPath() can throw IOException if path doesn’t exist or permission denied.
Functional Programming Concepts
Q60: What’s the difference between Function, Predicate, Consumer, and Supplier?
Answer: These are core functional interfaces with different signatures:
// Function<T, R> - transforms input to output:
Function<String, Integer> length = String::length;
Function<String, String> upper = String::toUpperCase;
// Predicate<T> - tests condition, returns boolean:
Predicate<String> isEmpty = String::isEmpty;
Predicate<Integer> isEven = n -> n % 2 == 0;
// Consumer<T> - consumes input, returns nothing:
Consumer<String> printer = System.out::println;
Consumer<List<String>> clearer = List::clear;
// Supplier<T> - supplies output, takes no input:
Supplier<String> randomUuid = () -> UUID.randomUUID().toString();
Supplier<LocalDateTime> now = LocalDateTime::now;
// Usage in streams:
List<String> words = List.of("hello", "world", "java");
words.stream()
.filter(isEmpty.negate()) // Predicate
.map(upper) // Function
.forEach(printer); // Consumer
Q61: How do method references work and when should you use them?
Answer: Method references are shorthand for lambdas when calling existing methods:
Four types:
- Static method:
ClassName::methodName
- Instance method of particular object:
object::methodName
- Instance method of arbitrary object:
ClassName::methodName
- Constructor:
ClassName::new
List<String> words = List.of("apple", "banana", "cherry");
// 1. Static method reference:
words.stream().map(String::valueOf); // String.valueOf(s)
// vs lambda: words.stream().map(s -> String.valueOf(s));
// 2. Instance method of particular object:
PrintStream out = System.out;
words.forEach(out::println); // out.println(s)
// vs lambda: words.forEach(s -> out.println(s));
// 3. Instance method of arbitrary object:
words.stream().map(String::toUpperCase); // s.toUpperCase()
// vs lambda: words.stream().map(s -> s.toUpperCase());
// 4. Constructor reference:
words.stream().map(StringBuilder::new); // new StringBuilder(s)
// vs lambda: words.stream().map(s -> new StringBuilder(s));
When to use: When the lambda just calls a single method without additional logic.
Q61: How do you identify invalid lambda expressions?
Answer:
- A lambda must match the functional interface method.
- Syntax rules:
(params)
orsingleVar
→{ block }
ORsingleExpression
- Compiler can infer: parameter types,
return
(for single-expression), braces (if single expression) - Check validity: ensure all required pieces are present and syntax is correct
Examples of valid lambdas:
x -> x + 1
(x) -> { return x + 1; }
(x, y) -> x * y
Examples of invvalid lambdas:
x, y -> x + y // missing parentheses around multiple parameters
(x) -> x + 1; // semicolon not allowed in single-expression lambda
() -> { "Hello"; } // block without return for non-void
x -> { return; } // returning void in lambda expecting a value
Q62: What is currying and how can it be implemented in Java?
Answer: Currying transforms a function with multiple parameters into a series of functions with single parameters:
// Regular function with 3 parameters:
static int multiply(int a, int b, int c) {
return a * b * c;
}
// Curried version using Function composition:
static Function<Integer, Function<Integer, Function<Integer, Integer>>> curriedMultiply() {
return a -> b -> c -> a * b * c;
}
// Usage:
Function<Integer, Function<Integer, Function<Integer, Integer>>> curried = curriedMultiply();
// Step by step:
Function<Integer, Function<Integer, Integer>> step1 = curried.apply(2);
Function<Integer, Integer> step2 = step1.apply(3);
Integer result = step2.apply(4); // 24
// Or all at once:
Integer result2 = curriedMultiply().apply(2).apply(3).apply(4); // 24
// Practical example - partially applied functions:
Function<Integer, Function<Integer, Integer>> multiplyBy5 = curriedMultiply().apply(5);
Function<Integer, Integer> multiplyBy5And3 = multiplyBy5.apply(3);
int result3 = multiplyBy5And3.apply(2); // 5 * 3 * 2 = 30
Use case: Creating specialized functions from general ones.
Advanced Concurrency
Q63: What’s the difference between CountDownLatch and CyclicBarrier?
Answer:
- CountDownLatch: One-time use, threads wait for countdown to zero
- CyclicBarrier: Reusable, threads wait for each other, then all proceed together
// CountDownLatch - wait for multiple tasks to complete:
CountDownLatch latch = new CountDownLatch(3); // Wait for 3 tasks
// Worker threads:
executor.submit(() -> {
doWork();
latch.countDown(); // Decrease count
});
// Main thread waits:
latch.await(); // Blocks until count reaches 0
System.out.println("All tasks completed");
// CyclicBarrier - coordinate multiple threads:
CyclicBarrier barrier = new CyclicBarrier(3, () ->
System.out.println("All threads reached barrier"));
// Each thread:
executor.submit(() -> {
doPhase1();
barrier.await(); // Wait for other threads
doPhase2(); // All threads start phase2 together
});
Key differences:
- Latch: N-to-1 (N threads signal 1 waiting thread)
- Barrier: N-to-N (N threads wait for each other)
Q64: What happens when you don’t shut down an ExecutorService?
Answer: The JVM won’t terminate because executor threads are still alive, even if main() completes:
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(() -> System.out.println("Task executed"));
// ❌ Without shutdown:
System.out.println("Main finished"); // Prints, but JVM doesn't exit
// ✅ Proper shutdown:
executor.shutdown();
try {
if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
executor.shutdownNow(); // Force shutdown if tasks don't finish
}
} catch (InterruptedException e) {
executor.shutdownNow();
}
}
Best practice: Always shutdown executors, preferably in try-with-resources or finally blocks.
Q65: What’s the difference between parallelStream() and regular streams with parallel execution?
Answer:
- parallelStream(): Creates parallel stream from collection
- stream().parallel(): Converts existing stream to parallel
Both use ForkJoinPool.commonPool() by default:
List<Integer> numbers = List.of(1, 2, 3, 4, 5, 6, 7, 8);
// Method 1 - parallel from start:
int sum1 = numbers.parallelStream()
.filter(n -> n % 2 == 0)
.mapToInt(Integer::intValue)
.sum();
// Method 2 - convert to parallel:
int sum2 = numbers.stream()
.parallel()
.filter(n -> n % 2 == 0)
.mapToInt(Integer::intValue)
.sum();
// Both are equivalent and use the same thread pool
When to use parallel streams:
- ✅ Large datasets (10,000+ elements)
- ✅ CPU-intensive operations
- ❌ I/O operations (blocking)
- ❌ Small datasets (overhead > benefit)