Java函数式编程模型之初识Lambda表达式

        2014年3月,Java8作为Java历史上变化最大的一个版本发布了。这里为什么说Java8是变化最大的版本呢,比如说它增加了Lambda表达式特性,它增加了Stream API等等,虽然这些新的特性能让我们编写的程序代码更加的简洁更加具有语义化,但是这些都只是表象。在我看来,之所以说Java8是Java历史上变化最大的版本的本质原因只有一个——它填补了Java在函数式编程领域一直以来的空白
        我们在学习Java这门语言的时候就知道了,在Java中万物皆对象,对象在Java编程模型中始终是“一等公民”的存在。现在同样,在函数式编程中,函数被视为了“一等公民”(维基百科原文 In functional programming, functions are treated as first-class citizens.)。
        好的,让我们回到本篇文章的主题,让我们认识下什么是lambda表达式。lambda表达式,维基百科上的解释是一种用于表示匿名函数和闭包的运算符,看到这个解释还是觉得很抽象,接下来我们看一个例子:

public class SwingTest {
    public static void main(String[] args) {
        JFrame jFrame = new JFrame("My JFrame");
        JButton jButton = new JButton("My JButton");

        jButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {                
                System.out.println("Button Pressed!");
            } 
        }); 
        
        jFrame.add(jButton); jFrame.pack(); 
        jFrame.setVisible(true); 
        jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 
    }
}

这是一段Swing编程中的代码,对Swing不熟悉的朋友也没关系,整体逻辑是给Button绑定一个监听事件,当点击Button时会在控制台输出"Button Pressed!"内容。这里创建了一个匿名内部类的实例来绑定到监听器,这也是以往比较常规的代码组织形式。但是仔细看一下我们会发现,实际上我们真正关注的就是一个ActionEvent类型的参数e和向控制台输出的语句:

            @Override
            public void actionPerformed(ActionEvent e) {                
                System.out.println("Button Pressed!");
            }

下面我们将上段程序中以匿名内部类创建接口实例的方式替换成Lambda表达式后,代码如下:

public static void main(String[] args) {
    JFrame jFrame = new JFrame("My JFrame");
    JButton jButton = new JButton("My JButton");

    jButton.addActionListener(e -> System.out.println("Button Pressed!"));//lambda

    jFrame.add(jButton);
    jFrame.pack();
    jFrame.setVisible(true);
    jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}

        关注最中间部分代码的变化,由原来的6行代码,现在1行就可以实现了。这就是Lambda表达式的一种简单形式。
        我们以lambda表达式的方式替换了原来由匿名内部类实现的形式,其中lambda表达式中的参数e就代表原来匿名内部类中actionPerformed方法的入参e,入参e是ActionEvent类型,lambda表达式中之所以可以省略参数类型是因为编译器可以通过上下文来推断出e是ActionEvent类型,并且当lambda表达式只有一个入参时,参数外面的括号可以省略。然后是方法实现部分,就是向控制台输出"Button Pressed!",同样,如果方法体中只有一行代码时,方法体外面的大括号也可以省略。
        所以,我们可以看出Lambda表达式的语法是:

(param1, param2, param3) -> {
    //todo
}

        到这里我们停一下,思考下这个问题,除了代码简洁了,Lambda表达式还给我们带来了什么变化吗?
        我们回忆一下,在Java中,我们是否无法将函数作为参数传递给一个方法,也无法声明返回值是一个函数的方法。在Java8之前,答案是肯定的。
        那么,在上面的例子中我们居然可以将一段代码逻辑作为参数传递给了监听器,告诉监听器事件触发时你可以这么做,而不再需要以匿名内部类的方式作为参数。这就是Java8带来的最大的变化:函数式编程。
        支持函数式编程的语言有很多,在JavaScript中,把函数作为参数传递,或者返回值是一个函数的情况非常常见,JavaScript是一门非常常见的函数式语言。
        在函数式编程语言中,lambda表达式的类型是函数。而在Java中,lambda表达式是对象,它们必须依附于一类特别的对象类型——函数式接口(Functional Interface)。
        函数式接口的定义:如果一个接口中,有且只有一个抽象的方法(Object类中的方法不包括在内),那这个接口就可以被看做是函数式接口。我们看下Runnable接口的声明:

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

        在Java8后,Runnable接口多了一个FunctionalInterface注解,表示该接口是一个函数式接口。但是如果我们不添加FunctionalInterface注解的话,如果接口中有且只有一个抽象方法时,编译器也会把该接口当做函数式接口看待。
        同时,下面这个自定义的接口,MyInterface也是一个函数式接口,因为toString()是Object类中的方法,只是在这里进行了复写,不会增加接口中抽象方法的数量。

@FunctionalInterface
public interface MyInterface {
    void test();
    String toString();
}

        上面提到lambda表达式属于Java对象,那么这个对象的类型是什么呢?我们回顾下SwingTest程序,这里以匿名内部类的方式创建了一个ActionListener接口实例:

jButton.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {                
        System.out.println("Button Pressed!");
    } 
}); 

        使用Lambda表达式改进后变成了:

jButton.addActionListener(e -> System.out.println("Button Pressed!"));

        可以看出我们使用Lambda表达式创建了一个ActionListener接口的实例,并提供了具体的内部实现。我们看下ActionListener接口的定义:

public interface ActionListener extends EventListener {
    /**
     * Invoked when an action occurs.
     */
    public void actionPerformed(ActionEvent e);
}

        只有一个抽象方法,虽然没添加FunctionalInterface注解,但是也符合函数式接口的定义,编译器会认为这是一个函数式接口。所以,通过lambda表达式,我们可以创建函数式接口的实例,即Lambda表达式返回的是函数式接口类型。

        我们除了可以通过lambda表达式创建函数式接口的实例之外,还可以有其他两种方式,参考FunctionalInterface注解的Java Doc:

Note that instances of functional interfaces can be created with lambda expressions, 
method references, or constructor references

        创建函数式接口实例的方式有三种:
        * lambda表达式
        * 方法引用
        * 构造方法引用

        先不着急学习其它两种创建函数式接口实例的方式,接下来再通过一个例子加深下通过lambda表达式来创建函数式接口实例:

public class Test1 {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
        list.forEach(new Consumer<Integer>() {
            @Override
            public void accept(Integer integer) {
                System.out.println(integer);
            }
        });
    }
}

        这段程序很简单,首先初始化一个Integer类型的集合然后向控制台输出每个元素。其中我们注意到forEach方法,它是Java8中新增加的默认方法。接口中的默认方法可以有具体的实现:

public interface Iterable<T> {
    .
    .省略
    .
    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }
}

         forEach方法被声明在Iterable接口中,并被关键字default修饰。这样任何一个该接口的子类型都可以继承forEach方法的实现,所以List接口因为是Iterable的间接子接口,所以也继承了该默认方法。Java8采用这种巧妙的方式既扩展了接口的功能,又兼容了老版本。

        接下来分析下forEach的实现,首先接收了一个Consumer类型的参数action,进行非空判断,然后遍历当前所有元素交由action的accept方法进行处理。那么Consumer又是什么鬼,看源码:

@FunctionalInterface
public interface Consumer<T> {

    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);
    .
    .省略
    .
}

        一个接口,有且仅有一个抽象方法,被@FunctionalInterface修饰,典型的函数式接口。好的,现在我们知道forEach接收的Consumer类型的参数是一个函数式接口,接口里唯一的accept抽象方法接收一个参数,不返回值。那通过上一篇文章我们知道,创建函数式接口类型的实例其中一种方式是使用lambda表达式,所以可以将最上面的程序改造一下:

public class Test1 {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
        //Lambda表达式 接收一个参数 不返回值
        list.forEach(item -> System.out.println(item));
    }
}

        该lambda表达式:

item -> System.out.println(item)

        接收一个参数 不返回值,符合accept方法定义,编译通过。也就是说如果使用lambda表达式来创建一个函数式接口实例,那这个lambda表达式的入参和返回必须符合这个函数式接口中唯一的抽象方法的定义。

        上面我们提到创建函数式接口实例的另外两种方式,方法引用和构造方法引用,接下来我们继续改造程序:

public class Test1 {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
        //方法引用
        list.forEach(System.out::println);
    }
}

        看到out后面有两个冒号,反正当时我是凌乱了。。。这个就是函数式接口实例第二种创建方式:方法引用。方法引用的语法是 对象::方法名(只是其中一种)。同样,使用方法引用方式去创建函数式接口实例也必须遵守函数式接口中抽象方法的定义,看下此处println方法源码:

public void println(Object x) {
    String s = String.valueOf(x);
    synchronized (this) {
        print(s);
        newLine();
    }
}

        接收一个参数,并不返回值,编译通过。
        最后我们来看下创建函数式接口实例的最后一种,第三种方式:构造方法引用 ,继续改程序:

public class Test1 {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
        //构造方法引用
        list.forEach(Test1::new);
    }
    
    Test1(Integer i){
        System.out.println(i);
    }
}

        构造方法引用的语法是:类名::new
        我们给Test1新添加了一个构造方法,该构造方法接收一个参数,不返回值,符合Consumer函数式接口中的抽象方法accept定义,编译通过。实际工作中我们不会如此实现构造方法的,此处只是为了演示使用构造方法引用创建函数式接口实例的语法使用。

        除了Consumer之外,Java8还内置了很多函数式接口方便我们直接使用,常用的如Function、BiFunction、Predicate等。后续包括Stream API都会介绍到。

        如果觉得本篇文章对你有一点点帮助的话,帮忙点个赞哈

        下一篇

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