Legacy Java You Will Meet at Work
Your first Java job will probably not start with a fresh project. Many Indian IT services companies, banks and insurers run applications written ten or fifteen years ago, and plenty of them still run on Java 8. You will be asked to fix a bug in a 2,000-line class full of Vector, Hashtable, SimpleDateFormat and casts, or to help upgrade it to a supported Java version.
This lesson is a field guide to that code. You will learn to recognise the older styles and APIs, understand the traps they hide, convert between old and new types at the edges, and upgrade a project step by step without breaking it. The rest of this course teaches the modern way; this lesson helps you live alongside the old way.
Recognising pre-generics and Java 1.0 code
Generics arrived in Java 5 (2004), lambdas and streams in Java 8 (2014). Code older than that looks like this:
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Vector;
public class OldStyle {
public static void main(String[] args) {
// Raw types: no generics, everything is Object
List names = new ArrayList();
names.add("Aarav");
names.add(42); // compiles: nothing stops it
Iterator it = names.iterator();
while (it.hasNext()) {
try {
String s = (String) it.next(); // cast needed, fails at run time
System.out.println("Name: " + s);
} catch (ClassCastException e) {
System.out.println("ClassCastException: a number was hiding in the list");
}
}
// Vector, Hashtable and Enumeration (Java 1.0 collections)
Vector<String> cities = new Vector<String>();
cities.addElement("Rohtak");
cities.addElement("Kochi");
Enumeration<String> e = cities.elements();
while (e.hasMoreElements()) {
System.out.println("City: " + e.nextElement());
}
Hashtable<String, Integer> pins = new Hashtable<String, Integer>();
pins.put("Rohtak", 124001);
try {
pins.put("Unknown", null);
} catch (NullPointerException ex) {
System.out.println("Hashtable rejects null values");
}
// Anonymous Comparator and explicit types: pre-Java 8 sorting
List<String> team = new ArrayList<String>();
Collections.addAll(team, "Meera", "Isha", "Kabir");
Collections.sort(team, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return a.length() - b.length();
}
});
System.out.println("Sorted by length: " + team);
// Boxing with the deprecated constructor
Integer old = new Integer(5);
System.out.println("new Integer(5) = " + old);
}
}Name: Aarav ClassCastException: a number was hiding in the list City: Rohtak City: Kochi Hashtable rejects null values Sorted by length: [Isha, Meera, Kabir] new Integer(5) = 5
A quick translation table for what you see here:
| Old | Modern replacement | Why |
|---|---|---|
List names = new ArrayList() (raw type) | List<String> names = new ArrayList<>() | the compiler catches wrong types |
Vector | ArrayList | Vector synchronises every call, which is slow and rarely enough for real thread safety |
Hashtable | HashMap, or ConcurrentHashMap for threads | same reason; Hashtable also rejects null |
Enumeration, elements(), keys() | Iterator or for-each | shorter and supports removal |
Stack | ArrayDeque | see Collections in Depth |
StringBuffer | StringBuilder | no needless synchronisation |
anonymous Comparator | Comparator.comparing(...) or a lambda | one readable line |
new Integer(5), new Double(x) | Integer.valueOf(5) or autoboxing | the constructors are deprecated for removal |
The compiler already knows about most of this. Compiling the file with javac -Xlint:all OldStyle.java lists every problem (shortened):
OldStyle.java:13: warning: [rawtypes] found raw type: List
List names = new ArrayList();
^
OldStyle.java:15: warning: [unchecked] unchecked call to add(E) as a member of the raw type List
names.add(42); // compiles: nothing stops it
^
OldStyle.java:54: warning: [removal] Integer(int) in Integer has been deprecated and marked for removal
Integer old = new Integer(5);
^Reading compiler warnings is the fastest way to map an old codebase. Turn them on in Maven or Gradle and fix them gradually, one area at a time.
Traps hiding in older code
Some bugs are typical of old code because the modern APIs were designed to avoid them:
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.TimeZone;
public class Traps {
public static void main(String[] args) {
// 1. Comparing Integer objects with ==
Integer a = 100, b = 100, c = 1000, d = 1000;
System.out.println("100 == 100: " + (a == b) + ", 1000 == 1000: " + (c == d)
+ ", equals: " + c.equals(d));
// 2. Arrays.asList is a fixed-size view of the array
String[] arr = {"Pune", "Agra"};
List<String> view = Arrays.asList(arr);
view.set(0, "Kochi");
System.out.println("Array changed too: " + arr[0]);
try {
view.add("Jaipur");
} catch (UnsupportedOperationException e) {
System.out.println("Arrays.asList cannot grow");
}
// 3. Legacy dates: build 15 August 2026 the old way
TimeZone ist = TimeZone.getTimeZone("Asia/Kolkata");
Calendar cal = Calendar.getInstance(ist);
cal.clear();
cal.setTimeZone(ist);
cal.set(2026, Calendar.AUGUST, 15); // Calendar.AUGUST is 7
Date oldDate = cal.getTime();
SimpleDateFormat oldFormat = new SimpleDateFormat("dd-MM-yyyy");
oldFormat.setTimeZone(ist);
System.out.println("Calendar.AUGUST = " + Calendar.AUGUST + ", formatted: " + oldFormat.format(oldDate));
// 4. Converting at the boundary: Date -> Instant -> LocalDate, and back
Instant instant = oldDate.toInstant();
LocalDate modern = instant.atZone(ZoneId.of("Asia/Kolkata")).toLocalDate();
System.out.println("As LocalDate: " + modern + ", day: " + modern.getDayOfWeek());
System.out.println("Formatted the new way: "
+ modern.format(DateTimeFormatter.ofPattern("dd-MM-yyyy")));
Date backAgain = Date.from(instant);
System.out.println("Round trip equal? " + backAgain.equals(oldDate));
java.sql.Date forJdbc = java.sql.Date.valueOf(modern);
System.out.println("java.sql.Date: " + forJdbc + ", back: " + forJdbc.toLocalDate());
}
}100 == 100: true, 1000 == 1000: false, equals: true Array changed too: Kochi Arrays.asList cannot grow Calendar.AUGUST = 7, formatted: 15-08-2026 As LocalDate: 2026-08-15, day: SATURDAY Formatted the new way: 15-08-2026 Round trip equal? true java.sql.Date: 2026-08-15, back: 2026-08-15
==onIntegercompares object references. Java caches boxed values from -128 to 127, so small numbers appear to work in testing and large amounts fail in production. Always useequals, or compareintvalues.Arrays.asListreturns a fixed-size list backed by the array: changes flow both ways, andaddthrows. Modern code usesList.of(...)(unmodifiable) ornew ArrayList<>(List.of(...)).Calendarmonths start at 0 (Calendar.AUGUSTis 7), andSimpleDateFormatis not thread-safe, so a sharedstaticformatter in a web application can silently produce wrong dates under load.DateTimeFormatteris immutable and safe to share. The Date & Time lesson coversjava.timein depth.- Convert at the boundary. You often cannot change an old API that takes or returns
Date. Convert once where you call it:date.toInstant(),Date.from(instant),java.sql.Date.valueOf(localDate)andsqlDate.toLocalDate(). Everything inside your new code then usesjava.time.
Upgrading an old project
Java 8 is old but still widely deployed, and the LTS releases after it are 11, 17, 21 and 25. Upgrading brings faster garbage collectors, records, text blocks, pattern matching, virtual threads and security fixes. Teams usually upgrade like this:
- Get a green build and tests first. Without tests, you cannot tell whether the upgrade broke something; add characterisation tests that pin down today's behaviour.
- Change only the JDK first, not the code style. Build and run with the new JDK while still compiling for the old level, then raise
maven.compiler.releaseor the Gradle toolchain. - Fix what the JDK removed. Java 11 removed the Java EE and CORBA modules (JEP 320), so code using JAXB (
javax.xml.bind) or JAX-WS must add those libraries as normal dependencies. Java 15 removed the Nashorn JavaScript engine (JEP 372). Java 17 strongly encapsulated JDK internals (JEP 403), so old libraries that reached intosun.*classes must be updated. Java 24 permanently disabled the Security Manager (JEP 486). - Run
jdeps(it ships with the JDK) on your JAR to list dependencies on JDK internals, and update old libraries, which is often the biggest part of the work. - Move from
javax.*tojakarta.*when upgrading frameworks: Jakarta EE 9 renamed the enterprise packages, so Spring Boot 3 and later usejakarta.servlet,jakarta.persistenceand so on. - Modernise gradually. Replace raw types, old collections and
Datewhen you touch a file for another reason, following the "leave the code a little better than you found it" rule. Big-bang rewrites of working systems are risky.
You will also meet methods such as finalize() (deprecated for removal since Java 18, JEP 421; use try-with-resources or java.lang.ref.Cleaner) and Thread subclasses that override run() (prefer an ExecutorService or virtual threads).
Common mistakes
- Rewriting everything at once. Upgrade the JDK first, keep behaviour the same, then modernise in small steps.
- Mixing
DateandLocalDatethroughout new code. Convert once at the edge of the legacy API. - "Fixing"
Integer ==bugs by testing with small numbers. Values from -128 to 127 hide the bug; test with large values. - Assuming
Vectormakes code thread-safe. Each call is synchronised, but check-then-act sequences still race. - Suppressing warnings instead of reading them.
@SuppressWarnings("unchecked")belongs only on code you have checked by hand.
Key points
- Raw types,
Vector,Hashtable,Enumeration,Stack,StringBufferand anonymous comparators mark pre-Java 8 code; each has a modern replacement. javac -Xlint:alllists raw types, unchecked calls and APIs deprecated for removal.- Compare boxed numbers with
equals;Arrays.asListis fixed-size;Calendarmonths start at 0;SimpleDateFormatis not thread-safe. - Convert
DatetoInstantandLocalDateat the boundary and usejava.timeinside. - Upgrade in steps: tests, then the JDK, then removed modules and libraries, then
javaxtojakarta, then gradual modernisation.
Exercise
Modernise this Java 1.4-style fee report without changing its output. Use generics, Map.of, a sorted map, streams or a for-each loop, and no casts:
Hashtable fees = new Hashtable();
fees.put("Rohan", new Integer(12000));
fees.put("Ananya", new Integer(0));
fees.put("Kabir", new Integer(4500));
fees.put("Meera", new Integer(8000));
Vector due = new Vector();
Enumeration keys = fees.keys();
while (keys.hasMoreElements()) {
String name = (String) keys.nextElement();
Integer amount = (Integer) fees.get(name);
if (amount.intValue() > 0) {
due.addElement(name);
}
}
Collections.sort(due, new Comparator() {
public int compare(Object a, Object b) {
return ((String) a).compareTo((String) b);
}
});
StringBuffer sb = new StringBuffer();
int total = 0;
for (int i = 0; i < due.size(); i++) {
String name = (String) due.elementAt(i);
int amount = ((Integer) fees.get(name)).intValue();
sb.append(name).append(": Rs ").append(amount).append("\n");
total += amount;
}
sb.append("Total due: Rs ").append(total);
System.out.println(sb.toString());Show solution
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
public class FeeReport {
public static void main(String[] args) {
Map<String, Integer> fees = Map.of(
"Rohan", 12000, "Ananya", 0, "Kabir", 4500, "Meera", 8000);
Map<String, Integer> due = fees.entrySet().stream()
.filter(e -> e.getValue() > 0)
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue,
(x, y) -> x, TreeMap::new));
String lines = due.entrySet().stream()
.map(e -> e.getKey() + ": Rs " + e.getValue())
.collect(Collectors.joining("\n"));
int total = due.values().stream().mapToInt(Integer::intValue).sum();
System.out.println(lines + "\nTotal due: Rs " + total);
}
}Kabir: Rs 4500 Meera: Rs 8000 Rohan: Rs 12000 Total due: Rs 24500
Both versions print exactly the same report, which you should check by running them side by side before deleting the old code. The TreeMap replaces the manual sort, the generic types remove every cast, and the old version's four compiler warnings disappear.
Quick Quiz
1. Why can Integer a = 1000, b = 1000; a == b be false?
Use equals to compare boxed values.
2. What is the modern replacement for Hashtable in single-threaded code?
HashMap avoids needless synchronisation; use ConcurrentHashMap when threads share the map.
3. You must call an old method that returns java.util.Date. What is the best approach?
Converting at the boundary keeps legacy types out of new code.
4. A project moves from Java 8 to 11 and fails because javax.xml.bind is missing. Why?
JEP 320 removed the Java EE and CORBA modules from the JDK in Java 11.
Learn Java with a mentor
Free lessons get you started. Our Java, C & C++ Programming training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
