关键字 extends
表明正在构造的新类派生于一个已存在的类。已存在的类称为超类 (superclass)、基类 (base class) 或父类 (parent class);新类称为子类 (subclass)、派生类 (derived class) 或孩子类 (child class),Java 中一般称为“超类”和“子类”。
子类继承于超类,拥有与超类相同的方法和实例域,并且还有一些超类不具有的新特性,这种现象称为 “继承”。一般来说,子类比超类具有更丰富的特性,“继承” 是一个把类从一般到特殊的过程。Java 中子类只允许继承自一个超类,即每个子类有且仅有一个超类。
技巧:在定义的过程中,仅需在子类中定义与超类不同的部分,与超类相同的部分不需要重复定义。
// Employee 类
class Employee
{
private String name;
private double salary;
private LocalDate hireDay;
public Employee(String n, double s, int year, int month, int day)
{
name = n;
salary = s;
hireDay = LocalDate.of(year, month, day);
}
public String getName()
{
return name;
}
public double getSalary()
{
return salary;
}
public LocalDate getHireDay()
{
return hireDay;
}
public void raiseSalary(double byPercent)
{
double raise = salary * byPercent / 100;
salary += raise;
return;
}
}
// Manager 类
public class Manager extends Employee
{
private double bonus;
public void setBonos(double bonus)
{
this.bonus = bonus;
}
}
由一个公共超类派生出来的所有类的集合被称为继承层次 (inheritance hierarchy),如图 1 所示。在继承层次中,从某个特定的类到其祖先的路径被称为该类的继承链 (inheritance chain)。通常,一个祖先类可以拥有多个子孙继承链。
当超类的方法在子类中并不适用时,可以在子类中重新定义该方法,从而实现 “方法覆盖” (override)。见下例:
public class Manager extends Employee
{
public double getSalary()
{
return salary + bonus; // won't work
}
}
这个方法并不能运行。这是因为 Manager
类的 getSalary
方法不能够直接地访问超类的私有域。也就是说,尽管每个 Manager
对象都拥有一个名为 salary
的域,但在 Manager
类的 getSalary
方法中并不能够直接地访问 salary
域。只有 Employee
类的方法才能够访问私有部分。如果 Manager
类的方法一定要访问私有域,就必须借助于公有的接口, Employee
类中的公有方法 getSalary
正是这样一个接口。
public class Manager extends Employee
{
public double getSalary()
{
double baseSalary = getSalary(); // still won't work
return baseSalary + bonus;
}
}
上面这段代码仍然不能运行。问题出现在调用 getSalary
的语句上,这是因为 Manager
类也有一个 getSalary
方法(就是正在实现的这个方法)所以这条语句将会导致无限次地调用自己,直到整个程序崩溃为止。
public class Manager extends Employee
{
public double getSalary()
{
double baseSalary = super.getSalary();
return baseSalary + bonus;
}
}
实际上,我们希望调用 Employee
类中的 getSalary
方法,因此,适用 super
关键字,可以指定调用父类中的方法。在“覆盖方法”时,需访问权限和调用的是哪个方法。
super
是提示编译器调用超类方法的特殊关键字,并非对象的引用,不能赋给另一个对象变量
this
是对特定对象的引用,可以赋给另一个对象变量,二者并不相同
public class Manager extends Employee
{
public Manager(String name, double salary, int year, int month, int day)
{
super(name, salary, year, month, day);
bonus = 0;
}
}
此处的 super
表示调用超类中的构造器 Employee(String n, double s, int year, int month, int day)
。由于 Manager 类的构造器不能访问 Employee
类的私有域, 所以必须利用 Employee
类的构造器对这部分私有域进行初始化,我们可以通过 super
实现对超类构造器的调用。调用构造器的语句只能作为另一个构造器的第一条语句出现。
如果子类的构造器没有显式地调用超类的构造器,则将自动地调用超类默认(没有参数)的构造器。如果超类没有不带参数的构造器,并且在子类的构造器中又没有显式地调用超类的其他构造器则 Java 编译器将报告错误。
在 Java 程序设计语言中,对象变量是多态的。 一个 Employee
变量既可以引用一个 Employee
类对象, 也可以引用一个 Employee
类的任何一个子类的对象(例如,Manager
、Executive
、Secretary
等)。
当超类变量引用 Employee
对象时,调用 getSalary
方法,则编译器调用 Employee
中的 getSalary
方法;当超类变量引用 Manager
对象时,调用 getSalary
方法,则编译器调用 Manager
中的 getSalary
方法。这样的现象称为“多态”。
public class ManagerTest
{
public static void main(String[] args)
{
// construct a Manager object
Manager boss = new Manager("Carl Cracker", 80000, 1987, 12, 15);
boss.setBonus(5000);
Employee[] staff = new Employee[3];
// fill the staff array with Manager and Employee objects
staff[0] = boss;
staff[1] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
staff[2] = new Employee("Tommy Tester", 40000, 1990, 3, 15);
// print out information about all Employee objects
for (Employee e : staff)
System.out.println("name=" + e.getName() + ",salary=" + e.getSalary());
}
}
/**
name=Carl Cracker,salary=85000.0
name=Harry Hacker,salary=50000.0
name=Tommy Tester,salary=40000.0
*/
注意:在上例中,boss
和 staff[0]
引用的是同一个 Manager
对象,但在定义 staff
时,定义为 Employee
类型,因此不能这样调用:。staff[0].setBonus(5000)
此处须区分与“多态”的区别:
setBonus
是子类特有的方法,而超类却不具有,因此超类变量不能调用子类特有的方法。在使用“多态”时,应当时刻关注变量类型和对象类型。
假设要调用 x.f(args)
方法,x
是声明为类 C
的一个对象,方法的调用过程如下:
C
类中名为 f
的方法和其超类中访问属性为 public
且名为 f
的方法。f
方法。若成功匹配,则进行下一步;若没有匹配成功,则会报错。(假设成功匹配到方法 f(String)
)private
方法、static
方法、final
方法或者构造器, 那么编译器将可以准确地知道应该调用哪个方法,我们将这种调用方式称为静态绑定 (static binding)。与此对应的是,调用的方法依赖于隐式参数的实际类型,并且在运行时实现动态绑定。x
所引用对象的实际类型最合适的那个类的方法。假设 x
的实际类型是 D
,它是 C
类的子类。如果 D
类定义了方法 f(String)
就直接调用它;否则,将在 D
类的超类中寻找 f(String)
以此类推。动态绑定有一个非常重要的特性: 无需对现存的代码进行修改,就可以对程序进行扩展。假设增加一个新类 Executive
, 并且变量 e
有可能引用这个类的对象,我们不需要对包含调用 e.getSalary()
的代码进行重新编译。如果 e
恰好引用一个 Executive
类的对象,就会自动地调用 Executive.getSalary()
方法。
final 类
不允许扩展的类被称为 final
类。如果在定义类的时候使用了 final
修饰符就表明这个类是 final
类。final
类不允许成为任何其他类的超类。
public final class Executive extends Manager
{
...
}
final 方法
类中的特定方法也可以被声明为 final
。如果这样做,子类就不能覆盖这个方法( final
类中的所有方法自动地成为 final
方法;但域不会,仍保持原本状态,用 final
修饰的域,构造对象之后就不允许改变它们的值了)。
public class Employee
{
public final String getName()
{
...
}
}
将一个值存人变量时,编译器将检查是否允许该操作。将一个子类的引用赋给一个超类变量,编译器是允许的。但将一个超类的引用赋给一个子类变量,必须进行类型转换,否则将无法通过编译器检查。
if (staff[1] instanceof Manager)
{
boss = (Manager) staff[1]:
...
}
如果这个类型转换不可能成功,编译器就不会进行这个转换。
instanceof
操作符进行检查。在一般情况下,应该尽量少用类型转换和 instanceof
运算符。
在类的继承层次中,越上层的超类,具有的功能越少,“通用性”越强;越下层的子类,具有的功能越多,“通用性”越差。因此,一般最上层的超类可以定义为抽象类。
在 Person
,Employee
和 Student
三者的关系中,Person
作为另外两个的超类。而在实际编程中,很少会用到 Person
对象,由于其太过抽象,功能太少,但它仍具有 Employee
和 Student
类的公共特性,例如姓名,年龄,性别等,因此可以考虑将 Person
定义为一个抽象类。
// 抽象类定义
public abstract class Person
{
public abstract String getDescription();
private String name;
public Person(String name)
{
this.name = name;
}
public String getName()
{
return name;
}
}
abstract
修饰,不需要定义其具体的实现过程,具体的实现在子类继承时完成,抽象方法在抽象类中起到一个占位符的作用。/**
* This program demonstrates abstract classes.
* @version 1.01 2004-02-21
* @author Cay Horstmann
*/
public class PersonTest
{
public static void main(String[] args)
{
Person[] people = new Person[2];
// fill the people array with Student and Employee objects
people[0] = new Employee("Harry Hacker", 50000, 1989, 10, 1);
people[1] = new Student("Maria Morris", "computer science");
// print out names and descriptions of all Person objects
for (Person p : people)
System.out.println(p.getName() + ", " + p.getDescription());
}
}
上例中,子类 Employee
和 Student
类已将 getDescription
方法扩展。在循环中,定义了 Person
变量 p
,由于 Person
为抽象类,因此 p
实际引用的是 Employee
和 Student
对象,而不可能是 Person
对象。
在抽象类 Person
中,若没有定义抽象方法 public abstract String getDescription();
,则上述程序会报错。原因在于:getDescription
方法只定义在子类中的话,p
为 Person
变量,超类无法调用子类方法。
更多抽象类介绍
任何声明为 private
的内容对其他类都是不可见的,这对于子类来说也完全适用,即子类也不能访问超类的私有域。
然而,在有些时候,人们希望超类中的某些方法允许被子类访问,或允许子类的方法访问超类的某个域。为此,需要将这些方法或域声明为 protected
。
受保护的方法更具有实际意义。如果需要限制某个方法的使用, 就可以将它声明为 protected
。这表明子类(可能很熟悉祖先类)得到信任,可以正确地使用这个方法,而其他类则不行。
修饰符总结:
private
:仅对本类可见public
:对所有类可见protected
:对本包和所有子类可见Object
类是 Java 中所有类的始祖,在 Java 中每个类都是由它扩展而来的。如果没有明确地指出超类,Object
就被认为是这个类的超类。下面介绍关于 Object
类的一些基本内容。
Object 类中的 equals 方法用于检测一个对象是否等于另外一个对象。在 Object 类中,这个方法将判断两个对象是否具有相同的引用。
然而,对于多数类来说,这种判断并没有什么意义。例如,采用这种方式比较两个 PrintStream
对象是否相等就完全没有意义。经常需要检测两个对象状态的相等性,如果两个对象的状态相等,就认为这两个对象是相等的。对于自定义类,我们可以覆盖 equals
方法。
public class Employee
{
...
// 注意,超类中的 equals 方法的签名是 equals(Object o)
// 如果自定义时,定义 equals(Employee e)
// 则定义的是另一个重载方法,而不是对超类的 equals 进行覆盖
// 可以用 @Override 对超类的方法进行标记,便于检查是否正确覆盖
// @Override public boolean equals(Employee other)
public boolean equals(Object otherObject)
{
// a quick test to see if the objects are identical
if (this == otherObject) return true;
// must return false if the explicit parameter is null
if (otherObject == null) return false;
// if the classes don't match, they can't be equal
if (getClass() != otherObject.getClass())
return false;
// now we know otherObject is a non-null Employee
Employee other = (Employee) otherObject;
// test whether the fields have identical values
return name.equals(other.name) && salary = other.salary && hireDay.equals(other.hireDay);
}
}
在子类中定义 equals 方法时,首先调用超类的 equals。如果检测失败,对象就不可能相等。如果超类中的域都相等,就需要比较子类中的实例域。
public class Manager extends Employee
{
...
public boolean equals(Object otherObject)
{
if (!super.equals(otherObject)) return false;
// super.equals checked that this and otherObject belong to the same class
Manager other = (Manager) otherObject;
return bonus == other.bonus;
}
}
Java 语言规范要求 equals 方法具有下面的特性:
x.equals(x)
应该返回 true
。y.equals(x)
返回 true
,x.equals(y)
也应该返回 true
。x.equals(y)
返回 true
,y.equals(z)
返回 true
,则 x.equals(z)
也应该返回 true
。x.equaIs(y)
应该返回同样的结果。x.equals(null)
应该返回 false
。一些重新定义 equals
方法的建议:
otherObject
,稍后需要将它转换成另一个叫做 other
的变量。this
与 otherObject
是否引用同一个对象。otherObject
是否为 null
,如果为 null
,返回 false
。这项检测是很必要的。this
与 otherObject
是否属于同一个类。如果 equals
的语义在每个子类中有所改变,就使用 getClass
检测。otherObject
转换为相应的类类型变量。散列码 (hash code) 是由对象导出的一个整型值。散列码是没有规律的。如果 x 和 y 是两个不同的对象,x.hashCode()
与 y.hashCode()
基本上不会相同。
public class Employee
{
// method 1
public int hashCode()
{
return 7 * name.hashCode()
+ 11 * new Double(salary).hashCode()
+ 13 * hireDay.hashCode();
}
...
}
// method 2
public int hashCode()
{
return 7 * Objects.hashCode(name)
+ 11 * Double.hashCode(salary) // 此处使用静态方法,避免新构造一个 Double 对象
+ 13 * Objects.hashCode(hireDay);
}
// method 3
public int hashCodeO
{
// Objects 是 JDK7 中引入的工具类
// Objects.hash 方法可以自动调用参数的 hashCode 方法
// 并将各个 hashCode 进行组合,得到最终的 hashCode
return Objects.hash(name, salary, hireDay);
}
如果重新定义 equals
方法,就必须重新定义 hashCode
方法,以便用户可以将对象插人到散列表中。Equals 与 hashCode
的定义必须一致:如果 x.equals(y)
返回 true
,那么 x.hashCode()
就必须与 y.hashCode()
具有相同的值。例如,如果用定义的 Employee.equals
比较雇员的 ID,那么 hashCode
方法就需要散列 ID,而不是雇员的姓名或存储地址。
在 Object
中还有一个重要的方法,就是 toString
方法,它用于返回表示对象值的字符串。toString
方法是一种非常有用的调试工具。在标准类库中,许多类都定义了 toString
方法,以便用户能够获得一些有关对象状态的必要信息。绝大多数(但不是全部)的 toString
方法都遵循这样的格式:类的名字,随后是一对方括号括起来的域值。
当自定义子类时也应该定义子类的 toString
方法,并将子类域的描述添加进去。如果超类使用了 getClass().getName()
,那么子类只要调用 super.toString()
就可以了。
只要对象与一个字符串通过操作符 “+” 连接起来,Java 编译就会自动地调用 toString
方法,以便获得这个对象的字符串描述。Object
类定义了 toString
方法,用来打印输出对象所属的类名和散列码。
建议:为自定义的每一个类增加 toString
方法。这样做不仅自己受益,而且所有使用这个类的程序员也会从这个日志记录支持中受益匪浅。
参考资料: