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
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
}
}1.2 Chained call NullPointer
public class NullPointTest {
public static void main(String[] args) {
// fruitService.getAppleService() may be null, causing NullPointerException
fruitService.getAppleService().getWeight().equals("OK");
}
}1.3 Equals method left NullPointer
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: ...");
}
}
}1.4 ConcurrentHashMap does not support null keys/values
public class NullPointTest {
public static void main(String[] args) {
Map map = new ConcurrentHashMap<>();
String key = null;
String value = null;
map.put(key, value);
}
}1.5 Directly accessing collection or array elements
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
}
}1.6 Directly accessing object properties
public class NullPointTest {
public static void main(String[] args) {
User user = null;
System.out.println(user.getAge()); // NullPointerException
}
}2. Date format "YYYY" pitfall
Using uppercase "YYYY" in SimpleDateFormat can produce unexpected year values because it is week‑based.
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));Result: 2020-12-31 Correct approach: use lowercase yyyy.
SimpleDateFormat dtf = new SimpleDateFormat("yyyy-MM-dd");
System.out.println("2019-12-31 -> " + dtf.format(testDate));3. Money calculation precision pitfall
Floating‑point arithmetic leads to inaccurate results.
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");
}
}
}Result shows precision loss. Use BigDecimal with string constructors:
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")));4. FileReader default charset causing garbled text
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);
}
}Result shows garbled characters because FileReader uses the JVM default charset (UTF‑8). Correct approach uses InputStreamReader with explicit charset:
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);
}
}5. Integer cache pitfall
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
}
}Values between -128 and 127 are cached. JVM parameter -XX:AutoBoxCacheMax=1000 can extend the range.
6. Static variable depending on Spring bean
private static SmsService smsService = SpringContextUtils.getBean(SmsService.class);This may be null due to class‑loading order. Safer lazy retrieval:
private static SmsService smsService = null;
public static SmsService getSmsService() {
if (smsService == null) {
smsService = SpringContextUtils.getBean(SmsService.class);
}
return smsService;
}7. ThreadLocal reuse causing data leakage
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;
}Because Tomcat reuses worker threads, previous request data may remain. Fix by clearing ThreadLocal in a finally block:
@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();
}
}8. Switch fall‑through pitfall
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";
}
}
}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
int[] array = {1, 2, 3};
List list = Arrays.asList(array);
System.out.println(list.size()); // 19.2 Returned list does not support add/remove
String[] array = {"1", "2", "3"};
List list = Arrays.asList(array);
list.add("5"); // throws UnsupportedOperationException9.3 Modifying the original array affects the list
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]Wrap with new ArrayList(Arrays.asList(arr)) to avoid these issues.
10. ArrayList.toArray() cast pitfall
List<String> list = new ArrayList<>(1);
list.add("...");
String[] array21 = (String[]) list.toArray(); // ClassCastExceptionUse list.toArray(new String[0]) instead.
11. Exception handling pitfalls
11.1 Losing stack trace
try { readFile(); } catch (IOException e) { throw new RuntimeException("System busy"); }Correct:
log.error("File read error", e); throw new RuntimeException("System busy");11.2 Defining exceptions as static variables
throw Exceptions.ONEORTWO; // may reuse same instanceCorrect:
throw new BusinessException("Business error", 0001);11.3 Avoid e.printStackTrace() in production
log.error("Exception", e);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
try { throw new RuntimeException("try"); } finally { throw new RuntimeException("finally"); }Handle cleanup in finally without re‑throwing, or catch and log the exception.
12. JSON serialization Long becomes Integer
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 IntegerJSON has no explicit Long type; numbers within Integer range are deserialized as Integer. Use string representation or custom serializer.
13. Executors.newFixedThreadPool OOM issue
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < Integer.MAX_VALUE; i++) {
executor.execute(() -> {
try { Thread.sleep(10000); } catch (InterruptedException e) {}
});
}newFixedThreadPool uses an unbounded LinkedBlockingQueue, causing task accumulation and OOM.
14. Large file or bulk DB read OOM
List<String> lines = Files.readAllLines(path, charset); // loads whole file into memoryUse Files.line() or stream processing and close resources promptly.
15. Query‑then‑update concurrency issue
if (selectIsAvailable(ticketId)) {
deleteTicketById(ticketId);
// add cash
} else {
return "No tickets";
}Replace with atomic delete operation returning affected rows.
16. MySQL utf8 vs utf8mb4 for emojis
MySQL utf8 supports up to 3‑byte characters; emojis need 4 bytes. Use utf8mb4 charset.
17. Transaction not effective pitfalls
Database engine does not support transactions
Method not public
Incorrect rollbackFor Self‑invocation bypasses proxy
Exception swallowed by try/catch
Self‑invocation example shown; avoid by calling through Spring proxy.
18. Reflection overload pitfall
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
}
}19. MySQL timestamp auto‑update pitfall
CREATE TABLE t (
a int,
b timestamp NOT NULL,
c timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);Both b and c may update to current time on row update. Use datetime or adjust explicit_defaults_for_timestamp.
20. MySQL 8 timezone pitfall
MySQL 8 defaults to UTC; set serverTimezone=Asia/Shanghai in JDBC URL to get correct local time.
jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/ShanghaiSigned-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
