Java 函数式编程

Posted xuanxuan爱吃肉

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 函数式编程相关的知识,希望对你有一定的参考价值。

Java 函数式编程

一、Lambda表达式

1.1 函数式编程思想概述

在数学中,函数就是有输入量、输出量的一套计算方案,也就是“拿数据做操作”

面向对象思想强调“必须通过对象的形式来做事情”

函数式思想强调则金量忽略面向对象的复杂语句:“强调做什么,而不是以什么形式去做”

而我们要学习的Lambda表达式就是函数式思想的体现

1.2 体验Lambda表达式

需求:启动一个线程,在控制台输出一句话:多线程程序启动了

方式1:

  • 定义一个类MyRunnable接口,重写run方法
  • 创建MyRunnable类的对象
  • 创建Thread类对象,把MyRunnable的对象作为构造参数传递
  • 启动线程
public class MyRunnable implements Runnable 
    @Override
    public void run() 
        System.out.println("多线程程序启动了");
    

        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();

方式2:

  • 在方式1的基础上进行改进,使用匿名内部类的方式
      new Thread(new Runnable() 
           @Override
            public void run() 
                System.out.println("多线程程序启动了");
            
        ).start();

方式3:

  • Lambda表达式的方式改进
        new Thread(() -> 
            System.out.println("多线程程序启动了");
        ).start();

1.3 Lambda表达式的标准格式

匿名内部类中重写run()方法的代码分析:

  • 方法形式参数为空,说明调用方法时不需要传递参数
  • 方法返回值类型为void,说明方法执行没有结果返回
  • 方法体中的内容,是我们具体要做的事情
      new Thread(new Runnable() 
           @Override
            public void run() 
                System.out.println("多线程程序启动了");
            
        ).start();

Lambda表达式的代码分析:

  • ():里面没有内容,可以看成是方法形式参数为空
  • ->:用箭头指向后面要做的事情
  • :包含一段代码,我们称之为代码块,可以看成是方法体中的内容
        new Thread(() -> 
            System.out.println("多线程程序启动了");
        ).start();

组成Lambda表达式的三要素:形式参数、箭头、代码块

Lambda表达式的格式:

  • 格式:(形式参数)->代码块
  • 形式参数:如果有多个参数,参数之间用逗号隔开;如果没有参数,留空即可
  • ->:由英文中画线和大于符号组成,固定写法。代表指向动作
  • 代码块:是我们具体要做的事情,也就是以前我们写的方法体内容

1.4 Lambda表达式的练习

Lambda表达式的使用前提

  • 有一个接口
  • 接口中有且仅有一个抽象方法

练习1:

  • 定义一个接口(Eatable),里面定义一个抽象方法:void eat();
  • 定义一个测试类(EatableDemo),在测试类中提供两个方法:
    • 一个方法是:useEatable(Eatable e)
    • 一个方法是主方法,在主方法中调用useEatable方法

定义一个接口:

public interface Eatable 
    void eat();

  • 方式一:传统接口实现类
public class EatableImpl implements Eatable
    @Override
    public void eat() 
        System.out.println("一日三餐,必不可少");
    

public class EatableDemo
    public static void main(String[] args) 
        Eatable eatable = new EatableImpl();
        eatable.eat();
    

    private static void useEatable(Eatable eatable)
        eatable.eat();
    

  • 方式2:匿名内部类
public class EatableDemo
    public static void main(String[] args) 
        useEatable(new Eatable() 
            @Override
            public void eat() 
                System.out.println("一日三餐,必不可少");
            
        );
    

    private static void useEatable(Eatable eatable)
        eatable.eat();
    

  • 方式3:Lambda表达式
public class EatableDemo
    public static void main(String[] args) 
        useEatable(()->
            System.out.println("一日三餐,必不可少");
        );
    

    private static void useEatable(Eatable eatable)
        eatable.eat();
    

运行结果均相同

练习2:

  • 定义一个接口(Flyable),里面定义一个抽象方法:void fiy(String s);
  • 定义一个测试类(FlyableDemo),在测试类中提供两个方法
    • 一个方法是:useFlyable(Flyable f)
    • 一个方法是主方法,在主方法中调用useFlayable方法
public interface Flyable 
    void fly(String s);

public class FlyableDemo 
    public static void main(String[] args) 

        useFlyable(new Flyable() 
            @Override
            public void fly(String s) 
                System.out.println(s);
                System.out.println("飞机可以起飞");
            
        );
        System.out.println("--------------------");
        useFlyable((String s)->
            System.out.println(s);
            System.out.println("飞机可以起飞");
        );
    
    private static void useFlyable(Flyable flyable)
        flyable.fly("风和日丽,晴空万里");
    

练习3:

  • 定义一个接口(Addable),里面定义一个抽象方法:int add(int x,int y);
  • 定义一个测试类(AddableDemo),在测试类中提供两个方法
    • 一个方法是:useAddable(Addable a)
    • 一个方法是主方法,在主方法中调用useAddable方法
public interface Addable 
    int add(int x,int y);

public class AddableDemo 
    public static void main(String[] args) 
        useAddable(new Addable() 
            @Override
            public int add(int x, int y) 
                return x + y;
            
        );

        useAddable((int x,int y)->
            return x + y;
        );
    

    private static void useAddable(Addable addable) 
        int sum = addable.add(10, 20);
        System.out.println(sum);
    

1.5 Lambda表达式的省略模式

省略规则:

  • 参数类型可以省略。如果有多个参数的情况下,不能只省略一个
  • 如果参数有且仅有一个,那么小括号可以省略
  • 如果代码块的语句只有一条,可以省略大括号和分号,甚至时return
public class LambdaDemo5 
    public static void main(String[] args) 
        //参数类型可以省略
        useAddable((x, y) -> 
            return x + y;
        );
        System.out.println("------------------------");
        //如果只有一个参数,小括号也可以省略
        useFlyable(s -> 
            System.out.println(s);
        );
        System.out.println("------------------------");
        //如果代码块的语句只有一条,可以省略大括号和分号(有return时要把return也去掉)
        useFlyable(s ->
            System.out.println(s)
        );

        useAddable((x,y)->x+y);
    

    private static void useFlyable(Flyable flyable) 
        flyable.fly("风和日丽,晴空万里");
    

    private static void useAddable(Addable addable) 
        int sum = addable.add(10, 20);
        System.out.println(sum);
    

接口类参考1.4

1.6 Lambda表达式的注意事项

注意事项:

  • 使用Lambda必须要有接口,并且要求接口中有且仅有一个抽象的方法
  • 必须有上下文环境,才能推导出Lambda对应的接口
    • 根据局部变量的赋值得知Lambda对应的接口:Runnable r =() ->System.out.println(“Lambda表达式”);
    • 根据调用方法的参数得知Lambda对应的接口:new Thread(()->System.out.println(“Lambda表达式”)).start();
public interface Inter 
    void show();

public class LambdaDemo6 
    public static void main(String[] args) 

        useInter(()->
                System.out.println("Lambda表达式")
        );
        new Thread(new Runnable() 
            @Override
            public void run() 
                System.out.println("匿名内部类");
            
        ).start();

        Runnable r = () -> System.out.println("Lambda表达式");
        new Thread(r).start();

        new Thread(()->
            System.out.println("Lambda表达式")
        ).start();
    

    private static void useInter(Inter inter)
        inter.show();
    

1.7 Lambda表达式和匿名内部类的区别

所需类型不同:

  • 匿名内部类:可以是接口,也可以是抽象类,还可以是具体类
  • Lambda表达式:只能是接口

使用限制不同:

  • 如果接口中有且仅有一个抽象方法,可以使用Lambda表达式,也可以使用匿名内部类
  • 如果接口中多于一个抽象方法,只能使用匿名内部类,而不能使用Lambda表达式

实现原理不同:

  • 匿名内部类:编译之后,产生一个单独的.class字节码文件
  • Lambda表达式:编译之后,没有一个单独的.class字节码文件,对应的字节码会在运行的时候动态生成

二、接口组成更新

1.1 接口组成更新概述

接口的组成

  • 常量:public static final
  • 抽象方法:public abstract
  • 默认方法(Java 8)
  • 静态方法(Java 8)
  • 私有方法 (Java 8)

1.2 接口中默认方法

接口中默认方法得定义格式:

  • 格式:public default 返回值类型 方法名(参数列表)
  • 范例:public default void show3()

接口中默认方法的注意事项:

  • 默认方法不是抽象方法,所以不强制被重写。但是可以被重写,重写的时候去掉default关键字
  • public可以省略,default不能重写
public interface MyInterface 
    void show1();

    void show2();

    default void show3()
        System.out.println("show3");
    

public class MyInterfaceImplOne implements MyInterface
    @Override
    public void show1() 
        System.out.println("One show1");
    

    @Override
    public void show2() 
        System.out.println("One show2");
    

public class MyInterfaceImplTwo implements MyInterface
    @Override
    public void show1() 
        System.out.println("Two show1");
    

    @Override
    public void show2() 
        System.out.println("Two show2");
    

public class InterfaceDemo 
    public static void main(String[] args) 
        MyInterface myInterface = new MyInterfaceImplOne();
        myInterface.show1();
        myInterface.show2();
        myInterface.show3();
        System.out.println("------------------");
        MyInterface myInterface2 = new MyInterfaceImplTwo();
        myInterface2.show1();
        myInterface2.show2();
        myInterface2.show3();
    

运行结果:

One show1
One show2
show3
------------------
Two show1
Two show2
show3

1.3 接口中静态方法

接口中静态方法的定义格式:

  • 格式:public static 返回值类型 方法名(参数列表)
  • 范例:public static void show()

接口中静态方法的注意事项:

  • 静态方法只能通过接口名调用,不能通过实现类名或者对象名调用
  • public可以省略,static不能省略
public interface Inter 

    void show();

    default void method() 
        System.out.println("Inter 中的默认方法执行了");
    

    public static void test()
        System.out.println("Inter 中的静态方法执行了");
    

public class InterImpl implements Inter
    @Override
    public void show() 
        System.out.println("show方法执行了");
    

public class InterDemo                                            
    public static void main(String[] args)                        
        Inter inter = new InterImpl();                             
        inter.show();                                             
        inter.method();                                           
        Inter.test();                                             
                                                                 
                                                                                                        

1.4 接口中私有方法

Java 9中新增了带方法体的私有方法,这其实在Java 8中就埋下了伏笔:Java 8允许在接口中定义带方法体的默认方法和静态方法。这样可能就会引发一个问题:当两个默认方法或者静态方法中包含一段相同的代码实现时,程序必然考虑将这段实现代码抽取成一个共性方法,而这个共性方法时不需要让别人使用的,因此用私有给隐藏起来,这就是Java 9增加私有方法的必然性。

接口中私有方法的定义格式:

  • 格式1:private 返回值类型方法名(参数列表)

  • 范例1:private void show()

  • 格式2:private static 返回值类型 方法名(参数列表)

  • 范例2:private static void method()

接口中私有方法的注意事项:

  • 默认方法可以调用私有的静态方法和非静态方法
  • 静态方法只能调用私有的静态方法
public interface Inter 

    default void show1()
        System.out.println("show1开始执行");
        method();
        System.out.println("show1结束执行");
    

    default void show2()
        System.out.println("show2开始执行");
        method();
        System.out.println("show2结束执行");
    

    static void method1()
        System.out.println("method1开始执行");
        method();
        System.out.println("method1结束执行");
    

    static void method2()
        System.out.println("method2开始执行");
        method();
        System.out.println("method2结束执行");
    

    static void method()
        System.out.println("初级工程师");
        System.out.println("中级工程师");
        System.out.println("高级工程师");
    

public class InterImpl implements Inter

public class InterDemo 
    public static void main(String[] args) 
        Inter inter = new InterImpl();
        inter.show1();
        System.out.println("------------------------");
        inter.show2();

浅谈Java 8的函数式编程

  关于“Java 8为Java带来了函数式编程”已经有了很多讨论,但这句话的真正意义是什么?

  本文将讨论函数式,它对一种语言或编程方式意味着什么。在回答“Java8的函数式编程怎么样”之前,我们先看看Java的演变,特别是它的类型系统,我们将看到Java8的新特性,特别是Lambda表达式如何改变Java的风景,并提供函数式编程风格的主要优势。

 函数式编程语言是什么?

  函数式编程语言的核心是它以处理数据的方式处理代码。这意味着函数应该是第一等级(First-class)的值,并且能够被赋值给变量,传递给函数等等。

  事实上,很多函数式语言比这走得更远,将计算和算法看得比它们操作的数据更重要。其中有些语言想分离程序状态和函数(以一种看起来有点对立的方式,使用面向对象的语言,这通常会将它们联系得更紧密)。

  Clojure编程语言就是一个这样的例子,尽管它运行于基于类的Java虚拟机,Clojure的本质是函数式语言,并且在高级语言源程序中不直接公布类和对象(尽管提供了与Java良好的互操作性)。

  下面显示的是一个Clojure函数,用于处理日志,是一等公民(First-class citizen),并且不需要绑定一个类而存在。

(defn build-map-http-entries [log-file]
 (group-by :uri (scan-log-for-http-entries log-file)))

  当写在函数中的程序,对给定的输入(不论程序中的其它状态如何)总是返回相同的输出,并且不会产生其它影响,或者改变任何程序状态,这时候函数式编程是最有用的。它们的行为与数学函数相同,有时候把遵循这个标准的函数称为“纯”函数。

  纯函数的巨大好处是它们更容易推论,因为它们的操作不依赖于外部状态。函数能够很容易地结合在一起,这在开发者工作流风格中很常见,例如Lisp方言和其它具有强函数传统的语言中很普遍的REPL(Read, Execute, Print, Loop)风格。

 非函数式编程语言中的函数式编程

  一种语言是不是函数式并不是非此即彼的状态,实际上,语言存在于图谱上。在最末端,基本上是强制函数式编程,通常禁止可变的数据结构。Clojure就是一种不接受可变数据的语言。

  不过,也有一些其它语言,通常以函数方式编程,但语言并不强制这一点。Scala就是一个例子,它混和了面向对象和函数式语言。允许函数作为值,例如:

val sqFn = (x: Int) => x * x

  同时保留与Java非常接近的类和对象语法。

  另一个极端,当然,使用完全非函数式语言进行函数式编程是可能的,例如C语言,只要维持好合适的程序员准则和惯例。

  考虑到这一点,函数式编程应该被看作是有两个因素的函数,其中一个与编程语言相关,另一个是用该语言编写的程序:

  1)底层编程语言在多大程度上支持,或者强制函数式编程?

  2)这个特定的程序如何使用语言提供的函数式特性?它是否避免了非函数式特性,例如可变状态?

 Java的一些历史

  Java是一种固执己见的语言,它具有很好的可读性,初级程序员很容易上手,具有长期稳定性和可支持性。但这些设计决定也付出了一定的代价:冗长的代码,类型系统与其它语言相比显得缺乏弹性。

  然而,Java的类型系统已经在演化,虽然在语言的历史当中相对比较慢。我们来看看这些年来它的一些形式。

 Java最初的类型系统

  Java最初的类型系统至今已经超过15年了。它简单而清晰,类型包括引用类型和基本类型。类、接口或者数组属于引用类型。

  • 类是Java平台的核心,类是Java平台将会加载、或链接的功能的基本单位,所有要执行的代码都必须驻留于一个类中。

  • 接口不能直接实例化,而是要通过一个实现了接口API的类。

  • 数组可以包含基本类型、类的实例或者其它数组。

  • 基本类型全部由平台定义,程序员不能定义新的基本类型。

  从最早开始,Java的类型系统一直坚持很重要的一点,每一种类型都必须有一个可以被引用的名字。这被称为“标明类型(Nominative typing)”,Java是一种强标明类型语言。

  即使是所谓的“匿名内部类”也仍然有类型,程序员必须能引用它们,才能实现那些接口类型:

Runnable r = new Runnable() { public void run() { System.out.println("Hello World!"); } };

  换种说法,Java中的每个值要么是基本类型,要么是某个类的实例。

 命名类型(Named Type)的其它选择

  其它语言没有这么迷恋命名类型。例如,Java没有这样的Scala概念,一个实现(特定签名的)特定方法的类型。在Scala中,可以这样写:

x : {def bar : String}

  记住,Scala在右侧标示变量类型(冒号后面),所以这读起来像是“x是一种类型,它有一个方法bar返回String”。我们能用它来定义类似这样的Scala方法:

def showRefine(x : {def bar : String}) = { print(x.bar) }

  然后,如果我们定义一个合适的Scala对象:

object barBell { def bar = "Bell" }

  然后调用showRefine(barBell),这就是我们期待的事:

showRefine(barBell) Bell

  这是一个精化类型(Refinement typing)的例子。从动态语言转过来的程序员可能熟悉“鸭子类型(Duck typing)”。结构精化类型(Structural refinement typing)是类似的,除了鸭子类型(如果它走起来像鸭子,叫起来像鸭子,就可以把它当作鸭子)是运行时类型,而这些结构精化类型作用于编译时。

  在完全支持结构精化类型的语言中,这些精化类型可以用在程序员可能期望的任何地方,例如方法参数的类型。而Java,相反地,不支持这样的类型(除了几个稍微怪异的边缘例子)。

 Java 5类型系统

  Java 5的发布为类型系统带来了三个主要新特性,枚举、注解和泛型。

  • 枚举类型(Enum)在某些方面与类相似,但是它的属性只能是指定数量的实例,每个实例都不同并且在类描述中指定。主要用于“类型安全的常量”,而不是当时普遍使用的小整数常量,枚举构造同时还允许附加的模式,有时候这非常有用。

  • 注解(Annotation)与接口相关,声明注解的关键字是@interface,以@开始表示这是个注解类型。正如名字所建议的,它们用于给Java代码元素做注释,提供附加信息,但不影响其行为。此前,Java曾使用“标记接口(Marker interface)”来提供这种元数据的有限形式,但注解被认为更有灵活性。

  • Java泛型提供了参数化类型,其想法是一种类型能扮演其它类型对象的“容器”,无需关心被包含类型的具体细节。装配到容器中的类型通常称为类型参数。

  Java 5引入的特性中,枚举和注解为引用类型提供了新的形式,这需要编译器特殊处理,并且有效地从现有类型层级结构分离。

  泛型为Java的类型系统增加了显著额外的复杂性,不仅仅因为它们是纯粹的编译时特性,还要求Java开发人员应注意,编译时和运行时的类型系统彼此略有不同。

  尽管有这些变化,Java仍然保持标明类型。类型名称现在包括List(读作:“List-of-String”)和Map,CachedObject>(“Map-of-Class-of-Unknown-Type-to-CachedObject”),但这些仍然是命名的类型,并且每个非基本类型的值仍是某个类的实例。

 Java 6和7引入的特性

  Java 6基本上是一个性能优化和类库增强的版本。类型系统的唯一变化是扩大注解角色,发布可插拔注解处理功能。这对大多数开发者没有任何影响,Java6中也没有真正提供可插拔类型系统。

  Java 7的类型系统没有重大改变。仅有的一些新特性,看起来都很相似:

  • javac编译器中类型推断的小改进。

  • 签名多态性分派(Signature polymorphic dispatch),用于方法句柄(Method handle)的实现细节,而这在Java 8中又反过来用于实现Lambda表达式。

  • Multi-catch提供了一些“代数数据类型”的小跟踪信息,但这些完全是javac内部的,对最终用户程序员没有任何影响。

 Java 8的类型系统

  纵观其历史,Java基本上已经由其类型系统所定义。它是语言的核心,并且严格遵守着标明类型。从实际情况来看,Java类型系统在Java 5和7之间没有太大变化。

  乍一看,我们可能期望Java 8改变这种状况。毕竟,一个简单的Lambda表达式似乎让我们移除了标明类型:

() -> { System.out.println("Hello World!"); }

  这是个没有名字、没有参数的方法,返回void。它仍然是完全静态类型的,但现在是匿名的。

  我们逃脱了名词的王国?这真的是Java的一种新的类型形式?

  也许不幸的是,答案是否定的。JVM上运行的Java和其它语言,非常严格地限制在类的概念中。类加载是Java平台的安全和验证模式的中心。简单地说,不通过类来表示一种类型,这是非常非常难的。

  Java 8没有创建新的类型,而是通过编译器将Lambda表达式自动转换成一个类的实例。这个类由类型推断来决定。例如:

Runnable r = () -> { System.out.println("Hello World!"); };

  右侧的Lambda表达式是个有效的Java 8的值,但其类型是根据左侧值推断的,因此它实际上是Runnable类型的值。需要注意的是,如果没有正确地使用Lambda表达式,可能会导致编译器错误。即使是引入了Lambda,Java也没有改变这一点,仍然遵守着标明类型。

 Java 8的函数式编程怎么样?

  最后,让我们回到本文开头提出的问题,“Java 8的函数式编程怎么样?”

  Java 8之前,如果开发者想以函数式风格编程,他或她只能使用嵌套类型(通常是匿名内部类)作为函数代码的替代。默认的Collection类库不会为这些代码提供任何方便,可变性的魔咒也始终存在。

  Java 8的Lambda表达式没有神奇地转变成函数式语言。相反,它的作用仍是创建强制的强命名类型语言,但有更好的语法支持Lambda表达式函数文本。与此同时,Collection类库也得到了增强,允许Java开发人员开始采用简单的函数式风格(例如filter和map)简化笨重的代码。

  Java 8需要引入一些新的类型来表示函数管道的基本构造块,如java.util.function中的Predicate、Function和Consumer接口。这些新增的功能使Java 8能够“稍微函数式编程”,但Java需要用类型来表示它们(并且它们位于工具类包,而不是语言核心),这说明标明类型仍然束缚着Java语言,它离纯粹的Lisp方言或者其它函数式语言是多么的遥远。

  除了以上这些,这个函数式语言能量的小集合很可能是所有大多数开发者日常开发所真正需要的。对于高级用户,还有(JVM或其它平台)其它语言,并且毫无疑问,将继续蓬勃发展。

  via:infoq

以上是关于Java 函数式编程的主要内容,如果未能解决你的问题,请参考以下文章

Java如何支持函数式编程?

学 Java8 函数式编程

《Java8实战》读书笔记12:函数式编程

《Java8实战》读书笔记12:函数式编程

Java函数式编程原理以及应用

谈谈 Java 对函数式编程的支持。