Huru
Technical Interviews

Java Interview Questions and Answers (2026): Core Java, OOP, Collections, Concurrency, Java 25 and Coding Exercises

Java interview questions with answers and code, checked against the Java Language Specification, the JDK documentation and openjdk.org: JDK vs JRE vs JVM, pass-by-value, == vs equals, the equals and hashCode contract, strings, OOP, records and sealed classes, exceptions, collections and how HashMap works, streams, synchronized, volatile and virtual threads, garbage collection, what's new from Java 21 to Java 27, output puzzles and coding exercises with solutions.

By Amine Boukioud Updated 24 min read
The Huru mascot snaps a glowing amber block into a tree of frosted-glass blocks shaped like a class hierarchy, next to a steaming coffee mug and a laptop showing code

The short answer: Java interviews check that you understand how the language and the JVM behave, beyond the syntax: pass-by-value, == vs equals(), the equals() and hashCode() contract, immutable strings, and the difference between the JDK, the JRE and the JVM. Expect questions on OOP (interfaces vs abstract classes, overriding, records and sealed classes), exceptions, collections (how HashMap works, which collection to choose), streams, concurrency (synchronized, volatile, executors and virtual threads), garbage collection, and a live coding exercise. Interviewers increasingly ask what’s current. Java 25 (September 2025) is the latest long-term support (LTS) release. Java 27 (September 15, 2026) is the newest release: it makes G1 the default garbage collector everywhere and turns on compact object headers by default. The answers below are checked against the Java Language Specification, the JDK documentation and openjdk.org, with sources at the end, and every code sample was compiled and run on Java 25.

Preparing for Java 8 questions in particular (lambdas, streams, Optional, java.time)? Our Java 8 interview questions cover them in more depth.

Java versions in 2026

Java ships a feature release every March and September. Oracle gives some of them long-term support; the next planned LTS release is Java 29, in September 2027.

VersionReleasedTypeOracle support
Java 27September 15, 2026Feature releasePremier Support until March 2027
Java 26March 17, 2026Feature releaseEnded September 2026
Java 25September 16, 2025LTSPremier until September 2030, extended until September 2033
Java 21September 2023LTSPremier until September 2028, extended until September 2031
Java 17September 2021LTSPremier ended September 2026, extended until September 2029
Java 11September 2018LTSExtended until January 2032
Java 8March 2014LTSExtended until December 2030

Dates come from Oracle’s support roadmap; other JDK vendors publish their own support dates. If a job posting says “Java 17+” or “Java 21”, expect questions on what those versions added: records, sealed classes, pattern matching and virtual threads, all covered below.

Core Java

“What’s the difference between the JDK, the JRE and the JVM?”

The JVM (Java Virtual Machine) runs compiled bytecode: it loads and verifies classes, interprets them, and compiles frequently used code to native machine code with its just-in-time (JIT) compiler. The JRE (Java Runtime Environment) is the JVM plus the class libraries a program needs to run. The JDK (Java Development Kit) adds the development tools: javac, jar, jshell, jlink, jcmd and others. Since Java 11, Oracle ships only the JDK, with no separate JRE; you can build a smaller custom runtime with jlink.

“Why is Java called platform independent?”

javac compiles source code to bytecode in .class files, not to machine code for one processor. Any JVM can run that bytecode, so the same .jar file runs on Windows, macOS or Linux. The JVM itself is platform specific: “write once, run anywhere” works because there is a JVM for each platform.

“Is Java pass-by-value or pass-by-reference?”

Always pass-by-value. The Java Language Specification says the argument values initialize new parameter variables when a method is called. For an object, the value copied is the reference, so a method can change the object, but it can’t make the caller’s variable point to a different object:

static void update(StringBuilder sb) {
  // Changes the shared object
  sb.append(" world");
  // Changes only the local copy
  sb = new StringBuilder("bye");
}

If the caller’s sb held hello, it holds hello world after update(sb), not bye.

“== or equals()?”

== compares primitive values, or, for objects, whether two references point to the same object. equals() compares logical equality as the class defines it; String, the wrapper classes, collections and records all override it. Compare strings and boxed numbers with equals(). The puzzles below show what goes wrong otherwise.

“What is the contract between equals() and hashCode()?”

From the Object documentation: if two objects are equal according to equals(), they must return the same hashCode(). Unequal objects may share a hash code, though fewer collisions make hash tables faster. If you override equals() without hashCode(), a HashMap or HashSet can’t find your objects, because two “equal” keys land in different buckets.

public final class Point {
  private final int x, y;

  public Point(int x, int y) {
    this.x = x;
    this.y = y;
  }

  @Override
  public boolean equals(Object o) {
    return o instanceof Point p
      && x == p.x && y == p.y;
  }

  @Override
  public int hashCode() {
    return Objects.hash(x, y);
  }
}

A record does all of this in one line, record Point(int x, int y) {}, which is one reason to use records for value types.

“Why is String immutable? String, StringBuilder or StringBuffer?”

A String can’t change after it’s created. That makes strings safe to share between variables and threads, lets string literals be reused (the Java Language Specification says equal literals refer to the same interned instance), and makes strings reliable HashMap keys, since their hash code can’t change. Since Java 9, strings that contain only Latin-1 characters store one byte per character (“compact strings”).

Every “change” creates a new string, so building a string with + in a loop creates many objects. Use StringBuilder (mutable, not synchronized) instead, or String.join(). StringBuffer is the older, synchronized version; you rarely need it.

StringBuilder sb = new StringBuilder();
for (String word : words) {
  sb.append(word).append(' ');
}
String sentence = sb.toString().trim();

“What are the primitive types? What is autoboxing?”

Java has eight primitive types: byte, short, int, long, float, double, char and boolean. Each has a wrapper class (Integer, Double and so on), because collections and generics hold only objects. Autoboxing converts between them automatically, which brings two traps: unboxing a null wrapper throws a NullPointerException, and == on wrappers compares references, not values.

“final, finally or finalize()?”

  • final on a variable means it can be assigned only once; on a method, it can’t be overridden; on a class, it can’t be extended (String is final). A final reference can’t be reassigned, but the object it points to can still change.
  • finally is a block that runs after try and catch, whether or not an exception was thrown. Use it, or better, try-with-resources, to release resources.
  • finalize() was a method the garbage collector might call before reclaiming an object. It was unpredictable and was deprecated for removal in Java 18. Use try-with-resources, or the Cleaner API for long-lived resources.

“What does static mean?”

A static field or method belongs to the class, not to an instance: there’s one copy, shared by all instances, and a static method can’t use this or instance fields directly. A static nested class holds no reference to an instance of the outer class. An inner (non-static) class does, which can keep the outer object in memory longer than you expect.

“Stack or heap?”

Each thread has its own stack, with a frame for each method call that holds its local variables: primitive values and references. Objects live on the heap, which all threads share and the garbage collector manages. A StackOverflowError usually means recursion that goes too deep; an OutOfMemoryError means the heap or another memory area is exhausted.

Object-oriented Java

“Explain the four principles of OOP in Java.”

  • Encapsulation: keep fields private and expose behavior through methods, so a class controls its own state.
  • Inheritance: a class extends one superclass and can implement many interfaces.
  • Polymorphism: a variable of type Shape can hold a Circle or a Square, and shape.area() runs the subclass’s method, chosen at run time.
  • Abstraction: interfaces and abstract classes describe what something does and hide how.

“Abstract class or interface?”

InterfaceAbstract class
A class can extend or implementManyOne
Instance fields (state)No, constants onlyYes
ConstructorsNoYes
Methods with a bodydefault, static and private methodsAny
Use it forA capability unrelated classes share (Comparable, Runnable)A base class that shares state and code among closely related classes

Default and static interface methods arrived in Java 8, private ones in Java 9.

“Overloading or overriding?”

Overloading means several methods with the same name and different parameter lists; the compiler picks one at compile time. Overriding means a subclass replaces an inherited method with the same signature; the JVM picks the implementation at run time from the object’s actual class. An overriding method can’t reduce visibility or throw broader checked exceptions, and it may return a subtype (a covariant return type). Add @Override so the compiler catches mistakes such as a misspelled name. Static methods are hidden, not overridden.

“Why doesn’t Java allow multiple inheritance of classes?”

To avoid the “diamond problem”: if two superclasses defined the same method or field, which one would the subclass inherit? A class can implement several interfaces instead. If two of them provide the same default method, the class must override it, and it can call one of them with InterfaceName.super.method().

“What is a record?”

A record (Java 16) is a class for immutable data. From its components, the compiler generates a constructor, accessor methods, equals(), hashCode() and toString(). Its fields are final, and it can’t extend another class. Add a compact constructor to validate the values:

record Money(BigDecimal amount,
             String currency) {
  Money {
    if (amount.signum() < 0) {
      throw
          new IllegalArgumentException(
              "Negative amount");
    }
  }
}

Records are only shallowly immutable: a record holding a List can’t swap the list for another, but the list itself can change unless you copy it with List.copyOf().

“What are sealed classes, and why do they matter with switch?”

A sealed class or interface (Java 17) lists exactly which classes may extend it. Combined with records and pattern matching for switch (Java 21), the compiler knows every possible subtype. A switch can then cover them all without a default branch, and adding a new subtype causes a compile error until every such switch handles it:

sealed interface Shape permits
    Circle, Square, Rectangle {}

record Circle(double r)
    implements Shape {}
record Square(double side)
    implements Shape {}
record Rectangle(double w, double h)
    implements Shape {}

static double area(Shape shape) {
  return switch (shape) {
    case Circle c ->
        Math.PI * c.r() * c.r();
    case Square s ->
        s.side() * s.side();
    case Rectangle(var w, var h) ->
        w * h;
  };
}

The last case is a record pattern (Java 21): it takes the record apart into its components.

“Composition or inheritance?”

Prefer composition when a class only needs another class’s behavior: hold it in a field and delegate to it. Inheritance ties a subclass to its parent’s implementation details, so a change in the parent can break the child. Keep inheritance for a true “is-a” relationship that the parent was designed for.

Exceptions

“Checked or unchecked exceptions?”

The compiler makes you catch or declare checked exceptions (IOException, SQLException), which signal conditions a caller can reasonably recover from. Unchecked exceptions are RuntimeException and its subclasses (NullPointerException, IllegalArgumentException) plus Error and its subclasses (OutOfMemoryError, StackOverflowError); they usually mean a programming bug or a problem the application can’t handle. A strong answer adds three habits: don’t catch what you can’t handle, never swallow an exception silently, and keep the cause when you wrap one, as in throw new ServiceException("Lookup failed", e).

“What does try-with-resources do?”

It closes every resource declared in its parentheses, in reverse order, when the block ends, even if an exception is thrown. A resource is any object that implements AutoCloseable. If both the block and close() throw, the block’s exception is thrown and the one from close() is attached to it as a suppressed exception.

try (var reader =
    Files.newBufferedReader(path)) {
  return reader.readLine();
}

“What causes a NullPointerException, and how do you avoid one?”

Calling a method or reading a field through a null reference, or unboxing a null wrapper. Since Java 14, the message names what was null (“helpful NullPointerExceptions”). Avoid them by checking inputs at boundaries with Objects.requireNonNull(), returning empty collections instead of null, using Optional for return values that may be missing, and initializing fields in the constructor.

Collections

“Which collection would you use?”

NeedUseCost of the main operations
A list you read by indexArrayListget constant; add at the end amortized constant; insert or remove in the middle linear
A stack or a queueArrayDequeMost operations amortized constant
Lookup by keyHashMapget and put constant on average
Keys in insertion order, or an LRU cacheLinkedHashMapLike HashMap
Keys kept sorted, or range queriesTreeMapget, put and remove in log(n), guaranteed
Unique elementsHashSet (TreeSet if sorted)Like the matching map
Always take the smallest (or largest) nextPriorityQueueoffer and poll in log(n); peek constant
A map shared between threadsConcurrentHashMapReads generally don’t block

The costs come from each class’s documentation. LinkedList is rarely the right choice: reading by index walks the list, and the documentation says ArrayDeque is likely faster than LinkedList as a queue and faster than Stack as a stack.

“How does HashMap work?”

A HashMap stores entries in an array of buckets. To place a key, it uses the key’s hashCode() to pick a bucket, then compares keys in that bucket with equals(). Since Java 8, a bucket that collects many colliding entries turns from a linked list into a balanced tree, which improves the worst case from O(n) to O(log n). When the number of entries passes capacity × load factor (16 and 0.75 by default), the map resizes to about twice as many buckets and redistributes the entries.

Lookups take constant time on average only if hash codes are well spread, which is why a poor hashCode() hurts. HashMap allows one null key and null values, makes no promise about order, and isn’t thread-safe.

“HashMap, Hashtable, Collections.synchronizedMap() or ConcurrentHashMap?”

Hashtable is a legacy class that synchronizes every method, and Collections.synchronizedMap() wraps a map the same way: one lock for everything. ConcurrentHashMap is built for concurrency, with full concurrency for retrievals and high expected concurrency for updates, plus methods such as computeIfAbsent() that run atomically. Unlike HashMap, it doesn’t allow null keys or values. Use it for maps shared between threads.

“Fail-fast or weakly consistent iterators?”

Iterators over ArrayList, HashMap and most java.util collections are fail-fast: if the collection is structurally changed after the iterator is created, other than through the iterator itself, they throw a ConcurrentModificationException. That check is best-effort, so don’t rely on it for correctness. The java.util.concurrent collections, such as ConcurrentHashMap, have weakly consistent iterators instead: they never throw that exception and may or may not show changes made during iteration. To remove while iterating, use Iterator.remove() or removeIf().

“Comparable or Comparator?”

Comparable gives a class its natural order through its own compareTo() method; String, Integer and LocalDate implement it. A Comparator is a separate object for any other order, and you can build one from method references:

employees.sort(Comparator
    .comparing(Employee::department)
    .thenComparing(Employee::salary,
        Comparator.reverseOrder()));

“What are sequenced collections?”

Java 21 added SequencedCollection, SequencedSet and SequencedMap for collections with a defined order. They give one consistent way to reach both ends: getFirst(), getLast(), addFirst(), addLast(), removeFirst(), removeLast() and reversed(). List, Deque, LinkedHashSet, SortedSet, LinkedHashMap and SortedMap all have them, so list.getLast() replaces list.get(list.size() - 1).

“List.of(), Collections.unmodifiableList() or Stream.toList()?”

List.of() (Java 9) creates an unmodifiable list and rejects null elements. Collections.unmodifiableList() returns a read-only view: you can’t change the list through it, but changes to the underlying list show through. Stream.toList() (Java 16) also returns an unmodifiable list. Trying to modify any of them throws UnsupportedOperationException.

Streams and lambdas

Since Java 8, collections are often processed with lambdas and streams. A pipeline has a source, lazy intermediate operations (filter, map, sorted) and one terminal operation (collect, toList, count) that triggers the work:

Map<String, Long> countByDept =
    employees.stream()
    .filter(e -> e.salary() > 50_000)
    .collect(Collectors.groupingBy(
        Employee::department,
        Collectors.counting()));

Java 24 added stream gatherers for custom intermediate operations. The built-in ones in Gatherers include fixed and sliding windows:

List<List<Integer>> batches =
    Stream.of(1, 2, 3, 4, 5)
    .gather(Gatherers.windowFixed(2))
    .toList();
// [[1, 2], [3, 4], [5]]

Our Java 8 interview questions go deeper on lambdas, functional interfaces, Optional, flatMap and parallel streams.

Concurrency

“How do you create and run threads?”

Write the work as a Runnable (no result) or a Callable (returns a result and can throw checked exceptions), and submit it to an ExecutorService instead of creating and managing Thread objects yourself. submit() returns a Future for the result. Always shut executors down. Since Java 19, ExecutorService is AutoCloseable, so try-with-resources shuts it down and waits for submitted tasks to finish.

“synchronized or volatile?”

synchronized gives mutual exclusion, one thread at a time per lock, and visibility: unlocking a monitor happens-before every later lock of that monitor, so the next thread sees the changes. volatile gives only visibility and ordering: a write to a volatile field happens-before every later read of that field. It doesn’t make compound actions atomic. count++ on a volatile field is still a read, an add and a write, so two threads can lose updates. Use AtomicInteger, LongAdder or a lock for counters:

private final AtomicInteger count =
  new AtomicInteger();

void increment() {
  count.incrementAndGet();
}

“What is a deadlock? How do you prevent one?”

Two or more threads each hold a lock and wait for a lock another one holds, so none of them can continue. Prevent it by always taking locks in the same global order, holding them as briefly as possible, using tryLock() with a timeout from java.util.concurrent.locks, or avoiding shared locks with concurrent collections and immutable data. To diagnose one, take a thread dump with jcmd <pid> Thread.print, which reports the deadlocks it finds.

“What are virtual threads?”

Virtual threads (Java 21) are lightweight threads that the JDK schedules onto a small pool of operating-system threads. When a virtual thread blocks on I/O, it frees its carrier thread for another virtual thread. That lets plain blocking code, one thread per request, scale to a very large number of concurrent tasks that mostly wait on networks or databases. Points interviewers check:

  • Create one per task and don’t pool them; they’re cheap. The executor in the example below starts a new virtual thread for each task.
  • They raise throughput for I/O-bound work. They don’t make CPU-bound computation faster.
  • Before Java 24, blocking inside a synchronized block pinned a virtual thread to its carrier thread. Java 24 removed nearly all such pinning.
  • Virtual threads can be very numerous, so use ThreadLocal sparingly, and never to cache expensive objects per thread. Scoped values, final in Java 25, are a lighter way to share immutable data.
var executor = Executors
    .newVirtualThreadPerTaskExecutor();
try (executor) {
  for (String url : urls) {
    executor.submit(() -> fetch(url));
  }
}
// close() waits for every task

“What are scoped values and structured concurrency?”

Scoped values (ScopedValue, final in Java 25) share immutable data with the methods a thread calls and with child threads, for a bounded scope. They’re easier to reason about than ThreadLocal, and cheaper. Structured concurrency treats a group of related subtasks as one unit of work, so that, for example, the others can be cancelled when one fails. It’s still a preview feature in Java 27 (its seventh preview), so it needs --enable-preview and its API may change.

“What is CompletableFuture?”

A Future you can compose: chain steps with thenApply(), thenCompose() and thenCombine(), handle errors with exceptionally() or handle(), and wait for several at once with allOf(). With virtual threads, the same logic can often be written as plain blocking code, which is easier to read and debug.

The JVM, memory and garbage collection

“How does garbage collection work?”

The garbage collector frees objects that can no longer be reached from GC roots, such as local variables on thread stacks and static fields. Most collectors are generational: most objects die young, so a young generation is collected often and cheaply, and long-lived objects move to an old generation. The HotSpot JVM’s collectors:

CollectorDesigned for
G1 (Garbage-First)A balance of throughput and pause times; the default
ZGCVery short pauses, even on large heaps; generational since Java 21, and only generational since Java 24
ShenandoahShort pauses, collecting concurrently with the application; generational mode a product feature since Java 25
ParallelMaximum throughput for batch jobs that can accept pauses
SerialSmall heaps and single-CPU machines

G1 became the default for server environments in Java 9. Until Java 27, the JVM still picked Serial on machines with one CPU or less than 1,792 MB of memory, such as small containers; Java 27 makes G1 the default everywhere.

“Can Java have memory leaks?”

Yes. The garbage collector frees only unreachable objects, so anything you still reference stays in memory: entries in a static map or a cache that never evicts, listeners that are never removed, ThreadLocal values in pooled threads. The symptom is a heap that keeps growing after each full collection, and eventually an OutOfMemoryError. Diagnose it with a heap dump (jcmd <pid> GC.heap_dump) and a heap analyzer, or record the application with JDK Flight Recorder (jcmd <pid> JFR.start).

“What does the JIT compiler do?”

The JVM starts by interpreting bytecode, profiles it, and compiles the “hot” methods it runs most into optimized native code. Because those optimizations use what the program actually does at run time, such as which implementation of an interface is really called, long-running Java code gets fast, while startup and the first requests are slower (“warm-up”). Java 24 and 25 added an ahead-of-time cache that can load and link classes, and reuse method profiles, from a training run, to shorten warm-up.

“What are compact object headers?”

Every object on the heap starts with a header that the JVM uses to find its class and for hashing, locking and garbage collection. On 64-bit platforms the header was 96 bits. Compact object headers cut it to 64 bits, which shrinks the heap for programs with many small objects. They became a product feature in Java 25 and the default in Java 27.

“How would you upgrade an application from Java 8 or 11 to Java 21 or 25?”

A senior question. Upgrade the build tools, frameworks and libraries first, since old versions often fail on new JDKs. Fix what the JDK removed: the Java EE and CORBA modules (such as JAXB) left the JDK in Java 11, so add them as dependencies. Code that reached into JDK internals through reflection breaks because Java 17 strongly encapsulates them; jdeps --jdk-internals finds it. Then run the full test suite, roll out gradually, and compare memory use, garbage collection and latency before and after.

What’s new from Java 21 to Java 27

VersionWhat comes up in interviews
Java 21 (LTS)Virtual threads; record patterns; pattern matching for switch; sequenced collections; generational ZGC
Java 22Unnamed variables and patterns (_)
Java 23Generational mode becomes ZGC’s default
Java 24Stream gatherers; virtual threads no longer pinned by synchronized; Security Manager permanently disabled; ahead-of-time class loading and linking
Java 25 (LTS)Compact source files and instance main methods; flexible constructor bodies; module import declarations; scoped values; compact object headers as a product feature; generational Shenandoah
Java 26HTTP/3 in the HTTP client; Applet API removed; warnings when deep reflection changes final fields
Java 27G1 the default garbage collector everywhere; compact object headers by default; post-quantum hybrid key exchange for TLS 1.3

Still in preview in Java 27: structured concurrency, lazy constants, and primitive types in patterns, instanceof and switch. The Vector API is still incubating.

Since Java 25, a small program can be this short. The new IO class is in java.lang, so it needs no import:

void main() {
  var name = IO.readln("Your name: ");
  IO.println("Hello, " + name);
}

And a constructor can now check its arguments before calling super(...), as long as it doesn’t use the object being built:

class Student extends Person {
  Student(String name, int age) {
    if (age < 16) {
      throw
          new IllegalArgumentException(
              "Too young: " + age);
    }
    super(name);
  }
}

Testing and tooling

“How do you test Java code?”

Unit tests with JUnit (JUnit 6, the current generation, needs Java 17 or later), Mockito to replace dependencies in unit tests, fluent assertions with AssertJ, and integration tests against real databases or message brokers running in containers (Testcontainers). Builds run on Maven or Gradle. Interviewers like hearing what you don’t mock: test behavior through public methods, and keep slower integration tests for the boundaries with databases and other services.

Output puzzles (“what does this print?”)

Boxed integers and ==

Integer a = 127, b = 127;
Integer c = 128, d = 128;
System.out.println(a == b);
System.out.println(c == d);

Prints true, then false with default settings. The Java Language Specification guarantees that boxing the same int from -128 to 127 gives the same object, so a == b. 128 is outside that range, so c and d are normally two different objects, and == compares references. Use equals().

Strings and ==

String s1 = "java";
String s2 = "java";
String s3 = new String("java");
System.out.println(s1 == s2);
System.out.println(s1 == s3);
System.out.println(s1.equals(s3));

Prints true, false, true. String literals are interned, so s1 and s2 are the same instance; new String(...) always creates a new object.

return inside finally

static int value() {
  try {
    return 1;
  } finally {
    return 2;
  }
}

Returns 2. When a finally block completes abruptly, with its own return or throw, that replaces whatever the try block was doing, including a pending return value or exception. Never return from finally: it can silently swallow exceptions.

Removing inside a for-each loop

List<String> names = new ArrayList<>(
  List.of("Ana", "Ben", "Cleo"));
for (String n : names) {
  if (n.startsWith("A")) {
    names.remove(n);
  }
}

Throws ConcurrentModificationException, because the list changed while its iterator was in use. Write names.removeIf(n -> n.startsWith("A")) instead. Removing the second-to-last element happens not to throw, which is why the documentation calls fail-fast behavior best-effort.

Adding two char values

System.out.println('a' + 'b');
System.out.println("" + 'a' + 'b');

Prints 195, then ab. Two char values are promoted to int before they’re added (97 + 98). Once a String is on the left, + means concatenation.

Floating-point equality

System.out.println(0.1 + 0.2 == 0.3);

Prints false: a double can’t represent 0.1 exactly, and the sum is 0.30000000000000004. Compare doubles with a tolerance, and use BigDecimal created from strings for money.

Coding exercises with solutions

Write each one, say its time and space cost, and test the edge cases: empty input, duplicates, no answer.

Two sum in one pass

static int[] twoSum(int[] nums,
                    int target) {
  Map<Integer, Integer> seen =
      new HashMap<>();
  int n = nums.length;
  for (int i = 0; i < n; i++) {
    int need = target - nums[i];
    Integer j = seen.get(need);
    if (j != null) {
      return new int[] { j, i };
    }
    seen.put(nums[i], i);
  }
  return new int[0];
}

One pass, O(n) time and space: for each number, check whether its complement was already seen.

First non-repeating character

static Optional<Character> firstUnique(
    String s) {
  Map<Character, Integer> counts =
      new LinkedHashMap<>();
  for (char c : s.toCharArray()) {
    counts.merge(c, 1, Integer::sum);
  }
  return counts.entrySet().stream()
      .filter(e -> e.getValue() == 1)
      .map(Map.Entry::getKey)
      .findFirst();
}

LinkedHashMap keeps insertion order, so the first entry with a count of 1 is the answer: w for "swiss". O(n).

Balanced brackets

static boolean balanced(String s) {
  Deque<Character> stack =
      new ArrayDeque<>();
  for (char c : s.toCharArray()) {
    switch (c) {
      case '(' -> stack.push(')');
      case '[' -> stack.push(']');
      case '{' -> stack.push('}');
      case ')', ']', '}' -> {
        if (stack.isEmpty()
            || stack.pop() != c) {
          return false;
        }
      }
      default -> { }
    }
  }
  return stack.isEmpty();
}

Push the closing bracket you expect; every closing bracket must match the top of the stack, and the stack must be empty at the end. The JDK documentation recommends a Deque such as ArrayDeque over the legacy Stack class. O(n).

An LRU cache

class LruCache<K, V>
    extends LinkedHashMap<K, V> {
  private final int capacity;

  LruCache(int capacity) {
    super(16, 0.75f, true);
    this.capacity = capacity;
  }

  @Override
  protected boolean removeEldestEntry(
      Map.Entry<K, V> eldest) {
    return size() > capacity;
  }
}

The true argument switches the map to access order, and removeEldestEntry() evicts the least recently used entry after each insert. Be ready to build it from scratch with a HashMap and a doubly linked list, and to say that this version isn’t thread-safe.

A thread-safe lazy singleton

public final class Config {
  private Config() { }

  private static class Holder {
    static final Config INSTANCE =
        new Config();
  }

  public static Config get() {
    return Holder.INSTANCE;
  }
}

The JVM initializes Holder only when get() first reads INSTANCE, and class initialization is thread-safe, so no locks are needed. An enum with a single constant is an even simpler singleton. If the interviewer asks about double-checked locking instead, the field must be volatile.

Questions by level and role

  • Freshers and junior developers: JDK vs JRE vs JVM, primitive types, String and immutability, == vs equals(), the four OOP principles, overloading vs overriding, interfaces vs abstract classes, checked vs unchecked exceptions, ArrayList vs LinkedList, basic HashMap use.
  • Mid-level: equals() and hashCode(), how HashMap and ConcurrentHashMap work, immutability and records, streams and collectors, exception design, executors and CompletableFuture, testing with JUnit and Mockito.
  • Senior and experienced: the Java memory model and happens-before, virtual threads vs reactive code, choosing a garbage collector and finding memory leaks, profiling with JDK Flight Recorder, upgrading from Java 8 or 11, API and module design, and system design.
  • By role: backend developers get Spring Boot (dependency injection, bean scopes, @Transactional, Spring Data JPA and the N+1 query problem), REST API design and SQL; see our Spring Boot interview questions, SQL interview questions and backend developer interview questions. Android roles usually add Kotlin; see our Android developer interview questions.

Behavioral questions for Java developers

Use the STAR method with a real example:

  • “Tell me about a production issue in a Java service that you debugged. How did you find the cause?”
  • “Describe a time you made a Java application faster or use less memory. What did you measure?”
  • “How did you upgrade a project to a newer Java version or framework? What broke?”
  • “Tell me about code you wrote that someone else had to maintain. What did you do to make it easy?”

More: our Spring Boot interview questions, software engineer interview questions, Python interview questions and system design interview guide.

Frequently asked questions

What Java questions are asked in interviews?
JDK vs JRE vs JVM, pass-by-value, == vs equals(), the equals() and hashCode() contract, String immutability, OOP (interfaces vs abstract classes, overloading vs overriding, records, sealed classes), checked vs unchecked exceptions, collections and how HashMap works, streams, concurrency (synchronized, volatile, executors, virtual threads), garbage collection, and a live coding exercise.
What is the latest version of Java?
Java 27, released on September 15, 2026. The latest long-term support (LTS) release is Java 25, from September 2025, and Oracle plans the next LTS release, Java 29, for September 2027.
Which Java version should I prepare for?
Know the current LTS releases, Java 21 and 25, and what each version added, since many production systems still run Java 17, 11 or 8: lambdas and streams (8), var (10), records (16), sealed classes (17), virtual threads and pattern matching for switch (21), and compact source files and scoped values (25).
What is the difference between the JDK, the JRE and the JVM?
The JVM runs bytecode. The JRE is the JVM plus the class libraries needed to run programs. The JDK adds development tools such as javac, jar, jshell and jlink. Since Java 11, Oracle ships only the JDK.
How does HashMap work internally?
It uses each key's hashCode() to pick a bucket in an array, then compares keys in that bucket with equals(). Buckets with many collisions become balanced trees (since Java 8). The map roughly doubles its buckets when entries exceed capacity times the load factor, 16 and 0.75 by default.
What are virtual threads in Java?
Lightweight threads, final since Java 21, that the JDK schedules onto a few operating-system threads. They let simple blocking code handle very many concurrent I/O-bound tasks. Create one per task, don't pool them, and don't expect them to speed up CPU-bound work.
Can Huru help me practice Java interview questions?
Yes, for the speaking parts. You answer out loud, an AI interviewer follows up, and every answer is scored out of 100 with a stronger version. Huru doesn't run code, so practice the exercises in an editor.

Sources

Practice this with Huru AI

Turn what you just read into a rehearsal. Unlimited AI mock interviews with instant, specific feedback.

Amine Boukioud

Amine Boukioud is a co-founder of Beatview, Inc., the company behind Huru, and Head of Growth at Huru. He writes and reviews Huru’s interview guides; each one starts from primary sources such as employers’ own careers pages, official data and research, and follows Huru’s editorial policy.

Published

How this article was made: researched and drafted with AI assistance, then written, fact-checked and signed by Amine Boukioud on November 4, 2026. Sources are linked where they are used. How we work: editorial policy.