学习《Java核心技术》——第6章:接口、lambda表达式与内部类
Java核心技术.卷I(第11版)
第6章:接口、lambda表达式与内部类
待加深理解
核心内容:
- 接口
- lambda表达式
- 内部类
- 服务加载器
- 代理
1.接口(interface)
interface
描述类应该做什么,而不制定它们应该如何做!(很抽象,待理解)
接口属性、特征:
一个类只能继承一个父类,但可以实现一个或多个接口,使用关键字implements
。
接口也可以更具体的接口被继承。
接口不是类,是对希望符合这个接口的类的一组需求。
接口中的方法,默认是public,可以省略public。但是在实现接口时,必须把方法声明为public。
接口没有实例字段,因此可以把接口看作是没有实例字段的抽象类,但不是类!!!;
接口可以包含常量,也是默认public,任何实现了该接口的类,都可以直接引用这些常量,不用“类名.常量”,但不建议这么用,影响扩展性。
接口不能被实例化,但可以声明接口变量,而这个变量必须引用实现了这个接口的类对象。
使用instanceof
检查一个对象是否实现了某个特定的接口。
常用的两个内置接口:Cloneable
, Comparable
2步实现接口:
- 将类声明为实现给定的接口
- 对接口中的所有方法提供定义
接口和抽象类比较:
接口不是类,一个类可以实现多个;抽象类只能被继承,Java继承是单继承。
只使用抽象类继承,没有接口,不方便。
因此,接口获得了多重继承的多数好处,又降低了多重继承的复杂性和低效率。
静态和私有方法:
-
Java8中,接口允许静态方法。有违接口作为抽象规范的初衷。
通常做法是将静态方法放在伴随类中,就是成对出现的接口和使用工具类。
如
Collection/Collections
,Path/Paths
。接口允许静态方法后,将在接口中合并工具类的功能。
-
Java9中,接口允许私有方法,只在内口本身的方法中使用。
默认方法:
必须使用default
修饰符标记方法,表示为该接口方法提供一个默认实现。比如:
public interface Collection{
int size(); // an abstract method
default boolean isEmpty(){
return size()==0;
}
}
为isEmpty方法提供一个默认实现,这样只需要实现size方法就行,不用管isEmpty方法。
默认方法的重要用法,接口演化(interface evolution),总结来说就是,将来在接口中增加方法,为该方法提供一个默认实现后,就可以编译通过,不会影响实现该接口的其他类。可以在默认实现里抛一个异常,这样被实现接口的类的实例对象调用时,可以发现没有覆盖该方法。
解决默认方法冲突:
如果在一个接口中将一个方法定义为默认方法,又在父类或者另一个接口定义同样的方法(总而言之,方法在不同接口中重复了),怎么办?
-
父类优先
继承;如果父类提供了一个具体方法,那么同名且参数类型相同的默认方法就会被忽略
-
接口冲突
实现;在不同接口的同名且参数类型一致的方法中,至少有一个接口提供了该方法的实现(默认方法),编译器就报错。
就是产生了二义性,编译器不知道选择哪一个。
所以必须要在实现了接口的类中覆盖这个同名的方法;如果调用接口方法,必须指定选择哪个接口。
-
父类和接口包含相同方法,子类只会考虑父类方法,接口的所有默认方法都会忽略。
就是类优先规则
例如,接口不要为Object类的toString或equals方法定义默认方法,由于类优先规则,所有的类继承Object,因此根本无效。
接口和回调(callback):
回调,一种程序设计模式,指定某个事件发生时应该采取的动作。
比如说,定时器的思路:(没彻底懂)
-
将对象传给这个定时器
-
这个传递对象的具体类要实现定时器接口的方法,将要执行的动作写在方法里
-
一定时间间隔,定时器就会调用传入对象的该方法
-
定时器每经过一定时间(参数1),就会通知监听器(参数2)一次
Comparator接口:
实现接口的public int compare(T first, T second)
方法,实现比较
Cloneable接口:
该接口标记了一个类具有一个安全的clone方法,继承自Object类的受保护的clone方法。
该clone方法默认克隆操作是浅拷贝。也就是只会克隆对象本身(对象引用+基本类型字段),不会克隆对象中引用的子对象,所以还是有部分共享的信息的。
因此,应该重新定义clone方法来实现深拷贝。即克隆对象的子对象也需要克隆。
Cloneable接口实际上给是一个标记接口(tagging interface),也就是不含任何方法的接口,只用来允许在类型查询中使用instanceof,用来标识的而已。
注意,Object是有proteced的clone方法的。对象直接调用clone方法时,会报错!要必须声明Cloneable接口,并实现Object的clone方法才可以(使用public修饰)。深层次原因?
@HotSpotIntrinsicCandidate
protected native Object clone() throws CloneNotSupportedException;
Object类中是这样的,没有具体实现。只覆盖Object的clone方法不声明Cloneable接口也会报错(克隆不支持异常)。
2.lambda表达式没搞懂!!!
lambda表达式,表示一个可传递的代码块,看成是一个被传递的函数,比如定时器、sort方法
别的语言有函数式编程,传递函数,java属于面向对象语言,采用lambda表达式来解决!
语法:传参,箭头(->),代码块
(参数...) -> //可以是无参
{
//代码块
...;
return ...;//允许有返回值,可能返回一个对象,用变量接收
}//{}称作lambda表达式的体
例如:
(String first, String second) -> {
if (first.length()<second.length()) return -1;
else if (first.length() > second.length()) return 1;
else return 0;
}
一些变形:
-
不传参,就是用
()
,空括号 -
如果编译器知道传入参数的类型,则可以省略
具体判定规则待定
-
如果只有一个参数,且编译器知道其类型,则可以省略小括号,直接使用
param -> {...}
形式 -
返回类型会自动推定(常规操作,类似
return 5-3
返回int
) -
要求代码块的所有分支出口返回的类型一致,否则报错
函数式接口(functional interface):?
针对的是只提供一个抽象方法的接口;这个接口会作为其他具体方法的参数,使用lambda表达式来覆盖这个抽象方法,执行调用。
大概是把lambda表达式作为参数传递给具体方法,相当于传递了一个方法。
方法引用(method reference):
就是lambda表达式的一种省略写法。
使用方法引用的前提是,lambda表达式的体只调用一个方法,不做其他多余操作,否则按代码块写。
System.out::println //指示编译器生成一个函数式接口的实例
指示编译器生成一个函数式接口的实例,并覆盖该接口唯一的抽象方法
::
的用法:
-
object::isntanceMethod
对象::实例方法,lambda表达式的参数直接传到实例方法作为参数
-
Class::instanceMethod
类::实例方法,lambda表达式第一个参数作为方法的隐式函数,类似于
(x,y)->{x.instanceMethod(y)}
。就相当于第一个参数做对象使用,好调用方法。 -
Class::staticMethod
类::静态方法,lambda表达式的参数都被传递到静态方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JiT0FfbY-1662378186946)(images/image-20220904170126580.png)]
使用this::instanceMethod
, super::instanceMethod
也是可以的
构造器引用:
就是方法名为new
的方法引用
,编译器会自动匹配合适的构造器。
用数组类型建立构造器:
int[]::new // x -> new int[x]
//创建 数组构造器引用
Person[] people = stream.toArray(Person[]::new);
变量作用域:
前面提到的lambda表达式,用到的参数都是表达式自己提供的;
那么,可以定义一个方法来包裹lambda表达式,这样lambda表达式可以使用到外部的变量;
这个变量也叫自由变量
另外,这个自由变量,要求引用的值不能改变。原因是并发执行不安全。
lambda表达式的组成:
- 一个代码块
- 参数
- 自由变量的值(指非lambda表达式的参数及代码块中定义的变量)
可捕获的变量规则:
- 要求捕获的变量是事实最终变量(effectively final),即该变量初始化以后不再赋新值
lambda表达式的体同嵌套块的作用域相同,故不能有同名的局部变量。
处理lambda表达式:抽象
一个重点目的是:延迟执行(deferred execution)
延迟执行的原因有:
- 在一个单独的线程中运行代码
- 多次运行代码
- 在算法的适当位置运行代码
- 必要时运行代码
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yWQ3Aphq-1662378186947)(images/image-20220904212217637.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NT7pyKOv-1662378186948)(images/image-20220904212309116.png)]
3.内部类(inner class)
定义在另一个类中的类
- 可以对同一包中的其他类隐藏
- 内部类方法可以访问定义这个类的作用域中的数据,包括原有的私有数据。?
使用内部类访问对象状态:
。。。
内部类的特殊语法规则:
- 内部类中声明的所有静态变量都必须是final,并初始化为一个编译时常量
- 内部类不能有static方法(就是不推荐,也能有,但只能访问外围类的静态字段和方法)
内部类是否有用、必要和安全:
。。。
局部内部类:
在方法中创建内部类
声明局部类不能有访问说明符,也就是只能默认状态。
其作用域也只在这个局部类的块中
除了这个方法以外,没有任何其他的知道该局部类的存在
从外部方法访问变量:
局部变量必须是事实最终变量,值不会改变。
局部类能够访问外部类的字段,更能够访问局部变量(如传递给方法的变量)
匿名内部类(anonymous inner class):
没有类名,继承类或者实现接口;因为没有类名,所以没有构造器。
语法:
new SuperType(construction parameters){
inner class methods and data;
}
SuperType可以是一个接口,也可以是一个类,与之对应的是实现这个接口或者扩展这个类。
用处:使用匿名内部类实现时间监听和其他无问题。但推荐是用lambda表达式更好。
静态内部类:
如果不需要内部内有外部类对象的一个引用,就是不引用外部类的变量,就可以把内部类声明为static,也就是可以直接OutClass.InnerClass in = InnerClass.func()
静态内部类可以有静态字段和方法。
4.服务加载器
就是提供一个服务接口,由设计者自己实现服务的特性。
每个实现类必须有一个无参构造器。?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XnQ3ivzr-1662378186948)(images/image-20220904222147241.png)]
5.代理(proxy)
利用代理可以在运行时创建实现了一组给定接口的新类。【为了运行时创建新类,这个新类实现你指定的接口】
一般是代理类和调用处理器类连用。
何时使用代理:
只有在编译时期无法确定需要实现哪个接口时才有必要使用代理。
代理类包含以下方法:
- 指定接口所需要的全部方法
- Object类中的全部方法,例如toString、equals等
调用处理器(invocation handler),是实现了InvovationHandler接口的类的对象。
InvovationHandler接口只有一个方法:
Object invoke(Object proxy, Method method, Object[] args)
调用代理对象的方法时,调用处理器的invoke方法就会被调用,并向其传递Method对象和原调用的参数。之后调用处理器必须确定如何处理这个调用。???
创建代理对象:
使用Proxy类的newProxyInstance方法,参数如下:
-
一个类加载器(class loader)
卷II,第九章
-
一个Class对象数组
每个元素对应需要实现各个接口
-
一个调用处理器
使用代理的一些需求:
- 将方法调用 路由到远程服务器
- 在运行的程序中,将用户界面时间与动作关联起来
- 为了调试,跟踪方法调用
代理类的特性:
代理类是在程序运行过程中动态创建的。一旦被创建,就成了常规类。
所有的代理类都继承Proxy类。
一个代理类只有一个字段,即调用处理器
原文:
现在,我们已经看到了代理类的应用,接下来了解它们的一些特性。需要记住,代理类是在程序运行过程中动态创建的。然而,一旦被创建,它们就变成了常规类,与虚拟机中的任何其他类没有什么区别。
所有的代理类都扩展Proxy类。一个代理类只有一个实例字段——即调用处理器,它在Proxy超类中定义。完成代理对象任务所需要的任何额外数据都必须存储在调用处理器中。例如,在程序清单6-10给出的程序中,代理Comparable对象时,TraceHandler就包装了实际的对象。
所有的代理类都要覆盖Object类的toString、equals 和 hashCode方法。如同所有代理方法一样,这些方法只是在调用处理器上调用invoke。Object类中的其他方法(如clone和getClass)没有重新定义。
没有定义代理类的名字,Oracle虚拟机中的Proxy类将生成一个以字符串$Proxy开头的类名。
对于一个特定的类加载器和预设的一组接口来说,只能有一个代理类。也就是说,如果使用同一个类加载器和接口数组调用两次newProxyInstance方法,将得到同一个类的两个对象。也可以利用getProxyClass方法获得这个类:
class proxyclass = Proxy.getProxyClass(null,interfaces);
代理类总是public 和 final。如果代理类实现的所有接口都是public,这个代理类就不属于任何特定的包;否则,所有非公共的接口都必须属于同一个包,同时,代理类也属于这个包。
可以通过调用Proxy类的isProxyClass方法检测一个特定的Class对象是否表示一个代理类。