面向对象范式的软件设计着重于对象以及对象上的操作。面向对象的方法结合了面向过程范式的强大之处,并且进一步将数据和操作集成在对象中。继承是Java在软件重用方面一个重要且功能强大的特征。假设要定义一个类,对学生和教师建模。这些类有很多共同的特性。设计这些类来避免冗余并使系统易于理解和易于维护的最好方式是什么?答案就是使用继承。
继承使得你可以定义一个通用的类 (即父类),之后继承该类为一个更特定的类(即子类)。
使用类来对同一类型的对象建模。不同的类可能会有一些共同的特征和行为,可以在一个通用类中表达这些共同之处,并被其他类所共享。可以定义特定的类继承自通用类。这些特定的类继承通用类中的特征和方法。
假设要设计类来对像学生和老师这样的职业建模,两者之间有许多共同的属性和行为。他们都有名字,性别和年龄。可以用一个通用类Person来建模所有的职业。这个类包括属性name、sex和age,相应的get和set方法,以及toString方法,用来返回该对象的字符串表示。
** Person类的属性和方法如下表所示:**
类型 | 名称 | 返回值类型 |
---|---|---|
属性 | name | String |
属性 | sex | String |
属性 | age | int |
方法 | getName | String |
方法 | setName | void |
方法 | getSex | String |
方法 | setSex | void |
方法 | getAge | int |
方法 | setAge | void |
方法 | toString | String |
Person类的代码如下:
public class Person {
private String name;
private String sex;
private int age;
public Person() {
}
public Person(String name, String sex, int age) {
this.name = name;
this.sex = sex;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", sex='" + sex + '\'' +
", age=" + age +
'}';
}
}
在Person类基础上,Student类会有自己的学号,Teacher类会有自己教授的课程名称。因此,通过继承Person类来定义Student和Teacher类是有意义的。
下面这段文字涉及到的定义较多,第一次学习的小伙伴可能会觉得有些绕。因此需要仔细阅读
在Java术语中,如果类C1继承自另一个类C2,那么就将C1称为子类(subclass),将C2称为超类(superclass)。超类也称为父类(parent class)或基类(base class),子类又称为继承类(extended class)或派生类(derived class)。子类从它的父类中继承可访问的数据域和方法,还可以添加新的数据域和方法。因此,Student和 Teacher都是Person的子类,Person是 Student和 Teacher的父类。一个类定义了一个类型。由子类定义的类型称为子类型(subtype),由父类定义的类型称为父类型(supertype)。因此,可以说Student是Person的子类型,而Person是Student的父类型。
通过继承,Student类继承了 Person类中所有可访问的数据域和方法。除此之外,它还有一个新的数据域 id,以及相关的获取方法和设置方法。Student类还包括setId和getId方法以设置id和返回id。
Student类的代码如下:
public class Student extends Person{
private int id;
public Student() {
}
public Student(int id) {
this.id = id;
}
public Student(String name, String sex, int age, int id) {
setName(name);
setSex(sex);
setAge(age);
this.id = id;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
Student类以下面的语法继承Person类:
public class Student extends Person
重载的构造方法 Student(String name, String sex, int age, int id) 是通过调用setName、setSex和setAge方法来设置Name、Sex和Age属性的。这三个公共方法是在Person类中定义的,并被Studen类所继承,因此可以在Student类中使用它们。
你可能会试图在构造方法中直接使用数据域name、sex和age,如下所示:
public Student(String name, String sex, int age, int id) {
this.name = name;
this.sex = sex;
this.age = age;
this.id = id;
}
然而这是错误的,因为在Person中,数据域name、sex和age是私有的,只能在Person类中访问。唯一读取和改变name、sex和age的方法就是调用它们的get和set方法。
关键字 super 指代父类,可以用于调用父类中的普通方法和构造方法。
子类继承它的父类中所有可访问的数据域和方法。它能继承构造方法吗?父类的构造方法能够从子类调用吗?
就像关键字this的作用,它是对调用对象的引用。关键字super 是指这个super关键字所在的类的父类。关键字super可以用于两种途径:
调用父类构造方法的语法如下:
super()或者super(arguments)
语句super()调用父类的无参构造方法,而语句super(arguments)调用与arguments匹配的父类的构造方法。语句 super()或super(arguments)必须出现在子类构造方法的第一行,这是显式调用父类构造方法的唯一方式。例如,在Student类中的构造方法Student(String name, String sex, int age, int id) 可以用下面代码替换:
public Student(String name, String sex, int age, int id) {
super(name, sex, age);
this.id = id;
}
构造方法可以调用重载的构造方法或父类的构造方法。如果它们都没有被显式地调用编译器就会自动地将 super()作为构造方法的第一条语句。
在任何情况下,构造一个类的实例时,将会调用沿着继承链的所有父类的构造方法。当构造一个子类的对象时,子类的构造方法会在完成自己的任务之前,首先调用它的父类的构造方法。如果父类继承自其他类,那么父类的构造方法又会在完成自己的任务之前,调用它自己的父类的构造方法。这个过程持续到沿着这个继承层次结构的最后一个构造方法被调用为止。这就是构造方法链(constructor chaining)。
思考以下代码:
public class Class4 extends Class3 {
public static void main(String[] args) {
new Class4();
}
Class4() {
System.out.println("This is Class4");
}
}
class Class3 extends Class2 {
Class3() {
System.out.println("This is Class3");
}
}
class Class2 extends Class1 {
Class2() {
System.out.println("This is Class2");
}
}
class Class1 {
Class1() {
System.out.println("This is Class1");
}
}
This is Class1
This is Class2
This is Class3
This is Class4
分析一下输出上述结果的原因:
在 Class4 类中,new Class4() 调用 Class4 的无参构造方法。由于 Class4 是 Class3 的子类,所以,在 Class4 构造方法中的所有语句执行之前,先调用 Class3 的无参构造方法。由于 Class3 是 Class2 的子类,所以,在 Class3 的构造方法中所有语句执行之前,先调用 Class2 的无参构造方法。同时又因为 Class2 是 Class1 的子类,因此在 Class2 的构造方法中所有语句执行之前,先调用 Class1 的无参构造方法。
因此整个过程可以看做一个递归的过程。代码的执行顺序为:Class1 的无参构造器代码 -> Class2 的无参构造器代码 -> Class3 的无参构造器代码 -> Class4 的无参构造器代码。
注意:因为子类会隐式地调用父类的无参构造器,而如果父类中没有给出无参构造器,则会报错。因此我们在定义一个以后会被继承的父类时,最好显式地给出其无参构造器,以免出现类似错误。
当子类中重写了父类的方法时,便可以使用super.方法名(参数) 的方式使用父类中的原方法。假设我们要在使用getName方法得到学生姓名的同时,在姓名前加上学生二字,我们可以在Student类中重写Person类的getName方法,代码如下:
@Override
public String getName() {
return "学生" + super.getName();
}
代码中的super.getName() 便是调用了Person类中的getName方法,获得name。
重写意味着在子类中提供一个对方法的新的实现。重载意味着使用同样的名字但是不同的签名来定义多个方法。
方法重写发生在具有继承关系的不同类中,且具有同样的签名。
例如在3.3中,通过重写Person类中的getName方法,实现了在得到的名字前加上学生二字的操作。
为了避免错误,可以使用一种特殊的Java语法,称为重写标注,即在子类的方法前面放一个 @override。该标注表示被标注的方法必须重写父类的一个方法。如果具有该标注的方法没有重写父类的方法,编译器将报告一个错误。
方法重载可以发生在同一个类中,也可以发生在具有继承关系的不同类中。方法重载具有同样的名字但是不同的参数列表。
比如在Person类中的多个构造器便是实现了方法的重载:
public Person() {
}
public Person(String name, String sex, int age) {
this.name = name;
this.sex = sex;
this.age = age;
}
Java中所有的类都继承自java.lang.Object类。
如果我们在定义一个类时没有指定继承,那么这个类的父类默认是Object类。即public class ClassName { } 等价于public class ClassName extends Object{ }。
下面是对 toString 方法的介绍:
tostring()方法的签名是 public String toString()。
调用一个对象的 tostrin()会返回一个描述该对象的字符串。默认情况下,它返回一个由该对象所属的类名、at 符号 (@)以及用十六进制形式表示的该对象的内存地址组成的字符串。但是在大多数情况下,该方法返回的字符串并没有什么信息和用处。因此,我们通常会在自己的类中重写toString方法。例如我们在Person类中重写toString的代码:
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", sex='" + sex + '\'' +
", age=" + age +
'}';
}
注意:也可以传递一个对象来调用System.out.println(object)或 System.out.print(object)。这等价于调用System.out.println(object.toString())或 System.out.print(object.toString())。