继承的概述
- 多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要继承单独的那个类即可;
- 多个类可以称为子类,单独这个类称为父类或者超类;
- 子类可以直接访问父类中的非私有的属性和行为。对于父类中私有的部分,子类对象是无法直接访问的;
- 通过extends关键字让类与类之间产生继承关系,如
class SubDemo extends Demo{}
。
这里用一个例子来让我们走近继承。例,将学生(Student)和工人(Worker)的共性描述提取出来,单独进行描述,只要让学生和工人与单独描述的这个类有关系,就可以了。
// 将学生和工人的共享代码向上抽取到一个共性的类型中,这个类型中即包括学生和工人
class Person // 父类(超类或基类)
{
String name;
int age;
}
// 描述学生,属性:姓名、年龄,行为:学习。让学生和Person产生关系,就可以让学生使用Person中的共性的内容。
// 通过一个关键字extends继承。
class Student /*子类*/ extends Person
{
// String name;
// int age;
void study()
{
System.out.println("good good");
}
}
// 描述工人,属性:姓名、年龄,行为:工作
class Worker extends Person
{
// String name;
// int age;
void work()
{
System.out.println("hard");
}
}
class ExtendsDemo
{
public static void main(String[] args)
{
Student s = new Student();
s.name = "小明";
s.age = 14;
s.study();
}
}
通过以上的例子,就可知道继承的好处了。
继承的好处
- 提高了代码的复用性,即多个类相同的成员可以放到同一个类中;
- 提高了代码的维护性,即如果功能的代码需要修改,修改一处即可;
- 让类与类之间产生了关系,有了这个关系,才有了多态的特性,其实这也是继承的一个弊端——类的耦合性很强;
- 设计原则:高内聚低耦合。
我们可以简单理解为:内聚就是自己完成某件事情的能力,耦合就是类与类之间的关系。我们在设计时候的原则是:自己能完成的就不麻烦别人,这样将来别人产生了修改,就对我的影响较小。由此可见,在开发中使用继承其实是在使用一把双刃剑。今天我们还是以继承的好处来使用,因为继承还有很多其他的特性。
继承的特点
Java只支持单继承,不支持多继承
Java语言中,Java只支持单继承(一个类只能有一个父类),不支持多继承(一个类可以有多个父类)。因为多继承容易带来安全隐患,当多个父类中定义了相同功能时,当功能内容不同时,子类对象不确定要运行哪一个。举例说明如下:
class A {
void show() {
System.out.println("a");
}
}
class B {
void show() {
System.out.println("b");
}
}
class C extends A, B {
C c = new C();
c.show(); // 此时该运行哪一个类中的方法呢?出现了调用的不确定性,因为方法的主体不同。
}
但是Java保留了这种机制,并用另一种体现形式来完成表示——(多实现),后面回讲。
Java支持多层继承(继承体系)
Java支持多层继承,也就是一个继承体系。
- 那么如何使用一个继承体系中的功能呢?
想要使用体系,先查阅体系父类的描述,因为父类中定义的是该体系中的共性功能,通过了解共性功能,就可以知道该体系的基本功能,那么这个体系就可以基本使用了。 - 那么在具体调用时,要创建最子类的对象,为什么呢?
一是因为有可能父类不能创建对象(如抽象类),二是创建子类对象可以使用更多的功能,包括基本的也包括特有的。
简而言之,查阅父类功能,创建子类对象使用功能。
子父类出现后,类成员的特点
类中成员有:
- 成员变量;
- 成员函数;
- 构造函数。
当子父类出现后,代码上有一些什么特点呢?
子父类中的成员变量
如果子类中出现非私有的同名成员变量时,子类要访问本类中的变量用this,子类要访问父类中的同名变量用super,super的使用和this的使用几乎一致。
- this代表的是本类对象的引用;
- super代表的是父类的那片空间,并不代表父类对象的引用。
首先观察如下代码,并试着运行一下,你会得到什么结果呢?
class Fu
{
int num = 4;
}
class Zi extends Fu
{
int num2 = 5;
void show()
{
int num3 = 6;
System.out.println("num = " + num);
System.out.println("num2 = " + num2);
System.out.println("num3 = " + num3);
}
}
class ExtendsDemo2
{
public static void main(String[] args)
{
Zi z = new Zi();
z.show();
}
}
从运行结果来看,上述程序代码在内存中是怎么体现的呢?
但是当子父类中出现了同名的成员变量时,如下:
class Fu
{
int num = 4;
}
class Zi extends Fu
{
int num = 5;
void show()
{
int num = 6;
System.out.println("num = " + super.num);
}
}
class ExtendsDemo2
{
public static void main(String[] args)
{
Zi z = new Zi();
z.show();
}
}
此时以上程序代码在内存中的体现大致是如下图:
子父类中的函数
- 当子类出现和父类一模一样的函数时,当子类对象调用该函数时,会运行子类函数的内容,如同父类的函数被覆盖一样,这种情况是函数的另一种特性:重写(覆盖);
- 父类中的私有方法不可以被覆盖;
- 在子类覆盖方法中,继续使用被覆盖的方法可以通过super.函数名获取。
首先观察如下代码,并试着运行一下,你会得到什么结果呢?
class Fu
{
void show()
{
System.out.println("fu show run");
}
}
class Zi extends Fu
{
void show1()
{
System.out.println("zi show run");
}
}
class ExtendsDemo3
{
public static void main(String[] args)
{
Zi z = new Zi();
z.show();
z.show1();
}
}
知道运行后的结果,我们应该可以画出这样一个内存调用图来:
现在我们还是回到对子父类中的函数的讨论中,当子类继承父类,沿袭了父类的功能,子类虽然具备了该功能,但是功能的内容要和父类不一致时,没有必要定义新功能,而是使用覆盖特性,保留父类的功能定义,并重写功能内容。
class Fu {
void show() {
System.out.println("fu show");
}
void speak() {
System.out.println("vb");
}
}
class Zi extends Fu {
void speak() {
System.out.println("java");
}
public void show() {
System.out.println("zi show");
}
}
class ExtendsDemo3 {
public static void main(String[] args) {
Zi z = new Zi();
z.speak();
}
}
使用函数覆盖这一特性时,要特别注意如下两点:
- 子类方法覆盖父类方法,必须要保证权限大于等于父类权限;
- 静态只能覆盖静态,或者被静态覆盖。
子父类中的构造函数
在对子类对象进行初始化时,父类的构造函数也会运行,那是因为子类的构造函数默认第一行有一条隐式的语句:super(),它访问父类中空参数的构造函数,而且子类中所有的构造函数默认第一行都是super()。那么为什么子类对象初始化一定要去访问父类中的构造函数呢? 因为父类中的数据,子类可以直接获取,所以子类对象在建立时,需要先查看父类是如何对这些数据进行初始化的,所以子类在对象初始化时,要先访问一下父类中的构造函数。如果要访问父类中指定的构造函数,可以通过手动定义super语句的方式来指定。我们应该特别注意以下两点:
- 当父类中没有空参数构造函数时,子类需要通过显示定义super语句指定要访问的父类中的构造函数;
- 用来调用父类构造函数的super语句在子类构造函数中必须定义在第一行,因为父类的初始化要先完成。
这里就会产生一个问题:this和super都是用于调用构造函数,它们可以同时存在吗?答案是不可以,因为它们只能定义在第一行,所以说有了this语句,就没有super语句,有了super语句,就没有this语句了。
试着运行如下程序代码,你会得到什么结果呢?
class Fu { // extends object
int num;
Fu() {
num = 60;
System.out.println("fu run");
}
Fu(int x) {
System.out.println("fu..."+x);
}
}
class Zi extends Fu {
Zi() {
System.out.println("zi run");
}
Zi(int x) {
this(); // 此构造函数没有super语句
System.out.println("zi...."+x);
}
}
class ExtendsDemo4 {
public static void main(String[] args) {
Zi z = new Zi(0);
System.out.println(z.num);
}
}
输出结果:
fu run
zi run
zi….0
60
从上述例子的运行结果,可以知道子类的实例化过程,即子类的所有的构造函数,默认都会访问父类中空参数的构造函数。因为子类中每一个构造函数的第一行都有一个隐式super();当父类中没有空参数的构造函数时,子类必须手动通过super语句形式来指定要访问的父类中的构造函数;当然,子类的构造函数第一行也可以手动指定this语句来访问本类中的构造函数,子类中至少会有一个构造函数会访问父类中的构造函数。
继承的注意事项
- 子类不能从父类继承私有成员(成员方法和成员变量),但是子类的对象是包括子类所不能从父类中继承的私有成员的。其实这也体现了继承的另一个弊端:打破了封装性;
- 子类不能继承父类的构造方法,但是可以通过super关键字去访问父类构造方法;
- 千万不要为了获取其他类的功能,简化代码而继承,必须是类与类之间有所属关系才可以继承,所属关系是:is a。
什么时候定义继承呢?
当事物之间存在所属(is a)关系时,可以通过继承来体现这个关系。例,xxx是yyy的一种,用代码体现就是:xxx extends yyy。
类与类之间除了有继承关系外,还有聚集(聚合、组合)的关系,所属关系是:has a。
- 聚合:(举例)球员与球队的关系;
- 组合:事物的联系程度更紧密,(举例)人的心脏和手。
final关键字
final:最终,作为一个修饰符。
- 可以修饰类、函数、变量;
- 被final修饰的类不可以被继承。为了避免被继承,被子类覆写功能;
- 被final修饰的方法不可以被覆写;
- 被final修饰的变量是一个常量,只能被赋值一次,而且后面需要跟一个值,不然编译失败!既可以修饰成员变量,又可以修饰局部变量。当在描述事物时,一些数据的出现,值是固定的,那么这时为了增强阅读性,都给这些值起个名字,方便于阅读,而这个值不需要改变,所以用final修饰,作为常量,常量的书写规范,所有字母都大写,如果由多个单词组成,单词间通过_连接;
内部类定义在类中的局部位置上时,只能访问该局部被fianl修饰的局部变量。
class Demo { final int x = 3; // 全局常量 public static final double MY_PI = 3.14; // 相当于加了一个锁 final void show1() { // 被final修饰的方法不可以被覆写 } void show2() { final int y = 4; // y = 4; // 被final修饰的变量是一个常量,只能被赋值一次,之后不允许被修改 System.out.println(3.14); } } class SubDemo extends Demo { // void show1() { // } }