Java 之继承和 final 关键字

  1. 继承的概述
  2. 继承的特点
  3. super 关键字
  4. 函数覆盖
  5. 子类的实例化过程
  6. final 关键字

1. 继承的概述

继承是类与类之间的关系.

继承的好处:

  • 提高了代码的复用性
  • 让类与类之间产生了关系, 给第三个特征多态提供了前提

继承按直接父类的个数分为两种:

  1. 单继承: 一个子类只能有一个直接父类
  2. 多继承: 一个子类可以有多个直接父类, 此继承方式在 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.

Java 之继承和 final 关键字

当要使用一个继承体系时
  1. 查看该体系中的顶层类(如上图中的 A 类), 了解该体系的基本功能
  2. 创建体系中的最子类对象(如上图中的 D 类), 完成功能的使用

什么时候定义继承呢?

  • 当类与类之间存在着所属关系时, 就定义继承.
  • 所属关系: is a 关系

    例如:狗类是犬科类的一种,狗类就可以继承犬科类.

Java 之继承和 final 关键字

2. 继承的特点

在子父类中,成员的特点体现

  1. 成员变量
  2. 成员函数
  3. 构造函数

成员变量

  • 当本类的成员变量和局部变量同名用 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();
}
}

Java 之继承和 final 关键字

// 示例: 写出程序结果
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

成员函数

当子父类中出现成员函数一模一样的情况, 会运行子类的函数, 这种现象称为覆盖操作. 这是函数在子父类中的特性.

覆盖操作注意事项:

  1. 子类方法覆盖父类方法时, 子类成员函数权限必须要大于等于父类成员函数时的权限.

    权限分为三种: public, private 和 默认权限.
  2. 静态只能覆盖静态, 或被静态覆盖
  3. 必须保证子类和父类函数一模一样, 即 返回值类型相同, 函数名一样, 参数列表一样.
// 示例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;} // 不可以, 静态只能覆盖静态

函数的两个特性:

  1. 重载: 发生在同一个子类中
  2. 覆盖: 也称为重写(覆写), 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();
}
}

运行结果:

Java 之继承和 final 关键字

在子类构造对象时, 发现访问子类构造函数时, 父类的构造函数也运行了.

为什么呢?

原因是: 在子类的构造函数中第一行有一个默认的隐式语句, 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

Java 之继承和 final 关键字

一个对象实例化过程

以 Person p = new Person(); 为例:

  1. JVM 会读取指定的路径下的 Person.class 文件, 并加载进内存,

    并会先加载 Person 的父类(如果有直接的父类的情况下)
  2. 在堆内存中, 开辟空间, 分配内存地址
  3. 在对象空间中, 对对象中的属性进行默认初始化
  4. 调用对应的构造函数进行初始化
  5. 在构造函数中, 第一行会先调用父类中的构造函数进行初始化
  6. 父类初始化完毕后, 在对子类的属性进行显示初始化
  7. 再进行子类构造函数的特定初始化
  8. 初始化完毕后, 将地址值赋给引用变量

5. final 关键字

  1. final 是一个修饰符, 可以修饰类, 方法, 变量
  2. final 修饰的类不可以被继承
  3. final 修饰的方法不可以被覆盖
  4. final 修饰的变量是一个常量, 只能赋值一次
  5. 写法规范: 常量所有单词都大写, 若有多个单词, 使用下划线(_)连接
  6. 内部类只能访问被 final 修饰的局部变量







    参考资料
上一篇:STL源码剖析 — 空间配置器(allocator)


下一篇:STL之空间配置器allocator