Java lambda 表达式详解

2020.08.14 02:08:23
47
阅读约 34 分钟

lambda表达式 #

runnable接口的匿名内部类实现

public class Test01 {
    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println("inside thread construct using lamda");
        }).start();
    }
}

表达式的值会自动返回,由于print方法返回的是void,所以该表达式同样返回void,与run方法返回类型相匹配
lamda表达式必须匹配接口中单一抽象方法签名的参数类型和返回类型。

将lambda表达式赋值给变量 #

public class Test02 {
    public static void main(String[] args) {
        Runnable r = () -> System.out.println("inside thread construct using lamda");
        new Thread(r).start();
    }
}

FileNameFilter #

匿名内部实现类,只返回java源文件

public class Test03 {
    public static void main(String[] args) {
        File file = new File("./src/com/lamda表达式/runnable接口的匿名内部类/");
        String[] list = file.list((dir, name) -> name.endsWith(".java"));
        System.out.println(Arrays.asList(list));
    }
}

方法引用 #

如果说lambda表达式本质上是将方法作为对象进行处理,那么方法引用就是将现有方法作为lambda表达式进行处理。
例如:foreach

Stream.of(1,2,3,4,5,5,6).forEach(System.out::println);

Consumer<Integer> printer = System.out::println;
Stream.of(1,2,3,4,5,6).forEach(printer);
  1. 方法引用往往更短
  2. 通常包括类的名称

静态方法中使用方法引用 #

Stream.generate(Math::random)
        .limit(10)
        .forEach(System.out::println);

语法:
方法引用包括以下三种形式,其中一种存在一定的误导性

  • object::instaceMethod
    引用特定对象的实例方法,如 System.out::println

  • Class:staticMethod
    引用静态方法,如Math::max

  • Class::instanceMethod
    调用特定类型的任意对象的实例方法,如String::length

最后一种形式令人迷惑,通过类名来调用实例方法语法的解释有所不同,其等效的lambda表达式为:

// String::length
x-> x.length

如果通过类名引用一个传入多个参数的方法,则上下文提供的第一个元素将作为方法的目标,其它元素作为方法的参数

例子

List<String> list = Arrays.asList("this", "is", "a", "lsit", "of", "strings");
List<String> collect = list.stream().sorted((s1, s2) -> s1.compareTo(s2)).collect(Collectors.toList());
List<String> collect1 = list.stream().sorted(Comparator.naturalOrder()).collect(Collectors.toList());

Stream接口定义的sorted方法传入Comparator作为参数,其单一抽象方法为intconpare(String other).。

在流处理中,如果需要处理一系列输入,则会频繁使用方法引用中的类名来访问实例方法。下面显示了对流中各个String调用length方法

/**
 * 使用方法引用在String上调用length方法
 */
public class Test04 {
    public static void main(String[] args) {
        Stream.of("this","is","a","stream","of","strings")
                .map(String::length)
                .forEach(System.out::println);
    }
}

方法引用的等效表达式

  // 等效表达式
 Stream.of("this","is","a","stream","of","strings")
       .map(s->s.length())
       .forEach(x->System.out.println(x));

构造函数引用 #

用户希望将方法引用作为流水线的一部分,以实例化某个对象。下面的例子是将一份人员列表转为相应的姓名列表。

List<Person> personList = new ArrayList<>();
personList.add(new Person("张三"));
personList.add(new Person("李四"));
List<String> collect = personList.stream()
        .map(person -> person.getName())
        .collect(Collectors.toList());

或者

List<String> collect1 = personList.stream()
         .map(Person::getName)
         .collect(Collectors.toList());

使用构造方法引用,给定一个字符串集合,通过lambda表达式或构造函数引用,可以将其中的每个字符串印射到person类

List<String> names = Arrays.asList("张三", "李四", "王五");
List<Person> people = names.stream().map(name -> new Person(name))
        .collect(Collectors.toList());
List<Person> people1 = names.stream().map(Person::new)
        .collect(Collectors.toList());

Person::new的作用是引用Perosn类中的构造函数。与所有lambda表达式类似,由上下文决定那个构造函数。有于上下文中只提供了一个字符串,使用单参数的String构造函数。

复制构造函数 #

复制构造函数传入一个Person参数,并返回一个具有相同特性的新Person,如下例所示

   Person(Person p){
        this.name=p.name;
    }

如果需要将流代码从原始实例中分离出来,复制构造函数很有用。假如我们有一个人员列表,先将其转为流,再转换会列表,那么引用不会发生变化。

 Person before = new Person("张三");
 List<Person> people = Stream.of(before)
        .collect(Collectors.toList());
Person after = people.get(0);
System.out.println(before==after);
before.setName("李四");
// before发生变化后,afte也变化了
System.out.println(after.getName());
System.out.println("李四".equals(after.getName()));

返回结果为两个true,可以通过构造函数切断连接。

 List<Person> people = Stream.of(before)
      .map(Person::new)
      .collect(Collectors.toList());
Person after= people.get(0);
System.out.println(before==after);  // false
System.out.println(before.equals(after)); //true

before.setName("王五");
System.out.println(before.equals(after)); //false

可变参数构造函数 #

Person(String... name){
        this.name= Arrays.stream(name).collect(Collectors.joining(" "));
   }

将列表中的每个字符串拆分为单个单词,并调用可变参数构造函数

List<String> names = Arrays.asList("张三", "李四", "王五");
List<Person> collect = names.stream()
         .map(name -> name.split(" "))
         .map(Person::new)
         .collect(Collectors.toList());
  1. 创建字符串流
  2. 印射到字符串数组流
  3. 印射到person流
  4. 收集到person列表
    在本例中,map方法的上下文包含Person;:new构造函数引用,他是一个字符串数组流,因此调用可变参数构造函数。

数组 #

构造函数引用也可以和数组一起使用,如果希望采用Person实例的数组(Person[])而非列表,可以使用Stream接口定义的toArray方法,它的签名为

<A> A[] toArray(IntFunction<A[]> generator)

采用A表示数组的泛型类型。数组包含流的元素,由所提供的generator函数创建。我们甚至还可以使用构造函数引用,如下

 List<String> names = Arrays.asList("张三", "李四", "王五");
 Person[] people = names.stream()
           .map(Person::new)
           .toArray(Person[]::new);

toArray 方法参数创建了一个大小合适的Person引用数组,并采用经过实例化的Person实例进行填充
构造方法引用其实是方法引用的称,通过关键字new调用构造函数。同样,由上下文决定调用哪个构造函数

函数式接口 #

函数式接口是一种包含单一抽象方法的接口,因此可以作为lambda表达式或方法引用的目标。
我们定义一个接口。

@FunctionalInterface
interface PalindromeChecker{
   boolean isPalidrome(String s);
}

@FunctionalInterface并非必须,但使用它可以触发编译时校验,有助于确保接口符合要求,如果接口不包含或包含多个抽象方法,程序将报错。
函数式接口中可以使用default和static方法,他们与仅包含一个抽象方法并不冲突。

@FunctionalInterface
interface PalindromeChecker{
   boolean isPalidrome(String s);

   default String dosome(){
       return "hello";
   }

   static void staticMethod(){
       System.out.println("im a static method");
   }
}

如果一个接口继承现有的函数式接口后,又添加了其它抽象方法,该接口就不再是函数式接口。

interface MyChildInterface extends PalindromeChecker {
    int anotherMehtod();
}

java.util.function 包 #

java.util.function 包中包含四类接口,分别是consumer(消费型接口),supplier(供给型接口),
predicate(谓词型接口)以及function(功能型接口)。consumer接口传入一个泛型参数(generic argument)
,不返回任何值;supplier接口不传入参数,返回一个值;predicate接口传入一个参数,返回一个布尔值;function接口传入一个参数,返回一个值。

consumer接口 #

希望编写实现java.util.function.Consumer包的lambda表达式,接口定义的方法如下,其抽象方法为void accept(T t)

void accept(T t);
default Consumer<T> andThen(Consumer<? super T> after);

accept 方法传入一个泛型参数并返回void。在所有传入Consumer作为参数的方法中,最常见的的是java.util.Interable接口的默认foreach方法

default void forEach(Consumer<? super T> action)

接口实现

 List<String> strings = Arrays.asList("this", "is", "a", "lsit", "of", "strings");
        strings.forEach(new Consumer<String>() { //匿名内部类实现
            @Override
            public void accept(String s) {
                System.out.println(s);
            }
        });

strings.forEach(s->System.out.println(s));  // lambda表达式
strings.forEach(System.out::println); //方法引用

其它consumer接口

接口 单一抽象方法
IntConsumer void accept(int x)
DoubleConsumer void accept(double x)
LongConsumer void accept(long x)
BiConsumer void accept(T t, U u)

supplier接口 #

使用lambda表达式或方法引用来实现 T get() 方法
使用supplier接口非常简单,它不包含任何静态或默认方法,只有一个抽象方法 T get()
为实现这个接口,需要提供一个不传入参数且返回泛型类型的方法。
一种简单运用是Math.random方法,它不传入参数且返回double型数据,如:

Logger logger = Logger.getLogger("com.lamda表达式.第二章.supplier.Test01");

DoubleSupplier randomSupplier = new DoubleSupplier() {
            @Override
 public double getAsDouble() {
                return Math.random();
        }
   };
randomSupplier= ()->Math.random();
randomSupplier =Math::random;
logger.info(String.valueOf(randomSupplier.getAsDouble()));

集合中搜索名称

  List<String> names = Arrays.asList("mail", "wash", "kaylee", "inara", "zoe","CC");

Optional<String> first = names.stream().filter(name -> name.startsWith("H"))
       .findFirst();
System.out.println(first);
System.out.println(first.orElse("None"));
System.out.println(first.orElse(String.format("No result found in %s",names.stream()
        collect(Collectors.joining(", ")))));
System.out.println(first.orElseGet(()-> String.format("No result found in %s",
        names.stream().collect(Collectors.joining(", ")))));
    }

predicate接口 #

用户希望使用java.util.function.Predicate接口筛选数据
Predicate接口定义的方法

default Predicate<T> and(Predicate<? super T> order)
static<T> Predicate<T> isEqual(Object tagetRef)
default Predicate<T> negate
default Predicate<T> or(Predicate<? super T> order)
boolean test(T t)

单一抽象方法,给定一个名称集合,可以通过流处理找出所有具有特定长度的实例

   public String getNamesOflength(int length,String... names){
        return Arrays.stream(names).filter(s->s.length()==length)  // 满足给定长度字符串的谓词
                .collect(Collectors.joining(", "));
    }

查找以给定字符串开头的字符串

public String getNamesStartingWith(String s,String... names){
    return Arrays.stream(names)
            .filter(str->str.startsWith(s))
            .collect(Collectors.joining(", "));
}

如果允许指定条件,predicate的通用性会更强。

public  String getNamesSatisfyingCondition(Predicate<String> condition,String...names){
    return Arrays.stream(names)
            .filter(condition)
            .collect(Collectors.joining(", "));
}

上述用法相当灵活,可以提供一些常量作为常用的谓词。

 public static  final  Predicate<String> LENGTH_FIVE = s->s.length()==5;
 public static final Predicate<String> START_WITH_S =s -> s.startsWith("S");

提供谓词作为参数的另一个优点是,可以通过默认方法and、or与negate,并根据一系列单个元素来创建复合谓词。

funtion接口 #

提供一个实现R apply(T t) 方法的lambda表达式

default <V> Function<T,V> andThen(Function<? super R,? extend V> after)
default <V> Function<V,R> compose(Function<? super V,? extend T> before)
R apply(T t)
static <T> Funciton<T,T> indentity()

funciton最常用的用法是作为Stream.map方法的一个参数。例如,为了将String转为一个整数,可以在每个实例上带哦用length方法。

List<String> names = Arrays.asList("mail", "wash", "kaylee", "inara", "zoe","CC");
List<Integer> nameLength = names.stream()
        .map(new Function<String, Integer>() {
            @Override
            public Integer apply(String s) {
                return s.length();
            }
        }).collect(Collectors.toList());

 nameLength = names.stream()
        .map(s -> s.length()).collect(Collectors.toList());

nameLength = names.stream()
        .map(String::length).collect(Collectors.toList());

流式操作 #

为支持函数式编程,java引入了新的流式隐喻。流是一种元素序列。通过一系列的流水线的中间操作传递元素,并利用终止表达式完成这一过程。

流的创建 #

使用java.util.stream.Stream接口定义的各种静态工厂方法,以及java.lang.Interable接口或者
java.util.Arrays类定义的stream方法。
具体而言,可以采用Stream.of,Stream.iterate,Stream.generate等静态方法创建流

Stream.of方法传入元素的可变参数列表:

static <T> Stream<T> of(T...value)

Stream.of方法的引用实现

public static<T> Stream<T> of(T...values){
    return Arrays.stream(values);
}

Stream.of方法简单创建流

String collect = Stream.of("ss", "dd", "cc").collect(Collectors.joining(", "));
System.out.println(collect);
String[] munsters = {"ss", "dd", "cc"};
String collect1 = Arrays.stream(munsters).collect(Collectors.joining(","));

由于需要提前创建数组,上述方案略有不便,但足以满足可变参数列表的需要,java Api 定义了Array.stream方法的多种重载形式,用于处理int、lang和double型数组,还定义了本例使用的泛型类
stream接口定义的另一种静态工厂方法是iterate,其签名如下:

static <T> Stream<T> iterate(T send,UnaryOperator<T> f)

iterate方法返回一个无限顺序的有序流,它有迭代应用到初始元素种子的函数f产生。如果有办法根据当前值生成流的下一个值,iterate方法将相当有用。

List<BigDecimal> collect = Stream.iterate(BigDecimal.ONE, n -> n.add(BigDecimal.ONE))
        .limit(10)
        .collect(Collectors.toList());
System.out.println(collect);

List<LocalDate> collect1 = Stream.iterate(LocalDate.now(), ld -> ld.plusDays(1L))
        .limit(10)
        .collect(Collectors.toList());
System.out.println(collect1);

第一段代码采用Bigdecimal实例,从1开始递增,第二段从当日开始按天递增。由于生成的两个流都是无界的,需要通过中间操作limit加以限制。
steam接口还定义了静态工厂方法generate,签名为:

static <T> Stream<T> generate(Supplier<T> s)

generate方法通过多次调用Supplier产生一个顺序的无序流,Supplier的一种简单应用是Math.random方法,它不传入参数而返回double型数据。

tream.generate(Math::random)
        .limit(10)
        .forEach(System.out::println);

从集合创建流 #

如果已有集合,可以利用Collection接口新增的默认方法stream。

List<String> strings = Arrays.asList("张三", "李四", "王五");
        String collect = strings.stream().collect(Collectors.joining(","));
        System.out.println(collect);

子接口 #

Stream接口定义了三种专门用于处理基本数据类型的子接口,他们是 IntStreamLongStreamDoubleStreamIntStreamLongStream还包括另外两种创建流所用的工厂方法range和rangClosed,签名如下

static IntStream range(int staticInclusive,int endExclusive)
static IntStream range(int startInclusive,int endInclusive)
static LongStream range(long startInclusive,long endExclusive)
static LongStream rangeClosed(long startInclusive,long endInclusive)

rangeClosed 包含终值,而range不包含。两种方法都返回一个顺序的有序流,从第一个参数开始逐一递增。

//range和rangeClosed方法

List<Integer> collect = IntStream.range(10, 15)
        .boxed()                   // 装箱
        .collect(Collectors.toList());
System.out.println(collect);

 List<Long> collect1 = LongStream.rangeClosed(10, 15)
        .boxed()
        .collect(Collectors.toList());
System.out.println(collect1);

装箱流 #

问题:用户希望利用基本类型流创建集合

方案:可以使用java.util.stream.InStream接口定义的boxed方法包裹元素,也可以使用合适的包装器类(wrapper class)来印射值,还可以使用collect方法的三参数类型。

讨论:在处理对象流时,可以通过Collectors类提供的某种静态方法将流转换为集合。例如,对于一个给定的字符串流:

List<String> collect = Stream.of("this", "is", "a", "list")
            .collect(Collectors.toList());

然而,同样的过程并不适合处理基本类型流,如

IntStream.of(1,2,3,4,5).collect(Collectors.toList());

有三种方案可以解决,第一种使用boxed

List<Integer> collect1 = IntStream.of(1, 2, 3, 4, 5)
         .boxed()
         .collect(Collectors.toList());

第二种使用mapToObJ

List<Integer> collect2 = IntStream.of(1, 2, 3, 4, 5)
           .mapToObj(Integer::valueOf)
           .collect(Collectors.toList());

mapToInt。。。其余三种方法为相关的基本类型流。

第三种方案是采用collect方法的三参数形式:

IntStream.of(1, 2, 3, 4, 5)
            .collect(ArrayList<Integer>::new,ArrayList::add,ArrayList::addAll);

另外,如果希望流转为数组而非列表,那么toArray还是不错的。

int[] ints = IntStream.of(1, 2, 3, 4, 5).toArray();

利用reduce方法实现归约操作 #

问题:用户希望通过流操作生成单一值
方案:使用reduce方法对每个元素进行累加
讨论:java的函数式范式经常采用“印射 - 筛选 - 归约” (map-filter-reduce) 的过程处理数据。

  1. 内置的归约操作
    基本类型流定义了多种内置在api中的归约操作
方法 返回类型
average OptionalDouble
count long
max OptionalInt
min OptionalInt
sum int
summaryStatistics IntSummaryStatistics
collec(Supplier supplier ,ObjIntConsumer) R
accumulatore,BiConsumer<R,R> combiner
reduce int,OptionalInt

sum、count、max、min、average等归约操作的用途不言而明,如果流中没有元素,将返回optional。下面的例子处理了字符串集合长度的归约操作。

String[] s = "this is a array of strings".split(" ");
long count = Arrays.stream(s)
        .map(String::length)
        .count();
int sum = Arrays.stream(s)
        .mapToInt(String::length)
        .sum();

OptionalDouble average = Arrays.stream(s)
        .mapToInt(String::length)
        .average();
OptionalInt max = Arrays.stream(s)
        .mapToInt(String::length)
        .max();
OptionalInt max = Arrays.stream(s)
      .mapToInt(String::length)
      .min();

countStream接口定义的一种方法,因此无需印射给IntStream
sumaverage方法仅用于处理基本类型流

基本归约实现 #

IntStream接口定义了reduce方法的两种重载形式:

OptionalInt reduce(IntBinaryOperator op)
int reduce(int identity ,IntBinaryOperator op)

第一条语句传入IntBinaryOperator并返回OptionalInt,第二条语句则需要提供identity以及IntBinaryOperator

在不使用sum的情况下,如何实现整数的求和呢? 采用reduce的方案。

 int i = IntStream.rangeClosed(1, 10)
                .reduce((x, y) -> x + y).orElse(0);
System.out.println(i);

编写代码时,通常采用垂直方式安排流的流水线,这是基于流畅api的一种方案,其中一个方法的结果将作为下一个方法的目标。在例子中,因为reduce方法返回的不是流,所以将orElse置于同一行。

在本例中,IntBinaryOperatorlambda表达式提供,它传入两个int类型的数据并返回两者之和。不难想象,如果为IntBinaryOperator添加一个筛选器,流是可以空的,其结果是OptionalInt.之后的orElse方法表明,如果流中没有元素,返回值应该是0。

在lambda表达式中,可以将二元运算符的第一个参数视为累加器,第二个参数视为流中每个元素的值。通过逐一打印各个元素能很容易的理解这一点。

int i = IntStream.rangeClosed(1, 10)
        .reduce((x, y) -> {
            System.out.printf("x=%d,y=%d%n", x, y);
            return x + y;
        }).orElse(0);
System.out.println(i);

img

二元运算符返回的值在下一次迭代时变为x的值,而y依次传入流的每一个值。
那么,如果我们希望先处理每个数字,然后在求和呢? 例如,在求和之前将所有的数字增加一倍。我们可能会写出下面的代码。

int i = IntStream.rangeClosed(1, 10)
        .reduce((x, y) -> x + 2 * y)
        .orElse(0);
System.out.println(i);

得到的值为109(少了1)问题出在reducelambda表达式上:xy的初始值为1和2.也就是说,流的第一个值不会增加一倍。
可以采用reduce方法的重载形式解决这个问题,也就是为累加器传入一个初始值。正确代码如下:

int reduce = IntStream.rangeClosed(1, 10)
                .reduce(0, (x, y) -> x + 2 * y);
System.out.println(reduce);

通过将累加器x的初始值设置为0,y的值被赋给流中的各个元素,从而实现所有元素增加一倍。
当使用具有初始值的reduce方法时,返回类型时int而非 OptionalInt。

java标准库中的二元运算符 #

Integer、Long和Double类都定义了sum方法,其作用就是对两数求和。Integer类中sum方法的实现如下所示:

public static int sum(int a,int b){
        return a+b;
}

为什么要专门定义一种只为实现两个整数求和的方法呢?这是因为sum方法属于BinaryOperator(更确切的说,属于IntBinaryOperatore),很容易就能用于reduce方法。

Integer reduce = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
        .reduce(0, Integer::sum);
System.out.println(reduce);

可以看到,无需使用IntStream就能得到相同的结果, Integer类还定义了max和min方法,他们也是二元运算符,用法和sum类似。

 Integer reduce = Stream.of(3, 1, 4, 1, 5, 9)
                .reduce(Integer.MIN_VALUE, Integer::max);
System.out.println(reduce);

另一个有趣的例子时string类定义的concat方法,它传入一个参数,看起来不怎么像二元运算符。

String concat(String str)

concat方法可以用于reduce方法,如下:

String reduce = Stream.of("this", "is", "a", "list")
                .reduce("", String::concat);
System.out.println(reduce);

第一个参数作为目标,第二个参数作为参数。由于结果返回的是String.目标、参数与返回类型均为同一类型,可以将其视为reduce方法的二元运算符。
concat方法能大大缩短代码的尺寸。

使用收集器 #

尽管concat方法可行,但效率很低,更好的方案是采用Collector的collect方法。
Stream接口定义了collect方法的一种重载形式,他传入三个参数,分别是用于创建集合的supplier,为集合添加单个元素的BiConsumer以及合并两个集合的BiConsumer。相应的实现如下:

 String reduce = Stream.of("this", "is", "a", "list")
                .collect(()->new StringBuilder(),
                        (sb,str)->sb.append(str),
                        (sb1,sb2)->sb1.append(sb2)).toString();

可以通过方法引用简化上述代码

Stream.of("this", "is", "a", "list").collect(StringBuilder::new,
                StringBuilder::append,
                StringBuilder::append).toString();

不过最简单的办法是Collectors.join方法

String collect = Stream.of("this", "is", "a", "list")
                .collect(Collectors.joining(""));

reduce方法的最一般形式 #

reduce方法的第三种形式如下

<U> U reduce(U identity,BiFunction<U,? super T,U> accumulator, BinaryOperator<U> combiner)

这种形式略显复杂,通常可以采用更简单的手段实现相同的目标,如
定义了一个Book类,它只有一个ID和一个标题。

class Book{
    private Integer id;
    private String  title;
  // getter 和 setter tostring
}

假设存在一个图书馆列表,我们希望将列表中的图书添加到某个map。

采用 collectors.toMap方法解决这个问题更容易

一种解决方案

 List<Book> books = Arrays.asList(new Book(1, "书本1"), new Book(2, "书本2"), new Book(3, "书本3"));

HashMap<Integer, Book> reduce = books.stream()
        .reduce(new HashMap<Integer, Book>(),
                (map, book) -> {
                    map.put(book.getId(), book);
                    return map;
                },
                (map1, map2) -> {
                    map1.putAll(map2);
                    return map1;
                }
        );
reduce.forEach((k,v)->System.out.println(k+": "+v));

我们从reduce的最后一个参数开始分析, 这是最简单的
第三个参数是combiner,它必须是BinaryOperator。在本例中,提供的lambda表达式传入两个参数,他将第二个印射中所有键复制到第一个印射,再返回第一个印射。
第二个参数是一个函数,用于将一本书添加到map。类似的,如果map的put方法在新条目添加完毕后能返回map,函数会更简单。
第一个参数是combiner函数的表示值。在本例中,标识值是一个为空的Map,因为该标识值与其它任何Map结合后返回的是其它Map。

利用reduce方法校验排序 #

利用reduce方法对BigDecimal求和

  Integer reduce1 = Arrays.asList(1, 3, 5, 7, 9, 11, 13, 15, 17, 19).stream()
                .reduce(0, Integer::sum);
        System.out.println(reduce1);
        
BigDecimal reduce = Stream.iterate(BigDecimal.ONE, n -> n.add(BigDecimal.valueOf(2)))
        .limit(10)
        .reduce(BigDecimal.ZERO, (acc, val) -> acc.add(val));
System.out.println(reduce);

输出结果都为100,这是reduce方法最典型的应用,下面是根据长度对字符串排序

 List<String> strings = Arrays.asList("this", "is", "a", "list");
        List<String> collect = strings.stream().sorted(Comparator.comparingInt(String::length))
                .collect(Collectors.toList());
        System.out.println(collect);

那么,如何验证排序是否正确呢?答案就是比较相邻的字符串。

collect.stream().reduce((pre,curr)->{
            if (pre.length()<= curr.length()) System.out.println("true");
            return curr;
        });

利用peek方法对流进行调试 #

对整数进行倍增、筛选与求和,考虑这样一个方法

public int sumDoublesDivisibleBy3(int start,int end){
    return IntStream.rangeClosed(start,end)
            .map(n->n*2)
            .filter(n->n%3==0)
            .sum();
}

上述代码虽然可以运行,但是如果出现问题,很难找出问题所在,我们为流水线添加一个map操作,传入并打印每一个值,然后再次返回这些值。

public int sumDoublesDivisibleBy33(int start,int end){
    return IntStream.rangeClosed(start,end)
            .map(n->{
                System.out.println(n);
                return n;
            })
            .map(n->n*2)
            .filter(n->n%3==0)
            .sum();
}

程序将打印开始到结束的数字,这正是peek方法的工作原理,该方法声明如下:

Stream<T> peek(Consumer<? super T > action)

由于consumer仅传入一个值,不反回任何值,所提供的consumer不会对值造成破坏。如:

public int sumDoublesDivisibleBy333(int start,int end){
    return IntStream.rangeClosed(start,end)
            .peek(n->System.out.printf("original: %d%n",n)) // 在倍增前打印
            .map(n->n*2)
            .peek(n->System.out.printf("double: %d%n",n))
            .filter(n->n%3==0)
            .peek(n->System.out.printf("filter: %d%n",n))
            .sum();
}

尽管peek方法有助于调试,但不应置于生产环境。

字符串与流之间的转换 #

String类实现 CharSequence接口,它引入了两种能生成IntStream的方法,他们都是接口中的默认方法。

default IntStream chars()
default IntStream codePoints()

chars 和 codePoints 方法的不同之处在于,chars方法用于处理 utf-16 ,一个用于处理完整的Unicode代码点。
检查字符串是否属于回文 java8:

 public static void main(String[] args) {

        boolean hfsbbsfh = isPalindrome("hfsbbsfh");
        System.out.println(hfsbbsfh);

    }

    public static boolean isPalindrome(String s ){
        String forward = s.toLowerCase().codePoints().filter(Character::isLetterOrDigit)
                .collect(StringBuilder::new,
                        StringBuilder::appendCodePoint,
                        StringBuilder::append).toString();

        String backward = new StringBuilder(forward).reverse().toString();
        return forward.equals(backward);
    }

获取元素数量 #

利用Stream.count方法获取元素数量

 long count = Stream.of(1, 3, 4, 5, 6, 7, 8, 10, 123)
                .count();

这是一种特殊的归约操作,相当于:

return mapToLong(e->1L).sum();

首先,流的每一个元素都被印射为 1(long)。然后,mapToLong方法生成LongStream,它定义了sum方法。

对根据长度划分的字符串计数

   List<String> strings = Arrays.asList("this", "is", "a", "list");
        Map<Boolean, Long> collect = strings.stream().collect(Collectors.partitioningBy(s -> s.length() % 2 == 0, Collectors.counting()));
        collect.forEach((k,v)->System.out.printf("%5s: %d%n",k,v));
        //false: 1
        // true: 3

partitioningBy 方法的第一个参数是predicate,其作用是将字符串分为满足谓词和不满足谓词的两类。如果partitioningBy方法只有这一个参数,则结果为 Map<Boolean,List<String>>,其中键为truefalse,值为偶数长度和奇数长度字符串的列表。
下面采用partitioningBy方法的双参数重载形式,他传入predicate和Collector。Collector被称为下游收集器,用于对返回的每个字符串列表进行后期处理。这就是Collectors.counting方法的用例。

汇总统计 #

问题:用户希望获取数值流中元素的数量、总和、最大值、最小值以及平均值。
方案:使用IntStream、DoubleStream或LongStream接口定义的summaryStatistics方法。
summaryStatistics方法:

DoubleSummaryStatistics statistics = DoubleStream.generate(Math::random)
        .limit(1_000_000)
        .summaryStatistics();
System.out.println(statistics);
System.out.println(statistics.getCount());
System.out.println(statistics.getMax());
System.out.println(statistics.getAverage());
System.out.println(statistics.getMin());
System.out.println(statistics.getSum());

DoubleSummaryStatistics还定义了以下两个有趣的方法:

void accept(double value)
void combine(DoubleSummaryStatistics other)

accept方法用于在汇总信息中记录另一个值,而combine方法将两个 DoubleSummaryStatistics 对象合二为一。

查找流的第一个元素 #

问题:用户希望查找满足流中特定条件的第一个元素
方案:findFirstfindAny
讨乱:findFirst方法返回Optional,而findAny方法返回描述流中某个元素的 Optional。两种方法都不传入参数,意味着印射或筛选操作已经完成。
例如,给定一个整数列表,为查找第一个偶数,可以在应用偶数筛选器后使用findFirst方法:

Optional<Integer> first = Stream.of(1, 2, 3, 4, 5, 56, 7, 8)
        .filter(n -> n % 2 == 0)
        .findFirst();
System.out.println(first.get());

如果流为空,则返回值是一个空的Optional

Optional<Integer> first = Stream.of(1, 2, 3, 4, 5, 1, 7, 8)
        .filter(n -> n > 10)
        .filter(n -> n % 2 == 0)
        .findFirst();
System.out.println(first);
 //Optional.empty

因为流具有出现顺序,所以并没有对每个元素都取模。
在并行流中使用firstEven

Optional<Integer> first = Stream.of(1, 2, 3, 4, 5, 7, 8)
        .parallel()
        .filter(n -> n % 2 == 0)
        .findFirst();
System.out.println(first.get());
//2

初看之下有些奇怪,为什么在同时处理多个数字时仍会得到同一个值呢?原因在于出现顺序的概念。
java api 将出现顺序定义为数据源使其元素可用的顺序。List和Array都有出现的顺序,但set没有。
BaseStream接口还定义了一种名为unordered的方法,它可能返回一个无序流作为中间操作。
findAny方法要么返回描述流中某个元素的Optional,要么在流为空时返回一个空Optional。在本例中,操作的行为具有显式不确定性,这意味着可以自由选择流中的元素。

 public static void main(String[] args) {
        Optional<Integer> any = Stream.of(1, 2, 3, 4, 5, 56, 7, 8)
                .unordered()
                .parallel()
                .map(Test01::delay)
                .findAny();
        System.out.println(any.get());
    }

    public static Integer delay(Integer n){
        try {
            Thread.sleep((long) Math.random()*100);

        }catch (Exception e){

        }

        return n;
    }

顺序流和并行流使用findAny方法

  Optional<Integer> any = Stream.of(1, 2, 3, 4, 5, 56, 7, 8)
                .unordered()
                .parallel()
                .map(Test01::delay)
                .findAny();

        System.out.println(any.get());

        Optional<Integer> any1 = Stream.of(1, 2, 3, 4, 5, 56, 7, 8)
                .unordered()
                .map(Test01::delay)
                .findAny();


        System.out.println(any1.get());
        // 8
        // 1

anyMatch、allMatch和noneMatch方法 #

方法签名:

boolean anyMatch(Predicate<? super T> predicate)
boolean allMatch(Predicate<? super T> predicate)
boolean noneMatch(Predicate<? super T> predicate)

质数校验

public class Test01 {
    public static void main(String[] args) {
        int num = 5;
        boolean prime = isPrime(num);
        System.out.println("num "+num+(prime?" 是质数":" 不是质数"));
    }
    public static boolean isPrime(int num){
        int limit = (int) (Math.sqrt(num) + 1);
        return num==2 || num>1 && IntStream.rangeClosed(2,limit)
                .noneMatch(divisor->num % divisor ==0);
    }
}

注意:如果流为空,无论提
供的谓词是什么,anyMatch和noneMatch方法将返回true,而allMatch方法将返回false。

使用flatMap和map方法 #

问题:用户希望以某种方式转换流,但不确定使用哪个。
方案:如果需要将每个元素转换成一个值,使用Stream.map;如果需要将每个元素转换成多个值,且需要将生成的流展平。使用Stream.flatMap
map 和 flatMap方法均传入Function作为参数。map方法的签名如下:

<R> Stream<R> map(Function<? super T,? extends R> mapper)

Function传入一个输入,并将其转为输出。map方法则将一个T类型的输入转换为一个R类型的输出。
我们创建一个由顾客名和Order集合构成的Customer类。为简单起见,Order类只包含一个整数ID。

// 一对多关系
 class  Customer {
    private String name;
    private List<Order> orders  = new ArrayList<>();

     public Customer(String name) {
         this.name = name;
     }

     public String getName() {
         return name;
     }

     public List<Order> getOrders() {
         return orders;
     }

     public Customer  addOrder(Order order){
         orders.add(order);
         return this;
     }
 }

 class Order{
    private int id;

     public Order(int id) {
         this.id = id;
     }

     public int getId() {
         return id;
     }
 }

接下来,我们创建若干新顾客并添加一些订单

Customer sheridan = new Customer("Sheridan");
Customer ivanova = new Customer("Ivanova");
Customer garibaldi = new Customer("Garibaldi");
 sheridan.addOrder(new Order(1)).addOrder(new Order(2)).addOrder(new Order(3));

ivanova.addOrder(new Order(4)).addOrder(new Order(5));
List<Customer> customers = Arrays.asList(sheridan, ivanova, garibaldi);

当输入参数和输出类型之间存在一一对应的关系时,将执行map操作。可以将顾客映射到他们的姓名并打印。

// 将顾客映射到他们的姓名
customers.stream()
                .map(Customer::getName)
                .forEach(System.out::println);
//Sheridan
//Ivanova
//Garibaldi

如果将顾客映射到订单而不是姓名,就得到了一个集合的集合。

// 将顾客印射到订单
customers.stream()
                .map(Customer::getOrders)   // Stream<List<Order>>
                .forEach(System.out::println);  // [Order{id=1}, Order{id=2}, Order{id=3}] [Order{id=4}, Order{id=5}] []
        customers.stream()
                .map(customer -> customer.getOrders().stream()) // Stream<Stream<Order>>
                .forEach(System.out::println);

map操作的结果为Stream<List<Order>>,其最后一个列表为空。如果在订单列表中调用stream方法,则结果为 Stream<Stream<Order>>,其最后一个内部流为空流。
flatMap方法的作用就在此,其签名如下:

<R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper)

对于每个泛型参数T ,函数生成的是 Stream<R>而不是R。之后,flatMap方法从各个流中删除每个元素并将他们添加到输出,从而展平生成的流。

// 对顾客订单应用flatMap方法
customers.stream()                                                          // Stream<Customer>
                .flatMap(customer -> customer.getOrders().stream()) // Stream<Order>
                .forEach(System.out::println);     // Order{id=1} Order{id=2} ...

flatMap操作的结果为Stream<Order>。由于它已被展平,无需担心嵌套流。
需要指出的是,Optional类同样定义了mapflatMap方法。

流的拼接 #

问题:用户希望将两个或多个流合并为一个流
方法:Stream.concat适用于合并两个流。如果需要合并多个,请使用flatMap
签名如下:

static <T> Stream<T> concat(Stream<? extends T> a , Stream<? extends T> b)

concat 方法将创建一个惰性的拼接流,其元素是第一个流的所有元素,后跟第二个流的所有元素。

两个输入流所包含的元素必须相同

// 拼接两个流
 public static void concat() {
        Stream<String> first = Stream.of("a", "b", "c").parallel();
        Stream<String> second = Stream.of("X", "Y", "Z");
        List<String> collect = Stream.concat(first, second)
                .collect(Collectors.toList());
        List<String> strings = Arrays.asList("a", "b", "c", "X", "Y", "Z");
        System.out.println(collect.equals(strings));

    }// true

在合并多个流时,使用flatMap方法是一种更好的解决方法。

      Stream<String> first = Stream.of("a", "b", "c").parallel();
        Stream<String> second = Stream.of("X", "Y", "Z");
        Stream<String> third = Stream.of("alpha", "beta", "gamma");
        Stream<String> fouth = Stream.empty();

        List<String> strings = Stream.of(first, second, third, fouth)
                .flatMap(Function.identity())
                .collect(Collectors.toList());
        List<String> strings1 = Arrays.asList("a", "b", "c", "X", "Y", "Z", "alpha", "beta", "gamma");
        System.out.println(strings.equals(strings1));

惰性流 #

问题:用户希望处理满足条件所需的最少数量的流元素

// 将100到200之间的所有整数倍增,再找出能被3整除的第一个整数
 OptionalInt first = IntStream.range(100, 200)
                .map(n -> n * 2)
                .filter(n -> n % 3 == 0)
                .findFirst();
        System.out.println(first);

如果不了解处理机制,我们可能会认为上述代码做了许多无用功

  • 创建100到199之间的整数(100次操作)
  • 倍增(100次操作)
  • 校验能否被3整除 (100次操作)
  • 返回第一个元素 (1次操作)

不过不要误会,流处理的机制并非如此。流是惰性的,在达到终止条件前不会处理元素,达到终止条件后才通过流水线逐一处理每个元素。为说明这一点,我们对代码进行重构。

// 显式处理
 void  dosome(){
        OptionalInt first = IntStream.range(100, 200)
                .map(this::multByTwo)
                .filter(this::divByThree)
                .findFirst();
        System.out.println(first);
    }

    public int multByTwo(int n){
        System.out.printf("Inside multByTwo with arg %d%n",n);
        return n*2;
    }
    public boolean divByThree(int n){
        System.out.printf("Inside divByThree with arg %d%n",n);
        return n%3==0;
    }
    //Inside multByTwo with arg 100
//Inside divByThree with arg 200
//Inside multByTwo with arg 101
//Inside divByThree with arg 202
//Inside multByTwo with arg 102
//Inside divByThree with arg 204

比较器与收集器 #

java 8 为 java.util.Comparator 接口新增了多种静态和默认方法,使排序操作变得更为简单。现在,只需要通过一系列库调用,就能根据一个属性对POJO集合进行排序。
java 8 还引入了一个新的工具类java.util.Collectors,它提供将流转换回各类集合所需的静态方法。此外,收集器也可以在下游使用,利用它们对分组或分区操作进行后期处理。

利用比较器实现排序 #

问题:用户希望实现对象的排序

// 根据字典序对字符串排序
 private List<String> sampleStrings = Arrays.asList("this","is","a","list","of","strings");
    public List<String> defaultSort(){
        Collections.sort(sampleStrings);  // java1.7默认排序
        return sampleStrings;
    }
    public List<String> defaultSortUsingStreams(){
        return sampleStrings.stream()
                .sorted()
                .collect(Collectors.toList());  // java1.8默认排序
    }

Collections.sort方法不符合java8所倡导的将不可变形置于首要位置的函数式编程原则。
java8采用Stream.sorted方法实现相同的排序,但不对原始集合进行修改,而是生成一个新的流。在上面的例子中,完成集合的排序后,程序根据类的自然顺序对返回列表进行排序。对于字符串,自然顺序是字符序;如果所有字符串均为小写,自然顺序就相当于字母顺序。
如果希望以其他方式排序字符串,可以使用sorted方法的重载形式,传入Comparator作为参数。

// 根据长度对字符串排序
 public List<String> lengthSortUsingSorted(){
        return sampleStrings.stream()
                .sorted((s1,s2)->s1.length()-s2.length())
                .collect(Collectors.toList());  //使用lambda表达式作为Comparator 根据长度进行排序
    }

    public List<String> lengthSortUsingComparator(){
        return sampleStrings.stream()
                .sorted(Comparator.comparingInt(String::length))
                .collect(Collectors.toList()); // 使用Comparator.comparingInt方法
    }

sorted方法的参数为java.util.Comparator,他是一种函数式接口。对于第一个方法lengthSortUsingSorted,所提供的lambda表达式用于实现Comparator.compare方法。

import static java.util.Comparator.comparing;
// 根据长度对字符串进行排序,长度相同则按字母顺序排序
public List<String> lengthSortThenAlphaSort(){
        return sampleStrings.stream()
                .sorted(comparing(String::length)
                .thenComparing(naturalOrder())
                ).collect(Collectors.toList());
                
    }

其中comparing是静态导入的方法。
即便没有实现Comparable接口,上述方案也试用于任何类。如下例子

// 描述高尔夫球手的Golfer类
class Golfer{
    private String first;
    private String last;
    private int score;
    
}

为了创建一个高尔夫锦标赛排行榜,可以依次根据各个球手的得分、姓、名字进行排序。

private static List<Golfer>  golfers = Arrays.asList(
            new Golfer("张","三",68),
            new Golfer("李","四",70),
            new Golfer("王","五",70),
            new Golfer("周","六",68),
            new Golfer("何","七",70),
            new Golfer("王","八",67)
    );

    public List<Golfer> sortByScoreThenLastThenFirst(){
        return golfers.stream()
                .sorted(comparingInt(Golfer::getScore)
                      .thenComparing(Golfer::getLast)
                        .thenComparing(Golfer::getFirst)
                ).collect(toList());

    }

调用方法

Golfer{first='王', last='八', score=67} 
Golfer{first='张', last='三', score=68} 
Golfer{first='周', last='六', score=68} 
Golfer{first='何', last='七', score=70} 
Golfer{first='王', last='五', score=70} 
Golfer{first='李', last='四', score=70} 

将流装换为集合 #

问题:流处理完成后,用户希望将流转换为Listset或其他集合。
方案:使用Collector工具类定义的toListtoSet、或toCollection方法。

java8 一般通过流水线的中间操作来传递流元素,并在达到终止操作后结束。Stream接口定义的collect方法就是一种终止操作,用于将流转为集合。
collect方法有两种重载形式,如

// Stream.collect方法
<R,A> R collect(Collector<? super T,A,R>   collector)
<R> R collect(Supplier<R> supplier,BiConsumer<R,? super T> accumulator,BiConsumer<R,R> combiner)

由于java.util.stream.Collector属于接口,无法被实例化。Collector接口包含一个称为of的静态方法,它用于生成Collector,但通常有更好的方式可以实现。

// 创建list
List<String> strings = Stream.of("mr1","mr2","mr3","mr4","mr5","mr6","mr7","mr8")
            .collect(Collectors.toList());

上述方法创建一个ArrayList类,并采用给定的流元素进行填充。

// 创建 Set
Set<String> villains = Stream.of("mr1","mr2","mr3","mr3").collect(Collectors.toSet());  // 重复的人名

上述方法创建了一个HashSet类的实例并加以填充,并忽略任何重复的人名。
上述方法均使用默认的数据结构,对于List是ArrayList,对于set是HashSet。
如果希望指定数据结构,则应使用Collectors.toCollection方法,它传入Supplier作为参数。

 List<String> actors =   Stream.of("mr1","mr2","mr3","mr4","mr5","mr6","mr7","mr8")
            .collect(Collectors.toCollection(LinkedList::new));

Stream接口还定义了一个用于创建对象数组的方法toArray,它有两种重载形式:

Object[] toArray;
<A> A[] toArray(IntFunction<A[]> generator)

第一种形式返回一个包含流元素的数组,但未指定类型。第二种形式传入一个函数并生成所需的新数组,数组的长度与流相同,很容易与数组构造函数引用一起使用。如

String[]  wannabes =  Stream.of("mr1","mr2","mr3","mr4","mr5","mr6","mr7","mr8")
            .toArray(String[]::new);

返回数组具有指定的类型,其长度与流中的元素数量匹配。
为了将流转为Map,Collectors.toMap方法需要传入两个Function实例。分别用于键与值。
我们以一个包装namerole的Actor为例讨论。

//创建map
Set<Actor> actors = mysteryMen.getActors();
Map<String,String> actorMap = actors.Stream()
.collect(Collectors.toMap(Actor::getName,Actor::getRole));

actorMap.forEach((key,value)->
        System.out.printf("%s played %s%n",key,value));

利用Collectors.toConcurrentMap方法对本例稍作修改,就能创建ConcurrentMap

将线性集合添加到印射 #

问题:用户希望将对象添加到Map,其中键为某种对象属性,值为对象本身。
方案:使用Collectors类定义的toMap方法以及Function接口定义的identidy方法。
假设存在一个由Book实例构成的List。Book是一个简单的POJO,由ID,书名,价格参数构成。

class Book{
    private int id;
    private String name;
    private double price;
    }

此外,假设存在一个由Book实例构成的集合,如

List<Book> books = Arrays.asList(
        new Book(1,"第一本书",49.99),
        new Book(2,"第二本书",49.99),
        new Book(3,"第三本书",39.99),
        new Book(4,"第四本书",27.64),
        new Book(5,"第五本书",23.76)
);

很多情况下,我们许哟啊的可能是Map而非ListMap的键为图书ID,值为图书本身。借由Collectors.toMap方法。很容易就能将图书添加到Map,下面是两种不同的方案

Map<Integer,Book> bookMap = books.stream()
        .collect(Collectors.toMap(Book::getId,b->b));  //lambda标识,给定一个元素并返回
Map<Integer,Book> bookMaps = books.stream()
        .collect(Collectors.toMap(Book::getId, Function.identity())); //静态方法可以实现同样的目的        

对印射排序 #

问题:用户希望根据键或值对Map排序
方案:使用Map.Entry接口新增的静态方法
讨论:Map接口始终包含一个称为Map.Entry的公共静态内部接口public static inner interface,他表示一个键值对。Map接口定义的entrySet方法返回Map.Entry元素的Set。

java8为Map.Entry接口引入了一些新的静态方法:

方法 描述
comparingByKey() 返回一个比较器,他根据键的自然顺序比较Map.Entry
comparingByKey(Comparator<? super K> cmp) 返回一个比较器,它使用给定的Comparator并根据键比较Map.Entry
comparingByValue() 返回一个比较器,他根据值的自然顺序比较
comparingByValue(Comparator<? super V> cmp) 回一个比较器,它使用给定的Comparator并根据值比较Map.Entry

我们以创建单词长度与单词数量的Map为例,演示上述方法的用法。

//将词典读入Map
  try(Stream<String > lines = Files.lines(Paths.get(dictionary))){
        lines.filter(s -> s.length()>20)
                .collect(Collectors.groupingBy(String::length,Collectors.counting()))
                .forEach((len,num)->System.out.printf("%d: %d%n",len,num));
       }catch(Exception e){
           e.printStackTrace();
       };
  • Collectors.groupingBy方法传入Function作为第一个参数,表示分类器(classifier)。如果groupingBy方法只传入一个参数,则结果为Map,其中键为分类器的值,值为匹配分类器的元素列表。这种情况下,groupingBy(String::length)将返回Map<Integer,List>,其中键为单词长度,值为该长度的单词列表。
  • 双参数形式的groupingBy方法传入另一个Collector,他称为下游收集器,用于对单词列表进行后期处理。这种情况下,groupingBy方法的返回类型是Map<Integer,long>,其中键位单词长度,值为词典中该长度的单词数量。
// 输出结果
21:82
22:41
23:17
24:5

分区与分组 #

问题:用户希望将元素分为若干个类别
方案
Collector.partitioningBy方法将元素拆为满足Predicate与不满足Predicate的两类。
Collectors.groupingBy方法生成一个类别构成的Map,其中值为每个类别中的元素。
讨论:假设存在一个由字符串构成的集合,可以通过partitioningBy方法将这些字符串按偶数长度和奇数长度进行划分。

List<String> strings = Arrays.asList("this","is","a","long","list",
            "of","strings","to","user","as","a","demo");
    
    Map<Boolean,List<String>> lengtMap =strings.stream()
            .collect(Collectors.partitioningBy(s->s.length()%2==0));
//{false=[a, strings, a], true=[this, is, long, list, of, to, user, as, demo]}

partitioningBy方法包括两种形式

static <T>
    Collector<T, ?, Map<Boolean, List<T>>> partitioningBy(Predicate<? super T> predicate)
    
static <T, D, A>
    Collector<T, ?, Map<Boolean, D>> partitioningBy(Predicate<? super T> predicate, Collector<? super T, A, D> downstream)

groupingBy方法的签名如下:

static <T, K> Collector<T, ?, Map<K, List<T>>>
    groupingBy(Function<? super T, ? extends K> classifier)

Function参数传入流的各个元素,并提取需要分组的元素。接下来,我们不是将字符串简单的分为两类,而是根据长度进行划分。

// 根据长度对字符串分组
static  Map<Integer,List<String>> lengthMap = strings.stream()
           .collect(Collectors.groupingBy(String::length));
 lengthMap.forEach((k,v)->System.out.printf("%d: %s%n",k,v));
//
//1: [a, a]
//2: [is, of, to, as]
//4: [this, long, list, user, demo]
//7: [strings]

下游收集器 #

问题:用户希望对groupingBypartitioningBy操作返回的集合进行后期处理
方案:使用java.util.stream.Collectors类定义的某种静态工具方法
讨论groupingBypartitioningBy方法返回的是map,其中键为类别(对于partitioningBy方法是布尔值,对于groupingBy方法是对象),值为满足各个类别的元素列表。

// 根据偶数或奇数长度对字符串分区
 List<String> strings = Arrays.asList("this","is","a","long","list",
            "of","strings","to","user","as","a","demo");
    
    Map<Boolean,List<String>> lengtMap =strings.stream()
            .collect(Collectors.partitioningBy(s->s.length()%2==0));
//{false=[a, strings, a], true=[this, is, long, list, of, to, user, as, demo]}

我们或许更希望知道每个类别包含多少元素,也就是说,我们只需要各个列表种元素的数量。partitioningBy方法的重载形式如下,其第二个参数为Collector类型

static <T, D, A>
    Collector<T, ?, Map<Boolean, D>> partitioningBy(Predicate<? super T> predicate, Collector<? super T, A, D> downstream)

静态方法Collectors.counting的作用就在于此

// 对分区的字符串进行计数
 Map<Boolean,Long> numberLengthMap = strings.stream()
           .collect(Collectors.partitioningBy(s->s.length()%2==0,Collectors.counting()));
//
// false:4
// true:8

这就是所谓的下游收集器,他对下游的结果列表进行后期处理,groupingBy方法也有重载形式。
Stream接口定义的部分方法在Collectors类中有类似的对应

Stream Collectors
count counting
map mapping
min minBy
max maxBy
IntStream.sum summingInt
DoubleStream.sum summingDouble
LongStream.sum summingLong
IntStream.summarizing summarizingInt
DoubleStream.summarizing summarizingDouble
LongStream.summarizing summarizingLong

查找最大值和最小值 #

问题:用户希望确定流种的最大值或最小值
方案:可以使用BinaryOperator接口定义的maxByminBy方法,也可以使用Stream接口定义的max和min方法,还可以使用Collectors定义的maxBy和minBy方法。
讨论BinaryOperatorjava.util.function包定义的一种函数式接口,它继承自BiFunction接口,适合在函数和返回值的参数属于同一个类时使用。
BinaryOperator接口包含的两种静态方法:

static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator)
static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator)

我们以一个Employee POJO为例,讨论如何获取流的最大值。Employee POJO包括name、salary和department这三个特性。

class Employee{
    private String name;
    private Integer salary;
    private String department;
}
// 
 List<Employee> employees = Arrays.asList(  // 员工集合
                new Employee("张", 123, "公司1"),
                new Employee("何", 1234, "公司2"),
                new Employee("王", 12356, "公司3"),
                new Employee("郑", 123456, "公司4"),
                new Employee("李", 1234567, "公司5"),
                new Employee("舞", 12345678, "公司6")
        );     new Employee("舞",12345678,"公司6")
        );
 // 流为空时的默认值
Employee employee = new Employee("a man has no name ", 0, "balck and white");

给定一个由员工构成的集合,可以使用Streme.reduce方法,传入BinaryOperator作为参数。展示了如何查找工资最高的员工信息。

// BinaryOperator.maxBy方法的应用
 Optional<Employee> optionEmp = employees.stream()
                .reduce(BinaryOperator.maxBy(Comparator.comparingInt(Employee::getSalary)));
                
System.out.println(optionEmp.orElse(employee));

请注意,reduce方法需要传入BinaryOperator作为参数。静态方法maxBy根据所提供的Comparator生成BinaryOperator,并按工资高低对员工进行比较。

上述方案是可行的,不过采用Stream.max方法其实更简单,该方法可以直接应用于流:

Optional<T> max(Comparator<? super T> comparator)

下面是用法

// Stream.max 方法的应用
Optional<Employee> max = employees.stream()
                .max(Comparator.comparingInt(Employee::getSalary));

Stream.max方法与BinaryOperator.maxBy方法的结果并无不同。

此外,几种基本类型流(IntStream、LongStream、DoubleStream)也提供一个传入任何参数的max方法。

// 查找最高工资
OptionalInt max1 = employees.stream()
                .mapToInt(Employee::getSalary)
                .max();
System.out.println("The max salary is "+max1);

在本例中,maxToInt方法通过调用getSalary方法将员工流转换为整数流,并返回IntStream。之后Max方法返回OptionalInt.
类似的,Collectors工具类也定义了一种称为maxBy的静态方法,可以直接用于查找最高工资。

// collectors.maxBy 用作下游收集器
  Map<String, Optional<Employee>> map = employees.stream()
                .collect(Collectors.groupingBy(Employee::getDepartment,
                        Collectors.maxBy(Comparator.comparingInt(Employee::getSalary))));
        map.forEach((house,emp)->{
            System.out.println(house+":"+emp.orElse(employee));
        });

实现Collector接口 #

问题:由于java.util.stream.Collectors类提供的工厂方法无法满足需要,用户希望手动实现java.util.stream.Collectors接口。
:为工厂方法Collector.of传入的supplieraccumulatorcombinerfinisher函数提供lambda表达式或方法引用,以及其他所需的特性。

// 利用collect方法返回List
public List<String> evenLengthStrings(String...strings){
        List<String> collect = Stream.of(strings)
                .filter(s -> s.length() % 2 == 0)
                .collect(Collectors.toList()); //将偶数长度的字符串收集到List中
        return collect;
    }

辨析自定义收集器的过程略显复杂。收集器使用5个函数,他们的作用是将条目累加到可变容器,并由选择性的对结果进行转换。这五个函数是supplieraccumulatorcombinerfinisher以及characteristics.

我们首先讨论characteristics函数,表示Collector.Characteristics枚举的一个不可变的元素Set。三个枚举常量为CONCURRENTIDENTITY_FINISHUNORDEREDCONCURRENT表示结果容器支持多个线程在结果容器上并发的调用累加器函数,UNORDERED表示集合操作无需保留元素的出现顺序(encounter order),IDENTITY_FINISH表示终止函数返回其参数而不做任何修改。

// 例子 各种Collector方法的用法
R container = collector.supplier.get();  // 创建累加器容器
for(T t :data){
    collector.accumulator().accept(container,t); //将每个元素添加到累加器容器
}
return collector.finisher().apply(container); // 通过finisher函数将累加器容器转换为结果容器

利用collect方法返回不可修改的SortdSet

public SortedSet<String> oddLengthStringSet(String...strings){
        Collector<String, ?, SortedSet<String>> intolist = Collector.of(TreeSet<String>::new, // 创建一个新的TreeSet
                SortedSet::add,  //添加每个字符串
                (left, right) -> {   // 将两个SortedSet实例合二为一
            left.addAll(right);
            return left;
        }, Collections::unmodifiableSortedSet);  // 创建不可修改的Set

        return Stream.of(strings)
                .filter(s->s.length()%2!=0)
                .collect(intolist);

    }

程序将输出一个经过排序且不可修改的字符串集合,他按字典序排序。
本例展示了如何通过Collector.of方法生成收集器。of方法包括以下两种形似

static<T, A, R> Collector<T, A, R> of(Supplier<A> supplier,
                                                 BiConsumer<A, T> accumulator,
                                                 BinaryOperator<A> combiner,
                                                 Function<A, R> finisher,
                                                 Characteristics... characteristics)
                                                 
static<T, R> Collector<T, R, R> of(Supplier<R> supplier,
                                              BiConsumer<R, T> accumulator,
                                              BinaryOperator<R> combiner,
                                              Characteristics... characteristics)

流式操作、lambda表达式与方法引用的相关问题 #

java.util.Objects #

问题:用户希望使用静态方法实现非空验证、比较等操作。
方案:使用java.util.Objects类,它在流处理中相当有用。

//静态方法
static boolean deepEquals(Object a ,Object b)
// 验证两个数组是否深层相等,该方法在数组比较中尤其有用。

static boolean equals(Object a ,Object b)
// 验证两个参数是否彼此相等,它是安全的

static int hash(Obejct...values)
//为输入值序列生成散列码

static String toString(Object o)
//如果参数不为null则返回调用toString的结果,否则返回null

static String toString(Obejct o,String nullDefault)
// 如果第一个参数不为null,返回调用tostring的结果。如果第一个参数为null,返回第二个参数。

此外,可以通过requireNonNull方法的各种重载形式来验证参数

static <T> T requireNonNull(T obj)
// 如果参数不为null则返回T ,否则抛出NullPointerException

static <T> T requireNonNull(T obj,String message)
// 与上一个方法相同,但由于参数为null而抛出的NullPointerException将显示指定的消息。

static <T> T requireNonNull(T obj,Supplier<String> messageSupplier)
// 与上一个方法相同,但如果第一个参数为null,则调用给定的Supplier为NullPointerException生成消息

可以看到,最后一个方法传入Supplier作为参数,这也是为什么本书会讨论Objects类的原因。不过,更有说服力的原因在于Java8为Obejcts类引入的isNullnonNull方法,二者返回布尔值。

static boolean isNull(Obejct obj)
//如果提供的引用为null则返回true,否则返回false

static boolean nonNull(Obejct obj)
//如果提供的引用不为null则返回true,否则返回false

上述方法的优点在于,他们可以用作筛选器的Predicate实例。
我们以返回集合的类为例进行说明。下面定义了两个方法,分别返回完整的集合与滤掉空元素的集合。

// 返回集合与滤掉空元素
List<String> strings = Arrays.asList("this", null, "is", "a", null, "list", "of", "strings", null);
        List<String> collect = strings.stream()
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
        System.out.println(collect);

我们将上述过程一般化,使之不仅适用于字符串。下面的例子可以从任何列表中滤掉空元素。

// 从泛型列表中滤掉空元素
 public <T> List<T> getNonNullElements(List<T> list){
        return  list.stream()
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
    }

可以看到,对于生成list的方法,通过上述代码滤掉其中的空元素并非难事。

lambda表达式与效果等同于final的变量 #

问题:用户希望从lambda表达式内部访问在其外部定义的变量。
方案:必须将在lambda表达式内部访问的局部变量声明为final,或使其具备等同于final的效果。

// 对list中的所有值求和
 static List<Integer> nums = Arrays.asList(3,1,43,4,5,1,23);

    public static void main(String[] args) {
        int total =0;
        for (int num : nums) {
            total+=num;                // 传统方式
        }
        total=0;
        nums.forEach(n->total+=n);  //无法编译

        total=0;
        total += nums.stream().mapToInt(n -> n).sum();  //lambda
    }

严格来说,函数以及在其环境中定义的可访问变量称为闭包

阅读:47 . 字数:9223 发布于 3 个月前
Copyright 2018-2020 Siques