命运的无常不会使我疯狂,如山顶松柏屹立在狂风暴雨中央——岛屿心情《游》
想要使用Arrays.sort()就必须要对排序类实现Comparable这个接口。
public interface Comparable
{
int compareTo(Object other);
}
任何实现 Comparable 接口的类都需要包含 compareTo 方法,并且这个方法的参数必须是一个 Object 对象,返回一个整型数值。
在 JavaSE 5.0 中,Comparable 接口已经改进为泛型类型。
public interface Comparable<T> {
int compareTo(T other) ; // parameter has type T
}
引入泛型的好处在于实现这个方法的时候不用再强制转换类型了。
接口中的所有方法自动地属于 public。 因此,在接口中声明方法时,不必提供关键字public。
为了让类实现一个接口, 通常需要下面两个步骤:
例如:
class Employee implement Comparable
比较两个浮点数的大小,例如x,y不能使用x-y,应该使用Double.compare(x,y),因为如果x和y非常接近的时候,x-y可能会四舍五入等于0。
为什么想实现排序服务不能给类直接提供一个 compareTo 方法,而必须实现 Comparable 接口呢?因为,于 Java 程序设计语言是一种强类型 ( strongly typed) 语言。在调用方法的时候, 编译器将会检查这个方法是否存在。如果a是一个Comparable对象的数组,就可以确保一定有compareTo方法。因为每个实现 Comparable 接口的类都必须提供这个方法的定义。通过接口检查。
有人认为, 将 Arrays 类中的 sort 方法定义为接收一个 Comparable[ ] 数组就可以在使用元素类型没有实现 Comparable 接口的数组作为参数调用 sort 方法时, 由编译器给出错误报告。但事实并非如此。在这种情况下, sort 方法可以接收一个 Object[ ] 数组, 并对其进行笨拙的类型转换:
// Approach used in the standard library not recommended
if (((Comparable) a[i]).compareTo(a[j]) > 0) {
// rearrange a[i] and a[j]
}
如果 a[i] 不属于实现了 Comparable 接口的类, 那么虚拟机就会抛出一个异常。所以必须要实现Comparable接口才能实现排序。
当发生父类与子类进行比较的情况的时候,会发生和实现equals相同的情况,如果x是一个Employee对象,y是一个Manager对象,调用x.compareTo(y)不会抛出异常,它只是将x和y都作为雇员进行比较。但是反过来,y.compareTo(x)将会抛出一个ClassCastException。有以下两种解决办法:
if (getClass() != other.getClass()) throw new ClassCastException();
接口不是类,尤其不能使用 new 运算符实例化一个接口。然而, 尽管不能构造接口的对象,却能声明接口的变量,接口变量必须弓I用实现了接口的类对象。可以使用instance 检查一个对象是否实现了某个特定的接口。
虽然在接口中不能包含实例域或静态方法,但却可以包含常量。例如:
public interface Powered extends Moveable
{
double milesPerCallonO;
double SPEED_LIHIT = 95; // a public static final constant
}
与接口中的方法都自动地被设置为 public—样,接口中的域将被自动设为 public static final。
Java 语言规范却建议不要书写这些多余的关键字,所以定义接口方法或者变量的时候不必再写修饰符。
为什么 Java 程序设计语言还要不辞辛苦地引入接口概念?
每个类只能扩展于一个类。但是每个类可以实现多个接口,而且多继承会让语言本身变得非常复杂(如同 C++,) 效率也会降低(如同 Eiffel)。实际上,接口可以提供多重继承的大多数好处,同时还能避免多重继承的复杂性和低效性。
在 Java SE 8 中,允许在接口中增加静态方法。目前为止, 通常的做法都是将静态方法放在伴随类中。
来看 Paths 类, 其中只包含两个工厂方法。可以由一个字符串序列构造一个文件或目录的路径, 如 Paths.getfjdk1.8.0", “jre”, “bin”。) 在 Java SE 8 中, 可以为 Path 接口增加以下方法:
public interface Path
{
public static Path get(String first, String... more) {
return Fi1eSystems.getDefault().getPath(first, more);
}
}
这样一来, Paths 类就不再是必要的了。实际上再Java11中确实提供了等价的方法:
public interface Path
{
public static Path of(String first, String... more) {....}
ublic static Path of(URI uri) {....}
}
再Java9中,接口的方法可以是private。private可以是静态方法或者实例方法,由于private修饰之后只能在接口本身的方法中调用,所以用法很有限,只能作为接口中其他方法的辅助方法。
可以为接口方法提供一个默认实现。 必须用 default 修饰符标记这样一个方法。
public interface Comparable<T>
{
default int compareTo(T other) { return 0; }
// By default, all elements are the same
}
有些情况下, 默认方法很有用。
默认方法的一个重要用法是“ 接口演化” (interface evolution。) 以 Collection 接口为例,这个接口作为 Java 的一部分已经有很多年了。假设很久以前你提供了这样一个类:public class Bag implements Collection
后来, 在 JavaSE 8 中, 又为这个接口增加了一个 stream 方法。假设 stream 方法不是一个默认方法。那么 Bag 类将不能编译, 因为它没有实现这个新方法。为接口增加一个非默认方法不能保证“源代码兼容”。将方法实现为一个默认方法就可以解决这个问题。
总得来说就是,一个类继承的超类和实现的接口中有相同的方法,或者实现的多个接口里面有相同的方法。这个时候就会发生方法冲突,有下面几个规则:
看第二个规则,考虑另一个包含 getName 方法的接口:
interface Named
{
default String getName() { return getClass() .getName() + "_" + hashCode() ; }
}
interface Person
{
default String getName() { return "this is Person"; }
}
如果有一个类同时实现了这两个接口会怎么样呢?类会继承 Person 和 Named 接口提供的两个不一致的 getName 方法。并不是从中选择一个,Java 编译器会报告一个错误,让程序员来解决这个二义性。只需要Student 类中提供一个 getName 方法。在这个方法中,可以选择两个冲突方法中的一个,如下所示:
class Student implements Person, Named
{
public String getName () { return Person.super.getName(); }
}
如果两个接口都没有为共享方法提供默认实现, 那么就与 Java SE 8之前的
情况一样,这里不存在冲突。 实现类可以有两个选择:实现这个方法,或者干脆不实现。如果是后一种情况,这个类本身就是抽象的。
回调是一种常见的程序设计模式。在这中模式中,可以指定某个特定事件发生时应该采取的动作。
Comparator接口和Comparable实现的功能差不多,都是为排序而生,但是Comparator接口适用面更广,例如想按照String字符串的长度进行排序,肯定不能让String类用两种不同的方式实现compareTo方法——更何况,String类也不应由我们来修改。
怎么使用Comparator接口呢?使用Arrays.sort(arr,Comparator),第二个参数使用Comparator比较器作为参数。
默认的clone方法是Object类的protected修饰的,当调用clone方法时,如果对象的字段都是数值或者基本类型的,那没有任何问题,但是当对象的数值是其他对象的引用的时候,会拷贝出相同子对象的另外一个引用,也就是原对象与拷贝的对象的引用会指向同一个对象。这不是我们想要的。这种复制叫做浅拷贝。
所以想要原对象与拷贝出来的对象的两个引用指向两个对象就需要重写clone方法。例如下:
public class ImplementMyInterface implements Cloneable{
private InterEmployee employee;
public InterEmployee getEmployee() throws CloneNotSupportedException {
return employee.clone();
}
public void setEmployee(InterEmployee employee) {
this.employee = employee;
}
public ImplementMyInterface() {
}
//默认提供的clone是浅克隆,就是如果一个类中的域是其他类的类型,那么克隆出来的两个对象会同时指向这个对象的引用所以还是要重写clone方法
public ImplementMyInterface clone() throws CloneNotSupportedException{
ImplementMyInterface implementMyInterface= (ImplementMyInterface) super.clone();
implementMyInterface.employee=employee.clone();
return implementMyInterface;
}
@Override
public String toString() {
return "ImplementMyInterface{" +
"employee=" + employee +
'}';
}
}
ImplementMyInterface 类中含有InterEmployee类的变量。InterEmployee如下。
public class InterEmployee implements Cloneable{
private String name;
private int age;
private double salary;
public InterEmployee() {
}
public InterEmployee(String name, int age, double salary) {
this.name = name;
this.age = age;
this.salary = salary;
}
@Override
public String toString() {
return "InterEmployee{" +
"name='" + name + '\'' +
", age=" + age +
", salary=" + salary +
'}';
}
public InterEmployee clone() throws CloneNotSupportedException {
return (InterEmployee) super.clone();
}
}
这个时候就可以实现ImplementMyInterface的克隆,为什么呢,且看代码当中,两个类都实现了Cloneable接口,在ImplementMyInterface中先克隆出自己,这个时候原对象与克隆出的对象的变量的值为InterEmployee对象的同一个引用(可以理解为指针指向同一个地址)。再使用
implementMyInterface.employee=employee.clone();
就可以实现两个变量赋值为两个不同的对象引用了,这个时候不再是浅拷贝了。那InterEmployee怎么就能拷贝了呢,因为InterEmployee对象中全部都是基本类型,也实现了Cloneable接口,重写Clone方法很简单调用
super.clone();
就可以了。
补充一下:
Object o=new Object();
o.clone();//error
不能使用o.clone方法,有两个原因,可以查看Object的类的clone方法如下:
protected native Object clone() throws CloneNotSupportedException;
首先,是protected修饰,不在同一个包下,对这个方法是没有访问权限的,其次是由native修饰,native修饰就是说java使用了一个并不是java语言书写的方法,他的运行速度比我们写的要快的多因为更贴近系统。所以对一个对象实现克隆推荐使用默认的clone方法。
所有的数组都有一个公共的clone方法,而不是受保护的。所以可以直接使用array.clone()。
Comparator<String> comp
= (first, second) // Same as (String first, String second) -> first.lengthO - second.lengthO;
对于只有一个抽象方法的接口, 需要这种接口的对象时, 就可以提供一个 lambda 表达式。这种接口称为函数式接口 ( functional interface )。把 lambda 表达式看作是一个函数,而不是一个对象, 另外要接受 lambda 表达式可以传递到函数式接口。
不能把丨ambda 表达式赋值给类型为 Object 的变量,Object 不是一个函数式接口。
Java API 在java.util.fimction 包中定义了很多非常通用的函数式接口。例如:java.util.function 包中有一个尤其有用的接口 Predicate:
public interface Predicate<T> {
boolean test(T t); // Additional default and static methods
}
ArrayList 类有一个 removelf 方法, 它的参数就是一个 Predicate。这个接口专门用来传递
lambda 表达式。例如,下面的语句将从一个数组列表删除所有 null 值:
list.removelf(e -> e == null);
举个例子:
Timer t = new Timer(1000, event -> System.out.println(event)):
Timer t = new Timer(1000, Systei.out::println);//二者等价
主要有3种情况:
构造器引用与方法引用很类似,只不过方法名为 new。
ArrayList<String> names = . . .;
Stream<Person> stream = names.stream().map(Person::new);
List<Person> people = stream.col1ect(Col1ectors.toList());
Java 有一个限制,无法构造泛型类型 T 的数组。数组构造器引用对于克服这个限制很有用。
例如,假设我们需要一个 Person 对象数组。Stream 接口有一个 toArray 方法可以返回 Object 数组:
Object[] people = stream.toArray();
不过,这并不让人满意。用户希望得到一个 Person 引用数组,而不是 Object 引用数组。流库利用构造器引用解决了这个问题。可以把 Person[]::new传入toArray 方法:
Person[] people = stream.toArray(Person[]::new);
toArray方法调用这个构造器来得到一个正确类型的数组。然后填充这个数组并返回。
通常, 你可能希望能够在 lambda 表达式中访问外围方法或类中的变量。例如:
public static void repeatMessage(String text, int delay) {
ActionListener listener = event ->
System.out.println(text):
Toolkit.getDefaultToolkitO.beep();};
new Timer(delay, listener).start0;
}
lambda 表达式的代码可能会在repeatMessage 调用返回很久以后才运行,而那时这个参数变量已经不存在了。 如何保留 text变量呢?
要了解到底会发生什么,下面来巩固我们对 lambda 表达式的理解 lambda 表达式有 3个部分:
public static void countDown(int start, int delay) {
ActionListener listener = event ->
{
start ; // Error: Can't mutate captured variable
System.out.println(start);
};
new Timer(delay, listener),start();
}
另外如果在 lambda 表达式中引用变量, 而这个变量可能在外部改变,这也是不合法的。lambda 表达式中捕获的变量必须实际上是最终变量 ( effectivelyfinal。)实际上的最终变量是指, 这个变量初始化之后就不会再为它赋新值。在这里,text 总是指示同一个 String 对象,所以捕获这个变量是合法的。
在方法中,不能有两个同名的局部变量, 因此, lambda 表达式中同样也不能有同名的局部变量。
在一个 lambda 表达式中使用 this 关键字时, 是指创建这个 lambda 表达式的方法的 this参数。
public class ApplicationO
{
public void init()
{
ActionListener listener * event ->
{
System.out.print n(this.toStringO);
...
}
...
}
}
表达式 this.toString()会调用 Application 对象的 toString方法, 而不是 ActionListener 实例的方法。在 lambda 表达式中, this 的使用并没有任何特殊之处。lambda 表达式的作用域嵌套在 init 方法中,与出现在这个方法中的其他位置一样, lambda 表达式中 this 的含义并没有变化。
使用 lambda 表达式的重点是延迟执行。毕竟, 如果想耍立即执行代
码,完全可以直接执行, 而无需把它包装在一个Lambda 表达式中。之所以希望以后再执行代码, 这有很多原因, 如:
常用函数式接口。
表 6-2 列出了基本类型 int、 long 和 double 的 34 个可能的规范。 最好使用这些特殊化规范来减少自动装箱。
最好使用表 6-1 或表 6-2 中的接口。 例如, 假设要编写一个方法来处理满足某个特定条件的文件。 对此有一个遗留接口 java.io.FileFilter, 不过最好使用标准的Predicate , 只有一种情况下可以不这么做, 那就是你已经有很多有用的方法可以生成 FileFilter 实例。大多数标准函数式接口都提供了非抽象方法来生成或合并函数。 例如, Predicate.isEqual(a) 等同于 a::equals, 不过如果 a 为 null 也能正常工作。已经提供了默认方法 and、or 和 negate 来合并谓词。 例如Predicate.isEqual⑻.or(Predicate.isEqual(b)) 就等同于 x -> a.equals(x) || b.equals(x)。
如果设计你自己的接口,其中只有一个抽象方法,可以用 @FunctionalInterface 注解来标记这个接口。这样做有两个优点。 如果你无意中增加了另一个非抽象方法, 编译器会产生一个错误消息。 另外 javadoc 页里会指出你的接口是一个函数式接口。
Comparator 接口包含很多方便的静态方法来创建比较器。这些方法可以用于lambda表达式或方法引用。
可以把比较器与 thenComparing 方法串起来。如下:
Arrays.sort(people ,
Comparator.comparing(Person::getlastName)
.thenConipari ng(Pe rson::getFi rstName));
另外, comparing 和 thenComparing 方法都有变体形式,可以避免 int、 long 或 double 值的装箱。要完成前一个操作, 还有一种更容易的做法:
Arrays.sort(people, Comparator.comparinglnt(p -> p.getName().
length()));
如果键函数可以返回 null, 可 能 就 要 用 到 nullsFirst 和 nullsLast 适配器。例如, 假设一个人没有中名时 getMiddleName 会返回一个 null, 就可以使用Comparator.comparing(Person::getMiddleName(),Comparator.nullsFirst(… )。nullsFirst 方法需要一个比较器,在这里就是比较两个字符串的比较器。naturalOrder 方法可以为任何实现了 Comparable 的类建立一个比较器。下面是一个完整的调用, 可以按可能为 null 的中名进行排序。这里使用了一个静态导人 java.util.C0mparator.*,以便理解这个表达式。注意 naturalOrder 的类型可以推导得出。
Arrays.sort(people, comparing(Person::getMiddleName , nullIsFi rst(naturalOrder())));
静态 reverseOrder 方法会提供自然顺序的逆序。要让比较器逆序比较,可以使用 reversed实例方法。 例如 naturalOrder().reversed() 等同于reverseOrder()。
内部类( inner class) 是定义在另一个类中的类。为什么需要使用内部类呢?
内部类的对象有一个隐式引用, 它引用了实例化该内部对象的外围类对象。通过这个指针, 可以访问外围类对象的全部状态。在 Java 中,static 内部类没有这种附加指针。
内部类既可以访问自身的数据域,也可以访问创建它的外围类对象的数据域。 内部类的对象总有一个隐式引用, 它指向了创建它的外部类对象。
这个引用在内部类的定义中是不可见的。外围类的引用在构造器中设置。编译器修改了所有的内部类的构造器, 添加一个外围类引用的参数。
例如TimePrinter是TalkingClock的内部类:
public TimePrinter(TalkingClock clock) // automatically generated code
{
outer = clock;
}
outer 不是 Java 的关键字。我们只是用它说明内部类中的机制。
只有内部类可以是私有类,而常规类只可以具有包可见性,或公有可见性。
可以通过显式地命名将外围类引用设置为其他的对象。例如, 如果 TimePrinter 是一个公有内部类,对于任意的语音时钟都可以构造一个 TimePrinter:
TalkingClock jabberer = new Ta1kingClock(1000, true);
TalkingOock.TiiePrinter listener = jabberer.new TimePrinter();
需要注意, 在外围类的作用域之外,可以这样引用内部类:
OuterClass.InnerClass
内部类中声明的所有静态域都必须是 final。原因很简单。我们希望一个静态域只有一个实例, 不过对于每个外部对象, 会分别有一个单独的内部类实例。如果这个域不是 final , 它可能就不是唯一的。
内部类不能有 static 方法。Java 语言规范对这个限制没有做任何解释。也可以允许有静态方法,但只能访问外围类的静态域和方法。显然,Java 设计者认为相对于这种复杂性来说, 它带来的好处有些得不偿失。
内部类的语法很复杂(可以看到,稍后介绍的匿名内部类更加复杂 )。它与访问控制和安全性等其他的语言特性的没有明显的关联。
javap 程序运行 ReflectionTest 程序:TimePrinter是TalkingClock的一个内部类,这时会看到下面的输出结果:
public class TalkingClockSTimePrinter
{
public TalkingGockJTimePrinter(TalkingCtock);
public void actionPerformed(java.awt.event.ActionEvent);
final TalkingClock this$();
}
可以清楚地看到, 编译器为了引用外围类, 生成了一个附加的实例域 this$0 (名字this$0 是由编译器合成的,在自己编写的代码中不能够引用它)。另外,还可以看到构造器的TalkingClock 参数.
内部类如何管理那些额外的访问特权呢?利用ReflectTest 程序査看一下 TalkingClock 类:
class TalkingClock
{
private int interval;
private boolean beep;
publ ic TalkingClock(int, boolean);
static boolean access$O(TalkingClock);
public void start();
}
请注意编译器在外围类添加静态方法 accessSO。它将返回作为参数传递给它的对象域beep。(方法名可能稍有不同,如 access$000, 这取决于你的编译器。)
内部类只在一个方法种创建这个对象使用一次的时候可以使用局部内部类。
例如:
public void start0 {
class TimePrinter implements ActionListener
{
public void actionPerforaed(ActionEvent event) {
System.out.println("At the tone, the tine is " + new Date());
if (beep) Toolkit.getDefaultToolkit().beep();
}
}
ActionListener listener = new TimePrinter();
Timer t = new Timer(interva1, listener); t.start();
}
局部类不能用 public 或 private 访问说明符进行声明。它的作用域被限定在声明这个局部类的块中。局部类有一个优势, 即对外部世界可以完全地隐藏起来。
与其他内部类相比较,局部类还有一个优点。它们不仅能够访问包含它们的外部类, 还可以访问局部变量。不过,那些局部变量必须事实上为 final。这说明, 它们一旦赋值就绝不会改变。
public void start(int interval, boolean beep) {
class TimePrinter implements ActionListener
{
public void actionPerformed(ActionEvent event) {
System.out.println("At the tone, the tiie is " + new Date());
if (beep) Toolkit.getDefaultToolkit().beep();
}
}
ActionListener listener = new TimePrinter();
Timer t = new Timer(interval, listener); t.start();
}
访问外部变量的控制流程:
为了能够让 actionPerformed方法工作,TimePrinter 类在 beep 域释放之前将 beep 域用start 方法的局部变量进行备份。
在 JavaSE 8 之前, 必须把从局部类访问的局部变量声明为 final。
例如, start 方法原本就应当这样声明, 从而使内部类能够访问 beep 参数:
public void start(int interval , final boolean beep);
将局部内部类的使用再深人一步。 假如只创建这个类的一个对象,就不必命名了。这种类被称为匿名内部类(anonymous inner class)。
public void start(int interval, boolean beep) {
ActionListener listener = new ActionListenerO
{
public void actionPerformed(ActionEvent event) {
System.out.println("At the tone, the time is " + new Date());
if (beep) Toolkit.getDefaultToolkit().beep();
}
};
Timer t = new Timer(interval, listener); t.start();
}
通常的语法格式为:
new SuperType(construction parameters)
{
inner class methods and data
}
由于构造器的名字必须与类名相同, 而匿名类没有类名,所以,匿名类不能有构造器。取而代之的是,将构造器参数传递给超类( superclass) 构造器。尤其是在内部类实现接口的时候, 不能有任何构造参数。还要像提供一组括号。
多年来,Java 程序员习惯的做法是用匿名内部类实现事件监听器和其他回调。如今最好还是使用 lambda 表达式。
下面的技巧称为“ 双括号初始化” (double brace initialization), 这里利用了内部类语法。
Object[] objects = new ArrayList<String>() {
{
add("I");
add("Love");
add("you");
}
}.toArray();
我们曾建议 equals 方法最好使用以下测试:
if (getClass() != other.getClass()) return false;
但是对匿名子类做这个测试时会失败。
生成日志或调试消息时, 通常希望包含当前类的类名, 如:
Systen.err.println("Something awful happened in " + getClass());
不过,这对于静态方法不奏效。毕竟, 调用 getClass 时调用的是 this.getClass(), 而静态方法没有 this。所以应该使用以下表达式:
new Object(){}.getCIass().getEndosingClass() // gets class of static method
在这里,newObject(){} 会建立 Object 的一个匿名子类的一个匿名对象,getEnclosingClass
则得到其外围类, 也就是包含这个静态方法的类。
有时候, 使用内部类只是为了把一个类隐藏在另外一个类的内部,并不需要内部类引用外围类对象。为此,可以将内部类声明为 static, 以便取消产生的引用。只有内部类可以声明为 static。静态内部类的对象除了没有对生成它的外围类对象的引用特权外, 与其他所冇内部类完全一样。
这里就贴个例子吧。
package com.myProxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Random;
/**
* 100个数字里面查找一个数字
* 使用代理类来跟踪调用的方法
*/
public class ProxyTest {
public static void main(String[] args) {
Object []objects=new Object[1000];
for(int i=0;i<objects.length;i++){
Integer integer=new Integer(i+1);
TraceHandler handler=new TraceHandler(integer);
Object proxy=Proxy.newProxyInstance(
ClassLoader.getSystemClassLoader(),
new Class[]{Comparable.class},handler
);
objects[i]=proxy;
}
Integer key=new Random().nextInt(objects.length)+1;
int result= Arrays.binarySearch(objects,key);
if (result>=0) System.out.println(objects[result]);
}
}
class TraceHandler implements InvocationHandler{
private Object target;
public TraceHandler(Object object) {
this.target = object;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//输出target的值
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);
}
}
克隆和代理是库设计者和工具构造者感兴趣的高级技术, 对应用程序员来说,它们并不十分重要。(我不这么认为)