title: 接口、lambda表达式与内部类
tag: 标签名
categories: 分类
comment: 是否允许评论(true or false)
description: 描述
top_img: https://z3.ax1x.com/2021/10/06/4xq2s1.png
cover: https://z3.ax1x.com/2021/10/06/4xq2s1.png
接口
接口概念
在Java程序设计语言中,接口不是类,而是对类的一组需求描述,这些类要遵从接口描述的统一格式进行定义。
我们经常听到服务提供商这样说:”如果类遵从某个特定接口,那么就履行这项服务。”看下面的一个示例,Arrays.sort方法承诺可以对对象数组进行排序,但要求满足下列前提:对象所属的类必须实现了Compareable接口。
public interface Compareable{
int compareTo(Object other);
}
这就是说,任何实现Compareable接口的类都需要包含compareTo方法,并且这个方法的参数必须是一个Object对象,返回一个整型数值。
上述这个接口只有一个方法,而有些接口可能包含多个方法。在接口中还可以定义常量,接口中绝不可以含有实例域,Java8之后可以在接口中提供简单方法了。但是这些方法中绝不可以引用实例域。
提供实例域和方法实现的任务应该由实现接口的那个类来完成。可以将接口看成是没有实例域的抽象类。
为了让类实现一个接口,通常需要下面两个步骤:
-
将类声明为实现给定的接口
-
对接口中的所有方法进行定义
要将类声明为实现某个接口,需要使用关键字implements:
class Employee implements Compareable
接口的特性
接口不是类,是不能使用new运算符实例化一个接口:
x = new Compare(...);//ERROR
尽管不能构造接口的对象,却能声明接口的变量:
Compareable x;// OK
接口变量必须引用实现了接口的类对象:
x = new Employee(...);
与可以建立的继承关系一样,接口也可以被扩展。这里运行在多条从具有较高通用性的接口到较高专用性的接口的链。
public interface Moveable
{
void move(double x, double y);
}
// 在它的基础上在扩展一个叫做Powered的接口:
public interface Powered extends Moveable{
double milesPerGallon();
}
虽然在接口中不能包含实例域或静态方法,但却可以包含常量
public interface Powered extends Moveable{
double milesPerGallon();
double SPEED_LIMIT = 95;
}
接口中的方法都自动地被设置为public一样,接口中的域将被自动设为public static final.
接口与抽象类
抽象类中存在这样的一个问题:每个类只能扩展一个类。假设Employee类已经扩展了一个类,例如Person,它就不能在扩展第二个类了。
class Employee extends Person,Compareable //Error
但是每个类可以像下面这样实现多个类接口:
class Employee extends Person implements Compareable // OK
静态方法
在Java SE8中,允许在接口中增加静态方法。理论上讲,没有任何理由认为这是不合法的。只是这有违于将接口作为抽象范的初衷。
到目前为止,通常的做法是将静态方法放在伴随类中。
默认方法
可以为接口方法提供一个默认实现。必须用default修饰符标记这样一个方法。
public interface Compareable<T>{
default int compareTo(T oehter){return 0;}
}
但是这样并没有太大用处,因为Compareable的每一个实际实现都要覆盖这个方法。
解决默认方法冲突
如果先在一个接口中将一个方法定义为默认方法,然后又在超类或另一个接口中定义了同样的方法。
- 超类优先。如果超类提供了一个具体方法,同名而且有相同参数类型的默认方法会被忽略。
- 接口冲突。如果一个超接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型相同的方法,必须覆盖这个方法类解决冲突。
lambda表达式
为什么引入lambda表达式
lamaba表达式是一个可传递的代码块,可以在以后执行一次或多次。
如何在指定时间间隔完成工作。将这个工作放在一个ActionListener的actionPerformed方法中:
class worker implements ActionListener{
public void actionPerformed(ActionEvent event){
// do some work
}
}
如果想要反复执行这个代码,可以构造worker类的一个实例。然后把这个实例提交到一个Timer对象。
考虑如何用一个定制比较器完成排序。如果想按长度而不是默认的字典顺序对字符串进行排序,可以向sort方法传入一个Comparator对象。
class LengthComarator implements Comparator<String>{
public int compare(String first,String second){
return first.length - second.length;
}
}
...
Arrays.sort(strings, new LengthComparator());
compare方法不是立即调用。实际上,在数组完成排序之前,sort方法会一直调用compare方法,只要元素的顺序不正确就会重新排列元素。
lambda表达式的语法
(String first,String second)
-> first.length() - second.length();
// 这个就是第一个lambda表达式。lambda表示式就是一个代码块,以及必须传入代码的变量规范。
一种lambda表达式形式:参数,箭头以及一个表达式。如果代码要完成的计算无法放在一个表示式中,就可以像写方法一样,把这些代码放在{}中,并且包含显式的retrun语句。如
(String first, String second)->{
if(first.length() < second.length())return -1;
else if(first.length() > second.length())return 1;
else return 0;
}
即使lambda表示式没有参数,仍然要提供空括号,就像无参数方法一样。
()-> {for (int i = 100; i>=0; i++)
System.out.println(i);
}
如果可以推导出一个lambda表达式的参数类型,则可以忽略其类型。
Comparator<String> comp
=(first, second) // same as (String first,String second)
-> first.length() - second.length();
无须指定lambda表达式返回的类型。lambda表达式的返回类型总是会由上下文推导出。
(String first, String second) -> first.length() - second.length();
函数式接口
Java中有很多封装代码块的接口,如ActionListener或Comparator。lambda表达式与这些接口是兼容的。
对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供给一个lambda表达式。这种 接口称为函数式接口。
为了展示如何转换为函数式接口,下面考虑Arrays.sort方法。它的第二个参数需要一个Comparator实例,Comparator就是只有一个方法的接口,所以可以提供一个lambda表达式:
Arrays.sort(words,(first,second) -> first.legnth() - second.legnth());
在底层,Arrays,sort方法会接收实现了Comparator的某个类的对象。在这个对象上调用compare方法会执行这个lambda表达式的体。
方法引用
有时,lambda表达式涉及一个方法。如果你希望只要出现一个定时器事件就打印这个事件对象。当然,为此可以调用:
var timer = new Timer(1000, event -> System.out.println(event));
如果直接把println方法传递到Timer构造器就更好了,具体如下:
var timer = new Timer(1000, System.out::println);
// 表示式System.out::println是一个方法引用,它指示编译器生成一个函数式接口的实例,覆盖这个接口的抽象方法来调用给定的方法。
// 在这个例子中,会生成一个ActionListener,它的actionPerformed(ActionEvent e)方法调用System.out.println(e);
假设你想对字符串进行排序,而不考虑字母的大小写。可以传递以下方法表达式:
Arrays.sort(strings,String::compareToIgnoreCase);
要想::运算分割方法名与对象或类名。主要由3种情况:
-
object::instanceMethod //在第1种情况下,方法引用等价于向方法传递参数的lambda表示式。对于Sytem.out::println,对象是System.out,所以 Class::instanceMethod //对于第2种情况,第1个参数会成为方法的隐式参数。例如,String::compareToIgnoreCase等同于(x,y)->x.compareToIgnoreCase(y); Class::staticMethod //对于第3中情况,所以参数都传递到静态方法:Math::pow等价于(x,y)->Math.pow(x,y);
注意,只有当lambda表达式的体只调用一个方法而不做其他操作时,才能把lambda表达式重写为方法引用。以下的表达式就不可以:
s->s.length() == 0;
// 这里有一个方法调用。但是还有一个比较,因此这里不能使用方法引用。
可以在方法引用中使用this参数。如,this::equals等同于x->this.equals(x).使用super也是合法的。下面的方法表达式
super::instanceMethod
// 使用this作为目标,会调用给定方法的超类版本。
构造器引用
构造器引用和方法引用很相似,只不过方法名为new.如,Person::new是Person构造器的一个引用。假设你有一个字符串列表。可以把它转换为一个Person对象数组,为此要在各个字符串上调用构造器,调用如下:
ArrayList<String> names = ...;
Stream<Person> stream = names.stream().map(Person::new);
List<Person> people = stream.collect(Collectors.toList());
可以用数组类型来建立构造器引用。如,int[]::new 是一个构造器引用,它有一个参数,即数组的长度。这等价于表达式x->new int[x];
假设我们需要一个Person对象数组。Stream接口有一个Array方法可以返回Object数组:
Object[] people = stream.toArray();
用户希望得到一个Person引用数组,而不是Object引用数组。流库利用构造器引用解决了整个问题。可以把Person[]::new传入toArray方法:
Person[] person = stream.toArray(Person::new);
// toArray方法调用这个构造器来得到一个有正确类型的数组。然后填充并返回这个数组。
变量作用域
下面这个例子:
public static void repeatMessage(String text, int delay){
ActionListener listener = event ->{
System.out.println(text);
Toolkit.getDefaultToolkit().beep();
};
new Timer(delay,listener).start();
}
现在来看lambda表达式中的变量text.注意这个参数并不是lambda表达式中定义的。这是repeatMessage方法的一个参数变量。这里有一个小问题,lambda表达式的代码可能在repeatMessage方法调用很久之后才开始执行,而那个时候参数变量已经不存在了。如何保留text变量呢?
我们知道lambda表达式中有3个部分:一个代码块,参数,自由变量的值,这里指非参数而且不在代码中定义的量。在上述例子中,有一个text变量,我们说它被lambda表达式捕获。
在lambda表达式中,只能引用值不会改变的变量。
public static void countDown(int start, int delay){
ActionListener listener = event ->{
start--; // Error
System.out.println(start);
};
new Timer(delay,listener).start();
}
如果在lambda表达式中更改变量,并发执行多个动作时就会不安全。
如果在lambda表达式中引用一个变量,而这个变量可能在外边改变,这也是不合法的
public static void repeat(String text, int count){
for (int i = 1; i < count; i++) {
ActionListener listener = event ->{
System.out.println(i+":"+text); // Error
};
new Timer(1000,listener).start();
}
}
这里有一条规则:lambda表达式中捕获的变量必须实际上是事实最终变量。事实最终变量是指,这个变量初始化后就不会再为它赋新值。
处理lambda表达式
使用lambda表达式的重点是延迟执行。毕竟,如果想要立即执行代码,完全可以直接执行,而无须把它包装在一个lambda表达式中。 之所以希望以后再执行代码,这有很多原因,如:
- 在一个单独的线程中运行代码
- 多次运行代码
- 在算法的适当位置运行代码
- 发生某种情况时执行代码(如,点击了一个按钮,数据到达,等等)
- 只在必要时才允许代码
假设你想要重复一个动作n次。将这个动作和重复次数传递到一个repeat方法:
repeat(10,()->System.out.println("Hello,World!"));
要接受这个lambda表达式,需要选择一个函数式接口。在这里我们可以使用Runnable接口。
public static void repeat(int n, Runnable action){
for (int i = 0; i < n; i++) {
action.run();
}
}
// 调用action.run()时会执行这个lambda表达式的主体。
现在我们希望告诉这个动作它出现在哪一次迭代中。为此需要选择一个合适的函数式接口,其中要包含一个方法,这个方法有一个int参数而且返回类型为void.处理int值的标准接口如下:
public interface IntConsumer{
void accept(int value);
}
public static void repeat(int n,IntConsumer action){
for (int i = 0; i < n; i++) {
action.accept(i);
}
}
内部类
内部类是定义在另一个类中的类。为什么需要使用内部类,主要有两个原因:
- 内部类可以对同一个包中的其他类隐藏
- 内部类方法可以访问定义这个类的作用域中的数据,包括原本私有数据。
使用内部类访问对象状态
public class TalkingClock{
private int interval;
private boolean beep;
public TalkingClock(int interval,boolean beep){
// ...
}
public void start(){
//...
}
public class TimePrinter implements ActionListener{
public void actionPerformed(ActiveEvent event){
System.out.println("At the tone,the time is"+ Instant.ofEpochMilli(event.getWhen()));
if (beep) Toolkit.getDefaultToolkit().beep();
}
}
}
// 我们发现TimePrinter类没有实例字段或者名为beep的变量,实际上,beep指示TalkingClock对象中创建这个TimePrinter的字段。可以看打一个内部类的方法可以访问自身的数据字段,也可以访问创建它的外围类对象的数据字段。
我们将外围内部类对象的引用称为outer.
public void actionPerformed(ActiveEvent event){
System.out.println("At the tone,the time is"+ Instant.ofEpochMilli(event.getWhen()));
if (outer.beep) Toolkit.getDefaultToolkit().beep();
}
内部类的特殊语法规则
我们解析了内部类有一个外围类的引用,我们把它叫做outer。事实上,使用外围类引用的正规语法还有复杂一些。表达式
OuterClass.this
表示外围类引用。
public void actionPerformed(ActiveEvent event){
...
if (TalkingClock.this.beep) Toolkit.getDefaultToolkit().beep();
}
由外部方法访问变量
与其他内部类相比较,局部类有一个优点。它不仅能够访问外部类的字段,还可以访问局部变量!但是这些局部变量必须是事实最终变量。也就是说,它们一旦被赋值就绝对不会改变。
匿名内部类
使用匿名内部类,通常还可以再进一步,如果只想创建这个类的一个对象,甚至不需要为类指定名字。
public void start(int interval, boolean beep){
var listener = new ActionListener{
public void actionPerformed(ActionEvent event){
System.out.println("At the tone,the time is"+ Instant.ofEpochMilli(event.getWhen()));
if(beep)Toolkit.getDefaultToolkit().beep();
}
};
var timer = Timer(interval,listener);
timer.start();
}
该语法的含义是这样的,创建一个类的新对象,这个类实现了ActionListener接口,需要实现的方法actionPerformed在括号{}内定义
一般地,语法如下:
new SuperType(construction parameters){
inner class methods and data
}
// superType可以是接口,也可以是一个类,如果是一个接口,内部类就要实现这个接口。如果是一个类,内部类就要扩展这个类。
由于构造器的名字必须与类名相同,而匿名内部类是没有类名的,所以,匿名内部类不能有构造器。构造参数要传递给超类构造器。只要内部类实现一个接口,就不能有任何构造参数。但是仍然可以提供一组小括号。
new interfaceType(){
methods and data
}
那个构造一个类的新对象与构造一个扩展了那个类的匿名内部类的对象之间有什么差别。
var queen = new Person("Mary");
var count = new Person("Mary"){
.....
}
// 如果构造参数列表的结束小括号后面跟一个开始大括号,就是在定义匿名内部类。
静态内部类
有时候,使用内部类只是为了把一个类隐藏在另外一个类的内部,并不需要内部类有外围类对象的一个引用。为此,可以将内部类声明为static,这样就不会生成那个引用。
下面是一个典型的例子:计算数组中的最小值和最大值。一个方法用于计算最小值,一个方法用于计算最大值。在调用这两个方法的时候,数组被遍历两次。如果只遍历数组一次,并且能够同时计算出最大值和最小值,就大大提高了效率。
double min = Double.POSITIVE_INFINITY;
double max = Double.NEGATIVE_INFINITY;
for(double v : values){
if(min > v) min =v;
if(max < v) max =v;
}
// 这个方法必须返回两个数,定义一个包含两个值的类Pair:
class Pair{
private double first;
private double second;
public Pair(double f,double s){
first = f;
second = s;
}
public double getFirst(){
return first;
}
public double getSecond(){
return second;
}
}
// minmax方法可以返回一个Pair类型的对象
class ArrayAlg{
public static Pair minmax(double[] values){
...
return new Pair(min,max);
}
}
// 这个方法的调用者可以使用getFirst和getSecond方法获得答案;
Pair p = ArrayAlg.minmax(d);
System.out.println("min =" + p.getFirst());
System.out.println("min =" + p.getSecond());
在Pair对象中不需要任何其他对象的引用,为此,可以将这个内部类声明为static,从而不生成那个引用。
class ArrayAlg{
public static class Pair{
...
}
}
只有内部类可以声明为static.静态内部类就类似于其他内部类,只不过静态内部类的对象没有生成它的外围类对象的引用。
具体代码实现:
public class StaticInnerClassTest {
public static void main(String[] args) {
double[] doubles = new double[20];
for (int i = 0; i <doubles.length ; i++) {
doubles[i] = 100 * Math.random();
}
ArrayAlg.Pair p = ArrayAlg.minmax(doubles);
System.out.println(p.getFirst());
System.out.println(p.getSecond());
}
}
/**
* 文件描述
*
* @Author: QJS
* @CreateDate: 2021/12/17 13:27
**/
public class ArrayAlg {
public static class Pair{
private double first;
private double second;
public Pair(double first, double second) {
this.first = first;
this.second = second;
}
public double getFirst() {
return first;
}
public double getSecond() {
return second;
}
}
public static Pair minmax(double[] values){
double min = Double.POSITIVE_INFINITY;
double max = Double.NEGATIVE_INFINITY;
for (double v : values){
if(min > v) min = v;
if(max < v) max = v;
}
return new Pair( min, max);
}
}
代理
利用代理可以在运行时创建实现了一组给定接口的新类。只有在编译时期无法确定需要实现哪个接口时才有必要使用代理。
如何使用代理
假设你想构造一个类的对象,这个类实现了一个或多个接口,但是在编译时你可能并不知道这些接口是什么。这个问题确实有些难度,要想构造一个具体的类,只需要使用newInstance方法或者使用反射找出构造器。但是,不能实例化接口。需要在运行的程序中定义一个新类。
为了解决这个问题,有些程序会生成代码,将这些代码放在一个文件中,调用这些编译器,然后在加载得到的类文件,很显然这样速度就很慢,并且需要将编译器和程序一起部署。而代理机制则是一种更好的解决方案。代理类可以在运行时创建全新的类。这样的代理类能够实现你指定的接口。具体地,代理类包含以下方法:
- 指定接口所需要的的全部方法
- Object类中的全部方法,如toString 、equals等。
在运行时不能为这些方法定义新代码。实际上,必须提供一个调用处理器(invocation handle)。调用处理器是实现了InvocationHandle接口的类的对象。这个接口只有一个方法:
Object invoke(Object proxy, Method method,Object[] args);
无论何时调用代理对象的方法,调用处理器的invoke方法都会被调用,并向其传递Method对象和原调用的参数。之后调用处理器必须确定如何处理这个调用。
创建代理对象
要想创建一个代理对象,需要使用Proxy类的newProxyInstance方法。这个方法有三个参数:
- 一个类加载器。作为Java安全模型的一部分,可以对平台和应用类、从因特网下载的类等送不同的类加载器。
- 一个Class对象数组,每个元素对应需要实现的各个接口
- 一个调用处理器
如何定义处理器?对于得到的对象能够做些什么?这个答案取决于我们想要用代理机制解决什么问题。使用代理可能出于很多目的,如
- 将方法调用路由到远程服务器
- 在运行的程序中将用户界面事件与动作关联起来
- 为了调试,跟踪方法调用
我们要使用代理和调用处理器跟踪方法调用。我们定义一个TranceHandle包装类存储包装的对象,其中的invoke方法会打印所调用方法的名字和参数。随后用包装的对象作为隐式参数调用这个方法。
class TraceHandle implements InvocationHandle{
private Object target;
public TranceHandler(Object t){
target = t;
}
public Object invoke(Object proxy,Method m,Object[] args)throw Throwable{
// print method name and parameters
...
// invoke actual method
return m.invoke(target,args);
}
}
下面说明如何构造可以跟踪方法调用的代理对象
Object value = ...;
var handler = new TraceHandler(value);
// construct proxy for one or more interfaces
var interfaces = new Class[]{Comparable.class};
Object proxy = Proxy.newProxyInstance(
ClassLoader.getSystemClassLoader(),
new Class[] {Comparable.class},handler;
)
// 现在只要在proxy上调用了某个接口的方法,就会打印打印这个方法的名字和参数,之后再用value调用这个方法。
代理类的特性
现在,我们已近看到了代理类的应用,接下来了解它们的一些特性。需要记住,代理类时在程序运行过程中动态创建的。一旦被创建,它们就变成了常规类,与虚拟机中的任何其他类没有什么区别。
所有的代理类都扩展Proxy类。一个代理类只有一个实例字段—即调用处理器,它在Proxy超类中定义。完成代理对象任务所需要的任何额外数据都必须存储在调用处理器中。
所有的代理类都要覆盖Object类的toString,equals和hashCode方法。
对于一个特定的类加载器和预设的一组接口来说,只能有一个代理类。也就是说。如果使用同一个类加载器和接口数组调用两次newProxyInstance方法,将得到同一个类的两个对象。
代理类总是public和final。如果代理类实现的所有接口都是public,这个代理类就不属于任何特定的包;否则,所有非公共的接口都必须属于同一个包。
API
Object invoke(Object proxy,Method method,Object[] args)
// 定义这个方法包含一个动作,你希望只要在代理对象上调用一个方法就完成这个动作
static Class<?> getProxyClass(ClassLoader loader,Class<?>...interfaces)
// 返回指定接口的代理类
static Object newProxyInstacnce(ClassLoader loader,Class<?>[] interfaces,InvocationHandler handler)
// 构造实现指定接口的代理类的一个新实例。所有方法都调用给定处理器对象的invoke方法。‘
static boolean isProxyClass(Class<?> cl)
// 如果cl是一个代理类则返回true.
,它在Proxy超类中定义。完成代理对象任务所需要的任何额外数据都必须存储在调用处理器中。
所有的代理类都要覆盖Object类的toString,equals和hashCode方法。
对于一个特定的类加载器和预设的一组接口来说,只能有一个代理类。也就是说。如果使用同一个类加载器和接口数组调用两次newProxyInstance方法,将得到同一个类的两个对象。
代理类总是public和final。如果代理类实现的所有接口都是public,这个代理类就不属于任何特定的包;否则,所有非公共的接口都必须属于同一个包。
API
Object invoke(Object proxy,Method method,Object[] args)
// 定义这个方法包含一个动作,你希望只要在代理对象上调用一个方法就完成这个动作
static Class<?> getProxyClass(ClassLoader loader,Class<?>...interfaces)
// 返回指定接口的代理类
static Object newProxyInstacnce(ClassLoader loader,Class<?>[] interfaces,InvocationHandler handler)
// 构造实现指定接口的代理类的一个新实例。所有方法都调用给定处理器对象的invoke方法。‘
static boolean isProxyClass(Class<?> cl)
// 如果cl是一个代理类则返回true.