Java之继承

一 、继承相关基础

1. 为什么需要继承

先看以下代码

// Dog.java
public class Dog {
string name ;
int age ;
float weight ;
public void eat (){
System . out . println ( name + " 正在吃饭 " );
}
public void sleep (){
System . out . println ( name + " 正在睡觉 " );
}
void Bark (){
System . out . println ( name + " 汪汪汪 ~~~" );
}
}
// Cat.Java
public class Cat {
string name ;
int age ;
float weight ;
public void eat (){
System . out . println ( name + " 正在吃饭 " );
}
public void sleep ()
{
System . out . println ( name + " 正在睡觉 " );
}
void mew (){
System . out . println ( name + " 喵喵喵 ~~~" );
}
}
以上代码中 通过观察上述代码会发现,猫和狗的类中存在大量重复,那能否将这些共性抽取呢?面向对象思想中提出了继承的概念,专门用来进行共性抽取,实现代码复用。

2.继承概念

继承 (inheritance) 机制 :是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加新功能 ,这样产生新的类,称 派生类 。继承呈现了面向对象程序设计的层次结构, 体现了 由简单到复杂的认知过程。继承主要解决的问题是:共性的抽取,实现代码复用(即通用代码重复使用)
例如:狗和猫都是动物,那么我们就可以将共性的内容进行抽取,然后采用继承的思想达到共用。
上述代码中, Dog Cat 都继承了 Animal 类,其中: Animal 类称为父类 / 基类或超类, Dog Cat 可以称为 Animal 的 子类/ 派生类,继承之后,子类可以复用父类中成员,子类在实现时只需关心自己新增加的成员即可。
从继承概念中可以看出继承最大的作用就是:实现代码复用,还有就是来实现多态 ( 后面讲)。

3. 继承的语法

Java 中如果要表示类之间的继承关系,需要借助 extends 关键字
修饰符 class 子类 extends 父类 {
// ...
}
见以下代码
// Animal.java
public class Animal {
String name ;
int age ;
public void eat (){
System . out . println ( name + " 正在吃饭 " );
}
public void sleep (){
System . out . println ( name + " 正在睡觉 " );
}
}
// Dog.java
public class Dog extends Animal {
void bark (){
System . out . println ( name + " 汪汪汪 ~~~" );
}
}

// Cat.Java
public class Cat extends Animal {
void mew (){
System . out . println ( name + " 喵喵喵 ~~~" );
}
}

4. 父类成员访问

在继承体系中,子类将父类中的方法和字段继承下来了,那在子类中能否直接访问父类中继承下来的成员呢?

4.1 子类中访问父类的成员变量
public class Base {
int a ;
int b ;
}
public class Derived extends Base {
int c ;
public void method (){
a = 10 ; // 访问从父类中继承下来的 a
b = 20 ; // 访问从父类中继承下来的 b
c = 30 ; // 访问子类自己的 c
}
}
4.2子类和父类成员变量同名
 class Base {
int a ;
char  b ;
int c ;
}
public class Derived extends Base {
int a ; // 与父类中成员 a 同名,且类型相同
int    b ; // 与父类中成员 b 同名,但类型不同
public void method (){
a = 100 ; // 访问父类继承的 a ,还是子类自己新增的 a
b='e';      //char类型只有父类中有,故访问父类的b
c = 102 ; // 子类没有 c ,访问的肯定是从父类继承下来的 c
// d = 103; // 编译失败,因为父类和子类都没有定义成员变量 b
}
}
在子类方法中 或者 通过子类对象访问成员时
先根据类型判断访问谁的变量。
如果访问的成员变量子类中有,优先访问自己的成员变量。
如果访问的成员变量子类中无,则访问父类继承下来的,如果父类也没有定义,则编译报错。
如果访问的成员变量与父类中成员变量同名,则优先访问自己的。
成员变量访问遵循就近原则,自己有优先自己的,如果没有则向父类中找
4.3 子类中访问父类的成员方法

同变量一样,只是当父类与子类发生方法重写(参数也一样)时访问子类,发生方法重载时根据参数判断访问谁。当父类与子类都没有该方法时,编译器报错。

二、super关键字

1.为什么要使用super关键字

如果子类中存在与父类中相同名字的成员时,这是该怎么办呢?使用 super 在子类中访问父类的变量和方法。

class Base {

int a;

int b;

public void methodB(){

System.out.println("Base中的methodB()");

}

}

public class Derived extends Base{

int a;

int b;

public void methodB() {

super.a = 200;

super.b = 201;

System.out.println("Derived中的methodB()方法");

System.out.println(a);

System.out.println(b); //子类的b

System.out.println(super.a);

System.out.println(super.b); //调用父类的b要用super

super.methodB(); //调用父类的方法要写super 

methodB();这里调用自己,形成死递归

}

}

class c {

public static void main(String[] args) {

Derived s=new Derived();

s.a=2;

s.b=5;

s.methodB();

}

}

屏幕输出结果

Derived中的methodB()方法
2
5
200
201
Base中的methodB()

以上代码我们发现一个问题,为什么不直接将主方法写在methodB这个类里面呢,这样因为父子的关系可以直接引用变量及方法,不用去实例化对象了。见以下代码

class Base {
int a;
int b;
public void methodB(){
System.out.println("Base中的methodB()");
}
}
public class Derived extends Base{
int a;
int b;
public void methodB() {
public static void main(String[] args) {
super.a = 200;
super.b = 201;
a=2;
b=5;
System.out.println("Derived中的methodB()方法");
System.out.println(a);
System.out.println(b); //子类的b
System.out.println(super.a);
System.out.println(super.b); //调用父类的b要用super
super.methodB(); //调用父类的方法要写super 
methodB();这里调用自己,形成死递归
}
}

上面这个代码编译器会报错,这是因为super是不能用在静态代码块中( 因为子类继承到父类的东西也属于对象),而 super可以看成父类的对象名 (与this看成对象名一样),正好main方法是一个静态方法(属于类,不属于对象),故只能间接访问。
总结:
1.在子类方法中,如果想要明确访问父类中成员时,借助super关键字即可
2.super只能在非静态方法中使用(因为子类继承到父类的东西也属于对象)
super 的其他用法在后文中介绍。

2.子类构造方法

父子父子,先有父再有子,即:子类对象构造时,需要先调用基类构造方法,然后执行子类的构造方法。

public class Base {
public Base (){
System . out . println ( "Base()" );
}
}
public class Derived extends Base {
public Derived (){
// super(); // 注意子类构造方法中默认会调用基类的无参构造方法: super(),
// 用户没有写时 , 编译器会自动添加,而且 super() 必须是子类构造方法中第一条语句(类比this在构造方法的作用)
System . out . println ( "Derived()" );
}
}
public class Test {
public static void main ( String [] args ) {
Derived d = new Derived ();
}
}
结果打印:
Base ()
Derived ()
从以上代码可以看出:先执行基类的构造方法,然后执行子类的构造方
注意:
1. 若父类显式定义无参的构造方法,在子类构造方法第一行默认有隐含的 super() 调用,即调用基类构造方法
2. 如果父类构造方法是带有参数的,此时需要用户为子类显式定义构造方法,并在子类构造方法中选择合适的父类构造方法调用(参数要一致),否则编译失败。
3. 在子类构造方法中, super(...) 调用父类构造时,必须是子类构造函数中第一条语句
4. super(...) 只能在子类构造方法中出现一次,并且不能和 this 同时出现(因为都要第一条)

3.superthis的对比

super this 都可以在成员方法中用来访问:成员变量和调用其他的成员函数,都可以作为构造方法的第一条语句,那他们之间有什么区别呢?
相同点
1. 都是 Java 中的关键字
2. 只能在类的非静态方法中使用,用来访问非静态成员方法和字段
3. 在构造方法中调用时,必须是构造方法中的第一条语句,并且不能同时存在
不同点
1. this 是当前对象的引用, super是父类对象的引用。this(...) 用于调用本类构造方法, super(...) 用于调用父类构造方法,两种调用不能同时在构造方法中使用。
class Base {
int a;
public Base(){
System.out.println("Base()");
}
}
public class Derived extends Base{
int a;
public Derived(){
System.out.println("Derived()");
}
this.a=1;  //看成Derived.a=1
super.a=2;  //看成Base.a=2
this();   //调用子类构造方法看成Derived()
super()  //调用父类构造方法看成Base()
}
//该代码没有主方法,不能运行,只是为了演示方便。
2. 构造方法中一定会存在 super(...) 的调用,用户 没有写编译器也会增加 ,但是 this(...) 用户不写则没有。

4. 再谈初始化

我们还记得之前讲过的代码块吗?我们简单回顾一下几个重要的代码块:实例代码块和静态代码块。在没有继承关系时的执行顺序。
1. 静态代码块先执行,并且只执行一次,在类加载阶段执行
2. 当有对象创建时,才会执行实例代码块,实例代码块执行完成后,最后构造方法执行
那么问题来了, 继承关系上的执行顺序是怎样的呢?看下面代码
class Person {
public String name ;
public int age ;
public Person ( String name , int age ) {
this . name = name ;
this . age = age ;
System . out . println ( "Person :构造方法执行 " );
}
{
System . out . println ( "Person :实例代码块执行 " );
}
static {
System . out . println ( "Person :静态代码块执行 " );
}
}
class Student extends Person {
public Student ( String name , int age ) {
super ( name , age );
System . out . println ( "Student :构造方法执行 " );
}
{
System . out . println ( "Student :实例代码块执行 " );
}
static {
System . out . println ( "Student :静态代码块执行 " );
}
}
public class TestDemo4 {
public static void main ( String [] args ) {
Student student1 = new Student ( " 张三 " , 19 );
System . out . println ( "===========================" );
Student student2 = new Student ( "gaobo" , 20 );
}
public static void main1 ( String [] args ) {
Person person1 = new Person ( "bit" , 10 );
System . out . println ( "============================" );
Person person2 = new Person ( "gaobo" , 20 );
}
//执行结果

Person :静态代码块执行
Student :静态代码块执行
Person :实例代码块执行
Person :构造方法执行
Student :实例代码块执行
Student :构造方法执行
===========================
Person :实例代码块执行
Person :构造方法执行
Student :实例代码块执行
Student :构造方法执行
通过分析执行结果,得出以下结论:
1 、父类静态代码块优先于子类静态代码块执行,且是最早执行
2 、父类实例代码块和父类构造方法紧接着执行
3 、子类的实例代码块和子类构造方法紧接着再执行
4 、第二次实例化子类对象时,父类和子类的静态代码块都将不会再执行
如果涉及了变量初始化怎么办呢?

首先说下类的加载(在生成class文件创建类时就执行了)会导致静态成员变量先初始化(父类的静态变量先初始化,子类的静态变量后初始化),而后是静态代码块初始化(父类的静态代码块先执行,子类的静态代码块再执行),因为类的加载只有一次,所以它们只能执行一次。然后就按上面的顺序。注意如果没有静态,则先父类先把构造方法执行完后再初始化实例成员变量。

三、protected关键字

在类和对象章节中,为了实现封装特性, Java 中引入了访问限定符,主要限定:类或者类中成员能否在类外或者其他包中被访问。
如果有时我们想要一个变量能在一个包之外去使用,但又不会像public范围那么大,这时就出现了protected这个修饰符。 允许变量还能在另一个包的所属子类中去使用。
我们先看在同一个包的情况(同一个包的情况下基本不会报错)

public class Date3 {

    protected int a=30;

}

class Date2 extends Date3{

    public void a(){

        System.out.println(super.a);

        System.out.println(a);

//以下的部分复杂化了,在继承关系中我们可以直接使用a,这里运行没有问题

Date3 date3 = new Date3();

        System.out.println(date3.a);

    }

    public static void main(String[] args) {

        Date2 date2  =new Date2();

        Date3 date3  =new Date3();

        System.out.println(date2.a);

        System.out.println(date3.a);

        date2.a();

    }

}

我们看下不同包的情况

package cot;

public class Date1 {

    protected int a=30;

}

package com;

import cot.Date1;

class Date2 extends Date1 {

    public void a(){

        System.out.println(super.a);

        System.out.println(a);

//以下代码会报错

Date1 date1 = new Date1();

        System.out.println(date1.a);

    }

    public static void main(String[] args) {

        Date2 date2  =new Date2();

        Date1 date1  =new Date1();

        System.out.println(date2.a);

       System.out.println(date1.a); //这里会报错:a 在 cot.Date1 中是 protected 访问控制

       date2.a();

    }

}

观察上面的代码,protected要求在不同包的子类中可以访问,但是上面的报错它都在子类中啊, 这里记住:protected修饰的变量或方法在不同包子类中只能通过super访问,而super不能出现在主方法中,所以只能通过方法间接访问。 见以下代码。

package cot;

public class Date1 {

    protected int a=30;

}

package com;

import cot.Date1;

class Date2 extends Date1 {

    public void a(){

        System.out.println(super.a);

    }

    public static void main(String[] args) {

        Date2 date2  =new Date2();

       // Date1 date1  =new Date1();

       // System.out.println(date2.a);

        //System.out.println(date1.a);

        date2.a();

    }

}

//输出结果:30

注意:父类中private成员变量虽然在子类中不能直接访问,但是也继承到子类中去了。 

四、final关键字

final 关键可以用来修饰变量、成员方法以及类。
1. 修饰变量或字段,表示常量 ( 即不能修改 )
final int a = 10 ;
a = 20 ; // 编译出错
2.修饰类:表示此类不能被继承
final public class Animal {
...
}
public class Bird extends Animal {
...
}
// 编译出错
Error :( 3 , 27 ) java : 无法从最终 com . bit . Animal 进行继续
3. 修饰方法:表示该方法不能被重写(后面介绍)

五、继承与组合

和继承类似 , 组合也是一种表达类之间关系的方式 , 也是能够达到代码重用的效果。组合并没有涉及到特殊的语法(诸如 extends 这样的关键字 ), 仅仅是将一个类的实例作为另外一个类的字段。
继承表示对象之间是 is-a(是a) 的关系 ,比如:狗是动物,猫是动物
组合表示对象之间是 has-a(有a)的关系,比如:汽车有轮胎、发动机、方向盘、车载系统等
汽车和其轮胎、发动机、方向盘、车载系统等的关系就应该是组合,因为汽车是有这些部件组成的。
// 轮胎类
class Tire {
// ...
}
// 发动机类
class Engine {
// ...
}
// 车载系统类
class VehicleSystem {
// ...
}
class Car {
private Tire tire ; // 可以复用轮胎中的属性和方法
private Engine engine ; // 可以复用发动机中的属性和方法
private VehicleSystem vs ; // 可以复用车载系统中的属性和方法
// ...
}
// 奔驰是汽车
class Benz extend Car {
// 将汽车中包含的:轮胎、发送机、车载系统全部继承下来
}
组合和继承都可以实现代码复用,应该使用继承还是组合,需要根据应用场景来选择,一般建议: 能用组合尽量用组合。
  • 164
    点赞
  • 104
    收藏
    觉得还不错? 一键收藏
  • 95
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 95
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值