Core Java Interview Questions and Answers: 60 Questions from Easy to Hard
This guide is for freshers and experienced developers in India preparing for a Core Java interview, whether it is a campus drive, a service company's technical round or a product company's coding and design rounds. It has 60 questions in three levels. Easy covers the basics every fresher is asked: OOP, strings, the main collections, exceptions and lambdas. Medium covers what people with one to four years meet in real projects: equals and hashCode, generics, streams, thread pools and the bugs they cause. Hard covers internals and design for senior roles: HashMap internals, the memory model, virtual threads, garbage collection and class loading. Answers are written for Java 17 and 21. Read the question, answer it aloud first, then compare, and run the code in jshell.
- 60 questions, 3 levels
- Updated
- CoreHRx Jobs
What Core Java interviews cover
- OOP and language basics: classes, inheritance, polymorphism, interfaces, static and final, and how Java passes arguments. Almost every fresher round starts here.
- Strings and collections: String immutability and the string pool, StringBuilder, and when to choose ArrayList, HashMap, HashSet or TreeSet, including how HashMap works inside.
- Contracts and type safety: equals and hashCode, generics with wildcards and type erasure, and checked versus unchecked exceptions.
- Java 8 to 21 features: lambdas, streams, records, sealed types, switch expressions and pattern matching. Interviewers increasingly expect Java 17 or 21 syntax.
- Concurrency and the JVM for experienced roles: executors, locks, atomics, ConcurrentHashMap, virtual threads, the memory model, heap and stack, and garbage collection.
How to prepare (freshers and experienced)
- Freshers: write a small program for every concept instead of only reading about it. If you say you know HashMap, be ready to explain buckets, hashCode and equals with an example.
- Install JDK 21 and use jshell, the interactive shell since Java 9, to test small ideas quickly, such as what == returns for two Integer objects.
- With one to four years of experience, connect each concept to a bug you have fixed: a ConcurrentModificationException, a duplicate key in Collectors.toMap, or a NullPointerException from unboxing.
- Senior candidates should talk in trade-offs and evidence: thread pool sizing, choosing a garbage collector, reading a heap dump, and measuring with JMH instead of guessing.
- Practise answering aloud in about two minutes: the point first, then why, then an example. Then take the timed mock test to find your weak topics.
Common mistakes and interview-day tips
- Do not answer from Java 8 memory alone. Say which version you mean, for example that records need Java 16 or later and record patterns need Java 21.
- Avoid confident claims you cannot back up, such as HashMap iteration order being predictable. Interviewers often probe exactly these points.
- When asked what code prints, trace it line by line out loud instead of guessing. Most trick questions hinge on one detail, such as overloading being chosen at compile time.
- In live coding, declare variables with interfaces such as List and Map, handle null and empty input, and use clear names. Small habits like these signal experience.
- If you do not know an answer, say what you do know and how you would check it, for example by reading the Javadoc or writing a quick test in jshell.
Core Java interview questions: Easy questions (freshers)
20 questions1. What happens between writing Hello.java and seeing its output on the screen?
The javac compiler turns Hello.java into Hello.class, which holds platform-neutral bytecode, not machine code. When you run java Hello, the JVM starts, class loaders find and load Hello.class, the bytecode verifier checks that it is safe, and the JVM calls main. Bytecode is interpreted at first, and methods that run often are compiled to native code by the JIT compiler. The JDK holds javac and the other tools, while the JVM actually runs the program. Since Java 11, java Hello.java also works for a single-file program: it compiles in memory and runs.
2. What is autoboxing, and why does == on two Integer objects sometimes return true and sometimes false?
Autoboxing is the compiler converting a primitive such as int into its wrapper Integer, and unboxing is the reverse. Boxing calls Integer.valueOf, which the language guarantees to cache for values from -128 to 127, so two boxed 127s are the same object and == is true. For 128 you normally get two different objects, so == compares references and gives false. Compare wrapper values with equals or after unboxing to int. Also watch for unboxing a null Integer, which throws a NullPointerException, a common bug with values read from maps.
Integer a = 127, b = 127;
Integer c = 128, d = 128;
System.out.println(a == b); // true: same cached object
System.out.println(c == d); // false (default JVM settings)
System.out.println(c.equals(d)); // true: compares values
Integer bonus = null;
int total = bonus + 500; // NullPointerException3. Is Java pass-by-value or pass-by-reference? Explain with an example.
Java is always pass-by-value. For a primitive, the method gets a copy of the number. For an object, the method gets a copy of the reference, so both variables point to the same object. That is why a method can change an object's contents and the caller sees it, but if the method assigns a new object to its parameter, only its local copy changes. In the example, the list gains Pune, while the new list and the change to count are lost when the method returns. Saying that Java passes objects by reference is the mistake interviewers listen for.
static void update(List<String> cities, int count) {
cities.add("Pune"); // changes the shared list
cities = new ArrayList<>(); // only the local copy moves
cities.add("Delhi");
count = 99; // local copy of the int
}
List<String> list = new ArrayList<>(List.of("Mumbai"));
int n = 1;
update(list, n);
System.out.println(list + " " + n); // [Mumbai, Pune] 14. Why does == sometimes work for comparing strings, and why should you still use equals()?
== checks whether two references point to the same object, while equals() compares the characters. String literals with the same text are interned, so they share one pooled object and == happens to be true; constant expressions such as "chen" + "nai" are interned too. A string built at run time, read from input or created with new String is a separate object, so == is false even when the text matches. Such bugs pass tests that use literals and fail on real input. Use equals(), and write "YES".equals(answer) to stay safe when answer is null.
String a = "chennai";
String b = "chennai";
String c = new String("chennai");
String d = "chen" + "nai"; // constant, joined at compile time
System.out.println(a == b); // true: same pooled literal
System.out.println(a == c); // false: a new object
System.out.println(a.equals(c)); // true: same characters
System.out.println(a == d); // true: constant is interned5. Why should you use StringBuilder instead of + to build a string inside a loop?
String is immutable, so each + inside a loop creates a new String and copies every character built so far. For n pieces that is roughly n squared work plus a lot of short-lived garbage. StringBuilder keeps one growable buffer and appends in place, so the loop is linear. A single expression such as name + " - " + city is fine, because the compiler and JVM handle it efficiently. StringBuffer does the same job with synchronized methods and is rarely needed today. A common slip is creating the StringBuilder inside the loop, which removes the benefit.
6. What is encapsulation, and how does it protect an object's data?
Encapsulation means keeping an object's fields private and allowing changes only through methods that enforce the rules. A savings account must never go below zero, so its balance field is private and withdraw checks the amount before changing it. If the field were public, any class could set it to a negative number and the bug would be hard to trace. Encapsulation also lets you change the internal representation later, say from double to a long count of paise, without breaking callers. Generating a public setter for every field is the common mistake: it protects nothing.
7. What is the difference between compile-time and run-time polymorphism?
Compile-time polymorphism is method overloading: several methods share a name but differ in parameters, and the compiler picks one using the declared types of the arguments. Run-time polymorphism is method overriding: a subclass replaces an inherited method, and the JVM picks the version from the actual object's class while the program runs. In the example, p is declared as Payment but holds a UpiPayment, so pay() runs the UPI version, while print chooses the Payment overload. Expecting overloading to follow the real object is a classic trap.
class Payment {
void pay() { System.out.println("generic payment"); }
}
class UpiPayment extends Payment {
@Override
void pay() { System.out.println("paid by UPI"); }
}
class Receipt {
static void print(Payment p) { System.out.println("payment"); }
static void print(UpiPayment u) { System.out.println("upi"); }
}
Payment p = new UpiPayment();
p.pay(); // paid by UPI (overriding: run time)
Receipt.print(p); // payment (overloading: compile time)8. In Java 17, when would you choose an abstract class over an interface?
Use an interface to describe a capability that unrelated classes can share, such as Comparable or Runnable, because a class can implement many interfaces. Since Java 8 interfaces can have default and static methods, and since Java 9 private methods, but they still cannot have instance fields or constructors. Use an abstract class when subclasses share state and setup logic, such as a base Report with a title field and a template method that calls abstract steps. A class can extend only one class, so prefer interfaces and add an abstract base only when shared state is really needed.
9. What does the static keyword mean for fields, methods and blocks?
A static member belongs to the class, not to any object. A static field has one copy shared by all instances, such as a counter of employees created. A static method can be called without an object, like Math.max, but it cannot use this or read instance fields directly, because there is no object. A static block runs once, when the class is initialised. main is static so the JVM can call it without creating an object. A common mistake in web applications is keeping per-user data in a static field, where every request and every user shares it.
10. What does final mean on a variable, a method and a class?
A final variable can be assigned only once, a final method cannot be overridden, and a final class, such as String, cannot be extended. For a reference variable, final fixes the reference, not the object: a final list can still have elements added, it just cannot be pointed at another list. To fix the contents as well, use an unmodifiable list such as one from List.of. final also matters for lambdas, which can capture only local variables that are final or effectively final. Do not confuse final with finally, the block after try, or with the deprecated finalize method.
11. In what order do constructors run when you create an object of a subclass?
The superclass part is always built first. Every constructor begins by calling another constructor, either this(...) in the same class or super(...) in the parent, and if you write neither, the compiler inserts super() with no arguments. So creating a Manager runs Person's constructor, then Employee's, then Manager's. If a parent has no no-argument constructor, every subclass must call super with arguments. It also explains why calling an overridable method from a parent constructor is risky: it can run before the subclass fields are set.
class Person {
Person() { System.out.println("Person"); }
}
class Employee extends Person {
Employee() { System.out.println("Employee"); }
}
class Manager extends Employee {
Manager() {
this("Sales");
System.out.println("Manager");
}
Manager(String dept) {
System.out.println("Manager of " + dept);
}
}
new Manager();
// Person
// Employee
// Manager of Sales
// Manager12. What is the difference between HashSet, LinkedHashSet and TreeSet?
All three reject duplicates, but they differ in order and cost. HashSet makes no promise about iteration order and gives constant-time add and contains on average. LinkedHashSet keeps insertion order for a little extra memory. TreeSet keeps elements sorted by natural order or a Comparator, with log n operations and extra methods such as first, floor and headSet. TreeSet uses compareTo, not equals, to spot duplicates, so a case-insensitive comparator treats Pune and PUNE as one element. Never depend on HashSet order: it can change as the set grows or between Java versions.
List<String> visits = List.of("Pune", "Agra", "Kochi", "Agra");
Set<String> linked = new LinkedHashSet<>(visits);
Set<String> sorted = new TreeSet<>(visits);
System.out.println(linked); // [Pune, Agra, Kochi]
System.out.println(sorted); // [Agra, Kochi, Pune]13. How does ArrayList work internally, and when is a plain array a better choice?
ArrayList stores its elements in an internal Object array. get and set by index take constant time. add at the end is amortised constant time: when the array is full, a larger one is allocated and the elements are copied, and in OpenJDK the capacity grows by about half each time. Inserting or removing in the middle shifts the later elements, so it is linear. A plain array suits fixed-size data and primitives, because an ArrayList of Integer boxes every number and uses several times the memory of an int array. If you know the size, pass an initial capacity.
14. How is the Java exception hierarchy organised, and which exceptions must you declare or catch?
Everything that can be thrown extends Throwable. Error covers serious JVM problems such as OutOfMemoryError and StackOverflowError that applications should not try to handle, while Exception covers conditions a program can deal with. RuntimeException and its subclasses, such as NullPointerException and IllegalArgumentException, are unchecked, so the compiler does not force you to handle them. All other exceptions, such as IOException, are checked: a method must catch them or declare them with throws. Catching Throwable or Error in normal code is usually a mistake, because it hides failures.
15. Does a finally block run if the try block returns a value?
Yes. finally runs whether the try block completes normally, returns or throws, which is why it is used for cleanup. The return value is evaluated and saved first, then finally runs, then the method returns. In the example finally changes x, but the caller still gets 10, because that value was already saved. finally does not run if the JVM stops, for example through System.exit or a crash. Never put a return statement in finally: it silently replaces the original result and also swallows any exception thrown from the try block.
static int score() {
int x = 10;
try {
return x; // the value 10 is saved here
} finally {
x = 20; // does not change the result
System.out.println("finally ran");
}
}
System.out.println(score());
// finally ran
// 1016. What is a lambda expression, and why must the local variables it uses be effectively final?
A lambda is a compact way to implement a functional interface, an interface with exactly one abstract method, such as Comparator or Runnable. Instead of an anonymous class you write the parameters, an arrow and the body. A lambda may use local variables of the enclosing method only if they are final or effectively final, meaning never reassigned. The lambda captures the value, and it may run later or on another thread, so allowing changes would make that captured copy misleading. To accumulate a result, use a stream operation such as sum or count instead of a changing local counter.
List<String> names = new ArrayList<>(List.of("Kavya", "Om", "Isha"));
// Anonymous class (before Java 8)
names.sort(new Comparator<String>() {
@Override
public int compare(String a, String b) {
return Integer.compare(a.length(), b.length());
}
});
// The same with a lambda
names.sort((a, b) -> Integer.compare(a.length(), b.length()));
int minLength = 3; // effectively final
names.removeIf(s -> s.length() < minLength);
// Adding minLength = 4; anywhere in this method
// would make the lambda above a compile error.17. Why do we use generics, such as a list of String, instead of a raw List?
Generics let the compiler check types. With a raw List you can add any object, and the mistake appears later as a ClassCastException when you read and cast the value. With a typed list, adding an Integer to a list of String is a compile error, and get returns a String with no cast. Generics also document intent: a Map from employee id to Employee says exactly what it holds. Raw types exist only for compatibility with code written before Java 5. The compiler warns about them, and those unchecked warnings should be fixed, not suppressed.
List raw = new ArrayList(); // raw type: compiler warning
raw.add("Asha");
raw.add(42); // accepted
String s = (String) raw.get(1); // ClassCastException at run time
List<String> names = new ArrayList<>();
names.add("Asha");
// names.add(42); // compile error
String first = names.get(0); // no cast needed18. What is the difference between calling start() and run() on a Thread?
start() asks the JVM to create a new thread, which then calls run() on that thread, and the caller carries on immediately. Calling run() yourself is an ordinary method call: the code runs on the current thread and nothing happens in parallel. In the example, called from main, the direct call prints main and the started thread prints worker. Calling start() twice on the same Thread object throws IllegalThreadStateException. In real code you seldom create threads by hand; you submit tasks to an ExecutorService, which reuses a pool of threads.
Runnable task = () ->
System.out.println(Thread.currentThread().getName());
Thread t = new Thread(task, "worker");
t.run(); // prints main: runs on the calling thread
t.start(); // prints worker: runs on the new thread
t.join(); // wait for it to finish19. What is stored on the stack and what is stored on the heap in Java?
Each thread has its own stack, holding a frame for every active method call: local variables, parameters and bookkeeping. A primitive local holds its value in the frame, while an object local holds only a reference. The objects themselves, including arrays and strings, live on the heap, which all threads share and the garbage collector manages. A frame disappears as soon as its method returns; an object lives until nothing can reach it. Very deep recursion fills the stack and throws StackOverflowError, and too many live objects fill the heap and cause OutOfMemoryError.
20. What is a record in Java 17, and what does the compiler generate for it?
A record is a concise class for plain data, standard since Java 16. You list the components in the header, and the compiler generates private final fields, a canonical constructor, accessors named after the components (name(), not getName()), and equals, hashCode and toString based on all components. Records are implicitly final, cannot extend another class and cannot declare extra instance fields, but they can implement interfaces and add methods and static members. They suit DTOs, map keys and multi-value returns. Their fields are only shallowly immutable.
record Employee(int id, String name, String city) { }
Employee a = new Employee(101, "Priya", "Indore");
Employee b = new Employee(101, "Priya", "Indore");
System.out.println(a.name()); // Priya
System.out.println(a.equals(b)); // true: same components
System.out.println(a);
// Employee[id=101, name=Priya, city=Indore] (OpenJDK format)Ready to check yourself? Take the free Core Java mock test
Core Java interview questions: Medium questions (1-3 years)
20 questions21. Write equals() and hashCode() for an Employee class identified by employee id, and explain each step.
First check for the same reference as a fast path. Then reject null and other types; instanceof does both, because it is false for null, and it is safe here because the class is final. Then compare only the fields that define identity, here empId. hashCode must use exactly the fields that equals uses, so equal objects always get the same hash. If you override equals but not hashCode, two equal employees go to different HashMap buckets and a HashSet keeps both. If identity is all the fields, a Java 17 record generates both methods correctly.
final class Employee {
private final String empId;
private String name; // not part of identity
Employee(String empId, String name) {
this.empId = Objects.requireNonNull(empId);
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Employee other)) return false;
return empId.equals(other.empId);
}
@Override
public int hashCode() {
return empId.hashCode();
}
}22. What is the difference between Comparable and Comparator, and how do you sort by more than one field?
Comparable defines a class's natural order through its own compareTo method, such as String being alphabetical, and a class can have only one. Comparator is a separate object, so you can define as many orders as you need without touching the class. Since Java 8 you build comparators from key extractors with comparing, thenComparing and reversed. The example sorts by city, then by years of experience from highest to lowest, then by name. Avoid writing compare as a - b on ints, because the subtraction can overflow; use Integer.compare or comparingInt.
record Emp(String name, String city, int years) { }
List<Emp> team = new ArrayList<>(List.of(
new Emp("Ravi", "Pune", 3),
new Emp("Asha", "Delhi", 7),
new Emp("Neha", "Pune", 7)));
team.sort(Comparator.comparing(Emp::city)
.thenComparing(Comparator.comparingInt(Emp::years).reversed())
.thenComparing(Emp::name));
// Asha (Delhi, 7), Neha (Pune, 7), Ravi (Pune, 3)23. Why does removing elements inside a for-each loop throw ConcurrentModificationException, and how do you remove items safely?
The enhanced for loop uses the list's iterator. ArrayList keeps a modification count, and the iterator checks it on every step; if the list was changed structurally by anything other than the iterator itself, it fails fast with ConcurrentModificationException. Despite the name, this usually happens in a single thread. Remove through the iterator with Iterator.remove or, more simply, use removeIf, which does the job in one pass. Fail-fast checking is only best effort, so never write code that depends on the exception being thrown.
List<String> leaves = new ArrayList<>(
List.of("approved", "pending", "rejected", "pending"));
// Broken: throws ConcurrentModificationException
for (String s : leaves) {
if (s.equals("pending")) {
leaves.remove(s);
}
}
// Fix 1: remove through the iterator
Iterator<String> it = leaves.iterator();
while (it.hasNext()) {
if (it.next().equals("pending")) {
it.remove();
}
}
// Fix 2 (Java 8+): one line
leaves.removeIf(s -> s.equals("pending"));24. What is the difference between Arrays.asList, List.of and new ArrayList, and which errors does each one cause?
new ArrayList gives a normal, fully modifiable list. Arrays.asList returns a fixed-size list backed by the array: set works and writes through to the array, but add and remove throw UnsupportedOperationException. List.of, added in Java 9, returns an unmodifiable list: add, remove and set all throw UnsupportedOperationException, and it rejects null elements with a NullPointerException. Use List.of for constants, and copy into a new ArrayList when you need to change the list. A method that receives a List cannot tell which kind it got, so document whether you modify it.
25. What do ? extends and ? super mean in generics, and when do you use each?
A list of Integer is not a list of Number, even though Integer is a Number, so wildcards add the flexibility. ? extends Number means some unknown subtype of Number: you can read elements as Number, but you cannot add anything except null, because the real list might hold Doubles. ? super Integer means Integer or one of its supertypes: you can add Integers, but reading gives only Object. The rule is producer extends, consumer super. Collections.copy uses both, and a total method should take ? extends Number so it accepts lists of Integer and of Double.
static double total(List<? extends Number> amounts) {
double sum = 0;
for (Number n : amounts) { // reading is safe
sum += n.doubleValue();
}
// amounts.add(5); // compile error
return sum;
}
static void addDefaults(List<? super Integer> target) {
target.add(0); // adding Integers is safe
target.add(100);
}
total(List.of(10, 20)); // a List<Integer>
total(List.of(2.5, 4.0)); // a List<Double>
addDefaults(new ArrayList<Number>());26. What is type erasure, and what can you not do with generics because of it?
The compiler checks generic types and then mostly erases them: in bytecode a list of String and a list of Integer are the same List class, with casts inserted where values are read. So at run time you cannot test whether an Object is a list of String, cannot write new T() or new T[] in a generic class, and cannot overload two methods that differ only in their type arguments, because they erase to the same signature. The usual workaround is to pass a Class object or a factory such as a Supplier. Erasure let old non-generic code keep working when Java 5 added generics.
class Repo<T> {
private final Supplier<T> factory;
Repo(Supplier<T> factory) {
this.factory = factory;
}
T create() {
// return new T(); // compile error: T is erased
return factory.get();
}
}
Repo<StringBuilder> r = new Repo<>(StringBuilder::new);
// Does not compile: both erase to process(List)
// void process(List<String> a) { }
// void process(List<Integer> b) { }
// With Object obj:
// obj instanceof List<String> compile error
// obj instanceof List<?> fine27. How do you design a custom exception, and why should you keep the original cause?
Extend RuntimeException for failures callers cannot reasonably fix, and extend Exception only when the caller is expected to recover, for example by retrying or asking for other input. Provide constructors that take a message and a cause, and pass both to super. Wrapping a low-level exception with its cause keeps the full stack trace, with a Caused by section that shows the real failure, such as the SQLException behind a PayrollException. The common mistakes are throwing a new exception without the cause, and catching an exception only to print it and carry on.
public class PayrollException extends RuntimeException {
public PayrollException(String message) {
super(message);
}
public PayrollException(String message, Throwable cause) {
super(message, cause);
}
}
try {
payslipDao.save(slip);
} catch (SQLException e) {
// keep e as the cause, or the root error is lost
throw new PayrollException("Save failed: " + slip.id(), e);
}28. In try-with-resources, in what order are resources closed, and what happens to an exception thrown by close()?
Resources are closed automatically in the reverse order of declaration, as soon as the try block ends and before any catch or finally block runs. Any class that implements AutoCloseable can be used. If the try block throws and close also throws, the exception from the body is the one that propagates, and the close exception is attached to it as a suppressed exception, readable with getSuppressed. With the old try-finally pattern, an exception from close in finally would replace the real error. Since Java 9 you can also list an existing effectively final variable.
class Res implements AutoCloseable {
private final String name;
Res(String name) { this.name = name; }
@Override
public void close() {
System.out.println("close " + name);
}
}
try (Res db = new Res("db"); Res file = new Res("file")) {
System.out.println("work");
} finally {
System.out.println("finally");
}
// work
// close file
// close db
// finally29. Why does Collectors.toMap throw IllegalStateException, and how do you fix it?
The two-argument toMap throws IllegalStateException with a Duplicate key message as soon as two elements produce the same key, because it cannot know which value to keep. Pass a merge function as the third argument to decide: keep the first, keep the latest or combine them. A fourth argument, such as TreeMap::new, chooses the map type. If you want every value for a key, use groupingBy instead. Also note that toMap throws NullPointerException when a value is null, so filter or replace nulls first. This bug often appears only in production, when real data has duplicates.
record Emp(String id, String city) { }
List<Emp> staff = List.of(
new Emp("E1", "Pune"),
new Emp("E2", "Pune"),
new Emp("E3", "Jaipur"));
// IllegalStateException: Duplicate key Pune ...
Map<String, String> bad = staff.stream()
.collect(Collectors.toMap(Emp::city, Emp::id));
// Combine ids per city, in a sorted map
Map<String, String> ok = staff.stream()
.collect(Collectors.toMap(Emp::city, Emp::id,
(a, b) -> a + "," + b, TreeMap::new));
// {Jaipur=E3, Pune=E1,E2}30. Streams are lazy. What does that mean, and what does this code print?
Intermediate operations such as filter and map only describe the pipeline; nothing runs until a terminal operation such as findFirst, collect or forEach starts pulling elements. Elements then flow through the whole pipeline one at a time, not stage by stage. Here findFirst needs only the first even number, so 3 and 4 are never examined and map runs once. That is why short-circuiting operations are cheap on large data. Two related rules: a stream can be consumed only once, and a second terminal operation throws IllegalStateException; and side effects inside filter or map are best avoided.
Optional<Integer> first = Stream.of(1, 2, 3, 4)
.filter(n -> {
System.out.println("filter " + n);
return n % 2 == 0;
})
.map(n -> {
System.out.println("map " + n);
return n * 10;
})
.findFirst();
System.out.println(first.get());
// filter 1
// filter 2
// map 2
// 2031. Two threads each increment a shared counter 10,000 times, but the total is often less than 20,000. Why, and how do you fix it?
count++ is not one step. It reads the value, adds one and writes it back, so two threads can read the same value and both write the same result, losing an update. This is a race condition. Make the increment atomic: use an AtomicInteger with incrementAndGet, which relies on atomic CPU instructions instead of a lock, or do the update inside a synchronized block so only one thread runs it at a time. For a heavily contended counter, LongAdder scales better. Marking the field volatile does not help: it makes writes visible but does not make read-modify-write atomic.
class Counter {
private int unsafe = 0;
private final AtomicInteger safe = new AtomicInteger();
void hit() {
unsafe++; // read, add, write: can lose updates
safe.incrementAndGet(); // atomic
}
}
Counter c = new Counter();
Runnable work = () -> {
for (int i = 0; i < 10_000; i++) c.hit();
};
Thread t1 = new Thread(work), t2 = new Thread(work);
t1.start(); t2.start();
t1.join(); t2.join();
// safe is always 20000; unsafe can be less32. Why use an ExecutorService instead of creating threads yourself, and how do you shut it down correctly?
Creating a thread per task is costly and unbounded: a spike in requests can create thousands of threads and exhaust memory. An ExecutorService keeps a pool of reusable threads and a queue of tasks, and submit returns a Future that carries the result or the exception. For CPU-bound work, size a fixed pool near the number of cores. Always shut the pool down, because its threads otherwise keep the JVM alive: call shutdown, wait with awaitTermination, then shutdownNow if it takes too long. Since Java 19, ExecutorService is AutoCloseable: close shuts it down and waits for submitted tasks.
33. How would you build a producer-consumer pipeline in Java, for example payslip generation feeding an email sender?
Use a BlockingQueue instead of hand-written wait and notify. put blocks when a bounded queue is full and take blocks when it is empty, so a fast producer cannot exhaust memory and the consumer waits without busy looping. An ArrayBlockingQueue with a fixed capacity gives natural back-pressure. To stop the consumer cleanly, send a special poison-pill item after the last real one, or use an executor and shut it down. If you do use wait and notify, always call wait inside a while loop that rechecks the condition, and only while holding the lock.
34. Why does "a.b.c".split(".") return an empty array, and what other split() surprises should you know?
split takes a regular expression, not plain text, and a dot in a regex matches any character. Every character becomes a separator, so all the pieces are empty, and split with no limit removes trailing empty strings, leaving nothing. Escape it as "\\." in Java source or use Pattern.quote. The same trailing rule means "Ravi,Pune,,".split(",") gives two elements; pass a limit of -1 to keep the empty ones, which matters when parsing CSV rows with blank last columns. Characters such as |, +, * and $ also need escaping.
String path = "a.b.c";
System.out.println(path.split(".").length); // 0
System.out.println(path.split("\\.").length); // 3
String row = "Ravi,Pune,,";
System.out.println(row.split(",").length); // 2
System.out.println(row.split(",", -1).length); // 4
String ver = "21|LTS";
String[] parts = ver.split(Pattern.quote("|")); // [21, LTS]35. Why should you not use double for money, and what are the traps with BigDecimal?
double is binary floating point and cannot store most decimal fractions exactly, so 0.1 + 0.2 prints 0.30000000000000004, and small errors add up over thousands of payroll or GST lines. Use BigDecimal, or a long count of paise. Create BigDecimal from a String or with BigDecimal.valueOf, not with new BigDecimal(0.1), which keeps the binary error. When dividing, give a scale and a RoundingMode, or a result such as 1/3 throws ArithmeticException. Finally, equals also compares scale, so 2.0 and 2.00 are not equal; use compareTo for amounts.
System.out.println(0.1 + 0.2); // 0.30000000000000004
BigDecimal a = new BigDecimal("0.1");
System.out.println(a.add(new BigDecimal("0.2"))); // 0.3
System.out.println(new BigDecimal(0.1));
// 0.1000000000000000055511151231257827021181583404541015625
BigDecimal fee = new BigDecimal("1000");
BigDecimal share =
fee.divide(new BigDecimal("3"), 2, RoundingMode.HALF_UP);
System.out.println(share); // 333.33
BigDecimal x = new BigDecimal("2.0");
BigDecimal y = new BigDecimal("2.00");
System.out.println(x.equals(y)); // false
System.out.println(x.compareTo(y)); // 036. Java has a garbage collector, so how can a Java application still leak memory?
The garbage collector frees only objects that are unreachable. A Java leak is an object that is still referenced but no longer needed, so it can never be collected. Common causes are static maps used as caches with no size limit or expiry, listeners registered and never removed, ThreadLocal values left on pooled threads, and queues or sessions that only grow. The symptom is heap use after each full GC climbing until OutOfMemoryError. Take a heap dump with jcmd, or with -XX:+HeapDumpOnOutOfMemoryError, and find what holds the largest retained size in a tool such as Eclipse MAT.
37. How do you validate data in a record, and why is a record with a List field not truly immutable?
Use a compact constructor: it has no parameter list, runs before the fields are assigned, and can check or normalise the parameters. Throw an exception for invalid values so a bad object can never exist. A record's fields are final, but final only fixes the reference. If a component is a mutable List, the caller who passed it can still change it, and so can anyone who calls the accessor. Make a defensive copy with List.copyOf in the compact constructor: it is unmodifiable and rejects nulls. The same care applies to arrays and other mutable components.
record Team(String name, List<String> members) {
Team {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("name required");
}
name = name.strip();
members = List.copyOf(members); // defensive copy
}
}
List<String> list = new ArrayList<>(List.of("Anil"));
Team t = new Team(" QA ", list);
list.add("Zoya"); // does not affect t
System.out.println(t.members()); // [Anil]
System.out.println(t.name()); // QA
// t.members().add("X") throws UnsupportedOperationException38. In Java 21, how do pattern matching for instanceof and record patterns remove casts?
Pattern matching for instanceof, standard since Java 16, tests the type and binds a typed variable in one step, so the separate cast disappears and cannot go wrong. Record patterns, standard in Java 21, go further: they test the type and extract the components, and they can be nested. In a switch, case labels can use type and record patterns with a when guard, and case null can be handled explicitly instead of throwing NullPointerException. A binding variable is in scope only where the match is certain, so using it in the else branch is a compile error.
record Circle(double r) { }
record Rect(double w, double h) { }
// Java 16+: type pattern, no cast
if (obj instanceof String s && !s.isBlank()) {
System.out.println(s.length());
}
// Java 21: record patterns and guards in switch
static String describe(Object o) {
return switch (o) {
case null -> "nothing";
case Circle(double r) when r == 0 -> "a point";
case Circle(double r) -> "circle, radius " + r;
case Rect(double w, double h) -> "area " + (w * h);
default -> "unknown";
};
}39. What happens when a class implements two interfaces that have the same default method?
The class does not compile until you resolve the conflict. Java applies three rules. A method declared in the class or its superclasses always wins over interface defaults. Otherwise the most specific interface wins, so a sub-interface overrides its parent. If two unrelated interfaces both supply a default, the class must override the method itself, and inside the override it can call either version with the Interface.super.method() syntax. This is why adding a default method to a published interface can break existing classes that already inherit the same method from another interface.
interface Camera {
default String start() { return "camera on"; }
}
interface Gps {
default String start() { return "gps on"; }
}
class Phone implements Camera, Gps {
@Override
public String start() { // required, or compile error
return Camera.super.start() + ", " + Gps.super.start();
}
}
System.out.println(new Phone().start()); // camera on, gps on40. What are the four kinds of method references, and what lambda does each one replace?
A method reference is a shorter lambda that only calls an existing method. A static method reference, Integer::parseInt, replaces s -> Integer.parseInt(s). A bound reference to a particular object, System.out::println, replaces x -> System.out.println(x), and the object is fixed when the reference is created. An unbound reference to an instance method, String::toUpperCase, uses the first parameter as the receiver: s -> s.toUpperCase(). A constructor reference, StringBuilder::new, replaces () -> new StringBuilder(). Prefer a lambda when the reference would hide extra logic.
Ready to check yourself? Take the free Core Java mock test
Core Java interview questions: Hard questions (experienced)
20 questions41. Why does HashMap keep its table size a power of two, and what does its hash-spreading step do?
With a power-of-two table, the bucket index is hash AND (n - 1), a cheap bit mask instead of a modulo. The catch is that only the low bits pick the bucket, so keys whose hash codes differ only in high bits would collide. HashMap therefore XORs the hash code with itself shifted right by 16 bits. When the size passes capacity times the 0.75 load factor, the table doubles and each bucket splits in two. Since Java 8, a bucket that grows past 8 entries becomes a red-black tree once the table has at least 64 slots, so heavy collisions on Comparable keys cost log n, not n.
// From OpenJDK's HashMap (simplified)
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// Bucket index for a table of length n (a power of two)
int index = (n - 1) & hash;42. What is the difference between fail-fast and weakly consistent iterators, and what does CopyOnWriteArrayList cost you?
Iterators of ArrayList and HashMap are fail-fast: on a best-effort basis they throw ConcurrentModificationException if the collection changes structurally during iteration, which exposes bugs early. Iterators of java.util.concurrent collections such as ConcurrentHashMap are weakly consistent: they never throw it, visit each element present at creation exactly once, and may or may not show later changes. CopyOnWriteArrayList iterates over a snapshot, so reads need no locks, but every add or set copies the whole array. It suits small, read-mostly lists such as event listeners.
43. What is the difference between Collections.unmodifiableList and List.copyOf, and why does it matter for a class that returns its internal list?
Collections.unmodifiableList returns a read-only view of the original list. Callers cannot change it through the view, but every change to the underlying list shows through, so a caller can see data change under them, or hit ConcurrentModificationException while iterating. List.copyOf, from Java 10, makes an independent unmodifiable copy, and generally skips the copy if the list is already unmodifiable from List.of or copyOf. A view is cheap and stays live; a copy is a stable snapshot that is safe to hand to other threads. Neither makes the elements themselves immutable.
List<String> internal = new ArrayList<>(List.of("Pune"));
List<String> view = Collections.unmodifiableList(internal);
List<String> copy = List.copyOf(internal);
internal.add("Nagpur");
System.out.println(view); // [Pune, Nagpur]: the view is live
System.out.println(copy); // [Pune]: the copy is fixed
// view.add("Goa") and copy.add("Goa") both throw
// UnsupportedOperationException44. Why is it hard to write equals() for a class hierarchy, and should you use instanceof or getClass()?
equals must be symmetric and transitive, and a subclass that adds a field breaks this when instanceof is used. If Point uses instanceof and ColorPoint adds a color, point.equals(colorPoint) is true but colorPoint.equals(point) is false. getClass() keeps symmetry by making objects of different classes never equal, but it breaks substitutability: a harmless subclass that only adds logging can never equal its parent. The cleanest options are to make value classes final, use records, or prefer composition, so a ColorPoint holds a Point instead of extending it.
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;
}
@Override public int hashCode() { return 31 * x + y; }
}
class ColorPoint extends Point {
final String color;
ColorPoint(int x, int y, String c) { super(x, y); color = c; }
@Override public boolean equals(Object o) {
return o instanceof ColorPoint cp
&& super.equals(cp) && cp.color.equals(color);
}
}
Point p = new Point(1, 2);
ColorPoint cp = new ColorPoint(1, 2, "red");
System.out.println(p.equals(cp)); // true
System.out.println(cp.equals(p)); // false: not symmetric45. Two records hold arrays with the same contents. Why are they not equal, and how do you fix it?
A record's generated equals compares reference components with Objects.equals, and for an array that means reference equality, because arrays do not override equals. So two records holding different array objects with the same numbers are not equal, their hash codes usually differ, and toString shows only a type code and a hash such as [I@ followed by hex digits. Fix it by storing an unmodifiable List, which compares contents, or override equals, hashCode and toString using Arrays.equals, Arrays.hashCode and Arrays.toString, and copy the array on the way in and out.
record Marks(String roll, int[] scores) { }
Marks a = new Marks("R1", new int[] {90, 85});
Marks b = new Marks("R1", new int[] {90, 85});
System.out.println(a.equals(b)); // false: arrays by reference
record Marks2(String roll, List<Integer> scores) {
Marks2 { scores = List.copyOf(scores); }
}
Marks2 c = new Marks2("R1", List.of(90, 85));
Marks2 d = new Marks2("R1", List.of(90, 85));
System.out.println(c.equals(d)); // true: List compares contents46. Why can a loop that waits on a plain boolean flag run forever, and what does happens-before mean?
Without synchronization, the Java Memory Model does not guarantee that one thread ever sees another thread's write. The JIT may read a non-volatile field once and keep it in a register, turning while (!stop) into an endless loop. Happens-before is the rule that makes a write visible: unlocking a monitor happens-before a later lock of it, a volatile write happens-before later reads of that field, and Thread.start and join create edges too. Declaring the flag volatile fixes this loop, but it does not make compound actions such as count++ atomic.
class Worker implements Runnable {
// Without volatile, run() may never see stop = true
private volatile boolean stop;
@Override
public void run() {
long n = 0;
while (!stop) {
n++;
}
System.out.println("stopped after " + n);
}
void shutdown() { stop = true; }
}47. Your code uses a ConcurrentHashMap but still loses updates. What is wrong, and how do merge and computeIfAbsent help?
Each single ConcurrentHashMap call is thread-safe, but a sequence of calls is not. A get followed by a put is check-then-act: two threads can both read 5 and both write 6. Use the atomic methods. merge adds or combines in one step, computeIfAbsent creates a value at most once per key, and compute updates under the bin's lock. Keep those functions short, because other updates to the same bin wait, and never modify the map from inside them. Note that the add on the list returned by computeIfAbsent runs outside the map's lock, so that list must be thread-safe too.
ConcurrentHashMap<String, Integer> hits = new ConcurrentHashMap<>();
// Broken: two threads can read the same old value
Integer old = hits.get("pune");
hits.put("pune", old == null ? 1 : old + 1);
// Atomic: one step
hits.merge("pune", 1, Integer::sum);
ConcurrentHashMap<String, List<String>> byCity =
new ConcurrentHashMap<>();
byCity.computeIfAbsent("pune", k -> new CopyOnWriteArrayList<>())
.add("E101");48. In Java 21, when do virtual threads help, and when do they not?
A virtual thread is a lightweight thread scheduled by the JVM. When it blocks on I/O, the JVM unmounts it from its carrier platform thread, which then runs another virtual thread, so you can have hundreds of thousands of them. They help thread-per-request code that mostly waits on databases or HTTP calls. They do not speed up CPU-bound work, because there are still only about as many carriers as cores. Do not pool them; create one per task. In Java 21, blocking inside synchronized pins the carrier, so prefer ReentrantLock there; JDK 24 removed most of that pinning.
// Java 21: one virtual thread per task
try (var ex = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 10_000; i++) {
int id = i;
ex.submit(() -> fetchStatus(id)); // blocking I/O call
}
} // close() waits for all tasks to finish
Thread.ofVirtual().name("report").start(() -> buildReport());49. When would you use ReentrantLock instead of synchronized?
synchronized is simpler and the JVM always releases the lock, even when an exception is thrown, so it is the default choice. ReentrantLock covers what synchronized cannot: tryLock to give up instead of waiting, tryLock with a timeout to avoid deadlocks, lockInterruptibly so a waiting thread can be cancelled, an optional fairness policy, and several Condition objects for different wait reasons. The price is that you must call unlock in a finally block yourself; forget it and the lock stays held. For data read far more than written, consider ReentrantReadWriteLock.
private final ReentrantLock lock = new ReentrantLock();
boolean transfer(Account from, Account to, long paise)
throws InterruptedException {
if (!lock.tryLock(500, TimeUnit.MILLISECONDS)) {
return false; // busy: let the caller retry
}
try {
from.debit(paise);
to.credit(paise);
return true;
} finally {
lock.unlock(); // always release
}
}50. How do you choose and tune a garbage collector for a Java 21 service?
Start from the goal. G1, the default on server machines, balances throughput and pause time and suits most services; set a pause target with -XX:MaxGCPauseMillis rather than many low-level flags. ZGC keeps pauses around a millisecond even on very large heaps, for latency-sensitive APIs; in Java 21 enable its generational mode with -XX:+UseZGC -XX:+ZGenerational. Parallel GC gives the best throughput for batch jobs that accept longer pauses. Enable GC logs with -Xlog:gc* and measure before and after. Many GC problems are really allocation problems.
51. Which memory areas does the JVM use, and what does each kind of OutOfMemoryError tell you?
The heap holds objects and is shared by all threads. Metaspace, which replaced PermGen in Java 8, holds class metadata in native memory. Each thread has its own stack, and the JVM also uses a code cache for JIT-compiled code and direct memory for NIO buffers. The error message names the problem: Java heap space means live objects exceed -Xmx; Metaspace usually means a class loader leak or endless generated classes; unable to create native thread means too many threads for the OS or container; Direct buffer memory means off-heap buffers are not freed.
52. How does class loading work, and why can a ClassCastException say a class cannot be cast to a class with the same name?
Classes are loaded lazily, when first used. Since Java 9 there are three built-in loaders: bootstrap for core classes such as java.lang.String, platform for other JDK modules such as java.sql, and the application loader for your class path. A loader normally delegates to its parent first, which stops application code from replacing core classes. At run time a class is identified by its name plus the loader that defined it, so one class file loaded by two loaders gives two different classes. That is how plugin systems end up with a ClassCastException between identical-looking classes.
53. How does the JIT compiler affect performance, and why are hand-written timing loops misleading?
HotSpot first interprets bytecode, then tiered compilation compiles hot methods: first with C1, quickly and with profiling, then with C2, which uses that profile to inline calls, remove dead code, unroll loops and avoid allocating objects that never escape a method. If a later call breaks an assumption, the code is deoptimised and compiled again. So a loop timed with System.nanoTime mixes interpretation and compilation, and the JIT may delete work whose result is never used. Use JMH, which handles warm-up, forks and dead-code elimination, with production-like data.
54. finalize() is deprecated for removal. What should you use instead to release native resources?
Finalization is unreliable: there is no guarantee when, or even whether, finalize runs, it slows garbage collection, and a finalizer can resurrect the object. It was deprecated in Java 9 and marked for removal in Java 18. The main answer is explicit cleanup: implement AutoCloseable and use try-with-resources. As a safety net for callers who forget, register an action with java.lang.ref.Cleaner. The action must not refer to the object being cleaned, or that object never becomes unreachable, so keep the state in a static nested class.
public final class NativeBuffer implements AutoCloseable {
private static final Cleaner CLEANER = Cleaner.create();
// Holds only what cleanup needs; no reference to NativeBuffer
private static final class State implements Runnable {
private final long address;
State(long address) { this.address = address; }
@Override public void run() { freeNative(address); }
}
private final Cleaner.Cleanable cleanable;
public NativeBuffer(long size) {
// allocate and freeNative stand for your native calls
cleanable = CLEANER.register(this, new State(allocate(size)));
}
@Override
public void close() {
cleanable.clean(); // runs State.run at most once
}
}55. What changed inside String in Java 9, and how does it affect memory?
Before Java 9 a String stored its text in a char array, two bytes per character. Java 9 introduced compact strings: the text is a byte array plus a coder flag, and strings that fit in Latin-1 use one byte per character, which roughly halves the memory for typical English text, IDs and JSON keys. Strings with other characters, such as Hindi text, still use two bytes per character. Java 9 also compiles string concatenation with invokedynamic, so the JVM picks the best strategy at run time. With G1, -XX:+UseStringDeduplication can share the arrays of equal strings.
56. Why are arrays covariant but generic lists invariant, and what is heap pollution?
String[] is a subtype of Object[], so a String array can be assigned to an Object array variable, but storing an Integer through it throws ArrayStoreException, because arrays know their element type at run time. Generics are erased and cannot check at run time, so the compiler makes a list of String not a list of Object and stops the error at compile time. That is also why new T[] and arrays of generic types are not allowed. Heap pollution is a variable of a generic type referring to an object of the wrong type, usually through raw types or generic varargs.
Object[] arr = new String[2];
arr[0] = "Mumbai"; // fine
arr[1] = 42; // ArrayStoreException at run time
List<String> names = new ArrayList<>();
// List<Object> objs = names; // compile error: invariant
List<?> any = names; // fine: unknown element type
// List<String>[] bad = new List<String>[3]; // compile error57. How do sealed interfaces and exhaustive switch in Java 21 help when a new case is added?
A sealed interface lists exactly which classes may implement it. With records as the implementations, a switch over the interface can cover every permitted type without a default, and the compiler checks that it is exhaustive. When someone later adds a type, such as a Wallet payment, every switch without a default stops compiling until it handles the new case, so nothing is missed silently. A default branch throws that check away, so avoid it for sealed types. This gives the safety of the visitor pattern with much less code.
sealed interface Payment permits Upi, Card, NetBanking { }
record Upi(String vpa) implements Payment { }
record Card(String last4) implements Payment { }
record NetBanking(String bank) implements Payment { }
static String label(Payment p) {
return switch (p) { // no default needed
case Upi u -> "UPI " + u.vpa();
case Card c -> "Card ending " + c.last4();
case NetBanking n -> "Net banking, " + n.bank();
};
}58. Why are exceptions expensive, and how should a large codebase use them?
Most of the cost is filling in the stack trace when the exception is created, which walks the call stack; the throw and catch are cheaper. So do not use exceptions for normal control flow, such as parsing input in a hot loop where most values are bad; validate first or return Optional. Translate exceptions at layer boundaries, so a service throws its own exception with the low-level one as the cause. For frequent, expected rejections, Throwable has a protected constructor that turns off the stack trace. Log an exception once, where it is handled, not at every layer.
// Expected business rejection: cheap, no stack trace
class LimitExceeded extends RuntimeException {
LimitExceeded(String msg) {
super(msg, null, false, false);
}
}
// Translate at the boundary, keeping the cause
try {
return repo.load(id);
} catch (SQLException e) {
throw new DataAccessFailure("load " + id, e);
}59. Why must the function passed to Stream.reduce be associative, and what goes wrong with subtraction?
reduce with an identity may group the elements in any way, and on a parallel stream each chunk is reduced separately before the partial results are combined. The answer is right only if the operation is associative and the identity is truly neutral, like 0 for addition or 1 for multiplication. Subtraction is not associative, so the sequential result of -10 can differ in parallel. A wrong identity also breaks: reduce(10, Integer::sum) can add 10 once per chunk. For sums use sum or Collectors.summingInt, and to build collections use collect, not reduce.
List<Integer> nums = List.of(1, 2, 3, 4);
int seq = nums.stream().reduce(0, (a, b) -> a - b);
System.out.println(seq); // -10 (left to right)
int par = nums.parallelStream().reduce(0, (a, b) -> a - b);
// depends on how the list is split: do not rely on it
int total = nums.parallelStream().reduce(0, Integer::sum);
System.out.println(total); // 10: associative, so correct60. What does the Java module system add, and why does reflection into JDK internals fail on Java 17?
A module, declared in module-info.java, names its dependencies with requires and lists the packages it makes public with exports. Packages that are not exported cannot be used by other modules even if their classes are public, which is strong encapsulation. Since Java 17 the JDK's internals are strongly encapsulated with no global switch to relax them, so deep reflection such as setAccessible on a private field of a JDK class throws InaccessibleObjectException. Use supported APIs, or grant access for one package with --add-opens. Code on the class path runs in the unnamed module.
// module-info.java
module com.example.payroll {
requires java.sql;
exports com.example.payroll.api; // public to other modules
opens com.example.payroll.model; // reflection only
}
// Java 17+: open one JDK package to class-path code
// java --add-opens java.base/java.lang=ALL-UNNAMED -jar app.jarPractice questions for November 2026
New every month3 questions from the Core Java mock test, the way they appear in it. A new set comes on the 1st of every month. Pick your answer, then open it.
-
What does this code print?
int day = 6; String type = switch (day) { case 1, 7 -> "Holiday"; case 2, 3, 4, 5, 6 -> "Working day"; default -> "Invalid"; }; System.out.println(type);- Compile error
- Holiday
- Invalid
- Working day
Show the answer
Answer: Working day. Arrow cases in a switch expression may list several labels and never fall through, so only the matching branch runs. 6 matches the second case. The whole switch is an expression, so its value is assigned straight to type.
-
What happens at the third line?
Stream<String> s = Stream.of("a", "b"); long n = s.count(); List<String> list = s.toList();- IllegalStateException: the stream has already been operated upon or closed
- list is empty, because count() has already consumed every element of the stream
- list contains a and b, because each operation restarts the stream
- It does not compile, because a Stream variable cannot be used twice
Show the answer
Answer: IllegalStateException: the stream has already been operated upon or closed. A stream can be consumed only once, and count() is a terminal operation that uses it up. Any further operation throws IllegalStateException. Call stream() again on the source, or use a Supplier<Stream<T>> to create fresh streams.
-
In Java 21, a virtual thread blocks on a socket read while inside a synchronized block. What happens?
- The virtual thread unmounts as usual and frees the carrier
- The synchronized block is skipped
- The JVM throws IllegalMonitorStateException
- The virtual thread is pinned to its carrier platform thread, which stays blocked as well
Show the answer
Answer: The virtual thread is pinned to its carrier platform thread, which stays blocked as well. In Java 21 a virtual thread cannot unmount while it holds a monitor, so a long blocking call inside synchronized occupies a carrier thread and limits scalability. ReentrantLock avoids this pinning; later releases removed the limit for synchronized.
Ready to apply?
Browse the open jobs on CoreHRx. Create a free profile once and apply with it.