chapter5.4 继承

继承,面向对象语言的三大基本特征之一,实现软件复用的重要操作,通过extends关键字实现,例如猫类继承动物类,苹果类继承水果类,实现继承的类叫做子类,被继承的类叫做父类或者基类……

4、继承

继承

继承,面向对象语言的三大基本特征,实现软件复用的重要操作,通过extends关键字实现,例如猫类继承动物类,苹果类继承水果类,实现继承的类叫做子类,被继承的类叫做父类或者基类。举例:

1
2
3
4
5
6
7
public class 
{
private String color;
public void view(String color){
System.out.println("color:"+ color);
}
}
1
2
3
4
5
6
7
8
public class Ostrich extends 
{
public static void main(String[] args)
{
Ostrich ostrich = new Ostrich();
ostrich.view("黑色");
}
}

上面的实例中我们先写了一个Birds作为父类,定义了一个属性color,一个view方法打印颜色,用一个Ostrich类继承Birds,Ostrich类就拥有了父类的方法,创建Ostrich对象就可以调用父类的方法,但是Ostrich对象并不能直接访问父类的私有属性,上面程序输出:

1
color:黑色

可是,为什么要用继承呢?这样的方法自己写了不是更好?

在上面的实例中,鸟类涵盖范围太大了,从麻雀到老鹰,从候鸟到留鸟,大部分的鸟类都会飞,但是飞的行为却不一样,所有的鸟类都要吃东西,但是有的食草有的食肉,还有各种各样的颜色。如果每一种鸟都去定义这样的方法,势必对出现大量重复的代码,所以才将共性的部分提取出来作为父类,事实上很多的继承并不是一开始就这么制定了,而是在优化的过程中不断抽取重复代码才出现的。

继承主要有以下特性:

  • 子类拥有父类非private的方法和属性;
  • 子类可以拥有自己独立的方法和属性,也就是子类的可扩展性;
  • 子类可以重写父类的方法;
  • java中的继承是单继承,也就是说一个父类可以有多个子类,但是一个子类只能继承于一个父类,如果我们要实现多重继承,就可以构造多个类,类A继承类B,类B继承类C。

方法重写:

上面的例子中,鸵鸟类集成了鸟类,有了显示颜色的方法,鸟类会飞翔,但是鸵鸟并不会飞翔,所以鸵鸟要去重写飞翔的方法实现自己的逻辑,修改如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class 
{
private String color;

public void view(String color){
System.out.println("color:"+ color);
}

public void fly(){
System.out.println("鸟儿会飞。。。");
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Ostrich extends 
{

public void fly() {
System.out.println("鸵鸟会跑不会飞。。。");
}

public static void main(String[] args)
{
Ostrich ostrich = new Ostrich();
ostrich.view("黑色");
ostrich.fly();
}
}

上面的例子中,如果子类没有重写fly方法,那么Ostrich的对象调用fly方法就会打印:鸟儿会飞,显然是不合逻辑的,而重写了这个方法之后,再去调用fly方法就是自己的实现了,打印出:鸵鸟会跑不会飞。一般我们会在重写的方法上一行写上@override标签来表示这是一个重写的方法,需要注意重写和重载的不同之处。

super关键字和构造方法调用:

super关键字和this关键字很相似,不过super指向的是父类的对象,this指向的是当前类的对象,同样的super也不能出现在static修饰的方法中,实例如下:

1
2
3
4
5

public void fly() {
super.view("鸵鸟颜色白加黑");
System.out.println("鸵鸟会跑不会飞。。。");
}

在fly方法中使用super调用的是父类的方法,super表示父类的对象。这里使用的super实际上是调用了父类无参的构造器,而且在默认情况下也是调用父类无参的构造器。但如果我们想用super去调用父类声明的有参构造方法呢?

重新写一个实例,我们知道构造方法是用来初始化一个对象的,来看一下子类是怎么执行父类的构造方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Employee {
private String id;
private String name;
private String department;

public Employee() {
System.out.println("父类无参的构造方法");
}

public Employee(String id, String name, String department) {
this.id = id;
this.name = name;
this.department = department;
System.out.println("父类有参的构造方法");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Manager extends Employee {

private double reward;

public Manager() {
this(2000);
}

public Manager(double reward) {
this.reward = reward;
}

public Manager(String id, String name, String department, double reward) {
super(id, name, department);
this.reward = reward;
}

public static void main(String[] args) {
Manager managerA = new Manager();
Manager managerB = new Manager(20000);
Manager managerC = new Manager("0002", "赵子龙", "集成工具部", 30000);
}
}

看一下这两个类,父类是雇员类,子类是经理类,这里同样也可以再说明一下什么时候使用继承,我们在处理两个对象时,如果有“is-a”的关系,就可以使用继承,比如上面的实例中,经理作为公司管理人员,在项目结束时会拿到奖金,而普通员工并没有,但同时经理也隶属于公司,同样作为雇员存在,所以可以用继承减少相同代码,去进行自己功能的扩展,同样作为雇员,人事、行政、技术、销售等各个岗位都会有自己的特性,各个岗位又有共性,此时就可以用继承来实现。

继续说到构造方法,上面的实例中,运行main()方法会输出如下结果:

1
2
3
父类无参的构造方法
父类无参的构造方法
父类有参的构造方法

我们对子类构造了三个不同的对象,分别使用子类无参、有参构造器和super调用父类构造器,可以看到,不管是否使用super关键字,每一个对象的创建都会先去调用父类中的构造方法一次,总结一下:

  • 如果子类构造器既没有super也没有this,则默认执行子类构造器前先执行父类无参构造器;
  • 如果子类构造器使用this调用子类另一个重载的构造器,系统会根据传入的实参去掉用对应的构造器,执行该构造器之前又会先去调用父类无参构造器;
  • 如果子类构造器第一行使用super调用父类构造器,系统会根据实参调用对应的父类构造器。

需要注意的是:this和super对构造器的调用都在代码第一行,所以它们不能同时出现。

Object类

在前面的例子中,我们构造的普通java类中有重写toString、hashCode和equals方法,然而我们并没有使用extends关键字去集成任何一个类,那么怎么会有这些重写的方法呢?原因很简单,Java中定义了一个Object类,这个类是所有类的超类,也就是说每一个类都是由它扩展而来,默认每一个类都继承这个类,但是并不需要显式指定。结合前面构造方法的调用,我们初始化一个子类会先去调用父类的构造方法,Object类就是最顶层的父类。既然是作为最顶级的父类,那就一定提供了一些所有类通用的方法,下面看几个常用的方法:

  • equals()方法:用来判断两个对象的内容是否相同,还记得前面说比较运算符说到的==吗,==在引用数据类型中比较的是对象的引用。先看一段代码,再讨论一下这二者之间的区别:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class EqualTest {
    private int id;
    public EqualTest(int id) {
    this.id = id;
    }
    public static void main(String[] args) {
    EqualTest e1 = new EqualTest(1);
    EqualTest e2 = new EqualTest(1);
    System.out.println(e1.equals(e2));
    System.out.println(e1 == e2);
    }
    }

    这个实例的输出结果是:

    1
    2
    false
    false

    很奇怪是不是,对于”==”运算符比较两个EqualsTest对象返回false,我们明白这点,因为e1和e2分别指向不同的对象,所以二者内存地址是不相同的。但是equals()方法呢?我们前面说比较的是内容,也就是说上例中拥有相同id的情况下我们认为两个对象是相等的,却返回了false。原因就是我们使用equals方法的时候没有去重写,所以此时调用的是Object类的equals()方法,这个原始方法内部的实现其实就是”==”,,看一下源码:

    1
    2
    3
    public boolean equals(Object obj) {
    return (this == obj);
    }

    所以,为了达到我们的期望值,就要重写equals()方法,让对象之间的比较按照我们需要的逻辑去比较内容,而不是内存地址,重写如下:

    1
    2
    3
    4
    5
    6
    7
    8

    public boolean equals(Object o) {
    if (o instanceof EqualTest) {
    EqualTest test = (EqualTest) o;
    return this.id == test.id;
    }
    return false;
    }

    再次运行,我们发现输出的结果就满足我们的预期了:

    1
    2
    true
    false

    上面重写的方法中用到了instanceof关键字,它可以用来判断某对象所指向的类型,判断返回boolean值,在上面的例子中传入的对象是EqualsTest类型,然后去比较id,也就是内容,返回比较的结果。

  • hashCode()方法:

    一般要求在重写equals()方法的同时也重写hashCode()方法,该方法返回一个对象的哈希值,后面再说。

  • toString()方法:

    该方法主要用来返回当前对象的字符串表达式,继续在上面的例子中重写如下:

    1
    2
    3
    4

    public String toString() {
    return "EqualTest{" + "id=" + id + "}";
    }

    打印该对象输出:

    1
    EqualTest{id=1}

    如果不去重写toString()方法,那么打印对象会输出如下结果(类名[email protected]+hashCode):

    1
    [email protected]1540e19d
  • 线程同步相关方法:

    wait()、notify()、notifyAll()方法,这几个方法在后面章节线程中会详细说明。

  • getClass()方法:

    该方法会返回一个对象的类对象,关于这个方法会在后面章节反射机制中说明。​

本节代码路径

下篇——Chapter5:05、多态