该文总结了排名前10位的错误,这些都是Java开发人员经常犯的。

1.把数组转成ArrayList

把数组转成ArrayList,开发者通常这样做:

1
2
3
4
String[] arr = new String[]{"a", "b", "c"};
List<String> list = Arrays.asList(arr);
System.out.println(list);
// [a, b, c]

Arrays.asList()将会返回一个ArrayList 它是一个private static class在Arrays中,并非java.util.ArrayList类。java.util.Arrays.ArrayList类有set(), get(), contains()等方法,但是没有任何添加元素的方法,所以它的大小是固定的。看下面这个例子:

1
2
3
4
String[] arr = new String[]{"a", "b", "c"};
List<String> list = Arrays.asList(arr);
System.out.println(list);
list.add("d");//java.lang.UnsupportedOperationException

为了创建一个实际的ArrayList,你应该这样做:

1
2
3
4
5
String[] arr = new String[]{"a", "b", "c"};
List<String> list = new ArrayList<String>(Arrays.asList(arr));
System.out.println(list);//[a, b, c]
list.add("d");
System.out.println(list);//[a, b, c, d]

ArrayList的构造函数可以接受一个集合类型,这包括了超类是java.util.Arrays.ArrayList的类。

2.检查一个集合中是否有一个值

开发者通常这样做:

1
2
Set<String> set = new HashSet<String>(Arrays.asList(arr));
return set.contains(targetValue);

这代码可以达到效果,但是在这里list根本没有必要转换成set。转换list成为set也是要花费时间的。使用下面代码就可以了:

1
Arrays.asList(arr).contains(targetValue);

或者:

1
2
3
4
5
for(String s: arr){
if(s.equals(targetValue))
return true;
}
return false;

第一个可读性比第二个好很多。

3.从list中循环删除元素

考虑一下下面的代码,在迭代过程中删除元素:

1
2
3
4
5
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]

该方法存在一个严重的问题。当一个元素被移除时,该列表的大小减小,而index数的却是变化的。所以,如果你想通过使用索引来删除一个循环中的多个元素,那将是无法正常工作。

你可能知道,使用迭代器是正确的方式来删除循环中的元素,你知道foreach循环中的Java就像一个迭代器,但实际上它不是。考虑下面的代码:

1
2
3
4
5
6
ArrayList<String> list = new ArrayList<String>(Arrays.asList("a", "b", "c", "d"));
for (String s : list) {
if (s.equals("a"))
list.remove(s); //ConcurrentModificationException.
}

它会抛出ConcurrentModificationException异常。

使用下面方法可以解决:

1
2
3
4
5
6
7
8
9
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() 在.remove()元素之后​​,这引起了ConcurrentModificationException异常。你最好看看 ArrayList.iterator() 源码深度理解,这将有助于你的理解。

4. Hashtable vs HashMap

通常,在算法中Hashtable是一种数据结构的名称。但是在java中这种数据结构的名称是HashMap。最主要的一个不同是Hashtable是同步的。所以很多时候你是不需要使用Hashtable的,通常使用HashMap。

5. 在集合中使用原始类型

在java中,原始类型和无界通配符类型很容易混合在一起。用set来举个例子,Set是原始类型,当设置Set<?>是无界通配符类型的时候。

考虑下面的代码,它使用原始类型列表作为参数:

1
2
3
4
5
6
7
8
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>是有很多不同的。Set vs. Set<?>Java Type Erasure Mechanism

6. 访问级别

很多时候开发人员会使用public的字段级别。这很容易通过引用来获得字段的值,但是这是一个很糟糕的设计。经验法则是会给成员尽可能低的访问级别。

Java 访问级别: public, protected, private

7. rrayList vs. LinkedList

当开发者不知道ArrayList 和 LinkedList的区别的时候,他们通常使用ArrayList,因为ArrayList看起来更加熟悉。然而,这直接将是一个很严重的性能问题。通常来说,LinkedList应该是首选的情况是,如果有大量的添加/删除操作和没有大量的随机访问操作。如果这对你有用的话,访问ArrayList vs. LinkedList获得有关他们的性能的详细信息。

8. 可变与不可变(Mutable vs. Immutable)

不可改变的对象有许多优点,例如简单性,安全性等,但它需要对每个不同的值穿件一个单独的对象,对象太多可能导致无用GC,成本过高。可变和不可变之间进行选择时应该有一个平衡点。

在一般情况下,使用可变对象,以避免产生过多的中间对象。一个典型的例子是连接大量的字符串。如果您使用的是不可变的字符串,你会产生很多有资格立即垃圾回收的对象。这浪费时间和精力在CPU上,使用可变对象来解决这个问题(如StringBuilder)。

1
2
3
4
String result="";
for(String s: arr){
result = result + s;
}

还有,可变对象在一些情况也是是可取的。例如通过可变对象的方法,可以让你收集多个结果,而无需通过太多的语法钻圈。另一个例子是排序和筛选:当然,你可以做一个方法,它传入原来的集合,返回一个新的有序的集合,但在大集合中会变得极其浪费空间。(来自dasblinkenlight在Stack Overflow的回答

在java中为什么字符串是不可变的

9. 超类和子类的构造函数

超类和子类构造函数

发生这种编译错误,是因为默认的超级构造函数是不确定的。在Java中,如果一个类没有定义构造函数,编译器会插入一个默认的无参数构造函数。如果构造函数是在超类中定义的,在Super(String s)情况下,编译器将不插入默认的无参数构造函数。Super 类就是以上的情况。

Sub类中的构造函数,无论是带参数还是不带参数,都需要调用 Super 类的无参构造函数。但Super类的默认构造函数是没有定义的,使用编译器报告了这个错误信息。

要解决此问题,只需在 Super中增加一个Super()构造函数,如下:

1
2
3
public Super(){
System.out.println("Super");
}

或删除自定义的Super构造函数,或增加 super(value)在sub类的构造方法中。

在java中子类与父类的构造函数

10. “” 还是构造函数?

String 能通过两种方式创建:

1
2
3
4
//1. use double quotes
String x = "abc";
//2. use constructor
String y = new String("abc");

那他们有什么不同?

下面的例子可以提供一个快速的答案:

1
2
3
4
5
6
7
8
9
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

关于它们是如何分配的内存的详细信息,请看java中创建字符串使用”” 或者 构造函数?

延伸阅读

这份名单是根据我对大量在GitHub上、Stack Overflow问题和流行的Google查询分析。没有评估,难以证明他们恰恰是前10名,但绝他们对是很常见的。请留下您的评论,如果你有不同的意见。如果你能指出一些错误是十分常见的,我将会十分感激。

参考文档