Java remains the dominant language at Amazon, Google, Goldman Sachs, and most enterprise tech companies. This guide covers the Java interview questions asked at these companies — from core OOP to JVM internals and modern Java features.
Core OOP and Language Features
1. Abstract class vs Interface — when to use each
// Abstract class: partial implementation, shared state, one inheritance
abstract class Shape {
protected String color; // Shared state
public Shape(String color) { this.color = color; }
public abstract double area(); // Must implement
public String describe() { // Default behavior
return color + " shape with area " + area();
}
}
// Interface: contract, multiple implementation, default methods (Java 8+)
interface Drawable {
void draw(); // Abstract by default
default void drawWithBorder() { // Default method (Java 8+)
System.out.println("Border");
draw();
}
static Drawable noOp() { // Static factory method
return () -> {}; // Lambda implements Drawable
}
}
// Use abstract class when: shared state/code, IS-A relationship, one parent
// Use interface when: multiple inheritance, capabilities/roles, looser coupling
class Circle extends Shape implements Drawable {
private double radius;
public Circle(String color, double radius) {
super(color);
this.radius = radius;
}
@Override public double area() { return Math.PI * radius * radius; }
@Override public void draw() { System.out.println("Drawing circle r=" + radius); }
}
2. Generics — bounded type parameters and wildcards
import java.util.*;
import java.util.function.*;
// Bounded type parameters
public class MathUtils {
// T must implement Comparable
public static <T extends Comparable> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
// Multiple bounds
public static <T extends Comparable & Cloneable> T cloneIfGreater(T a, T b) {
return a.compareTo(b) > 0 ? a : b;
}
}
// Wildcards: ? extends T (producer/upper bound) vs ? super T (consumer/lower bound)
// PECS: Producer Extends, Consumer Super
class Stack {
private List elements = new ArrayList();
public void push(T item) { elements.add(item); }
public T pop() { return elements.remove(elements.size() - 1); }
// Producer: we produce T, caller consumes; use ? extends T
public void pushAll(Iterable src) {
for (T t : src) push(t);
}
// Consumer: destination consumes our T; use ? super T
public void popAll(Collection dst) {
while (!elements.isEmpty()) dst.add(pop());
}
}
// Type erasure: generics are compile-time only; List == List at runtime
// Cannot: new T(), new T[10], instanceof List, T.class
3. equals() and hashCode() contract
import java.util.Objects;
public class Employee {
private final String id;
private final String name;
private int age; // Not part of identity
public Employee(String id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Employee)) return false;
Employee emp = (Employee) o;
return Objects.equals(id, emp.id) && Objects.equals(name, emp.name);
}
@Override
public int hashCode() {
return Objects.hash(id, name); // Must include same fields as equals!
}
}
// Contract:
// 1. equals is reflexive, symmetric, transitive, consistent
// 2. If a.equals(b), then a.hashCode() == b.hashCode() (required)
// 3. If !a.equals(b), hashCodes MAY differ (ideally do for HashMap performance)
// Violation: break HashMap/HashSet — objects get "lost"
Java Concurrency
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.concurrent.locks.*;
// AtomicInteger: lock-free compare-and-swap operations
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet(); // Atomic increment
counter.compareAndSet(5, 10); // CAS: if value is 5, set to 10
// ReentrantLock: more control than synchronized
ReentrantLock lock = new ReentrantLock(true); // true = fair lock
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
void read() throws InterruptedException {
rwLock.readLock().lock(); // Multiple readers allowed simultaneously
try { /* read */ } finally { rwLock.readLock().unlock(); }
}
void write() {
rwLock.writeLock().lock(); // Exclusive writer
try { /* write */ } finally { rwLock.writeLock().unlock(); }
}
// ExecutorService: thread pool management
ExecutorService pool = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors()
);
// CompletableFuture: async pipelines (Java 8+)
CompletableFuture future = CompletableFuture
.supplyAsync(() -> fetchUser(1)) // Run async
.thenApplyAsync(user -> transform(user)) // Chain async step
.exceptionally(ex -> "fallback") // Handle error
.thenCombine( // Combine two futures
CompletableFuture.supplyAsync(() -> fetchOrders(1)),
(user, orders) -> user + " has " + orders.size() + " orders"
);
String result = future.get(5, TimeUnit.SECONDS);
// Semaphore: limit concurrent access
Semaphore semaphore = new Semaphore(10); // Allow 10 concurrent threads
semaphore.acquire();
try { /* access limited resource */ } finally { semaphore.release(); }
// CountDownLatch: wait for N events
CountDownLatch latch = new CountDownLatch(3);
// 3 worker threads each call latch.countDown() when done
latch.await(); // Blocks until count reaches 0
Java Streams and Functional Programming
import java.util.*;
import java.util.stream.*;
import java.util.function.*;
List employees = getEmployees();
// Grouping and aggregation
Map avgSalaryByDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.averagingDouble(Employee::getSalary)
));
// Flat map: flatten nested collections
List allSkills = employees.stream()
.flatMap(e -> e.getSkills().stream())
.distinct()
.sorted()
.collect(Collectors.toList());
// Custom collector: top-N per group
Map<String, Optional> topEarnerPerDept = employees.stream()
.collect(Collectors.groupingBy(
Employee::getDepartment,
Collectors.maxBy(Comparator.comparingDouble(Employee::getSalary))
));
// Parallel streams: use for CPU-bound work on large datasets
long count = IntStream.range(0, 1_000_000)
.parallel()
.filter(n -> isPrime(n))
.count();
// Method references
List names = employees.stream()
.map(Employee::getName) // Instance method ref
.map(String::toUpperCase) // Instance method on stream element
.filter(Objects::nonNull) // Static method ref
.collect(Collectors.toList());
// Optional: avoid null pointer exceptions
Optional topEarner = employees.stream()
.max(Comparator.comparingDouble(Employee::getSalary));
topEarner
.map(Employee::getName)
.ifPresentOrElse(
name -> System.out.println("Top earner: " + name),
() -> System.out.println("No employees")
);
JVM Internals
| Area | Details |
|---|---|
| Class loading | Bootstrap → Extension → Application class loaders (delegation model) |
| Memory areas | Heap (young/old gen), Metaspace (class metadata), Stack (per thread), PC register |
| GC algorithms | G1 (default Java 9+), ZGC (sub-millisecond pauses), Shenandoah, Serial/Parallel |
| JIT compilation | Interpreter → C1 (client) → C2 (server) as method gets hotter |
| String pool | Interned strings in heap (Java 7+); intern() to add manually |
| volatile keyword | Visibility guarantee (CPU cache flush); not atomic for compound operations |
| happens-before | Ordering guarantee: monitor release HB monitor acquire; volatile write HB read |
Frequently Asked Questions
What is the difference between an interface and an abstract class in Java?
An interface defines a contract with abstract methods (and default/static methods since Java 8) — a class can implement multiple interfaces. An abstract class can have state (fields), constructors, and both abstract and concrete methods — a class can only extend one abstract class. Use interfaces to define capabilities (Runnable, Comparable); use abstract classes when sharing implementation across related classes.
What is the Java Memory Model and how does volatile work?
The Java Memory Model (JMM) defines how threads interact through memory. Without synchronization, threads may see stale cached values. volatile guarantees visibility: reads always fetch from main memory, writes flush immediately. volatile does NOT provide atomicity for compound operations (check-then-act, increment). For atomicity, use AtomicInteger or synchronized. volatile is correct for single-writer, multiple-reader flags.
What is the difference between HashMap and ConcurrentHashMap?
HashMap is not thread-safe — concurrent modifications cause ConcurrentModificationException or data corruption. ConcurrentHashMap uses segment-level locking (Java 7) or CAS + synchronized on individual buckets (Java 8+), allowing concurrent reads and localized writes with no full-table lock. ConcurrentHashMap does not allow null keys or values. For read-heavy scenarios, ConcurrentHashMap is far superior to a synchronized HashMap.
{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “What is the difference between an interface and an abstract class in Java?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “An interface defines a contract with abstract methods (and default/static methods since Java 8) — a class can implement multiple interfaces. An abstract class can have state (fields), constructors, and both abstract and concrete methods — a class can only extend one abstract class. Use interfaces to define capabilities (Runnable, Comparable); use abstract classes when sharing implementation across related classes.”
}
},
{
“@type”: “Question”,
“name”: “What is the Java Memory Model and how does volatile work?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “The Java Memory Model (JMM) defines how threads interact through memory. Without synchronization, threads may see stale cached values. volatile guarantees visibility: reads always fetch from main memory, writes flush immediately. volatile does NOT provide atomicity for compound operations (check-then-act, increment). For atomicity, use AtomicInteger or synchronized. volatile is correct for single-writer, multiple-reader flags.”
}
},
{
“@type”: “Question”,
“name”: “What is the difference between HashMap and ConcurrentHashMap?”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “HashMap is not thread-safe — concurrent modifications cause ConcurrentModificationException or data corruption. ConcurrentHashMap uses segment-level locking (Java 7) or CAS + synchronized on individual buckets (Java 8+), allowing concurrent reads and localized writes with no full-table lock. ConcurrentHashMap does not allow null keys or values. For read-heavy scenarios, ConcurrentHashMap is far superior to a synchronized HashMap.”
}
}
]
}