目录
1.5.1 super 关键字能让子类访问父类同名成员变量及方法
1. 继承
1.1 为什么要有继承
Java中使用类对现实世界中实体来进行描述,类经过实例化之后的产物对象,则可以用来表示现实中的实体,但是现实世界错综复杂,事物之间可能会存在一些关联,那怎么才能在程序中体现两个类其实是有联系的呢?如狗跟猫这两类,两者肯定是不一样的物种,但是是不是有共性呢?比如都是哺乳动物,睡觉,吃饭等。如果我写一个猫类跟一个狗类,相同的属性,相同的行为岂不是两个类都得重复写,这里是不是过于冗余了呢?那能否将这些共性抽取呢?
面向对象思想中提出了 继承 的概念,专门用来进行共性抽取,实现代码复用。
1.2 何为继承
继承(inheritance)机制:是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加新功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构, 体现了由简单到复杂的认知过程。继承主要解决的问题是:共性的抽取,实现代码复用。
继续拿上面猫和狗的例子:将共性进行抽取,形成 Animal 的父类,猫类和狗类继承 Animal 类的东西,并且在此基础上,增加了自己的特点。
1.3 继承的语法
在 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.4 父类成员的访问
1.4.1 子类中访问父类的成员变量
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.4.2 子类中访问父类的成员方法
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. 直接访问,则一定是访问子类中的,而不是父类
1.5 super 关键字
1.5.1 super 关键字能让子类访问父类同名成员变量及方法
由于设计不好,或者因场景需要,子类和父类中可能会存在相同名称的成员,如果要在子类方法中访问父类同名成员时,该如何操作?直接访问是无法做到的,Java 提供了 super 关键字,该关键字主要作用:在子类方法中访问父类的成员。
所以对于上一个代码,Derived 子类中,想要访问父类中的 methodB,在 methodC 中,应该这样子写:
super.methodB();
总结:
1. 在子类方法中,如果想要明确访问父类中成员变量和方法时,借助 super 关键字即可。
2. super 只能在非静态方法中使用
3. 如果是多层继承,super 只能访问上一级的父类,或者是直接父类
1.5.2 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. 第二次实例化子类对象时,父类和子类的静态代码块都将不会再执行
1.6 this 关键字与 super 关键字的区别
super 和 this 都可以在成员方法中用来访问:成员变量和调用其他的成员函数,都可以作为构造方法的第一条语句,那他们之间有什么区别呢?
相同点:
1. 都是 Java 中的关键字
2. 只能在类的非静态方法中使用,用来访问非静态成员方法和字段
3. 在构造方法中调用时,必须是构造方法中的第一条语句,并且不能同时存在
不同点:
1. this 是当前对象的引用,当前对象即调用实例方法的对象,super 相当于是子类对象中从父类继承下来部分成员的引用
2. 在非静态成员方法中,this 用来访问本类的方法和属性,super 用来访问父类继承下来的方法和属性
3. 在构造方法中:this(...) 用于调用本类构造方法,super(...) 用于调用父类构造方法,两种调用不能同时在构造 方法中出现
4. 构造方法中一定会存在 super(...) 的调用,用户没有写编译器也会增加,但是 this(...) 用户不写则不存在
1.7 protected 关键字
在类和对象章节中,为了实现封装特性,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。希望同学们能写代码的时候认真思考, 该类提供的字段方法到底给 "谁" 使用(是类内 部自己用, 还是类的调用者使用, 还是子类使用)。
1.8 继承的方式
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 关键字
1.9 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. 修饰方法:表示该方法不能被重写(后序介绍)
2. 组合
和继承类似, 组合也是一种表达类之间关系的方式, 也是能够达到代码重用的效果。组合并没有涉及到特殊的语法 (诸如 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(); 内存情况是如何的呢?
组合和继承都可以实现代码复用,应该使用继承还是组合,需要根据应用场景来选择,一般建议:能用组合尽量用组合。