Java โ The Complete Guide
A single-read, end-to-end reference for Java: enough to onboard onto a new codebase, write idiomatic production code, or walk into an SDET interview. Organized as a lookup you can also read top-to-bottom.
๐ Quick reference: Java โThis guide explains the why โ JVM internals, design trade-offs, interview questions. New to Java? Learn step by step with the Java cheat sheet and the Java learning path first, and look up syntax in the Quick Reference.
- Learn how source becomes bytecode and runs on the JVM
- Review OOP basics: classes, interfaces, and inheritance
- Get comfortable with collections and streams โ you'll use them in every test
- Read Why Java Underpins the Mainstream SDET Stack to see how it maps to tools
- Explain how the JVM compiles and runs code
- Use OOP, generics, collections, and streams
- Handle exceptions and basic concurrency
- Build a project with Maven or Gradle
Java source compiles once to bytecode, and the JVM does the rest: the classloader brings .class files in, the execution engine interprets or JIT-compiles them, and the memory manager handles heap allocation and garbage collection โ the same bytecode then runs unmodified on whatever OS and hardware host the JVM.
1. What Java Is, in Practical Termsโ
Java is a statically typed, compiled-to-bytecode, garbage-collected,
object-oriented language. Source (.java) compiles to platform-independent
bytecode (.class), which the JVM (Java Virtual Machine) executes โ
"write once, run anywhere." This matters directly for SDET work: Selenium,
Appium, TestNG, JUnit, REST Assured, and Cucumber-JVM are all Java-based,
so fluency in the language is the prerequisite for fluency in the entire
mainstream automation stack, not a separate skill from it.
| Term | Meaning |
|---|---|
| JDK (Java Development Kit) | Compiler (javac) + JVM + standard library โ what you install to build/run Java |
| JRE (Java Runtime Environment) | JVM + libraries only, no compiler โ just to run compiled Java (mostly folded into JDK distributions today) |
| JVM (Java Virtual Machine) | The runtime that loads, verifies, and executes bytecode; provides memory management (GC) and platform abstraction |
Current LTS releases in active industry use: Java 17 and Java 21 (virtual threads, pattern matching, records are now standard toolbox items in modern codebases and worth knowing even if a legacy project targets Java 8).
2. JVM Basics: Compilation, Classloading, Memoryโ
.java source
โ javac
โผ
.class bytecode โโโโโโโโโโโโโโโบ JVM
โ
โโโโโโโโโโโโโโผโโโโโโโโโโโโโ
โผ โผ โผ
Classloader Bytecode Runtime
(loads .class Verifier Data Areas
files at โ (heap, stack,
runtime) โผ method area)
JIT Compiler
(bytecode โ native
machine code, hot paths)
- Classloading is lazy and hierarchical (Bootstrap โ Extension/Platform โ Application classloader) โ a class is loaded the first time it's referenced, not all upfront.
- JIT (Just-In-Time) compilation โ the JVM interprets bytecode initially, then compiles "hot" methods (called frequently) to native machine code at runtime for near-native performance after warm-up. This is why JVM benchmarks often show a warm-up period before steady-state throughput.
- Memory areas:
- Heap โ all objects live here; shared across threads; garbage collected.
- Stack โ one per thread; holds local variables and method call
frames;
StackOverflowErroron unbounded recursion. - Method area / Metaspace โ class metadata, static fields.
- Garbage Collection โ automatic; modern default collectors (G1, and ZGC/Shenandoah for very low pause-time needs) reclaim unreachable objects. You don't manually free memory, but you can still leak it (e.g., static collections that grow forever, unclosed resources, listeners never deregistered).
3. Core Language: Types, Operators, Control Flowโ
Java is statically typed โ every variable's type is checked at compile time. It has two type categories:
| Category | Examples | Notes |
|---|---|---|
| Primitives | int, long, double, boolean, char, byte, short, float | Stored by value, not on the heap as objects, no null |
| Reference types | Classes, interfaces, arrays, String | Variables hold a reference to a heap object; can be null |
Autoboxing/unboxing converts between primitives and their wrapper
classes automatically (int โ Integer) โ convenient but a classic gotcha:
Integer a = 127, b = 127;
System.out.println(a == b); // true โ Integer caches -128..127
Integer x = 200, y = 200;
System.out.println(x == y); // false โ outside cache range, different objects!
System.out.println(x.equals(y)); // true โ always use .equals() for wrapper comparison
String is immutable โ every "modification" (concat, +, replace)
creates a new String object. In loops, prefer StringBuilder:
StringBuilder sb = new StringBuilder();
for (String s : items) {
sb.append(s).append(",");
}
String result = sb.toString(); // O(n), vs O(nยฒ) for repeated String +=
String literals are also interned in a string pool โ "abc" == "abc" is true, but new String("abc") == "abc" is false. Same rule as
above: use .equals() for content comparison, never ==.
4. OOP: Classes, Interfaces, Inheritanceโ
public interface Shape {
double area(); // implicitly public abstract
default String describe() { // default method โ since Java 8
return "A shape with area " + area();
}
}
public abstract class AbstractShape implements Shape {
protected String name;
protected AbstractShape(String name) { this.name = name; }
}
public class Circle extends AbstractShape {
private final double radius;
public Circle(double radius) {
super("Circle");
this.radius = radius;
}
@Override
public double area() { return Math.PI * radius * radius; }
}
- Interfaces define a contract; a class implements as many as it wants
(Java has no multiple class inheritance, but unlimited interface
implementation). Since Java 8, interfaces can carry
defaultandstaticmethod bodies. - Abstract classes can hold state (fields) and partial implementation; a class extends only one.
@Overrideis not required but should always be used โ the compiler catches signature mismatches (e.g., a typo'd method name that would otherwise silently create a new method instead of overriding).- Access modifiers:
private(class only) โ (package-private, no keyword) โprotected(package + subclasses) โpublic(everywhere). final: on a class = no subclassing; on a method = no overriding; on a variable = assign-once (reference is fixed, but afinal Listcan still have items added/removed โ final protects the reference, not the object's mutability).
equals() and hashCode()โ
Overriding equals() without hashCode() (or vice versa) breaks the
contract that equal objects must have equal hash codes โ silently corrupts
behavior in HashMap/HashSet (lookups fail even when an "equal" object is
present). IDEs and @EqualsAndHashCode (Lombok) generate both together for
exactly this reason.
Records (Java 16+)โ
public record Point(int x, int y) { }
// auto-generates constructor, accessors x()/y(), equals(), hashCode(), toString()
Immutable data carriers โ the modern replacement for boilerplate POJOs/DTOs in test data models and API request/response objects.
5. Genericsโ
Generics give compile-time type safety for containers and reusable APIs without casting.
public class Box<T> {
private T value;
public void set(T value) { this.value = value; }
public T get() { return value; }
}
Box<String> box = new Box<>(); // "diamond operator" infers <String>
box.set("hello");
String s = box.get(); // no cast needed
Bounded wildcardsโ
public void printAll(List<? extends Number> list) { } // read-only, accepts List<Integer>, List<Double>...
public void addNumbers(List<? super Integer> list) { } // write-only, accepts List<Integer>, List<Number>, List<Object>
Mnemonic โ PECS: Producer Extends, Consumer Super. Use ? extends T
when you only read from the structure; ? super T when you only write to
it.
Type erasure: generic type info exists only at compile time and is
erased at runtime โ List<String> and List<Integer> are the same class
(List) at runtime. This is why you can't do new T() or list instanceof List<String>
directly, and why generic arrays (new T[10]) aren't allowed.
6. Collections Frameworkโ
Collection
โโโโโโโโโโโโโผโโโโโโโโโโโโ
List Set Queue/Deque
ArrayList HashSet ArrayDeque
LinkedList LinkedHashSet PriorityQueue
Vector TreeSet
Map (separate hierarchy, not a Collection)
HashMap, LinkedHashMap, TreeMap
| Type | Ordering | Duplicates | Typical use |
|---|---|---|---|
ArrayList | Insertion order | Yes | Default general-purpose list; O(1) get, O(n) insert/remove in middle |
LinkedList | Insertion order | Yes | O(1) insert/remove at ends; implements Deque too; rarely beats ArrayList in practice |
HashSet | No guaranteed order | No | O(1) average membership check |
LinkedHashSet | Insertion order | No | Set + predictable iteration order |
TreeSet | Sorted (natural or Comparator) | No | O(log n) ops, sorted iteration |
HashMap | No guaranteed order | Keys unique | Default key-value store; O(1) average get/put |
LinkedHashMap | Insertion (or access) order | Keys unique | Predictable iteration; basis of simple LRU caches |
TreeMap | Sorted by key | Keys unique | Sorted key iteration, range queries |
Map<String, Integer> scores = new HashMap<>();
scores.put("alice", 90);
scores.merge("alice", 5, Integer::sum); // atomic-ish "update or insert" pattern
scores.computeIfAbsent("bob", k -> 0);
scores.getOrDefault("carol", -1);
List<String> immutable = List.of("a", "b", "c"); // Java 9+ immutable factory
Fail-fast iterators: modifying a collection while iterating over it
(other than via the iterator's own remove()) throws
ConcurrentModificationException. Use Iterator.remove(), or collect
removals into a separate list, or use removeIf():
list.removeIf(item -> item.isExpired()); // safe, correct way
7. Streams and Lambdas (Java 8+)โ
Lambdas are anonymous implementations of functional interfaces
(single-abstract-method interfaces like Runnable, Comparator,
Function<T,R>, Predicate<T>).
List<String> names = List.of("Charlie", "alice", "Bob");
names.stream()
.filter(n -> n.length() > 3)
.map(String::toUpperCase)
.sorted()
.forEach(System.out::println);
// collecting results
List<String> upper = names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
Map<Integer, List<String>> byLength = names.stream()
.collect(Collectors.groupingBy(String::length));
double avg = names.stream()
.mapToInt(String::length)
.average()
.orElse(0.0);
- Streams are lazy โ intermediate operations (
filter,map,sorted) don't execute until a terminal operation (collect,forEach,reduce,count) is called; this enables pipeline fusion and short- circuiting (findFirst,anyMatchstop as soon as satisfied). - A stream can only be consumed once โ calling a terminal operation
twice on the same stream throws
IllegalStateException. - Prefer method references (
String::toUpperCase,System.out::println) over equivalent lambdas for readability when the lambda body is just a single method call. Optional<T>โ wraps a possibly-absent value to force explicit null-handling (.orElse(),.orElseThrow(),.ifPresent()) instead of returning rawnulland riskingNullPointerExceptionat some distant call site.
8. Exception Handlingโ
public class InsufficientFundsException extends RuntimeException {
public InsufficientFundsException(String message) { super(message); }
}
try {
withdraw(balance, amount);
} catch (InsufficientFundsException e) {
log.warn("withdrawal rejected: {}", e.getMessage());
} catch (IllegalArgumentException | NullPointerException e) {
log.error("bad input", e); // multi-catch, Java 7+
} finally {
closeConnection(); // always runs
}
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
return reader.readLine();
} // reader.close() called automatically โ try-with-resources, Java 7+
| Category | Examples | Must be declared/caught? |
|---|---|---|
| Checked exceptions | IOException, SQLException | Yes โ compiler enforces throws or try/catch |
Unchecked (RuntimeException) | NullPointerException, IllegalArgumentException, IllegalStateException | No โ compiler doesn't force handling |
Error | OutOfMemoryError, StackOverflowError | No โ not meant to be caught/recovered from |
Custom exceptions for automation frameworks typically extend
RuntimeException rather than Exception โ checked exceptions force every
caller up the stack to declare or catch them, which becomes noisy
boilerplate in test code (e.g., a custom ElementNotInteractableException
wrapping a Selenium failure with extra context).
Best practice: never swallow an exception silently
(catch (Exception e) {}) โ at minimum log it; in test frameworks a
swallowed exception is a false-positive "pass."
9. Concurrency Basicsโ
Runnable task = () -> System.out.println("running on " + Thread.currentThread().getName());
Thread t = new Thread(task);
t.start();
t.join(); // wait for completion
ExecutorService โ the idiomatic way to run concurrent workโ
ExecutorService executor = Executors.newFixedThreadPool(4);
List<Future<Integer>> futures = new ArrayList<>();
for (int i = 0; i < 10; i++) {
int n = i;
futures.add(executor.submit(() -> n * n));
}
for (Future<Integer> f : futures) {
System.out.println(f.get()); // blocks until result ready
}
executor.shutdown();
- Never manage raw
Threadpools by hand in production/test-framework code โ always go throughExecutorServicefor lifecycle management (shutdown, queuing, rejection policy). CompletableFuturecomposes async pipelines without blocking:CompletableFuture.supplyAsync(() -> fetchUser(id))
.thenApply(User::getName)
.thenAccept(System.out::println);- Thread safety tools:
synchronizedblocks/methods for mutual exclusion;java.util.concurrent.atomic.AtomicIntegerfor lock-free counters;ConcurrentHashMapinstead of synchronizing aHashMapmanually;CountDownLatch/CyclicBarrierfor coordinating thread start/finish points (common in parallel test-execution harnesses). - Why this matters for SDET: TestNG and JUnit 5 both support
parallel test execution (
parallel="methods"intestng.xml, or JUnit 5's@Execution(CONCURRENT)) โ understanding thread safety is required to avoid shared mutable state (e.g., a static WebDriver instance) causing cross-test interference under parallel runs.
10. Build Tools: Maven and Gradleโ
Both manage dependencies, compilation, test execution, and packaging; almost every Java-based automation framework repo uses one of them.
Maven (pom.xml) โ declarative, convention-over-configurationโ
<project>
<dependencies>
<dependency>
<groupId>org.seleniumhq.selenium</groupId>
<artifactId>selenium-java</artifactId>
<version>4.21.0</version>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>7.10.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
mvn clean install # compile, test, package, install to local repo
mvn test # run tests only
mvn test -Dtest=LoginTest # run a single test class
mvn dependency:tree # inspect resolved dependency versions/conflicts
Gradle (build.gradle / build.gradle.kts) โ programmatic, faster incremental buildsโ
dependencies {
testImplementation 'org.seleniumhq.selenium:selenium-java:4.21.0'
testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2'
}
./gradlew build
./gradlew test --tests "com.example.LoginTest"
Maven vs. Gradle: Maven's XML is more verbose but highly standardized (easy to onboard onto any Maven repo instantly); Gradle's Groovy/Kotlin DSL is more flexible and generally faster via incremental build caching. Neither is objectively "correct" โ most enterprise SDET codebases still default to Maven for its predictability and the sheer volume of Maven-based example projects/plugins in the ecosystem.
11. Why Java Underpins the Mainstream SDET Stackโ
| Framework | Role | Why Java |
|---|---|---|
| Selenium WebDriver | Browser automation | Original bindings are Java; JVM ecosystem gives mature IDE support, debugging, and CI integration |
| TestNG | Test runner/orchestration | Annotations (@Test, @BeforeMethod), parallel execution, data providers, native to the JVM test ecosystem |
| JUnit 5 | Test runner (alternative to TestNG) | De facto standard for unit tests; extension model integrates with Spring, Mockito, etc. |
| REST Assured | API testing | Fluent Java DSL (given().when().then()) built directly on Java's HTTP client ecosystem |
| Appium | Mobile automation | Java client bindings speak the same WebDriver protocol as Selenium โ one mental model for web + mobile |
| Cucumber-JVM | BDD | Gherkin step definitions implemented as Java methods |
The practical implication: strong Java fundamentals (OOP for Page Object Models, generics for reusable utility classes, streams for test-data transformation, exception handling for custom framework errors, concurrency for parallel suites) transfer directly across the entire toolchain rather than being framework-specific trivia.
12. Interview-Ready Q&Aโ
Q: What's the difference between the JDK, JRE, and JVM?
A: The JVM is the runtime engine that executes bytecode and manages memory;
the JRE bundles the JVM with the standard libraries needed to run Java
programs; the JDK adds the compiler (javac) and development tools on top
of the JRE, needed to build Java programs. Modern JDK distributions
include everything, so the JRE-only distinction matters less day to day, but
it's still asked as a fundamentals check.
Q: Why does == sometimes appear to work for comparing two Integer
objects, and why is it wrong to rely on it?
A: The JVM caches boxed Integer values from -128 to 127, so two
Integer variables in that range holding the same value can point to the
same cached object, making == return true by coincidence. Outside that
range, autoboxing creates distinct objects and == returns false even for
equal values. The correct comparison is always .equals(), since == on
reference types compares identity, not value.
Q: Explain checked vs. unchecked exceptions and when you'd choose to
create a custom one as which.
A: Checked exceptions (subclassing Exception) must be declared with
throws or handled by every caller up the call chain โ the compiler
enforces it, useful for genuinely recoverable conditions like I/O failures.
Unchecked exceptions (subclassing RuntimeException) aren't enforced by the
compiler. In test-automation frameworks, custom exceptions almost always
extend RuntimeException because forcing every calling test method to
declare or catch a framework-internal exception (e.g., an element-not-found
wrapper) adds boilerplate without adding safety.
Q: What does PECS mean for generic wildcards, and give an example?
A: "Producer Extends, Consumer Super." Use ? extends T when a generic
parameter is only being read from (it produces values of type T or a
subtype), and ? super T when it's only being written to (it consumes
values of type T). Example: a method that copies from a source list to a
destination list would type the source List<? extends T> and the
destination List<? super T>.
Q: Why can a HashMap lookup fail to find an object that looks equal to
the one you inserted?
A: Because equals() was overridden without also overriding hashCode()
consistently (or vice versa). HashMap uses hashCode() to locate the
bucket and equals() to confirm the match within that bucket; if two
"equal" objects produce different hash codes, the map looks in the wrong
bucket and the lookup silently fails to find an entry that's actually there.
Q: What happens if you modify a List while iterating over it with a
for-each loop?
A: It throws ConcurrentModificationException โ the for-each loop uses an
Iterator under the hood, and most collection iterators are fail-fast,
detecting structural modification via an internal mod-count check. The fix
is to use Iterator.remove() directly, collect items to remove into a
separate list and remove them after iterating, or use
Collection.removeIf().
Q: How would parallel test execution in TestNG cause a flaky failure
that's actually a Java concurrency bug, not a test bug?
A: If the framework holds shared mutable state in a static field โ most
commonly a single static WebDriver instance meant to be reused โ parallel
threads running different test methods will read/write that same instance
concurrently, causing one test's browser actions to interleave with
another's. The fix is ThreadLocal<WebDriver> so each thread gets its own
isolated instance, not a shared one.
Q: Streams vs. traditional for-loops โ when do you actually prefer
streams?
A: Streams read more declaratively for filter/map/reduce-shaped
transformations of collections and enable easy parallelization
(.parallelStream()) for CPU-bound work on large datasets. For-loops remain
clearer when the logic has early returns, multiple accumulator variables, or
side effects that don't map cleanly onto map/filter/collect โ forcing
imperative logic into a stream pipeline just to "look modern" often hurts
readability more than it helps.
Save this as
Cme.javaand run it withjava Cme.java(Java 11 or newer):import java.util.*;
public class Cme {
public static void main(String[] args) {
List<String> tests = new ArrayList<>(List.of("legacy-a", "checkout", "legacy-b", "search"));
for (String t : tests) {
if (t.startsWith("legacy")) {
tests.remove(t);
}
}
System.out.println(tests);
}
}It should fail. Fix it using the safe pattern from the guide.
Done when: you first saw a
ConcurrentModificationException, and the fixed version prints[checkout, search].Given this data, use one stream pipeline per output. Print the count of results per status (sorted by key), the average duration of the passing tests, and the name of the slowest test:
record Result(String name, String status, long ms) {}
List<Result> results = List.of(
new Result("login", "PASS", 120),
new Result("checkout", "PASS", 300),
new Result("search", "FAIL", 90),
new Result("profile", "PASS", 180),
new Result("export", "FAIL", 400));Done when: the program prints
{FAIL=2, PASS=3}, then200.0, thenexport.Stretch: Call a terminal operation twice on the same stream and read the exception it throws.
Illustrative scenario. A lookup method returns null when a user is not found. Several layers up, a caller assumes a user always comes back and reads a field from it.
The failure surfaced as a NullPointerException in a place far from the lookup, so the stack trace pointed at the symptom, not the cause. Engineers spent their time working backwards through the call chain to find where the null had come from.
Returning Optional forces every caller to decide explicitly what to do when a value is absent, using orElse, orElseThrow, or ifPresent. That moves the failure to the point where the decision belongs, instead of some distant call site.
- Ask an assistant to turn a nested loop into a stream pipeline, then run both versions on the same data and check that order, nulls, and the single-use stream rule behave identically.
- Have it explain a compiler error involving generics or wildcards using PECS, and confirm the explanation by compiling a minimal example yourself.
- Ask it to review a class for resources that should use try-with-resources, and verify each finding by reading the code.
13. One-Line Summaryโ
Java's static typing, mature JVM tooling, and object-oriented model are why the entire mainstream automation stack โ Selenium, TestNG, JUnit, REST Assured, Appium โ is built in it; strong fundamentals in generics, collections, streams, exceptions, and concurrency transfer directly across all of them rather than being framework-specific trivia.