Programming Languages

Java Language

Java as used in interviews and on Android (ART): memory, GC, concurrency, collections, and the mistakes that fail loops.

~92 min read 0 interview questions
In 30 seconds
  • Java is the language; the JVM (HotSpot on servers, ART on Android) is the machine that loads bytecode, allocates objects and collects garbage. Kotlin compiles to the same bytecode; this page is about Java.
  • Objects live on the heap; locals and call frames live on the stack. Interviewers fail people who cannot draw stack vs heap, name GC roots, or state the equals/hashCode contract.
  • The Java Memory Model is about visibility and ordering, not just locks. synchronized, volatile and final fields each publish a different happens-before edge.
  • Collections are a complexity and concurrency story: HashMap is not thread-safe; ConcurrentHashMap is not a drop-in for every lock; fail-fast iterators throw when the structure mutates.
  • On Android, ART, dex compilation, boxed types on Binder, Parcelable, and a non-blocking main Looper are Java questions. Framework services (AMS, WMS) live on Android frameworks.

Java vs JVM vs ART vs Kotlin

Java is a language: source files, types, the standard library, and a set of contracts (equals, exceptions, the memory model). The JVM is a specification for a virtual machine that loads class files, verifies bytecode, JIT-compiles hot methods, and manages a garbage-collected heap. HotSpot is the usual desktop and server implementation. ART (Android Runtime) is Android's implementation of a Java-like VM: it still runs JVM bytecode in spirit, but the on-disk format is dex, the compiler is dex2oat, and the GC and JIT/AOT story is tuned for phones, not servers.

The JRE is a runtime (VM + class library) used to run programs. The JDK is the JRE plus tools: javac, javap, jar, a debugger. Saying "I installed Java" without saying JDK vs JRE is sloppy.

Kotlin is a different language that compiles to JVM bytecode (and to dex on Android). It can call Java and be called from Java. This page is Java. When an interviewer says "Kotlin on Android", they still expect you to know ART, the heap, and Binder; they do not expect a Kotlin language lecture here.

Analogy

Java is a recipe written in a standard notation. The JVM is the kitchen that knows how to cook from that notation. HotSpot is a restaurant kitchen; ART is a compact galley on a ship with less power and a different pantry (dex, profiles, a moving collector). Kotlin is a second recipe book whose dishes still come out of the same kitchen. The cook (runtime) can change; the recipe language you are being tested on here is Java.

Source (.java)  --javac-->  JVM bytecode (.class)
Kotlin  (.kt)   --kotlinc-->  JVM bytecode (.class)
On Android: .class / .jar  --d8/r8-->  dex  --dex2oat-->  oat/vdex  --ART-->  native + interp
On a server: .class  --HotSpot classload + JIT/AOT-->  native
Java the languageHotSpot JVMART
InputSource, types, APIsClass files, JAR/modulesDex in an APK/APEX; profiles
Compilerjavac to bytecodeC1/C2 JIT, optional AOTdex2oat AOT + JIT + profiles
Heap / GCLanguage does not pick a collectorG1 default; Parallel, ZGC, ShenandoahConcurrent Copying (moving) on modern releases
Class metadataN/AMetaspace (native); no PermGen since Java 8Native linear alloc; no PermGen
Process modelN/AOne VM per process, typicallyZygote forks a warm ART; apps share pages copy-on-write
Interview angle "Is Android a JVM?" A strong answer: ART implements the Java language and a bytecode-derived instruction set, but it is not HotSpot. Apps ship dex, not a pile of .class files; compilation is dex2oat; GC and JIT/AOT differ; there is no PermGen story. Kotlin is a frontend. Then stop before reciting AMS and WMS: that is Android frameworks.
Common pitfall Calling ART "the Android JVM" and then describing G1 generations as if they were on the phone. Name the runtime you mean.

Compilation and class loading

javac turns .java into class files: a header, a constant pool, field and method descriptors, and bytecode for each method. It does not produce machine code. javap -c -p is the tool that proves you can read what the compiler emitted. The VM then loads, links and initializes each class the first time it is needed.

Analogy

A class file is a shipping crate with a packing list (the constant pool) and assembly instructions (bytecode). The classloader is the warehouse that accepts crates, checks they are not damaged (verification), and shelves them under a unique address (the class object). Initialization is plugging the crate in and running the static setup before anyone may use the goods. Two warehouses can each accept a crate with the same label; they are still different products.

From source to a live Class object

  1. Compile javac emits bytecode. Overload resolution, generic erasure and checked-exception rules happen here.
  2. Load A classloader finds bytes (disk, dex, network) and defines a Class. On Android the defining loader works from dex, not a tree of .class files.
  3. Link: verify The VM checks that bytecode is type-safe and does not smash the stack. ART can persist verification in a vdex so it is not repeated every boot.
  4. Link: prepare Static fields get default values (0, null, false).
  5. Link: resolve Symbolic refs in the constant pool become concrete classes, fields and methods, often lazily on first use.
  6. Initialize Superclass first, then static initializers and static field assignments, in textual order, under a class lock. The JLS forbids seeing a class half-initialized from another thread.

Classloaders and parent delegation

A class is identified by its binary name plus its defining classloader. The bootstrap loader loads java.lang.*. The platform (extension) loader loads remaining JDK modules. The application (system) loader loads the classpath or module path. Each loader asks its parent first (parent delegation) so that java.lang.String is never replaced by a classpath impostor.

On Android, the app's PathClassLoader (or a descendant) loads the APK's dex. Shared framework classes come from the boot image Zygote already mapped. Plugin or hot-fix loaders are a common leak source: they pin the old APK's classes if any static or thread outlives the unload.

request load "com.example.Foo"
        │
        ▼
app loader ──ask parent──▶ platform ──ask parent──▶ bootstrap
        │                      │                         │
        │                      │                    java.*, core
        │                 jdk.* if not bootstrap
        └── define Foo from app dex if parents said no

Static vs instance; initialization order

Static fields and static blocks belong to the class; they run once when the class is initialized. Instance fields, instance blocks and constructors run on every new. Order for a new object:

  1. Superclass initialize if not already done (static side).
  2. This class initialize static fields and static { } in source order.
  3. Super constructor chain after the implicit or explicit super() (or this()) call.
  4. This class instance fields and { } blocks in source order, then the rest of the constructor body.
class Animal {
  static { System.out.println("Animal static"); }
  { System.out.println("Animal instance"); }
  Animal() { System.out.println("Animal ctor"); }
}
class Cat extends Animal {
  static { System.out.println("Cat static"); }
  { System.out.println("Cat instance"); }
  Cat() { System.out.println("Cat ctor"); }
}
// new Cat() prints:
// Animal static, Cat static, Animal instance, Animal ctor, Cat instance, Cat ctor

The first use that triggers initialization is a defined list: new, a static method or field (except final compile-time constants), or a reflective force. Reading public static final int N = 3; from another class does not initialize the declaring class; the constant was inlined.

Common pitfall Calling an overridable instance method from a constructor. The subclass fields are still at defaults. The subclass override runs and sees zeros and nulls.
Interview angle Draw the static/instance order for a pair of classes, then explain parent delegation and why "the same class name" can exist twice. Follow-up: what is stored in the constant pool, and what invokedynamic is used for (string concat, lambdas).

Memory layout: stack, heap, headers, generations

Each thread has a Java stack of frames. A frame holds local variables (including this and parameters) and an operand stack for bytecode. Locals that are object types are references, not the objects. Objects, arrays and class objects live on the heap. Native stacks (JNI, VM internals) sit beside the Java stack. Method metadata and class metadata live outside the Java heap: Metaspace on HotSpot, native linear-alloc on ART.

Analogy

The stack is a desk: the current method's papers sit on top, and when the method returns you throw that sheet away. The heap is the warehouse where the actual crates live. A local variable is a sticky note with a warehouse aisle number (the reference). Two sticky notes can point at the same crate. GC is the night crew that throws away crates nobody's sticky note, static shelf or JNI clipboard still names.

Thread T1 stack          Heap                         Metadata (native)
+-----------------+      +----------------------+     +-----------------+
| frame bar()     |      | object header +      |     | Klass / methods |
|   Point p  -----+------>| fields x, y          |     | constant pool   |
| frame foo()     |      | String "hi"          |     | (Metaspace/ART) |
|   int n = 3     |      | array, Class<Foo>    |     +-----------------+
+-----------------+      +----------------------+

Object header

A typical HotSpot object starts with a mark word (identity hash, GC age, lock bits) and a class pointer (which Klass this is). Then come the fields, then padding to the alignment (often 8 bytes). Arrays add a length. Compressed ordinary object pointers (oops) pack heap refs into 32 bits when the heap is under about 32 GB. Compressed class pointers pack the class word similarly. ART's object header is a different layout (lock word, class pointer, optional monitor) but the interview idea is the same: every object pays a fixed header tax before its fields.

HotSpot generations vs ART

HotSpot historically splits the heap into a young generation (Eden plus two Survivor spaces) and an old (tenured) generation. Most objects die in Eden. Survivors that live long enough are promoted. G1 (the default since Java 9) still uses young/old roles but the heap is a grid of equal-sized regions. ZGC and Shenandoah aim at short pauses with concurrent relocation.

ART on modern Android uses a moving Concurrent Copying (CC) collector: bump-pointer allocation into a young region, concurrent marking and copying, short stop-the-world (STW) roots. Older ART used concurrent mark-sweep. Zygote's preloaded objects sit in an image space that is almost never compacted, so copy-on-write pages stay shared across apps. Do not recite G1 Eden/Survivor as if it were the phone.

Escape analysis

If the JIT proves an object never escapes the method or thread (no store to a heap field, no pass to an unknown method), it may scalar-replace it: the object never exists on the heap; its fields become registers or stack slots. That is why a tiny new Point(x, y) in a hot loop can be free, and why you should not micro-optimise allocations the compiler already elides. Escape analysis is a HotSpot JIT feature; ART's JIT can do similar scalar replacement on some shapes, but you must not claim "Java never allocates" without a profiler.

Stack

Per-thread frames: primitives and references. Cheap to allocate, freed on return. Deep recursion blows it (StackOverflowError).

Heap

Shared objects. Allocated by new or reflection. Reclaimed by GC. OutOfMemoryError: Java heap space when it cannot grow or fragment-free.

Metaspace / native class data

Class metadata. OutOfMemoryError: Metaspace from classloader leaks, not from ordinary new.

Direct / native buffers

ByteBuffer.allocateDirect, JNI, ART linear alloc. Off-heap; different OOM messages; not moved by the Java GC the same way.

Interview angle Draw one stack frame pointing at a heap object, name the header words, then contrast HotSpot generations with ART CC and the Zygote image space. Mention escape analysis so you do not sound like "every new is a GC tax".

Garbage collection

GC finds objects that are not reachable from GC roots and reclaims them. Roots include local references in live frames, static fields, JNI global refs, and a few VM-internal handles. Reachability is graph reachability, not "I still have a pointer I forgot about in a cache".

Analogy

Think of a subway map. Stations (objects) stay open if you can still ride there from a depot (a GC root) by following tracks (references). A station with no path from any depot is abandoned, even if it has tracks to other abandoned stations (islands of garbage). Weak references are pencil lines the night crew is allowed to erase when they need the space. Phantom references are a postcard saying "we demolished that station; now you may close the file".

Young / old, STW vs concurrent

A minor (young) collection is frequent and cheap if most objects are dead. A major or mixed collection visits old regions. Stop-the-world means mutator threads pause at safepoints. Concurrent collectors do most marking and copying beside the app, but still take short STW pauses for roots or to flip spaces. "Concurrent" is not "pause-free".

IdeaHotSpot (typical interview)ART (modern)
Young objectsEden + survivors; copy surviving objectsBump-allocate; CC copies survivors
Old / long-livedTenured regions; G1 mixed collectionsOlder regions; still moving under CC
Class metadataMetaspace (native). PermGen is gone (Java 8+)Native; no PermGen
Low pauseZGC / Shenandoah on serversCC designed for UI jank, not 100 GB heaps
ZygoteN/AImage space kept stable so COW survives

finalize vs Cleaner; reference types

MechanismWhen it runsUse
finalize()Unspecified later, on a finalizer thread; can resurrect the objectDo not use. Unpredictable, stalls GC, hides leaks
Cleaner (or PhantomReference + queue)After the object is phantom-reachable; cannot resurrectNative / off-heap cleanup
Soft referenceCleared under memory pressure, after a delay heuristicCaches that should yield to the heap
Weak referenceCleared at the next GC once only weakly reachableCanonical maps, listener lists, WeakHashMap keys
Phantom referenceAfter finalization-equivalent; get() is always nullPost-mortem cleanup; Cleaner is built on this

A WeakHashMap is weak on keys, not values. If the value points back at the key, you have accidentally made the key strongly reachable.

GC roots you must name

  • Local variables and operand-stack slots of running frames.
  • Static fields of loaded classes.
  • JNI global references and critical/pinned regions.
  • Running-thread objects themselves and their monitors.
  • Objects held by the VM (interned strings historically; class objects).
Tip On Android, a moving collector plus JNI is a pin story: a native pointer into a Java array is only valid while the object is pinned or you copied the bytes. See C language for the C-side JNI rules.
Common pitfall Treating finalize as a destructor. It is not prompt, it is not ordered, and a finalizer that waits on a lock can deadlock the finalizer thread and starve the heap.
Interview angle Walk roots → mark → reclaim, then contrast STW with concurrent. Name weak/soft/phantom. Say PermGen is gone and Metaspace is native. On Android, say CC + image space, not G1 Eden.

equals, hashCode, toString, clone, compareTo

These methods are contracts, not decorations. Break them and HashMap, HashSet and TreeMap silently lose entries.

Analogy

A library catalogues books by a call number (hashCode) and then checks the title page (equals). If two copies of the same book get different call numbers, the second copy is shelved in another aisle and a search in the first aisle says "not here". If two different books share a call number, the librarian must still open both (hash collisions). compareTo is the shelf order; it must agree with equals if you use the class in a sorted set.

equals and hashCode

equals must be reflexive, symmetric, transitive, consistent, and x.equals(null) is false. If a.equals(b) then a.hashCode() == b.hashCode(). The converse is not required (collisions happen). Do not use mutable fields that can change while the object is a map key. Prefer Objects.equals and Objects.hash (or a cached hash on immutable objects).

== compares references (and primitives). equals compares values if you overrode it. For String, always equals unless you are talking interned identity.

toString, clone, compareTo

toString is for humans and logs. It must not throw and should not be enormous. clone is a broken pattern: Cloneable is a marker, Object.clone is protected and does a field-for-field copy (shallow). Mutable fields are shared. Prefer a copy constructor or a static factory. If you must clone, copy mutable children and document it.

compareTo must be a total order (or you document nulls), consistent with equals if the class is used in SortedSet/SortedMap, and sgn(a.compareTo(b)) == -sgn(b.compareTo(a)). A Comparator that returns 0 for unequal objects will collapse them in a TreeSet.

Immutability, intern, StringBuilder

An immutable class: final class (or careful sealing), all fields private final, no mutators, defensive copies of incoming and outgoing mutable state, and no leaking this from the constructor. String, boxed primitives, and well-written value types qualify.

String is immutable: length, hash and bytes (or coder + value array) do not change. Concatenation with + in a loop allocates many intermediates; use StringBuilder. StringBuffer is the synchronized leftover; do not use it in new code. The compiler and runtime already rewrite a single expression of + into a builder (and, on modern JDKs, invokedynamic concat).

String.intern() returns the canonical pooled instance. The pool lives on the heap (since Java 7, not PermGen). Interning every request string is a leak. == on interned strings is an identity trick; do not write production code that depends on it unless you intern both sides.

String a = new String("hi");
String b = "hi";
a == b;              // false: a is a distinct heap copy
a.intern() == b;     // true: both are the pooled "hi"
Integer x = 80, y = 80;
x == y;              // true: Integer cache, typically -128..127
Integer u = 200, v = 200;
u == v;              // false: not cached; use equals
Common pitfall Using == on boxed integers in a range you have not thought about, or putting a mutable key in a HashMap and then mutating it. The entry is lost until you remember the old hash.
Interview angle Recite the five equals properties, the hashCode implication, intern vs literal, Integer cache bounds, and why clone is shallow. Then implement equals/hashCode for a two-field class on the whiteboard.

OOP: access, types, nesting, newer syntax

Java's object model is single class inheritance, multiple interface implementation, and nominal types. Interviews test whether you pick inheritance for genuine "is-a" variation and composition for "has-a" reuse.

Analogy

A house (composition) contains rooms, wiring and a kitchen; you do not subclass House to get a dishwasher. A square is a rectangle only if you never change width independently of height; that is the classic inheritance lie. Interfaces are sockets on the wall: many appliances can plug in. Default methods are a factory-installed USB charger on an old socket standard so old houses still work.

Access and inheritance vs composition

ModifierSame classSame packageSubclassWorld
privateyesnonono
package-private (default)yesyesno*no
protectedyesyesyesno
publicyesyesyesyes

*A subclass in another package does not see package-private members. Prefer composition: hold a helper and delegate. Inherit only when you need polymorphism and the Liskov substitution rule holds.

Abstract class vs interface; default and sealed

Abstract class

  • Can have fields, constructors, protected state.
  • Single inheritance: one superclass.
  • Use when implementations share a skeleton.

Interface

  • Can have abstract, default, static and private methods (modern Java).
  • Fields are public static final.
  • A class may implement many. Use to define a capability.

Default methods let you evolve an interface without breaking existing implementors. They are not a substitute for a shared base class when you need state. Sealed classes and interfaces (newer Java) restrict which types may extend them; useful for exhaustive switch. Mention them; do not pretend every Android min-SDK has them. Records are transparent, final, shallowly immutable carriers with generated equals/hashCode/toString and a canonical constructor. They are not a full domain model.

Nested types

KindEnclosing instance?Typical use
Static nested classNoHelper that belongs in the namespace; Android Handler should be this
Inner classYes (hidden this$0)When it truly needs the outer instance
Local classYes, plus captured locals (effectively final)Rare; scoped to a method
Anonymous classYesOne-off listeners; same leak risk as inner

A non-static inner class keeps the outer object alive. A delayed Handler message holding an inner Handler keeps the Activity after onDestroy. Fix: static nested class + WeakReference, or clear callbacks. Details of the Looper sit in Android frameworks.

Interview angle Contrast abstract class and interface, then explain default methods as evolution, not multiple inheritance of state. Draw this$0 for an inner class and connect it to an Android leak.

Generics: erasure, PECS, wildcards, heap pollution

Generics are a compile-time type system. At runtime, a List<String> is a List. The compiler inserts casts. That is erasure. You cannot write new T(), new T[], or list instanceof List<String> in a useful way. You cannot overload two methods that erase to the same signature.

Analogy

The compiler is a customs officer who stamps every crate "electronics" or "glass" before it ships. At the warehouse (runtime) the stamps are peeled off; every crate is just a crate. PECS is the loading rule: a truck you only put into can accept a wider stamp (consumer, ? super); a truck you only take from must promise a narrower stamp (producer, ? extends).

PECS and wildcards

PECS: producer extends, consumer super. If a method only reads Ts from a collection, take Collection<? extends T>. If it only writes Ts in, take Collection<? super T>. If it does both, use Collection<T>.

void putAll(List<? super Integer> dest, List<? extends Integer> src) {
  dest.addAll(src);   // read Integers from src, write into dest
}
// dest can be List<Object> or List<Number>; src can be List<Integer> or List<? extends Integer>

? extends T is a producer: you can get T, you cannot add (except null). ? super T is a consumer: you can add T, you can only get Object.

Reified vs not; heap pollution

Java generics are not reified: the VM does not know T at runtime. Kotlin inline functions with reified are a compiler trick on the JVM, not a Java language feature. Arrays are reified: new String[n] knows its component type, which is why String[] is a subtype of Object[] (covariant arrays) and a store can throw ArrayStoreException.

Heap pollution is when a variable of type List<String> actually contains a Integer because someone used raw types or unchecked casts. The failure is a later ClassCastException, not at the add.

List list = new ArrayList<String>();  // raw
List<String> strings = list;
list.add(42);                          // unchecked, pollutes
String s = strings.get(0);             // ClassCastException here

Vararg methods of generic type (T...) allocate an array of the erased type and are a classic pollution source; the compiler warns. Prefer List<T> over T... for generics.

Interview angle Define erasure in one sentence, write a PECS signature, then explain why new T[] is illegal and how raw types cause heap pollution. Mention covariant arrays vs invariant generic lists.

Collections: implementations, cost, concurrency

The collections framework is interfaces plus implementations. Interviews want complexity, iteration failure modes, and which type is thread-safe. Algorithm patterns (two pointers, heaps) live on Data structures and algorithms; this page is the Java library and its traps.

Analogy

A List is a numbered ticket queue, a Set is a bag that refuses duplicates, a Map is a cloakroom: you hand in a ticket (key) and get a coat (value). HashMap is an unlocked cloakroom: two attendants will corrupt the hooks. Hashtable is one attendant who locks the whole room for every coat. ConcurrentHashMap is a room with many lockers that can be opened in parallel. Fail-fast iterators are a clerk who screams if someone rearranges hooks mid-count.

List, Set, Map at interview depth

TypeStructureGet / containsNotes
ArrayListGrowable arrayIndex O(1); contains O(n)Default choice. Grows by ~1.5×
LinkedListDoubly linkedO(n) indexRarely faster than ArrayList; deque use is the honest case
ArrayDequeRing bufferEnds O(1)Better stack/queue than Stack or LinkedList
HashSetHashMap keysO(1) avgNo order; null one allowed
LinkedHashSetHash + linked listO(1) avgInsertion (or access) order
TreeSetRed-black treeO(log n)Sorted; no null if natural order
HashMapArray of binsO(1) avg; O(log n) tree binNot thread-safe; one null key
LinkedHashMapHash + order listO(1) avgLRU by overriding removeEldestEntry
TreeMapRed-black treeO(log n)Sorted keys
EnumMap / EnumSetArray / bitsetO(1)Use whenever the key is an enum
IdentityHashMapIdentity ==O(1) avgReference equality; rare
WeakHashMapWeak keysO(1) avgEntries vanish when keys die

ArrayList growth

An empty ArrayList starts with a shared empty array. The first add allocates capacity 10. Later growth is newCap = oldCap + (oldCap >> 1) (one and a half times), not a doubling. Amortized add is still O(1). If you know the size, pass it to the constructor so you do not copy.

HashMap internals (the puzzle they always ask)

Capacity is a power of two. The bin index is (n - 1) & hash, after a mix: h ^ (h >>> 16). Each bin is a linked list until it treeifies: a bin becomes a red-black tree only when the chain length is at least 8 and the table capacity is at least 64. If the chain is long but the table is smaller than 64, HashMap resizes instead of treeifying. Untreeify happens when a tree bin shrinks to 6. Load factor defaults to 0.75. This is why "HashMap is O(1)" is average-case, and why a bad hashCode still hurts until treeify or resize kicks in.

table length n = 16
hash mix: h ^ (h >>> 16)
index = (n-1) & h

bin[i]:  e0 -> e1 -> e2 -> ...   (list)
         if count ≥ 8 and n ≥ 64  -->  red-black tree
         if count ≥ 8 and n < 64  -->  resize table first

Concurrent maps and synchronized wrappers

Thread-safe?HowNullsIterators
HashMapNo—one null keyFail-fast
HashtableYesSynchronized methods; coarseNo nullsFail-fast Iterator; legacy Enumeration is not
Collections.synchronizedMapYesOne lock on the wrapperDelegatesYou must synchronize on the wrapper while iterating
ConcurrentHashMapYesPer-bin / CAS; no global lock on getNo nullsWeakly consistent; no ConcurrentModificationException

Fail-fast iterators on ArrayList and HashMap compare a modCount to an expected value. A structural change on another thread (or the same thread, except through Iterator.remove) throws ConcurrentModificationException. It is a best-effort bug detector, not a concurrency protocol.

Common pitfall Sharing a HashMap across threads "because we only put during init". One missed race produces lost entries or an infinite loop on old implementations. Use ConcurrentHashMap or publish an immutable map after a safe barrier.
Interview angle State treeify thresholds 8 and 64 together. Contrast CHM, Hashtable and synchronized wrappers. Explain fail-fast vs weakly consistent. Give ArrayList's 1.5× growth.

Exceptions: checked, unchecked, resources

An exception is a signal that a method cannot finish its contract. Java splits the tree at Throwable: Error (the VM is in trouble) and Exception (the program might recover). RuntimeException is an Exception you are not forced to declare. That split is the checked vs unchecked rule.

Analogy

A checked exception is a form you must sign before you leave the building: either handle the problem or stamp it onto your job description (throws). An unchecked exception is a fire alarm: you may catch it, but the compiler does not make you write a plan for every possible trip. An Error is the building collapsing; catching it to "keep going" is usually dishonest.

Throwable
  ├── Error          (OutOfMemoryError, StackOverflowError, NoClassDefFoundError)
  └── Exception
        ├── RuntimeException   (unchecked: NPE, CCE, IAE, ConcurrentModificationException)
        └── other Exception    (checked: IOException, SQLException, InterruptedException)

Checked vs unchecked; Error vs Exception

Checked exceptions are for conditions a reasonable caller can be expected to handle: I/O, parse failures, missing files. Unchecked exceptions are for programming bugs (null, bad index, broken invariants) or for environments that cannot usefully recover at each call site. Error means the VM or linkage is compromised; do not catch OutOfMemoryError and pretend the heap is fine. Exception is the recoverable side of the tree, including both checked types and RuntimeException.

InterruptedException is checked on purpose: it is how a thread is asked to stop waiting. Swallowing it without restoring the interrupt flag (Thread.currentThread().interrupt()) hides cancellation.

try-with-resources and suppressed exceptions

A resource implements AutoCloseable (or Closeable). try-with-resources closes it in reverse order of declaration, even when the body throws. If close() also throws, that exception is suppressed and attached to the primary exception via addSuppressed. Always inspect getSuppressed() when debugging a close failure that "vanished".

try (InputStream in = Files.newInputStream(path);
     OutputStream out = Files.newOutputStream(dest)) {
  in.transferTo(out);
} // close out, then in; suppressed exceptions recorded on the first throw

A manual try/finally that closes in the finally can hide the original exception if close throws; try-with-resources was invented to stop that. Never return from finally (it swallows the in-flight exception and invents a new return). Never throw from finally if you can avoid it.

Common pitfall catch (Exception e) { log(e); } around a whole method. You lose the type, you swallow interrupts, and you turn bugs into empty catches. Catch the narrowest type you can handle, or let it propagate.
Interview angle Draw the Throwable tree, justify checked I/O, explain suppressed exceptions, and say what happens if finally returns. Follow-up: should Android Binder methods throw checked exceptions? (AIDL typically maps them to RemoteException or runtime failures; see Binder and AIDL.)

Concurrency: locks, j.u.c, pools, futures

Several threads, one heap. Without a happens-before edge, a write on thread A need not be visible to thread B, and the compiler and CPU may reorder independent accesses. java.util.concurrent is the library built on that model. This section is the toolkit; the next section is the JMM itself.

Analogy

Threads are cooks sharing one counter. synchronized is a single keyed pantry: only one cook enters, and when they leave, the next cook sees the pantry as they left it. volatile is a whiteboard everyone must look at through the glass: one field, always the latest scribble, but it is not a whole recipe lock. wait/notify is a cook who puts down the key, sits in the break room until a bell rings, then tries to get the key again. A thread pool is a fixed staff; ThreadLocal is a coat hook with each cook's name, which leaks if a substitute (pooled thread) keeps the previous cook's coat.

synchronized, volatile, wait/notify

synchronized (lock) acquires the monitor, runs the body, then releases. It is reentrant: the same thread may enter again. Entry and exit establish happens-before with the next thread that acquires the same monitor. volatile reads and writes of that field are not reordered with other accesses in ways that break the JMM; a write happens-before a subsequent read of the same variable. volatile does not make i++ atomic.

wait, notify and notifyAll must be called while holding the monitor of the same object. Always wait in a while loop that re-checks the condition (spurious wakeups, and notify-all with several predicates). notify wakes one waiter; notifyAll wakes all, who then contend for the monitor. Prefer Lock/Condition or higher-level latches unless you are implementing a simple monitor.

synchronized (queue) {
  while (queue.isEmpty()) {
    queue.wait();          // releases monitor; reacquires before returning
  }
  return queue.remove();
}

java.util.concurrent toolkit

TypeWhat it isInterview use
ReentrantLockExplicit lock; optional fairnessTry-lock, interruptible lock, multiple conditions
Conditionwait/notify bound to a LockSeveral wait-sets on one lock
CountDownLatchOne-shot counter to zeroStart gate or "wait for N workers"
CyclicBarrierReusable barrier; optional actionPhases: all N arrive, then continue
SemaphorePermitsBound concurrency (for example 3 disk jobs)
ConcurrentHashMapConcurrent mapSee collections; no nulls
ExecutorServiceSubmit Runnable/CallableDo not create unbounded threads by hand
ForkJoinPoolWork-stealing poolDivide-and-conquer; common pool for parallel streams

ThreadPoolExecutor parameters

Core size, max size, keep-alive, work queue, thread factory, and rejected-execution handler. The admission rule: if fewer than core threads, create one; else offer to the queue; if the queue is full and thread count is under max, create; else reject. A LinkedBlockingQueue with no capacity cap means max size is never reached (the queue never fills). A SynchronousQueue hands off to a thread or else grows to max (cached-thread style). AbortPolicy throws; CallerRunsPolicy runs on the submitter (backpressure); DiscardPolicy drops work silently (usually wrong).

submit(task)
  if workers < core      --> start worker
  else if queue.offer    --> sit in queue
  else if workers < max  --> start worker
  else                   --> handler (abort / caller-runs / discard)

Deadlock, livelock, ThreadLocal, CompletableFuture

Deadlock: two or more threads each hold a lock the other needs; nobody makes progress. Fix by lock ordering, try-lock with backoff, or fewer locks. Livelock: threads keep changing state in response to each other and never finish (polite people in a corridor). Starvation: a thread never gets the lock or CPU (unfairness, low priority).

ThreadLocal is a per-thread slot. On a pool, values survive to the next task unless you remove() in a finally. That is a leak of objects and of security context. Android's Looper is stored in a ThreadLocal; that is why Looper.prepare() is per thread.

CompletableFuture is a composable promise: supplyAsync, thenApply, thenCompose (flatMap), thenCombine, exceptionally, whenComplete, orTimeout. State the executor; the common ForkJoin pool is a bad default for blocking I/O. get() blocks and wraps exceptions in ExecutionException. Prefer join() only when you already handle unchecked wrapping, or stay async to the edge.

Interview angle Explain the ThreadPoolExecutor admission algorithm, then pick latch vs barrier vs semaphore. Write a wait-loop. For CompletableFuture, compose two async calls without blocking the caller. Deadlock: describe jstack / ART traces showing two Blocked threads and opposite monitors.

Java Memory Model: visibility, reorder, finals, DCL

The JMM defines when a write becomes visible and which reorderings are allowed. It is not "the heap layout". Without synchronization, the compiler, JIT and CPU may keep writes in registers, buffer them, or move them, as long as a single thread still sees its own program order (intra-thread semantics).

Analogy

Each thread has a private notebook (cache, registers) and there is a shared wall calendar (main memory). A plain write is a scribble in the notebook; other threads may still read last week's wall. A volatile write is pinning the page on the wall and making later readers look at the wall. Unlocking a monitor is publishing your notebook to the wall; the next lock holder must refresh. A final field is a form signed in the constructor that the JMM promises other threads will see fully filled in once they see the constructed object, if you did not leak this.

Happens-before (the edges you must name)

  • Program order in one thread: each action happens-before later actions in that thread.
  • Monitor unlock happens-before a later lock of the same monitor.
  • A volatile write happens-before a later read of that same variable.
  • A thread start happens-before the first action in the started thread.
  • The last action of a thread happens-before another thread successfully joins it.
  • An interrupt happens-before the interrupted thread detects it.
  • Happens-before is transitive.
  • Default initialization (zeros, nulls) happens-before any action.

A data race is two accesses to the same non-volatile location, at least one a write, not ordered by happens-before. Racy programs have almost no guarantees: you may see stale values, or torn values for long/double on some implementations.

final field safety

If a constructor writes final fields (and does not publish this early), then any thread that sees the object reference after construction sees those final fields initialized. That is why immutable objects can be published through a data race on the reference and still be safe to read. Do not call overridable methods or register listeners in the constructor; that leaks this and voids the guarantee.

Double-checked locking and why volatile is required

class Holder {
  private volatile Expensive inst;   // volatile is required
  Expensive get() {
    Expensive e = inst;              // local read; one volatile load on the fast path
    if (e == null) {
      synchronized (this) {
        e = inst;
        if (e == null) {
          inst = e = new Expensive();
        }
      }
    }
    return e;
  }
}

Without volatile, thread B can see a non-null inst whose constructor writes have not happened-before B's reads: a partially constructed object. The JMM allows the store of the reference to be reordered with stores to the fields. volatile on inst forbids that publication. Initialization-on-demand holder (a static nested class) is simpler for static singletons: class init is already synchronized by the VM.

Common pitfall Using DCL on a 32-bit long without volatile, or saying "volatile makes everything atomic". It makes that field's reads and writes ordered and visible; compound actions still need a lock or an atomic class.
Interview angle Define happens-before, give three edges, explain a data race, then walk DCL with and without volatile. Mention final-field safe publication as the reason immutable objects work.

Streams and Optional: when they hurt

Streams and Optional are library features, not a second language. Interviews punish tutorial dumps ("map, filter, reduce") and reward judgement: allocation, laziness, parallelism and null discipline.

Analogy

A stream is a conveyor: you describe stations (map, filter) and the items roll through once, lazily, when a terminal operation starts the belt. Optional is a labelled box that is either empty or holds one item, so you stop using null as "maybe". Putting a conveyor in a tight loop that already has an array index is hiring a construction crew to move three boxes across a room.

Streams that cost more than they save

  • Tiny collections: the pipeline objects, lambdas and boxing dwarf the work. A for-loop is clearer and faster.
  • Per-element boxing: Stream<Integer> over primitives; use IntStream or arrays.
  • parallel() on a tiny or I/O-bound stream, or on a pool already saturated. Parallel streams use the common ForkJoinPool and can stall unrelated work.
  • Side effects in map/filter: order and splitting become undefined; debugging dies.
  • Repeated pipelines over the same data when one loop could compute several aggregates.
  • Holding a stream: it is single-use; a second terminal operation throws.

Prefer streams when the pipeline is the domain language (filter, group, flat-map nested structures) and the size justifies it. State complexity: a well-written stream is still O(n); it is not free parallelism.

Optional without the religion

Optional is a return type for "value or absent" on a method that would otherwise return null. It is not a field type for every class (allocation, serialization pain). It is not a method parameter (the caller already knows; use overloading or two methods). Never Optional.get() without a guard; use orElse, orElseGet, orElseThrow, ifPresent. orElse(compute()) always computes; orElseGet(this::compute) is lazy. Do not wrap an Optional in another Optional. On Android, Optional is fine on modern ART but still not a Binder type.

Interview angle When would you refuse a stream? Name boxing, parallel-on-common-pool, and side effects. For Optional, say return type yes, field/parameter no, and the orElse eager trap.

Serialization, cloning, defensive copies

Java serialization (Serializable, ObjectOutputStream) is a hidden public constructor. It bypasses your constructors, participates in gadget attacks when the classpath is hostile, and is a poor IPC format. On Android, prefer Parcelable for Binder and a versioned explicit format (protobuf, JSON you control) for disk. See Binder and AIDL for Parcel rules.

Analogy

Serialization is vacuum-packing a whole desk, including drawers you forgot, and shipping it. Years later someone unpacks it in a different office (a new class version) and the chair legs do not fit. A defensive copy is photocopying a form before you file it so the visitor cannot scribble on your only copy. clone is a photocopier that by default copies the folder tabs but still points at the same papers inside (shallow).

Serialization pitfalls

  • serialVersionUID: if you do not set it, the compiler's default changes when you add a field; old bytes fail. Set it and document compatible changes.
  • Insecure defaults: deserializing untrusted bytes can call unexpected constructors via readObject gadgets. Do not deserialize from the network without a filter (JEP 290) and a tight allow-list.
  • No constructor: invariants in constructors do not run. Implement readObject and re-validate; consider the serialization proxy pattern (write a dedicated DTO, read it back into a real object).
  • Transient fields are skipped; you must restore them or document defaults.
  • Inheritance: a non-serializable superclass needs an accessible no-arg constructor or deserialization fails.
  • Not for Binder: reflection, temporary objects, huge graphs. Use Parcelable.

Cloning vs defensive copies

Object.clone is a field-for-field bitwise copy of the object header's payload. Nested mutable objects are shared. A copy constructor or builder copies what you name. Defensive copies: copy mutable arguments on the way in and mutable internals on the way out so callers cannot break immutability. If you store a Date or an array, copy it. Unmodifiable wrappers (List.copyOf, Collections.unmodifiableList) still share the backing store unless you copy first.

public Period(Date start, Date end) {
  this.start = new Date(start.getTime());
  this.end = new Date(end.getTime());
  if (this.start.after(this.end)) throw new IllegalArgumentException();
}
public Date start() { return new Date(start.getTime()); }
Common pitfall Returning Collections.unmodifiableList(internal) and later mutating internal, or accepting a list and storing it without copying. The "immutable" API was a suggestion.
Interview angle Why serialization skips constructors, why Parcelable exists on Android, and how you would copy a class with a mutable list field. Mention serialVersionUID and filters if they push security.

JNI from the Java side

A native method has no Java body. The VM looks up a C function when the class is linked (or when you register it). You load the library that contains that function with System.loadLibrary("foo"), which maps libfoo.so on Android/Linux (the lib prefix and .so suffix are added). Static initializers of the class that declares the natives are the usual place to load, so the first use cannot race a missing library.

Analogy

A native method is a door in the Java building that opens onto the C workshop. System.loadLibrary is unlocking the workshop. The default name on the door is a long brass plate (Java_com_example_Foo_bar). RegisterNatives is hanging your own sign so the plate can be short or the function can be hidden. What happens inside the workshop (local refs, GetStringUTFChars, exceptions) is the C page: C language. This page stops at the door.

public final class Crypto {
  static { System.loadLibrary("crypto_jni"); }
  public static native byte[] sha256(byte[] in);
}
// C side (not shown): JNIEXPORT jbyteArray JNICALL
// Java_com_example_Crypto_sha256(JNIEnv*, jclass, jbyteArray);

What Java-side interviews expect

  • Declaration: native, no body; can be static or instance. Overloads get extra name mangling (signatures in the symbol).
  • Load: loadLibrary vs load(absolutePath). Failure is UnsatisfiedLinkError at load or at first call if the symbol is missing.
  • RegisterNatives: the C side can bind function pointers; useful for obfuscation and for avoiding exported symbols. Java still just calls the native method.
  • Types: primitives map directly; objects are opaque handles. Arrays and strings are objects. You do not dereference them in Java as C pointers.
  • Exceptions: a native method may throw (the C side calls Throw); Java sees a normal exception. Pending JNI exceptions must be cleared on the C side before further JNI calls.
  • Threads: a Java thread that calls native is the same OS thread. Native code that calls back into Java must attach if it is not already a Java thread.

ART's moving GC means a raw pointer obtained in C to a Java array is only valid in a critical/pin region or after a copy. That is why the C page matters on Android even when your Java looks correct. C++ JNI wrappers are a different language layer; see C++ if the native side is C++.

Interview angle Write the Java declaration and the static load. Name UnsatisfiedLinkError causes (library path, ABI, missing symbol, load order). Do not invent C code here; point at C language.

Android Java: ART, dex, Binder types, the main thread

This is Java as it actually runs on a phone. It is not a second copy of AMS, WMS or Zygote service lists; those live on Android frameworks. IPC machinery lives on Binder and AIDL. Here: the runtime, the file formats, the types that cross Binder, and why the UI thread may not block.

Analogy

HotSpot is a factory that takes crates (.class) and builds machines as orders arrive (JIT). ART is a factory that also pre-builds popular machines overnight (dex2oat, profiles) because the shop floor (a phone) cannot spend a second compiling when the user taps. Dex is the crate format that factory accepts. The main thread is a single cashier; if they walk to the warehouse (disk, a slow Binder call), the line of customers (frames, input) does not move, and after a few seconds the store is declared unresponsive (ANR).

ART and dex / odex / vdex

The APK carries dex (Dalvik executable): a register-based bytecode derived from Java bytecode via d8/r8. vdex stores verified dex so verification is not repeated. odex / oat is the AOT output of dex2oat (native code plus metadata). A .art image is a pre-initialized heap of classes (boot image, sometimes app images) that Zygote maps. Compiler filters (verify, quicken, speed, speed-profile) trade install time, disk and CPU for runtime speed. JIT still compiles hot methods; profiles from production guide the next dexopt. First boot after OTA is slower because dexopt reruns; see frameworks for PMS/installd.

.java / .kt --> javac/kotlinc --> .class --> d8/r8 --> classes.dex (in APK)
                                                      │
install / idle / profile  --dex2oat-->  .vdex (verified) + .odex/.oat (compiled)
Zygote maps boot .art image; app process inherits it copy-on-write

Handler and Looper (brief)

The main thread is not a busy loop of your code. ActivityThread.main prepares a main Looper and calls Looper.loop(), which blocks in epoll until a Message is due, then dispatches to a Handler. Any thread may post; only the Looper thread runs the callback. Incoming Binder calls run on the Binder pool, not on that Looper. Full queue, sync barriers and Choreographer: Android frameworks.

Boxed types on Binder; Parcelable vs Serializable

AIDL and Parcel prefer primitives, String, arrays, List/Map of supported types, other Binders, file descriptors, and Parcelable. A boxed Integer or Boolean is an object: it can be null, it allocates on both sides, and it is easy to confuse with a primitive default. A primitive boolean cannot express "unset"; a boxed Boolean can, and then the receiver NPEs on auto-unbox. Do not send boxed types in a hot Binder path. Parcelable is explicit flatten/unflatten, no reflection, intended for IPC. Serializable is slow, reflective, and not a Binder-friendly format. Parcel is not a disk format.

Why the main thread cannot block

Input, lifecycle, traversals and most component callbacks are messages on the main Looper. If one message runs too long (disk, network, a synchronous Binder call to a stuck service, a lock held by a background thread doing I/O), later messages miss their deadlines. Typical ANR timeouts: input about 5 s, broadcast 10 s, service 20 s. The idle Looper.loop itself is asleep in the kernel and is not an ANR. Move work off the main thread; post results back with a Handler. Do not recopy AMS/WMS here.

Common pitfall A non-static inner Handler that outlives the Activity (leak), or doing a synchronous Binder call from the UI thread to a HAL that can block. Both are Java-level mistakes with framework symptoms.
Interview angle Dex vs oat vs vdex in one diagram, then "which thread runs my AIDL stub?" (Binder pool), then why boxed Boolean on Binder is a null trap, then ANR as a missed main-thread deadline. Stop before designing ATMS.

Common interview puzzles

These are the short items that decide a screening loop. Each one has a one-line trap and a precise fix. If you only revise one section the night before, use this plus quick revision.

Analogy

These puzzles are card tricks. The interviewer shows a familiar deck (a HashMap, a String, a thread) and asks you to name the hidden card (treeify 64, intern pool, start vs run). The trick stops being magic once you have seen the move; that is what this section is for.

HashMap treeify

Chain length 8 and table capacity 64. Below 64, resize instead of treeify.

String intern

Literals are pooled. new String("a") is not. Intern lives on the heap. Do not intern unbounded input.

String immutability

Sharing is safe; hash can be cached; the pool works. + in a loop is not.

Overload vs override

Overload is compile-time, same name, different signature. Override is runtime dispatch, same signature, subclass.

Pass-by-value

The reference is copied. The callee can mutate the object, not the caller's variable.

try / finally / return

A return in finally wins and drops the pending exception or earlier return.

start vs run

run() is a method on this thread. start() creates an OS thread that then calls run.

Pass-by-value of references

void clear(List<String> xs) { xs.clear(); }      // caller sees empty list
void rebind(List<String> xs) { xs = new ArrayList<>(); }  // caller unchanged
void swap(Integer a, Integer b) { /* cannot swap caller vars; Integer is immutable anyway */ }

Java never has C++-style reference parameters. Reassigning a parameter does nothing to the caller. Mutating the object does. Primitives are copied. This is the same rule as in DSA whiteboard code when you pass an array: the array object is shared; the variable is not.

try / finally / return

int f() {
  try { return 1; }
  finally { return 2; }   // f() is 2; the 1 is discarded
}
int g() {
  try { throw new RuntimeException("x"); }
  finally { return 0; }   // exception is swallowed; g() is 0
}

Overload vs override

Which overload runs is chosen at compile time from the static types of the arguments. Which override runs is chosen at runtime from the dynamic type of the receiver. A method is an override only if the signature matches (after erasure) and you intended it; @Override is mandatory in serious code. Return types may be covariant. Private, static and constructors are never overridden (static hides).

Interview angle They will ask you to predict printed output. Talk while you trace: static vs dynamic type, intern pool, cache range −128..127, finally-return, and whether start was called. Then volunteer treeify 8 and 64 so they do not have to prompt.

Quick revision

  • Java is the language; HotSpot is a JVM; ART is Android's runtime. Kotlin compiles to the same bytecode family; this page is Java.
  • javac emits class files (bytecode + constant pool), not machine code. ART's on-disk code is dex, then oat/vdex via dex2oat.
  • A class is (binary name, defining loader). Parent delegation asks the parent first so core types cannot be spoofed.
  • Initialization order: superclass statics, this statics, superclass instance + ctor, this instance + ctor. Compile-time constants do not trigger init.
  • Stack holds frames (primitives and references). Heap holds objects. Class metadata is Metaspace / ART native, not PermGen.
  • Object header: mark word + class pointer (+ array length). Compressed oops and compressed class pointers are optional 32-bit packs.
  • HotSpot: young/old (G1 regions). ART: Concurrent Copying, moving, Zygote image space kept stable for copy-on-write.
  • Escape analysis may scalar-replace a non-escaping new; do not claim every allocation hits the heap.
  • GC roots: locals, statics, JNI globals, thread/monitor internals. Weak / soft / phantom are not roots in the strong sense.
  • STW pauses mutators; concurrent GC still has short safepoint pauses. finalize is obsolete; use Cleaner.
  • equals is reflexive, symmetric, transitive, consistent, false for null. Equal objects must share a hashCode.
  • Never mutate a key while it sits in a hash map. Prefer immutable keys.
  • String is immutable; intern is a heap pool; do not intern unbounded input. StringBuilder for loops; skip StringBuffer.
  • Integer cache is typically −128..127. == on boxed integers outside that range is a bug.
  • clone is shallow; prefer a copy constructor. compareTo must agree with equals in sorted collections.
  • Composition over inheritance unless Liskov holds. Default methods evolve interfaces; they do not add instance fields.
  • Inner and anonymous classes hold this$0. Static nested + WeakReference for Android Handlers.
  • Records and sealed types are newer Java; mention them, do not assume every Android API level has them.
  • Generics erase. PECS: producer extends, consumer super. Heap pollution shows up later as ClassCastException.
  • Arrays are reified and covariant; generic lists are invariant and erased.
  • ArrayList grows about 1.5×; first add uses capacity 10. Prefer ArrayDeque over Stack/LinkedList for queues.
  • HashMap treeifies a bin at chain 8 only if table capacity is at least 64; otherwise it resizes.
  • HashMap is not thread-safe. Hashtable is coarse synchronized. ConcurrentHashMap is the concurrent map; no nulls.
  • Fail-fast iterators use modCount. CHM iterators are weakly consistent.
  • Checked exceptions must be declared or caught. Error is for the VM. Do not swallow InterruptedException.
  • try-with-resources closes in reverse and records suppressed exceptions. A return in finally discards the pending throw or return.
  • Happens-before: program order, unlock/lock, volatile write/read, start, join. A data race has almost no guarantees.
  • volatile is visibility and ordering of that field, not atomic i++. DCL requires volatile on the instance field.
  • final fields are safely published after construction if you do not leak this.
  • wait only inside the monitor, always in a while that re-checks the condition.
  • Latch = one-shot N-to-zero. Barrier = reusable "all arrive". Semaphore = permits. Lock + Condition = multiple wait-sets.
  • ThreadPoolExecutor: core, then queue, then max, then reject. An unbounded queue means max is never used.
  • Deadlock: circular lock wait. Livelock: busy yielding. Always ThreadLocal.remove on pooled threads.
  • CompletableFuture: compose with thenCompose; do not block the common pool with I/O.
  • Streams hurt on tiny data, boxed primitives, parallel() on the common pool, and side-effecting map/filter.
  • Optional is a return type, not a field or parameter. orElse(x) is eager; orElseGet is lazy.
  • Java serialization skips constructors and is a security hazard. On Android IPC use Parcelable, never Parcel-on-disk.
  • Defensive-copy mutable args and returns. Unmodifiable wrappers still share the backing list unless you copy.
  • native + System.loadLibrary on the Java side; C-side JNI is C language.
  • ART: dex in the APK, vdex verified, oat/odex compiled. Main thread is a Looper; block it and you ANR.
  • Binder calls run on the Binder pool. Boxed Boolean/Integer on Binder allocate and can be null.
  • Java is pass-by-value of the reference. Rebinding a parameter does not change the caller; mutating the object does.
  • Overload = compile-time, static types. Override = runtime, receiver type. Thread.start creates a thread; run does not.
  • String == is identity. Use equals unless both sides are interned by you on purpose.

Glossary

AOT
Ahead-of-time compilation to native code (dex2oat on Android, jaotc/Graal on some JVMs) before or during install, not only at first execution.
ART
Android Runtime: the VM that executes dex-derived code, with its own GC (Concurrent Copying on modern releases), JIT and profile-guided AOT.
Bytecode
The stack-machine instructions in a class file (or the register-machine instructions in dex). Not CPU machine code until a JIT or AOT compiler emits it.
CAS
Compare-and-swap: an atomic hardware update used by java.util.concurrent.atomic and ConcurrentHashMap.
Checked exception
An Exception that is not a RuntimeException; the compiler requires throws or a catch.
Class file
The binary javac emits: header, constant pool, fields, methods and bytecode.
Classloader
The object that locates bytes and defines a Class. Identity of a class includes its defining loader.
Cleaner
A post-mortem cleanup facility built on phantom reachability; the replacement for finalize.
Compressed oops
HotSpot packing of heap references into 32 bits when the heap is within a limited range (typically under ~32 GB).
Concurrent Copying (CC)
ART's moving collector: bump allocation, concurrent mark and copy, short STW root pauses.
ConcurrentHashMap
A thread-safe hash map using CAS and per-bin coordination; null keys and values are forbidden.
Condition
A wait-set attached to a Lock, analogue of wait/notify with multiple named queues.
Constant pool
The class-file table of literals and symbolic references that bytecode indexes.
CountDownLatch
A one-shot synchronizer that blocks until a count reaches zero.
CyclicBarrier
A reusable barrier: N parties must await before any proceeds; optional barrier action.
Dalvik / dex
Dex is the APK bytecode format (historically Dalvik). Modern devices run it on ART, not the old Dalvik VM.
Data race
Conflicting accesses to a non-volatile location with no happens-before order; the JMM gives almost no guarantees.
Default method
An interface method with a body, used to evolve APIs without breaking existing implementors.
Defensive copy
A copy of mutable state taken on the way in or out so callers cannot mutate your internals.
Double-checked locking
A lazy-init pattern that checks a reference outside and inside a lock; the reference field must be volatile.
Eden
The HotSpot young-generation space where most new objects are allocated before a minor GC.
Erasure
The compile-time-only nature of Java generics: List<String> is a raw List at runtime.
Escape analysis
A JIT analysis that may allocate an object on the stack or in registers if it never escapes the method or thread.
ExecutorService
An API to submit tasks to a thread pool and obtain Futures, instead of constructing Threads by hand.
Fail-fast iterator
An iterator that throws ConcurrentModificationException if it sees a modCount change it did not make.
Final-field safety
The JMM guarantee that a fully constructed object's final fields are visible to other threads that see the reference, if this was not leaked.
Finalize
An obsolete Object method queued for a finalizer thread; unpredictable, can resurrect objects; do not use.
ForkJoinPool
A work-stealing pool for divide-and-conquer tasks; hosts the common pool used by parallel streams.
GC root
A reference the collector starts from: locals, statics, JNI globals, and certain VM handles.
G1
Garbage-First, HotSpot's default region-based collector since Java 9.
Happens-before
The JMM partial order that defines visibility and ordering between actions in different threads.
HashMap treeify
Conversion of a long collision chain into a red-black tree when length ≥ 8 and table capacity ≥ 64.
Heap pollution
A generic variable whose runtime contents violate its static type, usually via raw types; fails later on a cast.
HotSpot
The usual desktop and server JVM: classloading, C1/C2 JIT, Metaspace, and a choice of collectors.
Intern pool
The set of canonical String instances returned by literals and String.intern(); on the heap since Java 7.
JIT
Just-in-time compilation of hot bytecode to native code at run time (HotSpot C1/C2; ART JIT).
JNI
Java Native Interface: native methods on the Java side and a C API on the other; see C language.
JMM
Java Memory Model: the language rules for visibility, atomicity of volatiles, and allowed reorderings.
JVM
The virtual machine specified by the Java Virtual Machine Specification; HotSpot is one implementation.
Metaspace
Native memory where HotSpot stores class metadata after the removal of PermGen in Java 8.
Monitor
The intrinsic lock associated with every Java object; used by synchronized, wait and notify.
oat / odex
ART's compiled native code and metadata produced by dex2oat from dex.
Optional
A container for a value that may be absent; intended as a method return type, not a field or parameter.
Parent delegation
The classloader rule: ask the parent loader before defining a class yourself.
Parcelable
Android's explicit flatten protocol for Binder and Intents; not Java serialization and not a disk format.
PECS
Producer Extends, Consumer Super: the wildcard rule for generic APIs.
PermGen
The old HotSpot space for class metadata, removed in Java 8; do not use it as an ART story.
Phantom reference
A reference that does not keep the object alive and whose get is always null; used for cleanup after death.
Record
A transparent, final, shallowly immutable Java class with generated accessors and equality.
ReentrantLock
An explicit lock in java.util.concurrent that the same thread may acquire again; supports fairness and conditions.
Safepoint
A point where the VM can stop a thread for GC or other VM operations.
Sealed type
A class or interface that permits only a listed set of subtypes (newer Java).
Semaphore
A counter of permits used to bound concurrent access to a resource.
Soft reference
A reference cleared under memory pressure, used for caches that should yield to the collector.
STW
Stop-the-world: mutator threads paused at safepoints for a GC phase.
ThreadLocal
A per-thread variable; must be removed on pooled threads or it leaks across tasks.
vdex
Verified dex: ART's cache of verification results so boot and load do not re-verify every time.
Volatile
A field modifier that inserts happens-before between a write and a later read of that field, and forbids certain reorderings.
Weak reference
A reference that does not prevent collection; cleared when the object is no longer strongly reachable.
Work stealing
Idle ForkJoin workers take tasks from other workers' deques; the scheduling model of ForkJoinPool.
Zygote
The warm ART process that preloads classes and forks app processes so they share memory copy-on-write.

Interview questions

Fundamentals

What is the difference between Java, the JVM, the JRE and the JDK?

Java is the language and its standard library contracts. The JVM is the specification of a virtual machine that loads class files, executes bytecode and manages a garbage-collected heap. The JRE is a product you run programs on (a JVM plus class libraries). The JDK is the JRE plus tools: javac, javap, jar, a debugger. Saying "I installed Java" without distinguishing JDK from JRE is incomplete.

What is ART, and how does it differ from a desktop HotSpot JVM?

ART is Android's runtime. It still executes a Java-like language, but apps ship dex, compilation is dex2oat plus a JIT and profiles, the collector on modern releases is moving Concurrent Copying, and processes are forked from Zygote so a warm heap and boot image are shared copy-on-write. HotSpot loads .class files, uses Metaspace, and typically G1 (or Parallel/ZGC). ART is not "HotSpot with a different name".

How does Kotlin relate to Java on Android?

Kotlin is a different language that compiles to JVM bytecode and then to dex on Android. It interops with Java types and the same ART heap, GC and Binder stubs. This page is Java: memory, concurrency and collections do not change because the source was .kt. A Kotlin-only syntax lecture is the wrong answer unless they asked for it.

What does javac produce, and what actually runs?

javac produces class files: constant pool plus bytecode. Nothing CPU-native is required at that step. HotSpot interprets and JIT-compiles bytecode. On Android, d8/r8 turn class files into dex; ART interprets, JITs and/or runs oat code from dex2oat.

What is Java bytecode?

A portable instruction set for a stack machine specified by the JVM spec: loads, stores, arithmetic, invokes, object allocation. It is verified at link time. It is not ARM or x86 machine code. Dex is a related but register-based format used on Android.

What is in a .class file?

A magic/version header, the constant pool (literals and symbolic refs), access flags, this/super/interfaces, field and method descriptors, and attributes (Code with bytecode, Exceptions, Signature for generics, and so on). javap -c -v dumps it.

What is a classloader?

An object that finds class bytes and calls into the VM to define a Class. A class's identity is its binary name plus its defining loader. Two loaders can each define com.example.Foo; those are different types and a cast between them fails.

Explain parent delegation.

A loader asks its parent to load a class before defining it itself. Bootstrap loads java.lang.*; the application loader loads your classpath only if parents do not. This keeps one String and prevents a JAR from replacing core types. Break the model only for plugins, and then watch for leaks.

What is the initialization order of a Java class and a new instance?

First the superclass is initialized (its statics), then this class's static fields and static blocks in source order. On new: the super constructor chain runs, then this class's instance fields and instance blocks in source order, then the rest of the constructor. Compile-time constants (static final primitives and strings initialized with literals) can be inlined and do not trigger class init.

Static vs instance members?

Static fields and methods belong to the class; there is one copy per class (per loader). Instance fields belong to each object. Static methods cannot use this or instance fields. Overriding is an instance-method concept; a static method with the same signature in a subclass hides, it does not override.

Where do locals, objects and references live: stack vs heap?

Each thread's Java stack holds frames: local primitives and references. Objects and arrays live on the heap. Two locals can point at one heap object. Class metadata lives in Metaspace (HotSpot) or native alloc (ART), not in an ordinary Java object slot.

What is an object header?

The fixed prefix of every heap object: typically a mark word (hash, age, lock bits) and a class pointer, then fields, then alignment padding. Arrays add a length. Compressed oops / compressed class pointers may shrink the pointer words. The header is why a one-boolean object is still many bytes.

What is garbage collection in one paragraph?

The VM finds objects not reachable from GC roots (locals, statics, JNI globals, some VM handles) and reclaims them. How it does that (copying, marking, concurrent vs STW) depends on the collector. Java has no deterministic destructor for ordinary objects.

Young generation vs old generation?

On HotSpot, most objects die young. A minor collection copies or discards Eden/survivors cheaply. Objects that live long enough are promoted to old space, collected less often. G1 uses regions with young/old roles. ART's CC is also generational in spirit (bump-allocate young, copy survivors) but is not "G1 Eden".

What is the difference between == and equals?

For references, == is identity: the same object. equals is the value contract if the class overrode it (and Object.equals is identity). For primitives, == is the only comparison. Always use equals for String and boxed numbers unless you are discussing intern or the Integer cache on purpose.

State the equals / hashCode contract.

equals must be reflexive, symmetric, transitive, consistent, and false for null. If a.equals(b) then a.hashCode() == b.hashCode(). Unequal objects may share a hash (collisions). A mutable key whose equals-fields change while it is in a HashMap is lost.

Why override both equals and hashCode?

Hash-based collections find a bin with the hash, then confirm with equals. If two equal objects have different hashes they never meet. If you override only equals, you inherit Object.hashCode (identity), so equal value objects miss each other in a HashSet.

Why is String immutable?

The character data cannot change after construction. That makes sharing and the intern pool safe, lets hashCode be cached, and makes strings usable as map keys. Security-sensitive APIs can treat a string as a snapshot. The cost is that concatenation creates new strings; use StringBuilder in loops.

What does String.intern() do?

It returns the canonical pooled instance equal to the receiver. Literals are interned automatically. The pool is on the heap (since Java 7, not PermGen). Interning every request or UUID is a leak. == after intern is identity of the pool entry, not a general string test.

String vs StringBuilder vs StringBuffer?

String is immutable. StringBuilder is a mutable buffer for building text on one thread. StringBuffer is the old synchronized builder; do not use it in new code. A single + expression is rewritten by the compiler; a + inside a loop is not.

What are Java's access modifiers?

private: same class. Package-private (no modifier): same package. protected: package plus subclasses. public: everywhere. A subclass in another package does not see package-private members. Nested classes can see the outer class's privates (the compiler emits accessors).

Inheritance vs composition: when do you pick each?

Inherit when you have a true subtype and callers should use the subtype wherever the parent is accepted (Liskov). Compose when you want reuse of behaviour: hold a helper and delegate. Inheritance of implementation couples you to the parent's internals and is the wrong tool for "I needed one method".

Abstract class vs interface?

An abstract class can hold fields, constructors and protected state; you get only one superclass. An interface defines a capability; a class may implement many. Modern interfaces may have default, static and private methods, but still no instance fields. Share a skeleton with an abstract class; share a role with an interface.

Overload vs override?

Overload: same name, different parameter types; resolved at compile time from the static types of the arguments. Override: same signature in a subclass; resolved at runtime from the dynamic type of the receiver. Static methods hide; they do not override. Use @Override so a signature mismatch fails to compile.

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

Pass-by-value. For objects, the value is a copy of the reference. The callee can mutate the object. Reassigning the parameter does not change the caller's variable. Primitives are copied. There are no C++-style reference parameters.

Checked vs unchecked exceptions?

Checked: Exception minus RuntimeException; you must catch or declare them (I/O, many library failures). Unchecked: RuntimeException and Error; used for bugs and VM failures. The compiler does not force you to handle unchecked types. InterruptedException is checked because cancellation is part of the contract.

Error vs Exception?

Both extend Throwable. Error means the VM or linkage is in a bad state (OutOfMemoryError, StackOverflowError, NoClassDefFoundError); catching it to continue is usually wrong. Exception is the application-level branch, including both checked types and RuntimeException.

What is try-with-resources?

A try that declares AutoCloseable resources and closes them in reverse order, even if the body throws. If close throws too, that exception is suppressed on the primary exception (getSuppressed()). It avoids the classic finally that hides the original failure.

List vs Set vs Map?

A List is ordered and allows duplicates; index access depends on the implementation. A Set forbids duplicates under equals (or identity for special sets). A Map is a key-to-value dictionary. Pick the interface by the contract, then the implementation by cost and concurrency.

ArrayList vs LinkedList?

ArrayList: contiguous array, O(1) index, O(1) amortized add at the end, O(n) insert in the middle, cache-friendly. LinkedList: O(1) splice at a known node, O(n) index, pointer-heavy, rarely faster in real apps. Prefer ArrayList; use ArrayDeque for a queue or stack.

HashMap vs Hashtable vs ConcurrentHashMap at a basic level?

HashMap is unsynchronized, allows one null key, fail-fast iterators. Hashtable is an old synchronized map, no nulls, coarse lock. ConcurrentHashMap is the modern concurrent map: no nulls, concurrent reads, weakly consistent iterators. Do not share a HashMap across threads.

HashSet vs TreeSet?

HashSet is a HashMap of keys: O(1) average, no order. TreeSet is a red-black tree: O(log n), sorted by Comparable or a Comparator, and compareTo must agree with equals or the set will lie. Use LinkedHashSet when you need insertion order.

synchronized vs volatile?

synchronized is mutual exclusion plus a happens-before on unlock/lock of that monitor. volatile is a lighter happens-before on each write/read of that field and forbids some reorderings; it does not make i++ atomic and does not protect a whole invariant across several fields unless you design it that way.

Thread.start() vs run()?

run() is an ordinary method: it executes on the caller. start() asks the VM to create an OS thread that will then invoke run. Calling run by mistake is a classic "why is this single-threaded" bug. Calling start twice throws IllegalThreadStateException.

What is a deadlock?

Two or more threads each hold a resource the other needs and wait forever. The usual Java shape is two monitors acquired in opposite order. Diagnose with thread dumps: Blocked threads and the monitors they hold vs want. Prevent with a global lock order, try-lock, or fewer locks.

What is Optional for?

A return type that makes "absent" explicit instead of null. Use orElse, orElseGet, orElseThrow, ifPresent. It is a poor field type (allocation, serialization) and a poor parameter type (the caller already knows). Never call get() blindly.

What is a Java Stream, in one paragraph?

A lazy pipeline of intermediate operations (filter, map) that runs when a terminal operation (collect, reduce, forEach) is invoked. It is not a data structure and it is single-use. Use it when the pipeline is clearer than a loop and the size justifies the allocation; see when they hurt in the streams section.

How do you declare and load a native method from Java?

Declare native with no body. In a static initializer call System.loadLibrary("foo") to load libfoo.so (Android/Linux). Missing library or missing symbol is UnsatisfiedLinkError. The C implementation and JNIEnv rules are on C language.

Why must the Android main thread not block?

The main thread is a Looper that must dispatch input, lifecycle and frames. A long message (disk, network, a stuck synchronous Binder call) delays everything behind it and becomes an ANR after a timeout (input ~5 s, broadcast 10 s, service 20 s). The idle loop itself sleeps in epoll and is not an ANR. Details: Android frameworks.

Parcelable vs Serializable, in one answer?

Parcelable is explicit write/read for Android IPC: fast, no reflection, not a stable disk format. Serializable is Java reflection-based flattening: convenient, slow, constructor-skipping, a security hazard on untrusted bytes. Use Parcelable (or AIDL-generated parcelables) on Binder; see Binder and AIDL.

Going deeper

Walk through load, link and initialize.

Load: the classloader finds bytes and defines a Class. Link: verify bytecode, prepare statics to defaults, resolve symbolic refs (often lazily). Initialize: acquire the class init lock, initialize the superclass, run static initializers in order. The JLS makes initialization thread-safe: other threads wait rather than seeing a half-initialized class.

What happens if two classloaders each load com.example.Foo?

You get two Class objects. A Foo instance from loader A is not assignment-compatible with loader B's Foo. Passing it across the boundary throws ClassCastException. This is how plugin isolation works and how "same name, different type" bugs appear in app-in-app or hot-fix loaders.

What is escape analysis?

The JIT proves an object does not escape the method or thread (no store to a heap field, no unknown call). It may then scalar-replace the object: fields become registers or stack slots and the heap allocation disappears. It is a compiler optimization, not a language guarantee. Profile before rewriting code to "avoid allocations" the JIT already removed.

Sketch HotSpot's generational heap.

Young: Eden plus two Survivor spaces; most objects die in Eden; survivors are copied and aged; promotion to old after a threshold. Old: tenured objects, collected less often. G1 uses equal-sized regions that play young or old roles, plus a concurrent mark. Metaspace is native class metadata, not a Java heap generation. PermGen is gone as of Java 8.

STW vs concurrent GC?

STW stops all mutators at safepoints for a phase (root scan, some compacting). Concurrent collectors do most marking and relocation beside the application but still pause briefly. "Concurrent" is not "zero pause". A moving concurrent collector (ART CC, ZGC) must still coordinate with mutators when it updates references.

finalize vs Cleaner (and phantom references)?

finalize is queued on a special thread at an unspecified time, can resurrect the object, and can deadlock the finalizer thread. Cleaner (or a PhantomReference plus a reference queue) runs after the object is phantom-reachable and cannot resurrect it. Use Cleaner for native resources; prefer explicit close and try-with-resources first.

Weak vs soft vs phantom references?

Weak: cleared when only weakly reachable; next GC can drop the referent. Soft: kept longer, cleared under memory pressure; cache-like. Phantom: get() is always null; enqueued after the object is otherwise unreachable, for cleanup. None of them are strong roots. WeakHashMap is weak on keys only.

Name the GC roots you would list on a whiteboard.

Local variables and operand-stack slots of live frames; static fields of loaded classes; JNI global references; the running Thread objects and their monitors; a few VM-internal handles (including interned strings as a historical example). Everything else is kept only if a path from a root remains.

ART Concurrent Copying vs HotSpot G1, at interview depth?

G1 is a region-based collector with young collections and mixed old collections, default on server Java. ART CC is a moving collector tuned for phone heaps and UI jank: bump-pointer young allocation, concurrent copy, short STW. Zygote's image space is almost never compacted so COW pages stay shared. Do not describe Eden/Survivor spaces as the Android heap.

What replaced PermGen, and does ART have it?

Java 8 removed PermGen. Class metadata lives in Metaspace (native memory, can still OOM). ART never had PermGen; class data is native linear-alloc. Compressed class pointers are a HotSpot packing trick for the class word, not an Android interview requirement, but you may mention them as an optional HotSpot detail.

State the compareTo contract and its relationship to equals.

Antisymmetric: sgn(a.compareTo(b)) == -sgn(b.compareTo(a)). Transitive. Consistent. If you use the class in a SortedSet/SortedMap, compareTo == 0 should match equals, or the collection will treat unequal objects as the same key. A Comparator that returns 0 for unequal elements collapses them in a TreeSet.

How do you make a class immutable?

Make the class final (or seal it), fields private final, no mutators, defensive copies of mutable constructor args and of returned internals, and do not publish this from the constructor. Then equals/hashCode can use those fields safely. Records give a shallow version of this for simple carriers.

What are default methods, and why do they exist?

Interface methods with a body. They let you add API to an interface without breaking every implementor (evolution). They are not multiple inheritance of state: no instance fields. If two interfaces provide conflicting defaults, the class must override and pick. Abstract classes remain the tool for shared state.

What are sealed classes (mention-level)?

A class or interface that lists its permitted subtypes. The compiler can check that a switch is exhaustive. They are relatively new Java; Android API levels may not expose them to app source even if the toolchain knows them. Mention as a language feature, not as a platform guarantee.

Static nested vs inner vs local vs anonymous classes?

Static nested: no enclosing instance; a namespaced helper. Inner: hidden this$0; keeps the outer object alive. Local: named class inside a method; captures effectively-final locals. Anonymous: one-off inner class; same leak as inner. Android Handlers should be static nested plus a WeakReference.

What is a record?

A final, transparent carrier: a canonical constructor, accessors, and generated equals/hashCode/toString. Fields are shallowly immutable. It is not a substitute for a full domain object with invariants beyond what the constructor checks. Mention-level on older Android Java.

What is type erasure?

Generic type arguments exist for the compiler. At runtime a List<String> is a List; the compiler inserts casts. You cannot instantiate T, create T[], or overload two methods that erase to the same descriptor. Reflection sees raw types plus a Signature attribute if you ask for generic metadata.

What is PECS?

Producer Extends, Consumer Super. If you only read Ts, take Collection<? extends T>. If you only write Ts, take Collection<? super T>. If you read and write, use Collection<T>. ? extends T forbids add (except null); ? super T only lets you get as Object.

? extends vs ? super with a one-line example?

List<? extends Number> src can be a List<Integer>; you may read Number, you may not add an Integer. List<? super Integer> dest can be a List<Number>; you may add Integer, you cannot treat get as Integer.

What is heap pollution?

A variable whose static type is List<String> but whose runtime list contains a non-String, usually because of raw types or an unchecked cast. The add may succeed; a later get throws ClassCastException. Generic varargs (T...) allocate an erased array and are a common source; prefer List<T>.

Why are Java generics not reified? How do arrays differ?

The JVM was designed before generics; erasure kept migration compatible and avoided reified instantiations of every List<T>. Arrays store their component type at runtime (reified) and are covariant: a String[] is an Object[], so a bad store throws ArrayStoreException. Generic lists are invariant and erased.

How does ArrayList grow?

Empty lists share an empty array. The first add allocates capacity 10. Later growth is old + (old >> 1) (1.5×), then a copy. Amortized add is O(1). If you know n, pass it to the constructor. This is the same amortized idea as a doubling buffer, with a 1.5 factor instead of 2.

What is a fail-fast iterator?

On ArrayList, HashMap and friends, the iterator snapshots modCount. A structural modification (add/remove other than Iterator.remove) makes the next use throw ConcurrentModificationException. It is best-effort, including on a single thread. It is not a concurrency protocol. ConcurrentHashMap iterators are weakly consistent instead.

Collections.synchronizedMap vs ConcurrentHashMap?

The wrapper holds one lock around every method; compound actions (check then put) still need external synchronization, and iteration must lock the wrapper. CHM allows concurrent reads and finer-grained updates, forbids nulls, and offers atomic putIfAbsent/compute. Use CHM for concurrent maps; use the wrapper only when wrapping a non-concurrent map you do not control.

HashMap internals: when does a bin become a tree?

A bin treeifies when the chain length is at least 8 and the table capacity is at least 64. If the chain is long but capacity is under 64, HashMap resizes instead. Trees untreeify when they shrink to 6. Hash is mixed with h ^ (h >>> 16); index is (n-1) & h because n is a power of two. Load factor defaults to 0.75.

What does happens-before mean, and name four edges?

It is the JMM's visibility/order relation: if A happens-before B, B sees A's writes (and later actions). Edges: program order in one thread; unlock happens-before later lock of the same monitor; volatile write happens-before later read of that variable; thread start happens-before the first action in the child; last action happens-before a successful join. Transitive.

Write the correct wait loop and explain why.

synchronized (lock) { while (!condition) lock.wait(); }. You must hold the monitor. wait releases it and reacquires before returning. A while (not if) re-checks after spurious wakeup and after a notifyAll meant for a different predicate. notify wakes one; notifyAll wakes all.

Lock and Condition vs synchronized?

ReentrantLock can try-lock, lock interruptibly, and be fair. You can attach several Conditions (several wait-sets) to one lock, which is awkward with a single Object monitor. You must unlock in finally; the language will not do it. Prefer synchronized when those extras are unused.

CountDownLatch vs CyclicBarrier?

A latch is one-shot: threads wait until a count hits zero (start gate, or "N workers done"). A barrier is reusable: N parties await, then all proceed, optionally running a barrier action; then they can cycle. Do not use a latch for multi-phase algorithms; do not use a barrier as a one-shot completion flag.

What is a Semaphore for?

A bag of permits. acquire blocks until one is free; release returns one. Use it to bound concurrency (at most 3 downloads, at most 1 writer). It is not a mutex unless you treat it as a single permit and remember it is not reentrant in the monitor sense.

What is ExecutorService and why not new Thread each time?

A pool that accepts Runnable/Callable and returns Futures. Creating a thread per task is expensive, unbounded, and hard to cancel. Shut down the pool (shutdown / shutdownNow) or you leak threads. Prefer bounded queues and a rejection policy over a cached pool for server-like loads.

Explain ThreadPoolExecutor's constructor parameters and admission order.

Core size, max size, keep-alive, work queue, thread factory, rejected-execution handler. Order: if workers < core, add a worker; else offer the queue; if the queue is full and workers < max, add a worker; else the handler runs (abort, caller-runs, discard). An unbounded LinkedBlockingQueue never fills, so max is unused. SynchronousQueue is a handoff (cached-thread style).

How does ThreadLocal leak on a thread pool?

The value is stored in the worker thread, not in the task. The next task on that worker still sees it (wrong user, wrong Looper-adjacent state, retained Activity). Always remove() in finally. Android's Looper itself is a ThreadLocal; that is why each thread needs Looper.prepare().

CompletableFuture at interview depth?

A composable promise: supplyAsync, thenApply (map), thenCompose (flatMap), thenCombine, exceptionally, whenComplete. Pass an executor; the common ForkJoin pool is wrong for blocking I/O. get() blocks and wraps failures in ExecutionException. Do not get() on the Android main thread.

What is double-checked locking, and why volatile?

Check a cached instance without a lock; if null, lock, check again, construct. Without volatile on the field, another thread can see a non-null reference whose constructor stores have not happened-before its reads (partially constructed object). volatile publishes the object safely. A static holder class is simpler for static singletons.

What is final-field safe publication?

If a constructor writes final fields and does not leak this, any thread that later sees the object reference sees those finals initialized. That is why immutable objects can be published through a data race on the reference. Leaking this (registering a listener in the constructor) voids the guarantee.

When do Streams hurt?

Tiny collections (pipeline objects cost more than the work); boxed Stream<Integer> instead of IntStream; parallel() on small or blocking work (common ForkJoin pool); side effects inside map/filter; reusing a consumed stream. Prefer a loop when the pipeline is not the domain language.

Name four Java serialization pitfalls.

It skips constructors, so invariants must be re-checked in readObject. Untrusted bytes are a gadget/RCE risk; use filters. Default serialVersionUID breaks when you add a field. It is a bad Binder format (reflection, allocations). Prefer an explicit versioned format on disk and Parcelable on Android IPC.

What is a defensive copy?

Copy mutable arguments before storing them, and copy mutable internals before returning them, so the caller cannot change your object. List.copyOf after you still hold a mutable list is not enough if you mutate the original. Unmodifiable wrappers share the backing store.

Handler and Looper from the Java side?

A Looper is one-per-thread (ThreadLocal), looping on a time-ordered MessageQueue. A Handler bound to that Looper enqueues work; any thread may post, only the Looper thread runs it. The main thread's Looper is prepared in ActivityThread.main. Binder incoming calls are not on that Looper. Full detail: Android frameworks.

Why are boxed types on Binder a trap?

An Integer or Boolean is an object: extra allocations on both sides, and null is a legal value. Auto-unbox of null is NPE. A primitive cannot express "unset". Hot paths should use primitives or a proper Parcelable with an explicit presence flag. See Binder and AIDL.

What are dex, vdex and oat/odex?

Dex is the APK bytecode. Vdex caches verification. Oat/odex is dex2oat output: native code and metadata. A .art image is a prebuilt heap of classes Zygote maps. Compiler filters trade CPU, disk and install time. This is ART, not javac emitting machine code.

What does return inside finally do?

It becomes the method's return and discards any pending return value or exception from try/catch. That is why it is a puzzle and why it is forbidden in serious code. A throw from finally similarly hides the original exception unless you were using try-with-resources suppression.

What is the Integer cache, and when does == lie?

Autoboxed int values in a configured range (default −128..127) are interned. Integer a = 80; Integer b = 80; a == b is true; 200 is typically false. Always equals for boxed numbers. The high bound can be raised with a VM flag; do not depend on it.

Advanced

What are compressed oops and compressed class pointers?

HotSpot can pack heap references into 32 bits when the heap is within a limited range (typically under ~32 GB), scaling the pointer. Compressed class pointers pack the class-word the same way, pointing into a class space. They save memory and cache traffic. They are a HotSpot detail; ART has its own header packing. Mention as optional; do not invent them on Android.

What reorderings does the JMM allow without synchronization?

Within one thread, the appearance of program order for that thread's own reads and writes is preserved (intra-thread semantics). Across threads, independent accesses to non-volatile locations may be reordered, buffered, or optimized into registers. A reader may see stale values or, for non-volatile long/double, a torn value on some implementations. Only happens-before edges restore a cross-thread order.

List the happens-before edges you would recite in a senior loop.

Program order; monitor unlock → later lock; volatile write → later read of that field; Thread.start → first action in the child; last action → successful join; interrupt → detection of the interrupt; default zeroing of fields → any use; write of a final in a constructor → freeze at the end of construction for safely published objects; transitivity. Also the synchronizer edges documented for j.u.c (release/acquire on locks, latches, and so on).

Why is volatile required for DCL at the bytecode/JMM level?

Construction is several stores (header, fields). The store of the reference to inst can be reordered with those field stores in a racy publication. Thread B can load a non-null inst and then read default field values. A volatile store of inst cannot be reordered with the preceding constructor writes in a way that breaks that publication, and the matching volatile load acquires them. A local variable on the fast path still does one volatile read.

How does ForkJoinPool differ from a typical ThreadPoolExecutor?

ForkJoin is for divide-and-conquer: tasks spawn subtasks; idle workers steal from others' deques (work stealing). The common pool also runs parallel() streams. A ThreadPoolExecutor is a producer-consumer pool with an external queue and a rejection policy, better for independent I/O or heterogeneous tasks. Blocking inside ForkJoin tasks without ManagedBlocker can stall the pool.

Deadlock vs livelock vs starvation?

Deadlock: circular wait, no progress, threads typically Blocked on monitors. Livelock: threads keep reacting and retrying, CPU is used, no useful progress (two people stepping aside forever). Starvation: a thread never obtains a lock or CPU because of unfairness, priority, or a flood of other work. Fair locks reduce starvation and can reduce throughput.

What do the atomic classes buy you?

AtomicInteger, AtomicReference, LongAdder and friends expose CAS and atomic updates without a monitor. They are for a single variable (or a marked reference). They do not make a multi-field invariant atomic. incrementAndGet is atomic; two separate atomics are not a transaction. Use them for counters and lock-free flags, not as a substitute for a lock around a map plus a list.

What is the InterruptedException contract?

Blocking methods that throw it have cleared the interrupt status. If you cannot handle cancellation, catch, restore Thread.currentThread().interrupt(), and exit or rethrow. Swallowing it makes the next blocking call ignore cancellation. Do not use interrupt as a normal boolean if you can use an explicit flag plus interrupt together.

How do classloader leaks happen?

A class holds statics; a class is reachable from its loader; a loader is reachable from every class it defined. If any thread, cache, or JNI global still points at one object from a discarded plugin APK, the entire loader and all its classes stay alive. Android hot-fix and webview-style loaders are the usual story. Metaspace / native class OOM is the symptom on HotSpot.

ConcurrentHashMap internals at a senior level?

A power-of-two table of bins; bins are lists or trees similar in spirit to HashMap. Updates use CAS on bin heads and synchronized-on-bin for lists/trees; gets are typically wait-free if a bin is stable. Resizing is cooperative: threads help transfer bins. Size is estimated with a striped counter. Nulls are banned so get can use null as "absent" without ambiguity. It is not a composite atomic for "check two maps".

When would you use WeakHashMap, IdentityHashMap or EnumMap?

WeakHashMap: cache-like entries that should die with the key; remember values must not strongly point back at keys. IdentityHashMap: keys compared with ==, used for topology (visiting objects, serialization graphs). EnumMap: array-backed, fastest and smallest when keys are an enum; always prefer it then.

Why is clone a poor default, and what do you use instead?

Cloneable is a marker; Object.clone is a shallow field copy; mutable children are shared; inheritance of clone is fragile (you must remember to deep-copy every new field). Use a copy constructor, a static factory, or a builder. If you must clone, document depth and copy every mutable field.

What is the serialization proxy pattern?

Instead of serializing the real object (which skips constructors), you write a private static nested DTO that holds the logical data. writeReplace emits the DTO; readResolve on the DTO constructs a real instance through the normal constructor, restoring invariants. It is the adult alternative to a huge readObject.

What must a Java programmer know about JNI references without writing C?

Java objects in native code are handles. Local refs are freed when the native method returns; leaking many locals in a tight C loop OOMs. Global refs are GC roots until deleted. A raw pointer into a Java array is only valid while pinned or in a critical section, especially under ART's moving CC. Details and JNIEnv belong on C language.

How do ART AOT, JIT and profiles fit together?

Install or idle dexopt runs dex2oat at a compiler filter (verify, speed-profile, ...). Profiles from production or the cloud tell it which methods to compile. A JIT still compiles hot methods that were not AOT'd. After OTA, filters rerun (first-boot cost). Zygote's boot image is a pre-initialized set of framework classes. This is why "is the APK interpreted?" is the wrong binary question.

Why does a moving GC matter for JNI and for Zygote?

Moving means object addresses change; native code that cached a pointer without pinning is use-after-free. Zygote shares pages copy-on-write; a compacting collection that rewrites the preloaded heap would dirty those pages for every app. ART keeps an image space that is rarely moved so COW survives. Frameworks coverage of Zygote is in Android frameworks.

How does modern string concatenation work, and why do loops still need a builder?

A single expression of + is compiled to a builder or, on recent JDKs, invokedynamic concat strategies. A loop of s = s + chunk is still O(n²) allocations of growing immutable strings. Use StringBuilder (or append on one builder) for loops. Do not use StringBuffer.

How do default methods affect binary compatibility?

Adding a default method to an interface is source- and binary-compatible for existing implementors (they inherit it). Adding an abstract method is not. If a class already has a method with that signature, it wins. If two interfaces add conflicting defaults, the class must override. It is evolution of behaviour, not of state.

What are suppressed exceptions, and when do you see them?

When try-with-resources has a primary exception from the body and close throws, the close exception is attached via addSuppressed rather than replacing the primary. Print the stack and getSuppressed(). A hand-written finally that throws loses the primary unless you add suppressed yourself.

When is a constructor write visible without volatile or a lock?

For final fields, after the constructor finishes, if the reference is published safely (or even racy, for those finals only). For other fields, you need a happens-before (lock, volatile publication, join, class init). "I constructed it on thread A, thread B read the reference from a plain field" is not enough for non-finals.

ForkJoinTask vs Future?

A Future is a handle for a result: get, cancel. A ForkJoinTask is a Future specialized for fork/join: fork (async in the pool), join (wait, may steal work instead of blocking the worker). RecursiveTask/RecursiveAction are the usual subtypes. Do not get() a ForkJoin task from inside another if you can join.

How do you handle errors in a CompletableFuture pipeline?

exceptionally recovers to a value; handle sees result or error; whenComplete is a side-effecting callback; thenApply is skipped if the previous stage failed. Exceptions are wrapped; unwrap CompletionException. Never ignore a failed stage and then join on the UI thread.

Why is HashMap capacity a power of two?

The bin index is a mask, (n - 1) & hash, which is a cheap modulo when n is 2^k. Resizing doubles n and re-bins each key to the same index or index + oldCap depending on the extra hash bit. A prime table (as some older maps used) needs a real modulo.

What is the ART boot image, in Java-visible terms?

A file (.art) containing already-initialized framework classes and interned strings that Zygote maps before it forks. App processes inherit those pages. Java code sees ordinary Class objects and strings; the performance story is fewer first-use initializers and shared RAM. Do not confuse it with the APK's own oat file.

Covariant arrays vs invariant generics: why both exist?

Arrays were covariant from Java 1.0 so String[] could be passed as Object[]; the VM checks stores. Generics are invariant (List<String> is not a List<Object>) because erasure cannot check stores the same way. Wildcards restore controlled variance. Mixing them (T[] via unchecked cast) is a heap-pollution vector.

What does NoClassDefFoundError vs ClassNotFoundException mean?

ClassNotFoundException is a checked exception from a loader API (Class.forName) when the class cannot be found. NoClassDefFoundError is an Error: the VM already failed to initialize or find a class that the bytecode assumes exists (often a leftover from a failed static initializer, or a missing transitive class at runtime). Interviewers use this to see if you distinguish Error from Exception.

How does synchronized interact with the object header (biased locking mention)?

The mark word stores lock state: unlocked, biased (historically), thin/inflated monitor. Contended locks inflate to a full monitor object. Exact biasing has been disabled or removed on recent HotSpot releases; the interview point is that an uncontended lock is cheap and a contended lock is not, and that the header is involved. Do not build a design on biased locking existing on ART.

What is heap vs direct ByteBuffer from a Java interview angle?

Heap buffers are Java arrays: movable by GC, extra copy at many JNI/OS boundaries. Direct buffers live off-heap; good for I/O, but allocation is expensive, and you must not leak them (Cleaner / explicit free APIs). ART and JNI still need care with addresses. Prefer heap buffers until you measure a copy on a hot path.

Scenario & debugging

You get ConcurrentModificationException from an enhanced-for over an ArrayList. What happened?

A structural add/remove happened during iteration: another thread, or the same thread calling list.remove instead of iterator.remove. Fail-fast saw modCount change. Fix: iterate with an Iterator and remove through it; or collect indices/items to remove after; or use a concurrent collection if another thread mutates. Do not catch CME and retry as a protocol.

Two threads share a HashMap. What can go wrong, and what do you do?

Lost updates, reads of half-resized tables, and on very old implementations an infinite loop during resize. Even "we only write during startup" fails if a reader races the last put. Fix: build the map then publish an immutable copy behind a volatile or a lock; or use ConcurrentHashMap; or confine the map to one thread. Never use HashMap as a concurrent cache.

A HashMap get returns null for a key you just put. You overrode equals. What did you forget?

Usually hashCode (or a hash that does not use the same fields as equals). Or you mutated the key after insert. Or you mixed interned and non-interned assumptions. Write a unit test: two equal keys must retrieve the same entry. Include the key in a HashSet as a second check.

An Activity leaks after rotation. The dump shows a Handler and a Message. Why?

A non-static inner Handler holds the Activity (this$0). A delayed message holds the Handler. After onDestroy the message is still in the main queue. Fix: static nested Handler + WeakReference<Activity>, and removeCallbacksAndMessages(null) in onDestroy. Same pattern for anonymous Runnables posted delayed. See Android frameworks.

The app ANRs; the main thread is in a Binder call or on a lock. How do you reason about it in Java terms?

The main Looper is stuck in one message. If the stack is a synchronous Binder call, the server (or a chain of servers) is slow or its pool is exhausted. If the stack is Blocked on a monitor, find the holder: often a background thread doing I/O inside the same lock. Fix: do not do disk/network/slow Binder on the main thread; do not hold a shared lock across I/O. Timeouts: input ~5 s. Do not start designing AMS here.

Two threads are deadlocked. How do you confirm and fix it?

Take a thread dump (jstack, or ART SIGQUIT / ANR traces). Find two (or more) Blocked threads, each holding a monitor the other wants. Draw the cycle. Fix: one lock order everywhere, or combine locks, or try-lock with timeout and backoff. Add a regression test that stresses the two paths. Logging "I took lock A" is not enough without the dump.

A request-scoped user id stored in ThreadLocal appears on the wrong user's request. Why?

The worker came from a pool and still held the previous task's ThreadLocal. Fix: remove() in finally on every task, or use a framework that wraps tasks. Treat leftover ThreadLocals as a security bug, not a nuisance.

Predict the result: try { return 1; } finally { return 2; } and try { throw ... } finally { return 0; }.

The first method returns 2; the 1 is discarded. The second returns 0 and swallows the exception. That is why return in finally is a defect. Use try-with-resources and let the primary exception propagate.

You see an NPE on a line that only unboxes an Integer. What happened?

The Integer was null (map miss, Binder boxed extra, failed parse). Auto-unbox compiles to intValue(). Fix: keep it boxed until you null-check, use int primitives, or OptionalInt / a clear default. The same bug happens with Boolean extras on Intents.

A consumer wait()s once with if (queue.isEmpty()) and sometimes hangs or processes twice. Why?

Spurious wakeup, or notifyAll for another condition, or a missed notify before wait. You must wait in a while that re-reads the queue under the same monitor. Also check that notify happens under that monitor after the put.

A singleton DCL without volatile "works on my machine" and fails on a phone. What is going on?

The JMM allows the reference store to become visible before constructor field stores. HotSpot or ART, ARM vs x86, and JIT tiering change whether you lose the race. The bug is real even if x86 TSO hid it in a desktop loop. Add volatile or use a static holder. This is a JMM question, not an Android frameworks question.

Metaspace or native class memory grows without bound after "reloading" code. Where do you look?

A classloader leak: static caches, ThreadLocals, shutdown hooks, JNI globals, or a listener still holding a class from the old loader. Heap dumps show the loader as a GC root cluster. On Android, leftover dex loaders from plugins do the same. Fix the root; raising Metaspace size only delays the crash.

A generic method with T... throws ClassCastException at a cast you did not write.

Heap pollution: the varargs array is erased, a caller passed mixed types or a raw type, and the compiler-inserted cast failed. Recreate with -Xlint:unchecked. Change the API to List<T>, or use @SafeVarargs only when you truly do not leak the array.

A TreeSet loses elements that equals says are distinct.

The Comparator or compareTo returns 0 for those elements. Sorted collections use ordering as equality. Fix the comparator to be consistent with equals, or do not use a sorted set if you need equals-based uniqueness plus a different display order.

System.loadLibrary("x") throws UnsatisfiedLinkError. Walk the Java-side checklist.

Library not packaged for the ABI; wrong name (must map to libx.so); load order (depends on another .so); the class's static init ran before extract completed; the native symbol name does not match (overload mangling, wrong package). Java cannot fix a missing JNI_OnLoad export; that is the C side (C language).

clone() of an object with a List field shares mutations with the original.

Shallow copy: both objects hold the same list reference. Copy the list (new ArrayList<>(old) or List.copyOf if elements are immutable) in the clone or, better, a copy constructor. Deep-copy elements if they are mutable. Document the depth.

UI jank correlates with GC in traces. What Java mistakes do you look for first?

Allocations on the main thread: boxing in a draw loop, hidden StringBuilder via +, stream pipelines, iterator objects, autoboxing in HashMap of Integer. Fix by reusing buffers, primitive collections or arrays, moving work off the main Looper. Then look at ART collector pauses as a second-order effect. Do not start rewriting AMS.

A Binder extra Boolean is sometimes null and crashes when compared with == true.

Boxed extras can be missing. == true unboxes. Use a primitive getter with a default, or null-check. Do not put boxed types on a hot AIDL API; use boolean plus a separate "was set" if you need three-state, or a small Parcelable. See Binder and AIDL.

A hot path uses Optional inside a tight loop and shows up in allocation traces.

Optional is an object (and the boxed payload may be too). In a per-pixel or per-message loop, use primitives, -1 sentinels, or a preallocated holder. Keep Optional on API boundaries, not in the inner loop. Same story for stream pipelines over a 4-element list.

A parallel stream in an Android process causes random slowness in unrelated work.

Parallel streams use the common ForkJoinPool. Blocking or CPU-heavy work there starves other parallel streams and some library tasks. On a phone you also fight the UI for cores. Fix: do not use parallel() for I/O or tiny data; use a dedicated bounded executor for background Java work; never parallelize on the main thread.

String a = new String("x"); String b = "x"; Why is a == b false and when would intern make it true?

"x" is the pooled literal. new String("x") copies it into a new heap object. == is identity. a.intern() == b is true because intern returns the pooled instance. Production code should use equals. Interning arbitrary input fills the heap pool.

You pass a list into a constructor, store it, and later the object is mutated from outside. How do you write the API?

Defensive copy on the way in (List.copyOf(in) or new ArrayList<>(in) then wrap). Return an unmodifiable view of your private copy, or another copy. If you only wrap the caller's list as unmodifiable, the caller still mutates the backing list. Document whether the class takes ownership.

A method overloads log(Object) and log(String). You pass a null String reference. Which runs?

Overload resolution uses the static type. If the argument is a String variable that happens to be null, log(String) is chosen. If the argument is null with no type (a raw null literal) and both apply, the most specific method wins (String is more specific than Object). This is compile-time; override would be runtime. Interviewers use this to mix overload with null.

Thread t = new Thread(task); t.run(); tests pass, production "concurrency" does not. Why?

Tests called run on the test thread. Production must start(). The task never raced, so race bugs hid. Also check that the production path does not run a Runnable that was meant for an executor. Add a test that asserts work happened on a different thread name if the contract requires it.