Java开发者必须知道的20个常见陷阱

更新日期: 2025-11-17 阅读: 32 标签: Java

作为Java开发者,在日常编码中经常会遇到各种"坑"。有些是语法问题,有些是设计误区。今天我们来盘点Java中最常见的20个陷阱,帮你避开这些雷区。

1. == 和 equals() 混淆

String a = new String("hello");
String b = new String("hello");
System.out.println(a == b);        // false
System.out.println(a.equals(b));   // true

问题:==比较对象引用地址,equals()比较对象内容。

正确做法:比较字符串内容时始终使用equals()方法。

2. Integer缓存陷阱

Integer a = 127;
Integer b = 127;
System.out.println(a == b);  // true

Integer c = 128;
Integer d = 128;
System.out.println(c == d);  // false

原因:Integer在-128到127之间有缓存,超出范围会创建新对象。

建议:比较Integer对象时使用equals()方法。

3. 字符串拼接性能问题

// 错误做法 - 性能差
String result = "";
for (int i = 0; i < 1000; i++) {
    result += "a";  // 每次都创建新StringBuilder和String对象
}

// 正确做法
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++) {
    sb.append("a");
}
String result = sb.toString();

4. 集合遍历时修改结构

// 错误做法 - 会抛出ConcurrentModificationException
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");

for (String item : list) {
    if ("a".equals(item)) {
        list.remove(item);
    }
}

// 正确做法1:使用迭代器
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    String item = iterator.next();
    if ("a".equals(item)) {
        iterator.remove();
    }
}

// 正确做法2:使用Java8的removeIf
list.removeIf(item -> "a".equals(item));

5. 忘记关闭资源

// 错误做法 - 资源泄露
FileInputStream fis = new FileInputStream("file.txt");
// 使用文件流...
// 忘记调用fis.close()

// 正确做法 - 使用try-with-resources
try (FileInputStream fis = new FileInputStream("file.txt");
     FileOutputStream fos = new FileOutputStream("output.txt")) {
    // 使用资源,自动关闭
} catch (IOException e) {
    e.printStackTrace();
}

6. 异常处理不当

// 错误做法 - 异常被吞掉
try {
    someRiskyOperation();
} catch (Exception e) {
    // 空的catch块,异常信息丢失
}

// 正确做法
try {
    someRiskyOperation();
} catch (SpecificException e) {
    logger.error("操作失败,参数: {}", someParam, e);
    throw new BusinessException("业务操作失败", e);
}

7. 数组和集合的toArray()陷阱

List<String> list = Arrays.asList("a", "b");

// 错误做法
Object[] array1 = list.toArray();  // 返回Object[]

// 正确做法
String[] array2 = list.toArray(new String[0]);
String[] array3 = list.toArray(new String[list.size()]);

8. 泛型类型擦除

// 错误做法 - 运行时无法获取泛型类型
public class GenericTest<T> {
    public void printType() {
        // 编译错误:Cannot resolve symbol 'T'
        // Class<T> clazz = T.class;
    }
}

// 解决方案:传递Class对象
public class GenericTest<T> {
    private final Class<T> type;
    
    public GenericTest(Class<T> type) {
        this.type = type;
    }
    
    public void printType() {
        System.out.println(type.getName());
    }
}

9. 可变参数的陷阱

public static void print(Object... args) {
    System.out.println(args.length);
}

print(null);        // 输出: 1 (null被视为一个元素)
print((Object) null); // 输出: 1
print();            // 输出: 0
print("a", "b");    // 输出: 2

// 正确做法 - 处理null情况
public static void safePrint(Object... args) {
    if (args == null || args.length == 0) {
        System.out.println("无参数");
        return;
    }
    System.out.println("参数个数: " + args.length);
}

10. switch语句忘记break

int day = 2;
switch (day) {
    case 1:
        System.out.println("星期一");
        // 忘记break,会继续执行
    case 2:
        System.out.println("星期二");
        break;
    case 3:
        System.out.println("星期三");
        break;
}
// 输出: 星期二 (期望只输出星期二,但实际输出星期一和星期二)

// 正确做法 - 每个case都要有break
switch (day) {
    case 1:
        System.out.println("星期一");
        break;
    case 2:
        System.out.println("星期二");
        break;
    default:
        System.out.println("其他");
        break;
}

// Java 14+ 更好的写法
String dayName = switch (day) {
    case 1 -> "星期一";
    case 2 -> "星期二";
    default -> "其他";
};

11. 浮点数比较精度问题

// 错误做法
double a = 0.1;
double b = 0.2;
double c = 0.3;
System.out.println(a + b == c);  // false
System.out.println(a + b);       // 0.30000000000000004

// 正确做法1:使用误差范围
boolean isEqual = Math.abs((a + b) - c) < 0.000001;

// 正确做法2:使用BigDecimal
BigDecimal bd1 = new BigDecimal("0.1");
BigDecimal bd2 = new BigDecimal("0.2");
BigDecimal bd3 = new BigDecimal("0.3");
System.out.println(bd1.add(bd2).equals(bd3));  // true

12. 日期时间处理陷阱

// 错误做法 - 使用过时的Date api
Date date = new Date(2023, 12, 25);  // 年份从1900开始,月份从0开始
System.out.println(date.getYear());  // 123 (2023-1900)

// 正确做法 - 使用java.time包
LocalDate date = LocalDate.of(2023, 12, 25);
LocalDateTime dateTime = LocalDateTime.now();
ZonedDateTime zonedDateTime = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));

13. 线程安全问题

// 错误做法 - SimpleDateFormat不是线程安全的
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");

// 线程1
new Thread(() -> {
    System.out.println(sdf.format(new Date()));
}).start();

// 线程2 - 可能产生不可预期的结果
new Thread(() -> {
    System.out.println(sdf.format(new Date()));
}).start();

// 正确做法1:每个线程使用独立的实例
new Thread(() -> {
    SimpleDateFormat localSdf = new SimpleDateFormat("yyyy-MM-dd");
    System.out.println(localSdf.format(new Date()));
}).start();

// 正确做法2:使用ThreadLocal
private static final ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = 
    ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

// 正确做法3:使用java.time
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
System.out.println(LocalDate.now().format(formatter));

14. HashMap容量设置

// 如果知道大概的元素数量,预设容量可以提高性能
int expectedSize = 1000;

// 错误做法 - 默认容量16,可能频繁扩容
Map<String, String> map1 = new HashMap<>();

// 正确做法 - 预设初始容量
Map<String, String> map2 = new HashMap<>(expectedSize);

// 更精确的做法 - 考虑负载因子
int initialCapacity = (int) (expectedSize / 0.75f) + 1;
Map<String, String> map3 = new HashMap<>(initialCapacity);

15. 空指针异常防护

// 错误做法
String str = null;
int length = str.length();  // NullPointerException

// 正确做法1:显式检查
if (str != null) {
    int length = str.length();
}

// 正确做法2:使用Optional
Optional<String> optionalStr = Optional.ofNullable(str);
int length = optionalStr.map(String::length).orElse(0);

// 正确做法3:Java 14+ 使用Objects.requireNonNull
String safeStr = Objects.requireNonNull(str, "字符串不能为null");

// 正确做法4:使用工具方法
public static int safeLength(String str) {
    return str == null ? 0 : str.length();
}

16. 递归没有终止条件

// 错误做法 - 无限递归导致栈溢出
public int factorial(int n) {
    return n * factorial(n - 1);  // StackOverflowError
}

// 正确做法 - 添加终止条件
public int factorial(int n) {
    if (n <= 1) {
        return 1;  // 终止条件
    }
    return n * factorial(n - 1);
}

// 更好的做法 - 使用迭代避免栈溢出
public int factorialIterative(int n) {
    int result = 1;
    for (int i = 2; i <= n; i++) {
        result *= i;
    }
    return result;
}

17. 忘记重写hashCode()

// 错误做法 - 只重写equals(),不重写hashCode()
public class Person {
    private String name;
    private int age;
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Person person = (Person) obj;
        return age == person.age && Objects.equals(name, person.name);
    }
    
    // 忘记重写hashCode()
}

// 正确做法 - equals()和hashCode()必须同时重写
@Override
public int hashCode() {
    return Objects.hash(name, age);
}

18. 静态变量的生命周期

public class Counter {
    private static int count = 0;  // 静态变量,所有实例共享
    
    public void increment() {
        count++;
    }
    
    public static int getCount() {
        return count;
    }
}

// 使用示例
Counter c1 = new Counter();
Counter c2 = new Counter();
c1.increment();  // count变为1
c2.increment();  // count变为2
System.out.println(Counter.getCount());  // 输出2,不是1

19. 字符串操作性能

// 错误做法 - 在老版本Java中substring可能导致内存泄漏
String largeString = readLargeString();  // 假设返回很大的字符串
String small = largeString.substring(0, 10);

// 在Java 7u6之前,small会持有largeString的引用
// 正确做法 - 如果需要释放大字符串,显式创建新字符串
String small = new String(largeString.substring(0, 10));

20. compareTo()实现问题

// 错误做法 - 可能整数溢出
public class Student implements Comparable<Student> {
    private int age;
    
    @Override
    public int compareTo(Student other) {
        return age - other.age;  // 如果age接近Integer极值,可能溢出
    }
}

// 正确做法
@Override
public int compareTo(Student other) {
    return Integer.compare(age, other.age);
}

// 或者对于多个字段的比较
@Override
public int compareTo(Student other) {
    int ageCompare = Integer.compare(age, other.age);
    if (ageCompare != 0) {
        return ageCompare;
    }
    return name.compareTo(other.name);
}

总结

这些常见的Java陷阱涵盖了从基础语法到高级特性的各个方面。避免这些陷阱的关键在于:

  1. 理解原理:不只是记住语法,要理解背后的机制

  2. 代码审查:多人review代码,互相学习

  3. 单元测试:编写全面的测试用例

  4. 持续学习:关注Java新特性和最佳实践

记住这些陷阱并采用正确的做法,将帮助你写出更健壮、更高效的Java代码。

本文内容仅供个人学习/研究/参考使用,不构成任何决策建议或专业指导。分享/转载时请标明原文来源,同时请勿将内容用于商业售卖、虚假宣传等非学习用途哦~感谢您的理解与支持!

链接: https://fly63.com/article/detial/13191

采用Java的ServerSocket进行编码一个简单的HTTP服务器

诸如tomcat等web服务器中间件简化了我们web的开发成本,但有时候我们或许并不需要这么一个完备的服务器,只是希望做一个简单地处理或者做特殊用途的服务器。

Spring Boot支持Crontab任务改造

在以往的 Tomcat 项目中,一直习惯用 Ant 打包,使用 build.xml 配置,通过 ant -buildfile 的方式在机器上执行定时任务。虽然 Spring 本身支持定时任务,但都是服务一直运行时支持。

lucene的suggest(搜索提示功能的实现)

首先引入依赖,既然要进行智能联想,那么我们需要为提供联想的数据建立一个联想索引(而不是使用原来的数据索引),既然要建立索引,那么我们需要知道建立索引的数据来源。我们使用一个扩展自InputIterator的类来定义数据来源

HashMap剖析之内部结构

本文是基于Java 8的HashMap进行分析,主要是介绍HashMap中的成员变量和类变量的用途,以及分析HashMap的数据结构。在HashMap中存在多个成员变量和类变量,搞清楚它们的用途有助于我们更深入了解HashMap,下面是它们的介绍:

自定义HttpMessageConverter接受JSON数据

Spring默认使用Jackson处理json数据。实际开发中,在业界中,使用非常受欢迎的fastjson来接受json数据。创建一个项目,在web目录下新建一个assets/js目录,加入jquery和json2的js文件,在lib下加入fastjson的jar文件。

统计两个IP地址之间的IP个数

求两个IP地址之间的IP个数,例如192.18.16.1~192.18.16.5,2001:DB8:0000:0023:0008:0800:200C:417C~2001:DB8:0:23:8:800:200C:417D之间的IP个数?

JSP和JSF之间的区别是什么?

JSP和JSF这两种技术都基于Java,主要用于基于Web的应用程序。那么它们之间有什么区别?下面本篇文章就来给大家简单比较一下JSP和JSF,介绍JSP和JSF之间的区别有哪些,希望对大家有所帮助。

JVM 发生 OOM 的 8 种原因、及解决办法

Java 堆空间:发生频率:5颗星造成原因1、无法在 Java 堆中分配对象 2、吞吐量增加 3、应用程序无意中保存了对象引用,对象无法被 GC 回收 4、应用程序过度使用 finalizer

Java版的7种单例模式

今天看到某一篇文章的一句话 单例DCL 前面加 V 。就这句话让我把 单例模式 又仔细看了一遍。Java 中的 单例模式 是我们一直且经常使用的设计模式之一,大家都很熟悉,所以这篇文章仅仅做我自己记忆。

常问的15个顶级Java多线程面试题

在任何Java面试当中多线程和并发方面的问题都是必不可少的一部分。如果你想获得更多职位,那么你应该准备很多关于多线程的问题。面试官会问面试者很多令人混淆的Java线程问题

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!