首页 > 编程笔记

什么是继承,Java继承详解(小白必读)

在讲解继承的概念之前,我们先来看一个示例,定义一个 Student 类和一个 Teacher 类,分别有 id、name、age、gender 属性,代码如下所示:
public class Student {
    private int id;
    private String name;
    private int age;
    private char gender;
    //getter、setter方法
}

public class Teacher {
    private int id;
    private String name;
    private int age;
    private char gender;
    //getter、setter方法
}
可以看到两个类中的属性完全一样,提供给外部调用的 setter 和 getter 方法也完全一致。我们要养成一个思维习惯,当看到代码中有完全重复的内容时就需要想办法进行优化,能不能将两个类中完全一致的内容提取出来,同时让这两个类来复用这些代码呢?

来找找 Student 和 Teacher 的共性,我们可不可以定义一个 People 类,然后让 Student 和 Teacher 拥有 People 类的属性和方法呢?这种代码优化的方式叫作继承,即一个类继承另外一个类的属性和方法,被继承的类叫父类,继承的类叫子类。People 就是父类,Student 和 Teacher 就是子类。那继承如何实现呢?

继承的基本语法如下:
//父类
public class 类名{
//属性和方法
}

//子类
public class 类名 extends 父类名{
//子类特有的属性和方法
}
举个简单的例子:
public class People {
    private int id;
    private String name;
    private int age;
    private char gender;
    //getter、setter方法
}

public class Student extends People {
}

public class Teacher extends People {
}
继承的好处是我们只需要定义一个父类 People,然后让 Student 和 Teacher 直接继承 People,Student 和 Teacher 中就不需要定义属性和方法了,而直接拥有了 People 的公有属性和方法。若子类中有特定的属性和方法,则只需要在继承的基础上,在子类中定义特有的属性和方法即可,此时子类的信息由两部分内容组成,一部分是继承自父类的属性和方法,另外一部分是自己特有的属性和方法。

程序中的继承和现实生活中的例子是一样的,儿子可以继承父亲的资产,那么儿子就不需要那么辛苦打拼,可以轻松拥有父亲给他的一切,同时儿子还可以在继承父亲资产的基础上继续创造属于自己的资产。

继承是面向对象编程思想的主要特征,Java 通过继承可以实现代码复用。Java 只支持单继承,即一个类只能有一个直接父类。注意,我们这里说的是只能有一个直接父类,父类的父类资源也是可以被继承的,相当于父亲从爷爷那里继承的资产,可以传到儿子手上。

子类访问父类

实现了继承关系的父子类,在创建子类对象时,无论调用无参构造还是有参构造,都会默认先创建父类对象,并且是通过父类的无参构造完成实例化的,例如:
public class People {
    //......
    public People() {
       System.out.println("调用了无参构造创建People对象");
    }

    public People(int id) {
       System.out.println("调用了有参构造创建People对象");
    }
}

public class Student extends People {
    public Student(){
       System.out.println("调用了无参构造创建Student对象");
    }
    public Student(int id){
       System.out.println("调用了有参构造创建Student对象");
    }
}   

public class Test {
    public static void main(String[] args) {
       Student student = new Student();
       Student student2 = new Student(1);
    }
}
程序中,父类 People 和子类 Student 的构造函数中分别打印相关信息,在测试类的 main 方法中分别调用 Student 的无参构造和有参构造来创建 Student 对象。程序的运行结果是:

调用了无参构造创建People对象
调用了无参构造创建Student对象
调用了无参构造创建People对象
调用了有参构造创建Student对象

可以看到,创建 Student 对象之前一定会优先创建 People 对象,同时无论是调用 Student 的有参构造还者是无参构造,创建 People 对象都是通过调用其无参构造来完成的。

那么问题来了,在创建 People 对象时,会不会调用其他类的构造函数呢?即 People 类有没有自己的父类?答案是 People 也有自己的父类,只不过这个父类不是我们自定义的,而是 Java 提供的。Java 中的每个类都有一个共同父类 Object,Object 类就是所有 Java 类的根(老祖宗),所有的 Java 类都是由 Object 类派生出来的。

我们现在明白了,在创建子类时会默认调用父类的无参构造来创建父类对象,那么能否让父类调用有参构造来创建对象呢?答案是可以的,如何完成?需要使用 super 关键字,super 关键字和 this 关键字类似,但用法完全不同,this 用作访问当前类的属性和方法,super 用作子类访问父类的属性和方法。要调用父类的有参构造,可以修改 Student 类,修改好的代码如下所示:
public class Student extends People {
    public Student(){
       super(1);
       System.out.println("调用了无参构造创建Student对象");
    }
    public Student(int id){
       super(1);
       System.out.println("调用了有参构造创建Student对象");
    }
}
再次运行程序,结果是:

调用了有参构造创建People对象
调用了无参构造创建Student对象
调用了有参构造创建People对象
调用了有参构造创建Student对象

可以看到当前代码全部是通过有参构造来创建 People 对象,同理调用父类无参构造的代码是 super(),并且这种方式是默认设置。当我们手动在子类构造方法中做出修改时,会覆盖掉默认的方式,改为调用父类有参构造的方式。

好了,我们已经学习了如何在子类构造函数中调用父类构造函数,那么在子类普通方法中如何调用父类的属性和普通方法呢?同样是使用 super 关键字,例如访问属性:“super.属性名”,调用普通方法:“super.方法名();”。

需要强调的是,子类只能访问父类的公有属性和方法,即使用 public 修饰的属性和方法,无法访问私有 private 修饰的属性和方法,父类的私有属性可以通过它的公有方法来访问和修改,具体调用参考下面的代码:
public class Student extends People {
    public void show(){
    super.setName("张三");
    System.out.println(super.getName());
    }
}   

public class Test {
    public static void main(String[] args) {
    Student student = new Student();
    student.show();
    }
}
程序运行结果为:

调用了无参构造创建People对象
张三

子类访问权限

我们知道,子类可以通过 super 关键字来访问父类的属性和方法,但不是所有的父类属性和方法都可以被子类访问,那么父类的哪些属性和方法是可以被子类访问的呢?在解答这个问题之前,我们首先要学习访问权限修饰符。

访问权限修饰符可以用来修饰类、属性和方法,不同的访问权限修饰符表示不同的作用域,包括 public、protected、默认修饰符和 private 这 4 种修饰符。一般使用public来修饰类,我们这里主要说的是对属性和方法的访问权限修饰符,其作用域如下表所示。

表:访问权限修饰符的作用域
  同一个类 同一个包 不同包 子类
public 可以访何 可以访问 可以访问 可以访问
protected 可以访问 可以访问 不可以访问 可以访问
默认修饰符 可以访问 可以访问 不可以访问 不可以访问
private 可以访问 不可以访问 不可以访问 不可以访问

从表中可以看到,子类只能访问父类 public 和 protected 修饰的属性和方法,默认修饰符和 private 修饰的属性和方法不能访问。

这里引入了一个新的概念:包(package)。为什么要有包呢?包用来管理 Java 类,类似于我们用不同的文件夹管理不同的文件,一个项目中不可避免地会出现同名的 Java 类,为了防止产生冲突,可以把同名的 Java 类分别放入不同的包中,如下图所示。


图 1 同名的Java类放在不同的包中

包的作用是:
  1. 管理 Java 类,便于查找和使用相应的文件;
  2. 区分同名的类,防止命名冲突;
  3. 实现访问权限控制。

创建包之后,该包中的所有 Java 代码第一行必须添加包信息,使用 package 声明包,如下图所示。


图 2 使用package声明包

包的命名规范是:
在一个类中调用不同包的类时,需要使用 import 关键字导入该类,语法:
import 包名.类名;
Eclipse 会自动提示需要导类,如下图所示。


图 3 Eclipse自动提示需要导类

导入成功后如下图所示。


图 4 成功导入类

例如,下列代码的运行结果是什么呢?
public class People {
    private int id = 3;
    public People(){
        System.out.println("编号是"+id);
    }
    public void setId(int id){
        this.id = id;
    }
    public void show(){
        System.out.println("编号是"+id);
    }
}

public class Student extends People {
    public Student(int id){
        super.setId(id);
    }
}

public class Test {
    public static void main(String[] args) {
        Student student = new Student(1);
        student.show();
    }
}
这里考察的知识点有两个:
所以 main 方法的第 1 行,“Student student = new Student(1);”调用了 Student 的有参构造,会默认调用 People 的无参构造,People 的无参构造函数会打印“编号是”和 id,id 是 People 的成员变量,值为 3,所以控制台输出为“编号是3”。

同时,Student 的有参构造调用了 People 的 setId() 方法,参数为 1,所以 People 的成员变量 id 的值被修改为 1。接下来调用 student 从 People 继承来的 show() 方法,会再次打印“编号是”和 id,此时控制台输出“编号是1”。

最终程序的运行结果是:

编号是3
编号是1

方法重写

子类在继承父类方法的基础上,对父类方法重新定义并覆盖的操作叫作方法重写。

儿子继承了父亲的房子,但是对房子的装修风格不满意,于是把之前的装修成果全部拆掉,按照自己的审美重新装修,就类似于方法重写的概念。

举个简单的例子:
public class People {
    public void show() {
        System.out.println("输出人员信息");
    }
}

public class Student extends People {

}

public class Teacher extends People {

}

public class Test {
    public static void main(String[] args) {
         Student student = new Student();
         student.show();
         Teacher teacher = new Teacher();
         teacher.show();
    }
}
程序运行结果为:

输出人员信息
输出人员信息

在父类 People 中定义了 show() 方法,打印“输出人员信息”,子类 Student 和 Teacher 继承了父类 People,同时继承了 show() 方法,在测试类中创建 Student 和 Teacher 对象,调用 show() 方法,看到结果打印了两次“输出人员信息”,此时并没有区分出 Student 和 Teacher,即没有体现出子类的特有信息。

现在使用方法重写对代码进行优化,Student 和 Teacher 的代码为:
public class Student extends People {
    //方法重写
    @Override
    public void show() {
        // TODO Auto-generated method stub
        System.out.println("这是一个学生");
    }
}

public class Teacher extends People {
    //方法重写
    @Override
    public void show() {
        // TODO Auto-generated method stub
        System.out.println("这是一个老师");
    }
}
再次运行,结果为:

这是一个学生
这是一个老师

通过重写的方式可以实现子类完成特定需求的功能,需要注意的是构造方法不能被重写。方法重写的规则是:
  1. 父子类的方法名相同;
  2. 父子类的方法参数列表相同;
  3. 子类方法返回值与父类方法返回值类型相同或者是其子类。
  4. 子类方法的访问权限不能小于父类。

1) 和 2) 很好理解,重点说明 3) 和 4)。要求子类方法返回值与父类方法返回值类型相同或者是其子类,代码如下所示:
public class People {
    public Object getObj(){
        Object obj = new Object();
        return obj;
    }
}
//返回值相同的重写。

public class Student extends People {
    //方法重写
    public Object getObj(){
        Object obj = new Object();
        return obj;
    }
}
//子类方法返回值类型是父类方法返回值类型子类的重写。

public class Student extends People {
    //方法重写
    public String getObj(){
        return "这是一个学生";
    }
}
子类方法的访问权限不能小于父类。这个规则跟前面讲过的访问权限修饰符有关,我们知道访问权限修饰符有 4 种,按照作用域范围从大到小排列为:

public > protected > 默认修饰符 > private

若父类方法的访问权限修饰符为 public,则子类重写方法的访问权限修饰符只能是 public;若父类方法的访问权限修饰符为 protected,则子类重写方法的访问权限修饰符可以是 public 和 protected;若父类方法的访问权限修饰符为默认修饰符,则子类重写方法的访问权限修饰符可以是 public、protected、默认修饰符。父类的静态方法不能被子类重写为非静态方法,父类的非静态方法不能被子类重写为静态方法,父类的私有方法不能被子类重写。

方法重写VS方法重载

对于初学者来说,方法重写和方法重载很容易产生混淆,一张表带你了解两者的区别。

表:方法重写VS方法重载
  所在位置 方法名 参数列表 返回值 访问权限
方法重写 子类 相同 相同 相同或是其子类 不能小于父类
方法重载 同一个类 相同 不同 没有要求 没有要求

推荐阅读