Java程序员最常犯的 10 个错误

本文总结了Java开发过程中常见的十个误区,包括数组转ArrayList的正确方式、高效判断数组包含元素的方法、遍历删除列表元素的正确做法等。这些知识点对于提高Java开发者的编程水平至关重要。

转载自 Java程序员最常犯的 10 个错误

这个列表总结了Java开发人员经常犯的10个错误。
一 、把数组转成ArrayList

为了将数组转换为ArrayList,开发者经常会这样做:

List list = Arrays.asList(arr);
     使用Arrays.asList()方法可以得到一个ArrayList,但是得到这个ArrayList其实是定义在Arrays类中的一个私有的静态内部类。这个类虽然和java.util.ArrayList同名,但是并不是同一个类。java.util.Arrays.ArrayList类中实现了set(), get(), contains()等方法,但是并没有定义向其中增加元素的方法。也就是说通过Arrays.asList()得到的ArrayList的大小是固定的。

    如果在开发过程中,想得到一个真正的ArrayList对象(java.util.ArrayList的实例),可以通过以下方式:

ArrayList<String> arrayList = new ArrayList<String>(Arrays.asList(arr));
java.util.ArrayList中包含一个可以接受集合类型参数的构造函数。因为java.util.Arrays.ArrayList这个内部类继承了AbstractList类,所以,该类也是Collection的子类。

二、判断一个数组是否包含某个值

    在判断一个数组中是否包含某个值的时候,开发者经常这样做:

Set<String> set = new HashSet<String>(Arrays.asList(arr));
return set.contains(targetValue);
    在Java中如何高效的判断数组中是否包含某个元素一文中,深入分析过,以上方式虽然可以实现功能,但是效率却比较低。因为将数组压入Collection类型中,首先要将数组元素遍历一遍,然后再使用集合类做其他操作。
    在判断一个数组是否包含某个值的时候,推荐使用for循环遍历的形式或者使用Apache Commons类库中提供的ArrayUtils类的contains方法。


三、在循环中删除列表中的元素

在讨论这个问题之前,先考虑以下代码的输出结果:

ArrayList<String> list = new ArrayList<String>(Arrays.asList("a","b","c","d"));
for(int i=0;i<list.size();i++){
    list.remove(i);
}
System.out.println(list);

输出结果:

[b,d]

以上代码的目的是想遍历删除list中所有元素,但是结果却没有成功。原因是忽略了一个关键的问题:当一个元素被删除时,列表的大小缩小并且下标也会随之变化,所以当你想要在一个循环中用下标删除多个元素的时候,它并不会正常的生效。

也有些人知道以上代码的问题就由于数组下标变换引起的。所以,他们想到使用增强for循环的形式:

ArrayList<String> list = new ArrayList<String>(Arrays.asList("a","b","c","d"));
for(String s:list){
    if(s.equals("a")){
        list.remove(s);
    }
}

但是,很不幸的是,以上代码会抛出ConcurrentModificationException,有趣的是,如果在remove操作后增加一个break,代码就不会报错:

ArrayList<String> list = new ArrayList<String>(Arrays.asList("a","b","c","d"));
for(String s:list){
    if(s.equals("a")){
        list.remove(s);
        break;
    }
}
    在Java中的fail-fast机制一文中,深入分析了几种在遍历数组的同时删除其中元素的方法以及各种方法存在的问题。其中就介绍了上面的代码出错的原因。
    迭代器(Iterator)是工作在一个独立的线程中,并且拥有一个 mutex 锁。 迭代器被创建之后会建立一个指向原来对象的单链索引表,当原来的对象数量发生变化时,这个索引表的内容不会同步改变,所以当索引指针往后移动的时候就找不到要迭代的对象,所以按照 fail-fast 原则 迭代器会马上抛出java.util.ConcurrentModificationException 异常。

    所以,正确的在遍历过程中删除元素的方法应该是使用Iterator:

ArrayList<String> list = new ArrayList<String>(Arrays.asList("a", "b", "c", "d"));
Iterator<String> iter = list.iterator();
while (iter.hasNext()) {
  String s = iter.next();
  if (s.equals("a")) {
       iter.remove();
    }
}
next()方法必须在调用remove()方法之前调用。如果在循环过程中先调用remove(),再调用next(),就会导致异常ConcurrentModificationException。原因如上。

四、HashTable 和 HashMap 的选择
    了解算法的人可能对HashTable比较熟悉,因为他是一个数据结构的名字。但在Java里边,用HashMap来表示这样的数据结构。Hashtable和 HashMap的一个关键性的不同是,HashTable是同步的,而HashMap不是。所以通常不需要HashTable,HashMap用的更多。
    HashMap完全解读、Java中常见亲属比较等文章中介绍了他们的区别和如何选择。

五、使用原始集合类型
    在Java里边,原始类型和无界通配符类型很容易混合在一起。以Set为例,Set是一个原始类型,而Set< ? >是一个无界通配符类型。 (可以把原始类型理解为没有使用泛型约束的类型)

    考虑下面使用原始类型List作为参数的代码:

public static void add(List list, Object o){
     list.add(o);
}

public static void main(String[] args){
    List<String> list = new ArrayList<String>();
    add(list, 10);
    String s = list.get(0);
}
上面的代码将会抛出异常:
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

    使用原始集合类型是很危险的,因为原始集合类型跳过了泛型类型检查,是不安全的。Set、Set< ? >和Set< Object >之间有很大差别。关于泛型,可以参考下列文章:《成神之路-基础篇》Java基础知识——泛型

六、访问级别
    程序员们经常使用public作为类中的字段的修饰符,因为这样可以很简单的通过引用得到值,但这并不是好的设计,按照经验,分配给成员变量的访问级别应该尽可能的低。参考Java中的四种访问级别

七、ArrayList与LinkedList的选择
    当程序员们不知道ArrayList与LinkedList的区别时,他们经常使用ArrayList,因为它看起来比较熟悉。然而,它们之前有巨大的性能差别。在ArrayList vs LinkedList vs Vector 区别、Java中常见亲属比较等文章中介绍过,简而言之,如果有大量的增加删除操作并且没有很多的随机访问元素的操作,应该首先LinkedList。(LinkedList更适合从中间插入或者删除(链表的特性))

八、可变与不可变
    在为什么Java要把字符串设计成不可变的一文中介绍过,不可变对象有许多的优点,比如简单,安全等等。同时,也有人提出疑问:既然不可变有这么多好处,为什么不把所有类都搞成不可变的呢?

    通常情况下,可变对象可以用来避免产生过多的中间对象。一个经典的实例就是连接大量的字符串,如果使用不可变的字符串,将会产生大量的需要进行垃圾回收的对象。这会浪费CPU大量的时间,使用可变对象才是正确的方案(比如StringBuilder)。

String result="";
for(String s: arr){
    result = result + s;
}

StackOverflow中也有关于这个的讨论。


九、父类和子类的构造函数
上图的代码中有两处编译时错误,原因其实很简单,主要和构造函数有关。首先,我们都知道:
如果一个类没有定义构造函数,编译器将会插入一个无参数的默认构造函数。
如果一个类中定义了一个带参数的构造函数,那么编译器就不会再帮我们创建无参的构造函数。
上面的Super类中定义了一个带参数的构造函数。编译器将不会插入默认的无参数构造函数。
我们还应该知道:
子类的所有构造函数(无论是有参还是无参)在执行时,都会调用父类的无参构造函数。
所以,编译器试图调用Super类中的无参构造函数。但是父类默认的构造函数未
定义,编译器就会报出这个错误信息。
要解决这个问题,可以简单的通过
➀ 在父类中添加一个Super()构造方法,就像这样:
public Super(){}
➁ 移除自定义的父类构造函数
➂ 在子类的构造函数中调用父类的super(value)。

十、””还是构造函数
关于这个问题,也是程序员经常出现困惑的地方,在该如何创建字符串,使用” “还是构造函数?中也介绍过.
如果你只需要创建一个字符串,你可以使用双引号的方式,如果你需要在堆中创建一个新的对象,你可以选择构造函数的方式。
在String d = new String("abcd")时,因为字面值“abcd”已经是字符串类型,那么使用构造函数方式只会创建一个额外没有用处的对象。


<think>嗯,用户想了解Java程序员错误,我需要整理一个结构清晰的回答。首先,我应该回忆常见的错误类型,比如基础语法、异常处理、集合框架、并发问题、内存管理、代码设计等。然后每个类别下要举具体的例子,这样用户更容易理解。 首先,基础语法部分,String的比较问题很典型,很多人用==而不是equals。还有可变对象的修改,比如Date的使用。然后是循环中修改集合结构,这会导致ConcurrentModificationException,需要提醒使用迭代器或者CopyOnWriteArrayList。 接下来是异常处理,捕获异常后不处理或者捕获太泛泛的Exception,这些都不好。资源泄漏也是一个点,需要提到try-with-resources来自动关闭资源。 集合框架方面,初始化集合时容量的问题,比如ArrayList默认大小是10,如果数据量大,扩容会影响性能。还有使用迭代器时的并发修改问题,可能需要再详细说明一下。 并发问题,线程安全的数据结构和非线程安全的混用,比如HashMap在并发环境下的问题。synchronized的使用不当,比如方法锁和对象锁的区别。volatile和原子类的应用场景也需要解释清楚。 内存管理方面,静态集合导致的内存泄漏,比如缓存没有清理机制。还有对象复用的例子,比如StringBuilder的正确使用方式。 代码设计方面,过度设计比如滥用设计模式,或者硬编码配置,应该用外部配置文件。还有忽略API文档,导致方法使用不当。 另外,可以补充一些其他常见问题,比如使用null时没检查导致的NPE,Java 8之后的Optional用法。还有日期时间类的问题,推荐使用新的java.time包。 需要确保每个点都有例子和解决方法,这样用户能明白如何避免。结构上分大类,每个大类下分小点,用列表展示。语言要口语化但专业,符合用户要求的中文回答。 检查一下是否有遗漏的常见错误,比如自动装箱拆箱的空指针问题,或者重写equals和hashCode的注意事项。可能这些也是值得提到的点,但暂时先按现有结构整理,避免内容太多太杂。最后总结一下建议,帮助用户形成良好的编程习惯。</think>作为Java开发者常错误可归纳为以下几类,结合具体案例说明: ### 一、基础语法问题 1. **字符串比较用`==`而非`equals()`** ```java String s1 = new String("hello"); String s2 = new String("hello"); System.out.println(s1 == s2); // 输出false(比较对象地址) System.out.println(s1.equals(s2)); // 输出true(比较内容) ``` 2. **循环中修改集合结构** ```java List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c")); for (String s : list) { list.remove(s); // 抛出ConcurrentModificationException } // 正确做法:使用迭代器的remove()方法 ``` ### 二、异常处理误区 1. **吞掉异常** ```java try { // 可能出错的代码 } catch (Exception e) { e.printStackTrace(); // 仅打印不处理 } ``` **改进**:记录日志后根据业务场景选择恢复或抛出 2. **资源未正确关闭** ```java FileInputStream fis = null; try { fis = new FileInputStream("file.txt"); } finally { if (fis != null) fis.close(); // 可能忘记关闭 } ``` **改进**:使用try-with-resources ```java try (FileInputStream fis = new FileInputStream("file.txt")) { // 自动关闭资源 } ``` ### 三、集合框架使用不当 1. **忽略初始容量** ```java List<Integer> list = new ArrayList<>(); // 默认容量10 for (int i = 0; i < 1_000_000; i++) { list.add(i); // 频繁扩容导致性能下降 } ``` **优化**:预先指定容量`new ArrayList<>(1000000)` 2. **错误使用线程安全集合** ```java Map<String, Integer> map = new HashMap<>(); // 多线程环境下put操作可能导致数据错乱 ``` **解决**:改用`ConcurrentHashMap`或同步包装类 ### 四、并发编程陷阱 1. **误用`synchronized`** ```java public synchronized void methodA() { // 锁住的是当前对象实例 } public static synchronized void methodB() { // 锁住的是Class对象 } ``` 需明确锁的作用范围 2. **可见性问题未处理** ```java class Counter { private int count; // 未使用volatile public void increment() { count++; } } ``` **风险**:多线程环境下可能读取到旧值 **解决**:使用`AtomicInteger`或`volatile`(需配合原子操作) ### 五、内存管理疏忽 1. **静态集合导致内存泄漏** ```java public class Cache { private static final Map<Long, Object> CACHE = new HashMap<>(); public static void store(Long id, Object obj) { CACHE.put(id, obj); // 对象永不释放 } } ``` **改进**:使用WeakHashMap或定期清理机制 2. **未复用不可变对象** ```java String s = new String("value"); // 错误 String s = "value"; // 正确(利用字符串常量池) ``` ### 六、代码设计缺陷 1. **过度设计** ```java // 简单的DTO类使用工厂模式+策略模式 // 应根据实际复杂度选择设计模式 ``` 2. **硬编码配置** ```java String dbUrl = "jdbc:mysql://localhost:3306/mydb"; // 应外置到配置文件 ``` ### 七、其他典型问题 1. **忽略`@Override`注解** 可能导致方法未被正确重写: ```java class Parent { public void show() { ... } } class Child extends Parent { public void Show() { ... } // 大小写错误,未重写 } ``` 2. **日期时间处理错误** ```java Date date = new Date(2023, 1, 1); // 月份从0开始,实际是二月 // 应使用java.time.LocalDate ``` ### 建议改进方向 1. 使用静态代码分析工具(如SonarQube) 2. 编写单元测试覆盖核心逻辑 3. 定期进行Code Review 4. 阅读JDK源码理解底层实现 5. 关注Java语言更新(如Records、Pattern Matching等新特性)
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值