JAVA高并发——函数式编程


在正式进入函数式编程之前,有必要先了解一下Java 8为支持函数式编程所做的基础性的改进。

1、FunctionalInterface注释

Java 8提出了函数式接口的概念。所谓函数式接口,简单地说,就是只定义了单一抽象方法的接口。比如下面的定义:

@FunctionalInterface
public interface IntHandler {
    void handle(int i);
}

注释FunctionalInterface用于表明IntHandler接口是一个函数式接口,该接口只包含一个抽象方法handle(),因此它符合函数式接口的定义要求。如果一个函数满足函数式接口的定义要求,那么即使不标注为@FunctionalInterface,编译器依然会把它看作函数式接口。这有点像@Override注释,如果你的函数符合重载的规范,无论你是否标注了@Override,编译器都会识别这个重载函数,但如果你进行了标注,而实际的代码不符合规范,那么就会得到一个编译错误的提示。下图展示了一个不符合规范却被标注为@FunctionalInterface的接口:
在这里插入图片描述

很显然,IntHandler接口包含两个抽象方法,因此不符合函数式接口的定义要求,又因为IntHandler接口被标注为函数式接口,产生矛盾,故编译出错。

这里需要强调的是,函数式接口只能有一个抽象方法,而不是只能有一个方法。我分两点来说明:首先,在Java 8中,接口运行存在实例方法(参见下一节的“接口默认方法”);其次,任何被java.lang.Object实现的方法都不能视为抽象方法,因为equals()方法在java.lang.Object中已经实现,所以NonFunc接口不是函数式接口。
在这里插入图片描述
函数式接口的实例可以由方法引用或者由lambda表达式构造,我们将在后面进一步举例说明。

2、接口默认方法

在Java 8之前,接口只能包含抽象方法。但从Java 8开始,接口也可以包含若干个实例方法。这一改进使得Java 8拥有了类似于多继承的功能。一个对象实例,将拥有来自多个不同接口的实例方法。

比如,接口IHorse的实现如下:
在这里插入图片描述
在Java 8中,使用default关键字可以在接口内定义实例方法。注意,这个方法并非抽象方法,而是拥有特定逻辑的具体实例方法。

所有的动物都能自由呼吸,所以,这里可以再定义一个IAnimal接口,它也包含一个默认方法breath():
在这里插入图片描述
骡是马和驴的杂交物种,因此骡(Mule)可以实现为IHorse,同时骡也是动物,因此有:
在这里插入图片描述
注意,上述代码中Mule实例同时拥有来自不同接口的实现方法,这在Java 8之前是做不到的。从某种程度上说,这种模式可以弥补Java单一继承的一些不便。但同时要知道,它将遇到和多继承相同的问题,如下图所示:
在这里插入图片描述
如果IDonkey也存在一个默认的run()方法,那么同时实现它们的Mule就会不知所措,因为它不知道应该以哪个方法为准。

增加一个IDonkey的实现:

public interface IDonkey {
    void eat();
    default void run(){
        System.out.println("Donkey run");
    }
}

修改Mule的实现如下,注意它同时实现了IHorse和IDonkey:

public class Mule implements IHorse,IDonkey,IAnimal{
    @Override
    public void eat() {
        System.out.println("Mule eat");
    }

    public static void main(String[] args) {
        Mule mule = new Mule();
        mule.run();
        mule.breath();
    }
}

此时,由于IHorse和IDonkey拥有相同的默认实例方法,故编译器会抛出一个错误:
在这里插入图片描述
为了让Mule同时实现IHorse和IDonkey,我们不得不重新实现一下run()方法,让编译器可以进行方法绑定。修改Mule的实现如下:

public class Mule implements IHorse,IDonkey,IAnimal{
    @Override
    public void eat() {
        System.out.println("Mule eat");
    }

    @Override
    public void run() {
        IHorse.super.run();
    }

    public static void main(String[] args) {
        Mule mule = new Mule();
        mule.run();
        mule.breath();
    }
}

在这里,将Mule的run()方法委托给IHorse实现,当然,大家也可以有自己的实现。

接口默认实现对于整个函数式编程的流式表达非常重要。比如,大家熟悉的java.util.Comparator接口,它在JDK 1.2时就已经被引入了,用于在排序时给出两个对象实例的具体比较逻辑。在Java 8中,Comparator接口新增了若干个默认方法,用于多个比较器的整合。其中一个常用的默认方法如下:
在这里插入图片描述
有了这个默认方法,在排序时,我们就可以非常方便地进行元素的多条件排序。比如,如下代码构造一个比较器,它先按照字符串长度排序,继而按照大小写不敏感的字母顺序排序:
在这里插入图片描述

3、lambda表达式

lambda表达式可以说是函数式编程的核心。lambda表达式即匿名函数,它是一段没有函数名的函数体,可以作为参数直接传递给相关的调用者,lambda表达式极大地增强了Java语言的表达能力。

下例展示了lambda表达式的使用方法,在forEach()方法中,传入的就是一个lambda表达式,它完成了对元素的标准输出操作。可以看到这段表达式并不像函数一样有名字,类似匿名内部类,它只是简单地描述了应该执行的代码段。
在这里插入图片描述
和匿名对象一样,lambda表达式也可以访问外部的局部变量,如下所示:
在这里插入图片描述
上述代码可以编译通过,正常执行并输出6。与匿名内部对象一样,在这种情况下,外部的num变量必须声明为final定义的,这样才能保证在lambda表达式中合法地访问它。

奇妙的是,对于lambda表达式而言,即使去掉上述的final,程序依然可以编译通过!但千万不要以为这样你就可以修改num的值了。实际上,这只是Java 8做的一个小处理,它会自动地将在lambda表达式中使用的变量视为final定义的。因此,下述代码是可以编译通过的:
在这里插入图片描述
但是,如果像下面这么写,就不行:
在这里插入图片描述
上述的num++会引起一个编译错误:
在这里插入图片描述

4、方法引用

方法引用是Java 8中提出的用来简化lambda表达式的一种手段。它通过类名和方法名来定位一个静态方法或者实例方法。

方法引用在Java 8中的使用非常灵活。总的来说,可以分为以下几种:

  • 静态方法引用:ClassName::methodName。
  • 超类上的实例方法引用:super::methodName。
  • 类型上的实例方法引用:ClassName::methodName。
  • 构造方法引用:Class::new。
  • 数组构造方法引用:TypeName[]::new。

首先,方法引用使用“::”定义,“::”的前半部分表示类名或者实例名,后半部分表示方法名。如果是构造函数,则使用new表示。

下例展示了方法引用的基本使用方法:
在这里插入图片描述
对于第一个方法引用“User::getName”,表示User类的实例方法。在执行时,Java会自动识别流中的元素(这里指User实例)是作为调用目标还是调用方法的参数。在“User::getName”中,显然流内的元素都应该作为调用目标,实际上,在这里调用了每一个User对象实例的getName()方法,并将这些User的name作为一个新的流。同时,对于这里得到的所有name,使用方法引用System.out::println处理。这里的System.out为PrintStream对象实例,因此,这里表示System.out实例的println方法。系统也会自动判断出流内的元素此时应该作为方法的参数传入,而不是调用目标。

一般来说,如果使用的是静态方法,或者调用目标明确,那么流内的元素会自动作为参数使用。如果方法引用表示实例方法,并且不存在调用目标,那么流内元素就会自动作为调用目标。

如果一个类中存在同名的实例方法和静态方法,那么编译器就会感到很困惑,因为此时它不知道应该使用哪个方法。它既可以选择同名的实例方法,将流内元素作为调用目标,也可以使用静态方法,将流内元素作为参数。

请看下面的例子:
在这里插入图片描述
上述代码试图将所有的Double元素转为String并将其输出,但是很不幸,在Double中同时存在以下两个方法:
在这里插入图片描述
此时,对方法引用的处理就出现了歧义,因此,这段代码在编译时就会抛出如下错误:
在这里插入图片描述
方法引用也可以直接使用构造函数。首先,查看模型类User的定义:

/**
 * @title User
 * @description 用户
 * @author: yangyongbing
 * @date: 2024/2/26 12:49
 */
public class User {
    private int id;
    private String name;

    public User(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

下面的方法引用调用了User的构造函数:

import java.util.ArrayList;
import java.util.List;

/**
 * @title ConstrMethodRef
 * @description 方法的引用
 * @author: yangyongbing
 * @date: 2024/2/26 12:51
 */
public class ConstrMethodRef {

    interface UserFactory<U extends User> {
        U create(int id, String name);
    }

    static UserFactory<User> uf = User::new;

    public static void main(String[] args) {
        List<User> users = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            users.add(uf.create(i, "billy" + Integer.toBinaryString(i)));
        }
        users.stream().map(User::getName).forEach(System.out::println);
    }

}

在此,UserFactory作为User的工厂类,是一个函数式接口。当使用User::new创建接口实例时,系统会根据UserFactory.create()的签名来选择合适的User构造函数。在这里,很显然就是public User(int id, String name)。在创建UserFactory实例后,对UserFactory.create()的调用,都会委托给User的实际构造函数进行,从而创建User对象实例。

5、走入函数式编程

在了解了Java 8的一些新特性后,我们就可以正式开始走入函数式编程了。为了能让大家更快地理解函数式编程,我们先从简单的例子开始:
在这里插入图片描述
上述代码循环遍历了数组内的元素,并且进行了数值的打印,这也是传统的做法。如果使用Java 8中的流,那么可以写成这样:
在这里插入图片描述
注意:Arrays.stream()方法返回了一个流对象。类似于集合或者数组,流对象也是一个对象的集合,它将使我们可以遍历处理流内元素。

这里值得注意的是这个流对象的forEach()方法,它接收一个IntConsumer接口,用于对每个流内的对象进行处理。之所以是IntConsumer接口,因为当前流是IntStream,也就是装有Integer元素的流,因此,它自然需要一个处理Integer元素的接口。forEach()方法会依次将流内的元素送入IntConsumer接口进行处理,循环过程被封装在forEach()方法内部,也就是JDK框架内。

除了IntStream流,Arrays.stream()方法还支持DoubleStream、LongStream和普通的对象流Stream,这完全取决于它接收的参数。Stream流的几种类型如下图所示:
在这里插入图片描述
这样的写法可能还不能让人满意,代码量似乎比原先更多,而且除了引入了不必要的接口和匿名类等复杂性外,似乎也看不出来有什么太大的好处。但是,我们的脚步并未就此打住。试想,既然forEach()方法的参数是可以从上下文中推导出来的,为什么还要不厌其烦地写出来呢?这些机械的推导工作就交给编译器去做吧!
在这里插入图片描述
从上述代码中可以看到,IntStream接口名称被省略了,这里只使用了参数名和一个实现体,看起来简洁很多了。但是还不够,因为参数的类型也是可以推导的。既然是IntConsumer接口,参数自然是int类型的。
在这里插入图片描述
好了,现在连参数类型也省略了,但是两个花括号特别碍眼。虽然它们对程序没有什么影响,但是为了简单的一句执行语句要加上一对花括号实属多余,干脆也去掉吧!去掉花括号后,为了清晰起见,就把参数声明和接口实现放在一行吧!
在这里插入图片描述
这样看起来就好多了。此时,forEach()方法的参数依然是IntConsumer,但是它却以一种新的形式被定义,这就是lambda表达式。表达式由“->”分割,左半部分表示参数,右半部分表示实现体。我们可以简单地理解为:lambda表达式只是匿名对象实现的一种新的方式。实际上也是这样的。

有兴趣的读者可以使用虚拟机参数-Djdk.internal.lambda.dumpProxyClasses启动带有lambda表达式的Java小程序,该参数会将lambda表达式相关的中间类型输出,方便调试和学习。在本例中,输出了HelloFunction6$$Lambda$1.class类,使用以下命令反汇编:
在这里插入图片描述
在输出结果中,可以清楚地看到:
在这里插入图片描述
限于篇幅,这里只给出了我们关心的内容。首先,这个中间类型确实实现了IntConsumer接口。其次,在实现accept()方法时,它内部委托给了一个名为HelloFunction6.lambda$0()的方法,这个方法是编译时自动生成的。

使用以下命令查看HelloFunction6的编译结果:
在这里插入图片描述
我们很惊喜地找到了期待已久的lambda$0()方法,其实现如下:
在这里插入图片描述
它被实现为一个私有的静态方法,实现内容就是简单地进行了System.out.println()方法的调用,也正是代码中lambda表达式的内容。

由此可见,Java 8中对lambda表达式的处理几乎等同于匿名类的实现,但是在写法上和编程范式上有明显的区别。

不过,简化代码的流程并没有结束,在上一节中已经提到,Java 8还支持方法引用,你甚至连参数声明和传递都可以省略:
在这里插入图片描述
至此,欢迎大家正式进入Java 8函数式编程的殿堂,那些看似玄妙的lambda表达式的解析和工作原理已经介绍完毕。

使用lambda表达式不仅可以简化匿名类的编写,与接口的默认方法相结合,还可以使用更顺畅的流式API对各种组件进行更自由的装配。

下面这个例子对集合中的所有元素进行两次输出,一次输出到标准错误中,另一次输出到标准输出中:
在这里插入图片描述
这里首先使用方法引用,直接定义了两个IntConsumer接口实例,一个指向标准输出,另一个指向标准错误。用接口默认方法IntConsumer.addThen()将两个IntConsumer进行组合,得到一个新的IntConsumer,新的IntConsumer会依次调用outprintln和errprintln,完成对数组中元素的处理。

其中方法IntConsumer.addThen()的实现如下:
在这里插入图片描述

6、并行流与并行排序

Java 8可以在接口不变的情况下,将流改为并行流。这样,就可以很自然地使用多线程进行集合中的数据处理。

6.1、使用并行流过滤数据

现在让我们考虑这么一个简单的案例,统计1~1000000内质数的数量。首先,我们需要一个判断质数的方法:
在这里插入图片描述
给定一个数字,如果这个数字是质数,上述方法就返回true,否则返回false。

接着,使用函数式编程统计给定范围内所有的质数:
在这里插入图片描述
上述代码首先生成一个1到1000000的数字流。接着使用过滤方法,只选择所有的质数,最后进行数量统计。

上述代码是串行的,将它改造成并行计算非常简单,只需要将流并行化即可:
在这里插入图片描述
在上述代码中,parallel()方法得到一个并行流,然后在并行流上进行过滤,此时PrimeUtil.isPrime()方法会被多线程并发调用,应用于流中的所有元素。

6.2、从集合得到并行流

在函数式编程中,我们可以从集合得到一个流或者并行流。下面这段代码试图统计集合内所有学生的平均分:
在这里插入图片描述
从集合对象List中,我们使用stream()方法可以得到一个流。如果希望将这段代码并行化,则可以使用parallelStream()方法:
在这里插入图片描述
可以看到,将原有的串行方式改造成并行方式是非常容易的。

6.3、并行排序

除了并行流,对于普通数组,Java 8也提供了简单的并行功能。比如,进行数组排序可以使用Arrays.sort()方法,当然这是串行排序方法,在Java 8中可以使用新增的Arrays.parallelSort()方法直接进行并行排序。

比如,你可以这样使用:
在这里插入图片描述
除了并行排序,Arrays中还增加了一些API用于数组中数据的赋值,比如:
在这里插入图片描述
这是一个函数式味道很浓的接口,它的第二个参数是一个函数式接口。如果我们想给数组中每一个元素都附上一个随机值,则可以这么做:
在这里插入图片描述
当然,以上过程是串行的。但是只要使用setAll()方法对应的并行版本,你就可以在多个CPU上执行它:
在这里插入图片描述

  • 19
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值