Appendix B - The Legacy Java Field Guide¶
Roughly half the Java you will be paid to read was written before Java 8. The other half was written by someone who learned Java before Java 8. This appendix is the field guide to the sediment - what each legacy idiom is, why it exists, why it's wrong now, and what to replace it with.
If you finish the main curriculum without this appendix, you'll trip over Vector on your first day at a real job. Don't.
B.1 Collections and Data Types¶
Vector, Hashtable, Stack¶
- What: Synchronized collections from Java 1.0. Every method takes a lock.
- Why it exists: Java 1.0 had no
java.util.concurrent. - Why it's wrong: Lock overhead per call; coarse-grained; iteration is not atomic anyway.
- Replace with:
ArrayList+ explicit synchronization, orCollections.synchronizedList, or - almost always the right answer -ConcurrentHashMap/CopyOnWriteArrayList/ArrayDeque.
Enumeration<E>¶
- What: Pre-
Iteratoriteration interface.hasMoreElements()/nextElement(). - Replace with:
Iterator(or, better, an enhancedforloop, or aStream).
Date, Calendar, SimpleDateFormat¶
- What: Java 1.0/1.1 date/time API.
- Why it's wrong: Mutable (
Date.setTime); not thread-safe (SimpleDateFormathas a mutableCalendarfield - every shop has been bitten by this); confused semantics (Dateis "instant" but pretends to be calendar;Calendarlives in default timezone; months are 0-indexed). - Replace with:
java.time(JSR-310, since 8).Instant,Duration,LocalDate,LocalDateTime,ZonedDateTime,OffsetDateTime,Period,DateTimeFormatter. Never mix the two in new code; for boundaries with legacy APIs, convert at the seam (Date.from(instant),instant.atZone(...))..
Raw types (List, Map without generics)¶
- What: Pre-Java-5 collections.
- Why it's wrong: No compile-time type safety; "unchecked" warnings; possible heap pollution.
- Replace with: Parameterized types. If maintaining old code, add generics gradually; the compiler will guide you.
Arrays-of-Object as poor-man's tuples¶
- Replace with:
record(since 14, final 16). Arecord Pair<A,B>(A a, B b) {}is the answer.
B.2 Concurrency¶
Thread t = new Thread(...); t.start();¶
- What: Direct OS-thread spawning.
- Why it's wrong: Unbounded creation kills servers; no naming, no lifecycle, no result, no cancellation.
- Replace with: An
ExecutorService(now usuallyExecutors.newVirtualThreadPerTaskExecutor()since 21), orThread.startVirtualThreadfor fire-and-forget tasks.
Thread.stop, Thread.suspend, Thread.resume¶
- What: Deprecated since Java 1.2 (!).
Thread.stopremoved in 20+. - Replace with: Cooperative cancellation via
volatile boolean running, orThread.interrupt()+ handlingInterruptedException.
synchronized everything¶
- What: The intrinsic lock attached to every Java object.
- Why it's complicated now: Pre-24, virtual threads inside
synchronizedblocks pin to the carrier thread. JEP 491 (24+) fixes this for most cases. - Replace with for blocking code paths:
ReentrantLock(orStampedLockfor read-heavy). For Loom-friendly code on pre-24 JVMs, preferReentrantLockoversynchronizedif the critical section may block. - Keep
synchronizedfor: small, non-blocking critical sections; pre-existing public APIs; cases where you know virtual threads aren't in play.
wait() / notify() / notifyAll()¶
- What: The intrinsic-lock-bound condition-variable mechanism.
- Why it's wrong: Fragile (spurious wakeups, lost notifications, signal-all-vs-signal); no
tryWait; hard to compose. - Replace with:
BlockingQueuefor producer/consumer,Semaphorefor resource pools,CountDownLatchfor one-shot signals,Phaserfor multi-phase barriers,Conditionon aReentrantLockif you must.
sun.misc.Unsafe¶
- What: Internal API for low-level memory ops; widely abused for 20 years.
- Why it's wrong: Internal, unsupported, increasingly restricted, deprecation-for-removal in progress (JEP 471 + follow-ups).
- Replace with:
VarHandle(9+) for atomic ops and ordering,MemorySegment(Panama, stable in 22) for off-heap memory.
ThreadLocal everywhere¶
- What: Per-thread storage.
- Why it's complicated: Classloader leaks in app servers; doesn't compose with virtual threads cheaply (each VT has its own); semantically a hidden global.
- Replace with:
ScopedValue(final in 25) for bounded scopes. KeepThreadLocalonly where required by external API (SLF4J's MDC, Spring'sSecurityContextHolderuntil they migrate).
B.3 Exceptions and Error Handling¶
throws Exception on every method¶
- Why it's wrong: Defeats checked-exception type safety; forces callers to catch-all.
- Replace with: Specific checked exceptions for genuinely recoverable I/O at boundaries;
RuntimeExceptionsubclasses inside business logic.
catch (Exception e) { e.printStackTrace(); }¶
- The most common production code smell in Java. Replace with:
- Logged, contextualized:
log.error("failed to X for {}", id, e); - Or rethrown with context:
throw new MyDomainException("X failed for " + id, e); - Never swallowed silently.
Checked exceptions in stream pipelines¶
- What:
Stream.map(x -> doIO(x))doesn't compile ifdoIOthrows checked. - Replace with: Wrap in a
RuntimeException, or use a library (jOOλ,Vavr), or - increasingly - skip streams and use an enhancedforloop with virtual threads behind it.
finally { ... close(); ... }¶
- Replace with: try-with-resources (since 7). Any
AutoCloseable. Resources are closed in reverse order; suppressed exceptions are attached automatically.
finalize()¶
- What: Deprecated since 9; for-removal trajectory (JEP 421 deprecates for removal).
- Why it's wrong: Unpredictable timing, can resurrect objects, performance disaster.
- Replace with:
try-with-resources+ explicitclose(). For native resources requiring eventual cleanup,java.lang.ref.Cleaner(since 9).
B.4 I/O and Serialization¶
java.io.Serializable¶
- Why it's wrong: Notorious source of security vulnerabilities (deserialization gadgets); fragile schema; coupled to internal field names.
- Replace with: JSON (Jackson), protobuf, Avro, or any explicit schema. Never accept untrusted
Serializableinput. JEP 154 (Serialization 2.0 / record-based serialization) has been explored but not delivered as of 25.
FileInputStream / FileOutputStream patterns¶
- Replace with:
java.nio.file.Files.readAllBytes,Files.lines,Files.newBufferedReader. ThePath-based API is the modern one.
Reading text without specifying charset¶
- Why it's wrong: Platform-default charset varies (and JEP 400 changed it to UTF-8 in 18, but legacy assumptions remain).
- Replace with: Always specify
StandardCharsets.UTF_8.
JNI¶
- What: The original native-interop mechanism.
- Why it's complicated: Boilerplate (
javah/javac -h), unsafe by construction, slow ergonomics. - Replace with: Panama / Foreign Function & Memory API (
java.lang.foreign, stable in 22+). Bind a C library declaratively, no C code on your side.
B.5 Frameworks and Ecosystem¶
J2EE / Java EE → Jakarta EE¶
- What: In 2018 Oracle transferred Java EE to the Eclipse Foundation, renamed "Jakarta EE." All
javax.*packages becamejakarta.*starting Jakarta EE 9 (2020). Spring Boot 3 (2022) made the jump. - Consequence: Mixing pre-Spring-Boot-3 libraries (still
javax.servlet) with Spring Boot 3 (which expectsjakarta.servlet) does not work. This is the migration headache of the early 2020s and still appears in stale codebases.
EJB¶
- Replace with: Spring or Quarkus or Micronaut. Nobody writes new EJBs.
JSP / JSTL / Struts / JSF¶
- Replace with: A modern templating engine (Thymeleaf, Freemarker) or a SPA frontend with a JSON API behind it.
Applets / Java Web Start¶
- Removed. If you see these, the project is older than the engineer maintaining it.
Log4j 1.x, java.util.logging (JUL) directly¶
- Replace with: SLF4J facade + Logback or Log4j2. (Log4j 1.x is EOL and had its own CVE history.)
JUnit 4 (@RunWith, @Rule)¶
- Replace with: JUnit 5 (Jupiter). Migration is mostly mechanical (
@Before→@BeforeEach, etc.). Vintage engine can run both side-by-side during migration.
Mockito 1.x style (@InjectMocks-everywhere)¶
- Modernize to Mockito 5+, prefer constructor injection in tests, use
@ExtendWith(MockitoExtension.class).
B.6 Language Features That Aged¶
enum as "the only ADT we have"¶
- Modernize with:
sealed interface+ records for true sum types.
Visitor pattern for ADT dispatch¶
- Replace with: Pattern matching for
switchover sealed types.
Anonymous inner classes for callbacks¶
- Replace with: Lambdas (since 8) and method references.
Telescoping constructors / Builder boilerplate¶
- For DTOs: records (with compact constructor for validation).
- For complex objects: keep the builder pattern, but consider
record+withers(the new pattern in modern stdlib code).
Lombok everywhere¶
- The 2026 stance: records replace
@Data/@Valuefor immutables.@Builderstill has no JDK equivalent.@Slf4jis convenience. Use Lombok sparingly and only where the JDK has no answer. Be aware: Lombok is a build-time bytecode-rewriter and breaks IDE/tooling pipelines occasionally.
Reflection-driven everything¶
- Modernize with:
MethodHandle/VarHandlefor performance; build-time DI (Quarkus, Micronaut) when native-image matters.
B.7 Removed / Deprecated for Removal in Modern JDKs¶
Keep this list mental - when you see them in old code, you know you're rewriting:
- CMS GC: removed in 14. ParallelOldGC: removed in 15. Code that sets
-XX:+UseConcMarkSweepGCwill fail to start on modern JVMs. - Nashorn (the JS engine in the JDK): removed in 15.
- Java applets,
Applet,JApplet: removed in 17. SecurityManager: deprecated for removal in 17 (JEP 411); removal in progress. Most code that relied on it had no business doing so.- RMI activation: removed in 17.
sun.misc.Unsafememory-access methods: deprecated for removal (JEP 471). Migrate toVarHandle+MemorySegment.- Pre-
java.timedate API: not removed, but treated as legacy; converted at the boundary only. finalize(): deprecated for removal.- The "old" Reflective access to
java.baseinternals: progressively closed since 9; by 17,--illegal-access=denyis enforced.
B.8 Migration Strategy¶
When inheriting a Java 8 (or 11) codebase, in order:
- Bump the JDK to 21 LTS (or 25 LTS). Don't refactor; just upgrade. Fix breakage at the seam (
javax.*→jakarta.*if you're moving to Spring Boot 3, removed flags, removed APIs). - Run the static analyzers (ErrorProne, SpotBugs, IntelliJ inspections). Knock down the warning count by 80% in low-risk passes.
- Modernize at the boundary first. New code uses records, sealed types, pattern matching, virtual threads. Old code stays until you have a reason to touch it.
- Migrate
Date/Calendaronly at the seams. Internal code goesInstantend-to-end; only at the external API does conversion happen. - Replace
Thread-spawning with executors. Bound them. Name them. Shut them down. - Bring up observability before refactoring concurrency. You cannot safely change concurrency you cannot measure.
- Then, and only then, consider larger refactors (Spring Boot 2 → 3, JPA → jOOQ, reactive → Loom, etc.).
The legacy is not the enemy. The legacy is what's keeping the lights on. Modernize with respect - and with tests.