《JDK8实战》笔记——0.引言-行为参数化&函数式接口

GitHub源码: https://github.com/alexandregama/java8-guides-tutorials

GitHub的JDK8 Demo主要包含两点:

  1. Lambda表达式的使用(几个JDK函数式接口类);
  2. Stream()的使用;

函数式接口?

    @FunctionalInterface
    public interface RealFunctionalInterface {
        
        // 唯一的抽象方法
        void doSomething();

        // 可以存在default修饰的普通方法
        default void doAnotherThing() {
            System.out.println("default-method");
        }

        // 可以有static方法
        static void doB() { 
            System.out.println("static-method");
        }
        
    }

说明:使用@FunctionalInterface修饰的接口,仅仅包含一个抽象方法,但是可以有其他的static方法和default方法(可以有方法体);

default方法是JDK8新加入的,让一个接口interface除了static方法,可以有普通的、具有方法体的方法,这个方法可以被继承,因此也可以被重写;

这里测试一下default的继承,顺便再复习一下Java父子之间的继承关系,eg:

class RealFunctionalInterface_class implements TestFunc.RealFunctionalInterface {

    @Override
    public void doSomething() {
        System.out.println("doSomething-class");
    }

    @Override
    public void doAnotherThing() {
        System.out.println("default-method-class");
    }

    // 可"继承-重写-重载"父类接口的default方法
    public void doAnotherThing(String s) {
        System.out.println("default-method-class");
    }

    // 
    static void doB() {
        System.out.println("static-method-class");
    }
    
}

测试:

    @Test
    public void TestSupperClassAndFunctionalInterf() {
        RealFunctionalInterface r1 = new RealFunctionalInterface_class();
        r1.doSomething();
        r1.doAnotherThing();
        // r1.doB();// error,父类引用指向子类对象,无法用该引用调用父类static方法(父类的static方法丢失)

        // r1.doAnotherThing("SSS");// error,父类引用指向子类对象,该引用无法调用子类有但是父类没有的方法,称为"方法丢失"
        RealFunctionalInterface_class r2 = new RealFunctionalInterface_class();
        r2.doAnotherThing("SSS");// √

        RealFunctionalInterface.doB(); //static方法"与类绑定",不存在"继承static方法",子类也可以写一个static方法doB(),但这不是继承/重写;
        RealFunctionalInterface_class.doB();
    }

Lambda表达式?

先从接口的使用开始说起,一般的使用情景:classA继承接口IA-实现其抽象方法doA()-使用方法doB(IA a),将classA的对象传入(实现多态);

这么做的目的是什么呢?

一般来说,我们真正需要的不是这个IA/classA类型的a对象而是classA中doA()方法的具体实现,因此我们关注的不是"对象"而是"方法(函数)",那么能不能将"方法(函数)"传入呢?这么做的好处——不用为了传入一个具体的方法实现而去定义classA,类似的思想有匿名类的使用(eg: new IA(){ @override doA(){...} }),但这种方式还是有很长一串代码,而我们关注的仅仅是doA()方法的内容(称之为"函数"),那么为什么不能将这个"函数"直接传入来取代接口的实例呢?——可以,使用Lambda表达式!

eg:经典的两个接口Runnable和Comparator,分别有run()和compare()方法需要实现,run()方法无返回值-无传入参数,compare()返回int-传入两个String;Lambda表达式写法:

    @Override
    public void run() {
        System.out.println("OK");
    }

其Lambda表达式写法: ()->System.out.println("OK");

    @Override
    public int compare(String s1, String s2) {
        return s1.compareTo(s2);
    }

其Lambda表达式—— (s1,s2)->s1.compareTo(s2); // return也可以省略,s1/s2类型可推出来,也可省略;

还有一种情况,Lambda表达式虽然作为方法参数代替了接口对象,但它毕竟不是真正的对象,无法作为句柄(引用)显示的访问接口/类的(default)方法,但是可以把Lambda表达式"赋值"给接口对象;eg:

        Comparator<Student> comparator = (s1, s2) -> s1.getName().compareTo(s2.getName());
        int result = comparator.compare(mockito, java);

几种JDK自带的常用函数式接口@FunctionalInterface?

这么理解,将函数式接口看做一种"操作"——包括:
(1)是否传入参数/传入几个参数;
(2)操作后,有没有返回结果/返回什么类型的结果;

1.Predicate<T>——传入一个T类对象,返回一个boolean结果;

public boolean test(T t);

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

2.Consumer<T>——传入一个T类对象,不返回任何值,相当于把"这个对象给消费了";

public void accept(T t)

有一个.andThen()方法,连接两个Consumer<T>对象;

3.Supplier<T>——不传入任何参数,返回一个T类型的对象,相当于一个"提供者";

public T get()

只有一个get()方法;

4.Function<T, R>——传入一个T类型参数,返回一个R类型对象,相当于"映射/处理"操作,将T对象处理转换成R对象;

public R apply(T t)

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

其他的还有二元运算...与上面类似;

Stream()流库?

针对集合类(List、Set、Map),更为高效的操作,可以利用CPU的多核性能parallelStream(),这种操作是"惰性"的,即只有需要结果的时候才会去计算,并且不会修改原有的集合类;

Stream操作像一个"生产线"一样,可以组合多个"中间操作",最后一步输出时需要"终端操作",会关闭流;

常用操作分类:

(1) "中间操作,返回流"——过滤filter(Predicate)、排序sorted(Comparator<T>)、映射map(Function<T,R>)、返回指定数量n的元素流limit(long n)、元素去重distinct()、丢弃流的前n个元素skip(long n);

(2) "终止操作,中断流,返回某个值"——匹配Match/noneMatch(Predicate)返回boolean、计数counting()返回流中元素个数、T reduce(T identity, BinaryOperator<T> accumulator)规约,制定初始一个T类对象,将第一个流元素与其操作得到一个T对象,依次遍历整个流,最后只得到一个T对象、R collect(Collector collector)将整个流的所有元素收集起来,放到一个Collector集合里面,如Collector.toList();*也可以接Collectors.groupingBy(Function classifier, Collector downstream)先分组,然后将每组的元素放入容器;

(3) JDK8 之后,对于有返回值的方法,不要再使用nut null判断了,应该使用Optional<T>,它相当于一个容器用来方返回值的,如reduce()的结果,他有get()取值/isPresent()判断非空,有值则consumer操作/orElse()为空则返回其他值,是一个防止NullPointException的漂亮工具;

《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();
    }

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

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

    @Test
    public void getnum() {//当 在一个方法A内使用匿名类new一个接口对象,并且在匿名内内部(无论是抽象方法内/外)使用该方法A内部定义的局部变量的时候,这个形参必须是final(隐式),不能被修改;
        int num = 10;
        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关键字的含义

Lambda表达式/内部类的this关键字

匿名类的被实现的方法的方法体内的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文件的内容即可发现。

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

  • Inner.class

  • Outer.class

  • 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) 优雅的做法——利用泛型,具体的代码示例如下:

package cn.ActionInJ8;

import org.junit.Test;

import 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 -> { // 通过编译,可以抛出非检查异常,如RuntimeException;
            throw new 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访问方法的局部变量;举个例子如下,

    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;

方法引用/构造函数引用;——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);
    }

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

(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)刚好与其相反;

其他的还有二元运算...与上面类似;

Stream()流库?

针对集合类(List、Set、Map),更为高效的操作,可以利用CPU的多核性能parallelStream(),这种操作是"惰性"的,即只有需要结果的时候才会去计算,并且不会修改原有的集合类;

Stream操作像一个"生产线"一样,可以组合多个"中间操作",最后一步输出时需要"终端操作",会关闭流;

常用操作分类:

        (1)  "中间操作,返回流"——过滤filter(Predicate)、排序sorted(Comparator<T>)、映射map(Function<T,R>)、返回指定数量n的元素流limit(long n)、元素去重distinct()、丢弃流的前n个元素skip(long n);

        (2)  "终止操作,中断流,返回某个值"——匹配Match/noneMatch(Predicate)返回boolean、计数counting()返回流中元素个数、T reduce(T identity, BinaryOperator<T> accumulator)规约,制定初始一个T类对象,将第一个流元素与其操作得到一个T对象,依次遍历整个流,最后只得到一个T对象、R collect(Collector collector)将整个流的所有元素收集起来,放到一个Collector集合里面,如Collector.toList();*也可以接Collectors.groupingBy(Function classifier, Collector downstream)先分组,然后将每组的元素放入容器;

        (3)  JDK8 之后,对于有返回值的方法,不要再使用nut null判断了,应该使用Optional<T>,它相当于一个容器用来方返回值的,如reduce()的结果,他有get()取值/isPresent()判断非空,有值则consumer操作/orElse()为空则返回其他值,是一个防止NullPointException的漂亮工具;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值