版权声明:本文为博主ExcelMann的原创文章,未经博主允许不得转载。
第6章 接口、lambda表达式与内部类
作者:ExcelMann,转载需注明。
第6章内容目录:
- 接口
- lambda表达式
- 内部类
- 服务加载器
- 代理
一、接口
1、接口的概念
- 接口不是类,而是对希望符合这个接口的类的一组需求。
比如,Arrays类中的sort方法承诺可以对对象数组进行排序,但是要求对象所属的类必须实现Comparable接口(这样,该对象所属的类保证有接口中的compareTo方法,保证sort方法可以调用该compareTo方法); - 接口中的所有方法都自动是public方法,因此在接口中声明方法时,不必提供关键字public;
接口中的字段总是public static final; - 接口绝不会有实例字段;也不会实现方法(但是会有默认方法);
- 类实现一个接口,需要完成下面两个步骤:
1)将类声明为实现给定的接口;
2)对接口中的所有方法提供定义; - compareTo方法在继承中会出现问题:当Manager继承了Employee,而Employee实现的是Comparable<Employee>,而不是Comparable<Manager>。当执行以下代码时会出现异常。
解决方案(类似于equals):
1)如果不同子类中的比较有不同的含义,就应该将属于不同类的对象之间的比较规定为非法;
2)如果存在一个能够比较子类对象的通用算法,那么可以在超类中提供一个compareTo方法,并将这个方法声明为final;
class Manager extends Employee{
public int compareTo(Employee e){
Manager manager = (Manager)e; //会出现ClassCastException
}
}
2、接口的属性
- 虽然不能实例化接口,但是可以声明接口的变量,引用实现了这个接口的类对象:
Comparable x = new Employee();
接下来可以使用instanceof检查一个对象是否实现了某个特定的接口;
if(anObject instanceof Comparable){…}
3、静态和私有方法
- 在Java8中,允许在接口中增加静态方法。(有违于将接口作为抽象规范的初衷)
- 在Java9中,接口中的方法可以是private;
4、默认方法
- 可以为接口方法提供一个默认实现(实现该接口的类可以不用定义该方法)。必须用default修饰符标记这样一个方法;
public interface Comparable<T>{
default int compareTo(T other){return 0;}
}
- 默认方法的用法(好处):
1)例如,对于Iterator接口,用于访问数据结构中的元素,这个接口中声明了一个默认的remove方法,只有当可写的迭代器实现该接口时,才需要定义该方法,而对于只读的迭代器实现该接口时,可以不用操心实现该方法;
default void remove(){…}
2)默认方法可以调用其他方法。例如,Collection接口定义一个便利方法:
3)“接口演化”的用法。以Collection接口为例,这个接口作为Java的一部分已经有很多年了。假设以前提供了这样一个类:
public class Bag implements Collection
后来,在Java8中,又为该接口增加了一个stream方法,如果该方法是默认的,那么Bag类可以正常编译,另外如果重新编译而加载这个类,并在一个Bag实例上调用stream方法,将调用Collection.stream方法;
2)
public interface Collection{
int size(); //public的抽象方法,由实现类定义
default boolean isEmpty(){return size()==0;}
//这样实现Collection的程序员就不用担心实现isEmpty方法了;
}
5、解决默认方法冲突
如果在接口中将一个方法声明为默认方法,并且与继承的超类或另一个接口中定义的方法相同,会发生什么情况?
- 超类有限(类优先原则):如果超类提供了一个具体方法,同名而且有相同参数类型的默认方法会被忽略;
- 接口冲突:不论是两个接口都默认实现了同个签名的方法,还是至少有一个接口默认实现了,编译器都会报告错误,程序员就必须解决这个二义性;
class Student implements Person,Named{
public String getName(){return Person.super.getName();}
}
6、接口与回调
这节将通过接口和回调,实现定时器和动作监听器的配合。
- 回调:在这种模式中,可以指定某个特定事件发生时应该采取的动作;
- 定时器:Timer构造器的第一个参数是一个时间间隔(单位是毫秒),即经过多长时间通知一次。第二个参数是监听器对象;
- 如何告诉告诉定时器要做什么?在Java中,通过向定时器传入某个类的对象,然后,定时器调用这个对象的方法;
- 定时器要求传递的对象所属的类实现了java.awt.event包的ActionListener接口。当到达指定的时间间隔时,定时器会调用监听器的actionPerformed方法;
public interface ActionListener{
void actionPerformed(ActionEvent event);
//注意ActionEvent参数,这个参数提供了事件的相关信息,例如,发生这个事件的时间;
}
7、Comparator接口
前面了解到可以通过实现Comparable接口,定义该接口的compareTo方法,从而与Arrays.sort搭配实现对象数组的排序。
另外,Array.sort方法还有第二个版本,用一个数组和比较器(comparator)作为参数,比较器是实现了Comparator接口的类的实例。
public interface Comparator<T>{
int compare(T first,T second);
}
//构造一个比较器
class LengthComparator implements Comparator<String>{
public int compare(String first,String second){
return first.length()-second.length();
}
}
//对数组进行排序
String[] friends={...};
Arrays.sort(friends, new LengthComparator());
8、对象克隆
- 浅拷贝:Object类的clone方法克隆一个对象时,并没有克隆对象中引用的其他对象;
影响:当原对象和浅克隆对象共享的子对象是不可变的,那么这种共享是安全的。或者,在对象的生命期中,子对象一直包含不变的常量,没有更改器方法会改变它,也没有方法会生成它的引用,这种情况也是安全的; - 深拷贝:可以通过重新定义clone方法建立深拷贝,即同时克隆所有子对象(若是不可变的,那么这种共享算安全);
class Employee implements Cloneable{
public Employee clone() throws CloneNotSupportedException{
Employee cloned = (Employee) super.clone();
cloned.hireDay = (Date)hireDay.clone(); //同时克隆子对象
return cloned;
}
}
- 标记接口:标记接口不包含任何方法,它唯一的作用就是允许在类型查询中使用instanceof;
- Cloneable接口就是标记接口。它没有指定clone方法,这个方法是从Object类继承的。不过,如果一个对象请求克隆,但是没有实现这个接口,就会生成一个检查型异常;
- 重新定义clone方法:当默认的clone方法不满足要求,或者你希望实现深拷贝,亦或是希望在其他类也能克隆自身类对象,那么可以在自身类中重新定义clone方法(为public);
class Employee implements Cloneable{
public Employee clone() throws CloneNotSupportedException{
return (Employee) super.clone();
}
}
- 必须当心子类的克隆。例如,一旦为Employee类定义了clone方法,任何人都可以用它来克隆Manager对象;
注意:子类覆盖父类的clone方法,方法中调用super.clone返回的是子类类型的对象; - 所有数组类型都有一个公共的clone方法,而不是受保护的!
二、lambda表达式
1、为什么引入lambda表达式
在Java中,想要传递代码段,必须构造一个对象,这个对象的类需要有一个方法包含所需的代码。
lambda表达式是一个可传递的代码块,可以在以后执行一次或者多次。
2、lambda表达式的语法
- lambda表达式,就是一个代码块,以及必须传入代码的变量规范。例子如下:
(String first, String second) //方法参数
-> first.length()-second.length() //方法执行代码,可用{代码块}
- 即使lambda表达式没有参数,仍然要提供空括号,就像无参数方法一样:
()->{for(int i=100;i>=0;i--) System.out.println(...);}
- 如果可以推导出一个lambda表达式的参数类型,则可以忽略其类型。例如:
//这里将lambda表达式赋值给一个字符串比较器(称作函数式接口)
Comparator<String> comp
= (first,second)
-> first.length()-second.length();
- 如果方法只有一个参数,而且这个参数的类型可以推导得出,那么还可以省略小括号:
ActionListener listener = event-> //event省略小括号
System.out.println(...);
- 无须指定lambda表达式的返回类型。它总是会由上下文推到得出。例如下面的表达式,可以在需要int类型结果的上下文中使用:
(String first, String second) //方法参数
-> first.length()-second.length()
- 如果一个lambda表达式只在某些分支返回一些值,而另外一些分支不返回值,这是不合法的。例如:(int x)->{if(x>=0) return 1;}不合法;
3、(★)函数式接口
- 函数式接口:对于只有一个抽象方法的接口,当需要这种接口的对象时,就可以提供一个lambda表达式。这种接口称为函数式接口;
如:
//这里的(first,second)->first.length()-second.length(),代替了一个Comparator接口对象
Arrays.sort(planets, (first,second)->first.length()-second.length());
- 解释:在上面的例子中,实质上在底层,Arrays.sort方法的第二个参数会接收实现了Comparator<String>的某个类的对象。而在这个对象上调用compare方法(也就是函数式接口的唯一抽象方法)时会反过来执行lambda表达式的体;
- 所以说,lambda表达式可以转换为函数式接口;
- 下面介绍两个比较有用的函数式接口:
1)java.util.function包中的Predicate接口(例子中用于ArraiyList类的removeIf方法):
2)供应者Supplier<T>接口。该接口没有参数调用的时候会生成一个T类型的值,主要用于实现懒计算;
1)
public interface Predicate<T>
{
boolean test(T t); //唯一的抽象方法
...
}
//removeIf方法的参数就是Predicate,该接口专门用来传递lambda表达式
list.removeIf(e -> e == null); //该语句将从一个数组列表删除所有null值
2)
public interface Supplier<T>
{
T get();
}
//下面这个不是最优的,因为我们预计day很少为null,所以希望在必要时才构造默认的LocalDate对象(实现懒计算)
//requireNonNullOrElseGet方法只有在需要值时才调用供应者
//LocalDate hireDay = Objects.requireNonNullOrElseGet(day, new LocalDate(1970,1,1));
LocalDate hireDay = Objects.requireNonNullOrElseGet(day, ()->new LocalDate(1970,1,1));
4、方法引用
- 方法引用:例如表达式System.out::println是一个方法引用,它指示编译器生成一个函数式接口的实例,覆盖这个接口的抽象方法来调用给定的方法;
例如:
//lambda表达式的写法
var timer = new Timer(1000, event -> System.out.println(event));
//方法引用的写法
//这里System.out是对象,println是调用的方法,参数为函数式接口唯一抽象方法的参数
var timer = new Timer(1000, System.out::println);
//这里会生成一个ActionListener实例,它的actionPerformed(ActionEvent e)方法要调用System.out.println(e);
- lambda表达式与方法引用的区别:1)方法引用类似于lambda表达式,不是一个对象。但是,为一个类型为函数式接口的变量赋值时会生成一个对象;2)考虑一个方法引用,如separator::equals,如果separator为null,构造方法引用的时候会立即抛出空指针异常,而对于lambda表达式x-> separator.equals(x),只有在调用时才抛出空指针异常;
- (★)方法引用的几种类型:
1)object::instanceMethod:例如,对于System.out::println,对象是System.out,所以方法表达式等价于x->System.out.println(x),其中x为函数式接口方法的参数;
2)Class::instanceMethod:这种情况下,第1个参数会成为方法的隐式参数。例如,String::compareToIgnoreCase等同于(x,y)->x.compareToIgnoreCase(y);
3)Class::staticMethod:这种情况下,所有参数都传递到静态方法作为参数。例如,Math::pow等价于(x,y)->Math.pow(x,y);
4)Class::new:构造器引用,lambda参数传到这个构造器。例如,Integer::new,相当于x-> new Integer(x);
5)Class[]::new:数组构造器引用,lambda参数是数组长度。例如,Integer[]::new,相当于n->new Integer[n]; - (★)注意:只有当lambda表达式的体只调用一个方法而不做其他操作时,才能将lambda表达式重写为方法引用!!
- 可以在方法引用中使用this参数和super参数。例如,this::equals等同于x->this.equals(x),或者super::greet等同于x->super.greet(x);
5、构造器引用
- Class::new:构造器引用,lambda参数传到这个构造器。例如,Integer::new,相当于x-> new Integer(x)。至于调用哪一个构造器,取决于参数;
6、变量作用域
- lambda表达式的3个部分:参数、一个代码块、自由变量的值(这里指非参数而且不在代码中定义的变量);
- lambda表达式可以捕获外围作用域变量的值。
例如:
public static void repeatMessage(String text){
ActionListener listener = event->
{
System.out.println(text); //text就是外围作用域变量
...
}
}
- 注意,lambda表达式中捕获的变量必须实际上是事实最终变量(即不会改变值的变量):
1)在lambda中,只能引用值不会改变的变量。因为如果在lambda表达式中更改变量,并发执行多个动作时就会不安全;
2)如果引用一个变量,而这个变量可能在外部改变,这也是不合法的; - lambda表达式的体与嵌套块有相同的作用域;
- 在lambda表达式中使用this关键字时,是指创建这个lambda表达式的方法的this参数。例如:
public class Application{
ActionListener listener = event ->{
System.out.println(this.toString());//这里会调用Application对象的toString方法
}
}
7、处理lambda表达式
-
使用lambda表达式的重点是延迟执行;
-
延迟执行的原因:
1)在一个单独的线程中运行代码;
2)多次运行代码;
3)在算法的适当位置运行代码(例如排序中的比较操作);
4)发生某种情况时执行代码(例如点击按钮);
5)只在必要时执行代码; -
常用的函数式接口:
-
注意:如果设计自己的接口,其中只有一个抽象方法,可以用@FunctionalInteface注解标记这个接口,作为函数式接口;
8、再谈Comparator
这节内容看不太懂。。。以后再补;
三、内部类
内部类的概念:内部类是定义在另一个类中的类。
使用内部类的原因主要有两个:
1)内部类可以对同一个包中的其他类隐藏;
2)内部类方法可以访问定义这个类的作用域中的所有数据,包括原本私有的数据;
1、使用内部类访问对象状态
- 内部类的对象总有一个隐式引用(除了静态内部类),指向创建它的外部类对象(一般称为outer,注意outer不是Java的关键字,只是用于说明内部类机制);(这个引用在内部类的定义中是不可见的)
- 编译器会自动修改所有的内部类构造器,添加一个对应外围类引用的参数(也是不可见的);
- 在外围类的方法中,如果构造了内部类的一个对象,编译器就会自动(不可见)将当前外围类的this引用传递给这个构造器;
2、内部类的特殊语法规则
- 表达式OuterClass.this表示外围类引用;
例如,可以像下面这样编写TimePrinter内部类的actionPerformed方法:
public void actionPerformed(ActionEvent event){
...
if(TimePrinter.this.beep) //这里的TimePrinter.this表示外围类引用
...
}
- 采用以下语法更加明确的编写内部类对象的构造器:
outerObject.new InnerClass(construction parameters)
(其中outerObject可以替换为任意外围类对象)
//当在外围类内部时
ActionListener listener = this.new TimePrinter();//this是当前外围类对象,一般省略
//当在外围类的作用域之外时
var jabberer = new TalkingClock();//首先构建外围类对象
TalkingClock.TimePrinter listener = jabberer.new TimePrinter();//jabberer是外围类对象
- 注意:在外围类的外部,可以这样引用内部类:
OuterClass.InnerClass - 注意:内部类中声明的所有静态字段都必须是final,并初始化为一个编译时常量;(如果这个字段不是一个常量,就可能不唯一)
- 注意:内部类不能有static方法;
3、内部类是否有用、必要和安全
通过增加看似精巧有趣、实属没必要的特性,Java是不是也开始走上了让许多语言保守折磨的毁灭性道路?
书中提到不打算就这个问题给出一个完备的答案。
相对于内部类,我认为lambda表达式会更加的简洁,而且用多了也会很熟悉,所以我暂时也跟作者持有相同意见。
- 内部类是一个编译器现象,与虚拟机无关。
编译器将会把内部类转换为常规的类文件,用$(美元符号)分隔外部类名和内部类名,而虚拟机则对此一无所知; - 内部类是如何获得额外的访问权限的?(可以访问外围类的私有数据)
对于TalkingClock类,采用反射机制进行分析,有以下结果:
可以看到,其中有个静态方法access$0,它将返回作为参数传递的那个对象的beep字段。
所以当内部类方法调用if(beep),实际上是调用if(TalkingClock.access$0(outer))
Class TalkingClock{
private int interval;
private boolean beep;
public TalkingClock(int,boolean);
static boolean access$0(TalkingClock);//该静态方法名可能稍有不同,也可能是access$000,取决于你的编译器
public void start();
}
4、局部内部类
按我的理解的话,局部内部类的目的和匿名内部类的原因是类似的,都是因为只利用创建的内部类少次。
区别在于:
对于匿名内部类(尤其是实现了接口的内部类),它确实是只能创建一次对象;
而对于局部内部类,它是作用域限定于当前块中,在该方法中还是可以创建多个对象的;
局部内部类的例子(将内部类放在某个块中):
public void start(){
class TimePrinter implements ActionListener{
public void actionPerformed(ActionEvent event){...}
}
var listener = new TimePrinter();
...
}
- 声明局部类时不能有访问说明符(即public或private);
- 局部类有个很大的优势,即对外部世界(当前块以外的)完全隐藏;
5、由外部方法访问变量
与其它的内部类相比,局部内部类还有一个优点:它们不仅可以访问外围类的数据,还可以访问块(如方法)的局部变量(如方法参数)。
(注意:这些局部变量必须是事实最终变量)
例子:
public void start(int interval, boolean beep){
class TimePrinter implements ActionListener{
public void actionPerformed(ActionEvent event){
if(beep) //这里使用的beep是方法的参数变量
...
}
}
var listener = new TimePrinter();
Timer timer = new Timer(interval,listener);
timer.start();
}
不过其实这会有一个问题,可能不能访问beep变量的值,让我们先仔细地考察这个控制流程:
1)调用start方法
2)调用内部类TimePrinter的构造器,以便初始化对象变量listener;
3)将listener引用传递给Timer构造器,定时器开始计时,start方法推出。此时,start方法的beep参数变量不复存在;
4)1秒之后,actionPerformed方法执行if(beep)…
这里的第4步,可能会出现问题,因为此时的beep参数已经不存在。
为了能够让第四步顺利执行,TimePrinter类在beep参数值消失之前必须将start的beep参数变量复制到类的beep字段。
使用反射程序查看TalkingClock$TimePrinter类,会发现结果确实是这样:当创建内部类对象的时候,beep值就会传递给构造器,并存储在val $beep字段中。
class TalkingClock$TimePrinter{
TalkingClock$TimePrinter(TalkingClock, boolean);
public void actionPerformed(java.awt.event.ActionEvent);
final boolean val$beep; //为了存储参数变量,在类中创建该字段
final TalkingClock this$0; //外围类引用
}
6、匿名内部类
- 使用匿名内部类的原因:(局部内部类—>匿名内部类)
假如只想创建实现了某个接口的类的一个对象,可以使用匿名内部类。
(注意:new的接口或类是已经存在的!而其它的内部类是新建一个类)
例如:
public void start(int interval, boolean beep){
var listener = new ActionListener() //匿名内部类
{
public void actionPerformed(ActionEvent event){
if(beep)
...
}
};
Timer timer = new Timer(interval,listener);
timer.start();
}
代码的含义是:创建一个类的对象,这个类实现了ActionListener接口,需要实现的方法actionPerformed在括号{}内定义。
- 创建类对象的语法如下:
new SuperType(construction parameters)
{
inner class methods and data
};
其中,SuperType是类,内部类要拓展这个类。而construciton构造参数将传递给超类构造器,匿名内部类不能有构造器。
- 创建接口对象的语法如下:
new SuperType()
{
inner class methods and data
};
其中,SuperType是接口,内部类要实现这个接口。并且,内部类不能有构造参数。
- 尽管匿名类不能有构造器,但是可以提供一个对象初始化块;
- “双括号初始化”:假设想构造一个只用一次的数组列表,可以使用以下方式(第一个括号代表这是一个匿名内部类,第二个括号代表这是类的初始化块);
new ArrayList<String>(){{add("Hraay"); add("Tony");}}
7、静态内部类
-
静态内部类的原因:当内部类不需要有外围类的引用时,可以将内部类声明为static。(注意,只有内部类可以声明为static)
-
与常规内部类不同,静态内部类可以有静态字段和方法;
-
在接口中声明的内部类自动是public和static(我的理解是接口不能创建对象,不会有外围类引用,所以内部类是static的);
-
静态内部类的一个例子:当一个方法需要返回两个值的时候,可以创建一个静态内部类(该类有两个字段,并有get方法);
四、服务加载器
有时候会开发一个服务架构的应用。JDK中提供一个加载服务的简单机制,这种机制由Java平台模块系统提供支持,详细内容在本书的卷2第9章。
如何实现服务和加载服务:
1)定义一个接口(或者超类),其中包含服务的各个实例应当提供的方法。例如,假设服务要提供加密。
package serviceLoader;
public interface Cipher
{
byte[] encrypt(byte[] source, byte[] key);
byte[] decrypt(byte[] source, byte[] key);
int strength();
}
2)服务提供者可以提供一个或多个实现这个服务的类(实现类可以放在任意的包中,而不一定是服务接口所在的包),例如:
package serviceLoader.imp;
public class CaesarCipher implements Cipher{
public byte[] encrypt(byte[] source, byte[] key){
...
}
public byte[] decrypt(byte[] source, byte[] key){...
}
public int strength(){...}
}
(注意:每个实现类必须有一个无参数构造器)
3)现在把这些实现类的类名增加到META-INF/services目录下的一个UTF-8编码文本文件中,文件名必须与完全限定类名一致。
我们的例子中,文件META-INF/services/serviceLoader.Cipher必须包含这样一行:
serviceLoader.impl.CaesarCipher
4)完成这个准备工作之后,程序可以如下初始化一个服务加载器(该初始化工作只在程序中完成一次):
public static ServiceLoader<Cipher> cipherLoader = ServiceLoader.load(Cipher.class);
5)服务加载器的iterator方法会返回一个迭代器来迭代处理所提供的所有服务实现。或者,使用流查找所要的服务。
6)最后,如果想要得到任何服务实例,只需要调用findFirst:
Optional<Cipher> cipher = cipherLoader.findFirst();
五、代理
这块内容暂时还没看。。待补充。。。