上一篇博客我们探讨了 Java 的接口,主要是设计自己的接口并使用它。
那么这篇博客,我们来探讨一下 Java 的内置接口:
我们都知道,在 Java 程序设计语言中,接口不是类,它是对类的一组需求描述,这些类要遵从接口描述的统一格式进行定义。
我们经常听到服务提供商这样说:“如果类遵从某个特定接口,那么就履行这项服务”。下面给出一个具体的示例。Arrays 类中的 sort 方法承诺可以对对象数组进行排序,但要求满足下列前提: 对象所属的类必须实现了 Comparable 接口。
Comparable 接口是 Java SDK 中的一个内置的泛型接口。
下面是 Comparable 接口的代码:
public interface Comparable<T>{
int compareTo(T other);
}
为了让类实现一个接口,通常需要下面两个步骤:
- 将类声明为实现给定的接口。
- 对接口中的所有方法进行定义。
要将类声明为实现某个接口,需要使用关键字 implements。当然,也需要实现 CompareTo 方法:
class Employee implements Comparable<Employee>{
public int compareTo(Employee other){
return Double.compare(salary,other.salary);
}
...
}
注意: 在这里,我们使用了静态方法 Double.compare 方法,如果第一个参数小于第二个参数,它会返回一个负值;如果二者相等返回 0;否则返回一个正值。
下面这个代码给出了对一个 Employee 类实例数组进行排序的完整代码,用于对一个员工数组排序:
Employee 类代码
package interfaces;
public class Employee implements Comparable<Employee>{
private String name;
private double salary;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
public String getName() {
return name;
}
public double getSalary() {
return salary;
}
public void raiseSalary(double byPercent) {
double raise = salary * byPercent / 100;
salary += raise;
}
/**
* Compares employee by salary
* @param other another Employee object
* @return a negative value if this employee has a lower salary than
* otherObject, 0 if the salaries are the same, a positive value otherwise
*/
public int compareTo(Employee other) {
return Double.compare(salary, other.salary);
}
}
测试代码
package interfaces;
import java.util.*;
/**
* This program demonstrates the use of the Comparable interface
* @version 1.8 2018-2-11
* @author ShenXueYan
*/
public class EmployeeSortTest {
public static void main(String[] args) {
Employee[] staff = new Employee[3];
staff[0] = new Employee("Harry Hacker", 35000);
staff[1] = new Employee("Carl Cracker", 75000);
staff[2] = new Employee("Tony Tester", 38000);
Arrays.sort(staff);
//print out information about all Employee objects
for(Employee e : staff) {
System.out.println("name=" + e.getName() + ", salary=" + e.getSalary());;
}
}
}
运行结果截图:
从以上内容中,我们已经了解了如何对一个对象数组排序,前提是这些对象是实现了 Comparable 接口的类的实例。例如,可以对一个字符串数组排序,因为 String 类实现了 Comparable< String > ,而且 String.compareTo 方法可以按照字典顺序比较字符串。
现在我们希望按照长度递增的顺序对字符串进行排序,而不是按照字典顺序进行排序。当然,我们肯定是不能让 String 类用两种不同的方法实现 compareTo 方法——更何况,String 类也不应该由我们来修改。
要处理这种情况,Arrays.sort 方法还有第二个版本,有一个数组和一个比较器(comparator)作为参数,比较器是实现了 Comparator 接口的类的实例。
public interface Comparator<T>{
int compare(T first,T second);
}
要按长度比较字符串,可以如下定义一个实现 Comparator< String > 的类:
class LengthComparator implements Comparator<String>{
public int compare(String first, String second){
return first.length()-second.length();
}
...
}
具体完成比较时,需要建立一个实例:
Comparator<String> comp = new LengthComparator();
if(comp.compare(words[i],words[j]) > 0)
...
将这个调用与 words[i].compareTo(words[j])作比较。这个 compare 方法是在比较器对象上调用,而不是在字符串本身上调用。
要对一个数组排序,需要为 Arrays.sort 方法传入一个 LengthComparator 对象:
String[] friends = {"Peter","Paul","Mary","Marz"};
Arrays.sort(friends, new LengthComparator());
使用 lambda 表达式可以更容易地使用 Comparator。
Java 程序设计语言中还有一个非常重要的内置接口,称为 Cloneable 。如果某个类实现了这个 Cloneable 接口,Object 类中的 clone 方法就可以创建类对象的一个拷贝。
那么我们开始讨论 Cloneable 接口:
要了解克隆的具体含义,先来回忆为一个包含对象引用的变量建立副本时会发生什么。
Employee original = new Employee("John Public", 50000);
Employee copy = original;
copy.raiseSalary(10); // oops--also changed original
在这种情况下,原变量和副本都是同一个对象的引用。这说明。任何一个变量改变都会影响另一个变量。
如果希望 copy 是一个新对象,它的初始状态与 original 相同,但是这之后它们各自会有自己不同的状态,这种情况下就可以使用 clone 方法。
Employee copy = original.clone();
copy.raiseSalary(10); // OK--original unchanged
不过并没有这么简单。 clone 方法是 Object 的一个 protected 方法,这说明你的代码不能调用这个方法。只有 Employee 类可以克隆 Employee 对象。
默认的克隆操作是浅拷贝,如果对象中的所有数据域都是数值或其他基本类型,拷贝这些域完全没有任何问题。但是如果对象包含子对象的引用,拷贝域就会得到相同子对象的另一个引用,这样一来,原对象和克隆对象仍然会共享一些信息。
浅拷贝会有什么影响吗? 这要看具体情况。如果原对象和浅克隆对象时不可变的,那么这个共享就是安全的。如果子对象属于一个不可变的类,如 String,就是这种情况。或者在对象的生命期中,子对象一直包含不变的常量,没有更改器方法会改变它,也没有方法会生成它的引用,这种情况下同样是安全的。
不过,通常子对象都是可变的,必须重新定义 clone 方法来建立一个深拷贝,同时克隆所有子对象。
注释: Object 类中 clone 方法声明为 projected。子类只能调用受保护的 clone 方法来克隆它自己的对象。必须重新定义 clone 为 public 才允许所有方法克隆对象。
在这里,Cloneable 接口的出现与接口的正常使用并没有关系。具体地说,它没有指定 clone 方法,这个方法是从 Object 类继承的。这个接口只是作为一个标记,指示类设计者了解克隆过程。对象对于克隆很“偏执”,如果一个对象请求克隆,但没有实现这个接口,就会生成一个受查异常。
即使 clone 的默认(浅拷贝)实现能满足要求,还是需要实现 Cloneable 接口,将 clone 重新定义为 public,再调用 super.clone()。下面给出一个例子:
public class Employee implements Cloneable {
public Employee clone() throws CloneNotSupportedException{
return (Emplyee)super.clone();
}
...
}
要建立深拷贝,还需要做更多的工作,克隆对象中可变的实例域。
下面给出完整实例代码:
Employee 类
package clone;
import java.util.Date;
import java.util.GregorianCalendar;
public class Employee implements Cloneable {
private String name;
private double salary;
private Date hireDay;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
hireDay = new Date();
}
public Employee clone() throws CloneNotSupportedException{
Employee cloned = (Employee)super.clone();
cloned.hireDay = (Date)hireDay.clone();
return cloned;
}
public void setHireDay(int year, int month, int day) {
Date newHireDay = new GregorianCalendar(year, month - 1, day).getTime();
hireDay.setTime(newHireDay.getTime());
}
public void raiseSalary(double byPercent) {
double raise = salary * byPercent / 100;
salary += raise;
}
public String toString() {
return "Employee[name=" + name + ", salary=" + salary + ", hireDay=" + hireDay + "]";
}
}
测试类
package clone;
public class CloneTest {
public static void main(String[] args) {
try {
Employee original = new Employee("Jone Q. public", 50000);
original.setHireDay(2000, 1, 1);
Employee copy = original.clone();
copy.raiseSalary(10);
copy.setHireDay(2002, 12, 31);
System.out.println("original=" + original);
System.out.println("copy=" + copy);
}
catch(CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
克隆并不常用,有些人认为应该完全避免使用 clone,而实现另一个方法达到相同的目的。什么方法? I don’t know.
最最重要的一点:
注释: 所有数组类型都有一个 public 的 clone 方法,而不是 protected。可以用这个方法建立一个新数组,包含原数组去所有元素的副本。例如:
int[] luckyNumbers = {2,3,5,7,11,13};
int[] cloned = luckyNumbers.clone();
cloned[5] = 12; // doesn’t change luckyNumbers[5]