《JDK8 实战》笔记——1.Lambda&Stream流库

《Java8 实战》

第 1 章

为什么关心JAVA 8 ?——改变很多,改变影响很大;简洁代码-Lambda,多核并行-Stream,线程与锁;

  1. 流处理,将不相关的操作分别交给多个CPU内核并行执行;
  2. 把代码作为参数传递给方法;
  3. 并行-共享数据,对于流的并行,需要输入"纯函数",不能多个进程同时访问该共享变量,因此用"代码替换对象";
  4. 对集合的操作,更多的关注"做什么"而不是给出Iterator遍历集合元素的过程;
  5. 编程的目的在于操作值(对象),方法/类看做是次要的/辅助的,现在将方法/Lambda也看做"一等公民";
  6. 方法引用,充分利用已有的方法ckass::method,简化写法;
  7. Lambda与匿名类,配合方法引用,Lambda更加简洁;
  8. 集合类-for each "外部迭代",流-forEach() "内部迭代";
  9. 多线程的问题——变量同步;Stream的设计,包括里面的各个方法在实际的时候就考虑到,让这些操作之间无冲突/可并行;这种思想有点像是"分治"或者"归并";
  10. 关于接口,如果JDK的某些接口改进,要增加功能,这将会带来大问题——它的子类太多了,每个子类都要修改,解决——允许接口有实例方法,即default方法,与普通方法一样,能被继承/重写/重载;这样的话,增加了已发布接口的新功能而不破坏已有的实现;
  11. 接上面的问题,这样可能会导致C++父类多继承的典型问题——"菱形继承",给出测试如下,结论:编译器不让一个类继承两个具有"完全一样的"default方法的接口;可以继承继承两个具有"重载关系的"default方法的接口;因为两个父类接口的同名default方法仅返回值不同,不构成"重载关系",因此也是不允许的;
package cn.ActionInJ8;

/**
* Created by 11103381 on 2019/8/29.
*/
public class TestDefaultMethod_01 implements IA, IB {
}

interface IA {
  default void doSth(){//第1类default方法,与B完全一样
    System.out.println("IA");
  }

  default void doIt() {//第2类default方法,与B是"重载"关系—
    System.out.println("do It");
  }

  default void okay() {//第3类default方法,与B是"非重载"关系——仅仅返回值不同
    System.out.println("do It");
   }

  default void oJbKay() {//第4类default方法,与B是"重载"关系——不仅返回值不同,传参也不同
    System.out.println("do It");
  }
}

interface IB {
// default void doSth(){//上面继承A、B会编译错误
// System.out.println("IB");
// }

  default void doIt(String s) {
    System.out.println("do It " + s);
  }

// default String okay(){//上面继承A、B会编译错误
// return "do It";
// }

  default String oJbKay(String s) {
    return "do It,oJbKay";
  }
}

第 2 章

行为参数化:定义一种行为,或者叫"功能/操作",以代码块的形式;把这个代码块准备好后,丢给其他程序,这样其他程序需要调用的时候才会调,这个代码块被"推迟执行";

  • 不断变化的需求;需求变化,比如说,更小的需求变化,不推荐的做法是:新写一个方法,复制旧方法的大量代码,改其中一个小逻辑;思想:将这些"比方法还小的功能抽象出来",让代码尽可能逻辑清晰-精简代码;那不用新方法实现新需求,该怎么办?——用代码块,即让"行为参数化";

具体操作:写个接口-把这个接口对象作为参数传递到旧方法中-接口里面定义了一个抽象方法-根据不同的需求去写这个接口不同的实现类-让这些实现类的对象作为不同方法的"载体"传入旧方法去实现不同的功能;

  • 以上的做法有一点不足——真正需要的不是这个接口对象,而是它的方法实现,这个方法的使用必须依赖这个接口对象,每次使用的时候,需要先定义接口的实现类实现方法;还有一点,这种接口有一个最大的特点,只有一个特定功能,即一个抽象方法!(这个规律可以利用一下-注解)

  • 面对上面的问题,第一次改进:不去写这个接口的实现类,而是使用匿名类,这样项目工程就会少很多类;结果发现,需要写的代码并没有减少多少,还有个问题,如果在一个方法内去new匿名类的对象,当方法内部/形参、匿名类的内部、匿名类的实现方法的内部 都有一个重名的变量的时候,对这个变量的操作规则就很复杂(配合this关键字),如下面的测试例子;

  • 尝试使用Lambda代替接口对象;eg:Runnable和Comparator,Lambda替换run()和compare(T t1,T t2);

关于匿名类的有趣的测试:

package cn.ActionInJ8;

import org.junit.Test;

/**
* Created by 11103381 on 2019/8/29.
*/
public class TestAnonymousClass {
int num = 10;//成员变量

@Test
public void printnum(int num) {
    System.out.println(num);//输出方法的形参
    System.out.println(this.num);//对于成员方法,this指代当前的类的对象,因此这个this.num输出(1)定义的成员变量"Class"
}

@Test
public void doSth() {
    int value = 10;//(1)
    TestAnonymous ta = new TestAnonymous() {
        int value = 9;//(2)

        @Override
        public void doA() {
            int value = 8;//(3)
            System.out.println(value);//输出doA()内部的局部变量(3) 8;
            System.out.println(this.value);//输出doA所在的匿名类的内部定义的成员变量(2) 9;
            this.value++;//允许修改,这里value不是final;
            value++;//允许修改,这里value不是final;
        }
    };
    ta.doA();
}

// 当在一个方法A内使用匿名类new一个接口对象,并且在匿名内内部(无论是抽象方法内/外)使用该方法A的形参 的时候,这个形参必须是final(隐式),不能被修改;
@Test
public void getnum(int num) {
TestAnonymous ta = new TestAnonymous() {
    // int myInt = num++;//error,不能修改形参num;

    @Override
        public void doA() {
            System.out.println(num);
            // num++;//error,不能修改形参num;
        }
    };
}

interface TestAnonymous {
    void doA();
}

(图)

匿名类中访问外部变量为隐式的final类型

补充:这里再补充一个例子,关于lambda与匿名类的this关键字的含义

this关键字在内部类和lambda表达式中的区别

匿名类的被实现的方法的方法体内的this,指的是这个匿名类的对象;外部类对象this需要显示的指明,即OuterClass.this;

而lambda的this指的是外部类对象;

有趣的帖子:Java匿名类遇上final - 简书(图)

1. final遇见内部类

Java中要求如果方法中定义的中类如果引用方法中的局部变量,那要要求局部变量必须要用final修饰(JDK8中已经不需要,但是本质也是和final类似——只读),实例代码如下:

interface Inner{
  void method();
}

class Outer{
    public Inner createInner(){
        final int a = 12;
        final Map map = new HashMap();
        Inner inner = new Inner(){
            public void method(){
                int b = a + 1;
                System.out.println(" in Inner, b=" + b);
                map.put("innerKey", "innerValue");
            }
        };
        System.out.println("in Outer, createInner finish!");
        return inner;
    }

    public static void main(String []args){
        Inner inner = new Outer().createInner();
        inner.method();
    }
}

输出如下

in Outer, createInner finish!
in Inner, b=13

Note: 上述代码仅仅是展示使用,其中createInner()方法中的map变量是存在内存泄漏的,因为外界无法访问他,但是却会被一致持有。关于内存泄漏的问题,通过查看上述代码便后的class文件的内容即可发现。

上述文件编译后,生成了三个文件

  1. Inner.class

  2. Outer.class

  3. Outer$1.class

    打开Outer$1.class可以看到如下内容:

  class Outer$1 implements Inner {
      Outer$1(Outer this$0, int var2, Map var3) {
          this.this$0 = this$0;
          this.val$a = var2;
          this.val$map = var3;
      }

      public void method() {
          int b = this.val$a + 1;
          System.out.println(" in Inner, b=" + b);
          this.val$map.put("innerKey", "innerValue");
      }
  }

可以看到编译后的内容,Inner匿名类拥有另一个带有三个参数的构造方法,

  • Outer this$0: 也就是拥有了Outer(外部类)当前对象的一个引用,所以我们Inner的子类中,可以通过Outer.this访问外部Outer类的当前实例。
  • var2: 此处应该为Outer createInner()方法中的局部变量a
  • var3: 此处应该为Outer createInner()方法中的局部变量Map

通过上述编译后的代码,我们大概可以明白为什么匿名类可以访问其外部数据的原因,接下来我们可以讨论一下为什么要对createInner()中的局部变量amap用final进行修饰

为了简化表述,以下将Inner匿名类里面的a表述为Inner().a, 将createInner()方法中的a表示为 createInner.a.

通过编译后的代码可以看出来,Inner().acreateInner.a不是同一个对象(在内存中不是同一个), 同样的,两个map(值,存在于堆)在内存中也是不同的,但是两个map的都指向了堆上的****同一个****HashMap对象。理论上我们是可以重新设置Inner().aInner().map的值的,但是java编译器并不允许这样做, 具体原因我认为可能是如下原因:

在匿名类内部访问外面的变量看起来是一个很正常的需求,而且直观看起来应该是同一个东西。但是在方法调用结束以后局部变量会被销毁(栈里面的内容,也就是createInner.acreateInner.map如果是同一个东西的话,那么意味着jvm在方法调用结束以后还不能销毁这些局部变量,需要将这些局部变量的生命周期保持到和Inner一样长,这样让jvm的实现起可能会更为复杂(提升这些变量的生命周期)。

所以,为了实现在Inner中可以访问createInner()中的amap,同时他们看起来和createInner()中的一样(一致),并且避免JVM对对象生命周期的管理过于复杂,采用了一个中折中的办法:

  1. 将被用到的变量作为Inner的构造函数参数传入并在Inner内部设置对应的实例(private)。
  2. createInner().acreateInner().map设置为final,并且匿名类类部不可以修改对应实例属性的值,保证一致性。

通过上述的 1中,可以很自然实现在Inner中很自然的访问createInner中局部变量的值;由于Inner中使用的变量实际上和外部函数中的局部变量是不一样的,通过上述2可以保证他们一致(都不允许修改了,肯定一致), 否则开发者在内部修改值,但是却不会影响到外面的局部变量,这会让人困惑(天然看起来应该是一个东西啊,但是却不能一起变化)。

2. 闭包

此处引出了Java对闭包的支持,其实Java目前是支持了闭包的,匿名类就是一个典型的例子。将自由变量createInner.a,createInner.map)封装到Inner中,但是Java的闭包确实有条件的闭包,因为Java只实现了capture-by-value, 只是把局部变量的值copy到了匿名类中, 没有实现capture-by-reference。如果是capture-by-reference的实现方式,可能需要将局部变量提升到对象中(也就是将局部变量的生命周期延长,变为和Inner类一样长, 那么在createInner()执行完毕以后,就不会销毁 amap了)。

关于闭包的定义:Ruby之父松本行弘在《代码的未来》一书中解释的最好:闭包就是把函数以及变量包起来,使得变量的生存周期延长

3. 内存泄漏

Java中的闭包并没有真正的实现延长生命周期, 但是间接实现了createInner.map的生命周期,因为Inner.map是一个对实际的HashMap()(位于堆中)对象的引用, 所以在createInner()方法中创建,但是却不会在该方法执行以后被GC回收, 该对象的生命周期和其创建Inner实例一样长。在本例中的代码的内存泄漏就由此而生。也就是说,如果是仅仅使用方法的局部变量(如map),而不是把map赋值给Inner的一个对象作为方法的返回值,则map在方法调用完成后就会被GC回收。

第 3 章

Lambda表达式

  1. 函数式接口;第2章说过,"让这些接口的实现类的对象作为不同方法的"载体"传入",这种接口有一个最大的特点,只有一个特定功能,即一个抽象方法!现在使用注解来标记这种类型的接口,@FunctionalInterface即函数式接口(编译时会检查,抽象方法超过1个会报错);
  2. 关于基本类型的"装箱与拆箱",自动装箱会影响性能,如int型到Integer对象会增加所需的内存,并且需要额外的内存搜索来获取被"包装起来"的原始基本类型的值;在函数式接口中的泛型T,如Integer,再使用过程中带来了优化,避免了"自动装箱"来提升性能;
  3. Lambda与异常;JDK自带的函数式接口不允许抛出受检查异常(如常见的IOException),而自己定义的@FunctionalInterface可以抛出;IO异常很常见,但又不能抛出,怎么解决呢

思路1:在方法体,即Lambda的{}内部try/catch,缺点是只能自己catch这个异常,如果自己处理不了这个异常怎么办?;
思路2:优雅的做法——利用泛型,具体的代码示例如下:

    import [java.io](http://java.io/).IOException;
    import java.util.stream.Stream;

    /**
     * Created by 11103381 on 2019/8/29.
     */
    public class TestFuntionalInterface {

        private static void UseInterface(IfTestFuntionalInterface_02 a) {
            try {
                a.doA();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        @Test
        public void func_01() {
            UseInterface(() -> {//自定义的函数式接口可以抛出受检查异常
                System.out.println("A");
                throw new IOException();
            });
        }

        @Test
        public void func_02() {
            //传入Consumer
            // Stream.of(1,2,3,4,5).forEach(i->{throw new **IOException()**;});//**编译错误**,Consumer不能抛出受检查异常,如IOException;
            Stream.of(1, 2, 3, 4, 5).forEach(i -> {
                throw new RuntimeException();
            });//通过编译,可以抛出非检查异常,如RuntimeException;
        }

        //**针对上面不能抛出受检查异常的问题**,**编写一个泛型方法对异常进行包装**
        static <E extends Exception> void doThrow(Exception e) throws E {
            throw (E) e;
        }

        @Test
        public void func_03() {//
            Stream.of(1, 2, 3, 4, 5).forEach(i -> {
                doThrow(new IOException());
            });//编译成功,实现了Consumer抛出受检查异常
        }
    }

    interface IfTestFuntionalInterface_01 {
        void doA() throws RuntimeException;

        default void doB() {
        }
    }

    @FunctionalInterface
    interface IfTestFuntionalInterface_02 {
        void doA() throws IOException;
    }
  • Lambda表达式可以直接赋值给该函数式接口的引用(就像是一个对象赋值给一个引用),那么Lambda是不是一个Object对象呢?并不是!测试如下:
@Test
public void func_04() {//Lambda是不是Object对象?
  IfTestFuntionalInterface_01 IF = ()->System.out.println("A");//编译成功,Lambda能直接赋值给该函数式接口的引用;
  IF.doB();
  // Object o1 = ()->{System.out.println("A");};//编译失败Lambda不能直接赋值给Object;
  // ()->{System.out.println("A");}.doB();//编译失败,Lambda不能直接作为句柄访问接口的方法;
}
  • Lambda访问方法的局部变量;举个例子如下,
public Runnable func_05() {
  int a = 10;
  List<String> l = new ArrayList<>();
  l.add("aaa-");
  a++;
  Runnable r = () -> {
    System.out.println("Run!" + (l.add("bbb-")));//编译成功,因为引用是final,里面的内容可以改;
// System.out.println(a);//这句如果加上去,则编译错误,编译器提示Lambda使用的a为隐式的final;
  };
  a++;
  r.run();
  System.out.println(l);//
  System.out.println(a);
  return r;
}

为什么必须是final的?还有个问题,Lambda访问的是"真正的"方法局部变量a和l吗?不是!

因为当这个方法有返回值,如返回这个Runnable r时候,方法执行完,返回Runnable对象,方法的局部变量被回收(局部变量和引用在虚拟机栈上,对象本身放在堆里面,引用指向的对象也因为可达性分析没有root连接的被GC),而这个Runnable作为一个仍然存在的返回值,仍然"持有"a和l,这是矛盾的!

怎么解决?——实际上,Runnable作为一个内部类的对象,其Lambda获取的a和l是局部变量的"副本",这样就可以在方法结束后仍然有局部变量指向的对象的引用;这又出现了一个问题!当Lambda内部修改这个副本的时候,不就导致了Lambda内的a和l,与外部局部变量的a和l的值"不一致"吗?(因为一开始的目的是让"Lambda访问外部的局部变量",也就是说二者至少看上去要是"一致的")

怎么办?——让他们都是final的就可以了,都不给修改,这样就"一致"了,看上去"像是Lambda访问了外面的方法内的局部变量","好像延长了局部变量的生命周期";

如果觉得难以理解,记住即可,与匿名类类似,访问的局部变量必须是隐式的final;

  1. 方法引用/构造函数引用;——Lambda的语法糖
    利用现有的方法(JDK)自带的,减少代码的复制,进一步精简;

(1)方法引用

1. 1个参数-对象a作为toString()方法的调用者,可以省略;eg:(Interger a)->a.toString(); 转成 Integer::toString;
2. 1个参数-对象s作为println()的传入参数,可以省略;eg:(String s)->System.out.println(s); 转成System.out::println;
3. 无参数-无参数传入,调用Thread类的方法;eg:()→Thread.currentThread();转成Thread::currentThread;
4. 2个参数,a.doSth(b)的形式,可以省略,要注意顺序;eg:(str,i)→str.subString(i);转成String::subString;

注意:这里仅关注参数的个数和形式,不关注是否有返回值

(2)构造函数引用
构造函数其实是隐式的static的,把它看作是名字为new的static函数即可;需要注意构造函数的传参个数;eg:

1.无参构造器;
Supplier<Apple> s = Apple::new; Apple apple1 = s.get();//Supplier<T>/T get() 无传入,返回一个T对象;

2.一个参数;
Function<Integer, Apple> f = Apple::new; Apple apple2 = f.apply(10);//Function<T,R>/R get(T) 传入T类对象,返回一个R对象;

3.两个参数;
BiFunction<Integer,String, Apple> bf = Apple::new; Apple apple3 = bf.apply(10,"BigApple");//Function<T1,T2,R>/R get(T1,T2) 传入T1和T2两个对象,返回一个R对象;

构造函数引用的好处延迟new对象,即Lambda表达式仅仅是一种操作-产生对象的工具,什么时候产生对象取决于我们什么时候需要这个对象(调用上面的get/apply方法);eg:

static Map<String,Function<Double,Fruit_01>> fruitFactory = new HashMap<>();//key-ID,value-产生不同水果的方法;
static {
  fruitFactory.put("apple",Apple::new);
  fruitFactory.put("Orange",Orange::new);
//...
}
static Fruit_01 getOneFuit_01(String name,double weight){
  return fruitFactory.get(name.toLowerCase()).apply(weight);
}
@Test
public void func_01() {
  Fruit_01 apple = getOneFuit_01("APPLE",10);//来10斤苹果;
}

(3)嵌套使用

@Test
public void func_02() {//嵌套使用方法引用
  List<Apple> la = new ArrayList<>(Arrays.asList(new Apple(15),new Apple(10)));
**  la.sort(Comparator.comparing(Apple::getWeight));**//comparing传入一个Function,对Function的apply的返回值调用compareTo:(c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
  la.stream().forEach(System.out::print);
}

*7. 谓词复合(解释式编程,像是口述告诉程序的流程怎么走);

(1)比较器Comparator 逆序/比较链
.reversed()对比较器"取反";.thenComparing()如果前面的比较得到的两个相等的元素,则按照下一个比较器继续比较;

(2)Predicate<T>
可以使用.and(Predicate<T>)/.or(Predicate<T>)连接两个断言对象,表示与/或关系;p.negate()返回一个对断言的逻辑取"非"的Predicate对象;

(3)Consumer<T>
.andThen()方法,连接两个Consumer<T>对象,相当于顺序执行,即传入一个对象T t,对其先后执行Consumer1.accept(t);Consumer2.accept(t');

(4)Function<T, R>
.compose(Function<T> before),接另一个Function对象,将T t传给前一个Function,before.apply(t)返回的值作为当前Function的输入;.andThen(Function<T> after)刚好与其相反;

第 4 章

引入流

为什么引入JAVA8流?

  1. 直抒型编程,不关系怎么遍历集合、是否取出一些元素、是否需要中间容器...只关心一套流程下来每一步需要做什么;
  2. 流的操作是"惰性"的,取值的时候才会执行;流不改变原来集合,减少"无效的中间变量"的使用
  3. 充分利用CPU多核并行能力(并行代码很难写,Stream的操作方法由JDK帮实现,不用担心处理数据任务并行而去操心线程与加锁);

1.流相关的概念
元素;源(集合);单个操作(filter、map...);流水线(多个操作的组合);

2.流与集合

  • 流并不储存元素。这些元素可能存储在底层的集合中,或这是按需生成。
  • 流的操作不会修改其数据源。例如,filter方法不会从新的流中移除元素,而是会生成一个新的流,其中不包含被过滤掉的元素。
  • 流的操作是尽可能惰性执行的。这意味着直至需要其结果时,操作才会执行。例如如果我们只想查找前5个长单词(长度大于x的String)而不是所有长单词,那么filter方法就会在匹配到第5个单词后停止过滤。
  • Stream不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的Iterator。原始版本的Iterator,用户只能显式地一个一个遍历元素并对其执行某些操作;高级版本的Stream,用户只要给出需要对其包含的元素执行什么操作,比如“过滤掉长度大于10的字符串”、“获取每个字符串的首字母”等,Stream会隐式地在内部进行遍历,做出相应的数据转换。Stream就如同一个迭代器(Iterator),单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返
  • 和迭代器又不同的是,Stream可以并行化操作,迭代器只能命令式地、串行化操作。顾名思义,当使用串行方式去遍历时,每个item读完后再读下一个item。而使用并行去遍历时,数据会被分成多个段,其中每一个都在不同的线程中处理,然后将结果一起输出。Stream的并行操作依赖于Java7中引入的Fork/Join框架来拆分任务和加速处理过程。
  • Stream的另外一大特点是,数据源本身可以是无限的。获取一个数据源(source)→数据转换→执行操作获取想要的结果,每次转换原有Stream对象不改变,返回一个新的Stream对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道。

3.关于流只能遍历一次

public class TestStream_01 {
@Test
public void func_01(){
  Stream s = Stream.of(8,3,9,7,7,1,9,9,5);
  s.forEach(System.out::println);
  s.forEach(System.out::println);//java.lang.IllegalStateException: stream has already been operated upon or closed
}
}

控制台输出:
java.lang.IllegalStateException: stream has already been operated upon or closed...

4.外部迭代/内部迭代

有趣的小例子:A让B去把地上的所有玩具放到箱子里去:

(1)集合-显示的外部迭代
A:地上有玩具吗?B:有,看到一个球;
A:把球放到箱子里。还有吗?B:还有个娃娃;
A:把娃娃放到箱子里。还有吗?B:还有...

(2)Stream-隐式的内部迭代
A:把地上所有玩具放到箱子里。B:好的!

好处
1. B可以一手拿球,一手拿娃娃,一起放到箱子里;(并行
2. B可以选择先拿那个距离箱子最近的娃娃;(顺序优化Stream库的内部迭代可以自动选择一种适合硬件的数据表示和并行实现,无需我们关心这些复杂的功能怎么实现;

5.流操作

  • 中间操作:对流处理(内部迭代),然后生成个新的流作为返回值,进行接下来的流程;filter、sorted、map、limit、skip...;
  • 终端操作:生成结果,"生产线"停止;forEach、Match、collect、Reduce...;

第 5 章

*使用流

  1. 筛选和切片
  • filter(Predicate<T> p) 接收一个Predicate,即对流元素遍历作为Predicate的参数-返回一个boolean,为true则将元素放入一个新的流,遍历完所有流元素,组后将生成的新流返回;
  • distinct() 无参函数,相当于对每一个流元素判断是否存在于新流中,不存在则加入,存在则跳过,最后将这个不含重复元素的新流返回,有点像遍历集合元素放入Set后,返回这个Set;
  • limit(Long n) 取流中的前n个元素,放入新流中后返回新流;
  • skip(Long n) 跳过n个元素,把剩下的元素按序放入新流中后返回新流;

1. 映射

  • map(Function<T,R> f) 对流中的每个元素做映射,即T类型的流元素经过Function得到一个R类型的结果,将这个结果放入新的流,返回新流;
  • flatMap(Function<T,R> f) 对流中的每个元素做映射,仅仅针对每个映射后得到的结果是Stream的情况(例如之前元素是数组,转成流后,现在新流的每个流元素就变成Stream对象了),会自动地将新流的所有的Stream元素"合并"到另一个新流后返回;一般配合数组的Arrays.stream()方法*,示例如下:
    @Test
    public void func_02() {
        Stream<String> s = Stream.of("abc", "def", "gh");
        // s.map(str->str.split("")).forEach((ss)-> System.out.println(Arrays.toString(ss)));//流的每个元素变成String[],打印3个数组[a, b, c],[d, e, f],[g, h]
        // s.map(str->str.split("")).map(Arrays::stream).forEach(st->st.forEach(System.out::println));//每个String[]类型的元素变成了流,即现在的元素是3个Stream<String>
        s.map(str -> str.split("")).flatMap(Arrays::stream).forEach(System.out::println);//**基于上面一步,在将每个数组元素转成流后,将每个流类型的元素"合并"到一个新流里面,有点像addAll那种感觉**;
        System.out.println("-----example-----");
        Stream<Stream<String>> streamOfStream = Stream.of(Stream.of("a", "b"), Stream.of("c"), Stream.of("d", "e"));//Stream的元素是Stream
        // streamOfStream.map((Eachstream) -> (Eachstream)).forEach(System.out::println);//此时元素还是Stream<String>
        streamOfStream.flatMap( **Function.<Stream<String>>identity() **).
        forEach(System.out::println);//此时元素变成了**String,Function.identity()等效于**上面一句,即**(a)->(a)不做任何操作**返回;
    }

2. 查找和匹配

匹配都是"终端操作"

  • anyMatch(Predicate<T> t) 对流每个元素做匹配,只要有一个满足判断,则返回true;
  • allMatch(Predicate<T> t) 对流每个元素做匹配,要求所有元素都满足判断,才返回true;
  • noneMatch(Predicate<T> t) 对流每个元素做匹配,要求所有元素都不满足判断,才返回true;

查找

  • findAny(Predicate<T> t) 对流每个元素做匹配,找到一个满足判断的元素,返回这个元素;
  • findFirst(Predicate<T> t)对流每个元素做匹配,找到"逻辑顺序下"的第一个满足判断的元素,返回这个元素;例如List转化成的流;

注意:流的遍历在后台优化,保证最优查找(例如并行查找),因此返回的元素的顺序不确定;其次,返回值为Optional类,它替代了所有的返回类型的对象,用来做非空判断和防止空指针;

3. 规约

是一个"终端操作"

  • reduce(T initValue,BiFunction<T,T,T> bf)从流的第一个元素开始,与指定的initValue做运算,返回一个结果,将这个结果作为下一次运算的初值,依次遍历整个流,最终所有的元素归化了一个T类型的结果;

应用:对流元素求和、求最大(小)值;eg:

Optional<Integer> sum = number.stream().reduce(0,(a,b)->(a+b));
Optional<Integer> max = number.stream().reduce(Integer.MIN_VALUE,Math::min);

思考:如果要求实现并行求和,怎么操作?对共享变量sun加锁,对求和内容分块,将每一块的运行结果相加;用流库只需要把stream()改成parallelStream()即可
注意:并行流求和需要满足两个条件——传给reduce的Lambda不能改变状态(如实例变量);操作满足结合律,即与顺序先后无关a+b=b+a;

*流操作:无状态和有状态

  • 无状态:filter、map,对流的每一个元素分别操作,不依赖其他元素的操作结果;
  • 有状态:reduce-sum/max,依赖前面已有的累积的计算结果(状态累积),而且这种状态是有界的;诸如sorted、distinct,需要直到先前的历史,例如排序需要将所有元素放入缓冲区后才能给输出流假如一个项目,这一操作的存储要求是无界的,当流很大(如无限流),就可能会出现问题,如质数流的反转,因为流是无限的,因此无法返回最大的质数;

数值流

int sum = myList.stream().reduce(Integer::sum);这一句存在问题:由于使用Integer::sum方法,存在一个暗含的"装箱成本",每个Integer元素都会自动拆箱成int再求和计算

怎么解决?IntStream数值流——专门用来处理数值;包含sum、max等实用方法;

int sum = myList.stream().mapToInt(Apple::getNum).sum();//其他常用的还有mapToDouble()和mapToLong();

mapToInt()区别于map()的地方在于applyAsInt(T t),返回值不是普通的stream而是IntStream;

构建流

(1)由值创建,Stream<String> s = Stream.of("A","b");使用Stream.of()方法;
(2)数组转成流,Fruit_01[] f1 = new Fruit_01[]{};Stream<Fruit_01> sf = Arrays.stream(f1); 注:int[]数组会直接转成IntStream;
(3)由文件生成流,eg:读取文件中的所有不重复的单词的总数量

    @Test
    public void func_03() {
        String filePath = "file//a.txt";
        try (Stream<String> lines = Files.lines(Paths.get(filePath), Charset.defaultCharset())) {
            long uniqueWords = lines. flatMap(line -> Arrays.stream(line.split(""))).distinct().count();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

(4)创建无限流,通过:迭代iterate(T initvalue,Function<T,T> t)、生成generate(Supplier<T> s),因为流是无限长的,一般配合limit()使用;例如:

    @Test
    public void func_02() {
        Stream.iterate(1, n -> 2 * n).limit(100).forEach(System.out::println);//2的幂
        Stream.iterate(new int[]{0, 1}, t -> new int[]{t[0], t[0] + t[1]}).map(n -> n[0]).limit(100).forEach(System.out::println);//斐波那契数列,每个元素为2个int构成的数组,map(n→n[0])打印第一个;
        Stream.generate(Math::random).limit(10).forEach(System.out::println);//打印10个[0,1]之间的随机数;
    }

:创建流的generate()内部的Lambda(Supplier)应该是"无状态"的,这样在并行流中才是安全的

第 6 章

用流收集数据-collect(),前面的所有中间操作得到了一个结果流,要对这个流的所有元素做某种处理,来展示我们需要的直观信息(结果可能需要一个值、一个字符串、一个List或者一个Map);

  1. 数字类型的汇总
    Collectors.summarize方法,得到对数字的汇总(数量、最大/小值、平均、求和),eg:
@Test
public void func_02(){
//reduce实现-求最值、求和
  int res_01 = Stream.of(20, 19, 0, 8, 30).reduce(Integer.MAX_VALUE, Math::min);
  int res_02 = Stream.of(20, 19, 0, 8, 30).reduce(Integer.MIN_VALUE, Math::max);
  int res_03 = Stream.of(20, 19, 0, 8, 30).reduce(0, (n1, n2) -> n1 + n2);
  System.out.println(res_01+" "+res_02+" "+res_03);
//collector-**summarize**()方法
  IntSummaryStatistics summary = Stream.of(20, 19, 0, 8, 30).collect(Collectors.summarizingInt((n)->(n)));
  System.out.println(summary.getMin()+" "+summary.getMax()+" "+summary.getSum());
}

2. 得到字符串
Collectors.joining方法合并Stream<String>中的元素,eg:

@Test
public void func_01(){
  Stream<String> s = Stream.of("apple", "orange", "pineapple");
  String res = s.collect(**Collectors.joining**(", "));
  System.out.println(res);
}

3. 分组
分组的目的是对之前处理得到的一个结果流,做一个"分类",按照某种"分类规则"(如流元素的一个属性name),对流分类,放入一个map,map的key为属性,value为一个List流元素的集合;

@Test
public void func_03() {//Collectors.groupingBy的用法
  List<Dish_tc> list = new ArrayList<>();
  list.add(new Dish_tc(3, "S"));
  list.add(new Dish_tc(1, "A"));
  list.add(new Dish_tc(2, "D"));
  list.add(new Dish_tc(3, "X"));
  list.add(new Dish_tc(2, "Y"));
  list.add(new Dish_tc(1, "Z"));

//**(1)按照value属性分组**
  System.out.println(list.stream().collect(Collectors.groupingBy(Dish_tc::getValue)));

//**(2)对value进一步划分(value属性太细了,现在要求只分2类)**
  System.out.println(list.stream().collect(Collectors.groupingBy(
    d -> {
    if (d.getValue() > 2) return ">2";
    else return "<=2";
    }
  )));

//**(3)多级分组,对map的value的List集合进一步分组**
  Map<Integer, Map<String, List<Dish_tc>>> mmp = list.stream().collect(Collectors.groupingBy(Dish_tc::getValue, Collectors.groupingBy(Dish_tc::getName)));
  System.out.println(mmp);

//**(4)不需要组内(每个List)的具体元素信息,只需要统计个数;**
  System.out.println(list.stream().collect(Collectors.groupingBy(Dish_tc::getValue, Collectors.counting())));
}

class Dish_tc {
  public int getValue() {
    return value;
  }
  int value;

  public String getName() {
    return name;
  }
  String name;

  Dish_tc(int value, String name) {
    this.value = value;
    this.name = name;
  }

  boolean isDelicoous() {return name.contains("D") || name.contains("Z");}//"是否好吃"

  public String toString() {
    return this.value + "-" + this.name + ";";
  }
}

4. 分区 Collectors.patitioningBy(Predicate p)

分区和分组很像,类似上面的(2)的代码,对Map有个特定限制——就是map的key类型为boolean,也就是说分成了两类;

    @Test
    public void func_03() {
        Map<boolean, List<Dish_tc> mp = list.stream().collect(Collectors.patitioningBy(Dish_tc::isDelicious)); //根据"是否好吃"分成两类

        //针对上面的(2)的自定义分组,现在仅仅分两组true/false,即Map的key为boolean,使用partitioning()方法,该方法可以嵌套
        System.out.println(list.stream().collect(Collectors.partitioningBy(Dish_tc::isDelicoous))); //根据"是否好吃"分成true/false两类
        System.out.println(list.stream().collect(Collectors.partitioningBy(Dish_tc::isDelicoous, Collectors.partitioningBy((d) -> d.getValue() > 2)))); //可嵌套
        System.out.println(list.stream().collect(Collectors.partitioningBy(Dish_tc::isDelicoous, Collectors.groupingBy(Dish_tc::getName)))); //与groupingBy可嵌套
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值