继承
多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要继承那一个类即可。如图所示:
其中,多个类可以称为子类,单独那一个类称为父类、超类(superclass)或者基类。继承描述的是事物之间的所属关系,这种关系是: is-a 的关系。例如,图中兔子属于食草动物,食草动物属于动 物。可见,父类更通用,子类更具体。我们通过继承,可以使多种事物之间形成一种关系体系。继承主要解决的问题是共性抽取。
定义
继承:就是子类继承父类的属性和行为,使得子类对象具有与父类相同的属性、相同的行为。子类可以直接 访问父类中的非私有的属性和行为。
继承的格式
通过 extends 关键字,可以声明一个子类继承另外一个父类,定义格式如下:
继承演示,代码如下:
定义父类
package demo01; // 定义一个父类:员工 public class Employee { public void method() { System.out.println("方法执行!"); } }
定义子类
package demo01; // 定义了一个员工的子类:讲师 public class Teacher extends Employee { }
定义测试类
package demo01; /* 在继承的关系中,“子类就是一个父类”。也就是说,子类可以被当做父类看待。 例如父类是员工,子类是讲师,那么“讲师就是一个员工”。关系:is-a。 定义父类的格式:(一个普通的类定义) public class 父类名称 { // ... } 定义子类的格式: public class 子类名称 extends 父类名称 { // ... } */ public class Demo01Extends { public static void main(String[] args) { // 创建了一个子类对象 Teacher teacher = new Teacher(); // Teacher类当中虽然什么都没写,但是会继承来自父类的method方法。 teacher.method(); } }
好处
- 提高了代码的复用性(多个类相同的成员可以放到同一个类中)
- 提高了代码的维护性(如果方法的代码需要修改,修改一处即可)
- 继承的出现让类与类之间产生了关系,提供了多态的前提。
- 继承的作用中除了可以让代码复用之外,还有非常重要的两个作用,那就是有了继承之后才会衍生出方法的覆盖和多态机制。
注意:不要仅为了获取其他类中某个功能而去继承
子类的特点
- 子类继承了父类,就继承了父类的方法和属性。
- 在子类中,可以使用父类中定义的方法和属性,也可以创建新的数据和方法。
- 在Java 中,继承的关键字用的是“extends”,即子类不是父类的子集,而是对父类的“扩展”。
继承的规则:
- B类继承 A类,则称 A类为超类(superclass)、父类、基类,B类则称为子类(subclass)、派生类、扩展类。
- 子类不能直接访问父类中私有的(private)的成员变量和方法。
Java只支持单继承和多层继承,不允许多重继承
- 一个子类只能有一个父类
- 一个父类可以派生出多个子类
class SubDemo extends Demo{ } //ok
class SubDemo extends Demo1,Demo2...//error
java 中规定,子类继承父类,除构造方法不能继承之外,剩下都可以继承。但是私有的属性无法在子类中直接访问。(父类中private修饰的不能在子类中直接访问。可以通过间接的手段来访问。)
java 中的类没有显示的继承任何类,则默认继承 Object类,Object类是 java 语言提供的根类(老祖宗类),也就是说,一个对象与生俱来就有 Object类型中所有的特征。
继承的弊端
- 继承让类与类之间产生了关系,类的耦合性增强了,当父类发生变化时子类实现也不得不跟着变化,削弱了子类的独立性
继承的应用场景
- 使用继承,需要考虑类与类之间是否存在is..a的关系,不能盲目使用继承。is..a的关系:谁是谁的一种,例如:老师和学生是人的一种,那人就是父类,学生和老师就是子类
继承中成员方法的访问特点
创建父类对象,只能使用父类的东西,没有任何子类内容
本质上,子类继承父类之后,是将父类继承过来的方法归为自己所有。实际上调用的也不是父类的方法,是他子类自己的方法(因为已经继承过来了就属于自己的)。
通过子类对象访问一个方法
- 先去子类成员范围找
- 在去父类成员范围找
- 如果都没有就报错(不考虑父亲的父亲…)
继承中变量的访问特点
创建父类对象,只能使用父类的东西,没有任何子类内容
创建子类对象,在子类方法中访问一个变量,采用的是就近原则。
- 先去子类局部范围找
- 在去子类成员范围找
- 之后去父类成员范围找
- 如果都没有就报错(不考虑父亲的父亲…)
访问子类方法中重名的三种变量
- 访问局部变量: 直接写成员变量名
- 访问本类的成员变量:this.成员变量名
- 访问父类的成员变量: super.成员变量名
继承中构造方法的访问特点
- 子类中所有的构造方法默认都会访问父类中无参的构造方法
- 子类会继承父类中的数据,可能还会使用父类的数据。所以,子类初始化之前,一定要先完成父类数据的初始化,原因在于,每一个子类构造方法的第一条语句默认都是:super()
- 构造方法的名字是与类名一致的。所以子类是无法继承父类构造方法的
- 对象在堆内存中,会单独存在一块super区域,用来存放父类的数据
问题:如果父类中没有无参构造方法,只有带参构造方法,该怎么办呢?
- 通过使用super关键字去显示的调用父类的带参构造方法
- 在父类中自己提供一个无参构造方法
super 概述
super 和 this 可以对比着学习:
this
- this 是一个引用,保存内存地址指向自己。
- this 出现在实例方法中,谁调用这个实例方法,this 就代表谁,this 代表当前正在执行这个动作的对象。
- this 不能出现在静态方法中。
- this 大部分情况下可以省略,在方法中区分实例变量和局部变量的时候不能省略。
- “this(实际参数列表)”出现在构造方法第一行,通过当前的构造方法去调用本类当中其它的构造方法。
super
- super能出现在实例方法和构造方法中。
- super的语法是:“super.”、“super()”
- super不能使用在静态方法中。
- super. 大部分情况下是可以省略的。
- super.什么时候不能省略呢? ???????
- super() 只能出现在构造方法第一行,通过当前的构造方法去调用“父类”中的构造方法,目的是:创建子类对象的时候,先初始化父类型特征。
- super()表示通过子类的构造方法调用父类的构造方法。模拟现实世界中的这种场景:要想有儿子,需要先有父
重要的结论:
当一个构造方法第一行:既没有this()又没有super()的话,默认会有一个super();表示通过当前子类的构造方法调用父类的无参数构造方法。所以必须保证父类的无参数构造方法是存在的。 注意:this()和super() 不能共存,它们都是只能出现在构造方法第一行。
- 无论是怎样折腾,父类的构造方法是一定会执行的。
- 在构造方法执行过程中一连串调用了父类的构造方法,父类的构造方法又继续向下调用它的父类的构造方法,但是实际上对象只创建了一个。
三种用法
/* super关键字的用法有三种: 1. 在子类的成员方法中,访问父类的成员变量。 2. 在子类的成员方法中,访问父类的成员方法。 3. 在子类的构造方法中,访问父类的构造方法。 super关键字用来访问父类内容,而this关键字用来访问本类内容。用法也有三种: 1. 在本类的成员方法中,访问本类的成员变量。 2. 在本类的成员方法中,访问本类的另一个成员方法。 3. 在本类的构造方法中,访问本类的另一个构造方法。 在第三种用法当中要注意: A. this(...)调用也必须是构造方法的第一个语句,唯一一个。 B. super和this两种构造调用,不能同时使用。 */
this和super的区别
父类空间优先于子类对象产生
继承中构造方法的访问特点
- 子类中所有的构造方法默认都会访问父类中无参的构造方法
- 子类会继承父类中的数据,可能还会使用父类的数据。所以,子类初始化之前,一定要先完成父类数据的初始化,原因在于,每一个子类构造方法的第一条语句默认都是:super()
- 构造方法的名字是与类名一致的。所以子类是无法继承父类构造方法的
- 对象在堆内存中,会单独存在一块super区域,用来存放父类的数据
package demo07; /* 继承关系中,父子类构造方法的访问特点: 1. 子类构造方法当中有一个默认隐含的“super()”调用,所以一定是先调用的父类构造,后执行的子类构造。 2. 子类构造可以通过super关键字来调用父类重载构造。 3. super的父类构造调用,必须是子类构造方法的第一个语句。不能一个子类构造调用多次super构造。 总结: 子类必须调用父类构造方法,不写则赠送super();写了则用写的指定的super调用,super只能有一个,还必须是第一个。 */ public class Demo01Constructor { public static void main(String[] args) { Zi zi = new Zi(); } }
问题:如果父类中没有无参构造方法,只有带参构造方法,该怎么办呢?
- 通过使用super关键字去显示的调用父类的带参构造方法
- 在父类中自己提供一个无参构造方法
方法覆盖 Override
概念
- 子类出现了和父类中一模一样的方法声明(方法名一样,参数列表也必须一样)就发生了方法覆盖。
方法重写的应用场景
- 当子类需要父类的功能,而功能主体子类有自己特有内容时,可以重写父类中的方法,这样,即沿袭了父类的功能,又定义了子类特有的内容
方法覆盖的条件
- 方法覆盖发生在具有继承关系的父子类之间,这是首要条件
- 覆盖之后的方法与原方法具有相同的返回值类型、相同的方法名、相同的形式参数列表
代码示例
package com.wrg; /* * 方法的重写(override / overwrite) * * 1.重写:子类继承父类以后,可以对父类中同名同参数的方法,进行覆盖操作 * * 2.应用:重写以后,当创建子类对象以后,通过子类对象调用子父类中的同名同参数的方法时,实际执行的是子类重写父类的方法。 * * 3. 重写的规定: * 方法的声明: 权限修饰符 返回值类型 方法名(形参列表) throws 异常的类型{ * //方法体 * } * 约定俗称:子类中的叫重写的方法,父类中的叫被重写的方法 * ① 子类重写的方法的方法名和形参列表与父类被重写的方法的方法名和形参列表相同 * ② 子类重写的方法的权限修饰符不小于父类被重写的方法的权限修饰符 * >特殊情况:子类不能重写父类中声明为private权限的方法 * ③ 返回值类型: * >父类被重写的方法的返回值类型是void,则子类重写的方法的返回值类型只能是void * >父类被重写的方法的返回值类型是A类型,则子类重写的方法的返回值类型可以是A类或A类的子类 * >父类被重写的方法的返回值类型是基本数据类型(比如:double),则子类重写的方法的返回值 类型必须是相同的基本数据类型(必须也是double) * ④ 子类重写的方法抛出的异常类型不大于父类被重写的方法抛出的异常类型(具体放到异常处理时候讲) * ********************************************************************** * 子类和父类中的同名同参数的方法要么都声明为非static的(考虑重写),要么都声明为static的(不是重写)。 * * 面试题:区分方法的重载与重写 */ public class Test { public static void main(String[] args) { // 创建子类对象 NewPhone np = new NewPhone(); // 调用父类继承而来的方法 np.call("张三"); // 调用子类重写的方法 np.showNum(); } } //父类 class Phone { //父类的方法 public void sendMessage() { System.out.println("发短信"); } public void call(String name) { System.out.println("给" + name + "打电话"); } public void showNum() { System.out.println("来电显示号码"); } } //智能手机类,子类 class NewPhone extends Phone { //重写父类的来电显示号码功能,并增加自己的显示姓名和图片功能 public void showNum() { //调用父类已经存在的功能使用super关键字 super.showNum(); //增加自己特有显示姓名和图片功能 System.out.println("显示来电姓名"); System.out.println("显示头像"); } }
另外,在使用方法覆盖的时候,需要有哪些注意事项呢?
- 由于覆盖之后的方法与原方法一模一样,建议在开发的时候采用复制粘贴的方式,不建议手写
- @Override:写在方法前面,用来检测是不是有效的正确覆盖重写。这个注解就算不写,只要满足要求,也是正确的方法覆盖重写。
- 私有的方法不能被继承,所以不能被覆盖
- 构造方法不能被继承,所以也不能被覆盖
- 覆盖之后的方法不能比原方法拥有更低的访问权限,可以更高
- 覆盖之后的方法不能比原方法抛出更多的异常,可以相同或更少
- 方法覆盖只是和方法有关,和属性无关
- 静态方法不存在覆盖,方法覆盖只能针对实例方法。
子类对象的实例化过程
子类对象实例化的全过程
- 从结果上来看:(继承性)子类继承父类以后,就获取了父类中声明的属性或方法。 创建子类的对象,在堆空间中,就会加载所有父类中声明的属性。
- 从过程上来看:当我们通过子类的构造器创建子类对象时,我们一定会直接或间接的调用其父类的构造器,进而调用父类的父类的构造器,...直到调用了java.lang.Object类中空参的构造器为止。正因为加载过所有的父类的结构,所以才可以看到内存中有父类中的结构,子类对象才可以考虑进行调用。
- 明确:虽然创建子类对象时,调用了父类的构造器,但是自始至终就创建过一个对象,即为new的子类对象。