20 Java Backend Pitfalls You Must Avoid
This article compiles and explains twenty frequent Java backend development mistakes—from classic NullPointer exceptions and incorrect date formatting to BigDecimal precision issues, improper use of ConcurrentHashMap, ThreadLocal data leakage, misuse of Arrays.asList, transaction pitfalls, and serialization quirks—providing code examples and best‑practice solutions to help developers write safer, more reliable code.
Preface
Recently I read GeekTime's "100 Common Java Business Development Errors" and combined it with some pitfalls I encountered, writing this summary to help everyone.
1. Six Typical NullPointer Issues
Wrapper type NullPointer
Chained call NullPointer
Equals method left NullPointer
ConcurrentHashMap does not support null keys or values
Directly accessing collection or array elements
Directly accessing object properties
1.1 Wrapper type NullPointer
<code>public class NullPointTest {
public static void main(String[] args) throws InterruptedException {
System.out.println(testInteger(null));
}
private static Integer testInteger(Integer i) {
return i + 1; // wrapper type may be null, causing NullPointerException
}
}
</code>1.2 Chained call NullPointer
<code>public class NullPointTest {
public static void main(String[] args) {
// fruitService.getAppleService() may be null, causing NullPointerException
fruitService.getAppleService().getWeight().equals("OK");
}
}
</code>1.3 Equals method left NullPointer
<code>public class NullPointTest {
public static void main(String[] args) {
String s = null;
if (s.equals("666")) { // s may be null, causing NullPointerException
System.out.println("Public account: ...");
}
}
}
</code>1.4 ConcurrentHashMap does not support null keys/values
<code>public class NullPointTest {
public static void main(String[] args) {
Map map = new ConcurrentHashMap<>();
String key = null;
String value = null;
map.put(key, value);
}
}
</code>1.5 Directly accessing collection or array elements
<code>public class NullPointTest {
public static void main(String[] args) {
int[] array = null;
List list = null;
System.out.println(array[0]); // NullPointerException
System.out.println(list.get(0)); // NullPointerException
}
}
</code>1.6 Directly accessing object properties
<code>public class NullPointTest {
public static void main(String[] args) {
User user = null;
System.out.println(user.getAge()); // NullPointerException
}
}
</code>2. Date format "YYYY" pitfall
Using uppercase "YYYY" in SimpleDateFormat can produce unexpected year values because it is week‑based.
<code>Calendar calendar = Calendar.getInstance();
calendar.set(2019, Calendar.DECEMBER, 31);
Date testDate = calendar.getTime();
SimpleDateFormat dtf = new SimpleDateFormat("YYYY-MM-dd");
System.out.println("2019-12-31 -> " + dtf.format(testDate));
</code>Result:
2020-12-31Correct approach: use lowercase
yyyy.
<code>SimpleDateFormat dtf = new SimpleDateFormat("yyyy-MM-dd");
System.out.println("2019-12-31 -> " + dtf.format(testDate));
</code>3. Money calculation precision pitfall
Floating‑point arithmetic leads to inaccurate results.
<code>public class DoubleTest {
public static void main(String[] args) {
System.out.println(0.1+0.2);
System.out.println(1.0-0.8);
System.out.println(4.015*100);
System.out.println(123.3/100);
double amount1 = 3.15;
double amount2 = 2.10;
if (amount1 - amount2 == 1.05) {
System.out.println("OK");
}
}
}
</code>Result shows precision loss. Use
BigDecimalwith string constructors:
<code>System.out.println(new BigDecimal("0.1").add(new BigDecimal("0.2")));
System.out.println(new BigDecimal("1.0").subtract(new BigDecimal("0.8")));
System.out.println(new BigDecimal("4.015").multiply(new BigDecimal("100")));
System.out.println(new BigDecimal("123.3").divide(new BigDecimal("100")));
</code>4. FileReader default charset causing garbled text
<code>public class FileReaderTest {
public static void main(String[] args) throws IOException {
Files.deleteIfExists(Paths.get("jay.txt"));
Files.write(Paths.get("jay.txt"), "你好,捡田螺的小男孩".getBytes(Charset.forName("GBK")));
System.out.println("System default charset:" + Charset.defaultCharset());
char[] chars = new char[10];
String content = "";
try (FileReader fileReader = new FileReader("jay.txt")) {
int count;
while ((count = fileReader.read(chars)) != -1) {
content += new String(chars, 0, count);
}
}
System.out.println(content);
}
}
</code>Result shows garbled characters because FileReader uses the JVM default charset (UTF‑8). Correct approach uses
InputStreamReaderwith explicit charset:
<code>try (FileInputStream fis = new FileInputStream("jay.txt");
InputStreamReader isr = new InputStreamReader(fis, Charset.forName("GBK"))) {
int count;
while ((count = isr.read(chars)) != -1) {
content += new String(chars, 0, count);
}
}
</code>5. Integer cache pitfall
<code>public class IntegerTest {
public static void main(String[] args) {
Integer a = 127;
Integer b = 127;
System.out.println("a==b:" + (a == b)); // true
Integer c = 128;
Integer d = 128;
System.out.println("c==d:" + (c == d)); // false
}
}
</code>Values between -128 and 127 are cached. JVM parameter
-XX:AutoBoxCacheMax=1000can extend the range.
6. Static variable depending on Spring bean
<code>private static SmsService smsService = SpringContextUtils.getBean(SmsService.class);
</code>This may be null due to class‑loading order. Safer lazy retrieval:
<code>private static SmsService smsService = null;
public static SmsService getSmsService() {
if (smsService == null) {
smsService = SpringContextUtils.getBean(SmsService.class);
}
return smsService;
}
</code>7. ThreadLocal reuse causing data leakage
<code>private static final ThreadLocal<Integer> currentUser = ThreadLocal.withInitial(() -> null);
@GetMapping("wrong")
public Map wrong(@RequestParam("userId") Integer userId) {
String before = Thread.currentThread().getName() + ":" + currentUser.get();
currentUser.set(userId);
String after = Thread.currentThread().getName() + ":" + currentUser.get();
Map result = new HashMap();
result.put("before", before);
result.put("after", after);
return result;
}
</code>Because Tomcat reuses worker threads, previous request data may remain. Fix by clearing ThreadLocal in a finally block:
<code>@GetMapping("right")
public Map right(@RequestParam("userId") Integer userId) {
String before = Thread.currentThread().getName() + ":" + currentUser.get();
currentUser.set(userId);
try {
String after = Thread.currentThread().getName() + ":" + currentUser.get();
Map result = new HashMap();
result.put("before", before);
result.put("after", after);
return result;
} finally {
currentUser.remove();
}
}
</code>8. Switch fall‑through pitfall
<code>public class SwitchTest {
private static String testSwitch(String key) {
switch (key) {
case "1":
System.out.println("1");
case "2":
System.out.println(2);
return "2";
case "3":
System.out.println("3");
default:
System.out.println("return default");
return "4";
}
}
}
</code>Without break, execution falls through to the next case until a return or break is encountered.
9. Arrays.asList pitfalls
9.1 Primitive arrays become a single element
<code>int[] array = {1, 2, 3};
List list = Arrays.asList(array);
System.out.println(list.size()); // 1
</code>9.2 Returned list does not support add/remove
<code>String[] array = {"1", "2", "3"};
List list = Arrays.asList(array);
list.add("5"); // throws UnsupportedOperationException
</code>9.3 Modifying the original array affects the list
<code>String[] arr = {"1", "2", "3"};
List list = Arrays.asList(arr);
arr[1] = "4";
System.out.println(Arrays.toString(arr)); // [1, 4, 3]
System.out.println(list); // [1, 4, 3]
</code>Wrap with
new ArrayList(Arrays.asList(arr))to avoid these issues.
10. ArrayList.toArray() cast pitfall
<code>List<String> list = new ArrayList<>(1);
list.add("...");
String[] array21 = (String[]) list.toArray(); // ClassCastException
</code>Use
list.toArray(new String[0])instead.
11. Exception handling pitfalls
11.1 Losing stack trace
<code>try { readFile(); } catch (IOException e) { throw new RuntimeException("System busy"); }
</code>Correct:
log.error("File read error", e); throw new RuntimeException("System busy");11.2 Defining exceptions as static variables
<code>throw Exceptions.ONEORTWO; // may reuse same instance
</code>Correct:
throw new BusinessException("Business error", 0001);11.3 Avoid e.printStackTrace() in production
<code>log.error("Exception", e);
</code>11.4 Handling exceptions in thread pool tasks
Submit swallows exceptions. Solutions: try/catch inside task, use
Future.get(), set
UncaughtExceptionHandler, override
afterExecute.
11.5 Finally block re‑throwing hides original exception
<code>try { throw new RuntimeException("try"); } finally { throw new RuntimeException("finally"); }
</code>Handle cleanup in finally without re‑throwing, or catch and log the exception.
12. JSON serialization Long becomes Integer
<code>Long idValue = 3000L;
Map<String, Object> data = new HashMap<>();
data.put("id", idValue);
String jsonString = JSON.toJSONString(data);
Map map = JSON.parseObject(jsonString, Map.class);
Object idObj = map.get("id"); // instance of Integer
</code>JSON has no explicit Long type; numbers within Integer range are deserialized as Integer. Use string representation or custom serializer.
13. Executors.newFixedThreadPool OOM issue
<code>ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < Integer.MAX_VALUE; i++) {
executor.execute(() -> {
try { Thread.sleep(10000); } catch (InterruptedException e) {}
});
}
</code>newFixedThreadPool uses an unbounded
LinkedBlockingQueue, causing task accumulation and OOM.
14. Large file or bulk DB read OOM
<code>List<String> lines = Files.readAllLines(path, charset); // loads whole file into memory
</code>Use
Files.line()or stream processing and close resources promptly.
15. Query‑then‑update concurrency issue
<code>if (selectIsAvailable(ticketId)) {
deleteTicketById(ticketId);
// add cash
} else {
return "No tickets";
}
</code>Replace with atomic delete operation returning affected rows.
16. MySQL utf8 vs utf8mb4 for emojis
MySQL
utf8supports up to 3‑byte characters; emojis need 4 bytes. Use
utf8mb4charset.
17. Transaction not effective pitfalls
Database engine does not support transactions
Method not public
Incorrect
rollbackForSelf‑invocation bypasses proxy
Exception swallowed by try/catch
Self‑invocation example shown; avoid by calling through Spring proxy.
18. Reflection overload pitfall
<code>public class ReflectionTest {
private void score(int score) { System.out.println("int grade =" + score); }
private void score(Integer score) { System.out.println("Integer grade =" + score); }
public static void main(String[] args) throws Exception {
ReflectionTest t = new ReflectionTest();
t.getClass().getDeclaredMethod("score", Integer.TYPE).invoke(t, Integer.valueOf("60")); // int version
t.getClass().getDeclaredMethod("score", Integer.class).invoke(t, Integer.valueOf("60")); // Integer version
}
}
</code>19. MySQL timestamp auto‑update pitfall
<code>CREATE TABLE t (
a int,
b timestamp NOT NULL,
c timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
</code>Both
band
cmay update to current time on row update. Use
datetimeor adjust
explicit_defaults_for_timestamp.
20. MySQL 8 timezone pitfall
MySQL 8 defaults to UTC; set
serverTimezone=Asia/Shanghaiin JDBC URL to get correct local time.
<code>jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
</code>macrozheng
Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.