接口
接口技术主要用来描述类具有什么功能。
一个类可以实现(implement)一个或多个接口,并在需要接口的地方,随时使用实现了相应接口的对象。
lambda表达式,这是一种表示可以在将来某个时间点执行的代码块的简洁方法。使用lambda表达式,可以用一种精巧而简洁的方式表示使用回调或变量行为的代码
内部类是定义在另外一个类的内部,其中的方法可以访问包含它们的外部类的域。主要用于设计具有相互协作关系的类集合。
代理(proxy),这是一种实现任意接口的对象。代理是一种非常专业的构造工具,它可以用来构建系统级的工具。
接口
接口
在Java程序设计语言中,接口不是类,而是对类的一组需求描述,这些类要遵从接口描述的统一格式进行定义。
🙂:Arrays类中的sort方法承诺可以对对象数组进行排序,但要求满足下列前提:对象所属的类必须实现了Comparable接口。note: 在Java SE 5.0中,Comparable接口已经改进为泛型类型。
接口中的所有方法自动地属于public,因此不必提供关键字public.
关于CompareTo()函数,在调用x.compareTo(y)的时候,这个compareTo方法必须确实比较两个对象的内容,并返回比较的结果。当x小于y时,返回一个负数;当x等于y时,返回0;否则返回一个正数。
在接口中还可以定义常量。
提供实例域和方法实现的任务应该由实现接口的那个类来完成。
类实现某个接口,需要两个步骤:
- 将类声明为实现给定的接口;
- 对接口中的所有方法进行定义。
实现接口时,必须把方法声明为public; 可以为泛型Comparable接口提供一个类型参数。
语言标准规定:对于任意的x和y,实现必须能够保证sgn(x.compareTo(y))=-sgn(y.compareTo(x))。
子类y覆盖了父类x的CompareTo方法后,不符合”反对称“的规则,调用x.compareTo(y)不会抛出异常,它只是将x和y都作为雇员进行比较。但是反过来,y.compareTo(x)将会抛出一个ClassCastException。
如果存在这样一种通用算法,它能够对两个不同的子类对象进行比较,则应该在超类中提供一个compareTo方法,并将这个方法声明为final。
rank方法,使得不管如何基于Rank值的方法都能正确。
接口的特性
接口不是类,尤其不能使用new运算符实例化一个接口;但是尽管不能构造接口的对象,却能声明接口的变量,接口变量必须引用实现了接口的类对象。
如同使用instanceof检查一个对象是否属于某个特定类一样,也可以使用instance检查一个对象是否实现了某个特定的接口。
与可以建立类的继承关系一样,接口也可以被扩展。
虽然在接口中不能包含实例域或静态方法,但却可以包含常量。
与接口中的方法都自动地被设置为public一样,接口中的域将被自动设为public static final。
注释:可以将接口方法标记为public,将域标记为public static final。
每个类只能够拥有一个超类,但却可以实现多个接口。
接口和抽象类
为什么还要定义接口,不是有抽象类了吗?接口可以提供多重继承的大多数好处,同时还能避免多重继承的复杂性和低效性。
静态方法
Java SE 8中,允许在接口中增加静态方法。但是这有违接口作为抽象规范的初衷。目前为止,通常的做法都是将静态方法放在伴随类中。
在标准库中,可以成对出现的接口和实用工具类,如Collection/Collections或 Path/Paths。可以将Paths类中的工厂方法移动到Path接口中,这样一来,不再需要为实用工具方法另外提供一个伴随类。
默认方法
可以为接口方法提供一个默认实现。必须用default修饰符标记这样一个方法。当然,每一个实际实现会覆盖这个方法。
注释:在Java API中,你会看到很多接口都有相应的伴随类,这个伴随类中实现了相应接口的部分或所有方法,如Collection/AbstractCollection或MouseListener/MouseAdapter。在Java SE 8中,这个技术已经过时。现在可以直接在接口中实现方法。
解决默认方法冲突
Java的处理规则:
- 超类优先。如果超类提供了一个具体方法,同名而且有相同参数类型的默认方法会被忽略。
- 接口冲突。如果一个超接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型(不论是否是默认参数)相同的方法,必须覆盖这个方法来解决冲突。
警告⚠:千万不要让一个默认方法重新定义Object类中的某个方法。由于“类优先”规则,这样的方法绝对无法超越Object.toString或Objects.equals。
接口示例
了解接口的实际使用。
接口与回调
回调(callback)是一种常见的程序设计模式。在这种模式中,可以指出某个特定事件发生时应该采取的动作。
Comparator接口
假设我们希望按长度递增的顺序对字符串进行排序,而不是按字典顺序进行排序。肯定不能让String类用两种不同的方式实现compareTo方法:要处理这种情况,Arrays.sort方法还有第二个版本,有一个数组和一个比较器(comparator)作为参数,比较器是实现了Comparator接口的类的实例。
对象克隆
如果希望copy是一个新对象,它的初始状态与original相同,但是之后它们各自会有自己不同的状态,这种情况下就可以使用clone方法。
拷贝和clone是不同的!
默认的克隆操作是“浅拷贝”
注意:如果原对象和浅克隆对象共享的子对象是不可变的,那么这种共享就是安全的。如果子对象属于一个不可变的类,如String,就是这种情况。
通常子对象都是可变的,必须重新定义clone方法来建立一个深拷贝,同时克隆所有子对象。对于每一个类,需要确定:
- 默认的clone方法是否满足要求;
- 是否可以在可变的子对象上调用clone来修补默认的clone方法;
- 是否不该使用clone。
实际上第3个选项是默认选项。如果选择第1项或第2项,类必须:
1)实现Cloneable接口;
2)重新定义clone方法,并指定public访问修饰符。
注释:Object类中clone方法声明为protected,所以你的代码不能直接调用anObject.clone()。
标记接口:不包含任何方法;它唯一的作用就是允许在类型查询中使用instanceof:建议你自己的程序中不要使用标记接口。
即使clone的默认(浅拷贝)实现能够满足要求,还是需要实现Cloneable接口,将clone重新定义为public,再调用super.clone()。
必须当心子类的克隆。子类中可能会有需要深拷贝或不可克隆的域。
注释:所有数组类型都有一个public的clone方法,而不是protected。
lambda表达式
针对这种类型的代码:将一个代码块传递到某个对象(一个定时器,或者一个sort方法)作出优化。
lambda表达式的语法
(String fir, String sec) -> fir.length() - sec.length();
lambda表达式就是一个代码块,以及必须传入代码的变量规范。
Java中的一种lambda表达式形式:参数,箭头(->)以及一个表达式。
即使lambda表达式没有参数,仍然要提供空括号,就像无参数方法一样;
如果可以推导出一个lambda表达式的参数类型,则可以忽略其类型。例如,在对只有一个抽象方法的接口变量进行初始化时,就可以忽略其类型。
如果方法只有一个参数,而且这个参数的类型可以推导得出,那么甚至还可以省略小括号。
无需指定lambda表达式的返回类型。lambda表达式的返回类型总是会由上下文推导得出。
注释:如果一个lambda表达式只在某些分支返回一个值,而在另外一些分支不返回值,这是不合法的。
函数式接口
对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambda表达式。这种接口称为函数式接口(functional interface)。
如何转换为函数式接口?
Arrays.sort方法,第二个参数需要一个Comparator实例,Comparator就是只有一个方法的接口,可以提供一个lambda 表达式。
Arrays.sort(array, (String fir, String sec) -> fir.length() - sec.length());
在底层,Arrays.sort方法会接收实现了Comparator
最好把lambda表达式看作是一个函数,而不是一个对象,另外要接受lambda表达式可以传递到函数式接口。
实际上,在Java中,对lambda表达式所能做的也只是能转换为函数式接口.
注释:甚至不能把lambda表达式赋给类型为Object的变量,Object不是一个函数式接口。
Java API在java.util.function包中定义了很多非常通用的函数式接口。
java.util.function包中有一个尤其有用的接口Predicate:ArrayList类有一个removeIf方法,它的参数就是一个Predicate。这个接口专门用来传递lambda表达式。
方法引用
有时,可能已经有现成的方法可以完成你想要传递到其他代码的某个动作。
表达式System.out::println是一个方法引用(method reference),它等价于lambda表达式 x->System.out.println(x)。
假设你想对字符串排序,而不考虑字母的大小写。可以传递以下方法表达式,
Arrays.sort(strings, String::compareToIgnoreCase)
用::操作符分隔方法名与对象或类名。有时,方法引用等价于提供方法参数的lambda表达式;在这里,第1个参数会成为方法的目标。例如,String::compareToIgnoreCase等同于(x,y)->x.compareToIgnoreCase(y)。
注释:如果有多个同名的重载方法,编译器就会尝试从上下文中找出你指的那一个方法。
!可以在方法引用中使用this参数、super。
构造器引用
构造器引用与方法引用很类似,只不过方法名为new。
例如,Person::new是Person构造器的一个引用。哪一个构造器呢?这取决于上下文。
假设你有一个字符串列表。可以把它转换为一个Person对象数组,为此要在各个字符串上调用构造器,
ArrayList<String> names = ...;
Stream<Person> stream = names.stream().map(Person::new)
List<Person> peoples = stream.collect(Collectors.toList())
可以用数组类型建立构造器引用。例如,int::new是一个构造器引用,它有一个参数:即数组的长度。这等价于lambda表达式x->new int[x]。
Java有一个限制,无法构造泛型类型T的数组。数组构造器引用对于克服这个限制很
有用。流库利用构造器引用解决了这个问题。
变量作用域
当lambda表达式中有一个自由变量时。表示lambda表达式的数据结构必须存储自由变量的值,在这里就是字符串"Hello"。我们说它被lambda表达式捕获(captured)。(将lambda表达式转换为包含一个方法的对象,这样自由变量的值就会复制到这个对象的实例变量中)
注释:关于代码块以及自由变量值有一个术语:闭包(closure)。如果有人吹嘘他们的语言有闭包,现在你也可以自信地说Java也有闭包。在Java中,lambda表达式就是闭包。
在lambda表达式中,只能引用值不会改变的变量。
规则:lambda表达式中捕获的变量必须实际上是最终变量(effectively final)。
在lambda表达式中声明与一个局部变量同名的参数或局部变量是不合法的。lambda表达式的体与嵌套块有相同的作用域。这里同样适用命名冲突和遮蔽的有关规则。
在一个lambda表达式中使用this关键字时,是指创建这个lambda表达式的方法的this参数。
处理lambda表达式
如何编写方法处理lambda表达式?
使用lambda表达式的重点是延迟执行(deferred execution)。
方法 | 效果 | 具体 |
---|---|---|
repeat | 重复动作n 次 | repeat(n, lambda表达式) |
要接受这个lambda表达式,需要选择(偶尔可能需要提供)一个函数式接口。使用Runnable接口后,在方法体中调用对象的run()方法会执行这个表达式的主体。
流程:
- 选择合适的函数式接口(根据lambda表达式的参数和返回类型选择);
- 定义方法,接收接口;
- 调用方法,传入lambda表达式;
下表列出了基本类型int、long和double的34个可能的规范。
提示:最好使用表6-1或表6-2中的接口。
注释:大多数标准函数式接口都提供了非抽象方法来生成或合并函数。已经提供了默认方法and、or和negate来合并谓词。
注释:如果设计你自己的接口,其中只有一个抽象方法,可以用@FunctionalInterface注解来标记这个接口。这样做有两个优点。如果你无意中增加了另一个非抽象方法,编译器会产生一个错误消息。
再谈Comparator
Comparator接口包含很多方便的静态方法来创建比较器。这些方法可以用于lambda
表达式或方法引用。
重要:可以设置多个比较器,比较器含有变体形式(comparingInt。。。),并且针对键函数可能返回null,可以用到nullFirst等适配器。这些静态方法会修改现有的比较器,从而在遇到null值时不会抛出异常,而是将这个值标记为小于或大于正常值。
内部类
内部类(inner class)是定义在另一个类中的类。
嵌套是一种类之间的关系,而不是对象之间的关系。一个LinkedList对象并不包含Iterator类型或Link类型的子对象。嵌套类的好处:命名控制和访问控制。在Java中,只有内部类能够实现这样的控制。
然而,Java内部类还有另外一个功能,内部类的对象有一个隐式引用,它引用了实例化该内部对象的外围类对象。通过这个指针,可以访问外围类对象的全部状态。
在Java中,static内部类没有这种附加指针。
TODO
代理
利用代理可以在运行时创建一个实现了一组给定接口的新类。这种功能只有在编译时无法确定需要实现哪个接口时才有必要使用。
何时使用代理
表示接口的Class对象,确切类型在编译时无法确定,此时想要构造一个实现这些接口的类,需要使用newInstance方法或反射找出这个类的构造器,但是不能实例化一个接口,需要在运行时定义一个新类。代理类可以在运行时创建全新的类。这样的代理类能够实现指定的接口。
每个代理实例都有一个关联的调用处理程序(invocation handler)。当在代理实例上调用一个方法时,方法调用被编码并分派到其调用处理程序的方法。
无论何时调用代理对象的方法,调用处理器的invoke方法都会被调用,并向其传递Method对象和原始的调用参数。调用处理器必须给出处理调用的方式。
创建代理对象
要想创建一个代理对象,需要使用Proxy类的newProxyInstance方法。
代理类的特性
代理类是在程序运行过程中创建的。然而,一旦被创建,就变成了常规类,与虚拟机
中的任何其他类没有什么区别。
所有的代理类都扩展于Proxy类。一个代理类只有一个实例域——调用处理器,它定义在Proxy的超类中。为了履行代理对象的职责,所需要的任何附加数据都必须存储在调用处理器中。
所有的代理类都覆盖了Object类中的方法toString、equals和hashCode。如同所
有的代理方法一样,这些方法仅仅调用了调用处理器的invoke。
对于特定的类加载器和预设的一组接口来说,只能有一个代理类。
代理类一定是public和final。如果代理类实现的所有接口都是public,代理类不属于某个特定的包;否则,所有非公有的接口都必须属于同一个包,同时,代理类也属于这个包。
调用Proxy类中的isProxyClass方法检测一个特定的Class对象是否代表一个代理类。
克隆和代理是库设计者和工具构造者感兴趣的高级技术。