Java 之继承和 final 关键字
- 继承的概述
- 继承的特点
- super 关键字
- 函数覆盖
- 子类的实例化过程
- final 关键字
1. 继承的概述
继承是类与类之间的关系.
继承的好处:
- 提高了代码的复用性
- 让类与类之间产生了关系, 给第三个特征多态提供了前提
继承按直接父类的个数分为两种:
- 单继承: 一个子类只能有一个直接父类
- 多继承: 一个子类可以有多个直接父类, 此继承方式在 java 中不允许.
java 对此种继承方式进行了改良, 即通过"多实现"的方式来实现的.
// 不直接支持多继承的原因: 多个父类中有相同成员, 会产生调用的不确定性
class A
{
void show()
{
System.out.println("a");
}
}
class B
{
void show()
{
System.out.println("b");
}
}
class C extends A,B
{
}
new C().show(); // 由于 C 继承了 A 和 B, 此时,出现调用的不确定性
继承体系
- java 支持多重继承, 即 C 继承 B, B 继承 A.
当要使用一个继承体系时
- 查看该体系中的顶层类(如上图中的 A 类), 了解该体系的基本功能
- 创建体系中的最子类对象(如上图中的 D 类), 完成功能的使用
什么时候定义继承呢?
- 当类与类之间存在着所属关系时, 就定义继承.
- 所属关系: is a 关系
例如:狗类是犬科类的一种,狗类就可以继承犬科类.
2. 继承的特点
在子父类中,成员的特点体现
- 成员变量
- 成员函数
- 构造函数
成员变量
- 当本类的成员变量和局部变量同名用 this 区分;
当子父类中的成员变量同名用 super 区分. - this 和 super 的用法很相似.不同之处在于
- this: 代表一个本类对象的引用
- super: 代表一个父类的空间, 不代表父类对象
- 子类不能直接访问父类中私有内容, 但是可以通过 get 方法间接访问父类中私有内容
class Fu
{
int num = 4;
}
class Zi extends Fu
{
int num = 5;
void show()
{
System.out.println(this.num+"..."+super.num);
}
}
class ExtendsDemo
{
public static void main(String[] args)
{
Zi z = new Zi();
z.show();
}
}
// 示例: 写出程序结果
class Super
{
int i = 0;
public Super(String a) // 带参数构造函数
{
System.out.println("A");
i = 1;
}
public Super() // 空参数构造函数
{
System.out.println("B");
i += 2;
}
}
class Demo extends Super
{
public Demo(String s) // 带参数构造函数
{
System.out.println("C");
i += 5; // 子类中没有定义 i, 直接访问父类的 i
}
public static void main(String[] args)
{
int i = 4;
Super d = new Demo("A");
System.out.println(d.i);
}
}
// 输出结果: B C 7
成员函数
当子父类中出现成员函数一模一样的情况, 会运行子类的函数, 这种现象称为覆盖操作. 这是函数在子父类中的特性.
覆盖操作注意事项:
- 子类方法覆盖父类方法时, 子类成员函数权限必须要大于等于父类成员函数时的权限.
权限分为三种: public, private 和 默认权限. - 静态只能覆盖静态, 或被静态覆盖
- 必须保证子类和父类函数一模一样, 即 返回值类型相同, 函数名一样, 参数列表一样.
// 示例1: 写出程序结果
class Super
{
public int get(){return 4;}
}
class Demo extends Super
{
public long get() {return 5;} // 编译失败, 函数调用的不确定性
public static void main(String[] args)
{
Super s = new Demo();
System.out.println(s.get());
}
}
// 示例2: 写出错误答案错误的原因, 用单行注释的方式
class Demo
{
int show(int a, int b){return 0;}
}
下面哪些函数可以存在于 Demo 的子类中
A. public int show(int a, int b){return 0;} // 可以, 函数覆盖
B. private int show(int a, int b){return 0;} // 不可以, 权限不够
C. private int show(int a, long b){return 0;} // 可以, 子类特有方法
D. public short show(int a, int b){return 0;} // 不可以, 调用的不确定性
E. static int show(int a, int b){return 0;} // 不可以, 静态只能覆盖静态
函数的两个特性:
- 重载: 发生在同一个子类中
- 覆盖: 也称为重写(覆写), override
什么时候使用覆盖操作?
当对一个类进行子类的扩展时, 子类需要保留父类的功能声明,
但是要定义子类中该功能的特有内容时, 就使用覆盖操作完成.
// 早期手机来电显示, 只能显示电话号码
class ExtendsDemo
{
public static void main(String[] args)
{
Phone p = new Phone();
p.show
}
}
class Phone
{
void show()
{
System.out.println("number"); //来电显示电话号码
}
}
// 手机升级, 增加来电显示姓名, 大头贴和电话号码
// 此时,相当于代码升级,不应该在源代码上过分修改.
class NewPhone extends Phone // 继承父类
{
void show() // 函数覆盖
{
System.out.println("name"); // 增加姓名
System.out.println("pic"); // 增加大头贴
// System.out.println(number);
super.show(); // 调用父类 show() 方法
}
}
构造函数
class Fu
{
Fu() //父类构造函数
{
System.out.println("fu run");
}
}
class Zi extends Fu
{
Zi() // 子类构造函数
{
System.out.println("zi run");
}
}
class ExtendsDemo
{
public static void main(String[] args)
{
new Zi();
}
}
运行结果:
在子类构造对象时, 发现访问子类构造函数时, 父类的构造函数也运行了.
为什么呢?
原因是: 在子类的构造函数中第一行有一个默认的隐式语句, super(); 调用的就是父类中的空参数的构造函数.
如果父类中的构造函数有参数, 需要使用 super 指定.
为什么子类实例化的时候要访问父类中的构造函数呢?
因为子类继承了父类, 获取到了父类中的内容(属性), 所以在使用父类内容之前,要先看父类是如何对自己
的内容进行初始化的.所以子类在构造对象时, 必须访问父类中的构造函数.为类完成这个必须的动作, 就在
子类的构造函数中加入了 super() 语句.
如果父类中没有定义空参数构造函数, 那么子类的构造函数必须用 super 明确要调用父类中哪个构造函数.
同时, 子类的构造函数中如果使用 this 调用类本类构造函数时,那么 super 就没有了, 因为 super 和
this 都只能定义在第一行, 所以只能有一个. 但是可以保证的是, 子类中肯定会有其他的构造函数访问父类
的构造函数.
注意: super 语句必须定义在子类构造函数的第一行, 因为父类的初始化动作要先完成.
// 子类实例化过程: 子类中所有的构造函数默认都会访问父类中的空参数的构造函数
class Fu
{
Fu()
{
System.out.println("A");
}
Fu(int x)
{
System.out.println("B"+ x);
}
}
class Zi extends Fu
{
Zi()
{
System.out.println("C");
}
Zi(int x)
{
System.out.println("D"+ x)
}
}
class ExtendsDemo
{
public static void main(String[] args)
{
new Zi(6);
}
}
// 输出结果: A D6
// 子类初始化,必须先访问父类的构造函数
class Fu
{
Fu()
{
System.out.println("A");
}
Fu(int x)
{
System.out.println("B"+ x);
}
}
class Zi extends Fu
{
Zi()
{
super(); // 通过这个构造函数,默认的隐式语句 super(); 访问父类中的空参数构造函数
System.out.println("C");
}
Zi(int x)
{
this(); // 访问本类中其他构造函数
System.out.println("D"+ x)
}
}
class ExtendsDemo
{
public static void main(String[] args)
{
new Zi(6);
}
}
4. 子类的对象实例化过程
class Fu
{
Fu() // 构造函数
{
super(); // 默认隐式语句
show();
return;
}
void show()
{
System.out.println("fu show");
}
}
class Zi extends Fu
{
int num = 8;
Zi() // 默认构造函数
{
super();
return;
}
void show()
{
System.out.println("zi show..."+num);
}
}
class ExtendsDemo
{
public static void main(String[] args)
{
Zi z = new Zi();
}
}
// 运行结果: zi show...0
一个对象实例化过程
以 Person p = new Person(); 为例:
- JVM 会读取指定的路径下的 Person.class 文件, 并加载进内存,
并会先加载 Person 的父类(如果有直接的父类的情况下) - 在堆内存中, 开辟空间, 分配内存地址
- 在对象空间中, 对对象中的属性进行默认初始化
- 调用对应的构造函数进行初始化
- 在构造函数中, 第一行会先调用父类中的构造函数进行初始化
- 父类初始化完毕后, 在对子类的属性进行显示初始化
- 再进行子类构造函数的特定初始化
- 初始化完毕后, 将地址值赋给引用变量
5. final 关键字
- final 是一个修饰符, 可以修饰类, 方法, 变量
- final 修饰的类不可以被继承
- final 修饰的方法不可以被覆盖
- final 修饰的变量是一个常量, 只能赋值一次
- 写法规范: 常量所有单词都大写, 若有多个单词, 使用下划线(_)连接
- 内部类只能访问被 final 修饰的局部变量
参考资料