目录
1.5.1 super 关键字能让子类访问父类同名成员变量及方法
Java中使用类对现实世界中实体来进行描述,类经过实例化之后的产物对象,则可以用来表示现实中的实体,但是现实世界错综复杂,事物之间可能会存在一些关联,那怎么才能在程序中体现两个类其实是有联系的呢?如狗跟猫这两类,两者肯定是不一样的物种,但是是不是有共性呢?比如都是哺乳动物,睡觉,吃饭等。如果我写一个猫类跟一个狗类,相同的属性,相同的行为岂不是两个类都得重复写,这里是不是过于冗余了呢?那能否将这些共性抽取呢?
面向对象思想中提出了 继承 的概念,专门用来进行共性抽取,实现代码复用。
继承(inheritance)机制:是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加新功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构, 体现了由简单到复杂的认知过程。继承主要解决的问题是:共性的抽取,实现代码复用。
继续拿上面猫和狗的例子:将共性进行抽取,形成 Animal 的父类,猫类和狗类继承 Animal 类的东西,并且在此基础上,增加了自己的特点。

在 Java 中如果要表示类之间的继承关系,需要借助 extends 关键字,具体如下:
修饰符 class 子类 extends 父类 {
//........
}
- class Animal{
- public String name;
- public int age;
- public double weight;
-
- public static String type = "mammal";
-
- public void sleep(){
- System.out.println(this.name+"正在睡觉");
- }
-
- public void eat(){
- System.out.println(this.name+"正在进食");
- }
-
- public void play(){
- System.out.println(this.name+"正在跟主人玩耍!");
- }
-
- public void show(){
- System.out.println(this.name +" "+ "今年" +this.age + "岁了 " + "体重 "+this.weight+"斤");
- }
- }
-
- class Cats extends Animal{
- public void mews(){
- System.out.println(this.name+" 喵喵喵~");
- }
- }
-
- class Dogs extends Animal{
- public void barks(){
- System.out.println(this.name+" 汪汪汪!");
- }
- }
- public class Text1 {
- public static void main(String[] args) {
- Cats cat = new Cats();
- cat.name = "hello kitty";
- cat.age = 2;
- cat.weight = 7;
- cat.mews();
- cat.eat();
- cat.play();
- cat.sleep();
- cat.show();
-
- Dogs dog = new Dogs();
- dog.name = "旺财";
- dog.age = 1;
- dog.weight = 8;
- dog.barks();
- dog.show();
- dog.eat();
- dog.play();
- dog.sleep();
- }
- }
注意:
1. 子类会将父类中的成员变量或者成员方法继承到子类中了
2. 子类继承父类之后,必须要新添加自己特有的成员,体现出与基类的不同,否则就没有必要继承了
1. 子类和父类不存在同名成员变量时:
- class Base{
- public int a;
- public int b;
- }
-
- class Derived extends Base{
- public int c;
- public void method(){
- a = 10; //访问从父类继承下来的a
- b = 20; //访问从父类继承下来的b
- c = 30; //访问子类自己的c
- }
- }
2. 子类和父类存在成员变量同名时:
- class Base{
- public int a;
- public int b;
- public char c;
- public int p;
- }
- class Derived extends Base{
- public int a;
- public char b;
- public int c;
- public int d;
-
- public void method(){
- a = 10;
- super.a = 100;
- b = 'A';
- super.b =200;
- c = 30;
- super.c = 'W';
- d = 666;
- //f = 777;
- p = 999;
- }
- }

总结:在子类方法中或者通过子类对象访问成员时
1. 如果访问的成员变量子类中有,优先访问自己的成员变量,像上述代码中的 a、b、c、d,其中 d 是只要子类有
2. 如果访问的成员变量子类中无,则访问父类继承下来的,如果父类也没有定义,则编译报错,像上述代码中的 p 、f
3. 如果访问的成员变量与父类中成员变量同名,则优先访问自己的,像上述代码中的 a、b、c,但如果想访问父类的,则需要super.变量名来进行访问
成员变量访问遵循就近原则,自己有优先自己的,如果没有则向父类中找。
1. 成员方法名字不同时:
- class Base{
- public void methodA(){
- System.out.println("A方法");
- }
- }
-
- class Derived extends Base{
-
- public void methodB(){
- System.out.println("B方法");
- }
-
- public void methodC(){
- methodA();
- methodB();
- }
- }
- public class Text {
- public static void main(String[] args) {
- Derived derived = new Derived();
- derived.methodA();
- derived.methodC();
- }
- }

成员方法没有同名时的总结:在子类方法中或者通过子类对象访问方法时,则优先访问自己的,自己没有时再到父类中找,如果父类中也没有则报错。
2. 父类子类成员方法名字相同时:
- class Base{
- public void methodA(){
- System.out.println("A方法");
- }
- public void methodB(){
- System.out.println("B方法");
- }
- }
- class Derived extends Base{
- public void methodA(int a){
- System.out.println("A方法"+ a);
- }
- public void methodB(){
- System.out.println("B方法");
- }
- public void methodC(){
- methodA(); //无参,调用的是父类中的methodA
- methodA(10); //有参,调用的是子类中的methodA
- methodB(); /// 直接访问,则永远访问到的都是子类中的methodB()
- }
- }
通过派生类对象访问父类与子类同名方法时的总结:
1. 如果父类和子类同名方法的参数列表不同(也能构成重载),根据调用方法适传递的参数选择合适的方法访问,如果没有则报错;
2. 直接访问,则一定是访问子类中的,而不是父类
由于设计不好,或者因场景需要,子类和父类中可能会存在相同名称的成员,如果要在子类方法中访问父类同名成员时,该如何操作?直接访问是无法做到的,Java 提供了 super 关键字,该关键字主要作用:在子类方法中访问父类的成员。
所以对于上一个代码,Derived 子类中,想要访问父类中的 methodB,在 methodC 中,应该这样子写:
super.methodB();
总结:
1. 在子类方法中,如果想要明确访问父类中成员变量和方法时,借助 super 关键字即可。
2. super 只能在非静态方法中使用
3. 如果是多层继承,super 只能访问上一级的父类,或者是直接父类
学了继承,有了父类和子类的概念,很容易想到父类的构造方法长下面代码那样:
- class Base{
- public int a;
- public int b;
-
- public Base(int a, int b) {
- this.a = a;
- this.b = b;
- }
- }
-
- class Derived extends Base{
- public int c;
- public void method(){
- a = 10;
- b = 20;
- c = 30;
- }
- }
结果-?编译直接报错!!!!那么父类和子类的构造方法到底该如何表示呢?
子类对象中成员是有两部分组成的,基类继承下来的以及子类新增加的部分 。父子父子肯定是先有父再有子,所以在构造子类对象时候 ,先要调用基类的构造方法,将从基类继承下来的成员构造完整,即先初始化父类的成员变量,然后再调用子类自己的构造方法,将子类自己新增加的成员初始化完整 。
父类的构造方法定义情况有如下情况:
1. 若父类显式定义无参或者默认的构造方法,在子类构造方法第一行默认有隐含的 super() 调用(用户写与不写都可以),即调用基类构造方法
2. 如果父类构造方法是带有参数的,此时需要用户为子类显式定义构造方法,并在子类构造方法中选择合适的父类构造方法调用,否则编译失败。
3. 如果父类构造方法既有无参的构造方法,又有带参的构造方法,那么子类构造方法就看用户需要写
- class Base{
- public int a;
- public int b;
-
- public Base(int a, int b) {
- this.a = a;
- this.b = b;
- }
-
- //表示无参或者默认的构造函数
- public Base() {
-
- }
- }
-
- class Derived extends Base{
- public int c;
- public void method(){
- a = 10;
- b = 20;
- c = 30;
- }
-
- public Derived(int c){
- super(10,30);
- this.c = c;
-
- }
-
- public Derived(int a,int b,int c){
- super(a,b); //使用super(...)调用父类的构造方法进行初始化
- this.c = c; //对子类成员变量进行初始化
- }
-
- public Derived(){
- super();
- }
-
- public Derived(int a,int b ){
- super();
- }
- }
注意:
1. 在子类构造方法中,super(...) 调用父类构造时,必须是子类构造函数中第一条语句。
2. super(...) 只能在子类构造方法中出现一次,并且不能和 this 同时出现
请问如下代码输出何种结果?
- class Animal{
- static{
- System.out.println("AnimalStatic");
- }
-
- {
- System.out.println("Animal{}");
- }
-
- public Animal() {
- System.out.println("Animal()");
- }
- }
-
- class Cat extends Animal{
- static {
- System.out.println("CatStatic");
- }
-
- {
- System.out.println("Cat{}");
- }
-
- public Cat(){
- super();
- System.out.println("Cat()");
-
- }
- }
-
- public class Text {
- public static void main(String[] args) {
- Cat cat = new Cat();
- }
- }
输出:
AnimalStatic
CatStatic
Animal{}
Animal()
Cat{}
Cat()
前面的学习我们知道,只要类被加载,就会执行静态代码块,所以在程序开始,加载了 Animal 类及 Cat 类。而非静态代码块其实会与构造方法合并,执行子类构造方法的第一步,是执行父类构造方法。那么我们可以有以下总结:
1. 父类静态代码块优先于子类静态代码块执行,且是最早执行
2. 父类实例代码块和父类构造方法紧接着执行
3. 子类的实例代码块和子类构造方法紧接着再执行
4. 第二次实例化子类对象时,父类和子类的静态代码块都将不会再执行
super 和 this 都可以在成员方法中用来访问:成员变量和调用其他的成员函数,都可以作为构造方法的第一条语句,那他们之间有什么区别呢?
相同点:
1. 都是 Java 中的关键字
2. 只能在类的非静态方法中使用,用来访问非静态成员方法和字段
3. 在构造方法中调用时,必须是构造方法中的第一条语句,并且不能同时存在
不同点:
1. this 是当前对象的引用,当前对象即调用实例方法的对象,super 相当于是子类对象中从父类继承下来部分成员的引用
2. 在非静态成员方法中,this 用来访问本类的方法和属性,super 用来访问父类继承下来的方法和属性
3. 在构造方法中:this(...) 用于调用本类构造方法,super(...) 用于调用父类构造方法,两种调用不能同时在构造 方法中出现
4. 构造方法中一定会存在 super(...) 的调用,用户没有写编译器也会增加,但是 this(...) 用户不写则不存在
在类和对象章节中,为了实现封装特性,Java 中引入了访问限定符:
| 范围 | private | default | protected | public | |
| 1 | 同一包中同一类 | ✔ | ✔ | ✔ | ✔ |
| 2 | 同一包中不同类 | ✔ | ✔ | ✔ | |
| 3 | 不同包中的子类 | ✔ | ✔ | ||
| 4 | 不同包中的非子类 | ✔ |
在这一小节中,将探讨 protected 的使用方式
同一个包中同一类:
- public class Text {
- protected int a = 100;
- public static void main(String[] args) {
- Text text = new Text();
- System.out.println(text.a);
- }
- }
同一包中不同类:
- class Text1 {
- public static void main(String[] args) {
- Text text = new Text();
- System.out.println(text.a+10);
- }
- } //类Text1 调用了 类Text 中 protected 修饰的成员变量a
-
- public class Text {
- protected int a = 1000;
- public static void main(String[] args) {
- Text text = new Text();
- System.out.println(text.a);
- }
- }
不同包中的子类:
- //包 demo1
- package demo1;
- import demo2.TextDemo2;
-
- public class TextDemo1 extends TextDemo2{
- void fun(){
- System.out.println(super.a);
- super.bbKing();
- }
- }
-
- //包 demo2
- package demo2;
-
- public class TextDemo2 {
- protected int a = 666;
- protected void bbKing(){
- System.out.println("BBBBBBB");
- }
- }
那父类中不同访问权限的成员,在子类中的可见性又是什么样子的呢?
- // 为了掩饰基类中不同访问权限在子类中的可见性,为了简单类B中就不设置成员方法了
- // extend01包中
- public class B {
- private int a;
- protected int b;
- public int c;
- int d;
- }
- // extend01包中
- // 同一个包中的子类
- public class D extends B{
- public void method(){
- // super.a = 10; // 编译报错,父类private成员在相同包子类中不可见
- super.b = 20; // 父类中protected成员在相同包子类中可以直接访问
- super.c = 30; // 父类中public成员在相同包子类中可以直接访问
- super.d = 40; // 父类中默认访问权限修饰的成员在相同包子类中可以直接访问
- }
- }
- // extend02包中
- // 不同包中的子类
- public class C extends B {
- public void method(){
- // super.a = 10; // 编译报错,父类中private成员在不同包子类中不可见
- super.b = 20; // 父类中protected修饰的成员在不同包子类中可以直接访问
- super.c = 30; // 父类中public修饰的成员在不同包子类中可以直接访问
- //super.d = 40; // 父类中默认访问权限修饰的成员在不同包子类中不能直接访问
- }
- }
- // extend02包中
- // 不同包中的类
- public class TestC {
- public static void main(String[] args) {
- C c = new C();
- c.method();
- // System.out.println(c.a); // 编译报错,父类中private成员在不同包其他类中不可见
- // System.out.println(c.b); // 父类中protected成员在不同包其他类中不能直接访问
- System.out.println(c.c); // 父类中public成员在不同包其他类中可以直接访问
- // System.out.println(c.d); // 父类中默认访问权限修饰的成员在不同包其他类中不能直接访问
- }
- }
注意:父类中 private 成员变量虽然在子类中不能直接访问,但是也继承到子类中
什么时候下用哪一种修饰符呢?
我们希望类要尽量做到 "封装", 即隐藏内部实现细节, 只暴露出必要的信息给类的调用者。因此我们在使用的时候应该尽可能的使用比较严格的访问权限。例如如果一个方法能用 private , 就尽量不要 用 public。希望同学们能写代码的时候认真思考, 该类提供的字段方法到底给 "谁" 使用(是类内 部自己用, 还是类的调用者使用, 还是子类使用)。
Java中支持以下的继承方式:
1. 单继承:

public class A{
......
}
class B extends A{
......
}
2. 多层继承

public class A{
......
}
class B extends A{
......
}
class C extends B{
......
}
3. 不同类继承同一个类

public class A{
......
}
class B extends A{
......
}
class C extends A{
......
}
Java中不支持多继承

时刻牢记, 我们写的类是现实事物的抽象。而我们真正在公司中所遇到的项目往往业务比较复杂, 可能会涉及到 一系列复杂的概念, 都需要我们使用代码来表示, 所以我们真实项目中所写的类也会有很多。类之间的关系也会 更加复杂。但是即使如此, 我们并不希望类之间的继承层次太复杂。一般我们不希望出现超过三层的继承关系。如果继承层次太多, 就需要考虑对代码进行重构。
如果想从语法上进行限制继承, 就可以使用 final 关键字
final 关键字可以用来修饰变量、成员方法以及类。
1. 修饰变量或字段,表示常量(即不能修改)
- final int A = 20;
- System.out.println(A);
- A = 666; //编译报错!
此时的变量名要大写
那如果 final 修饰数组呢?
- final int[] arr = {1,2,3,4,5};
- arr[0] = 666;
- System.out.println(Arrays.toString(arr));
- arr = new int [10]; //编译报错!

要知道 new 一个,是为 arr 开辟一个新的空间,那么这样 arr 里面存放的地址就会改变,而 arr 已经被 final 修饰了,所以 final 修饰数组就意味着,arr 存放的地址不能更改!
2. 修饰类:表示此类不能被继承
final public class Animal{
......
}
//编译报错!!
public class Bird extends Animal{
......
}
我们平时是用的 String 字符串类, 就是用 final 修饰的, 不能被继承
3. 修饰方法:表示该方法不能被重写(后序介绍)
和继承类似, 组合也是一种表达类之间关系的方式, 也是能够达到代码重用的效果。组合并没有涉及到特殊的语法 (诸如 extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段。
继承表示对象之间是 is-a 的关系,比如:狗是动物,猫是动物
组合表示对象之间是 has-a 的关系,比如:汽车
汽车和其轮胎、发动机、方向盘、车载系统等的关系就应该是组合,因为汽车是有这些部件组成的。
// 轮胎类
class Tire{
// ...
}
// 发动机类
class Engine{
// ...
}
// 车载系统类
class VehicleSystem{
// ...
}
class Car{
private Tire tire; // 可以复用轮胎中的属性和方法
private Engine engine; // 可以复用发动机中的属性和方法
private VehicleSystem vs; // 可以复用车载系统中的属性和方法
// ...
}
// 奔驰是汽车
class Benz extend Car{
// 将汽车中包含的:轮胎、发送机、车载系统全部继承下来
}
请问如果 Car car = new Car(); 内存情况是如何的呢?

组合和继承都可以实现代码复用,应该使用继承还是组合,需要根据应用场景来选择,一般建议:能用组合尽量用组合。