java8特性之forEach篇

2,503 阅读3分钟

forEach介绍

forEach是java8的特性之一,它可以大大简化代码的操作,比如有关HashMap的操作:

HashMap<Integer, String> hashMap = new HashMap<>(3);
hashMap.put(1, "张三");
hashMap.put(2, "李四");
hashMap.put(3, "王五");
for (Map.Entry<Integer, String> entry : hashMap.entrySet()) {
    System.out.println(entry.getKey() + "," + entry.getValue());
}

这是使用映射的视图来遍历整个hashmap来输出键值对的逻辑,输出如下:

写起来比较繁琐,看起来也有点累,那么使用forEach就可以简化为如下代码:

Map<Integer, String> hashMap = new HashMap<>(3);
hashMap.put(1, "张三");
hashMap.put(2, "李四");
hashMap.put(3, "王五");
hashMap.forEach((k, v) -> System.out.println(k + "," + v));

可以发现,它简化了大部分的操作。那么我们就会有几个问题,比如什么情况下可以使用forEach,以及它的底层迭代原理是什么,性能跟传统的foreach相比如何等。

使用条件

点进forEach方法中,可以发现,它是Iterable接口的一个方法,因此可以得出一个结论,只要一个类实现了此接口,那么此类的实例一定可以使用forEach方法。

同时我们可以看到,Collection接口继承了此接口。而我们大部分的集合类接口都继承了Collection接口,具体有Set、List、Map、SortedSet、SortedMap、HashSet、TreeSet、ArrayList、LinkedList、 Vector、Collections、Arrays、AbstractCollection。所以只要是上述的实现类,都可以使用forEach方法。

迭代原理

让我们回归Iterable接口,看看接口中的方法:

default void forEach(Consumer<? super T> action) {
    Objects.requireNonNull(action);
    for (T t : this) {
        action.accept(t);
    }
}

方法的形参是一个Consumer类型的action,我们可以猜到,这一定是跟lambda表达式相关的一个东西。事实上,它是一个函数式接口,让我们看看Consumer:

@FunctionalInterface
public interface Consumer<T> {
​
    /**
     * 可实现方法,接受一个参数且没有返回值
     *
     * @param t the input argument
     */
    void accept(T t);
​
    /**
     * 返回一个组合的{@code consumer},该组合的{@code consumer}按顺序执行此操作,然后执行     * {@code after}操作。如果执行任一操作时引发异常,则将其中继到组合操作的调用方。如果执行此操作       * 引发异常,则{@code after}操作将不会执行。
     *
     * @param after the operation to perform after this operation
     * @return a composed {@code Consumer} that performs in sequence this
     * operation followed by the {@code after} operation
     * @throws NullPointerException if {@code after} is null
     */
    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

因此实际上,它还是使用了foreach来遍历迭代对象,在一个个对参数执行对应的操作。

性能

为了测试性能,我们可以编写一个循环,来输出遍历完的时间,具体如下:

public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
​
    for (int i = 0; i < 100000; i++) {
        list.add(i);
    }
​
    long l = System.currentTimeMillis();
    list.forEach(i -> {
    });
    System.out.println(System.currentTimeMillis() - l);
​
    l = System.currentTimeMillis();
    for (Integer s : list) {
    }
    System.out.println(System.currentTimeMillis() - l);
}

输出结果如下:

大约相差了十五倍。那么为什么.forEach就会比foreach慢了个十倍左右的数量级呢?细细比较两者区别可以想到forEach多了一个Consumer的声明,那么我们再来测试一下:

public static void main(String[] args) {
    List<Integer> list = new ArrayList<>();
​
    for (int i = 0; i < 100000; i++) {
        list.add(i);
    }
​
    // 声明Consumer
    long l = System.currentTimeMillis();
    Consumer<Integer> consumer = integer -> {
    };
    System.out.println(System.currentTimeMillis() - l);
​
    // forEach
    l = System.currentTimeMillis();
    list.forEach(consumer);
    System.out.println(System.currentTimeMillis() - l);
​
    // foreach
    l = System.currentTimeMillis();
    for (Integer integer : list) {
    }
    System.out.println(System.currentTimeMillis() - l);
}

输出结果如下:

确实可以得出结论:forEach相比较foreach10倍级的开销大部分都消耗在了实例化Consumer上,迭代器本身并没有什么区别。

但forEach效率真的这么低吗?

其实不是的,java会使用预热,当第二次第三次调用forEach的时候,速度不比foreach慢,有可能跟JIT有关。