1. 接口
抽象类和接口统称为超类型。
假设希望使用 Arrays 类的 sort 方法对 Employee 对象数组进行排序, Employee 类就必须实现 Comparable 接口。
接口不是类,而是对类的一组需求描述,这些类要遵从接口描述的统一格式进行定义。就是说,接口定义了一组如果实现该接口必须定义的方法。
接口示例:
声明接口时,接口中的所有方法自动地属于 public
。但在实现接口时,必须把方法声明为 public; 否则, 编译器
将认为这个方法的访问属性是包可见性, 即类的默认访问属性,
接口不能包含实例域,接口没有实例。由实现接口的那个类来完成提供实例域和方法实现的任务。
/域是指属性,实例域是实例化对象的属性,静态域是被static修饰的属性/
使用关键字implements将类声明为某个接口(前面是类,后面是接口):
为什么不能在 Employee 类直接提供一个 compareTo 方法,而必须实现 Comparable 接口呢?
主要原因在于 Java 程序设计语言是一种强类型 ( 总是强制类型定义的语言,要求变量的使用要严格符合定义,所有变量都必须先定义后使用) 语言。在调用方法的时候, 编译器将会检查这个方法是否存在。
实现Comparable 接口能够确保参与比较的对象都实现了compareTo方法。
1.1 接口的特性
接口不是类,不能使用new实例化接口。
可以声明接口的变量。接口变量必须弓I用实现了接口的类对象:
Comparable x; // OK
x = new Employee(. . .); // OK provided Employee implements Comparable
如同使用 instanceof检查一个对象是否属于某个特定类一样, 也可以使用instance 检查一个对象是否实现了某个特定的接口:
if (anObject instanceof Comparable) { . . . }
接口也可以被扩展,即可以继承自另一个接口:
虽然在接口中不能包含实例域或静态方法,但却可以包含常量,接口接口中的域将被自动设为 public static final
:
public interface Powered extends Moveable
{
double milesPerCallonO;
double SPEED_LIMIT = 95; // a public static final constant
}
实现该接口的类会自动继承这些常量,并可以在方法中直接引用这些常量,而无需借助域访问符。
每个类只能有一个超类,但是可以实现多个接口。
class Employee implements Cloneable, Comparable
1.2 接口与抽象类
为什么不将必须实现的操作直接设计成抽象类?这样也能确保类实现了对应的方法。
因为每个类只能扩展于一个类,而接口则没有这个限制。所以使用从一个基类派生,用接口辅助作为抽象规范的方式是比较合理且灵活的。
1.3 静态方法
在 Java SE 8 中,允许在接口中增加静态方法。理论上讲,没有任何理由认为这是不合法的。只是这有违于将接口作为抽象规范的初衷
1.4 默认方法
可以为接口方法提供一个默认实现。 必须用 default 修饰符标记这样一个方法。
public interface Comparable<T>
{
default int compareTo(T other) { return 0; }
}
默认方法可以调用任何其他方法。
为接口提供默认方法可以实现接口演化,在增加新的接口方法后,可以不用修改实现了接口的类。例:
public class Bag implements Collection
假设对Collection新增了一个方法newMethod。
当新接口方法非默认:
1.重新编译Bag 时,无法通过编译。
2.不重新编译这个Bag ,而只是使用原先的一个包含这个类的 JAR 文件。程序可以正常构造Bag 实例,但是当试图调用新接口方法时,就会出现AbstractMethodError。
当新方法提供了默认实现:
1.可以重新编译Bag
2.如果不重新编译Bag ,创建该Bag 的实例,可以使用新接口方法,调用的是Colletcion.newMethod。
1.5 解决默认方法冲突
1.5.1 超类优先
一个类扩展了一个超类,同时实现了一个接口,并从超类和接口继承了相同的方法。则接口中同名而且有相同参数类型的默认方法会被忽略。
1.5.2 接口冲突
如果一个接口提供了一个默认方法, 另一个接口提供了一个同名而且
参数类型(不论是否是默认参数)相同的方法(不一定要默认)。有一个类同时实现了这两个接口,不能使用接口的默认实现,必须覆盖这个方法来解决冲突。
两个接口如何冲突并不重要。如果至少有一个接口提供了一个实现,
编译器就会报告错误, 而程序员就必须解决这个二义性。
如果两个接口都没有为共享方法提供默认实现, 那么就与Java SE 8 之前的
情况一样,这里不存在冲突
2. 接口实例
2.1 比较器
Arrays,sort(friends, new LengthComparator())
可以给sort函数传入一个比较器对象自定义比较规则。
比较器是比较器是实现了Comparator 接口的类的实例。如上述的LengthComparator定义如下:
2.1 对象克隆
默认情况下,拷贝对象变量得到的副本和原变量指向的是同一个对象实例。这种拷贝被称为浅拷贝,如果原对象和浅克隆对象共享的子对象是不可变的, 那么这种共享就是安全的。(比如String对象)
而通常情况下,子对象都是可变的,所以需要使用clone
方法,为每个对象变量创造一个对象实例。这种拷贝成为深拷贝。
Object 类如何实现clone。它对于这个对象一无所知, 所以只能逐个域地进行拷贝。如果对象中的所有数据域都是数值或其他基本类型,拷贝这些域没有任何问题、但是如果对象包含子对象的引用,拷贝域就会得到相同子对象的另一个引用,这样一来, 原对象和克隆的对象仍然会共享一些信息。
所以在定义某个类的clone时,需要把内部所有的子对象都调用clone。
所有数组类型都有一个public 的clone 方法, 而不是protected: 可以用这个方法建立一个新数组, 包含原数组所有元素的副本。例如:
2.3 回调
实现周期性完成某个操作:
1.定义一个实现了ActionListener接口的类,定义回调所执行的操作。
2.构造这个类的一个对象, 并将它传递给Timer 构造器。
Timer 构造器的第一个参数是发出通告的时间间隔, 它的单位是毫秒。这里希望每隔10秒钟通告一次。第二个参数是监听器对象。
3.启动定时器
3.lambda表达式
将一个代码块传递到某个对象。(比如前面的一个定时器, 或者一个
sort 方法)。不能直接传递代码段,Java 是一种面向对象语言, 所以必须构造一个对象,这个对象的类需要有一个方法能包含所需的代码。
lambda 表达式就是一个代码块, 以及必须传入代码的变量规范。
lambda表达式形式为:参数,->,表达式/函数体
1.如果可以推导出一个lambda 表达式的参数类型,则可以忽略其类型。
2.如果方法只有一个参数, 而且这个参数的类型可以推导得出,那么甚至还可以省略参数的小括号。
3.无需指定lambda 表达式的返回类型。
如果一个lambda 表达式只在某些分支返回一个值, 而在另外一些分支不返回值,
这是不合法的。例如,(int x)-> { if (x >= 0) return 1; } 就不合法。
3.1 函数式接口
对于只有一个抽象方法的接口, 需要这种接口的对象时, 就可以提供一个lambda 表达式,在使用时,可以将该表达式转换为函数式接口。这种接口称为函数式接口( functional interface )。
不是接口中的所有方法都是抽象的吗?
接口完全有可能重新声明Object 类的方法, 如toString 或clone
,这些声明有可能会让方法不再是抽象的。
比如说Arrays.sort 方法。它的第二个参数需要一个Comparator 实例,Comparator 就是只有一个方法的接口, 所以可以提供一个lambda 表达式:
Arrays.sort (words ,(first , second) -> first.length() - second.length()
) ;
3.2 方法引用
可以直接把println 方法传递到Timer 构造器。
具体做法如下:
Timer t = new Timer(1000, System.out::println) ;
表达式System.out::println 是一个方法引用( method reference ), 它等价于lambda 表达式:
x -> System.out.println(x)
方法引用用::
操作符分隔方法名与对象或类名。有三者情况:
•object::instanceMethod
•Class::staticMethod
•Class::instanceMethod
前两种方法引用的参数都传给lambda调用的方法。如:
System.out::println 等价于x
-> System.out.println(x
)。 类似地,Math::pow 等价于(x,y
) ->Math.pow(x, y
)。
而最后一种第1 个参数会成为方法的目标。如:
String::compareToIgnoreCase 等同于 (x, y
) ->x
.compareToIgnoreCase(y
)
类似于lambda 表达式, 方法引用不能独立存在,总是会转换为函数式接口的实例。
可以在方法引用中使用this 参数。例如,this::equals 等同于x -> this.equals(x)。使用super 也是合法的。下面的方法表达式
super::instanceMethod,会调用给定方法的超类版本。
3.3 构造器引用
构造器引用与方法引用很类似,只不过方法名为new。例如,Person::new 是Person 构造器的一个引用。如果有多个Person 构造器, 编译器会选择参数最匹配的构造器。
使用示例:
重点是map 方法会为各个列表元素调用Person(String) 构造器。如果有多个Person 构造器, 编译器会选择有一个String 参数的构造器, 因为它从上下文推导出这是在对一个字符串调用构造器。
3.4 变量作用域
lambda 表达式的体与嵌套块有相同的作用域,可以使用同一作用域的变量。在lambda 表达式中声明与一个局部变量同名的参数或局部变量是不合法的。
在lambda 表达式中, 只能引用值不会改变的变量。因为如果在lambda 表达式中改变变量, 并发执行多个动作时就会不安全。另外如果在lambda 表达式中引用变量, 而这个变量可能在外部改变,这也是不合法的。
在一个lambda 表达式中使用this 关键字时, 是指创建这个lambda 表达式的方法的this参数。
3.5 处理lambda 表达式
使用lambda 表达式的重点是延迟执行( deferred execution ),如:
•在一个单独的线程中运行代码;
•多次运行代码;
•在算法的适当位置运行代码(例如, 排序中的比较操作);
•发生某种情况时执行代码(如, 点击了一个按钮, 数据到达, 等等);
•只在必要时才运行代码。
JAVAAPI提供了如下函数式接口:
最好使用规范化的基本类型的函数式接口来减少自动装箱。例如,使用IntConsumer 而不是Consumer< lnteger >。
3.6 再谈Comparator
静态comparing 方法接受一个“ 键提取器” 函数作为参数,键提取器将类型T 映射为一个可比较的类型( 如String )。对要比较的对象应用这个函数, 然后对返回的键完成比较。
比如:
Arrays.sort(people, Comparator.comparing(Person::getName))
;
可以把比较器与thenComparing 方法串起来。例如,
Arrays.sort(people,Comparator.comparing(Person::getlastName).thenConiparing(Person::getFirstName))
;
如果两个人的姓相同, 就会使用第二个比较器。
可以为comparing 和thenComparing 方法提取的键指定一个比较器。例如,可以如下根据人名长度完成排序:Arrays.sort(people, Comparator.companng(Person::getName,(s, t) ->Integer.compare(s.length() , t.length())))
;
或者使用函数式接口,避免装箱:Arrays.sort(people,Comparator.comparinglnt(p -> p.getName() .length()))
;
如果键函数会返回空值,要用到nullsFirst 和nullsLast 适配器,这些静态方法会修改现有的比较器, 从而在遇到null 值时不会抛出异常, 而是将这个值标记为小于或大于正常值。
4. 内部类
内部类的意义:
•内部类方法可以访问该类定义所在的作用域中的数据, 包括私有的数据。
•内部类可以对同一个包中的其他类隐藏起来。
•当想要定义一个回调函数且不想编写大量代码时,使用匿名(anonymous) 内部类比较便捷。
C++的嵌套类和JAVA内部类不同,和JAVA静态内部类一样。嵌套是一种类之间的关系, 而不是对象之间的关系。一个LinkedList 对象并不包含Iterator 类型或Link 类型的子对象。相当于嵌套的类之间除了**访问权限(内部类可以访问外层类的所有成员)和命名规则(内部类在外部代码使用时,需要加上外层类作用域)**变化之外,是两个独立的类。
4.1 使用内部类访问对象状态
内部类既可以访问自身的数据域,也可以访问创建它的外围类对象的数据域。内部类的对象总有一个隐式引用, 它指向了创建它的外部类对象。这个引用在内部类的定义中是不可见的。
内部类的例子:
外围类的引用在构造器中设置。编译器修改了所有的内部类的构造器, 添加一个外围类引用的参数。因为TimePrinter 类没有定义构造器, 所以编译器为这个类生成了一个默认的构造器,其代码如下所示:
outer就是内部类TimePrinter对外围类TalkingClock的隐士引用,outer不是Java 的关键字。我们只是用它说明内部类中的机制
TimePrinter 类声明为私有的。这样一来, 只有TalkingClock 的方法才能够构造TimePrinter 对象。只有内部类可以是私有类,而常规类只可以具有包可见性,或公有可见性。
4.2 内部类的特殊语法规则
内部类需要借助外围类对象完成实例化。
使用如下方法使用外围类引用:
可用如下语句明确调用内部类构造器:
外围类引用被设置为创建内部类对象的方法中的this引用,this可以省去。
或者可以通过显式地命名将外围类引用设置为其他的对象。比如如下方式:
在外围类的作用域之外,可以这样引用内部类:
OuterClass.InnerClass
内部类中声明的所有静态域都必须是final。原因很简单。我们希望一个静态域只有一个实例, 不过对于每个外部对象, 会分别有一个单独的内部类实例,内部类是外围类实例的一部分,而不是外围类的一部分。如果这个域不是final , 它可能就不是唯一的。
内部类不能有static 方法。
4.3 内部类的安全性
内部类是一种编译器现象, 与虚拟机无关。编译器将会把内部类翻译成用$ ( 美元符号)分隔外部类名与内部类名的常规类文件, 而虚拟机则对此一无所知。
比如,在TalkingClock 类内部的TimePrinter 类将被翻译成类文件TalkingClock$TimePrinter.class。
为什么内部类能访问外围类所有成员?
编译器在外围类添加静态方法。它将返回作为参数传递给它的对象域。
这样看起来任何人都能使用这个方法,怎么解决安全问题?
这个静态方法名字是不合法的,所以从程序层面无法调用。但是可以使用十六进制编辑器轻松地创建一个用虚拟机指令调用那个方法的类文件。
4.4 局部内部类
如果内部类名字只在创建这个类型的对象时使用了一次,则可以以在一个方法中定义局部类。
局部类不能用public 或private 访问说明符进行声明。它的作用域被限定在声明这个局部类的块中。
局部类有一个优势, 即对外部世界可以完全地隐藏起来。即使TalkingClock 类中的其他代码也不能访问它。除start 方法之外, 没有任何方法知道TimePrinter 类的存在。
4.5 由外部方法访问变量
局部类还有一个优点。它们不仅能够访问包含它们的外部类, 还可以访问局部变量。不过, 那些局部变量必须事实上为final。这说明, 它们一旦赋值就绝不会改变。
使用局部变量带来的一个问题就是局部变量会随着方法结束而消亡,而通过方法创建的对象还在,并且其方法可能需要访问局部变量。
解决方案是:编译器必须检测对局部变量的访问, 为每一个变量建立相应的数据域, 并将局部变量拷贝到构造器中, 以便将这些数据域初始化为局部变量的副本。前面的final约束,是为了使得局部变量与在局部类内建立的拷贝保持一致。
有时候,final的约束无法满足使用要求,比如想要在内部类内加上一个计数器。这时可以将计数值存储在数组里,可以改变数组对应的值。
4.6 匿名内部类
如果只创建这个类的一个对象,就不必命名了。这种类被称为匿名内部类(anonymous inner class)。在声明时也完成了类的唯一一次实例化。
匿名内部类示例:
创建一个实现ActionListener 接口的类的新对象,需要实现的方法actionPerformed 定义在括号内。
通常的语法格式为:
SuperType 可以是ActionListener 这样的接口, 于是内部类就要实现这个接口。SuperType 也可以是一个类,于是内部类就要扩展它。
由于构造器的名字必须与类名相同, 而匿名类没有类名,所以匿名类不能有构造器。取而代之的是,将构造器参数传递给超类(superclass) 构造器。
第一条语句是直接构造对象;第二条语句是构造扩展了Person类的匿名内部类对象;
4.7 静态内部类
普通内部类是类之间关系(内部类可以直接访问外部类成员)+对象之间(内部类需借助外部类实例化,内部类对象包含外部类对象的隐式引用的关系。而静态内部类和嵌套类类似,是类之间的关系(内部类可以直接访问外部类成员)。
有时候, 使用内部类只是为了把一个类隐藏在另外一个类的内部,并不需要内部类引用外围类对象。为此,可以将内部类声明为static, 以便取消产生的引用。
静态内部类的对象除了没有对生成它的外围类对象的引用特权外, 与其他所有内部类完全一样。
在内部类不需要访问外围类对象的时候, 应该使用静态内部类。有些程序员用嵌套类(nested class ) 表示静态内部类。
与常规内部类不同, 静态内部类可以有静态域和方法。
声明在接口中的内部类自动成为static 和public 类。
5. 代理
代理,顾名思义,即通过代理对象访问目标对象。
静态代理就是把目标对象所实现的接口都实现,需要在定义代理的代码里真实写出来,还可以扩展自己的接口。
动态代理不用真的写目标对象的接口,因为事实上,很多情况编译时不知道要用哪个接口。而是提供了调用处理器,当有调用请求时,通过调用处理器调用所需方法。
利用代理可以在运行时创建一个实现了一组给定接口的新类,这种功能只有在编译时无法确定需要实现哪个接口时才有必要使用。
代理类具有:
•指定接口所需要的全部方法。
•Object 类中的全部方法, 例如, toString、equals 等。
为实现代理类,提供一个调用处理器( invocation handler)。调用处理器是实现了InvocationHandler
接口的类对象。在这个接口中只有一个方法:
Object invoke(Object proxy, Method method, Object args)
通过代理对象调用目标对象的方法, 实际调用的是调用处理器的invoke 方法, 代理对象会向其传递Method 对象和原始的调用参数以完成调用。
5.1 创建代理对象
代理对象使用示例:
(应该是对Object元素的compareTo和to_string调用,标记有误)
5.2 代理类特性
代理类是在程序运行过程中创建的。然而, 一旦被创建, 就变成了常规类,与虚拟机中的任何其他类没有什么区别。
所有的代理类都扩展于 Proxy 类。一个代理类只有一个实例域—调用处理器,它定义在 Proxy 的超类中。
没有定义代理类的名字,Sun 虚拟机中的 Proxy类将生成一个以字符串 SProxy 开头的类名。
特定的类加载器和预设的一组接口来说,只能有一个代理类。 也就是说,如果使用同一个类加载器和接口数组调用两次 newProxylnstance方法的话, 那么只能够得到同一个类的两个对象。
可以利用 getProxyClass方法获得已创建的代理类:
Class proxyClass = Proxy.getProxyClass(null, interfaces);
代理类一定是 public 和 final。如果代理类实现的所有接口都是 public, 代理类就不属于某个特定的包;否则, 所有非公有的接口都必须属于同一个包,同时,代理类也属于这个包。
可以通过调用 Proxy 类中的 isProxyClass
方法检测一个特定的 Class 对象是否代表一个代理类。