Java 的 List 实际使用注意要点

最近想自己写一个简单的 Rss 阅读器练手,在阅读 Github 上面的项目 android-rss 时,在 List 的使用上碰到一些不同于入门书籍的代码,并不是很明白意思。之后,不断百度 + Google 终于明白。 代码原文:

1
2
3
4
5
6
7
public java.util.List<String> getCategories() {
    if (categories == null) {
      return java.util.Collections.emptyList();
    }

    return java.util.Collections.unmodifiableList(categories);
  }

最好的解释在《Effective Java(第二版)》的第七章第 43 条。 大体意思是: 如果代码需要返回列表,且列表长度有可能为 0,常见的新手(比如我 O_O)的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
...
public List<T> getList() {
    List<T> list = new ArrayList();

    ...     // 其他数据处理代码

    if (list.size() == 0) {
        return null;
    }
    return list;
}
...

这样,会把零长度列表变成了一种特例(null),在使用的时候逻辑就会非常奇怪 —— 必须加上诸如 if (list != null) 这样的判断分支。 这么做无谓地增加了代码地长度,而且调用时容易忘记判断分支导致空指针错误。尤其,当某个库文件这么写地时候,使用者的调用代码会经常出错,因为库使用者经常使用链式调用方式(getList().a().b().c())完成一系列任务。 而且,即使不返回 null 也不代表代码就会出错,比如:

1
2
3
    for (T element: list) {
        ...     // 对每个元素的操作
    }

只要程序员写的是标准的遍历代码,就已经暗含了零长度的边界值判断,不会发生对空列表元素的各种操作。

虽然可以自己新建立一个空的列表并返回,如 return new ArrayList();。 但这么做并不可取,因为:

  • 列表实际是可以修改的,可能会有错误的操作(尤其涉及多线程时)将本该时空列表的结果进行更改,这样很不安全,而且错误难以发现
  • 每次返回空列表其实都会重新建立一个对象,但又不会对它内部进行什么有效操作,浪费了资源

为了解决这两个问题,Java 的集合库包含了只读零长度列表的实现,需要的时候调用 Collections.emptyList() 即可。这样的空列表如果调用 get() 方法会抛出异常 IndexOutOfBoundsException,但是正常的遍历代码不会受到影响,会因零长度而不满足遍历的条件判断。

注意:另外,如果需要对返回的列表指定元素类型,Java 7 及之后可以直接这么使用就好,而 Java 5 和 Java 6 则要声明类型,如:

1
2
3
4
5
    /* start with Java 7 */
    List<foo> list = Collections.emptyList();

    /* start with Java 5 until Java 7 */
    List<foo> list = Collections.<foo>emptyList();

如果没有列表元素类型的要求,甚至可以直接使用常量 Collections.EMPTY_LIST

要使用 Collections.unmodifiableList() 加工只读的列表,防止使用时修改了只读列表,如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
    class Example {

        private List<String> list;

        ...

        public List<String> getList() {
            return Collections.unmodifiableList(list);
        }

        ...
    }

实际工程中经常遇到需要处理只读列表的场景,这样的代码就能很好的防止调用代码修改结果列表,算是防御编程(defensive programming)的良好实践。这样的结果列表就是 Immutable List,是类似 String 的 Immutable 对象。

使用 Immutable 对象有以下的优点:

  • 更安全:对不可靠的客户代码库来说,使用它也无法修改数据,可以在未受信任的类库中安全的使用这些对象
  • 线程安全:Immutable 对象在多线程下没有竞态条件,可以直接共享
  • 更高效:不需要支持可变性, 可以尽量节省空间和时间的开销
  • 可以当成常量反复使用:通过静态工厂方法从缓存中取出已经存在的 Immutable 对象,而不是重新创建

但是,也应该注意它的缺点:Immutable 对象无法修改,当处理大量数据时可能会制造大量垃圾(类似 String),需要根据要求谨慎使用。

这样的修改应该发生在代码重构环节,原始编码时可以不予考虑,但一旦要将代码部署到生产环节就必须进行仔细的修改。 另外,《Effective Java》和《重构 —— 改善既有代码的设计》这样的数还是应该好好读读的。

参考: http://stackoverflow.com/questions/5552258/collections-emptylist-vs-new-instance https://my.oschina.net/jasonultimate/blog/166810