Skip to content

Java Programming Practice Test — 30 Problems

Java Programming Practice Test — 30 Problems

Section titled “Java Programming Practice Test — 30 Problems”

This practice test covers 30 problems across five major domains of Java programming: Object-Oriented Programming, Collections Framework, Concurrency, Streams API, and Exception Handling. Each problem tests code analysis, debugging, and understanding of Java semantics. Work through all problems before checking the answer key.

  • Time limit: 90 minutes (3 minutes per problem)
  • Format: Code analysis and debugging — trace the output, identify errors, or select the correct implementation
  • Marking: 1 mark per problem, 30 marks total
  • Conditions: Attempt without notes. Trace code by hand.
  • After the test: Check the answer key at the bottom. Study the explanations for any problems you got wrong.
DomainProblemsMarks
Object-Oriented ProgrammingP1–P77
Collections FrameworkP8–P147
ConcurrencyP15–P206
Streams APIP21–P266
Exception HandlingP27–P304
Total3030

What is the output of the following code?

class Animal {
void speak() { System.out.print("Animal "); }
}
class Dog extends Animal {
void speak() { System.out.print("Dog "); }
}
class Puppy extends Dog {
void speak() { System.out.print("Puppy "); }
}
public class Main {
public static void main(String[] args) {
Animal a = new Puppy();
a.speak();
((Dog) a).speak();
}
}
#Option
APuppy Puppy
BDog Puppy
CAnimal Dog
DPuppy Dog
EClassCastException

Correct: A (index 0)

The variable a is declared as Animal but references a Puppy instance. a.speak() uses dynamic dispatch — it calls Puppy.speak() (outputs “Puppy ”). The cast (Dog) a succeeds because Puppy is-a Dog. (Dog) a).speak() also uses dynamic dispatch on the same Puppy object, calling Puppy.speak() again. Both calls output “Puppy”.

medium — 1 mark


What is the output?

interface Greetable {
default void greet() { System.out.print("Hello "); }
}
interface Formal {
default void greet() { System.out.print("Dear "); }
}
class Diplomat implements Greetable, Formal {
public void greet() {
Greetable.super.greet();
System.out.print("colleague");
}
}
public class Main {
public static void main(String[] args) {
new Diplomat().greet();
}
}
#Option
AHello colleague
BDear colleague
CCompiler error — ambiguous default method
DHello Dear colleague
EClassCastException

Correct: A (index 0)

When a class implements two interfaces with the same default method, the compiler requires the class to override the method and explicitly choose which interface’s version to call. Diplomat overrides greet(), calls Greetable.super.greet() (outputs “Hello ”), then prints “colleague”.

medium — 1 mark


What is the output?

record Point(int x, int y) {
Point { // compact constructor
if (x < 0 || y < 0) throw new IllegalArgumentException("Negative");
}
}
public class Main {
public static void main(String[] args) {
Point p1 = new Point(3, 4);
Point p2 = new Point(3, 4);
System.out.print(p1 == p2 + " ");
System.out.print(p1.equals(p2) + " ");
System.out.println(p1.x() + p2.y());
}
}
#Option
Atrue true 7
Bfalse true 7
Cfalse false 7
Dtrue false 7
ECompiler error

Correct: B (index 1)

Records generate equals() based on component values, so p1.equals(p2) is true. However, == compares references — p1 and p2 are different objects, so p1 == p2 is false. p1.x() returns 3, p2.y() returns 4, sum is 7.

easy — 1 mark


Which statement about sealed classes in Java 17+ is correct?

#Option
ASealed classes can only be extended by classes in the same package
BPermitted subclasses must be final, sealed, or non-sealed
CSealed classes cannot implement interfaces
DSealed classes replace abstract classes entirely
EPermitted subclasses can be in any module

Correct: B (index 1)

A sealed class restricts which classes may extend it by listing permits in the class declaration. Each permitted subclass must be declared final (no further extension), sealed (further restricted), or non-sealed (opens the hierarchy back up). Permitted subclasses can be in different packages if they are in the same module.

medium — 1 mark


What happens when you use a custom class as a HashMap key without overriding equals and hashCode?

class FileKey {
String path;
FileKey(String path) { this.path = path; }
}
public class Main {
public static void main(String[] args) {
java.util.Map<FileKey, String> map = new java.util.HashMap<>();
FileKey k1 = new FileKey("/tmp/a.txt");
FileKey k2 = new FileKey("/tmp/a.txt");
map.put(k1, "value");
System.out.println(map.get(k2));
}
}
#Option
Avalue
Bnull
CClassCastException
DCompilation error
EInfinite loop

Correct: B (index 1)

Without overriding equals and hashCode, FileKey uses the default Object implementations — equals compares references, and hashCode is based on memory address. k1 and k2 are different objects, so k2 is not equal to k1. map.get(k2) returns null because no matching key is found.

medium — 1 mark


What is the output?

class Builder {
Builder configure() {
System.out.print("Base ");
return this;
}
}
class WebBuilder extends Builder {
WebBuilder configure() {
System.out.print("Web ");
return this;
}
}
public class Main {
public static void main(String[] args) {
Builder b = new WebBuilder();
Builder result = b.configure();
System.out.println(result.getClass().getSimpleName());
}
}
#Option
ABase Builder
BWeb Builder
CWeb WebBuilder
DCompiler error — return type mismatch
EBase WebBuilder

Correct: B (index 1)

Java allows covariant return types — WebBuilder.configure() returns WebBuilder (a subtype of Builder), which is valid. b.configure() uses dynamic dispatch, calling WebBuilder.configure() (outputs “Web ”). The return type of the reference result is Builder, so getClass().getSimpleName() returns “Builder”.

medium — 1 mark


P7 — Anonymous Classes and Effectively Final

Section titled “P7 — Anonymous Classes and Effectively Final”

What is the output?

public class Main {
public static void main(String[] args) {
int x = 10;
Runnable r = new Runnable() {
public void run() {
System.out.print(x);
}
};
// x = 20; // uncommented
r.run();
}
}
#Option
A10
B20
CCompiler error — x must be final
DRuntime error
E0

Correct: A (index 0)

Local variables referenced from an inner class must be effectively final (never reassigned after initialization). x = 10 is assigned once and never changed (the x = 20 line is commented out), so the code compiles. The anonymous class captures the value 10 and prints it.

easy — 1 mark


Which operation is O(1) for ArrayList but O(n) for LinkedList?

#Option
AAdd at the beginning
BAdd at the end
CRemove from the beginning
DRandom access by index
ESearch for an element

Correct: D (index 3)

ArrayList provides O(1) random access via its underlying array. LinkedList requires traversal from the head or tail to reach the nth element, making index-based access O(n). Both have O(n) search. Adding at the beginning is O(1) for LinkedList but O(n) for ArrayList (shift required).

medium — 1 mark


What is the time complexity of HashMap.get() in the worst case?

#Option
AO(1)O(1)
BO(logn)O(\log n)
CO(n)O(n)
DO(nlogn)O(n \log n)
EO(1)O(1) amortised

Correct: C (index 2)

In the worst case, all keys hash to the same bucket, forming a linked list (or red-black tree after Java 8’s treeification threshold of 8). Traversing the bucket is O(n). With treeification, worst case becomes O(log n), but the theoretical worst case before treeification is O(n).

medium — 1 mark


Which statement about TreeMap is true?

#Option
AIt uses a hash table for storage
BKeys are in insertion order
CKeys are sorted using natural ordering or a Comparator
DIt allows null keys
EIt provides O(1) average-case lookup

Correct: C (index 2)

TreeMap is a SortedMap backed by a red-black tree. Keys are kept in sorted order — either by their natural ordering (implementing Comparable) or by a Comparator provided at construction time. It does not allow null keys (throws NullPointerException). Lookup is O(log n).

easy — 1 mark


P11 — Iterator and ConcurrentModificationException

Section titled “P11 — Iterator and ConcurrentModificationException”

What is the output?

import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
if (s.equals("b")) it.remove();
}
System.out.println(list);
}
}
#Option
A[a, c, d]
B[a, b, c, d]
CConcurrentModificationException
D[a, d]
E[b, c, d]

Correct: A (index 0)

Using Iterator.remove() is the safe way to remove elements during iteration. It updates the iterator’s internal state, so no ConcurrentModificationException is thrown. The element “b” is removed, leaving [a, c, d].

easy — 1 mark


Which statement about ConcurrentHashMap is true?

#Option
AAll operations are synchronised on a single lock
BIt permits null keys and null values
CIt uses segment-level locking for concurrent access
DIt is slower than Collections.synchronizedMap for all operations
EIt does not support putIfAbsent

Correct: C (index 2)

ConcurrentHashMap uses a more fine-grained locking strategy (bucket-level or striping in Java 7, CAS + synchronized on individual buckets in Java 8+). This allows concurrent reads and writes without locking the entire map. It does not permit null keys or values. It is significantly faster than synchronizedMap under contention.

medium — 1 mark


What happens when you call add on an unmodifiable list?

import java.util.*;
public class Main {
public static void main(String[] args) {
List<String> list = Collections.unmodifiableList(
new ArrayList<>(Arrays.asList("x", "y"))
);
list.add("z");
}
}
#Option
A"z" is added successfully
BNullPointerException
CUnsupportedOperationException
DCompiler error — cannot call add on unmodifiable list
E[x, y, z]

Correct: C (index 2)

Collections.unmodifiableList returns a wrapper that delegates to the original list but throws UnsupportedOperationException for any mutating operation (add, remove, set). The compiler cannot prevent this because List declares these methods — the error occurs at runtime.

medium — 1 mark


What is the output?

import java.util.*;
public class Main {
public static void main(String[] args) {
PriorityQueue<Integer> pq = new PriorityQueue<>();
pq.add(5);
pq.add(1);
pq.add(3);
while (!pq.isEmpty()) {
System.out.print(pq.poll() + " ");
}
}
}
#Option
A5 3 1
B1 3 5
C5 1 3
D3 1 5
E1 5 3

Correct: B (index 1)

PriorityQueue is a min-heap by default. poll() removes and returns the smallest element. Elements are dequeued in ascending order: 1, 3, 5.

easy — 1 mark


What is the output?

public class Counter {
private int count = 0;
public void increment() {
synchronized (this) {
count++;
}
}
public int getCount() { return count; }
public static void main(String[] args) throws InterruptedException {
Counter c = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) c.increment();
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) c.increment();
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(c.getCount());
}
}
#Option
AAlways 2000
BAlways less than 2000
CSometimes less than 2000 without synchronization
DCompilation error
EDeadlock

Correct: A (index 0)

The synchronized (this) block ensures mutual exclusion — only one thread executes count++ at a time. Both threads iterate 1000 times, so the result is always 2000. Without synchronization, the result would be nondeterministic (sometimes less than 2000 due to race conditions).

medium — 1 mark


Which statement about the volatile keyword is correct?

#Option
AIt makes variables thread-safe for compound operations
BIt guarantees atomicity of i++
CIt ensures visibility of writes across threads
DIt replaces the need for synchronized in all cases
EIt prevents CPU caching entirely

Correct: C (index 2)

volatile guarantees that reads and writes to the variable are visible across threads — a write by one thread is immediately visible to reads by other threads. It does not provide atomicity for compound operations like i++ (read-modify-write). It is appropriate for flags and status variables, not for counters or accumulators.

medium — 1 mark


What is a key characteristic of virtual threads in Java 21+?

#Option
AThey run on dedicated OS threads
BThey cannot perform blocking I/O
CThey are lightweight threads managed by the JVM, not the OS
DThey use more memory than platform threads
EThey require the synchronized keyword for all operations

Correct: C (index 2)

Virtual threads are managed by the JVM’s scheduler, not the operating system. They are extremely lightweight — you can create millions of them. When a virtual thread performs blocking I/O, the JVM unmounts it from its carrier thread and mounts another virtual thread, allowing efficient utilisation.

easy — 1 mark


What is the output?

import java.util.concurrent.*;
public class Main {
public static void main(String[] args) throws Exception {
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> "Hello");
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> " World");
String result = f1.thenCombine(f2, (a, b) -> a + b).get();
System.out.println(result);
}
}
#Option
AHello World
BWorld Hello
CHello
DExecutionException
Enull

Correct: A (index 0)

thenCombine combines the results of two futures once both complete. f1 produces “Hello”, f2 produces ” World”. The combiner function concatenates them: “Hello” + ” World” = “Hello World”. .get() blocks until the result is available.

easy — 1 mark


Which of the following is NOT a necessary condition for deadlock?

#Option
AMutual exclusion
BHold and wait
CNo preemption
DCircular wait
EThread priority inversion

Correct: E (index 4)

The four necessary conditions for deadlock (Coffman conditions) are: (1) mutual exclusion — resources cannot be shared, (2) hold and wait — threads hold resources while waiting for others, (3) no preemption — resources cannot be forcibly taken, (4) circular wait — a cycle of threads exists. Thread priority inversion is a scheduling problem, not a deadlock condition.

medium — 1 mark


Which advantage does ReentrantLock have over synchronized?

#Option
AIt is simpler to use
BIt supports try-lock with timeout
CIt does not require explicit unlock
DIt is always faster
EIt provides automatic deadlock detection

Correct: B (index 1)

ReentrantLock provides features that synchronized does not: tryLock() with a timeout, lockInterruptibly(), and multiple Condition objects. synchronized automatically releases the lock when the block exits; ReentrantLock requires an explicit unlock() in a finally block. ReentrantLock is not inherently faster — it is designed for situations where synchronized is insufficient.

medium — 1 mark


What is the output?

import java.util.stream.*;
public class Main {
public static void main(String[] args) {
Stream.iterate(0, n -> n + 1)
.filter(n -> {
System.out.print("f" + n + " ");
return n % 2 == 0;
})
.limit(3)
.forEach(System.out::print);
}
}
#Option
Af0 0f2 2f4 4
B0 2 4
CInfinite loop
Df0 0 f2 2 f4 4
Ef00f22f44

Correct: A (index 0)

Streams are lazy — filter is invoked only when forEach requests elements. The pipeline requests elements until 3 match. For each element: filter prints “f0”, element 0 passes (prints 0), filter prints “f1”, element 1 fails, filter prints “f2”, element 2 passes (prints 2), filter prints “f3” (fails), filter prints “f4”, element 4 passes (prints 4). Output: f0 0f2 2f4 4.

hard — 1 mark


What is the output?

import java.util.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
Map<String, List<Integer>> result = Stream.of(1, 2, 3, 4, 5, 6)
.collect(Collectors.groupingBy(n -> n % 2 == 0 ? "even" : "odd"));
System.out.println(result);
}
}
#Option
A{odd=[1, 3, 5], even=[2, 4, 6]}
B{even=[1, 3, 5], odd=[2, 4, 6]}
C{odd=3, even=3}
DCompiler error
E{[1, 3, 5], [2, 4, 6]}

Correct: A (index 0)

Collectors.groupingBy partitions elements by the classifier function. Odd numbers (1, 3, 5) are grouped under “odd”, even numbers (2, 4, 6) under “even”. The result is a Map<String, List<Integer>>. Map iteration order is not guaranteed, but the grouping is correct.

easy — 1 mark


What is the output?

import java.util.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
List<List<Integer>> nested = List.of(
List.of(1, 2),
List.of(3, 4),
List.of(5)
);
List<Integer> flat = nested.stream()
.flatMap(Collection::stream)
.collect(Collectors.toList());
System.out.println(flat);
}
}
#Option
A[[1, 2], [3, 4], [5]]
B[1, 2, 3, 4, 5]
C[15]
DCompiler error
E[6, 12]

Correct: B (index 1)

flatMap maps each element to a stream and flattens the results into a single stream. Each inner list is converted to a stream, and all elements are combined into one flat stream. The result is [1, 2, 3, 4, 5].

easy — 1 mark


What is the output?

import java.util.stream.*;
public class Main {
public static void main(String[] args) {
int product = IntStream.rangeClosed(1, 5)
.reduce(1, (a, b) -> a * b);
System.out.println(product);
}
}
#Option
A15
B120
C5
D0
E1

Correct: B (index 1)

reduce(1, (a, b) -> a * b) computes the product: 1 * 1 * 2 * 3 * 4 * 5 = 120. The identity value is 1 (multiplicative identity). IntStream.rangeClosed(1, 5) produces the stream 1, 2, 3, 4, 5.

easy — 1 mark


What is the output?

import java.util.*;
import java.util.stream.*;
public class Main {
public static void main(String[] args) {
Optional<String> result = Stream.of("apple", "banana", "cherry")
.filter(s -> s.startsWith("b"))
.findFirst();
result.ifPresent(s -> System.out.print(s.length()));
}
}
#Option
A5
B6
Capple
DNothing is printed
Ebanana

Correct: B (index 1)

findFirst() returns an Optional<String>. The filter keeps only “banana” (starts with “b”). “banana” has length 6. ifPresent prints 6 if the Optional contains a value.

easy — 1 mark


Which statement about parallel streams is correct?

#Option
AParallel streams always outperform sequential streams
BThey use the ForkJoinPool by default
CThey are thread-safe for all operations
DThey require explicit thread management
EThey cannot be used with ordered data

Correct: B (index 1)

Parallel streams use the common ForkJoinPool (available via ForkJoinPool.commonPool()). They are not always faster — small datasets or expensive operations may see no benefit or even performance degradation. They are not automatically thread-safe for side-effecting operations (use ConcurrentHashMap or reduce instead).

medium — 1 mark


What is the output?

class Resource implements AutoCloseable {
Resource() { System.out.print("open "); }
public void close() { System.out.print("close "); }
}
public class Main {
public static void main(String[] args) {
try (Resource r = new Resource()) {
System.out.print("use ");
}
}
}
#Option
Aopen use close
Buse open close
Copen close use
DCompiler error
Eopen use

Correct: A (index 0)

Try-with-resources acquires the resource first (prints “open ”), executes the block (prints “use ”), then automatically calls close() (prints “close ”) even if an exception occurs. This ensures deterministic resource cleanup.

easy — 1 mark


What is the output?

public class Main {
static void methodA() {
try {
methodB();
} catch (RuntimeException e) {
System.out.print("caught ");
}
}
static void methodB() {
throw new RuntimeException();
}
public static void main(String[] args) {
methodA();
System.out.print("done");
}
}
#Option
Adone
Bcaught done
CUnhandled exception — program terminates
Dcaught
ERuntimeException done

Correct: B (index 1)

methodB throws a RuntimeException. It propagates up to methodA, where the catch block catches it (prints “caught ”). Execution continues after the try-catch, printing “done”.

easy — 1 mark


What is the output?

public class Main {
public static void main(String[] args) {
try {
String s = null;
s.length();
} catch (NullPointerException | IndexOutOfBoundsException e) {
System.out.print(e.getClass().getSimpleName());
}
}
}
#Option
AException
BNullPointerException
CIndexOutOfBoundsException
DCompiler error — multi-catch must not overlap
ERuntimeException

Correct: B (index 1)

The multi-catch block handles either NullPointerException or IndexOutOfBoundsException. null.length() throws NullPointerException. The catch block prints the exception’s simple class name: “NullPointerException”. Multi-catch is syntactic sugar — the variable e is implicitly final.

easy — 1 mark


What is the output?

class AppException extends Exception {
AppException(String msg, Throwable cause) {
super(msg, cause);
}
}
public class Main {
public static void main(String[] args) {
try {
try {
throw new java.io.IOException("disk error");
} catch (java.io.IOException e) {
throw new AppException("failed", e);
}
} catch (AppException e) {
System.out.print(e.getMessage() + " ");
System.out.print(e.getCause().getMessage());
}
}
}
#Option
Afailed disk error
Bdisk error failed
Cfailed
DIOException
EStackOverflowError

Correct: A (index 0)

The inner try throws an IOException. The catch block wraps it in an AppException with message “failed”. The outer catch prints getMessage() (“failed”) and getCause().getMessage() (“disk error”). Exception chaining preserves the root cause while adding context.

medium — 1 mark


Click to reveal the answer key
QuestionAnswerQuestionAnswerQuestionAnswer
P1AP11AP21A
P2AP12CP22A
P3BP13CP23B
P4BP14BP24B
P5BP15AP25B
P6BP16CP26B
P7AP17CP27A
P8DP18AP28B
P9CP19EP29B
P10CP20BP30A

DifficultyCount
Easy11
Medium18
Hard1


  1. Trace code by hand. Follow each variable through the method call stack. Do not guess.
  2. Know the Collections contracts. Understanding equals/hashCode, Comparable, and iterator semantics is essential.
  3. Understand the “why”. Java design decisions (generics type erasure, checked exceptions, virtual threads) have clear rationale. Understanding the motivation makes the rules easier to remember.
  4. Practise concurrency mentally. Visualise thread interleavings to identify race conditions and deadlocks.
  5. Retake after one week. Java has many subtle rules — spaced repetition is essential for retaining the details.

Last updated: 24 July 2026

Written by Wyatt. For questions or feedback, visit wyattau.com.