接口(interface)是用来描述类应该做什么,而不指定它们具体如何做。一个类可以实现(implement)一个或多个接口。在Java中,接口不是类,而是对希望符合这个接口的类的一组需求。接口中的所有方法都自动是public。因此,在接口的声明中,不必提供关键字public。不过,在实现接口时,必须把方法声明为public;否则,编译器将认为这个方法的访问属性是包可见性,这是类的默认访问属性,之后编译器就会报错,指出程序员试图提供更严格的访问权限。
CUE:在Java 5中,Comparable接口已经提升为一个泛型类型。
接口还可以定义常量。不过接口绝不会有实例字段,在Java 8 之前,接口中绝对不会实现方法(现在已经可以在接口中提供简单方法了。当然,这些方法不能引用实例字段——接口没有实例)。提供实例字段和方法的实现的任务应该由实现接口的那个类来完成。为了让类实现一个接口,通常需要完成以下两个步骤:
1)将类声明为实现给定的接口。
2)对接口中的所有方法提供定义。
要将类声明为实现某个接口,需要使用关键字implements。
CUE:Java是一种强类型(strongly typed)语言。
部分API如下:
接口不是类。具体来说,不能使用new运算符实例化一个接口。不过,尽管不能构造接口的对象,却能声明接口的变量,接口变量必须引用实现了这个接口的类对象。像使用instanceof检查一个对象是否属于某个特定类一样,也可以使用instanceof检查一个对象是否实现了某个特定的接口。与建立类的继承层次一样,也可以扩展接口。允许有多条接口链,从通用性较高的接口扩展到专用性较高的接口。与接口中的方法都自动被设置为public一样,接口中的字段总是public static final。尽管每个类只能有一个超类,但却可以实现多个接口,使用逗号将想要实现的各个接口分隔开。
CUE:C++允许一个类有多个超类,这种特性称为多重继承。Java不支持多重继承。
在Java 8中,允许在接口中增加静态方法。在Java 9中,接口中的方法可以是private。private方法可以是静态方法或实例方法。可以为接口方法提供一个默认实现。必须使用default修饰符标记这样一个方法。接口中默认方法也可以调用其他方法。默认方法的一个重要用法是“接口演化”。为接口增加一个非默认方法不能保证“源代码兼容”(source compatible),只可以保证“二进制兼容”。如果将方法实现为一个默认方法就可以解决这两个问题。 当默认方法发生冲突时,Java规定如下:
1)超类优先。如果超类提供了一个具体方法,同名而且有相同参数类型的默认方法会被忽略。
2)接口冲突。如果一个接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型(不论是否是默认参数)相同的方法,必须覆盖这个方法来解决冲突。
当一个类扩展了一个超类同时实现了一个接口,并从超类和接口继承了相同的方法。在这种情况下,只会考虑超类的方法,接口的所有默认方法都会被忽略。这正是“类优先”规则。“类优先”规则可以确保与Java 7的兼容性。
回调(callback)是一种常见的程序设计模式。在这种模式中,可以指定某个特定的事件发生时应该采取的动作。
部分AIP如下:
Cloneable接口指示一个类提供了一个安全的clone方法。Cloneable接口是Java提供的少数标记接口(tagging interface)之一(又称之为记号接口(marker interface))。标记接口不包含任何方法;它唯一的作用就是允许在类型查询中使用instanceof。clone方法是Object的一个protected方法。在Java 1.4之间,clone方法的返回类型是Object,而现在是可以为clone方法指定正确的返回类型。如果在一个对象上调用clone,但这个对象的类并没有实现Cloneable接口,Object类的clone方法就会抛出一个CloneNotSupportedException。默认的克隆操作是“浅拷贝”,并没有克隆对象中引用的其他对象。与此对应的深拷贝,会同时克隆所有的子对象。所有数组类型都有一个公共的clone方法,而不是受保护的。可以调用这个方法建立一个新数组,包含原数组所有元素的副本。关于clone,对于每个类,需要确定:
1)默认的clone方法是否满足需求;
2)是否可以在可变的子对象上调用clone来修补默认的clone方法。
3)是否不该使用clone。
实际上第3个选项是默认选项。如果选择第1或第2项,类必须:
1)实现Cloneable接口
2)重新定义clone方法,并指定public修饰符。
lambad表达式是一个可传递的代码块,以及必须传入代码的变量规范。它可以在以后执行一次或多次(也可以说带参数变量的表达式就被称为lambda表达式)。Java中的一种lambad表达式形式为:参数,箭头(->)以及一个表达式。如果代码要完成的计算无法放在一个表达式中,就可以像写方法一样,把这些代码放在**{}**中,并包含显示的return语句。即使lambad表达式没有参数,仍然要提供空括号,就像无参方法一样。如果可以推导出一个lambad表达式的参数类型,则可以忽略其类型。如果方法只有一个参数,而且这个参数的类型可以推导出,那么甚至还可以省略小括号。无需指定lambad表达式的返回类型。lambad表达式的返回类型总是会由上下文推导得出。如果一个lambad表达式只在某些分支返回一个值,而另外一些分支不返回值,这是不合法的。
对于只有一个抽象方法的接口,需要这种接口的对象时,就可以提供一个lambad表达式。这种接口称为函数式接口(funtional interface)。最好把lambad表达式看作是一个函数,而不是一个对象,另外要接受lambad表达式可以传递到函数式接口。lambad表达式是可以转换为接口的。而实际上,在Java中,对lambad表达式所能做的也只是转换为函数式接口。注意,不能把lambad表达式赋给类型为Object的变量,Object不是函数式接口。
有时,lambad表达式会涉及一个方法,当遇到这种情况时,可以使用方法引用(method reference)。方法引用,它指示编译器生成一个函数式接口,覆盖这个接口的抽象方法来调用给定的方法。类似于lambad表达式,方法引用不是一个对象。不过,为一个类型为函数式接口的变量赋值时会生成一个对象。要用 :: 运算符分隔方法名与对象或类名。主要分为以下3种情况:
1)Object::instanceMthod
2)Class::instanceMthod
3)Class::staticMthod
在第1种情况下,方法引用等价于向方法传递参数的lambad表达式。对于第2种情况,第1个参数会成为方法的隐式参数。在3种情况下,所有参数都传递到静态方法。部分方法引用示例如下:
注意,只有当lambad表达式的体只调用一个方法而不做其他操作时,才能把lambad表达式重写为方法引用。类似于lambad表达式,方法引用不能独立存在,总是会转换为函数式接口的实例。可以在方法引用中使用this参数,使用super也是合法的。
构造器引用与方法引用很类似,只不过方法名为new。可以用数组类型建立构造器引用。Java有一个限制,无法构造泛型类型T的数组。
lambad表达式有3个部分:
1)一个代码块;
2)参数;
3)自由变量的值,这是指非参数而且不在代码中定义的变量。
关于代码块以及自由变量值有一个术语:闭包(closure)。因此,在Java中,lambad表达式就是闭包。lambad表达式可以捕获外围作用域中变量的值。在lambad表达式中,只能引用不会改变的变量。也就是说,lambad表达式中捕获的变量必须实际上是事实最终变量(effectively final)。事实最终变量是指,这个变量初始化之后就不会再为它赋新值。lambad表达式的体与嵌套块有相同的作用域。这里同样适用于命名冲突和遮蔽的有关规则。在lambad表达式中声明与一个局部变量同名的参数或局部变量是不合法的。在一个方法中,不能有两个同名的局部变量,因此,lambad表达式中同样不能有同名的局部变量。在一个lambad表达式中使用this关键字时,是指创建这个lambad表达式方法的this参数。
使用lambad表达式的重点是延迟执行。Java API中最重要的函数式接口如下:
基本类型的函数式接口如下:
CUE:大多数标准函数式接口都提供了非抽象方法来生成或合并函数。
内部类(inner class)是定义在另一个类中的类。使用内部类主要有如下两个原因:
1)内部类可以对同一个包中的其它类隐藏
2)内部类方法可以访问定义这个类的作用域中的数据,包含原本私有数据。
就像C++里的嵌套类一样。被嵌套的类包含在外围类的作用域内。嵌套类就与Java中的内部类很类似。内部类的对象会有一个隐式引用,指向实例化这个对象的外部类对象。通过这个指针,它可以访问外部对象的全部状态。在Java中,静态内部类没有这个附加的指针,所有Java的静态内部类就相当于C++中的嵌套类。外围类的引用在构造器中设置。编译器会修改所有的内部类构造器,添加一个对应外围类引用的参数。
内部类中声明的所有静态字段都必须是final,并初始化为一个编译时常量。如果这个字段不是一个常量,就可能不唯一。内部类不能有static方法。Java语言规范对这个现在没有做任何解释。也可以允许有静态方法,但只能访问外围类的静态字段和方法。内部类是一个编译器现象,与虚拟机无关。编译器将会把内部类转化为常规的类文件,用$(美元符号)分隔外部类名与内部类吗,而虚拟机则对此一无所知。
声明局部类时不能有访问说明符(即public或private)。局部类的作用域被限定在声明这个局部类的块中。与其他内部类相比,局部类的优点有,它们不仅能够访问外部类的字段,还可以访问局部变量!当然,那些局部变量必须是事实最终变量(effectively final)。这说明,它们一旦赋值就绝不会改变。
如果只想创建这个类的一个对象,甚至不需要为类指定名字。这样一个类被称为匿名内部类(anonymous inner class)。一般的,语法如下:
new SuperType(construction parameters)
{
inner class methods and data
}
其中,SuperType可以是接口,如果是这样,内部类就要实现这个接口。SuperType可以是一个类,如果是这样,内部类就要扩展这个类。由于构造器的名字必须于类名相同,而匿名内部类没有类名,所以,匿名内部类不能有构造器。构造一个类型的新对象与构造一个扩展了那个类的匿名内部类的对象之间的差别是,如果构造参数列表的结束小括号后面跟一个开始大括号,就是在定义匿名内部类。尽管匿名内部类不能有构造器,但可以提供一个对象初始化块。
当内部类被声明为static时,该内部类就是个静态内部类。静态内部类就类似于其他内部类,只不过静态内部类的对象没有生成它的外围类对象的引用。只要内部类不需要访问外围类对象,就应该使用静态内部类。有些人会用嵌套类表示静态内部类。与常规内部类不同,静态内部类可以有静态字段和方法。在接口中声明的内部类自动是static和public。
与服务加载器相关的API如下:
利用代理(proxy)可以在运行时创建实现了一组给定接口的新类。具体地,代理类包含以下方法:
1)指定接口所需要的全部方法
2)Object类中的全部方法,如,toString等。
但不能在运行时为这些方法定义新代码。必须提供一个调用处理器(invocation handler)。调用处理器是实现了InvocationHandler接口的类的对象。这个接口只有一个invoke方法。方法签名如下:
Object invoke(Object proxy,Mehtod method,Object[] args)
无论何时调用代理对象的方法,调用处理器的invoke方法都会被调用,并向其传递Method对象和原调用的参数。之后调用处理器必须确定如何处理这个调用。创建一个代理对象,需要使用Proxy类的newProxyInstance方法。该方法有三个参数:
1)一个类加载器(class loader)
2)一个Class对象数组,每个元素对应需要实现的各个接口。
3)一个调用处理器。
需要记住,代理类是在程序运行过程中动态创建的。然而,一旦被创建,它们就变成常规类,与虚拟机中的任何其他类没有什么区别。所有的代理类都扩展Proxy类。一个代理类只有一个实例字段——即调用处理器,它在Proxy超类中定义。完成代理对象任务所需要的任何额外数据都必须存储在调用处理器中。所有的代理类的都要覆盖Object类的toString、equals和hasCode方法。没有定义代理类的名字,Oracle虚拟机就中的Proxy类将生成一个以字符串$Proxy开头的类名。对于一个特定的类加载器和预设的一组接口来说,只能有一个代理类。代理类总是public和final。如果代理类实现的所有接口都是public,这个代理了就不属于任何特定的包;否则,所有非公共的接口都必须属于一个包,同时,代理类也属于这个包。可以通过调用Proxy类的isProxyClass方法检测一个特定的Class对象是否表示一个代理类。
代理类相关的API如下: