jisonami A Java Dev Engineer

Java集合框架(二)--Java8新增的函数式集合操作方式


这是Java集合框架第二篇,介绍关于Java8新增的函数式集合操作方式

在我看来,Java8新增的所有特性都是为FP(函数式编程)服务的,这就要求我们要有FP思维。

长久以来,我们一直在OOP(面向对象编程)的思想下编程,OOP确实很不错,提供了清晰的接口声明,但是OOP的实现代码比较啰嗦,冗余的代码也比较多。

而FP提供了更加简洁明了的语法,但是纯用FP的代码又比较晦涩难懂。这时就有人提倡接口声明和框架分层之间使用OOP,而在具体的实现或者算法封装中使用FP,这样就把OOP和FP的优点都结合起来了。

更有甚者,有篇文章提到,接口声明使用Java提供,具体实现使用Groovy(JVM上一门动态语言,也包含了函数式的特性,还包含了元编程)实现。

参考这篇文章: Java与groovy混编 —— 一种兼顾接口清晰和实现敏捷的开发方式

这涉及到JVM多语言混合编程,提高了编程的复杂性,其实我们使用Java8的新特性也可以做到函数式编程了。

而在Java8中,新增了很多函数式的特性,比如lambda表达式、高阶函数、闭包等,尽管依旧没有提供函数柯里化与偏函数(函数部分调用)的特性。

简单说一说Java8的新特性

解释一下Java8中新增的函数式特性的概念

高阶函数

高阶函数:一种可以将函数作为参数传入或者将函数作为返回值的函数。

lambda表达式

lambda表达式:一种将类似数学表达式赋值给变量的语法,这个数学表达式其实就是被语法糖简化了的函数。

然后将这个lambda表达式作为参数或返回值的函数就是高阶函数。

lambda有点像Java8之前的匿名内部类,其实不是。它们在JVM中的底层实现方式是不一样的,所以造成的结果也不完全一样。

lambda与匿名内部类一个明显的区别就是this指针的引用。匿名内部类的this是指向该匿名内部类本身,而lambda则是指向使用该lambda表达式的对象。

Java8的lambda表达式是作为实现只有单个抽象方法的接口来使用的。这种接口也叫函数式接口。

闭包

闭包:一种变量作用域,其实就是在lambda表达式中引用了外部变量时,这个外部变量自动变成final修饰的不可变变量了。

也就是说这个变量在被赋予初始值后,不管是在lambda表达式外部还是内部都不可改变了。

重新设计的接口语法

Java8的接口语法被重新设计了,原本接口是全部都是抽象函数的OOP契约,现在接口中可以有很多default默认方法(其实就是实例方法)和static静态方法。

其实变得跟抽象类的作用差不多了。不过永远记住default方法只在重构代码时才考虑引入,否则会容易造成多重继承代码无法编译通过的情况。

流式编程

Java8的流式编程,这个才是Java8函数编程的核心,通过在集合框架的顶层接口中新增default方法的方式,为原本无法扩展的集合API提供了函数式编程的入口方法,比如stream()方法和parallelStream()方法。

此外在其它的API中也提供了流式编程的入口,这里只介绍Java集合方面的。

Java8新的集合操作方式

在做了这么多啰嗦的介绍之后,终于进入本部分的正题了。

Iterator接口中新增的foreach()默认方法

先看下foreach方法的源代码:

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

这个默认方法接受一个函数式接口Consumer作为参数,所以我们可以使用lambda作为参数,这个方法其实就是将集合遍历了一下,然后对每个集合元素进行了参数中lambda表达式的执行。

其实就是将遍历的行为交给了forEach()方法,我们只需要提供遍历的行为即可。

在没有forEach()方法之前,我们是这样进行遍历的

    for (Integer e : ad) {  
        System.out.println(e);  
    }  

而使用forEach()方法则把三行代码变成了一行

ad.forEach(e -> System.out.println(e));  

实际上我们是实现了源码中的action.accept(t);这一行代码。

好吧,我承认这个forEach()方法好像并没有什么用。

Collection接口新增的removeIf()默认方法

先看一下removeIf()方法的源代码

default boolean removeIf(Predicate<? super E> filter) {  
        Objects.requireNonNull(filter);  
        boolean removed = false;  
        final Iterator<E> each = iterator();  
        while (each.hasNext()) {  
            if (filter.test(each.next())) {  
                each.remove();  
                removed = true;  
            }  
        }  
        return removed;  
}  

其实removeIf()方法就是帮我们把集合遍历了一把,然后把满足条件的元素删掉了。

没有removeIf()方法之前,我们是这样做的

首先有这么一个集合

    List<Integer> al = new ArrayList<Integer>();  
    al.add(4);  
    al.add(3);  
    al.add(5);  

然后我们要遍历和删除大于3的元素

    Iterator<Integer> it = al.iterator();  
    while(it.hasNext()){  
        if(it.next()>3){  
            it.remove();  
        }  
    }  

而使用removeIf()方法代码则一行代码就解决了

al.removeIf(e -> e>3);  

Collection接口新增的stream()和parallelStream()方法

stream()和parallelStream()方法都是获取了一个Stream对象,之后的编程方式是一样。

不同的是stream()得到的是串行流,也就是流对象的操作是单线程执行的。

parallelStream()得到的是并行流,也就是流对象的操作可能是多线程同时进行的,然后将结果合并到一起。

永远记住Java8的流式编程没有改变原来的集合类,而是在流式方法调用之后产生了新的集合或者结果。

这里以stream()为例介绍函数式编程的三板斧(filter、map、reduce)

其实就是筛选、映射、合并。

还是先以面向对象的例子说起

如果我们要在一个集合中遍历并筛选出部分元素进行做乘2操作后求和,之前我们是这样做的

先有一个集合

    List<Integer> al = new ArrayList<Integer>();  
    al.add(4);  
    al.add(3);  
    al.add(5);  
    al.add(7);  
    al.add(6);  

筛选大于4的元素记性乘2操作后求和

    int sum = 0;  
    for (Integer e : al) {  
        if(e >4){  
            sum = sum + e*2;  
        }  
    }  

如果使用stream()的方式来操作是这样的

int sum = al.stream()  
    .filter(e -> e>4)  
    .map(e -> e*2)  
    .reduce((e1,e2) -> (e1+e2)).get();  

有些人认为使用collect来替代reduce好一些,其实是一样的,只是少了个get()操作而已。

int sum = al.stream()  
    .filter(e -> e>4)  
    .map(e -> e*2)  
    .collect(Collectors.summingInt(e -> e));  

List接口新增的sort()默认方法

先看下源码

default void sort(Comparator<? super E> c) {  
    Object[] a = this.toArray();  
    Arrays.sort(a, (Comparator) c);  
    ListIterator<E> i = this.listIterator();  
    for (Object e : a) {  
        i.next();  
        i.set((E) e);  
    }  
}  

可以看到sort方法就是接受了一个Comparator比较器对象,然后将List集合转换成数组,然后再调用数组工具类Arrays的sort()方法进行排序,最后把排好序的数组遍历并赋值到原来的List集合上。

原来我们是使用Collections.sort()方法来进行排序的,现在这个Collections.sort()方法直接将实现委托到List接口的默认方法sort()下了。其实是一样的。

在Java8里面唯一比以前方便的就是可以直接使用lambda表达式传入Comparator比较器的实现参数了。

假设我们在List存放的元素是下面这个类

class KeyTest{  
    int value;  
  
    public KeyTest(int value){  
        this.value = value;  
    }  
      
    public int getValue() {  
        return value;  
    }  
      
}  

然后往List插入几个KeyTest元素

    List<KeyTest> al = new ArrayList< KeyTest>();  
    al.add(new KeyTest(4));  
    al.add(new KeyTest(3));  
    al.add(new KeyTest(5));  

然后排序

al.sort((e1,e2) -> Integer.valueOf(e1.getValue()).compareTo(e2.getValue()));  

当然,sort()方法也可以传入null值作为参数,这要求插入List中的元素必须实现Comparable接口,即

如果KeyTest实现了Comparable接口

class KeyTest implements Comparable<KeyTest>{  
        int value;  
  
        public KeyTest(int value){  
            this.value = value;  
        }  
          
        public int getValue() {  
            return value;  
        }  
  
        @Override  
        public int compareTo(KeyTest o) {  
            return Integer.valueOf(this.value).compareTo(o.value);  
        }  
    }  

那么排序时就可以这样

al.sort(null);  

至此,Iterator继承系列的接口新增的默认方法就介绍完了,接下来说一说Map接口新增的一些默认方法,不过Map接口没有提供转换成Stream接口的方法。

Map接口新增的forEach()默认方法

这个Map接口的forEach()默认方法与Collection接口的forEach()方法用法是一致的,不过这里遍历的是Map的entrySet,看下源码就知道了。

default void forEach(BiConsumer<? super K, ? super V> action) {  
        Objects.requireNonNull(action);  
        for (Map.Entry<K, V> entry : entrySet()) {  
            K k;  
            V v;  
            try {  
                k = entry.getKey();  
                v = entry.getValue();  
            } catch(IllegalStateException ise) {  
                // this usually means the entry is no longer in the map.  
                throw new ConcurrentModificationException(ise);  
            }  
            action.accept(k, v);  
        }  
}  

我们要实现的方法是action.accept(k, v);

map.forEach((k, v) -> System.out.println(k + "->" + v));  

Map接口新增的replaceAll()默认方法

这个方法的逻辑其实是把entrySet遍历了一把,然后把所有的value替换成lambda表达式计算的结果,key值保存不变。

先看下源码

default void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {  
        Objects.requireNonNull(function);  
        for (Map.Entry<K, V> entry : entrySet()) {  
            K k;  
            V v;  
            try {  
                k = entry.getKey();  
                v = entry.getValue();  
            } catch(IllegalStateException ise) {  
                // this usually means the entry is no longer in the map.  
                throw new ConcurrentModificationException(ise);  
            }  
  
            // ise thrown from function is not a cme.  
            v = function.apply(k, v);  
  
            try {  
                entry.setValue(v);  
            } catch(IllegalStateException ise) {  
                // this usually means the entry is no longer in the map.  
                throw new ConcurrentModificationException(ise);  
            }  
        }  
}  

关键的代码是这行

v = function.apply(k, v);  

我们传入的lambda参数需要实现这个apply()方法

有这么一个Map集合

    Map<String, String> map = new HashMap<String, String>();  
    map.put("test", "value test");  
    map.put("jdbc", "value jdbc");  
    map.put("spring", "value spring");  

我们需要对这个map的所有value的”value”字符串变成”element”,那么

map.replaceAll((k, v) -> v.replaceAll("value", "element"));  

使用之前的forEach()方法遍历就可以看到结果

HashMapForEach

Map接口其余的默认方

以下几个方法均是对value取值和设置的方法

  • 若key值对应的value为null,则返回defaultValue
V getOrDefault(Object key, V defaultValue)
  • 若key值对应为value为null,设值为value,否则不操作
V putIfAbsent(K key, V value)  
  • 若key值对应的value为null,根据mappingFunction计算的值设值,否则不操作
V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
  • 若key值对应的value不为空,根据remappingFunction计算的值设值,否则不操作
V computeIfPresent(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)
  • 根据remappingFunction计算的值对key对应的value设值,若计算的值为null,则删除key-value键值对
V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)  
  • 若key对应的value为null,则直接将第二个参数value对key对应的value设值。若key对应的value不为null,根据remappingFunction计算的值对key对应的value设值。若计算的值为null,则删除key-value键值对。
V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction)  

最后,Java8对集合框架新增的默认方法大部分都是传入lambda表达式作为参数的高阶函数。

刚接触函数式编程的同学可能觉得不容易看懂,其实对函数式编程的概念理解清楚之后,直接看这些API的源代码是比较容易的。


Similar Posts

Comments