本文总结了JAVA程序员最常犯的10个错误。
1. 将数组转化成ArrayList
将数组转化成 ArrayList, 开发者经常这么做:
List<String> list = Arrays.asList(arr);
Arrays.asList() 将会返回一个 ArrayList ,它是Arrays的一个私有的静态内部类,而不是 java.util.ArrayList 。 java.util.Arrays.ArrayList 类有set(), get(), contains() 方法, 但没有任何方法用于添加元素(如add()), 因此它的大小是固定的。 要真正创建一个 ArrayList, 你应该这么做:
ArrayList<String> arrayList = new ArrayList<String>(Arrays.asList(arr));
ArrayList 的构造方法能接受一个Collection类型的对象。Collection类也是java.util.Arrays.ArrayList.
2. 检查数组是否包含某个值
开发者经常这么做:
Set<String> set = new HashSet<String>(Arrays.asList(arr));
return set.contains(targetValue);
上面的代码可以工作,但没有必要先将list转化成set,并且将list转化成set需要额外的时间。可以很简单地写成:
Arrays.asList(arr).contains(targetValue);
或者
for(String s: arr){
if(s.equals(targetValue))
return true;
}
return false;
第一个要比第二个更具可读性。
3. 在循环中将元素从List中删除掉
观察下面的代码,它在循环中移除List中的元素:
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的一个元素被移除后,List变小了,索引也变了。所以在循环中是无法正确地移除多个元素的。
你可能知道使用迭代器iterator是在循环中删除元素的正确方法,而且你知道foreach循环在Java中和迭代器是一样的,但事实上并不是。
ArrayList<String> list = new ArrayList<String>(Arrays.asList("a", "b", "c", "d"));
for (String s : list) {
if (s.equals("a"))
list.remove(s);
}
这将会抛出 ConcurrentModificationException异常。
而下面的代码可以正确运行。
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()方法之前调用。 在foreach结构中, 编译器会在删除元素操作后调用.next()方法, 这是引起ConcurrentModificationException异常的原因。你应该看一看ArrayList.iterator()的源代码.
4. Hashtable vs HashMap
在算法的约定中,Hashtable是一种数据结构的名称。但是在Java中,这种数据结构被称为HashMap。Java中Hashtable和HashMap的一个关键不同点在于Hashtable是同步的。很多时候你并不需要Hashtable,你应该使用HashMap。
5. 使用Collection的原生类型(raw type)
在Java中,原生类型(raw type)和无界通配符类型(unbounded wildcard type)很容易混淆。举个例子: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);
}
代码会抛出一个异常:
Exception in thread “main” java.lang.ClassCastException:
java.lang.Integer cannot be cast to java.lang.String at …
使用原生类型是非常危险的,因为原生类型的集合会跳过泛型的检查。Set
、Set<?>
和Set<Object>
之间存在很大的差异。
原生类型和无界通配符类型比较 and 泛型擦除.
6. 访问级别
很多的开发者在类成员使用public修饰符。这样就可以很容易的通过直接引用而得到类成员的值,但这是一个非常不好的设计。成员的权限设置的经验规则是进可能地低。
public, default, protected, and private
7. ArrayList 对比 LinkedList
当开发者还不知道 ArrayList
和 LinkedList
的区别时,他们通常使用ArrayList
,因为它看着熟悉。然而它们之间存在巨大的性能差异。简而言之,当存在大量的添加删除操作,并且没有太多的随机访问操作时,LinkedList
是首要的选择。
8. 可变(Mutable) 对比不可变(Immutable)
不可变对象有很多的优点诸如简单、安全。但是每一个不同的值都要创建新的对象,创建太多的对象会造成垃圾回收。在可变与不可变自己需要权衡一二。
一般来说,可变对象用于防止创建太多的不可变对象。一个经典的例子是连接大量的字符串。如果使用一个不可变string,你将创建大量的对象而立刻触发垃圾回收。这回浪费CPU的时间和资源,这时候使用可变对象是好的解决方案(例如:StringBuilder)
String result="";
for(String s: arr){
result = result + s;
}
有一些情况下,可变对象是可取的。例如:将一个可变的对象传入方法中可以让你收集多个结果而避免过多的语法累赘。另一个例子是排序和过滤:当然,你可以使一个方法接收原始的集合,然后将排序完的集合返回。但是,在大集合的情况下,这会造成巨大的浪费。
9. 父类和子类的构造器
发生此编译错误的原因在于没有生命父类的构造器。在 Java 中,如果一个类没有声明构造器,编译器默认会自动创建一个无参构造器。如果父类创建了一个构造器,比如例子中的 Super(String s)
,编译器将不会再创建默认的无参构造器。这就是上面Super类对应的情况。
Sub类的构造器,无论是带参的还是无参的都会调用父类中的无参构造器。编译器尝试在子类的俩个构造器中插入super(),但父类中并没有定义默认的构造器,所以编译器报了两个错误。
有3种解决办法:
- 在父类中添加Supper()构造器:
public Super(){
System.out.println("Super");
}
- 去掉自定义的Super构造器
- 在子类的构造器中添加super(value)方法。
10. “”还是构造器?
String 可以通过两种方法创建:
//1. use double quotes
String x = "abc";
//2. use constructor
String y = new String("abc");
两种方法的区别是?
下面的例子可以提供答案:
String a = "abcd";
String b = "abcd";
System.out.println(a == b); // True
System.out.println(a.equals(b)); // True
String c = new String("abcd");
String d = new String("abcd");
System.out.println(c == d); // False
System.out.println(c.equals(d)); // True
For more details about how they are allocated in memory, check out Create Java String Using ” ” or Constructor?.
写在最后
本文基于我对GitHub上的大量开源项目、Stack Overflow提问和Google热门搜索的分析。并没有证据证明它们是就是Top10,但无疑它们都是很常见的。请留下您的评论,如果您不同意其中的任何一点,我非常感谢您能指出其他更常见的错误!