• Java 8-Lambda表达式、方法引用、标准函数接口与流操作、管道操作之间的关系


    1.Lambda表达式与接口之间的关系

    只要Lambda表达式的声明形式与接口相一致,在很多情况下都可以替换接口。见如下代码

    Thread t1 = new Thread(new Runnable() {
        public void run() {
            System.out.println("hi");
        }
    });
    t1.start();
    
    Thread t2 = new Thread(() -> System.out.println("hi"));
    t2.start();
    

    t1与t2完成相同的功能。t2中的Lambda表达式() -> System.out.println("hi")Runnable接口中的方法public abstract void run();的形式一样:

    1. 没有返回值。
    2. 没有传入参数。

    下面一个例子中

    String[] arr = {"111","22","3"};
    Arrays.sort(arr,new Comparator<String>() {
        @Override
        public int compare(String o1, String o2) {
            return o1.length()-o2.length();
        }
    });
    
    Arrays.sort(arr, (o1,o2)->o1.length()-o2.length());
    

    (o1,o2)->o1.length()-o2.length()的形式与Compartor中public int compare(String o1, String o2) 形式也一样:

    1. 两个入参。
    2. 一个返回值,且返回值为int类型。

    2.Lambda表达式、匿名内部类与this

    Lambda表达式在用法上看起来很像匿名内部类,但其并不是匿名内部类。比如,在以下代码中,在Lambda表达式中不能获得this。

    Thread t1 = new Thread(new Runnable() {
        public void run() {
             System.out.println(this);//打印匿名内部类
        }
    });
    t1.start();
    
    Thread t2 = new Thread(()->{
        //System.out.println(this);//无法编译通过
    });
    t2.start();
    

    观察如下代码,会发现Lambda表达式中的this与所处环境有关,在这里this是对外部对象的引用。

    class Foo{
        Runnable r1 =  ()->{
           System.out.println(this);
        };
        Runnable r2 =  ()->{
            System.out.println(this);
         };
        
        void test(){
            r1.run();
            r2.run();
        }
    }
    //测试代码如下
    Foo foo = new Foo();
    System.out.println(foo);
    foo.test();
    

    从输出可以看出,输出了三个对象实际上是同一个对象。

    Foo@87aac27
    Foo@87aac27
    Foo@87aac27
    

    3. 标准函数式接口与方法引用

    3.1 函数式接口

    Java8中为Iterable引入了默认实现方法default void forEach(Consumer<? super T> action)。用法如下:

    List<String> strs = Arrays.asList("1","222","33");  //List接口间接继承了Iterable接口,所以strs也会有forEach方法。
    strs.forEach(e->System.out.println(e)); //将strs中的每个元素迭代输出
    

    为什么可以将Lambda表达式e->System.out.println(e)作为Consumer<? super T> action类型的参数。
    先看一下Consumer的代码

    @FunctionalInterface
    public interface Consumer<T> {
         void accept(T t);
         //其他代码
    }
    

    可以看到void accept(T t)e->System.out.println(e)形式上是一致的,所以可以将该Lambda表达式作为输入参数。
    注意:这里使用了@FunctionalInterface标注该结构为函数式接口。也可以自己创建函数式接口。但要注意函数接口只能有一个抽象方法
    如下代码可以通过:

    @FunctionalInterface
    interface MyFuncInterface{
    	void test();
    }
    

    但如下代码却无法编译通过

    @FunctionalInterface
    interface MyFuncInterface{
    	void test();
    	void test1();
    }
    
    

    JDK中大量使用了几个常用的标准函数接口。如下所示:

    public interface Consumer<T> {//无返回值,消费传入的T。可接受e->System.out.println(e)或System.out::println
        void accept(T t);
        //其他代码
    }
    public interface Function<T, R> {//将t转化为r。可接受e->Integer.parseInt或Integer::parseInt,将String类型转化为int
        R apply(T t);
        //其他代码
    }
    public interface Predicate<T> {//根据传入t判断真假。可接受x->x>3或String::equals(与传入String对象比较,返回True或False)
        boolean test(T t);
        //其他代码
    }
    public interface Supplier<T> {//无输入参数,直接获取T。可接受()->Arrays.asList("1","2","3"}或
        T get();
    }
    

    3.2 方法引用

    前面出现的System.out::println就是方法引用。下面的代码中,strs.forEach的入参类型为Consumer<? super T> action
    前面已经提到可以使用e->System.out.println(e)作为入参,同时我们知道System.out.println方法签名中返回值为void、
    无入参也符合要求,所以我们可以使用System.out::println来替代e->System.out.println(e)。注意:要使用::来引用相关
    的方法。
    ···
    List strs = Arrays.asList("1","222","33");
    strs.forEach(e->System.out.println(e));
    strs.forEach(System.out::println);
    ···
    方法引用不仅可以引用jdk中已有类的方法,还可以引用自定义类的相关方法。比如:

    class Foo{
    	<T> void myPrintX(T t) { //必须创建Foo对象才能对非static进行方法引用
    		System.out.println("x="+t);
    	}
    	
    	static <T> void myPrint(T t) {
    		System.out.println("element="+t);
    	}
    }
    
    
    //测试代码
    List<String> strs = Arrays.asList("1","222","33");
    strs.forEach(Foo::myPrint);
    strs.forEach(new Foo()::myPrintX);
    

    输出结果为

    element=1
    element=222
    element=33
    x=1
    x=222
    x=33
    

    4.Lambda、方法引用、标准函数接口与Stream

    从Java 8起,可以将集合中数据放入流并进行管道式操作。
    管道式操作包含3部分:

    1. 数据源(集合、数组等)
    2. 0个或多个中间操作(filter、map等)
    3. 终端操作(forEach、collect、average, sum, min, max, and count)。

    中间操作产生的还是流,那么通过filter得到的流还可以继续进行filter。
    终端操作产生的就不是流了(可能是一个List、Map或int等),对一个流进行终端操作后,就不能在进行任何其他中间操作。
    对一个流一旦进行完终端操作,就不能再进行中间操作,运行如下代码

    List<Integer> intList = Arrays.asList(1, 2, 3, 4, 5);
    Stream<Integer> stream1 = intList.stream().filter(e->e>3);
    stream1.forEach(System.out::println);
    Stream<Integer> stream2 = stream1.filter(e->e>4);
    stream2.forEach(System.out::println);
    

    会提示stream has already been operated upon or closed

    4.1 stream的的filter、map方法与Predicate、Function接口

    Predicate接口(boolean test(T t))的作用是根据传入参数t判断真假。
    Function接口(R apply(T t);)的作用是将T类型的t转换成R类型。
    观察如下代码:

    List<String> strs = Arrays.asList("1","222", null, "33");
    Stream<Integer> stream = strs.stream().filter(e -> e != null).map(Integer::parseInt);
    stream.forEach(e -> System.out.println(1 + e));
    

    输出

    2
    223
    34
    
    

    其中strs.stream().filter(e->e!=null)的filter方法声明如下Stream<T> filter(Predicate<? super T> predicate);,即这里需要一个
    Predicate<? super T> predicate类型的参数。前面可以看到Predicate接口中的方法为boolean test(T t);,即接受一个t返回
    boolean值。e->e!=null符合这样的要求。
    strs.stream().filter(e->e!=null).map(Integer::parseInt);中的map方法声明如下
    Stream<R> map(Function<? super T, ? extends R> mapper)
    即这里需要一个Function<? super T, ? extends R> mapper)类型的参数。前面可以看到Function接口中的方法为R apply(T t);
    即接受一个类型为T的元素,将其转换为元素R。在这里实际上就是将String类型元素转化成int类型元素。Integer::parseInt刚好
    符合这种要求。

    4.2 Optional中map方法与Function接口

    从刚才的例子中,我们可以看Function接口的作用可以将一个类型的转换成另外一个类型。比如

    Student s1 = new Student("zhang san");
    String name = s1.getName(); //对应的方法引用是Student::getName()
    

    中Student::getName()相当于Student类型转换成String类型。
    如下代码中,一个Course有很多Student(stuList),每个Student有都可以getName()。现在想要获取该Course中某个学生的姓名。
    以往的代码如果使用course.getStuList().get(i).getName()来获取某个学生的姓名,看起来代码风格固然流畅,然而却没有正确处理:
    course1为null,get(i)为null,getName为null的情况。那么必须在整个处理过程编写大量的判断null的代码。
    可以使用Optional进行改进,即保持了流畅的编码风格,又可以正确处理null。
    以下代码中:Optional.ofNullable方法可以将给定值转化为Optional类型(可包含代表给定值的Optional对象,也可包含代表null的Optional对象)

    import java.util.ArrayList;
    import java.util.List;
    import java.util.Optional;
    class Student{
    	private String name;
    
    	public String getName() {
    		return name;
    	}
    
    	public void setName(String name) {
    		this.name = name;
    	}
    
    	public Student(String name) {
    		this.name = name;
    	}
    	
    }
    class Course{//课程
    	private String name;
    	private List<Student> stuList;
    	
    	public String getName() {
    		return name;
    	}
    	public void setName(String name) {
    		this.name = name;
    	}
    	public void addStudent(Student... stus) {
    		for (Student student : stus) {
    			stuList.add(student);
    		}	
    	}
    	public Student getStu(int i) {
    		return stuList.get(i);
    	}
    	public List<Student> getStuList() {
    		return stuList;
    	}
    	
    	public Course(String name) {
    		this.name = name;
    		stuList = new ArrayList<>();
    	}
    	
    }
    
    public class TestOptional {
    	public static void main(String[] args) {
    		Course course = new Course("数学");
    		Student s0 = new Student("s1");
    		Student s1 = new Student(null);
    		Student s2 = null;
    		course.addStudent(s0, s1, s2);
    		String result = findStudent(course, 0);// orElse,当处理过程中过程中有一个null时,即返回orElse中的值
    		System.out.println("均不为空情况下姓名为:" + result);
    		result = findStudent(course, 1);
    		System.out.println("student的name为null的情况:" + result);
    		result = findStudent(course, 2);
    		System.out.println("student为null的情况:" + result);
    		Course courseNull = null;
    		result = findStudent(courseNull, 3);
    		System.out.println("course为null的情况:" + result);
    	}
    
    	private static String findStudent(Course course, int i) {
    		Optional<Course> courseNullable = Optional.ofNullable(course);
    		String result = courseNullable.map(e -> e.getStu(i)).map(Student::getName).orElse("查询不到");
    		return result;
    	}
    }
    
    

    注意:

    1. Optional的map方法入参为Function类型,所以map(e->e.getStu(0))map(Student::getName)形式均可执行。
    2. Optional的map方法返回值为Optional类型,所以可以以链式风格map(e->e.getStu(2)).map(Student::getName)流畅的编写对应代码。
    3. 该例子中不考虑stuList为null的情况,因为只要创建了Course,默认就创建了stuList。
    4. 这里没有对不同种的null情况(student为null,course为null)进行处理,返回的结果统一是查询不到,会造成理解上的混淆。

    4.3 stream的mapToInt方法与ToIntFunction函数式接口

    List<String> strs = Arrays.asList("1", "222", null, "33");
    IntStream intStream = strs1.stream().filter(e -> e != null).mapToInt(e -> e.length());
    intStream.forEach(System.out::println);
    

    mapToInt(e -> e.length())的mapToInt方法参数类型为ToIntFunction<? super T> mapper,查询源代码ToIntFunction包含方法
    int applyAsInt(T value);,即需要一个方法接受T类型输入参数,然后将其转化为int。在这里,e -> e.length()起到了这个作用。
    代码的作用就是要将求得流中每个非null的字符串的长度,然后放入intStream中。

    4.4 stream的reduce方法与BinaryOperator函数式接口

    int[] arr = {1,2,3,4,5};
    int x = 0;
    for (int i = 0; i < arr.length; i++) {
        x = x + arr[i];
    }
    System.out.println(x);
    

    这段代码每回从数组中取出一个元素,然后与前一个元素相加。最后求的所有元素值的和。这类操作经常使用,可以使用stream中的
    reduce方法来简化实现。

    List<Integer> intList = Arrays.asList(1, 2, 3, 4, 5);
    Stream<Integer> intStream1 = intList.stream();
    Optional<Integer> result = intStream1.reduce((a, b) -> a + b);
    Integer integer = result.get();
    System.out.println(integer);// 15
    

    intStream1的reduce方法入参为BinaryOperator其继承自接口BiFunction,内有一方法R apply(T t, U u);,将传入参t与u进行运算,
    然后返回一个结果。(a,b)->a+b就满足如此行事,其中a最开始为流中第1个元素,b为第2个元素,a+b以后再赋给a,然后b为第3个元
    素,依次类推。
    reduce方法还有另外一种形式,可以指定初始值,如下述代码指定迭代开始的初始值为10,即a开始为10,b为流中第1个元素 。然后
    将a+b放入a,b为流中第2个元素,然后依次类推。

    Integer x = intList.stream().reduce(10, (a, b) -> a + b);
    System.out.println(x);
    

    输出

    25
    

    其中的reduce(10, (a, b) -> a + b)类型为T reduce(T identity, BinaryOperator<T> accumulator);,可以看到该方法的返回值由identity的类型决定,
    在这里由10来决定,即返回值类型应为Integer。

    4.4 使用Supplier接口生成流

    Supplier接口(提供者)的定义如下

    @FunctionalInterface
    public interface Supplier<T> {
        T get();
    }
    
    

    可以看到,其通过get方法返回一个对象。我们可以将Supplier当作一个生成某个对象的工厂。
    为了对流进行一些管道操作的实验,且因为流不能反复操作,我们需要不断生成内部元素完全相同的流。
    如下代码中,通过Supplier<Stream<Integer>> factory = () -> Stream.of(1, 2, 3, 4, 5);声明Supplier类型变量
    factory,通过该factory的get方法就可以不断生成流,实际上就是不断调用() -> Stream.of(1, 2, 3, 4, 5);。而
    这段() -> Stream.of(1, 2, 3, 4, 5);Lambda表达式形式上是与Supplier标准函数式接口是一致:无入参,有一个返回值。

    Supplier<Stream<Integer>> factory = () -> Stream.of(1, 2, 3, 4, 5);
    Stream<Integer> stream1 = factory.get();
    Stream<Integer> stream2 = factory.get();
    System.out.println(stream1 == stream2); // false
    

    4.5 流的扁平化-flatmap

    如何抽取二维数组Integer[][] arr1 = {{1,2},{2,3}}每个元素(排除掉重复的元素),即将1、2、3抽取出来?
    可以使用flatmap方法。

    Integer[][] arr1 = {{1,2},{2,3}};
    Stream<Integer[]> t1 = Arrays.stream(arr1);//流中每个元素是一行(一维数组)
    Stream<Integer> flatMap = t1.flatMap(Arrays::stream);//扁平化处理后,流中的每一个元素是一个Integer
    flatMap.distinct().forEach(System.out::println); //distinct()排除掉重复的元素
    

    不过这种方法对基本类型数组,如int[][]就不起作用。不知道为何?
    更多参考资料见:

    结论

    本文使用了几个例子展示了Java 8中常用函数式接口在流的管道操作中的应用,Lambda表达式、方法引用与函数式接口之间的关系。希望大家以后在使用流的管道操作时,可以知其然也知其所以然。

    参考资料:

    Java学习笔记(第8版) 林信良
    Java Tutorial中的Lambda ExpressionsAggregate Operations
    Java 8 Stream Tutorial

  • 相关阅读:
    集合框架总结笔记三
    ANDROID_MARS学习笔记_S03_006_geocoding、HttpClient
    ANDROID_MARS学习笔记_S03_005_Geocoder、AsyncTask
    ANDROID_MARS学习笔记_S03_004_getAllProviders、LOCATIONLISTENER、getBestProvider
    ANDROID_MARS学习笔记_S03_003_LocationManager、LocationListener
    ANDROID_MARS学习笔记_S03_002_设置可见性及扫描蓝牙设备
    ANDROID_MARS学习笔记_S03_001_获取蓝牙匹配列表
    ANDROID_MARS学习笔记_S02_015_Gson解析json串为对象集合
    ANDROID_MARS学习笔记_S02_014_GSON解析JSON串为对象
    ANDROID_MARS学习笔记_S02_013_Gson解析json串
  • 原文地址:https://www.cnblogs.com/zhrb/p/10011744.html
Copyright © 2020-2023  润新知