Java核心教程4: 函数式编程

函数式编程语言使得操纵代码片段就像操作数据一样容易。 虽然 Java 不是函数式语言,但 Java 8 Lambda 表达式和方法引用使得你可以以函数式编程的思想来编程。

函数式编程是现在比较流行的一个编程方式,它并没有严格的定义,但有以下几个特点:函数是第一等公民、只用表达式不用语句、无副作用、不修改状态、引用透明性

这里面的后面四个特点我们可以暂时不需要了解,现在只需要弄懂第一个就可以了,函数是第一等公民的意思就是:函数在编程语言里地位很高,不仅允许把函数本身作为参数传入另一个函数,还允许返回一个函数!

但我们知道,Java 里函数并不是一等公民,它必须得依附类而存在,那该如何进行函数式编程?

让我们首先来看看下面这一个案例:

你需要写一个对字符串的加密程序,但要实现可以动态替换加密算法,你可以很简单的写出如下代码:

//加密器
public class Cipher {
    private CipherStrategy cipherStrategy=new CipherOne();
    //更改加密算法
    public void setCipherStrategy(CipherStrategy strategy){
        this.cipherStrategy=strategy;
    }

    public String cipher(String source){
        return new String(cipherStrategy.cipher(source.getBytes()));
    }

    //加密算法的接口
    interface CipherStrategy{
        byte[] cipher(byte[] source);
    }

    //加密算法1
    static class CipherOne implements CipherStrategy{
        @Override
        public byte[] cipher(byte[] source) {
            //省略加密代码
        }
    }

    //加密算法2
    static class CipherTwo implements CipherStrategy{
        @Override
        public byte[] cipher(byte[] source) {
            //省略加密代码
        }
    }

    //加密算法3
    static class CipherThree implements CipherStrategy{
        @Override
        public byte[] cipher(byte[] source) {
            //省略加密代码
        }
    }
}

使用起来也很简单:

//初始化一个加密器
Cipher cipher=new Cipher();

//将字符串进行加密并输出
String result=cipher.cipher("I am happy.");
System.out.println(result);

//更改加密算法为“加密算法2”
cipher.setCipherStrategy(new CipherTwo());

//使用匿名内部类临时采用其他加密算法
cipher.setCipherStrategy(new CipherStrategy(){
    public byte[] cipher(byte[] source){
        //其他加密算法...
    }
});
result=cipher.cipher("I am sad.");

上面的做法就是不支持函数式编程时我们经常采取的方法,也叫做策略模式(设计模式中的一种模式),但在 Java 8 之后我们有了更优雅的解决方式:

cipher.setCipherStrategy(source->{
    //省略加密算法
});

//假设在Test类中有如下静态方法,那我们还可以这样做
public static byte[] cipherFour(byte[] source){
    //省略加密算法
}
cipher.setCipherStratigey(Test::cipherFour);

这两种方式分别叫做 Lambda 表达式方法引用,让我们来逐个击破吧!


一、Lambda 表达式

除了Java之外,很多其他编程语言也有 lambda 表达式:

# python
func=lambda a:print(a)
//javascript
func=(a)=>{
    console.log(a)
}
//c++
auto f=[](int a,int b){return a>b;};

Lambda 表达式本质上其实就是匿名函数。但在 Java 中,一切都是类,因此在 Java 中的 lambda 表达式实际返回的也是一个类,只是编译器在后面做了很多工作使它看起来就像一个函数一样——但作为程序员,你可以高兴地假装它们“只是函数”。

下面的代码中展示了几种 lambda 表达式的语法形式:

public class Main {
    interface OneArgInterface {
       void test(int a);
    }

    interface TwoArgsInterface {
        void test(int a, int b);
    }
  
    interface ReturnInterface {
        int test(int a,int b);
    }
    
    interface NoArgInterface {
        void test();
    }
  
    public static void useInterface(OneArgInterface func){
        func.test(789)
    }

    public static void main(String[] args) {
        TwoArgsInterface x = (a, b) -> {
            System.out.println(a);
        };  //[1]
        useInterface(a -> {
            System.out.println(a);
        });  //[2]
        ReturnInterface z = (a, b) -> a+b;  //[3]
        NoArgInterface b = () -> System.out.println("Nothing"); //[4]
        x.test(123, 456);
        y.test(78);
        int a = z.test(234,789);
        //调用起来没什么特别的,就和正常的类一样调用
    }
}

[1] Lambda表达式的最完整形式,括号里面写参数列表,然后用箭头语法连接方法体。之所以括号里的参数列表不需要写类型是因为编译器可以自动根据左边的变量类型自动推断出来,这句话可以等价于下面的匿名内部类的代码:

x = new TwoArgsInterface(){
    @Override
    public void test(int a, int b) {
        System.out.println(a);
    }
};

可以看出 lambda 表达式只能用来简化只有一个抽象函数的接口或抽象类的匿名内部类的书写,而多个抽象函数的则不行,所以它其实也是一个“语法糖”

[2] 对于只有一个参数的类型,括号可以省略。

[3] 如果方法体里只有一条语句那方法体的大括号也可以省略,返回值直接就是这条语句的返回值。

[4] 如果抽象方法没有参数则括号不能省略


二、方法引用

lambda 表达式很优雅的解决了单方法的匿名内部类占太多代码行数的问题,而且更加直观明了。但如果我想将一个已经存在的方法作为抽象方法的执行语句的话就需要用到方法引用了。

回想一下上面代码中的第二条 lambda 表达式语句:

useInterface(a -> {
    System.out.println(a);
});

我们只在方法体中执行了一条方法调用语句,有些颇为浪费,有了方法引用后我们就可以这么做的:

useInterface(System.out::println);

方法引用的语法组成:类名或对象名,后面跟 ::,然后跟方法名称。

有些同学看到这里可能就会问了,System.out.println()有这么多重载方法,它怎么知道该调用哪一个呢?

这个问题问得好。这不得不又得扯到编译器自动推断了,因为useInterface()的参数是已经确定的,它只支持放入OneArgInterface类型的对象,而因为OneArgInterface里的抽象方法的参数是单个 int 类型,所以编译器自动推断出来应该调用参数为单个 int 类型的 println 方法,不得不多说编译器真的替我们做了好多事情啊!

方法引用使用起来很简单,但还有下面两个值得讲解的地方。

未绑定的方法引用

未绑定的方法引用是指没有关联对象的非静态方法的引用。 让我们先来看看这个例子:

class Test {
    String func(int i) { return "Test::func()"; }
}

interface InterfaceA {
    String run(int i);
}

interface InterfaceB {
    String run(Test test, int i);
}

public class UnboundMethodReference {
    public static void main(String[] args) {
        // InterfaceA inter = Test::func; // 这条语句会报错
        InterfaceB inter = Test::func;
        Test test = new Test();
        System.out.println(inter.run(test, 123)); 
        System.out.println(test.func(123)); // 与上面那句话同等效果
    }
}

刚看到这段代码的时候可能会感到非常迷惑,没事,我们一步一步来。

首先,这次我们没有像之前那样用的是“对象名::方法名”,而是用的类名,所选的调用方法也是非静态方法,这就叫做未绑定方法。我们都知道,非静态方法必须得依附一个对象存在,也就是所谓的this;没有对象,自然也就无法调用非静态方法。而与之相反的是静态方法可以独立于对象存在,直接用类名加.进行调用。

所以我们可以把非静态方法用另一种方式书写,例如最后一句代码里的test.func(123)可以等价转换为Test.func(test, 123) (PS: 只能是这样理解,但语法上并不允许)

这样你就应该明白为什么 InterfaceBrun函数开头需要多一个参数了吧,这是为了绑定方法运行的对象用的。

稍微总结一下的话就是形似“类名::非静态方法的方法名”的方法引用会自动在参数列表前面加一个该类对象的参数。

构造函数引用

构造函数引用非常容易理解,就是将构造函数引用成抽象类或接口,对于有多个构造函数的情况,编译器也可以自动推断出应该引用哪一个构造函数。

class Dog {
  String name;
  int age = -1; // For "unknown"
  Dog() { name = "stray"; }
  Dog(String nm) { name = nm; }
  Dog(String nm, int yrs) { name = nm; age = yrs; }
}

interface MakeNoArgs {
  Dog make();
}

interface Make1Arg {
  Dog make(String nm);
}

interface Make2Args {
  Dog make(String nm, int age);
}

public class CtorReference {
  public static void main(String[] args) {
    MakeNoArgs mna = Dog::new; // [1]
    Make1Arg m1a = Dog::new;   // [2]
    Make2Args m2a = Dog::new;  // [3]

    Dog dn = mna.make();
    Dog d1 = m1a.make("Comet");
    Dog d2 = m2a.make("Ralph", 4);
  }
}



三、内置的函数式接口

由于 Java 所有函数都依附于类的特性,使得想要进行纯粹的函数式编程非常麻烦,例如之前看到的代码例子,我们每次都需要创建各种各样的接口来满足不同的方法类型,这着实非常麻烦,于是 Java 就给我们提供了一大堆已经写好的满足函数式编程的接口。如下表所示:

特征 示例
无参数; 没有返回值 Runnable
无参数; 有返回值 Supplier<T> BooleanSupplier IntSupplier LongSupplier DoubleSupplier
一个参数; 没有返回值 Consumer<T> IntConsumer LongConsumer DoubleConsumer
两个参数;没有返回值 BiConsumer
两个参数 ,分别是一个引用类型、一个基本类型;没有返回值 ObjIntConsumer ObjLongConsumer ObjDoubleConsumer
一个参数; 有返回值 Function<T,R> IntFunction<R> LongFunction<R> DoubleFunction<R> ToIntFunction<T> ToLongFunction<T> ToDoubleFunction<T> IntToLongFunction IntToDoubleFunction LongToIntFunction LongToDoubleFunction DoubleToIntFunction DoubleToLongFunction
一个参数;参数与返回值类型相同 UnaryOperator<T> IntUnaryOperator LongUnaryOperator DoubleUnaryOperator
两个参数,类型相同; 参数与返回值类型相同 BinaryOperator<T> IntBinaryOperator LongBinaryOperator DoubleBinaryOperator
两个参数; 返回值是布尔型 Predicate<T> BiPredicate<T,U> IntPredicate LongPredicate DoublePredicate
参数是基本类型; 返回值也是基本类型 IntToLongFunction IntToDoubleFunction LongToIntFunction LongToDoubleFunction DoubleToIntFunction DoubleToLongFunction
两个参数,类型不同 BiFunction<T,U,R> BiConsumer<T,U> BiPredicate<T,U> ToIntBiFunction<T,U> ToLongBiFunction<T,U> ToDoubleBiFunction<T,U>

这个表格看起来有点复杂,不过可以稍微总结一下:

  • 没有参数且没有返回值的是Runnable
  • 有一个参数,没返回值的是Consumer,Consumer的意思是消耗者,也就是只消耗东西(参数)却没有产生新东西(返回值)
  • 没有参数,有一个返回值的是Supplier,Supplier的意思是提供者,也就是没有获得东西(参数)却提供新东西(返回值)
  • 有两个参数并且有返回值,参数与返回值相同,是因为操作符运算就符合这个特征,例如加减乘除就是两个同类型的数字相加再返回同类型的数字;而 Binary 表示二元运算,Unary 表示一元运算,所以分别表示有两个参数和一个参数
  • 然后剩下的 Bi 开头的表示有两个参数;XXXToYYYFunction 表示参数是XXX,返回值是 YYY;Predicate 表示返回值是布尔值

有了这些接口以后就不再需要自己写函数式接口了,例如最开始的加密程序就可以变成这样了:

public class Cipher {
    private CipherStrategy cipherStrategy=new CipherOne();
    public void setCipherStrategy(CipherStrategy strategy){
        this.cipherStrategy=strategy;
    }

    public String cipher(String source){
        //这里调用的方法名也变了,不同的函数式接口,方法名也不相同,查看接口的源码可知
        byte[] result = cipherStrategy.apply(source.getBytes());
        return new String(result);
    }

                             //表示方法的参数和返回值相同,且为字节数组
    class CipherStrategy implements UnaryOperator<byte[]>{}
    
    //...省略其他代码
}

除此之外,这些函数式接口不只是帮你节省了写接口的时间,还可以实现诸如方法组合以及柯里化等高级操作,具体方法由于时间问题就不讲述了,在下面的拓展阅读中有详细介绍。

关于 Java 函数式编程还想了解更多的同学可以看看这个网页 https://lingcoder.gitee.io/onjava8/#/book/13-Functional-Programming

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容