jdk8新特性之函数式编程

参考文档/内容:

    本系列文章内容主要来自于《java8 实战》,加上了自己一些想法和重新编写的例子,主要目的是为了加深自己的印象,方便后期的复习。

    所以有任何问题请购阅《java8 实战》内容,支持正版。

一、原书问题和内容的阐述

1.1、Java怎么又变了

  原因具体如下:

  • 因为之前的JDK不好用(具体可参考上一篇的文章)
  • 多核心CPU的利用和大数据的处理优化考虑
  • 其它技术相关优点的借鉴,“要么改变,要么衰亡”

二、函数编程

2.1、什么是函数编程?

  一等公民
    编程语言的整个目的就在于操作值:原始值(例如:int类型42、double类型3.14等)、对象(更严格地说是对象的引用值),这些值被称为一等公民

  二等公民
    编程语言中的其他结构也许有助于我们表示值的结构(如方法和类等,虽然方法可以来定义类,类还可以实例化来产生值,但方法和类本身都不是值),但在程序执行期间不能传递,因而是二等公民

  函数编程
    首先:编程语言中的函数一词通常是指方法,尤其是静态方法;将方法(方法引用)作为一等公民进行操作的过程就称之为函数编程

2.2、函数式接口

  函数式接口就是只定义一个抽象方法的接口

  后面会介绍到:接口现在还可以拥有默认方法,哪怕接口中有很多默认方法,只要接口只定义了一个抽象方法,它就仍然是一个函数式接口。

  @FunctionalInterface注解:
    新版本的Java API,会发现函数式接口带有@FunctionalInterface的注解,这个注解用于表示该接口会设计成一个函数式接口。

    如果你用@FunctionalInterface定义了一个接口,而它却不是函数式接口的话,编译器将返回一个提示原因的错误。

    所以通常情况下我们约定俗成在函数式接口中添加 @FunctionalInterface注解(如果没有这个注解也不会报错)

2.3、Lambda表达式(匿名函数)的介绍

  Lambda表达式会生成函数接口的一个实例,所以Lambda表达式(匿名函数)的参数类型和返回值类型必须要和函数接口中抽象方法的保持一致

  可以把Lambda表达式理解为简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表。

  () -> {}

  Lambda(匿名函数)表达式的介绍:

    1、下面的Lambda表达式表示:参数为空,方法体为空,没有返回值的匿名函数

() -> {}

    2、下面的Lambda表达式表示:参数为空,返回值为"Raoul"的匿名函数

() -> "Raoul"

    3、下面的Lambda表达式和第二个表达式含义相同

() -> {return "Mario";}

    4、下面的Lambda表达式表示:参数为两个Apple 对象,返回值为 int 类型,方法体为:两个Apple对象中 weight 值的比较

(Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());

  回到上一章提到的“模板代码”问题:

    new Thread(new Runnable() {
        @Override
        public void run() {
            System.err.println("run......");
        }
    }).start();

  使用Lambda表达式来优化代码如下:

    new Thread(() -> System.err.println("lambda run......")).start();

  怎么理解这个上面的代码?

    Lambda表达式会生成函数接口的一个实例,上面的代码中 () -> System.err.println(“lambda run…”) 相当于执行了以下几步操作:

  • 动态的生成一个匿名的 Runnable 函数接口实现类
  • Lambda函数内容动态覆盖匿名实现类中唯一的run方法
  • 创建匿名实现类的实例对象
  • 将当前实现类对象作为参数传递
	//	第一步:动态创建匿名的函数接口实现类
    class Runnable_12315 implements Runnable{
        @Override
        public void run() {            
        }
    }

    class Runnable_12315 implements Runnable{
        //  第二步:Lambda函数内容动态覆盖匿名实现类中唯一的run方法
        @Override
        public void run() {
            System.err.println("lambda run......");
        }
    }

	//	第三步:创建匿名实现类的实例对象
    final Runnable_12315 runnable_12315 = new Runnable_12315();

    //	第四步:将当前实现类对象作为参数传递    
    new Thread(runnable_12315).start();

2.4、方法引用

  这里我有一个需求:指定一个目录,获取这个目录下所有的目录

    public File[] findDirs(File dir){
        File[] dirFiles = dir.listFiles(new java.io.FileFilter() {
            @Override
            public boolean accept(File file) {
                return file.isDirectory();
            }
        });
        return dirFiles;
    }

  考虑一下:我们已经有一个方法isDirectory可以使用,为什么非得把它包在一个啰嗦的FileFilter类里面再实例化呢?因为在Java 8之前你必须这么做!

  如今在Java 8里,使用Lambda表达式构建第一个版本:

    public File[] findDirs2(File dir){
        return dir.listFiles((file) -> {
            return file.isDirectory();
        });
    }

  使用Lambda表达式构建第二个版本:

    public File[] findDirs2(File dir){
        return dir.listFiles(file -> file.isDirectory());
    }

  方法引用的版本:

    public File[] findDirs3(File dir){
        return dir.listFiles(File::isDirectory);
    }

  由于你已经有了函数isDirectory,因此只需用Java 8的方法引用::语法(即“把这个方法作为值”)将其传给listFiles方法;当写下File::isDirectory的时候,你就创建了一个方法引用,你同样可以传递它.

   File::isDirectory 相当于是 file -> file.isDirectory() 相当于执行了以下几步操作:

  • 动态的生成一个匿名的FileFilter 函数接口实现类
  • 将匿名实现类中唯一 accept 方法中的内容修改为:file.isDirectory()
  • 创建匿名实现类的实例对象
  • 将当前实现类对象作为参数传递
	//	第一步:动态的生成一个匿名的**FileFilter** 函数接口实现类
    class FileFilter_12315 implements java.io.FileFilter{
        
    }

    class FileFilter_12315 implements java.io.FileFilter{
    	// 第二步:将匿名实现类中唯一 accept 方法中的内容修改为:file.isDirectory()
        @Override
        public boolean accept(File file) {
            return file.isDirectory();
        }
    }

	//	第三步:创建匿名实现类的实例对象
	java.io.FileFilter filter = new FileFilter_12315();

	//	第四步:将当前实现类对象作为参数传递
	dir.listFiles(filter);

2.5、流(Stream)

  在Unix或Linux中,很多程序都从标准输入(Unix和C中的stdin,Java中的System.in)读取数据,然后把结果写入标准输出(Unix和C中的stdout,Java中的System.out)。

  Unix的cat命令会把两个文件连接起来创建一个流,tr会转换流中的字符,sort会对流中的行进行排序,而tail -3则给出流的最后三行。Unix命令行允许这些程序通过管道(|)连接在一起,比如:

$ cat file1 file2 | tr "[A-Z]" "[a-z]" | sort | tail -3

  上面的命令:会(假设file1和file2中每行都只有一个词)先把字母转换成小写字母,然后打印出按照词典排序出现在最后的三个单词。我们说sort把一个行流①作为输入,产生了另一个行流(进行排序)作为输出。

  这个过程就像汽车组装流水线一样,汽车基础零件排队进入加工站,每个加工站会接收、修改汽车,然后将之传递给下一站做进一步的处理。尽管流水线实际上是一个序列,但不同加工站的运行一般是并行的,就好比工厂开辟了多条流水线并行处理数据。

  现在再理解流的概念:流是一系列数据项(汽车基础零件),一次只生成一项(流水线中某个节点某个时间点只能处理一个汽车基础零件)。程序可以从输入流中一个一个读取数据项,然后以同样的方式将数据项写入输出流。

  基于这一思想,Java 8在java.util.stream中添加了一个Stream API;Stream就是一系列T类型的项目。你现在可以把它看成一种比较花哨的迭代器。Stream API的很多方法可以链接起来形成一个复杂的流水线,就像先前例子里面链接起来的Unix命令一样。

  这种做法的好处是:Java 8程序可以很方便的底层“暗箱操作”,使用多个CPU内核去分别执行你Stream流中的数据——这是几乎免费的并行,用不着去费劲搞Thread了

  Stream 中的方法分为两种:惰性求值方法及早求值方法

  • 惰性求值方法:方法的返回值是 Stream 对象的方法,如果 Stream 链式对象只调用惰性求值方法,则惰性求值方法的内容不会被调用(可以理解为变量没有使用,则变量没有初始化)
  • 及早求值方法:方法的返回值是另一个值或为空的方法,只有Stream 链式对象最终调用了及早求值方法,之前配置的所有方法才会被一一调用。

  惰性求值方法的特点介绍例子1:
    由于 Stream 对象没有调用及早求值方法,所以惰性求值方法中的内容并不会被调用

    @Test
    public void inertiaMethod(){
        final Stream<Integer> integerStream = Stream.of(1, 3, 5, 9, 21, 36);
        //  由于 Stream 对象没有调用及早求值方法,所以惰性求值方法中的内容并不会被调用
        final Stream<Integer> add1Stream = integerStream.map(num -> {
            System.err.println(num);
            return num + 1;
        });
        System.err.println("inertiaMethod()执行完毕........");
    }

  执行结果:

inertiaMethod()执行完毕........

  惰性求值方法的特点介绍例子2:
    先调用了惰性求值方法,然后调用了及早求值方法,所以惰性求值方法内容会被执行

    @Test
    public void earlyMethod(){
        final IntStream intStream = IntStream.of(1, 3, 5, 9, 21, 36);
        //  这里先调用了惰性求值方法,然后调用了及早求值方法,所以惰性求值方法内容会被执行
        final int sum = intStream.map(num -> {
            System.err.println(num);
            return num + 1;
        }).sum();
        System.err.println("earlyMethod()执行的结果........"+sum);
    }

  执行结果:

1
3
5
9
21
36
earlyMethod()执行的结果........81

  惰性求值方法的特点介绍例子3:
    Stream 处理数据的特点:流中的每个数据项 依次调用第一个map方法, 第二个map方法,及早求值reduce方法,执行完所有配置了的 Stream 链式方法,下一个数据项才开始从头开始重新执行处理

    @Test
    public void stream01(){
        final int sum = IntStream.of(2, 3, 5)
                //  先调用了第一个map惰性求值方法
                .map(num -> {
                    System.err.println("第一个map方法:"+num);
                    return num + 1;

                //  然后调用第二个map惰性求值方法
                }).map(num -> {
                    System.err.println("第二个map方法:"+num);
                    return num * 2;

                //  这里调用了及早求值方法
                }).reduce(0,(tempSum, item) -> {
                    System.err.println("reduce方法:"+item);
                    return tempSum + item;
                });
        System.err.println("执行的结果:"+sum);
    }

  执行结果:

第一个map方法:2
第二个map方法:3
reduce方法:6
第一个map方法:3
第二个map方法:4
reduce方法:8
第一个map方法:5
第二个map方法:6
reduce方法:12
执行的结果:26

2.6、默认方法

  大家考虑一下这个问题:
    Stream 提供了非常棒的数据处理方式,同时也是一个容器,但是目前我们最最最常用的容器为 List 或者是 Collection,所以我们需要在List 或者 Collection接口中添加一个转换为 Stream 的方法,这样是不是很棒?

    貌似很棒,但是你有没有考虑到:那些已经实现了List 或者Collection接口实现类由于你添加了转换为 Stream的抽象方法,都编译报错了,这…(头皮爆炸了)

    所以为了兼容低版本的JDK,所以JDK8中引入了默认方法的概念:使用 default 关键字,即使默认方法是在接口中,默认方法也不是抽象方法(方法内是有方法内容的),如果List 或者 Collection接口将对应的转换方法声明为默认方法,即使接口的实现类并没有实现对应的转换方法,程序也能正常运行(会调用接口的默认方法)。

  例子:
    定义 MyCollection 接口,接口中定义一个默认方法

    //  定义一个接口
    interface MyCollection<E>{
        boolean add(E e);
        void clear();
        
        //  这里添加一个默认方法(注意默认方法并不是抽象方法)
        default Stream stream() {
            System.err.println("MyCollection接口的stream()......");
            return Stream.of(1,2,3,4);
        }
    }

    定义一个接口实现类,可以看到接口中的默认方法可以不实现

    class MyCollectionImple1 implements MyCollection{
        @Override
        public boolean add(Object o) { return false; }

        @Override
        public void clear() {
            System.err.println("MyCollectionImple实现类的clear()......");
        }
    }

    定义一个接口实现类,可以覆盖接口中的默认方法

    class MyCollectionImple2 implements MyCollection{
        @Override
        public boolean add(Object o) { return false; }

        @Override
        public void clear() {}

        @Override
        public Stream stream() {
            System.err.println("MyCollectionImple2接口实现类的stream()......");
            return Stream.of(5,6,7,8);
        }
    }

  测试:

    @Test
    public void test(){
        final MyCollectionImple1 imple1 = new MyCollectionImple1();
        imple1.stream();

        final MyCollectionImple2 imple2 = new MyCollectionImple2();
        imple2.stream();
    }

  执行的结果:

MyCollection接口的stream()......
MyCollectionImple2接口实现类的stream()......

  这里可以看到:接口实现类可以不实现接口中的默认方法,也可实现默认方法;有点类似于类与类之间的继承(但这可比类方法继承复杂的多,因为类只能单继承,接口实现类可以实现多个接口,如果多个接口中有相同的名称的默认方法,那该如何处理呢?)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值