Java 9之前集合类的一些变化

2022-04-05   


本文介绍Java 8,Java 9中关于Collection(集合类)中的新特性。

  1. Iterable接口中的forEach
    非常常见的场景,循环一个list,输出所有元素。现在假设list是String,在Java 7之前这么写:
for(int i = 0; i < list.size(); i++){
    System.out.println(list.get(i));
}

或这么写:

Iterator it = list.listIterator();
while(it.hasNext()) {
    System.out.println(it.next());
}

Java 7引入了语法糖for each,于是这么写:

for(String s : list){
    System.out.println(s);
}

Java 8封装了重复代码,可以变为一行:

list.forEach(System.out::println);
  1. remove
    如果要删除元素的话,要使用iterator的remove,所以Java 7只能退回到传统for循环来这么写:
for(Iterator it = list.listIterator(); it.hasNext();) {
    String s = it.next();
    if(s.startsWith("A")){
        it.remove();
    }
}

这里额外有一个算法相关的问题需要说一下。ArrayList中如果使用上面这种写法,做完删除操作的时间复杂度是O(n^2),因为每回删除后都要将后面的元素整体向前移动,如果n很大而且移除的元素也很多的话效率就低下了,所以推荐直接复制到新list的写法,这样牺牲空间复杂度使时间复杂度变为O(n),因为总的来说空间还是比较好获得的。写法如下:

for(String s : list) {
    if(!s.startsWith("A")) {
        newList.add(s);
    }
}    

Java 8新增了removeIf,而且在ArrayList中的重写方法是基于复制的O(n)算法,可以变为一行:

list.removeIf(s -> s.startsWith("A"));
  1. replaceAll
    如果想要将list中元素进行替换的话,以往是没有replace相关操作的,只能手动get和set。
for(int i = 0; i < list.size(); i++) {
    list.set(i, list.get(i).toUpperCase());
}

Java 8新增replaceAll可以一行,不过这只是函数封装,内部实现还是get和set:

list.replaceAll(String::toUpperCase);

再说说Map

  1. forEach
    Java 7:
for(Map.Entry<String, String> entry : map.entrySet()) {
    System.out.println(entry.getKey() + entry.getValue());
}

我比较喜欢用entry的方式,因为一次可以把key,value都取出来,如果只取key,用key去取value的时候还有开销。不过取entry的初始开销也不小,所以还是要分情况讨论。

Java 8依然是函数封装,具体实现和上面差不多,可以变为一行:

map.forEach((k, v) -> System.out.println(k + v));
  1. replaceAll
    Java 7:
for(Map.Entry<String, String> entry : map.entrySet()) {
    entry.setValue(entry.getValue().toUpperCase());
}

Java 8:

map.replaceAll((k, v) -> v.toUpperCase());

这里有一个强调的地方,只能对value做操作,如果要操作key,只能回到Java 7的写法了。

在Java 8中运用Comparator做排序更优雅了,比如下面这个先用姓,再用名,名为null在最前的一段代码:

students.sort(comparing(Student::getLastName)
              .thenComparing(Student::getFirstName,
                             nullsFirst(naturalOrder())));

谈了这么多Java 8,说说Java 9。

  1. 不可变集合
    Java 9增加了很多静态工厂方法,来生成不可变的集合。
List.of(e1, e2);
Set.of(e1, e2);
Map.of(k1, v1, k2, v2);
Map.ofEntries(entry(k1, v1), entry(k2, v2));

值得一提的是这里面有很多重载方法,其中Map的一个重载可以接受20个参数,Map.of(k1, v1, k2, v2, k3, v3, k4, v4, k5, v5, k6, v6, k7, v7, k8, v8, k9, v9, k10, v10),如果超过20了,就只能使用ofEntries的可变参数方法了。我的天,这是不是在开玩笑。虽然经常被吐槽,但是Map还是保留了这样一个神奇的方法。官方说,可变参数是有开销的,这我能理解,不过为什么选择了10这样一个界限呢。

另外,这是用来生成不可变集合的,如果要生成可变集合,只能使用各种老方法,其中Map就最啰嗦了,需要put好多行。

  1. 随机迭代
    当我知道这一点的时候挺惊讶的。在我的理解中,HashMap,HashSet等类每回迭代返回的结果就应该是不一定的,随机的。但是官方说以前实际是相对稳定的,这导致好多同行写代码还以此为基准。关键保序的有LinkedHashMap,TreeSet这样的集合类啊,怎么能期待Hash的呢?无论如何,到了Java 9,这些Hash的集合类不在确定迭代顺序了。

  2. 不接受Null
    这可真是个大变化,这原来是考题。List,Set,HashMap的key,value都能是null,但Hashtable的key,value都不能。
    到了Java 9一个都不能了。。。不过这应该说的是新API,否则Java的完全向后兼容就不成立了。
    官方说,在1.2的时候允许null被加入集合类就是个大错误!而且在Java 9中,null在一些API方法中被赋予了特殊含义,表明不存在,如果null还等同于一个普通元素可以被任意添加的话,API就乱套了。

总的来说,Java 8和9都在努力改进集合类,无论是新增的方法还是对原有方法的内部实现的改进。8这一代的Stream和Lambda相关的操作对集合类影响还是较大的,但9这一代相比起来就是小提升了。

Q.E.D.


Talk is cheap, show me the code