Saltar a contenido

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, or Collections.synchronizedList, or - almost always the right answer - ConcurrentHashMap / CopyOnWriteArrayList / ArrayDeque.

Enumeration<E>

  • What: Pre-Iterator iteration interface. hasMoreElements() / nextElement().
  • Replace with: Iterator (or, better, an enhanced for loop, or a Stream).

Date, Calendar, SimpleDateFormat

  • What: Java 1.0/1.1 date/time API.
  • Why it's wrong: Mutable (Date.setTime); not thread-safe (SimpleDateFormat has a mutable Calendar field - every shop has been bitten by this); confused semantics (Date is "instant" but pretends to be calendar; Calendar lives 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). A record 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 usually Executors.newVirtualThreadPerTaskExecutor() since 21), or Thread.startVirtualThread for fire-and-forget tasks.

Thread.stop, Thread.suspend, Thread.resume

  • What: Deprecated since Java 1.2 (!). Thread.stop removed in 20+.
  • Replace with: Cooperative cancellation via volatile boolean running, or Thread.interrupt() + handling InterruptedException.

synchronized everything

  • What: The intrinsic lock attached to every Java object.
  • Why it's complicated now: Pre-24, virtual threads inside synchronized blocks pin to the carrier thread. JEP 491 (24+) fixes this for most cases.
  • Replace with for blocking code paths: ReentrantLock (or StampedLock for read-heavy). For Loom-friendly code on pre-24 JVMs, prefer ReentrantLock over synchronized if the critical section may block.
  • Keep synchronized for: 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: BlockingQueue for producer/consumer, Semaphore for resource pools, CountDownLatch for one-shot signals, Phaser for multi-phase barriers, Condition on a ReentrantLock if 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. Keep ThreadLocal only where required by external API (SLF4J's MDC, Spring's SecurityContextHolder until 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; RuntimeException subclasses 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 if doIO throws checked.
  • Replace with: Wrap in a RuntimeException, or use a library (jOOλ, Vavr), or - increasingly - skip streams and use an enhanced for loop 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 + explicit close(). 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 Serializable input. 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. The Path-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 became jakarta.* 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 expects jakarta.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 switch over 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/@Value for immutables. @Builder still has no JDK equivalent. @Slf4j is 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 / VarHandle for 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:+UseConcMarkSweepGC will 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.Unsafe memory-access methods: deprecated for removal (JEP 471). Migrate to VarHandle + MemorySegment.
  • Pre-java.time date API: not removed, but treated as legacy; converted at the boundary only.
  • finalize(): deprecated for removal.
  • The "old" Reflective access to java.base internals: progressively closed since 9; by 17, --illegal-access=deny is enforced.

B.8 Migration Strategy

When inheriting a Java 8 (or 11) codebase, in order:

  1. 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).
  2. Run the static analyzers (ErrorProne, SpotBugs, IntelliJ inspections). Knock down the warning count by 80% in low-risk passes.
  3. 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.
  4. Migrate Date/Calendar only at the seams. Internal code goes Instant end-to-end; only at the external API does conversion happen.
  5. Replace Thread-spawning with executors. Bound them. Name them. Shut them down.
  6. Bring up observability before refactoring concurrency. You cannot safely change concurrency you cannot measure.
  7. 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.

Comments