第六章:接口,lambda表达式与内部类
6.1 接口
6.1.1 接口的概念
接口不是类,而是对希望符合这个接口的类的一组需求
接口是所有方法都自动是public方法,以此不必提供关键字public(自动是:不代表绝对是,Java9中可以是private)
接口绝对不会有实例字段,在Java8之前,接口中绝对不会包含实现方法(现在可以提供简单的方法,但是不能引用实例字段——接口没有实例)
注意:由于接口都是public所以实现接口的方法必须为public,而不能提供更严格的访问条件。
//对类访问控制符的一些疏漏的理解
class A{
private int a;
public void copy(A other){
this.a = other.a;
}
//对于other,这也是在类的内部,故可以直接访问
}
class B{
private int b;
public void copy(A other){
this.b = other.a;
//不在A类的内部,访问private,报错
}
}
实现接口的主要意义:Java是强类型语言,在调用方法的时候,编译器要检查这个方法确实存在。为了确保依赖于接口定义的方法必须实现才能运行的类的方法。(例如,只有实现了Comparable接口,即能进行比较,才能实现sort,即排序)
注意:规定,对于任意x和y,需保证sgn(x.compareTo(y)) = - sgn(y.compareTo(x)) 其中sgn为符号函数,而且如果x.compareTo(y)抛出异常,那么y.compareTo(x)也需要抛出异常。
这就遇到了equals方法的问题,解决方法类似:
- 类不相同,直接返回false
- 在超类中定义实现,而且声明为final,只比较超类定义的字段来获得结果。
6.1.2 接口的属性
接口不是类,不能用new运算符来实例化一个接口
但是是可以声明接口变量的
package com.package1;
import java.lang.reflect.*;
public class Test {
public static void main(String[] args) throws Exception {
Employee e = new Employee();
Comparable x = e;
//可以声明接口的变量,接口变量必须引用实现了这个接口的类对象
System.out.println(e instanceof Comparable);
//如instanceof检查一个对象是否属于某个特定的类一样
//也可以使用instanceof检查一个对象是否实现了某个特定的接口
}
}
class Employee implements Comparable<Employee>{
@Override
public int compareTo(Employee o) {
return 0;
}
}
可以扩展接口,允许有多条接口链
注意:在同一个Java程序中,只能有一个public接口或者类
interface Moveable{
void move(double x, double y);
}
interface Powered extends Moveable{
}
class A{...}
class B extends A implements Movable,Powered{...}
接口中可以定义常量,并且自动为public static final,实现了这个接口的类可以直接用这个常量。也有一些只有常量的接口,但是不推荐这样使用
6.1.3 接口与抽象类
为什么不使用抽象类:出于复杂性和效率性的考虑,Java中不允许多重继承,但是接口可以多重implements
6.1.4 静态和私有方法
Java8中允许在接口中增加静态方法。
通常的做法是将静态方法放在伴随类中。例如在标准库中,成对出现的接口和实用工具类。Collection/Collections,Path/Paths。
通俗点讲,可以讲工具类里面的静态方法直接放入接口中,这样工具类大多就不需要存在了。
Java9中,接口的方法可以是private。private方法可以是静态方法或者实例方法,使用比较局限,只能作为其他public方法的辅助方法。
6.1.5 默认方法
可以为接口方法提供-一个默认实现,必须用default修饰符标记这样的一个方法。
interface Movable {
default void move(double x, double y) {//do noting}
}
}
class B implements Movable {
//不需要显式的实现move方法
}
默认方法可以调用其他方法
public interface Collection{
int size();
default boolean isEmpty(){
return size() == 0;
}
}
默认方法的一个重要用法是接口演化:接口新增方法时,如果不添加默认实现,所有使用此接口的类都需要修改来实现这个新的方法。
6.1.6 解决默认方法冲突
如果方法签名相同,遵从两个原则:
- 超类优先。类优先。类的定义的方法永远覆盖接口的方法。
- 接口冲突,必须覆盖整个方法来解决冲突
对于多个方法签名相同的接口,如果至少有一个接口提供了实现,编译器就会报错。(如果都没有实现,那么整个类本身就是抽象的,也就不存在错误了)
基于类优先原则,为一个接口增加默认方法可以确保在增加之前能正常工作的代码不受影响。 也绝不能让一个默认方法重新定义Object的方法,始终是无效的,始终会被Object类覆盖。
6.1.7 接口与回调
利用接口实现某个特定事件发生时应该采取的动作。书上利用swing里演示,这里不做多了解
6.1.8 Comparator接口
对于自定义排序规则,可以使用Arrays.sort方法的另一个签名,它有一个数组和一个比较器作为参数.比较器是实现了Comparator接口类的实例
package com.package1;
import java.util.Arrays;
import java.util.Comparator;
public class Test {
public static void main(String[] args) throws Exception {
LengthComparator comparator = new LengthComparator();
String[] strings = {"wuhu","wifei","ccc"};
System.out.println(comparator.compare(strings[0],strings[1]));
//自定义comparator的数组排序
Arrays.sort(strings,comparator);
for(String s : strings){
System.out.println(s);
}
}
}
class LengthComparator implements Comparator<String>{
@Override
public int compare(String o1, String o2) {
return o1.length() - o2.length();
}
}
6.1.9 对象克隆
Cloneable接口。
对于直接赋值,相当于引用的是同一个堆内存,而克隆是新的堆内存。
clone方法是protected的,以保证防止在对克隆对象一无所知的情况下进行克隆,如果克隆的对象包含一些对其他对象的引用,这势必会造成藕断丝连,也就是浅拷贝。所以声明为protected来完成深拷贝,克隆所有的子对象。(如果子对象是不可变的,这种浅拷贝当然不会有太多问题,但是如果是可变的,而且这恰恰是绝大多数情况,就会造成逻辑的混乱)
class Employee implements{
public Employee clone() throws CloneNotSupportException{
return (Employee) super.clone();
}
//在之前,clone方法的返回值总是Object,但是现在可以指定具体的返回类型。这也是第五章提到的协变返回类型的应用
}
如果要进行深拷贝,就需要重新定义clone方法,而且声明为public。子类只能调用protected的clone方法得到它自己的对象,必须重新定义public才能允许所有的方法克隆对象。
class Employee implements Cloneable{
public Employee clone(){
Employee cloned = (Employee) super.clone();
cloned.hireDay = (Date) hireDay.clone();//hireDay为一个Date对象
return cloned;
}
}
Cloneable接口是Java提供的少数标记接口(记号接口),它没有要求去显示的实现(从Object继承下来了clone方法),但是如果一个对象请求克隆而没有实现Cloneable接口,编译器就会抛出一个检查型错误。
即使clone默认(浅拷贝)能够满足需求,也必须实现Cloneable接口。
//所有数组类型都有一个public的clone方法,可以获得一个原数组副本
int[] numer = {2,3,4,5};
int[] cloned = numer.clone();
cloned[0] = 4;//numer[0] doesn't change
6.2 lambda 表达式
6.2.1 引入lambda表达式的原因
为了解决Java面向对象,无法不构造对象,直接传入代码块的问题。
6.2.2 lambda表达式的语法
lambda表达式就是一个代码块,以及必须传入代码的变量规范。
(String first, String second)
->first.length() - second.length();
也可以{},显示return
(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)
->first.length()- second.length();
如果方法只有一个参数,而且这个参数的类型可以推导出,甚至还可以省略小括号
ActionListener listener = event ->
System.out.println("The time is "+Instant.ofEpochMilli(event.getWhen()))
无须指定lambda表达式的返回类型,它总是会由上下文推导出
package com.package1;
import java.util.Arrays;
public class Test {
public static void main(String[] args){
String[] plants = new String[] {"Wuhu","Qifei","cccc"};
System.out.println(Arrays.toString(plants));
Arrays.sort(plants);
System.out.println(Arrays.toString(plants));
Arrays.sort(plants,(String first,String second) ->first.length()-second.length());
System.out.println(Arrays.toString(plants));
}
}
//result :
//[Wuhu, Qifei, cccc]
//[Qifei, Wuhu, cccc]
//[Wuhu, cccc, Qifei]
6.2.3 函数式接口
只有一个抽象方法的接口,需要这种接口的对象时,就可以提供lambda表达式提供,这种接口称之为函数式接口。
建立一个特定的函数式接口,具体的类去实现这个接口,具体调用时再用lambda表达式传入。
个人理解为,由程序员传入具体逻辑,类再根据这个具体逻辑得到不同的结果。实现了重用。
//下面提供两个例子
//java.uti.function有个尤其有用的接口Predicate
public interface Predicate<T>{
boolean test(T t);
//addtional default and static methods
}
//ArrayList有一个removeIf方法,它的参数就是一个Predicate,这个接口专门用来传递lambda表达式。
list.removeIf(e -> e == null);//会删除list中所有的null值
//Supplier<T>接口
public interface Supplier<T>{
T get();
}
//Supplier用于实现懒计算
LocalDate hireDay = Objects.requireNonNullElse(day,
()->new LocalDate(1970,1,1));
//这样,只有在需要值的时候才会调用Supplier
6.2.4 方法引用
Timer timer = new Timer(1000,event -> System.out.println(event));
//使用方法引用:
Timer timer - new Timer(1000,System.out::println);
方法引用会动态的根据函数接口的参数类型选择合适的方法执行(例如System.out::println具有10个重载函数
)
注意:只有当lambda表达式的体只调用一个方法而不做其他操作时,才能把lambda表达式重写为方法引用。
s -> s.length() == 0
//有一个比较,不能用方法引用
用::运算符分割方法名与对象或者类名,有以下三种情况:
- object::instanceMethod
- Class::instanceMethod
- Class::staticMethod
这里比较玄学,具体见书P248
也可以在方法引用中使用this参数
class Greeter{
public void greet(ActionEvent event){
System.out.println("Time:"+ Instant.ofEpochMilli(event.getWhen()));
}
}
class RepeatedGreeter extends Greeter{
public void greet(ActionEvent event){
Timer timer = new Timer(1000,super::greet);
timer.start();
}
}
6.2.5 构造器引用
类似于方法引用,只不过方法名为new。编译器会根据上下文动态推导出调用哪一个构造器
可以用数组类型建立构造器引用
int[]::new
//x -> new int[x]
数组构造器可以克服构造泛型类型T的数组的限制。
new T[n] //error,会改成 new Object[]
Object people = stream.toArray();
//但是我们期望得到Person类型的数组,而不是Object
Person people = stream.toArray(Person::new);
6.2.6 变量作用域
在lambda表达式中访问外围方法或者类中的变量,会储存这个自由变量的值,说这个自由变量被捕获了,称这种情况为闭包。
在lambda表达式中,只能引用值不会改变的变量。
public static void countDown(int start, int delay){
ActionListener listener = event ->
{
start--;//error
}
new Timer(delay,listener).start();
}
这个值在外部改变,也是不合法的。
public static void repeat(String text, int count){
for(int i = 0; i < count; i++){
ActionListener listener = event ->
{
System.out.println(i);//error
};
new Timer(1000,listener).start();
}
}
规则:lambda表达式捕获的变量必须是初始化之后就不会再给它赋新值的事实最终变量。
注意:lambda表达式的体与嵌套块具有相同的作用域。
Path first = Path.of("/user/bin");
Comparator<String> comp = (first,second) -> first.length() - second.length();
//error "first"
在lambda表达式中使用this关键字时,是指创建这个lambda表达式的方法的this参数。
public class Application{
public void init(){
ActionListener listener = event ->{
System.out.println(this.toString());
//会调用Application的toString方法,而不是ActionListener的
}
}
}
6.2.7 处理lambda表达式
使用lambda表达式的重点是 延迟执行,如果想要立即执行代码,完全可以直接执行,而无需把他放在一个lambda表达式中。
6.2.8 再谈Comparator
没看懂。P254,255
6.3 内部类
定义在另一个类中的类
class LinkedList{
public:
class Iterator{
public:
void insert(int x);
int erase();
...
private:
Link * current;
LinkedList * owner;//c++中定义了这个指针来找到他的外部类。但是在java中会有一个隐式引用完成这个工作。
};
...
private:
Link * head;
Link * tail;
};
6.3.1 使用内部类访问对象状态
内部类的对象总有一个隐式引用,指向创建它的对象
class A{
private int a;
public class B{
private int b;
public void say(){
System.out.println("a:"+a+" b "+b);
//是可以访问的,不报错
}
}
}
内部类可以是私有的,而普通类具有包可见性或者公共可见性
6.3.2 内部类的特殊语法规则
outerClass.this //表示外部引用
class Outer{
private int a;
private class Inner{
private int b;
public void say(){
System.out.println(Outer.this.a);
}
}
}
outerObject.new InnerClass(construction parameters)
//可以使用这个语法更加明确的编写内部类对象的构造器,但是没必要
class Outer{
private int a;
public static class Inner{
private static int b = 1;
public static void say(){
System.out.println(b);
}
}
}
这段代码看起来没什么问题,但是很可能在运行时出现逻辑错误
规定:
- 内部类声明的所有静态字段必须是final,并初始化为一个编译时常量。
- 内部类不能有static方法,即使有static方法,也只能访问外围的静态字段和方法,绝不能有static字段。
根本冲突是static字段是公共的,对于不同的Outer类,他们的inner类应该是互不影响的,但是与static矛盾。也需要声明为final,防止修改。
(只是说应该这样规定,但是编译器没有报错,证明在具体情况下是可以违反的,只是后果自负)
6.3.3 内部类是否有用,必须和安全
内部类具有更强的访问权限(可以访问外部类的字段)
但是这种访问权限是来自于编译器的处理,例如
class Outer{
private int a;
//实际上编译器生成了这个方法
static int access$0(Outer);// 它将返回作为参数传递的那个对象的a字段的值.可能是$任何值,这取决于编译器
public class Inner{
public void say(){
System.out.println(a);
//所以实际上的调用为(Outer.access$0(outer)
}
}
}
编译器为了满足更强的访问权限所新加的方法由于具有包可见性,就可能被其他方法调用,由此可能获得private的内容,破坏了封装性。(例如黑客在包中添加了自己的类,如果熟悉虚拟机指令,就有可能获得private的值)
6.3.4 局部内部类
局部类对外界完全隐藏,作用域被限制在声明这个局部类的块中
6.3.5 由外部方法访问变量
局部类不仅可以访问外部的字段,还可以访问局部变量(应该要求为 事实最终变量)
class A{
public void test(int a){
class B{
public void f(){
System.out.println(a);//访问了局部变量
}
}
}
}
6.3.6 匿名内部类
new SuperType(construction parameters){
inner class methods and data;
}
SuperType可以是接口,内部类就要实现这个接口;也可以是类,内部类就需要扩展这个类
因为构造器需要和类名相同,而匿名类没有名字,理所当然的,就不能有构造方法。但是可以提供一个对象初始化块。
package com.package1;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.time.Instant;
public class Test {
public static void main(String[] args){
var clock = new TalkingClock();
clock.start(1000,false);
JOptionPane.showMessageDialog(null,"Quit program?");
System.exit(0);
}
}
class TalkingClock{
public void start(int interval, boolean beep){
var listener = new ActionListener(){
public void actionPerformed(ActionEvent event){
System.out.println("time: "+Instant.ofEpochMilli(event.getWhen()));
if(beep){
Toolkit.getDefaultToolkit().beep();
}
}
};
var timer = new Timer(interval,listener);
timer.start();
}
}
(但是实际上这个程序用lambda表达式更加简单)
public void start(int interval, boolean beep){
var timer = new Timer(interval,event ->{
System.out.println("time: "+Instant.ofEpochMilli(event.getWhen()));
if(beep){
Toolkit.getDefaultToolkit().beep();
}
});
timer.start();
}
双括号初始化:
package com.package1;
import java.util.ArrayList;
public class Test {
public static void main(String[] args){
var friends = new ArrayList<String>();
friends.add("njq");
friends.add("mjf");
fun(friends);
//可以这样写
fun(new ArrayList<String>(){{add("mjf");add("njq");}});
//这里是利用了匿名类,以及初始化块的语法。
}
private static void fun(ArrayList<String> friends){
//do nothing;
}
}
匿名类利用getClass方法做equals方法的改写测试会失效。
System.err.println("Something awful happend in"+ getClass());
//但是很遗憾,这对静态方法并不管用。这里调用的是this.getClass(),但是对静态方法,压根没this
new Object(){}.getclass().getEnclosingClass();
//新建一个Object的匿名子类的一个匿名对象,getEncolsingClass则得到其外围类,也就是想要得到的类。
6.3.7 静态内部类
使用内部类只是为了将内部类隐藏,而不用那个隐式的对外部类的引用的时候,可以将内部类声明为static
在接口中声明的内部类自动是static和public
package com.package1;
import java.util.ArrayList;
public class Test {
private static final int SIZE = 20;
public static void main(String[] args){
var values = new double[SIZE];
for(int i = 0; i < SIZE; i++){
values[i] = Math.random()*100;
}
ArrayAlg.Pair p = ArrayAlg.minmax(values);
//用途上,ArrayAlg是一个工具类。
//所以方法为static很合理。
System.out.println(p.getFirst());
System.out.println(p.getSecond());
}
}
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 i : values){
if(min > i)
min = i;
if(max < i)
max = i;
}
return new Pair(min,max);
}
}
6.4 服务加载器
没看懂,暂时不重要,感觉就是Ioc容器的基础,先放着。
6.5 代理(proxy)
利用代理可以在运行时创建了实现了一组给定接口的新类,只有在编译时期无法确定需要实现哪一个接口的时候才有必要使用代理。
6.5.1 何时使用代理
能在运行时创建全新的类,能够实现指定的所以接口
代理类包括以下方法:
- 指定接口所需要的全部方法
- Object类中的全部方法,例如:toString,equals
必须调用实现了InvocationHandler接口的调用处理器对象
//它只有这一个方法
Object invoke(Object proxy, Method method, Object[] args);
6.5.2 创建代理对象
创建一个proxy对象需要调用所以的newProxyInstance方法,它包括如下三个参数:
- 一个类加载器(class loader),是Java安全模型的一部分,可以对平台和应用类,互联网上下载的类使用不同的类加载器。(下面我们指定”系统类加载器“加载平台和应用类)
- 一个class对象数组,每个元素对于需要实现的各个接口
- 一个调用处理器
package com.package1;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Random;
public class Test {
public static void main(String[] args){
var elements = new Object[1000];
for(int i = 0; i < elements.length; i++){
Integer value = i + 1;
var handler = new TraceHandler(value);
Object proxy = Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),
new Class[]{Comparable.class} ,handler);
elements[i] = proxy;
}
Integer key = new Random().nextInt(elements.length) + 1;
int result = Arrays.binarySearch(elements,key);
if(result >= 0){
System.out.println(elements[result]);
}
}
}
class TraceHandler implements InvocationHandler {
private Object target;
public TraceHandler(Object t){
target = t;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.print(target);
System.out.print("."+method.getName()+"(");
if(args != null){
for(int i = 0;i < args.length; i++){
System.out.print(args[i]);
if(i < args.length - 1)
System.out.print(",");
}
}
System.out.println(")");
return method.invoke(target,args);
//调用method的invoke,具体在这里是compareTo
//可以理解为AOP切面的底层实现。
}
}
6.5.3 代理类的特性
代理类是程序运行的时候动态创建的。一旦被创建,他们就成了常规类,与虚拟机的其他类没有声明区别。
所有的代理类都扩展自Proxy类。一个代理类只有一个实例字段——调用处理器,调用处理器封装需要处理的数据。
所有代理类都覆盖了Object类的toString,equals和hashCode方法(前面也提到,重写equals方法必须重写hashCode方法,以确保两个方法的一致性)。
代理类总是public和final的。如果代理类实现的所有接口全是public,这个代理类就不属于任何特定的包,否则,所有非公共的接口必须熟悉同一个包,包括这个代理类也属于这个包。
可以调用isProxyClass判断一个Class对象是否是代理对象