java接口与lambda表达式

java接口与lambda表达式

接口

借口不是类,而是对类的需求(功能)的描述,凡是实现接口的类,都需要实现接口中定义的需求(方法)。例如Comparable接口,描述的功能就是比较,一个类是否可以比较大小就看它是否实现了Comparable接口。

接口中声明方法时,默认为public,因此可以不用加public关键字;但是实现的时候必须要加关键字,否则会默认protected,接着编译器会发出警告。

接口中只能描述功能(方法),不能描述概念(属性),因此接口中只有一系列public方法,没有属性,但是可以定义常量(在接口中定义的域均默认为final static、必须在声明时赋值)。

Java SE8之前不能在接口中实现方法,但是Java SE8及其之后可以在接口中提供方法的默认实现。

接口特性

  1. 接口可以用来定义指针,但是不能用来实例化(new)。

  2. 检测一个对象是否实现了某个接口可用instanceOf。

  3. 接口可以被扩展(继承)。

  4. 接口只有public方法和public static final域。

  5. 接口没有实例域。

  6. Java SE8之前,接口没有静态方法;Java SE8及其之后,接口可以提供静态方法。详细描述在后面。

  7. 一个类只能继承一个类,然而可以实现多个接口。例如一个类可以同时实现Comparable接口、Cloneable接口。

  8. Java SE8之前,接口不能实现方法;Java SE8及其之后,接口可以提供方法的默认实现,需要用default修饰。详细描述在后面。

接口与抽象类

接口与抽象类最大的区别是接口可以实现多个,而类只能继承一个,这样非常不灵活(但是C++中由于支持多继承,所以C++没有接口,而是采用抽象类的方式)。

接口静态方法

Java SE8之前,接口没有静态方法;Java SE8及其之后,接口可以提供静态方法。

在标准库中常常简单这样的例子Collection/Collections、Path/Paths、数组/Arrays等这样的接口/伴随类的搭配,后者仅仅是提供一些操作前者的静态方法。

在接口支持静态方法之后,就可以将后者的静态方法统一搬到前者接口中去,尽管这不太符合将接口作为抽象功能规范的初衷。

Java标准库中的接口并没有采用这种特性,这是因为重构整个Java库的代价太大了。但是用户却可以这么做。

默认方法

Java SE8之前,接口不能实现方法;Java SE8及其之后,接口可以提供方法的默认实现,需要用default修饰。

在某些情况下,用户只需要接口中定义的部分功能(方法),但是将接口拆分开又显得过于繁琐;比如鼠标监听器包含了左键、右键、双击等回调,然而我们很可能只需要左键单击这一个功能,按照Java SE8之前的做法,我们需要实现所有的方法(哪怕是个空的什么也不做的实现)。

有了默认方法之后,我们可以在接口中将这几个回调添加默认方法体(什么也不做),用户实现接口时就可以有选择地选择功能。

另外有些方法实现很简单,但是不可或缺,这样的方法就可以使用默认实现,而不需要让用户每次都重新实现。比如:

image-20200801093647495

这样实现了Collection的用户就不需要关心isEmpty,只需要关心size方法就行了。

超类和接口默认方法的冲突

按照Java SE8之前的做法,并不会出现这种冲突,因为接口并没有提供方法体。

Java SE8及其之后,如果超类和子类实现的接口有同名方法,或者实现的多个接口中有同名(包括方法名和参数)方法,则会发生冲突,Java中对冲突的处理如下:

  1. 超类和借口冲突:超类优先。如果子类重写了该方法,自然没有争议,如果没有重写,那么超类优先。

    由于这条规则的存在,我们不应该在接口中用默认方法重新定义Object的方法,因为就算你定义了,由于超类优先,在使用的时候仍然用的是Object提供的。

  2. 接口之间冲突:若多个接口描述了相同的方法,并且有接口(哪怕只有一个)提供了默认实现,实现类都必须自己实现该方法,否则编译器会报错。

Comparable接口

Comparable接口是一个常用的接口,他描述的功能是“比较”,实现它的类可以进行比较,进而可以进行基于比较的排序等操作。

Comparable接口在实现事可以是通过指定T来指定类型。

Comparable返回int值,在Comparable内部,当两个数进行比较的时候,尽量使用Integer.compare()、Double.compare()等方法,而不是x-y这样的方式,除非你很明确x-y这样的形式不会造成溢出

Comparable接口同equals方法一样,在继承时可能会有两种情况:

若比较概念由子类定义(子类继承改变了超类的语义),则子类定义comparableTo方法,并且在比较时添加Class对象比较的步骤(不同类抛出异常)。

若比较概念由超类提供(子类继承只改变了实现方法、没有改变语义),则超类提供compareTo方法并添加final关键字,不允许子类改变比较的语义。

假如不按照上面的方式,可能出现这种情况:A继承于B,然后A.compareTo(B)返回一个 int值,因为B引用可以转换为A,但是B.compareTo(A)可能会抛出异常,因为A引用不一定是B(可能是A的其他子类)。这不符合比较的反对称原则。

Comparator接口

对于Comparable来说,当你实现了Comparable也就意味着你的comparableTo方法已经写死了,排序时只能按照这一种规则。

对于有不同排序需求的对象来说,Comparator是一种解决方法。Comparator是一个接口,描述了一个比较器,可以为同一个对象定义多个比较器,排序时使用对应的比较器即可,Arrays.sort方法支持传入比较器。

Cloneable接口

Cloneable接口描述了克隆功能clone方法,返回一个副本。

clone方法是Object类中的protected方法,因此一般来说,不能通过对象指针调用,但是可以在子类中用super指针访问。

Object类中的clone方法默认将对象的所有域拷贝一份,是浅拷贝。因为不是所有的对象都可以克隆,所以Object默认的clone方法不会进行深度拷贝,这也是为什么Object类的clone方法设为protected的原因(如果设为public,就意味着所有的对象都可以通过对象指针调用clone方法,这是不符合某些对象的语义的)。

Cloneable接口和Object类中都有clone方法,但是Cloneable中没有默认实现,所以默认采用Object类的实现。

当想要为一个类向外提供public clone方法时,需要重写clone方法,改为public方法,并且实现Cloneable接口,如果不实现Cloneable接口的话,Object.clone方法会抛出异常(尽管类确实实现了clone方法)。

即使默认的clone方法(按域拷贝)可以满足使用需求,但仍需要重写clone为public方法,并且调用super.clone()。

lambda表达式

lambda表达式的意思是“带参数的表达式(代码块)”,本质上是一个匿名函数(方法),函数不正是带参数的表达式?

在Java中lambda表达式表达的实体是函数式接口。详细描述见后面。

很多时候我们创造一个对象,其实只是想用他的某一个方法,并非是整个对象,例如Comparator,当我们实例化一个实现了Comparator接口的对象并传入Arrays.sort中,Arrays.soft只是简单地通过comparator.compare()来调用compare方法;由于Java是面向对象的,所以必须构造一个类进行包装,略显复杂。

lambda表达式是一个可选的解决方案。

lambda语法

lambda表达式的语法:

一般例子:(String first, String second)->first.length()-second.length();

  1. 上述是一个一般的lambda表达式。

  2. 如果->后的表达式太长,无法用一条语句完成,可以像方法一样,用{}框住,并显示包含return语句

  3. 当参数为空,仍然需要一个空括号,不能不写:()->...。

  4. 如果lambda表达式的参数类型可以根据上下文推导出来,那么参数类型可以省略。比如:

    Comparator comp=(first,second)->first.length()-second.length();

  5. 如果参数只有一个,并且类型可以被推导出来,那么括号和参数类型可以同时省略。

  6. lambda表达式的返回类型不需要指定,会根据上下文进行推导。、

  7. 如果lambda表达式内部分支语句可能造成返回值类型不同,将无法进行编译(编译报错)。

函数式接口

对于只有一个抽象方法(不需要abstract关键字,只要不提供默认实现即可)的接口,当需要这种接口的对象的时候,可以通过lambda表达式生成,这样的接口叫做函数式接口。Comparator接口就是一个函数式接口。

  1. 函数式接口有且仅有一个抽象方法(非dufault)。
  2. 函数式接口可以有多个default方法。
  3. 函数式接口常在声明时加上注解@functional interface,但不是必须的。

lambda表达式可以即可理解为函数式接口的实现的简略版本,lambda表达式可以根据赋值的函数式接口类型自动推导生成相应的对象。

java.util.function

在java.util.function包中,定义了很多非常通用的函数式接口。

例如BiFunction<U,T,R>接口,描述了一个参数为U、T,返回值为R的函数。例如:

BiFunction<String, String, Integer> comp
    = (first, second) -> frst.length()-second.length();

方法引用,双冒号语法

如果lambda表达式的代码块已经存在于某个方法中,那么可以通过方法引用进行引用,进而推导出lambda表达式。

一般有以下几种引用:

  1. 类名::静态方法

    当通过函数式接口调用方法时,实际上是ClassName.staticMethod()这样调用的。

  2. 类名::实例方法

    对于这种情况,比较特殊,返回来的方法引用参数会增加一个(第一个)。增加的参数是this指针,需要认为指定相应的this(调用者)。

  3. 对象::实例方法

    实际调用是obj.method()。

  4. 类名::new

    类似于类名::静态。参数为构造器对应的参数(会自动寻找合适的构造器)。

  5. 类型[]::new

    同上。不过参数为int。

使用如下:

package com.ame;

import java.util.Arrays;

interface InterfaceA {
    void fnuc();
}

interface InterfaceB {
    void func(ClassA classA);
}

interface InterfaceC {
    ClassA func();
}

interface InterfaceD {
    int[] func(int t);
}

class ClassA {
    private int i = 0;

    public static void g() {
        System.out.println("g");
    }

    public void f() {
        System.out.println("f:" + i++);
    }

}

public class Main {
    public static void main(String[] args) throws CloneNotSupportedException {
        int i = 1;
        System.out.println("test:" + i++);
        test1();
        System.out.println("test:" + i++);
        test2();
        System.out.println("test:" + i++);
        test3();
        System.out.println("test:" + i++);
        test4();
        System.out.println("test:" + i++);
        test5();
    }

    //类名::静态方法
    public static void test1() {
        ClassA classA = new ClassA();
        InterfaceA interfaceA = null;
        interfaceA = ClassA::g;
        interfaceA.fnuc();
    }

    //类名::实例方法
    public static void test2() {
        ClassA classA = new ClassA();
        InterfaceB interfaceB = null;
        interfaceB = ClassA::f;
        interfaceB.func(classA);
        interfaceB.func(classA);
        interfaceB.func(classA);
    }

    //对象::实例方法
    public static void test3() {
        ClassA classA = new ClassA();
        InterfaceA interfaceA = null;
        interfaceA = classA::f;
        interfaceA.fnuc();
        interfaceA.fnuc();
        interfaceA.fnuc();
    }

    //类名::new
    public static void test4() {
        ClassA classA = new ClassA();
        InterfaceC interfaceC = null;
        interfaceC = ClassA::new;
        classA = interfaceC.func();
        classA.f();
        classA.f();
        classA.f();
    }

    //类型[]:new
    public static void test5() {
        InterfaceD interfaceD = null;
        interfaceD = int[]::new;
        int[] arr = interfaceD.func(3);
        System.out.println(Arrays.toString(arr));
    }
}

执行结果:

test:1
g
test:2
f:0
f:1
f:2
test:3
f:0
f:1
f:2
test:4
f:0
f:1
f:2
test:5
[0, 0, 0]

变量作用域

有时候希望lambda表达式访问,自身表达式以外的变量。

我们知道lambda表达式最后会被封装成一个对象(实现了对应的函数式接口),如果引用了自由变量(非lambda表达式自身定义),那么封装lambda表达式的时候会把这个变量也封装(复制)过去。

在lambda表达式中,只允许引用值不变的自由变量,这里的值不变有两重含义,一是被定义为final,二是没有被定义为final,但是从定义到销毁,引用值没有发生过改变(即effective final)。假如引用了值可能会发生改变的变量,当并发执行程序的时候,语义不明确。

  1. 可以引用final自由变量。
  2. 可以引用effective final自由变量。

注意1:被引用的变量在外部不能发生改变,在内部也不能。

注意2:由于this指针的值在一个方法体内是不会变的,因此lambda可以引用this指针。

例如:

package com.ame;

interface InterfaceE {
    void func();
}

public class Test {
    public static void main(String[] args) {
        final int y = 1;
        int x = 0;
        InterfaceE interfaceE = null;
        interfaceE = () -> {
            System.out.println("hello world:" + x + "." + y);
            // x++;不能在内部改变自由变量值
        };
        //x++;不能在外部改变自由变量值
        interfaceE.func();
    }
}

执行结果:

hello world:0.1

闭包

将在返回函数(即lambda表达式)中引用自由变量(外部定义)的程序结构称之为闭包。

lambda和闭包是两个东西。

在java中,lambda就是闭包。

闭包的作用是在捕获自由变量,在这里lambda表达式可以捕获外部的final(或effective final)变量,因此是闭包。

执行lambda表达式

lambda表达式最大的特点就是延迟执行,在执行了生成lambda的代码之后,并不能确定lambda的内容何时执行。

常用的函数式接口

要想接受一个lambda表达式,需要一个函数式接口,有时甚至需要提供,下面列出了Java中提供的最重要的函数式接口:

image-20200801171820264

Runnable:代表仅运行。无参数、无返回。run

Supplier:代表提供。无参数、有返回。get

Consumer:代表处理。有参数、无返回。accept

Function:代表普通函数。有参数、有返回。apply

UnaryOperator:一元操作。有参数、有返回。apply

BinaryOperator:二元操作。有参数、有返回。apply

Predicate:二值函数(布尔)。有参数、有返回。

前缀Bi代表Binary。

常用基本类型的函数式接口

另外,在利用上述函数式接口进行处理lambda表达式时,由于泛型机制,只能处理对象,而不能处理基本类型。尽管可以通过Integer、Boolean等对象使用,但是 由于频繁装箱拆箱,会带来额外的开销。

为了解决上述问题,Java提供了常用的操作基本数据类型的函数式接口:

image-20200801175909531

当需要定义lambda表达式时,最好使用上述接口。

函数式接口中的default方法

为了便于用户实现函数式接口中的抽象方法,很多函数式接口的开发者都提供了一系列default方法,辅助用户实现抽象方法;或者是静态方法,供用户引用。

posted @ 2020-08-01 18:05  燈心  阅读(684)  评论(0编辑  收藏  举报