skip to content
everything.effectiveJava.generalProgramming

general programming

items 57–68: scope, libraries, floats, boxing, strings, naming.

chapter
chapter 16.08
reading time
11 min read

items 57 to 68 are the everyday habits: keep variables in the smallest scope, prefer for-each, use the library before writing your own, never use double for money, prefer primitives to boxes, don't stringly-type things, build strings with a StringBuilder in loops, and measure before optimizing.

twelve small items. the JDK grew a lot since 2018, so amber shows up more here than anywhere else.

writing it

item 57 · the smallest scope wins

declare a variable where it's first used, initialize it right there, and keep methods small. a variable that lives longer than it's needed is a variable someone can misuse later. for loops are better than while loops for exactly this reason: the loop variable dies with the loop.

don'ti2 outlives its loop
java
Iterator<String> i = a.iterator();
while (i.hasNext()) use(i.next());
 
Iterator<String> i2 = b.iterator();
while (i.hasNext()) use(i2.next());
// copy-paste bug: checks i, not i2.
// compiles, silently does nothing
doscoped to the loop
java
for (String s : a) use(s);
for (String s : b) use(s);
// the old s is gone. nothing to
// confuse it with

item 58 · for-each over index loops

an index or an iterator is one more variable to get wrong (and nested loops multiply the chances). for-each works on arrays and on anything Iterable, and costs nothing. use an explicit iterator or index only when you need to remove while iterating (or just use removeIf), replace elements (replaceAll), or walk several collections in lockstep.

item 59 · the library already did it

library code is written by experts, used by millions, and gets faster with every JDK, for free. the classic trap is random numbers: Math.abs(rnd.nextInt()) % n is biased, and is negative for one unlucky value. the library's version isn't. some things people still rewrite by hand:

you needthe JDK has itsince
a random int in a rangerng.nextInt(1, 7), RandomGenerator.of(...)17
modulo that's never negativeMath.floorMod(-7, 3) → 28
bytes ↔ hexHexFormat.of().formatHex(bytes)17
read a whole fileFiles.readString(path)11
an http callHttpClient.newHttpClient()11
repeat a string"ab".repeat(3)11
two results from one stream passCollectors.teeing(a, b, merge)12
clamp a numberMath.clamp(x, lo, hi)21
Main.java
import java.util.random.RandomGenerator;
 
void main() {
    var rng = RandomGenerator.of("L64X128MixRandom");
    int die = rng.nextInt(1, 7);              // 1..6, unbiased
    IO.println(die >= 1 && die <= 6);
    IO.println(Math.floorMod(-7, 3) + " " + (-7 % 3));
    IO.println("ab".repeat(3) + " " + HexFormat.of().formatHex(new byte[] { (byte) 0xCA, (byte) 0xFE }));
}
output, from jdk 25
true
2 -1
ababab cafe

this one needs a real JVM (a library class the browser runtime doesn't have), so it doesn't run in the browser. copy it into java Main.java to try it.

know java.lang, java.util, java.util.function, java.util.stream, java.util.concurrent and java.io/java.nio.file well; skim the release notes of each new JDK (that's what java 8 → 27 is for).

numbers and strings

item 60 · no double for money

float and double are binary fractions. 0.1 has no exact binary representation, the same way 1/3 has no exact decimal one, so errors creep in with every operation:

double0.10x3fb999999999999a
sign · 1 bitexponent · 11 bitsmantissa · 52 bits
actually stored: 0.10000000000000000555…
the real bits of 0.1 as a double. the fraction repeats forever in binary, so it gets rounded to the nearest of 2⁵² steps. close, never exact.
Main.java
import java.math.BigDecimal;
 
void main() {
    double wallet = 1.00;
    for (int i = 0; i < 10; i++) wallet -= 0.10;  // ten 10-cent candies
    IO.println(wallet);                           // not zero
 
    BigDecimal exact = new BigDecimal("1.00");    // from a String, never from a double
    for (int i = 0; i < 10; i++) exact = exact.subtract(new BigDecimal("0.10"));
    IO.println(exact);
 
    long cents = 100;                             // or: count the smallest unit in a long
    for (int i = 0; i < 10; i++) cents -= 10;
    IO.println(cents);
}
output
1.3877787807814457E-16
0.00
0

BigDecimal is exact and lets you choose rounding, at the cost of speed and verbosity. int or long cents are fast and exact if you track the decimal point yourself (a long counts up to about 92 quadrillion dollars in cents). why floats work this way is in floating point.

item 61 · primitives over boxes

int and Integer differ in three ways that bite: boxes have identity (so == compares references), boxes can be null (so unboxing can throw), and boxes cost allocations (item 6).

predict the output
java
void main() {
    Map<String, Integer> stock = new HashMap<>(Map.of("pen", 1000, "cap", 100));
    Integer pens = stock.get("pen"), want = 1000;
    Integer caps = stock.get("cap"), few = 100;
    IO.println((caps == few) + " " + (pens == want));
}

and the null case: int ink = stock.get("ink"); compiles fine, then unboxes a null:

text
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()"
    because the return value of "java.util.Map.get(Object)" is null

use boxes only where you must: collection elements, type arguments, and reflection.

item 62 · strings aren't every type

a string is a fine type for text. it's a bad stand-in for numbers, enums, booleans, keys made of several parts ("user:" + id + ":" + region), or capabilities. parsing it back out is fragile, and the compiler can't help.

don'ta key glued from strings
java
String key = className + "#" + userId;
cache.put(key, value);
// what if className contains '#'?
doa type that says what it is
java
record CacheKey(String className,
                long userId) {}
cache.put(new CacheKey(cls, id), value);
// equals and hashCode for free

item 63 · concatenation in a loop is quadratic

strings are immutable, so s += x copies all of s every time. inside a loop that's 1 + 2 + 3 + ... + n copies, which is n²/2:

+= , n = 5,0004.03 ms
+= , n = 10,00014.45 ms
+= , n = 20,00054.7 ms
+= , n = 40,000207.62 ms
StringBuilder, n = 40,0000.28 ms
log scale
building a string of n lines. best of 5, jdk 25, arm mac. every time n doubles, += takes about 4x longer; StringBuilder barely moves. log scale.
don'tcopies everything, every time
java
String report = "";
for (var line : lines)
    report += line + "\n";
doone growing buffer
java
var sb = new StringBuilder();
for (var line : lines)
    sb.append(line).append('\n');
String report = sb.toString();
// or: String.join("\n", lines)

types and tools

item 64 · declare the interface, not the class

declare List<Order> orders, not ArrayList<Order> orders, even when you create an ArrayList. code that talks to the interface can switch implementations (to a LinkedList, a CopyOnWriteArrayList, an unmodifiable view) by changing one line. use the class only when you need its specific behavior (like LinkedHashMap's order) and no interface captures it.

item 65 · reflection is a last resort

reflection gives up compile-time checking, is verbose, and is slower. use it only to create objects of classes unknown at compile time (plugins, frameworks), then talk to them through an interface or superclass known at compile time:

java
Class<? extends Codec> cls = Class.forName(name).asSubclass(Codec.class);
Codec codec = cls.getDeclaredConstructor().newInstance();   // reflection stops here
codec.encode(data);                                          // normal, checked calls from here

item 66 · native code, rarely

native methods used to be the way to reach platform APIs, legacy libraries, or (supposedly) speed. today the JVM is fast enough that native code is rarely a performance win, and every native call is a chance for memory corruption that java can't catch.

Main.java
import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
 
void main() throws Throwable {
    Linker linker = Linker.nativeLinker();
    MethodHandle strlen = linker.downcallHandle(
        linker.defaultLookup().findOrThrow("strlen"),
        FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS));
 
    try (Arena arena = Arena.ofConfined()) {
        MemorySegment cString = arena.allocateFrom("hello");   // freed when the arena closes
        long n = (long) strlen.invokeExact(cString);
        IO.println(n);
    }
}
output, from jdk 25
5

this one needs a real JVM (the foreign function api), so it doesn't run in the browser. copy it into java Main.java to try it.

(java prints a warning about calling a restricted method; run with --enable-native-access=ALL-UNNAMED to acknowledge it.)

item 67 · measure before you optimize

write good programs, not fast ones: clean architecture and good APIs are what let you fix performance later. hard-to-change decisions (an API that forces copies, a wire format) are where to think about speed up front. everything else: measure first, optimize the part the profiler points at, measure again. the JIT makes guessing especially unreliable in java.

item 68 · name things the java way

whatconventionexample
package, modulelowercase, reverse domaincom.example.billing
class, interface, record, enumUpperCamelCase nounInvoiceLine, Comparable
methodlowerCamelCase verbtoString, isEmpty, asList
field, locallowerCamelCaselineCount
constant (static final, immutable)UPPER_SNAKEMAX_LINES
type parameterone capital letterT, E, K, V, R

grammar matters too: is/has for booleans, to for conversions (toList), as for views (asList), of/from/valueOf for factories (item 1).

keylines
  • declare variables at first use, in the smallest scope. for-each wherever it fits.
  • check the JDK before writing a utility. it's probably there and faster.
  • money is BigDecimal or long cents, never double.
  • == on boxes compares references. unboxing null throws. prefer primitives.
  • give structured data a type. build strings in loops with a StringBuilder.
  • refer to objects by interfaces; reflection and native code only as a last resort; measure before optimizing.