JDK1.8中的新特性——Lambda表示式和Stream

JDK1.8中的新特性——Lambda表达式和Stream

文章目录

一、函数接口(functional interface)

带有单个抽象方法的接口是特殊的,值得特殊对待。这些接口现在被称作函数接口。

二、Lambda表达式(Lambda表达式优于匿名类)

Java允许利用Lambda表达式创建这些函数接口的实例。

Lambda类似于匿名类的函数,但是比它简洁得多。

举例:

List<String> list = new ArrayList<>();
list.add("a");
list.add("ddd");
list.add("cc");
list.add("eeeeee");
list.add("bbbbb");

使用匿名内部类实现排序功能:

Collections.sort(list, new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return Integer.compare(o1.length(), o2.length());
            }
        });

使用lambda表达式实现排序功能:

// 使用lambda表示式实现与匿名内部类相同的功能
Collections.sort(list, (s1,s2)->Integer.compare(s1.length(), s2.length()));

使用方法引用:

// 方案一:使用方法的引用
Collections.sort(list, Comparator.comparingInt(String::length));
// 方案二:使用方法的引用
list.sort(Comparator.comparingInt(String::length));

2.1 关于lambda表达式

  • 编译器利用一个称作类型推导的过程,根据上下文推断出这些类型;

    编译器式从泛型获取到得以执行类型推导的大部分类型信息的。

    扫描二维码关注公众号,回复: 11201572 查看本文章
  • Lambda没有名称和文档;如果一个计算本身不是自描述的,或者超出了几行,那就不要把它放在一个Lambda中;

    对于Lambda而言,一行是最理想的,三行诗合理的最大极限。

  • Lambda表达式咸鱼接口函数

  • Lambda无法获得对自身的引用。在Lambda中,关键字this是指外围实例。在匿名类中,关键字this是指匿名类实例。

  • 无法通过实现来序列化和反序列化Lambda与匿名类共享的属性。

    因此,尽可能不要序列化一个Lambda(或者匿名类实例)。如果想要可序列化的函数对象,如Comparator,就使用私有静态嵌套类的实例。

三、方法引用

只要方法引用能做的事,就没有Lambda不能完成的。

3.1 五种方法引用概述:

方法引用类型 范例 Lambda等式
静态 Integer::parseInt str->Integer.parseInt(str)
有限制 Instant.now()::isAfter Instant then = Instant.now(); t->then.isAfter(t)
无限制 String::toLowerCase str->str.toLowerCase()
类构造器 TreeMap<K,V>::new ()->new TreeMap<K,V>
数组构造器 int[]::new len->new int[len]

只要方法引用更加简洁、清晰,就用方法引用;如果方法引用并不简洁,就坚持使用Lambda。

四、使用Lambda时,坚持使用标准的函数接口

只要标准的函数接口能够满足需求,通常应该优先考虑,而不是专门再构建一个新的函数接口

好处:

  1. 这样会使API更加容易学习;
  2. 通过减少它的概念内容,显著提升操作性优势;

标准的函数接口

java.util.function包中有43个接口。如果能记住其中6个基础接口,必要时就可以推断出其余接口了。

(1)基础接口

基础接口作用于对象引用类型

  • Operator 接口代表其结构与参数类型一致的函数;

  • Predicate 接口代表一个参数并返回一个boolean的函数;

  • Function 接口代表其参数与返回的类型不一致的函数;

  • Supplier 接口代表没有参数并且返回一个值的函数;

  • Consumer 代表的是带有一个参数但不返回任何值的函数,相当于消费掉了其参数;

接口 函数签名 范例
UnaryOperator T apply(T t) String::toLowerCase
BinaryOperator T apply(T t1, T t2) Integer::sum
Predicate Boolean test(T t) Collection::isEmpty
Function R apply(T t) Integer::parseInt
Supplier T get() Instant::now
Consumer void accept(T t) System.out::println

(2)基础接口的三种变体

这六个基础接口各自还有三种变体,分别可以作用于基本类型:int、long和double。它们的命名方式是在其基础接口名称前面加上基本类型而得。例如:

//        IntUnaryOperator
//        LongUnaryOperator
//        DoubleUnaryOperator

这些变体除了Function变体外,都不是参数化的:

@FunctionalInterface
public interface DoublePredicate {
    boolean test(double value);
  ...
}

@FunctionalInterface
public interface IntFunction<R> {
    R apply(int value);
  ...
}

LongFunction<int[]>表示带有一个long类型的参数,并返回一个int[]数组

(3)Function接口还有9种变体,用于结果类型为基本类型的情况

源类型和结果类型始终不一样。

因为从类型到自身的函数就是Operator

如果源类型和结果类型均为基本类型,就在Function前面添加格式如SrcToResult

这样有6种变体:

//        IntToLongFunction
//        IntToDoubleFunction
//        LongToIntFunction
//        LongToDoubleFunction
//        DoubleToIntFunction
//        DoubleToLongFunction

如果源类型为对象类型,结果类型是一个基本类型,就在Function前面添加To<Src>

这样有三种变体:

//        ToIntFunction
//        ToLongFunction
//        ToDoubleFunction

(4)带两个不同参数的版本(都为对象引用,三种)

// BiPredicate<T, U>
// BiFunction<T, U, R>
// BiConsumer<T, U>

(5)Function带有两个不同参数,并返回基本类型(三种)

// ToIntBiFunction<T, U>
// ToDoubleBiFunction<T, U>
// ToLongBiFunction<T, U>

(6)Consumer带有一个对象引用和一个基本类型(三种)

// ObjIntConsumer<T>
// ObjLongConsumer<T>
// ObjDoubleConsumer<T>

(7)BooleanSupplier接口,Supplier接口的一种变体

// BooleanSupplier

注意事项:

千万不要用带包装类型的基础函数接口来代替基本函数接口

现有的大多数标准接口函数都支持基本类型。千万不要用带包装类型的基础函数接口来代替基本函数接口,使用装箱基本类型进行批量操作处理,最终会导致致命的性能问题。

必须始终用@FunctionalInterface注解对自己编写的函数接口进行标注。

不要在相同的参数位置,提供不同的函数接口来进行多次重载的方法

五、Stream

JKD1.8 中增加了Stream API,简化了串行或并行的大批量操作。这个API提供了两个关键的抽象:Stream(流)代表数据元素有限或无限的顺序,Stream pipeline(流管道)则代表这些元素的一个多级计算。

5.1 Stream概述

5.1.1 Stream流

Stream中的数据元素可以是对象引用,或者基本类型值。它支持三种基本类型:int、long和double。

5.1.2 Stream pipeline 流管道

一个Stream pipeline中包含一个源Stream,接着是0个或者多个中间操作和一个终止操作

  • Stream pipeline通常是lazy的

    直到调用终止操作时才会开始计算,对于完成终止操作不需要的数据元素,将永远不会被计算。

  • 默认情况下,Stream pipeline是按顺序运行的

    要使pipeline并发执行,只需在该pipeline的任何Stream上调用parallel方法即可,但通常不建议这么做

(1)中间操作

所有的中间操作都是将一个Stream转换成另一个Stream。

(2)终止操作

终止操作会在最后一个中间操作产生的Stream上执行一个最终的计算。

5.1.3 Stream的使用

(1)最好避免利用Stream来处理char值;

(2)重构现有代码来使用Stream,并且只在必要的时候才在新代码中使用;

5.2 Stream并不只是一个API,它是一种基于函数编程的模型

纯函数是指其结果只取决于输入的函数:它不依赖任何可变的状态,也不更新任何状态。为了做到这一点,传入Stream操作的任何函数对象,无论是中间操作还是终止操作,都应该是无副作用的。

一个错误的实例(下面的代码根本不是Stream代码;只不过是伪装成Stream代码的迭代式代码):

因为这段代码利用一个改变外部状态(频率表)的Lamdba,完成了终止操作的forEach中的所有工作

    /**
     * 一段伪装成Stream代码的迭代式代码
     * 因为这段代码利用一个改变外部状态(频率表)的Lamdba,完成了在终止操作的forEach中的所有工作
     * @param file
     */
    private static void readFileCountWorld(File file){
        Map<String, Long> freq = new HashMap<>();
        try(Stream<String> words = new Scanner(file).tokens()){
            words.forEach(world->{freq.merge(world.toLowerCase(), 1L, Long::sum);});
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }

正确的Stream代码如下:

    private static void readFileCountWorldWithStream(File file){
        Map<String, Long> freq;
        try(Stream<String> words = new Scanner(file).tokens()){
            freq = words.collect(groupingBy(String::toLowerCase, counting()));
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }

为了获得Stream带来的描述性和速度,有时还有并行性,必须采用范型以及API。

Stream泛型最重要的部分是把计算构造成一系列变型,每一级结果都尽可能靠近上一级结果的纯函数。

5.2.1 forEach操作应该只用于报告Stream计算的结果,而不是执行计算

forEach操作是终止操作中最没有威力的,也是对Stream最不友好的。它是显式迭代,因而不适合并行。

有时候,也可以将forEach用于其他目的,比如将Stream计算的结果添加到之前已经存在的集合中去。

5.2.2 收集器(collector)

Collectors API 有39种方法。

静态导入Collectors的所有成员是惯例也是明智的,因为这样可以提升Stream pipeline的可读性

(1)将Stream的元素集中到一个真正的Collection里去:

toList()toSet()toCollection(collectionFactory)
  • toList() 返回一个列表

  • toSet() 返回一个集合

  • toCollection(collectionFactory) 返回程序员指定的集合类型

Map<String, Long> freq = new HashMap<>();
...
List<String> topThree = freq.keySet().stream()
  .sorted(Comparator.comparing(freq::get).reversed())
  .limit(3)
  .collect(toList());

(2)Collectors 中的另外36种方法大多数是为了便于将Stream集合到映射中

toMap
  • 带两个参数:toMap(keyMapper,valueMapper) 最简单的映射收集器

这种简单的形式如果出现多个Stream元素映射到同一个键,pipeline就会抛出一个IllegalStateException异常将它终止。

public enum Operation {
  ....
    private static final Map<String, Operation> stringToEnum =
            Stream.of(values()).collect(Collectors.toMap(Object::toString, e-> e));
  ...
}
  • 带三个参数:toMap(keyMapper,valueMapper,mergeFunction) 除了提供键和值以外,还提供一个合并函数

合并函数是一个BinaryOperator。合并函数将与键关联的任何其他值与现有值合并起来,因此,假如合并函数是乘法,得到的值就是与该值映射的键关联的所有值的积。

List<WorldObj> worldObjs = new ArrayList<>();
worldObjs.add(new WorldObj("aa", 2));
worldObjs.add(new WorldObj("aa", 3));
worldObjs.add(new WorldObj("bb", 1));
Map<String, WorldObj> topWorldObj = worldObjs.stream()
                .collect(toMap(WorldObj::getWorld, worldObj -> worldObj,
                BinaryOperator.maxBy(Comparator.comparingInt(WorldObj::getLen))));
  • 带有三个参数的toMap形式还有另一种用途,即生成一个收集器,当有冲突时强调“保留最后更新”(last-write-wins)

格式:

toMap(keyMapper, valueMapper, (oldVal, newVal)->newVal)

案例:

List<WorldObj> worldObjs = new ArrayList<>();
worldObjs.add(new WorldObj("aa", 2));
worldObjs.add(new WorldObj("aa", 3));
worldObjs.add(new WorldObj("bb", 1));
worldObjs.add(new WorldObj("aa", 23));
Map<String, WorldObj> map =
     worldObjs.stream().collect(toMap(WorldObj::getWorld, worldObj -> worldObj, (oldVal, newVal) -> newVal));
        

前三种版本还有另外的变换形式,命名为toConcurrentMap,能有效地并行运行,并生成ConcurrentMap实例

  • 带有四个参数的toMap形式: toMap(keyMapper, valueMapper, mergeFunction, mapFactory)

带有第四个参数,这是一个映射工厂,在使用时要指定映射实现,如EnumMap或者TreeMap

groupingBy
  • groupingBy(classifier) 最简单的一个版本,只带一个分类器,并返回一个映射

    Map<String, List<WorldObj>> map = worldObjs.stream().collect(groupingBy(WorldObj::getWorld));
    
  • groupingBy(classifier, downstream) 指定一个分类器和一个下游收集器(downstream)

    Map<String, Integer> map02 = worldObjs.stream().collect(groupingBy(WorldObj::getWorld, summingInt(WorldObj::getLen)));
    

    下游收集器最简单的用法时传入toSet(),结果生成一个映射,这个映射值为元素集合而非列表;另一种方法是在下游收集器的位置上传toCollection(collectionFactory),允许创建存放各元素类别的集合。

  • groupingBy使用counting()作为下游收集器

    这样会生成一个映射,它将每个类别与该类别中的元素数量关联起来

    Map<String, Long> map03 = worldObjs.stream().collect(groupingBy(WorldObj::getWorld, counting()));
    
  • groupingBy(classifier, mapFactory, downstream)的第三个版本,除了下游收集器,还可以指定一个映射工厂

    注意参数mapFactory要在downStream参数之前,而不是在它之后。(这个方法违背了标准的可伸缩参数列表模式)

    这个版本可以控制所包围的映射,以及所包含的集合,以及所包围的集合

    TreeMap<String, Long> treeMap = worldObjs.stream().collect(groupingBy(WorldObj::getWorld, TreeMap::new, counting()));
    
  • groupingByConcurrent 方法提供groupingBy所有三种重载的变体

    这些变体可以有效地并发运行,生成ConcurrentHashMap实例

  • partitioningBy 方法提供groupingBy两种变体

    带有一个断言参数: partitioningBy(predicate) ,返回一个键为Boolean的映射

    Map<Boolean, List<WorldObj>> map04 = worldObjs.stream().collect(partitioningBy(worldObj -> worldObj.getLen() > 10));
    

    带有一个断言参数和一个下游收集器:partitioningBy(predicate, downstream)

    Map<Boolean, Long> map05 = worldObjs.stream().collect(partitioningBy(worldObj -> worldObj.getLen() > 10, counting()));
    

5.2.3 类似count的方法

通过Stream上的count方法,直接就有相同的功能,因此压根没有理由使用collect(counting())

  • count
  • summingaveragingsummarizing
  • reducingfilteringmappingflatMappingcollectingAndThen

5.2.4 joining 它只在CharSequence实例的Stream中操作

joining只在CharSequence实例的Stream中操作,例如字符串。

        String[] strArr = {"world", "hello"};
        String str01 = Stream.of(strArr).collect(joining());
        String str02 = Stream.of(strArr).collect(joining("$"));
        String str03 = Stream.of(strArr).collect(joining("#", "{", "}"));

5.3 Stream和Iterable,以及flatMap方法的使用

  • 将Stream<E>变成Iterable<E>, 用于提供给forEach遍历

    		/**
         * 适配器, 将Stream<E> 编程 Iterable<E>
         * @param stream
         * @param <E>
         * @return
         */
        public static <E> Iterable<E> iterableOf(Stream<E> stream){
            return stream::iterator;
        }
    
  • Iterable<E>变成Stream<E>, 便于用Stream pipeline进行处理

        /**
         * 适配器,将 Iterable<E> 变成 Stream<E>
         * @param iterable
         * @param <E>
         * @return
         */
        public static <E> Stream<E> streamOf(Iterable<E> iterable){
            return StreamSupport.stream(iterable.spliterator(), false);
        }
    
  • Collection接口是Iterable的一个子类型,它有一个stream方法,因此提供了迭代和stream访问

    对于公共的、返回序列的方法,Collection或者适当的子类型通常是最佳的返回类型

    数组也通过Arrays.asListStream.of方法提供了简单的迭代和stream访问。

  • 千万别在内存中保存巨大的序列,将它作为集合返回即可

5.4 通过AbstractList定制集合

假设要返回一个指定集合的幂集,例如:{a,b,c} 的幂集是{}、{a}、{b}、{c}、{a,b}、{a,c}、{b,c}、{a,b,c}

如果集合中有n个元素,它的幂集就有 2 n 2^n 个。

在二进制数0至 2 n 1 2^n-1 和有n位元素的集合的幂集之间,有一个自然映射。

获取指定集合(集合元素个数不能大于 2 n 2^n )的幂集:

    public static final <E>Collection<Set<E>> of(Set<E> s){
        List<E> src = new ArrayList<>(s);
        if (src.size()>30){
            throw new IllegalArgumentException("Set too big" +s );
        }
        return new AbstractList<Set<E>>() {
            // 2 to the power srcSize
            // 这个方法限制了 序列的长度位 Integer.MAX_VALUE  或者 2^31-1
            @Override
            public int size() {
                return 1<<src.size();
            }

            @Override
            public boolean contains(Object o) {
                return o instanceof Set && src.containsAll((Set)o);
            }

            @Override
            public Set<E> get(int index) {
                Set<E> result = new HashSet<>();
                System.out.println("index : " + index);
                for (int i = 0; index != 0; i++, index >>= 1) {
                    if ((index & 1) == 1) {
                        System.out.println("i = " + i);
                        result.add(src.get(i));
                    }
                }
                return result;
            }
        };
    }

使用Stream来获取幂集:

Stream.concat方法将空列表添加到返回到Stream;flatMap方法生成一个包含了所有前缀的所有后缀的Stream。

flatMap 这个操作将Stream中的每格元素映射到一个Stream中,然后将这些新的Stream全部合并到一个Stream(或将他们扁平化)

通过映射IntStream.rangeIntStream.rangeClosed 返回的连续int值的Stream,生成了前缀和后缀。

/**
 * @Date 2020/2/10
 * @Author lifei
 */
public class SubLists {
    /**
     * 获取元素的幂集
     *   flatMap 生成一个包含了所有前缀的所有后缀的Stream
     * @param list
     * @param <E>
     * @return
     */
    public static <E> Stream<List<E>> of(List<E> list){
        return Stream.concat(Stream.of(Collections.emptyList()), prefixes(list)
                .flatMap(SubLists::suffixes));
    }
    /**
     * 列表的前缀
     * @param list
     * @param <E>
     * @return
     */
    private static <E> Stream<List<E>> prefixes(List<E> list){
        return IntStream.rangeClosed(1, list.size()).mapToObj(end->list.subList(0, end));
    }

    /**
     * 列表的后缀
     * @param list
     * @param <E>
     * @return
     */
    private static <E> Stream<List<E>> suffixes(List<E> list){
        return IntStream.range(0, list.size()).mapToObj(start -> list.subList(start, list.size()));
    }
}

第二种Stream方案:

    public static <E> Stream<List<E>> of02(List<E> list){
        return IntStream.range(0, list.size())
                .mapToObj(start -> IntStream.rangeClosed(start + (int)Math.signum(start), list.size()).mapToObj(end -> list.subList(start, end)))
                .flatMap(x->x);
    }

    public static <E> Stream<List<E>> of03(List<E> list){
        return Stream.concat(Stream.of(Collections.emptyList()), IntStream.range(0, list.size())
                .mapToObj(start-> IntStream.rangeClosed(start + 1, list.size()).mapToObj(end -> list.subList(start, end)))
                .flatMap(x->x));
    }

5.5 谨慎使用Stream并行

在Java中编写并发程序变得越来越容易,但是要编写出正确又快速的并发程序,则一向没那么简单。

  • 千万不要任意地并行Stream pipeline, 它造成的性能后果有可能是灾难性的。

  • 在Stream上通过并行获得的性能,最好是通过ArrayList、HashMap、HashSet和ConcurrentHashMap实例,数组、int范围和long范围等。

    (1)这些数组结构的共性是,都可以被精确、轻松地分成任意大小的子范围,使并行线程中的分工变得更加轻松。

    Stream类库用来执行这个任务的抽象是分割迭代器(spliterator),它是由Stream和Iterable中的spliterator方法返回的;

    (2)这些数据结构共有的另一项重要特征是,在进行顺序处理时,他们提供了优异的引用局部性。

    引用局部性:序列化的元素引用一起保存在内存中。

    引用局部性对于并行批处理来说至关重要:没有它,线程就会出现闲置,需要等待数据从内存转移到处理器的缓存中。

    具有最佳引用局部性的数据结构是基本类型数组,因为数据本身是相邻地保存在内存中的。

  • Stream pipeline 的终止操作本质上也影响了并发执行的效率;

  • 并行Stream不仅可能降低性能,包括活性失败,还可能导致结果出错,以及难以预计的行为;

  • 并行Stream是一项严格的性能优化。对于任何优化都必须在改变前后对性能进行测试,以确保值得这么做;

    一般来说,程序中所有的并行Stream pipeline都是在一个通用的fork-join池中运行的。只要有一个pipeline运行异常,都会损害到系统中其他不相关部分的性能。

原创文章 161 获赞 19 访问量 6万+

猜你喜欢

转载自blog.csdn.net/hefrankeleyn/article/details/104236547