Java核心技术之继承
目录
一、继承的定义
多个类中存在相同属性和行为,将这些内容抽取到单独的一个类中,那么多个类无需再定义这些属性和行为,只需要继承那个类即可。 —— [ ImportNew ]
二、继承的实现
1.类的继承
定义子类
由继承的定义,可以得出超类和子类的概念,一般也称之为父类和子类。
定义中,“多个类”可以称之为子类,“单独的一个类”称为父类或超类。
子类可以直接访问父类中的非私有属性和行为。关键字extend表示继承。采用书上的例子进行讲解:
public class Employee
{
private String name;
private double salary;
private LocalDate hireDay;
public Employee(String name,double salary,int year,int month,int day){
this.name = name;
this.salary = salary;
this.hireDay = LocalDate.of(year,month,day);
}
public String getName(){
return this.name;
}
public double getSalary(){
return this.salary;
}
public LocalDate getHireDay(){
return this.hireDay;
}
}
下面是由继承Employee类来定义Manager类的格式:
public class Manager extends Employee
{
/**添加方法和域**/
}
extend关键字代表,Manger类这一新派生的类继承自Employee类,Employee类是已存在的类,称为父类;Manager类是由Employee类派生得来的,称为子类。根据继承的定义,新派生的Manager可以直接访问Employee类中的非私有属性和方法。在Manager类中不需要定义getName()等方法,直接访问父类的方法就可以获得返回值。
在Manager类中新,增加一个用于存储奖金信息的域,以及用于设置这个域的新方法。
public class Manager extends Employee{
private double bonus;
...
public void set Bonus(double bonus){
this.bonus = bonus;
}
}
对与类来说,父类是对一类事物最抽象最通用的域和方法的抽象,子类再继承这些通用的内容后,可以在自己的类内定义新的只属于自己的域和内容。所以,子类会被父类拥有更加丰富的功能。
抽象类
抽象就是从多个事物中将共性、通用的内容抽象出来。抽象方法是只有定义没有方法体,其具体实现由子类去完成。包含一个或多个抽象方法的类称之为抽象类。Employee类和Manager类其实都可以继承自Person类,而Person类又可以派生出Student和Teacher类。假设,需要为Student类和Teacher类加一个getDescription方法,用于对不同身份的类进行信息描述。在父类Person中就可以将getDescription定义为一个抽象方法,具体的实现由相应的子类去完成。
public class Person(){
.......
public abstract String getDescription();
}
public class Student extends Person(){
......
public String getDescription(){
system.out.println("I am a student!");
}
}
public class Teacher extends Person(){
......
public String getDescription(){
system.out.println("I am a Teacher!");
}
}
如果多个对象都具有相同的功能,但是功能的具体内容并不相同,在父类中可以抽象这个功能的定义,将该方法定义成一个抽象方法,不去做具体实现。
抽象类的特点:
- 抽象方法一定在抽象类中;
- 抽象方法和抽象类都必须被abstract关键字修饰;
- 抽象类不可以用new创建对象,因为调用抽象方法没有意义;
- 抽象类中的抽象方法要被使用,必须由子类复写父类的所有抽象方法后建立子类对象的调用;如果子类只覆盖了部分的抽象方法,那么该子类还是一个抽象类;
- 抽象类可以有抽象方法,也可以有非抽象方法;
- 如果一个类是抽象类,那么,继承该类的子类要么是抽象类,要么重写了所有的抽象方法。
特殊,如果一个类被定义成抽象类,但是在该类中并没有抽象方法,这样做仅仅是为了不让该类创建对象。
抽象类的注意事项:
- final:如果方法被抽象,在子类中该法规就会被覆盖,而final不可以被覆盖,所以冲突。
- private:如果函数被私有化,子类就无法直接访问,而抽象方法就是为了让子类访问并复写。
- static:static方法不需要对象,可以用类名直接调用抽象方法,而抽象方法本身是没有实现的,所以调用抽象方法本身并没有意义。
2.覆盖方法
前面例子中的代码以及涉及到了覆盖方法的例子。就是,当我们父类定义好的方法并不适用于子类时,子类需要提供一个新的方法来覆盖(override)父类中的这个方法。根据定义Employee的getSalary方法只返回固定薪资salary的值,而Manager类生成的对象是“经理”,经理所获得薪资定义为固定薪资salary加上奖金bonus。此时Employee类中的getSalary方法就不在适用于当前的子类Manager类。
应该如何实现?
public double getSalary(){
return salary+bonus; //don't work编译时报错
}
这是因为Manager类并不能访问父类中的私有域。虽然每个Manager对象拥有一个salary域,但是只能通过Employee类中的非私有方法getSalar方法获取返回值,并不能直接访问到salary域。
如果将对salary域的访问替换成调用父类中公共方法getSalary是不是就能正常运行了呢?
/**运行时报错**/
public double getSalary(){
double salary = getSalary()
return salary+bonus;
}
如果Manager类一定要访问父类的私有域,只能借助于公有的接口getSalary()。虽然,借助getSalary能够访问到父类的私有域,但是上面的代码仍不能正确执行。这是因为,在Manager类中也有一个getSalary方法(就是我们需要实现的这个方法),所以这条语句会无限次的调用自己,知道程序崩溃。这里需要指出,我们希望调用Employee类中的getSalary方法时,需要使用特定的关键字super。
super.getSalary()
上述语句调用的就是父类中的getSalary方法。
/**运行时报错**/
public double getSalary(){
double salary = super.getSalary()
return salary+bonus;
}
super和this有什么区别?
super是一个关键字,代表父类的存储空间标识。(可以理解为父亲的引用)
super和this的用法相似。
this代表对象的引用(谁调用就代表谁);
super代表当前子类对父类的引用。
3.接口的实现
定义
接口是抽象方法和常量值的集合。从本质上讲,接口是一种特殊的抽象类,这种抽象类只包含常量和方法的定义,没有变量和方法的实现。
定义接口
interface Instrument{
int VALUE = 5;
void play(Note n);
void adjust();
}
下面是用关键字implements实现接口的格式:
class Wind implements Instrument{
public void play(Note n){
print(this + ".play() " + n);
}
public void adjust(){
print(this + ".adjust()");
}
}
class Percussion implements Instrument{
public void play(Note n){
print(this + ".play() " + n);
}
public void adjust(){
print(this + ".adjust()");
}
}
......
特点
- 接口不能被实例化;
- 一个类如果实现了接口,要么是抽象类(实现接口的部分方法),要么实现接口中的所有方法。
总结
继承与实现的区别:
- 类与类之间称为继承关系:因为该类无论是抽象还是非抽象,它的内部都可以定义非抽象方法,这个方法可以被子类直接使用。只能单继承,可以多层继承。
- 类与接口直接是实现关系:因为接口中的方法都是抽象的,必须由子类实现才可以实例化。可以单实现也可以多实现,还可以在继承一个类的同时实现多个接口。
抽象类与接口的区别
我们将接口视为一个特殊的抽象类。
成员变量
- 抽象类能有变量,也可以有常量
- 接口只有常量
成员方法
- 抽象类可以有非抽象方法,也可以有抽象方法
- 接口只能有抽象方法
构造方法
- 抽象类有构造方法
- 接口没有构造方法
构造方法
类与抽象类和接口关系
- 类与抽象类的关系是继承 extends
- 类与接口的关系是实现 implements
接口的思想特点
- 接口是对外暴露的规则;
- 接口是程序的功能扩展;
- 接口的出现降低耦合性;(实现了模块化开发,定义好规则,每个人实现自己的模块,大大提高了开发效率)
- 接口可以用来多实现;
- 多个无关的类可以实现同一个接口;
- 一个类可以实现多个相互直接没有关系的接口;
- 与继承关系类似,接口与实现类之间存在多态性。
4.对象的多态
在Java程序语言中,对象变量是多态的,一个Employee变量既可以引用一Employee类对象,也可以引用一个Employee类的任何一个子类的对象,如Manager类。父类对象可以用子类替换。
Manager boss = new Manager(...);
Employee[] staff = new Employee[3];
staff[0] = boss;
在例子中staff[0]和boss引用了同一个对象。但编译器将staff[0]看作是Employee对象,所以
boss.setBonus(5000);//it's ok
staff[0].setBonus(5000);//Error
staff[0]的声明的类型是Employee,而setBonus是Manager类的方法。多态至始至终都是子类再发生变化。
多态的前提:
- 要有继承或者实现关系。
- 要有方法的重写。
- 要有父类引用指向子类对象。
三、继承的实例 —— Object
Object类是Java中所有类的超类(父类)。在Java中每个类都是由Object类扩展来的。我们可以看下Object类被抽象出来的几个方法:
1.equal()方法
Object类中的equals方法用于检测两个对象是否相等,看下equals方法源码是如何实现的:
通过源码可以看出,equals方法是在比较两个对象是不是同一个对象。而且equals方法不是Object类的私有方法,在定义新类时,可以重写equals()方法。以下,是JDK规定重写equals()方法应该遵守的约定:
自反性:x.equals(x)必须返回true。
对称性:x.equals(y)与y.equals(x)的返回值必须相等。
- 传递性:x.equals(y)为true,y.equals(z)也为true,那么x.equals(z)必须为true。
- 一致性:如果对象x和y在equals()中使用的信息都没有改变,那么x.equals(y)值始终不变。
- 非null:x不是null,y为null,则x.equals(y)必须为false。
它在Object类中的实现是判断两个对象是否指向同一块内存区域。这中测试用处不大,因为即使内容相同的对象,内存区域也是不同的。如果想测试对象是否相等,就需要覆盖此方法,进行更有意义的比较。例如
class Employee{
...
public boolean equals(Object otherObj){
//快速测试是否是同一个对象
if(this == otherObj) return true;
//如果显式参数为null,必须返回false
if(otherObj == null) reutrn false;
//如果类不匹配,就不可能相等
if(getClass() != otherObj.getClass()) return false;
//现在已经知道otherObj是个非空的Employee对象
Employee other = (Employee)otherObj;
//测试所有的字段是否相等
return name.equals(other.name)
&& salary == other.salary
&& hireDay.equals(other.hireDay);
}
}
2.hashCode()方法
可以看出,hashCode()是一个native方法,而且返回值类型是整形;实际上,该native方法将对象在内存中的地址作为哈希码返回,可以保证不同对象的返回值不同。
与equals()方法类似,hashCode()方法可以被重写。JDK中对hashCode()方法的作用,以及实现时的注意事项做了说明:
hashCode()在哈希表中起作用,如java.util.HashMap。
如果对象在equals()中使用的信息都没有改变,那么hashCode()值始终不变。
如果两个对象使用equals()方法判断为相等,则hashCode()方法也应该相等。
如果两个对象使用equals()方法判断为不相等,则不要求hashCode()也必须不相等;但是开发人员应该认识到,不相等的对象产生不相同的hashCode可以提高哈希表的性能。