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.
- item 57the smallest scope winsvar
- item 58for-each over index loops
- item 59the library already did itRandomGenerator, HexFormat, Files.readString
- item 60no double for money
- item 61primitives over boxesvalhalla
- item 62strings aren't every typerecords, enums
- item 63concatenation in a loop is quadraticindy string concat
- item 64declare the interface, not the classvar
- item 65reflection is a last resortstrong encapsulation
- item 66native code, rarelyFFM API
- item 67measure before you optimizeJMH, JFR
- item 68name things the java way
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.
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 nothingfor (String s : a) use(s);
for (String s : b) use(s);
// the old s is gone. nothing to
// confuse it withitem 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 need | the JDK has it | since |
|---|---|---|
| a random int in a range | rng.nextInt(1, 7), RandomGenerator.of(...) | 17 |
| modulo that's never negative | Math.floorMod(-7, 3) → 2 | 8 |
| bytes ↔ hex | HexFormat.of().formatHex(bytes) | 17 |
| read a whole file | Files.readString(path) | 11 |
| an http call | HttpClient.newHttpClient() | 11 |
| repeat a string | "ab".repeat(3) | 11 |
| two results from one stream pass | Collectors.teeing(a, b, merge) | 12 |
| clamp a number | Math.clamp(x, lo, hi) | 21 |
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 }));
}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:
0.1 as a double. the fraction repeats forever in binary, so it gets rounded to the nearest of 2⁵² steps. close, never exact.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);
}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).
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:
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()"
because the return value of "java.util.Map.get(Object)" is nulluse 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.
String key = className + "#" + userId;
cache.put(key, value);
// what if className contains '#'?record CacheKey(String className,
long userId) {}
cache.put(new CacheKey(cls, id), value);
// equals and hashCode for freeitem 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:
String report = "";
for (var line : lines)
report += line + "\n";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:
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 hereitem 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.
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);
}
}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
| what | convention | example |
|---|---|---|
| package, module | lowercase, reverse domain | com.example.billing |
| class, interface, record, enum | UpperCamelCase noun | InvoiceLine, Comparable |
| method | lowerCamelCase verb | toString, isEmpty, asList |
| field, local | lowerCamelCase | lineCount |
constant (static final, immutable) | UPPER_SNAKE | MAX_LINES |
| type parameter | one capital letter | T, 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).
- 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
BigDecimalorlongcents, neverdouble. ==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.