Backend Development 27 min read

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.

macrozheng
macrozheng
macrozheng
20 Java Backend Pitfalls You Must Avoid

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-31

Correct 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

BigDecimal

with 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

InputStreamReader

with 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=1000

can 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

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

<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

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.

<code>jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
</code>
Javaperformancebackend developmentbest practicesCommon Pitfalls
macrozheng
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.