java接口与lambda表达式
接口
借口不是类,而是对类的需求(功能)的描述,凡是实现接口的类,都需要实现接口中定义的需求(方法)。例如Comparable接口,描述的功能就是比较,一个类是否可以比较大小就看它是否实现了Comparable接口。
接口中声明方法时,默认为public,因此可以不用加public关键字;但是实现的时候必须要加关键字,否则会默认protected,接着编译器会发出警告。
接口中只能描述功能(方法),不能描述概念(属性),因此接口中只有一系列public方法,没有属性,但是可以定义常量(在接口中定义的域均默认为final static、必须在声明时赋值)。
Java SE8之前不能在接口中实现方法,但是Java SE8及其之后可以在接口中提供方法的默认实现。
接口特性
-
接口可以用来定义指针,但是不能用来实例化(new)。
-
检测一个对象是否实现了某个接口可用instanceOf。
-
接口可以被扩展(继承)。
-
接口只有public方法和public static final域。
-
接口没有实例域。
-
Java SE8之前,接口没有静态方法;Java SE8及其之后,接口可以提供静态方法。详细描述在后面。
-
一个类只能继承一个类,然而可以实现多个接口。例如一个类可以同时实现Comparable接口、Cloneable接口。
-
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之前的做法,我们需要实现所有的方法(哪怕是个空的什么也不做的实现)。
有了默认方法之后,我们可以在接口中将这几个回调添加默认方法体(什么也不做),用户实现接口时就可以有选择地选择功能。
另外有些方法实现很简单,但是不可或缺,这样的方法就可以使用默认实现,而不需要让用户每次都重新实现。比如:
这样实现了Collection的用户就不需要关心isEmpty,只需要关心size方法就行了。
超类和接口默认方法的冲突
按照Java SE8之前的做法,并不会出现这种冲突,因为接口并没有提供方法体。
Java SE8及其之后,如果超类和子类实现的接口有同名方法,或者实现的多个接口中有同名(包括方法名和参数)方法,则会发生冲突,Java中对冲突的处理如下:
-
超类和借口冲突:超类优先。如果子类重写了该方法,自然没有争议,如果没有重写,那么超类优先。
由于这条规则的存在,我们不应该在接口中用默认方法重新定义Object的方法,因为就算你定义了,由于超类优先,在使用的时候仍然用的是Object提供的。
-
接口之间冲突:若多个接口描述了相同的方法,并且有接口(哪怕只有一个)提供了默认实现,实现类都必须自己实现该方法,否则编译器会报错。
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();
-
上述是一个一般的lambda表达式。
-
如果->后的表达式太长,无法用一条语句完成,可以像方法一样,用{}框住,并显示包含return语句。
-
当参数为空,仍然需要一个空括号,不能不写:()->...。
-
如果lambda表达式的参数类型可以根据上下文推导出来,那么参数类型可以省略。比如:
Comparator comp=(first,second)->first.length()-second.length();
-
如果参数只有一个,并且类型可以被推导出来,那么括号和参数类型可以同时省略。
-
lambda表达式的返回类型不需要指定,会根据上下文进行推导。、
-
如果lambda表达式内部分支语句可能造成返回值类型不同,将无法进行编译(编译报错)。
函数式接口
对于只有一个抽象方法(不需要abstract关键字,只要不提供默认实现即可)的接口,当需要这种接口的对象的时候,可以通过lambda表达式生成,这样的接口叫做函数式接口。Comparator接口就是一个函数式接口。
- 函数式接口有且仅有一个抽象方法(非dufault)。
- 函数式接口可以有多个default方法。
- 函数式接口常在声明时加上注解@functional interface,但不是必须的。
lambda表达式可以即可理解为函数式接口的实现的简略版本,lambda表达式可以根据赋值的函数式接口类型自动推导生成相应的对象。
java.util.function
在java.util.function包中,定义了很多非常通用的函数式接口。
例如BiFunction接口,描述了一个参数为U、T,返回值为R的函数。例如:
BiFunction comp
= (first, second) -> frst.length()-second.length();
方法引用,双冒号语法
如果lambda表达式的代码块已经存在于某个方法中,那么可以通过方法引用进行引用,进而推导出lambda表达式。
一般有以下几种引用:
-
类名::静态方法
当通过函数式接口调用方法时,实际上是ClassName.staticMethod()这样调用的。
-
类名::实例方法
对于这种情况,比较特殊,返回来的方法引用参数会增加一个(第一个)。增加的参数是this指针,需要认为指定相应的this(调用者)。
-
对象::实例方法
实际调用是obj.method()。
-
类名::new
类似于类名::静态。参数为构造器对应的参数(会自动寻找合适的构造器)。
-
类型[]::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)。假如引用了值可能会发生改变的变量,当并发执行程序的时候,语义不明确。
- 可以引用final自由变量。
- 可以引用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中提供的最重要的函数式接口:
Runnable:代表仅运行。无参数、无返回。run
Supplier:代表提供。无参数、有返回。get
Consumer:代表处理。有参数、无返回。accept
Function:代表普通函数。有参数、有返回。apply
UnaryOperator:一元操作。有参数、有返回。apply
BinaryOperator:二元操作。有参数、有返回。apply
Predicate:二值函数(布尔)。有参数、有返回。
前缀Bi代表Binary。
常用基本类型的函数式接口
另外,在利用上述函数式接口进行处理lambda表达式时,由于泛型机制,只能处理对象,而不能处理基本类型。尽管可以通过Integer、Boolean等对象使用,但是 由于频繁装箱拆箱,会带来额外的开销。
为了解决上述问题,Java提供了常用的操作基本数据类型的函数式接口:
当需要定义lambda表达式时,最好使用上述接口。
函数式接口中的default方法
为了便于用户实现函数式接口中的抽象方法,很多函数式接口的开发者都提供了一系列default方法,辅助用户实现抽象方法;或者是静态方法,供用户引用。