Chapter 8: General Programming

effective-java general-programming java best-practices performance

Book: Effective Java, 3rd Edition — Joshua Bloch
Status: 🟩 Complete
Difficulty: Medium
Items: 57-68 (12 items)
Time to complete: ~50 min


Overview

Chapter 8 is a collection of practical programming wisdom — the day-to-day habits and decisions that distinguish a careful Java engineer from a careless one. The items range from micro-level concerns (variable scope, loop style, string concatenation) to architectural ones (reflection, native code, naming). None are glamorous; all are consequential.

The unifying theme is discipline: discipline about what you write, what you use, and what you avoid. Good Java code isn’t clever — it’s clear, safe, and unsurprising. These items teach you to default to the safer and more readable path, and to reach for powerful tools like reflection and native methods only when the simpler path genuinely cannot work.


Items


Item 57: Minimize the Scope of Local Variables

The Problem

Declaring local variables far from their use, or too early “just in case,” increases cognitive load. A reader must remember the variable through code that has nothing to do with it, and the variable can be accidentally used before it is properly initialized.

// BAD: Iterator declared outside loop — scope larger than necessary
Iterator<Element> i = c.iterator();
while (i.hasNext()) {
    doSomething(i.next());
}
// 20 more lines of unrelated code...
// Bug potential: copy-paste of the while loop accidentally reuses old 'i'
 
// BAD: Declared before initialization logic is complete
String result;
if (condition) {
    result = computeA();
} else {
    result = computeB();
}
// 'result' could be used in a half-initialized state if logic is extended carelessly
 
// BAD: Variable declared outside a for loop when only needed inside
int i = 0;
for (; i < n; i++) {
    doWork(items[i]);
}
// i is still accessible (and misleadingly holds n) after the loop

The Solution

Declare every variable as close to its first use as possible, with the smallest scope that is sufficient.

// GOOD: Iterator scoped inside for loop — completely inaccessible after loop
for (Iterator<Element> i = c.iterator(); i.hasNext(); ) {
    Element e = i.next();
    doSomething(e);
}
 
// GOOD: For-each loop — even more tightly scoped
for (Element e : c) {
    doSomething(e);
}
 
// GOOD: Capture complex initialization in one place
String result = condition ? computeA() : computeB();
 
// GOOD: Initialize at the point of first use
// Pattern: declare with initializer, not empty declaration
int[] results = computeResults(); // declare and initialize together

Why This Works

  • Small scope = small mental model. Readers need only hold the variable in mind while reading the narrowest possible block.
  • Variables are initialized at the point they are needed, not before, so there is no “uninitialized but declared” window.
  • Loop variables (int i, iterators) declared in the for-loop initializer are guaranteed to be unreachable after the loop — no accidental reuse.
  • If you find you cannot declare a variable with an initializer (because the value depends on logic), that is a signal to refactor into a helper method.

When to Apply / When NOT to Apply

  • Apply universally. There is no good reason to declare a variable earlier than its first use.
  • Exception: a variable referenced in a catch/finally block must be declared in the enclosing scope (not inside the try block).

Java 17 Update

var (Java 10+, local variable type inference) makes tightly-scoped declarations more concise but does not change the scoping rule:

// GOOD: var at the point of first use — type inferred
var users = userRepository.findAll(); // type is List<User> — inferred
for (var user : users) {              // var in for-each
    process(user);
}
 
// CAUTION: var can hurt readability when the type is not obvious
var x = process(); // what type does process() return? Reader must look it up

Use var when the type is obvious from the right-hand side. Avoid it when the inferred type would surprise a reader.


Item 58: Prefer For-Each Loops to Traditional For Loops

The Problem

Traditional indexed for loops and Iterator-based loops are error-prone. Index management (i, j, off-by-one errors, wrong collection variable) adds noise and bug potential. When iterating over multiple collections simultaneously, the opportunity for mistakes multiplies.

// BAD: Traditional for loop — index management is error-prone
for (int i = 0; i < suits.size(); i++) {
    for (int j = 0; j < ranks.size(); j++) {
        deck.add(new Card(suits.get(i), ranks.get(j)));
    }
}
 
// COMMON BUG: n^2 instead of n iterations — wrong variable in inner loop
for (Iterator<Suit> i = suits.iterator(); i.hasNext(); ) {
    for (Iterator<Rank> j = ranks.iterator(); i.hasNext(); ) { // BUG: i.hasNext()!
        deck.add(new Card(i.next(), j.next()));                 // next() called too many times
    }
}

The Solution

Use the enhanced for loop (for-each) wherever possible:

// GOOD: For-each eliminates index and iterator management
for (Suit suit : suits) {
    for (Rank rank : ranks) {
        deck.add(new Card(suit, rank));
    }
}
 
// GOOD: Works on arrays too
int sum = 0;
for (int element : elements) {
    sum += element;
}
 
// GOOD: Works on any Iterable — not just Collections and arrays
public interface Iterable<E> {
    Iterator<E> iterator();
}
// Your custom data structure: implement Iterable<E>, get for-each for free

Three Situations Where You Cannot Use For-Each

  1. Destructive filtering: if you need to remove elements while iterating, use Iterator.remove() or Collection.removeIf() (Java 8+).
  2. Transforming a list: if you need to replace elements by index, use a list iterator with set().
  3. Parallel iteration over multiple collections: if you need the same index to walk two parallel arrays simultaneously.
// When you must use Iterator — destructive filtering
for (Iterator<Element> i = c.iterator(); i.hasNext(); ) {
    Element e = i.next();
    if (shouldRemove(e)) i.remove();
}
 
// Better (Java 8+):
c.removeIf(e -> shouldRemove(e));

Why This Works

  • For-each is simpler, clearer, and has no moving parts to get wrong.
  • Iterator variables are never in scope where they can be misused.
  • The JVM compiles for-each to the same bytecode as iterator-based loops — zero performance cost.

Java 17 Update

  • var in for-each headers (Java 10+): for (var element : collection) — useful when the type is verbose.
  • Collection.removeIf(Predicate) (Java 8+) handles destructive filtering cleanly.
  • Stream pipelines (stream().filter().forEach()) are an alternative for complex iteration with filtering/transformation but carry their own overhead for simple cases.

Item 59: Know and Use the Libraries

The Problem

Many developers re-implement functionality that is already in the Java standard library — poorly. Classic example: Random.nextInt(n) was frequently reimplemented incorrectly (biased results, poor distribution) by developers who did not realize it already existed.

// BAD: Reinventing the wheel — biased, incorrect for large n
static Random random = new Random();
static int random(int n) {
    return Math.abs(random.nextInt()) % n; // BUG: biased; Math.abs(MIN_VALUE) is negative
}
 
// BAD: Rolling your own HTTP client when java.net.http exists (Java 11+)
// Manual URL connection, stream reading, timeout management...
 
// BAD: Writing merge sort when Arrays.sort() uses TimSort (highly optimized)

The Solution

Know the libraries and use them. You benefit from the expertise of the people who wrote them, their testing, and their ongoing optimization.

// GOOD: Use ThreadLocalRandom (Java 7+) — faster, better than Math.random()
int randomInt = ThreadLocalRandom.current().nextInt(n);
 
// GOOD: Arrays.sort / Collections.sort — dual-pivot quicksort + TimSort
Arrays.sort(myArray);
Collections.sort(myList);
 
// GOOD: java.net.http.HttpClient (Java 11+) — built-in, no third-party dep
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.example.com/data"))
    .GET()
    .build();
HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
System.out.println(response.body());
 
// GOOD: Files.readString() / Files.writeString() (Java 11+) — simple I/O
String content = Files.readString(Path.of("/tmp/data.txt"));
Files.writeString(Path.of("/tmp/out.txt"), content);
 
// GOOD: String.join() instead of manual loop
String csv = String.join(", ", names); // "Alice, Bob, Charlie"

Key Libraries to Know

  • java.lang, java.util, java.io — always.
  • java.util.concurrent — concurrent utilities (ConcurrentHashMap, CountDownLatch, ExecutorService).
  • java.util.stream — stream processing.
  • java.time — date/time (replaces Date/Calendar).
  • java.nio.file — modern file I/O (Path, Files).
  • java.net.http (Java 11+) — HTTP client.
  • java.util.regex — regular expressions (cache Pattern.compile()).

Why This Works

  • Library code is written by domain experts and reviewed by thousands of users.
  • Library code is optimized across JVM versions (sort algorithms, I/O buffers, concurrency primitives).
  • Library code is tested exhaustively; your reimplementation is not.
  • Using the library avoids a maintenance burden — you do not own the bugs.

When to Apply

  • Always check the standard library first. Then check widely-used third-party libraries (Guava, Apache Commons).
  • Only roll your own when the library genuinely cannot meet your requirements (custom algorithm, performance tuning after profiling).

Java 17 Update

  • java.net.http.HttpClient (Java 11+) — replaces HttpURLConnection and third-party clients for most use cases. Supports HTTP/1.1, HTTP/2, and WebSocket.
  • Files.readString(), Files.writeString() (Java 11+) — trivial single-file I/O.
  • String.strip(), String.isBlank(), String.lines() (Java 11+) — Unicode-aware string utilities.
  • String.repeat(n) (Java 11+).
  • InputStream.readAllBytes(), InputStream.transferTo(OutputStream) (Java 9+).
  • Collection.copyOf(), Map.copyOf() (Java 10+).

Item 60: Avoid Float and Double if Exact Answers Are Required

The Problem

float and double are binary floating-point types. They cannot represent many decimal fractions exactly — 0.1 in binary is an infinite repeating fraction, like 1/3 in decimal. This causes rounding errors in financial, monetary, or any computation where exact decimal results are required.

// BAD: Float arithmetic for currency — rounding errors compound
System.out.println(1.03 - 0.42);   // prints 0.6100000000000001
System.out.println(1.00 - 9 * 0.10); // prints 0.09999999999999998
 
// BAD: Counting money with double
double funds = 1.00;
int itemsBought = 0;
for (double price = 0.10; funds >= price; price += 0.10) {
    funds -= price;
    itemsBought++;
}
System.out.println(itemsBought + " items; $" + funds + " left");
// Prints: 3 items; $0.3999999999999999 left — wrong! Should be 4 items, $0.00

The Solution

Use BigDecimal for exact decimal arithmetic, or use int/long with a fixed-point representation (i.e., store amounts in cents).

// GOOD: BigDecimal — exact decimal arithmetic
BigDecimal funds = new BigDecimal("1.00"); // Use String constructor, NOT double!
int itemsBought = 0;
for (BigDecimal price = new BigDecimal("0.10");
     funds.compareTo(price) >= 0;
     price = price.add(new BigDecimal("0.10"))) {
    funds = funds.subtract(price);
    itemsBought++;
}
System.out.println(itemsBought + " items; $" + funds + " left");
// Prints: 4 items; $0.00 left — correct!
 
// GOOD: Use int/long for fixed-point (cents) — faster than BigDecimal
int funds = 100; // 100 cents = $1.00
int itemsBought = 0;
for (int price = 10; funds >= price; price += 10) { // prices in cents
    funds -= price;
    itemsBought++;
}
System.out.println(itemsBought + " items; $" + funds/100.0 + " left");

BigDecimal Trade-offs

  • BigDecimal is slower and less convenient than double (no +, -, * operators — must use methods).
  • BigDecimal eliminates rounding errors completely.
  • You must choose a RoundingMode (e.g., HALF_UP, HALF_EVEN) for division.
  • Always use the String constructor or BigDecimal.valueOf(double) — never new BigDecimal(0.1) which inherits the double’s imprecision.

int/long Trade-offs

  • int/long fixed-point (e.g., cents) is fastest and simplest.
  • Limited range: long overflows at ~9.2 × 10^18 cents ($92 quadrillion). Sufficient for virtually all financial applications.
  • You must track the scale manually (cents, millicents, etc.).

When to Apply / When NOT to Apply

  • Use BigDecimal for monetary values, taxes, interest calculations, and any computation where exact decimal results are mandated.
  • Use int/long with explicit scale when performance matters and range is known.
  • Use float/double for scientific/engineering calculations where approximate results are acceptable and speed matters.

Java 17 Update

No fundamental changes — BigDecimal and fixed-point int/long remain the correct approaches. BigDecimal gained no new API between Java 8 and 17. However, if using java.util.Currency for currency names and MonetaryAmount (JSR-354 / javax.money — not in SE) in modern applications, be aware that the reference implementation (Moneta) handles these correctly.


Item 61: Prefer Primitive Types to Boxed Primitives

The Problem

Java has two type systems: primitive types (int, long, double, boolean, etc.) and reference types (Integer, Long, Double, Boolean, etc.). Autoboxing and auto-unboxing blur the boundary, but the differences are real and consequential.

// BAD: == on boxed types compares references, not values
Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);
naturalOrder.compare(new Integer(42), new Integer(42)); // returns 1 — BUG!
// i == j compares references, not values; two Integer(42) objects are different references
 
// BAD: Null unboxing — NullPointerException
Integer i = null;
if (i == 42) { ... } // NullPointerException on unboxing!
 
// BAD: Catastrophic performance — boxing in a tight loop
Long sum = 0L; // boxed Long
for (long i = 0; i < Integer.MAX_VALUE; i++) {
    sum += i; // auto-boxes 'i' to Long, unboxes sum, adds, boxes result
}
// Creates ~2 billion unnecessary Long objects

The Solution

Use primitive types everywhere except where reference types are required.

// GOOD: Correct comparator using Comparator.comparingInt()
Comparator<Integer> naturalOrder = Comparator.comparingInt(Integer::intValue);
// Or:
Comparator<Integer> naturalOrder = Integer::compare;
 
// GOOD: Use primitive — no null risk, no boxing overhead
int i = 42; // primitive, cannot be null
 
// GOOD: Use long primitive for summation
long sum = 0L; // primitive — no boxing
for (long i = 0; i < Integer.MAX_VALUE; i++) {
    sum += i; // pure primitive arithmetic
}
 
// When you MUST use boxed primitives:
// 1. As type parameters: List<Integer> (List<int> is not legal)
// 2. Nullable fields: Integer value = null means "not set"
// 3. Reflective method invocations
// 4. Maps and sets — Map<String, Integer>, Set<Double>

Three Key Differences: Primitive vs. Boxed

AspectPrimitiveBoxed
Identity vs. valueN/A== compares identity, not value
NullCannot be nullCan be null
PerformanceFast, no allocationSlower, allocation + GC

Why This Works

  • Primitives cannot be null — no NPE risk from unboxing.
  • Primitives use value equality naturally — no == vs .equals() confusion.
  • Primitives do not allocate objects — no GC pressure.
  • The JVM can keep primitives in registers; boxed types require heap allocation.

Java 17 Update

The advice is unchanged. Autoboxing continues to be a source of subtle bugs and performance issues. Modern tooling (IntelliJ, SpotBugs) can detect many of these issues automatically. Valhalla (future Java feature — value types / primitive classes) will eventually blur this distinction further, but as of Java 17 the advice stands: use primitives wherever possible.

Stream primitives (IntStream, LongStream, DoubleStream) exist for exactly this reason — they avoid boxing in stream pipelines:

// GOOD: IntStream avoids boxing
int sum = IntStream.rangeClosed(1, 1000).sum(); // no Integer objects created

Item 62: Avoid Strings Where Other Types Are More Appropriate

The Problem

Strings are the universal data format for serialization and user input, but they should not be used as a substitute for more specific types in program logic.

// BAD: String as a type for structured data
String compoundKey = className + "#" + i.next(); // fragile — delimiter could appear in class name
 
// BAD: String as enum substitute
static final String ROCK     = "ROCK";
static final String PAPER    = "PAPER";
static final String SCISSORS = "SCISSORS";
// No type safety — a typo like "ROKC" compiles but fails at runtime
 
// BAD: String as capability (token / unique ID)
// Two threads creating the same String literal share it — accidental sharing of access
public static final String CAPABILITY = "my.capability";
 
// BAD: Parsing a string to get a numeric value — roundabout
String temperature = "98.6";
if (Double.parseDouble(temperature) > 100.0) { ... } // why not store as double?
 
// BAD: Storing a complex aggregate in a string
String person = "Alice" + "#" + "30" + "#" + "Engineer";
// Extracting requires splitting — fragile, slow, hard to read

The Solution

Use the type that best represents the data:

// GOOD: Use int/double for numeric data
double temperature = 98.6;
if (temperature > 100.0) { ... }
 
// GOOD: Use enum for fixed sets of constants (Item 34)
public enum RockPaperScissors { ROCK, PAPER, SCISSORS }
RockPaperScissors move = RockPaperScissors.ROCK; // type-safe, IDE-complete
 
// GOOD: Use a class for structured data
public record Person(String name, int age, String role) {}
Person person = new Person("Alice", 30, "Engineer");
 
// GOOD: For thread-local capabilities, use unique Object references (not String)
// Two 'new Object()' calls always produce distinct keys — no accidental sharing
public static final Object LOCK_KEY = new Object();

Why This Works

  • Type safety: the compiler catches type errors. Strings do not provide this.
  • Expressiveness: an enum is self-documenting; a String constant requires context.
  • Performance: numeric operations on int/double are far faster than string parsing.
  • Correctness: record/class fields are individually accessible; parsing a compound string is error-prone.

Java 17 Update

  • Records (Java 16+) make small value-holding classes nearly as lightweight as strings but fully type-safe: record Point(int x, int y) {}.
  • Sealed interfaces (Java 17+) with pattern matching allow type-safe tagged union representations.
  • String-based “stringly typed” code is an antipattern increasingly called out by modern static analysis tools.

Item 63: Beware the Performance of String Concatenation

The Problem

The + operator for String is convenient but creates a new String object for every concatenation. In a loop over n items, this produces O(n²) character copies — a classic performance antipattern.

// BAD: O(n²) string concatenation in a loop
public String statement() {
    String result = "";
    for (int i = 0; i < numItems(); i++) {
        result += lineForItem(i); // each += creates a new String object
    }
    return result;
}
// For 100 line items: 100 String objects created, ~5050 total character copies
// For 1000 items: ~500,500 character copies

The Solution

Use StringBuilder explicitly when building strings in a loop:

// GOOD: StringBuilder — O(n) time and space
public String statement() {
    StringBuilder sb = new StringBuilder(numItems() * LINE_WIDTH); // pre-size if known
    for (int i = 0; i < numItems(); i++) {
        sb.append(lineForItem(i));
    }
    return sb.toString();
}
 
// GOOD: String.join() for delimiter-separated values (Java 8+)
String result = String.join(", ", items);
 
// GOOD: Collectors.joining() in a stream
String result = items.stream()
    .map(Item::getDescription)
    .collect(Collectors.joining(", ", "[", "]")); // prefix, suffix optional
 
// GOOD: String.formatted() (Java 15+) for template-style strings
String msg = "Hello, %s! You have %d messages.".formatted(name, count);
// Equivalent to String.format(...) but called on the format string directly
 
// GOOD: Text blocks (Java 15+) for multi-line strings — no concatenation needed
String json = """
        {
            "name": "%s",
            "age": %d
        }
        """.formatted(name, age);

When + Is Fine

  • Concatenating a fixed small number of strings (not in a loop): the compiler optimizes these to a single StringBuilder call.
  • "Hello, " + name + "!" is fine — the compiler generates new StringBuilder().append("Hello, ").append(name).append("!").toString().
  • The rule is about loops, not single expressions.

Why This Works

  • StringBuilder pre-allocates a buffer and appends in-place. toString() does one final copy.
  • O(n) vs O(n²) is the difference between “fast” and “unusable” for large n.
  • Pre-sizing with an estimated capacity (new StringBuilder(n * lineWidth)) avoids internal resizing copies.

Java 17 Update

  • String.formatted(Object... args) (Java 15+) — instance method equivalent of String.format(fmt, args). More readable for short templates.
  • Text blocks (Java 15+) — triple-quoted strings for multi-line content. Eliminate most concatenation for SQL, HTML, JSON strings.
  • The JDK 9+ StringConcatFactory (invokedynamic-based) optimizes compile-time constant concatenations further, but loop concatenation still requires explicit StringBuilder.

Item 64: Refer to Objects by Their Interfaces

The Problem

Declaring variables, parameters, and return types using a concrete class (e.g., LinkedList, HashMap) instead of an appropriate interface (e.g., List, Map) unnecessarily locks in an implementation choice and reduces flexibility.

// BAD: Concrete class as variable type — locks in LinkedList
LinkedList<String> names = new LinkedList<>();
 
// BAD: Concrete class as parameter — excludes all other List implementations
public void process(ArrayList<String> names) { ... }
 
// BAD: Concrete class as return type — caller cannot substitute another implementation
public TreeMap<String, Integer> getWordCounts() { ... }

The Solution

Use the most general interface that satisfies the requirement:

// GOOD: Interface type — easy to switch to ArrayList, CopyOnWriteArrayList, etc.
List<String> names = new LinkedList<>();
// Switching implementation = one-word change on the right side only:
List<String> names = new ArrayList<>();
 
// GOOD: Interface parameter — accepts any List
public void process(List<String> names) { ... }
 
// GOOD: Interface return type
public Map<String, Integer> getWordCounts() { ... }
 
// When a concrete class IS appropriate:
// 1. No appropriate interface exists (e.g., PriorityQueue, which has heapq-specific methods)
// 2. You deliberately need a specific class's extra methods
//    (e.g., LinkedList for deque operations not in List)
// 3. Value types — String, BigInteger, BigDecimal (no interface)
PriorityQueue<Task> queue = new PriorityQueue<>(); // fine: no relevant interface

Why This Works

  • If you declare List<String> names, changing the implementation requires modifying one line. Changing LinkedList<String> names to ArrayList<String> names is a single word change.
  • If the concrete type is embedded everywhere (parameters, return types, field types), a change requires touching every call site and usage.
  • Interface types are more informative to callers — they advertise what operations are valid, not how they are implemented.

When to Apply / When NOT to Apply

  • Always use interface types for local variables, fields, parameters, and return types.
  • Exception: when you need methods specific to the concrete class (e.g., TreeMap.firstKey()), use the concrete type only for the portion of code that needs those methods.
  • Exception: class hierarchies without interfaces — String, BigDecimal, framework base classes.

Java 17 Update

  • Sealed interfaces (Java 17+) provide richer interface hierarchies where the permitted implementations are known.
  • var (Java 10+) can inadvertently infer the concrete type: var names = new LinkedList<String>() makes names of type LinkedList<String>, not List<String>. Be explicit with interface types when the inferred type would be wrong.
// CAUTION: var infers concrete type
var names = new LinkedList<String>(); // type is LinkedList<String>, not List<String>
 
// EXPLICIT: Cast to interface if using var with a concrete instantiation
List<String> names = new LinkedList<>(); // explicit interface type — preferred

Item 65: Prefer Interfaces to Reflection

The Problem

java.lang.reflect allows programs to inspect and manipulate classes, fields, and methods at runtime. This power is seductive, but it comes at enormous cost: loss of compile-time type safety, loss of IDE support, verbose code, and significant performance overhead (5-100x slower for reflective method invocation).

// BAD: Using reflection to call a simple method
try {
    Class<?> clazz = Class.forName("com.example.MyService");
    Method method = clazz.getMethod("processData", String.class);
    Object result = method.invoke(clazz.newInstance(), "data");
} catch (ClassNotFoundException | NoSuchMethodException |
         IllegalAccessException | InvocationTargetException |
         InstantiationException e) {
    // 5 checked exceptions to handle!
    e.printStackTrace();
}
// No compile-time check that MyService or processData exists
// No IDE refactoring support
// Slower than direct call

The Solution

Use reflection only for instance creation — then program against an interface.

// GOOD: Use reflection only to instantiate; interface for all subsequent use
public static Set<String> createSet(String className, String... args)
        throws ReflectiveOperationException {
    Class<?> clazz = Class.forName(className);
    return (Set<String>) clazz.getDeclaredConstructor().newInstance();
}
 
// Usage: reflective instantiation, interface-typed variable
Set<String> s = createSet("java.util.TreeSet");
s.add("hello"); // compile-time type safety: we know it's a Set
s.add("world");
System.out.println(s); // [hello, world] — sorted, because TreeSet
 
// GOOD: Dependency Injection frameworks use this pattern
// They instantiate your @Component classes reflectively at startup,
// then inject them as interface types — you never call the reflective API yourself

Legitimate Uses of Reflection

  • Framework infrastructure (dependency injection, serialization, testing frameworks like JUnit).
  • Plug-in systems where class names are configuration values.
  • Tools (IDEs, debuggers, class browsers) that inspect arbitrary classes.
  • When an optional dependency might not be on the classpath — reflect to check, interface to use.

Why This Works

  • Reflection’s costs: no compile-time type checking, no refactoring support, verbose exception handling, performance hit.
  • Limiting reflection to instantiation confines those costs to one method; all subsequent code is normal, type-safe, performant.
  • The JVM cannot optimize reflective call paths as aggressively as direct dispatch.

Java 17 Update

  • Strong encapsulation (Java 9+ modules, fully enforced by Java 17): module-info.java opens modules explicitly. Reflective access to non-exported packages is now denied by default (--illegal-access is removed in Java 17). Code relying on deep reflection into JDK internals will break.
  • MethodHandles (Java 7+, improved in Java 9+) provide a faster, more type-safe alternative to traditional reflection for dynamic method invocation.
  • VarHandle (Java 9+) for field access.
  • Records cannot have their components set reflectively in the same way as regular fields — use the canonical constructor.
// BETTER: MethodHandles — faster and more type-safe than Method.invoke()
MethodHandles.Lookup lookup = MethodHandles.lookup();
MethodType mt = MethodType.methodType(void.class, String.class);
MethodHandle mh = lookup.findVirtual(MyService.class, "process", mt);
mh.invoke(myService, "data"); // faster than Method.invoke()

Item 66: Use Native Methods Judiciously

The Problem

The Java Native Interface (JNI) allows Java code to call native methods written in C or C++. This was historically used for platform-specific functionality, performance, and legacy library access. It comes with serious costs.

The Solution

Avoid JNI unless there is no alternative.

// BAD (conceptually): Using JNI for something the JDK now provides
// e.g., calling native code for file system timestamps when java.nio.file.Files exists
 
// When JNI might be legitimately needed:
// 1. Accessing platform-specific resources with no Java API (e.g., device drivers)
// 2. Integrating legacy C/C++ libraries (e.g., native BLAS for matrix math)
// 3. Performance-critical inner loops where JVM JIT cannot close the gap (rare)

Why to Avoid JNI

  • Native code is not portable — must compile separately for each OS/architecture.
  • Native code is not safe — memory corruption, buffer overflows, undefined behavior.
  • Native code is not garbage collected — manual memory management in native code.
  • JNI boundary crossing has measurable overhead — not always faster than pure Java.
  • Debugging across JNI boundaries is painful (crash dumps, native debuggers).

Java 17 Update

  • Project Panama (Foreign Function & Memory API — incubator in Java 17, more stable in Java 21) provides a much safer and more ergonomic alternative to JNI for calling native code and accessing native memory.
  • For most use cases where JNI was previously needed (accessing OS-level APIs, calling C libraries), Panama’s Foreign Function API provides typed, memory-safe access without the JNI overhead and complexity.
  • If you must use native code today, prefer established frameworks like JNA (Java Native Access) or JNR (Java Native Runtime) over raw JNI.

Item 67: Optimize Judiciously

The Problem

Premature optimization — optimizing code before understanding where the real bottlenecks are — produces code that is complex, hard to maintain, and often not even faster (because the optimized path is not the bottleneck).

// BAD: Micro-optimizing before measuring
// "Cache the size() call to avoid recomputation" — premature
int size = list.size();
for (int i = 0; i < size; i++) { ... }
// list.size() is O(1) for ArrayList; this "optimization" adds noise with no gain
 
// BAD: Using bitwise shift instead of multiplication for "speed"
int doubled = n << 1; // vs. n * 2 — JIT already optimizes n * 2 to a shift
// The "bit trick" makes code harder to read for zero performance benefit

The Solution

  1. Design the architecture well — no optimization will fix a bad design.
  2. Implement cleanly and correctly first.
  3. Measure (profile) to find actual bottlenecks.
  4. Optimize only the proven hotspots.
  5. Measure again to verify the optimization worked.
// GOOD: Profile first, optimize second
// Step 1: Write clean, correct code
public List<Result> processAll(List<Input> inputs) {
    return inputs.stream()
        .map(this::process)
        .collect(Collectors.toList());
}
 
// Step 2: Profile with JMH, JVisualVM, async-profiler, YourKit, etc.
 
// Step 3: If process() is the bottleneck and is CPU-bound:
public List<Result> processAll(List<Input> inputs) {
    return inputs.parallelStream()  // optimized based on profiling evidence
        .map(this::process)
        .collect(Collectors.toList());
}

Jackson Principle / Hoare’s Aphorism

“Premature optimization is the root of all evil.” — Donald Knuth (quoting Tony Hoare)

Rules of Optimization

  1. Don’t do it.
  2. (For experts only) Don’t do it yet.
  3. Profile first, then optimize the proven bottleneck.
  4. Prefer algorithms with better asymptotic complexity — O(n log n) beats O(n²) no matter how well you micro-optimize.
  5. Avoid designs that limit performance (e.g., a public API that forces unnecessary object copies).
  6. Measure before and after every optimization attempt.

When to Apply / When NOT to Apply

  • Good architecture decisions that are performance-aware from the start are not “premature optimization” — they are good engineering. Example: choosing StringBuilder over + in loops is always correct.
  • Micro-optimizations (bit tricks, avoiding method calls, manual inlining) require profiler evidence before applying.

Java 17 Update

  • JMH (Java Microbenchmark Harness) is the standard for measuring Java performance at the method level. Always use it for micro-benchmarks — System.currentTimeMillis() timing is unreliable.
  • JIT compiler in modern JVMs (Graal JIT, C2) performs many optimizations automatically: loop unrolling, escape analysis (eliminating heap allocations), branch prediction. Manual micro-optimizations often undo JIT work.
  • parallelStream() is an easy way to parallelize CPU-bound work, but is not a free lunch — measure before using.

Item 68: Adhere to Generally Accepted Naming Conventions

The Problem

Java has well-established naming conventions defined in the Java Language Specification and reinforced by decades of practice. Violating them makes code feel foreign, confusing, and harder to read — even if it compiles correctly.

// BAD: Naming conventions violated
class user_account { ... }       // should be UserAccount (UpperCamelCase)
int MyVariable = 5;              // should be myVariable (lowerCamelCase)
final int max_items = 100;       // should be MAX_ITEMS (SCREAMING_SNAKE_CASE)
interface Printable_ { }         // trailing underscore — not a convention
void DoWork() { }                // should be doWork (lowerCamelCase)
class e extends Exception { }    // type name too short — should be descriptive

The Solution

Follow the conventions in the table below:

// GOOD: All conventions followed
package com.example.myapp.util;    // all lowercase, dot-separated
class UserAccount { ... }          // UpperCamelCase
interface Serializable { ... }     // UpperCamelCase
enum DayOfWeek { MON, TUE, ... }  // class: UpperCamelCase; constants: SCREAMING_SNAKE
 
public class UserAccount {
    private static final int MAX_RETRIES = 3;   // constant: SCREAMING_SNAKE_CASE
    private String userName;                     // field: lowerCamelCase
 
    public String getUserName() { ... }          // method: lowerCamelCase, verb phrase
    public boolean isActive() { ... }            // boolean getter: is/has/can prefix
    public void setUserName(String name) { ... } // setter: set prefix
 
    public <T extends Comparable<T>> T max(T a, T b) { ... } // type param: single uppercase
}
 
public record Point(int x, int y) {}             // record: UpperCamelCase; components: lowerCamelCase

Complete Naming Conventions Table

Identifier TypeConventionExamples
PackageLowercase, dot-separated; reverse domain prefixcom.google.guava, java.util.concurrent
Class / Interface / RecordUpperCamelCase (PascalCase); noun or noun phraseUserAccount, Runnable, ArrayList, HttpClient
Enum typeUpperCamelCase (same as class)DayOfWeek, Planet, RoundingMode
Enum constantsSCREAMING_SNAKE_CASEMONDAY, HALF_UP, MAX_VALUE
Annotation typeUpperCamelCaseOverride, SuppressWarnings, FunctionalInterface
MethodlowerCamelCase; verb or verb phrasecomputeValue(), isEmpty(), getUserName()
Boolean methodlowerCamelCase with is, has, can, should prefixisActive(), hasNext(), canRead()
Conversion methodtoType() or asType()toString(), toArray(), asLong()
Static factoryof, from, valueOf, create, getInstance, newTypeList.of(), LocalDate.from(), Files.newBufferedReader()
Field (instance)lowerCamelCase; noun or noun phrasefirstName, maxRetries, isActive
Constant fieldSCREAMING_SNAKE_CASE (static final)MAX_VALUE, DEFAULT_TIMEOUT_MS, EMPTY_ARRAY
Local variablelowerCamelCase; brief, contextuali, n, result, userName
Type parameterSingle uppercase letter or UpperCamelCase descriptorT (type), E (element), K/V (key/value), R (result), N extends Number
Lambda parameterBrief, often single letter; match the element types -> s.length(), (k, v) -> ...

Special Naming Patterns

// "is" for boolean getters
public boolean isEmpty() { ... }
public boolean isUserActive() { ... }
 
// "to" for type conversions that produce new objects
public String toString() { ... }
public int[] toArray() { ... }
 
// "as" for conversions returning a view (same data, different type)
public List<E> asList() { ... }
 
// "value" for methods returning the value of an object
public int intValue() { ... }
public BigDecimal bigDecimalValue() { ... }
 
// Abbreviations: only well-known ones
// OK: min, max, i, n, idx, str, len, buf, pos
// NOT OK: usrAcct, proc, calc (too cryptic; spell out or use lowerCamelCase)
 
// Type parameters by convention
<T>                 // generic type
<E>                 // element type (collections)
<K, V>              // key, value (maps)
<R>                 // result type (functions)
<N extends Number>  // numeric type
<T extends Comparable<T>> // bounded type parameter

Why This Works

  • Conventions are shared knowledge — any experienced Java developer can read code that follows them immediately.
  • IDEs, linters, and static analysis tools encode these conventions; violations are flagged automatically.
  • Consistent naming makes refactoring safer — tools can distinguish field vs. local vs. constant by convention.

Java 17 Update

  • Records (Java 16+): component names follow lowerCamelCase; the record class name follows UpperCamelCase. Accessor methods are named after the component (no get prefix): record Point(int x, int y) → accessor is point.x(), not point.getX().
  • Sealed interfaces (Java 17+): follow standard interface/class naming.
  • Pattern variables in instanceof patterns: follow lowerCamelCase local variable naming: if (obj instanceof String s).

Interview Questions & Exercises

Q1: Explain why Long sum = 0L in a summation loop is a performance disaster.

Context: Classic Java performance question asked at all levels. Tests understanding of autoboxing.

Answer: Long sum = 0L declares a boxed Long reference. On each iteration sum += i, Java must: (1) unbox sum to long, (2) add i, (3) box the result back to a new Long object, (4) assign the new reference to sum. Over Integer.MAX_VALUE iterations (~2 billion), this creates approximately 2 billion Long objects on the heap, saturating the garbage collector and turning a trivial summation into a major performance event. The fix is trivially long sum = 0L — primitive arithmetic, zero boxing, no GC. This is a factor-of-6x or more slower in benchmarks. The lesson: watch for autoboxing in tight loops. The compiler will not warn you.

Follow-up: What are the three differences between primitive and boxed types? (1) Boxed types can be null; primitives cannot. (2) Boxed type == compares identity, not value; primitive == compares value. (3) Boxed types carry memory and GC overhead; primitives do not.


Q2: When is it safe to use float or double? When must you use BigDecimal or int/long?

Context: Financial systems design, numeric correctness interviews.

Answer: float/double are appropriate for: scientific/engineering calculations where approximate answers are acceptable (physics simulation, statistics, ML), performance-critical numeric code where exact decimal representation is not required. Use BigDecimal for: monetary values, tax calculations, any computation where the result must be exactly correct in decimal. Use int/long (fixed-point) for: monetary values where range is bounded, when you need exact arithmetic and maximum performance. The key insight: 0.1 cannot be represented exactly in binary floating-point. new BigDecimal("0.1") represents it exactly; new BigDecimal(0.1) inherits the double’s imprecision.

Follow-up: Why must you use new BigDecimal("0.1") with a String, not new BigDecimal(0.1) with a double? Because 0.1 as a double is already an approximation (0.1000000000000000055511151231257827021181583404541015625). The BigDecimal(double) constructor captures this approximation exactly. The BigDecimal(String) constructor parses the decimal string precisely.


Q3: What are the three situations where you cannot use a for-each loop, and what should you use instead?

Context: Standard Java interview question, tests thoroughness of language knowledge.

Answer: (1) Destructive filtering: removing elements while iterating requires an Iterator.remove() call; for-each does not expose the iterator. Use explicit Iterator or Collection.removeIf(Predicate) (Java 8+). (2) Transforming: replacing elements in a list by index requires ListIterator.set(). Use a ListIterator. (3) Parallel iteration: walking two or more parallel collections at the same index requires explicit index management. Use a traditional indexed for loop. For all other cases, for-each is preferred — clearer, simpler, and compiles to the same bytecode as an explicit iterator loop.

Follow-up: Is there a performance difference between for-each and an explicit iterator loop? No — the compiler generates identical bytecode. Both use Iterator under the hood. For arrays, both generate the same index-based bytecode.


Q4: How does strong encapsulation in Java 9+ (modules) restrict reflection, and why does this matter for Item 65?

Context: Senior Java question about the module system and reflection limits.

Answer: Java 9 introduced the module system where packages are not accessible by default — they must be exported or opened. Reflection to non-exported packages throws InaccessibleObjectException. In Java 16, the --illegal-access option that softened this restriction was deprecated; in Java 17, it was removed entirely. This means code that used reflection to access JDK internals (sun.misc.Unsafe, com.sun. packages) breaks in Java 17 without explicit --add-opens JVM flags. This reinforces Item 65’s advice: use reflection only for legitimate public APIs. If you control the module, opens com.example.impl to framework.module; in module-info.java explicitly grants reflective access to specific consumers.

Follow-up: What is MethodHandles.privateLookupIn() and when is it used? It requests a Lookup with private access to a specific class, used by frameworks (Hibernate, Jackson) to access private fields reflectively. The accessed class must explicitly grant this in the module system.


Q5: What is the correct naming for record component accessors, and why does it differ from JavaBean naming?

Context: Java 16+ records naming — increasingly common interview question.

Answer: Record component accessors are named after the component with no prefix: record Point(int x, int y) has accessors point.x() and point.y(), not point.getX(). This differs from JavaBean convention (getX()) intentionally — records are data carriers, not mutable beans. They follow a more functional style. If you implement Comparable or serialize a record, the component accessors are what matter. Mixing record accessors with JavaBean-style code can cause issues with frameworks that expect getX() names (e.g., some serialization frameworks need configuration to use record accessors).

Follow-up: Can records implement interfaces? Yes — a record can implement any interface. It implicitly extends java.lang.Record and cannot extend any other class.


Q6: A developer claims that referring objects by their concrete class type is “safer” because you always know exactly what type you have. How do you respond?

Context: Code review debate about Item 64. Tests understanding of polymorphism and API design.

Answer: This is backwards. Declaring List<String> names is actually safer than ArrayList<String> names because: (1) The interface List documents the contract — what operations are valid. You are less likely to accidentally call ArrayList-specific methods. (2) When you need to switch to CopyOnWriteArrayList for thread safety or LinkedList for queue-like access, you change one line (the right side of the declaration). If you used ArrayList everywhere, you change every usage. (3) Callers of a method that accepts List can pass any List implementation; callers of a method that accepts ArrayList are locked in. The concrete type on the right side of = is implementation; the interface on the left is the contract. Keep the contract as general as possible.

Follow-up: When should you use the concrete type? When you need methods specific to that concrete class that are not on any interface (e.g., TreeMap.floorKey(), LinkedList.addFirst()). Use the concrete type only for the region of code that needs those extra methods.


Q7: Describe the var keyword in Java 10+ and its interaction with Items 57 and 64.

Context: Modern Java interview question; tests understanding of type inference pitfalls.

Answer: var enables local variable type inference — the type is inferred from the initializer. It applies to Item 57 (minimize local variable scope) by making tightly-scoped declarations more concise. However, it interacts problematically with Item 64 (refer to objects by interfaces) because var names = new LinkedList<String>() infers LinkedList<String>, not List<String>. This defeats the goal of coding to the interface. Rule of thumb: use var when the inferred type is (1) obvious from the right-hand side and (2) is already an interface type or immutable. Prefer explicit types when you want to express “this is a List” rather than “this is a LinkedList.” var does not affect field types, parameter types, or return types — only local variables.

Follow-up: Does var affect runtime performance? No — var is purely a compile-time feature. The bytecode is identical to explicit type declarations. The JVM never sees var.


Q8: Walk me through how you would investigate a “this code is slow” report without premature optimization (Item 67).

Context: Engineering process interview question about profiling and systematic optimization.

Answer: (1) Reproduce: confirm the slowness with a reproducible test case. (2) Profile: use a profiler (async-profiler, YourKit, JVisualVM) or flame graphs to identify where CPU time or memory is spent. Never guess — the bottleneck is almost never where you think. (3) Identify the bottleneck: is it CPU-bound? I/O-bound? GC pressure? Lock contention? Database? Network? Each requires a different solution. (4) Hypothesize and measure: form a hypothesis (e.g., “string concatenation in loop is O(n²)”), implement the fix, measure again with JMH for micro-benchmarks. (5) Verify: confirm the fix improves the actual metric (latency, throughput, GC pauses) in the real workload. (6) Review: did the optimization make the code harder to read? Document why the optimized code looks the way it does.

Follow-up: What is JMH and why is System.currentTimeMillis() timing unreliable for micro-benchmarks? JMH (Java Microbenchmark Harness) handles JVM warm-up (JIT compilation), dead code elimination, and measurement harness overhead. System.currentTimeMillis() misses JIT warm-up effects — the first iterations are interpreted; the benchmark might measure interpreter speed rather than JIT-compiled speed.


Key Takeaways

  • Item 57: Declare local variables as late and as close to their first use as possible. Always initialize at the point of declaration. Use var (Java 10+) when the type is obvious, but do not let it obscure interface types.
  • Item 58: For-each loops are clearer, safer, and compile to identical bytecode. Use explicit iterators only for destructive filtering, in-place transformation, or parallel index access.
  • Item 59: Know java.util, java.util.concurrent, java.time, java.nio.file, and java.net.http. Using the library means using code that is expert-written, extensively tested, and continuously optimized.
  • Item 60: float and double cannot represent many decimal fractions exactly. Use BigDecimal for monetary/exact-decimal computations. Use int/long fixed-point when you need speed and can manage scale manually.
  • Item 61: Three differences between primitive and boxed types: identity vs. value equality, nullability, and performance. Avoid autoboxing in tight loops. Use IntStream/LongStream/DoubleStream in streams to avoid boxing.
  • Item 62: Strings are for text. Use enum for fixed sets, numeric types for numbers, records/classes for structured data, and Object references for unique identity tokens.
  • Item 63: + in a loop is O(n²). Use StringBuilder, String.join(), Collectors.joining(), or text blocks (Java 15+). Single-expression + is fine — the compiler optimizes it.
  • Item 64: Code to interfaces, not implementations. Use List, Map, Set as variable, parameter, and return types. One-word change to switch implementations.
  • Item 65: Reflection sacrifices type safety, IDE support, and performance. Use it only to instantiate classes whose names come from configuration; program against interfaces for all subsequent use.
  • Item 66: JNI is not portable, not safe, hard to debug, and often not faster. Avoid it. Project Panama (Java 17 incubator, Java 21+ preview) provides a better alternative.
  • Item 67: Measure, don’t guess. Optimizations that are not based on profiler evidence are usually wrong. Algorithmic complexity beats micro-optimization every time.
  • Item 68: Follow Java naming conventions faithfully. Records use component-name accessors (no get prefix). Constants are SCREAMING_SNAKE_CASE. Type parameters are single uppercase letters or short descriptors.

Cross-references: ch05-generics (type parameters, wildcards), ch06-enums-annotations (enum naming), ch09-exceptions (exception handling in loops), ch11-concurrency (thread-safe alternatives), ch07-methods (parameter design)

Last Updated: 2026-05-10