什么是继承,Java继承详解(小白必读)
在讲解继承的概念之前,我们先来看一个示例,定义一个 Student 类和一个 Teacher 类,分别有 id、name、age、gender 属性,代码如下所示:
来找找 Student 和 Teacher 的共性,我们可不可以定义一个 People 类,然后让 Student 和 Teacher 拥有 People 类的属性和方法呢?这种代码优化的方式叫作继承,即一个类继承另外一个类的属性和方法,被继承的类叫父类,继承的类叫子类。People 就是父类,Student 和 Teacher 就是子类。那继承如何实现呢?
继承的基本语法如下:
程序中的继承和现实生活中的例子是一样的,儿子可以继承父亲的资产,那么儿子就不需要那么辛苦打拼,可以轻松拥有父亲给他的一切,同时儿子还可以在继承父亲资产的基础上继续创造属于自己的资产。
继承是面向对象编程思想的主要特征,Java 通过继承可以实现代码复用。Java 只支持单继承,即一个类只能有一个直接父类。注意,我们这里说的是只能有一个直接父类,父类的父类资源也是可以被继承的,相当于父亲从爷爷那里继承的资产,可以传到儿子手上。
那么问题来了,在创建 People 对象时,会不会调用其他类的构造函数呢?即 People 类有没有自己的父类?答案是 People 也有自己的父类,只不过这个父类不是我们自定义的,而是 Java 提供的。Java 中的每个类都有一个共同父类 Object,Object 类就是所有 Java 类的根(老祖宗),所有的 Java 类都是由 Object 类派生出来的。
我们现在明白了,在创建子类时会默认调用父类的无参构造来创建父类对象,那么能否让父类调用有参构造来创建对象呢?答案是可以的,如何完成?需要使用 super 关键字,super 关键字和 this 关键字类似,但用法完全不同,this 用作访问当前类的属性和方法,super 用作子类访问父类的属性和方法。要调用父类的有参构造,可以修改 Student 类,修改好的代码如下所示:
好了,我们已经学习了如何在子类构造函数中调用父类构造函数,那么在子类普通方法中如何调用父类的属性和普通方法呢?同样是使用 super 关键字,例如访问属性:“super.属性名”,调用普通方法:“super.方法名();”。
需要强调的是,子类只能访问父类的公有属性和方法,即使用 public 修饰的属性和方法,无法访问私有 private 修饰的属性和方法,父类的私有属性可以通过它的公有方法来访问和修改,具体调用参考下面的代码:
访问权限修饰符可以用来修饰类、属性和方法,不同的访问权限修饰符表示不同的作用域,包括 public、protected、默认修饰符和 private 这 4 种修饰符。一般使用public来修饰类,我们这里主要说的是对属性和方法的访问权限修饰符,其作用域如下表所示。
从表中可以看到,子类只能访问父类 public 和 protected 修饰的属性和方法,默认修饰符和 private 修饰的属性和方法不能访问。
这里引入了一个新的概念:包(package)。为什么要有包呢?包用来管理 Java 类,类似于我们用不同的文件夹管理不同的文件,一个项目中不可避免地会出现同名的 Java 类,为了防止产生冲突,可以把同名的 Java 类分别放入不同的包中,如下图所示。
图 1 同名的Java类放在不同的包中
包的作用是:
创建包之后,该包中的所有 Java 代码第一行必须添加包信息,使用 package 声明包,如下图所示。
图 2 使用package声明包
包的命名规范是:
在一个类中调用不同包的类时,需要使用 import 关键字导入该类,语法:
图 3 Eclipse自动提示需要导类
导入成功后如下图所示。
图 4 成功导入类
例如,下列代码的运行结果是什么呢?
所以 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”。
最终程序的运行结果是:
儿子继承了父亲的房子,但是对房子的装修风格不满意,于是把之前的装修成果全部拆掉,按照自己的审美重新装修,就类似于方法重写的概念。
举个简单的例子:
现在使用方法重写对代码进行优化,Student 和 Teacher 的代码为:
1) 和 2) 很好理解,重点说明 3) 和 4)。要求子类方法返回值与父类方法返回值类型相同或者是其子类,代码如下所示:
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对象
那么问题来了,在创建 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对象
好了,我们已经学习了如何在子类构造函数中调用父类构造函数,那么在子类普通方法中如何调用父类的属性和普通方法呢?同样是使用 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类放在不同的包中
包的作用是:
- 管理 Java 类,便于查找和使用相应的文件;
- 区分同名的类,防止命名冲突;
- 实现访问权限控制。
创建包之后,该包中的所有 Java 代码第一行必须添加包信息,使用 package 声明包,如下图所示。
图 2 使用package声明包
包的命名规范是:
- 包名由小写字母组成,不能以.开头或结尾。
- 包名一般由小写字母组成,可以包含数字,但不能以数字开头,使用“.”来分层,不能将“.”用作开头或结尾。
- 包的命名方式一般采用网络域名的反向输出,如 com.southwind.test 和 com.southwind.entity。
在一个类中调用不同包的类时,需要使用 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(); } }程序运行结果为:
输出人员信息
输出人员信息
现在使用方法重写对代码进行优化,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)。要求子类方法返回值与父类方法返回值类型相同或者是其子类,代码如下所示:
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方法重载
对于初学者来说,方法重写和方法重载很容易产生混淆,一张表带你了解两者的区别。所在位置 | 方法名 | 参数列表 | 返回值 | 访问权限 | |
---|---|---|---|---|---|
方法重写 | 子类 | 相同 | 相同 | 相同或是其子类 | 不能小于父类 |
方法重载 | 同一个类 | 相同 | 不同 | 没有要求 | 没有要求 |