开发者社区> 程序员鱼丸> 正文
阿里云
为了无法计算的价值
打开APP
阿里云APP内打开

Java基础篇:继承

简介: Java 中继承是一种机制,其中一个对象获取父对象的所有属性和行为。 它是 `OOP(面向对象的编程系统)` 的重要组成部分。 Java 继承的思想是,您可以 `创建` 基于现有类构建的新类。 从现有类继承时,可以 `复用` 父类的 `方法` 和 `字段` 。 此外,您还可以在继承后的类中添加新的方法和字段
+关注继续查看

介绍

Java 中继承是一种机制,其中一个对象获取父对象的所有属性和行为。
它是 OOP(面向对象的编程系统) 的重要组成部分。
Java 继承的思想是,您可以 创建 基于现有类构建的新类。
从现有类继承时,可以 复用 父类的 方法 和 字段 。
此外,您还可以在继承后的类中添加新的方法和字段

为什么需要继承

试想一种情况:
有一个 Aminal (动物),它的定义如下:

属性:
体重、身高、年龄  

方法:
移动

ok,当我们这个类定义好之后,现在再来定义一个 Dog(狗)的类别:

属性:
体重、身高、年龄 、毛色

方法:
移动、吃、睡、吠叫  

现在我们发现狗类和动物类中定义的属性有很多重复。
在现实中,狗是一种动物,应该拥有动物的属性及方法,然后再加上狗专属的属性和方法。
以这个例子来说,我们可以把动物当成 父类别(或称超类别super class) ,狗 继承 动物,狗是 子类别 subclass 。

子类别会拥有父类别的所有属性、方法,再加上自己定义的属性及方法,所以可以说子类别是父类别的延伸(extend)。  (tips:这句话超重要!多看个几十遍,想一想)

总结两点:

类别图

image.png

这是 UML  (统一建模语言,Unified Modeling Language) 的类别图,常用来描述类别之间的关系。
实心箭头表示继承关系,由子类别指向父类别。
图中读作 Dog  继承 Animal 。另外一种常见的说法是: Dog is a Animal. 
继承的概念用 is a 来表述。反过来说 Animal is a Dog.是不成立的,利用 is a 可以帮助思考。

继承中使用的术语

  • Class:类是具有共同属性的一组对象。它是从中创建对象的模板或蓝图。
  • Sub Class / Child Class:子类是继承其他类的类。也称为派生类,扩展类或子类。
  • Super Class/ Parent Class:父类是子类从中继承要素的类。也称为基类或父类。
  • Reusability:顾名思义,可重用性是一种机制,可在创建新类时方便您重用现有类的字段和方法。您可以使用在现有类中已经定义的相同字段和方法。

Java继承的语法

class Child extends Parent 
{  
   //methods and fields  
}  

extends 关键字表示正在新建从现有类派生的新类,extends 的意思是扩展。
在 Java 术语中,被继承的类称为 父类 或 超类 ,而新类称为 子级 或 子类 。

Java继承示例

参照 类别图 :

class Animal {
    int height;
    int weight;
    int age;

    void move() {
        System.out.println("移动 -> 移动");
    }
}
class Dog extends Animal {
    String hairColor = "金黄";
    void eat() {
    }

    void sleep() {
    }

    void bark() {
    }

    public static void main(String[] args) {
        Dog dog = new Dog();
        System.out.print("狗开始移动:");
        dog.move();
        System.out.println("狗的颜色:"+ dog.hairColor);
    }
}

输出:

狗开始移动:移动 -> 移动
狗的颜色:金黄

.

Java中的继承类型

在类的基础上,Java 中可以有三种继承类型:

  • 单继承
  • 多级继承
  • 阶级式继承

在 Java 编程中,仅通过 接口 支持 多重继承 和 混合继承 ,之后我们将学习接口。

单继承 

image.png 

class Person {
    void eat() {
        System.out.println("吃饭...");
    }
}
class Male extends Person {
    void speak() {
        System.out.println("中国话...");
    }
}

多级继承

image.png

class Person {
    void eat() {
        System.out.println("吃饭...");
    }
}
class Male extends Person {
    void speak() {
        System.out.println("雄厚声...");
    }
}
class BabyBoy extends Male {
    @Override
    void speak() {
        System.out.println("嚎嚎大哭...");
    }
}

阶级式继承

image.png

class Person {
    void eat() {
        System.out.println("吃饭...");
    }
}
class Male extends Person {
    void speak() {
        System.out.println("雄厚声...");
    }
}
class Female extends Person {
    void speak() {
        System.out.println("细细声...");
    }
}

为什么 Java 不支持混合继承

为了降低复杂性并简化语言,java 不支持多重继承。
考虑一个场景,其中 A,B 和 C 是三个类别。 C 类继承 A和B 类。
如果 A 和 B 类具有相同的方法,并且您从子类对象调用它,则将有歧义来调用 A 或 B 类的方法。
由于编译时错误比运行时错误要好,因此如果您继承 2 个类,则 Java  会呈现编译时错误。
因此,无论您使用的是相同的方法还是不同的方法,都会出现编译时错误。

class A{  
void msg(){System.out.println("Hello");}  
}  
class B{  
void msg(){System.out.println("Welcome");}  
}  
class C extends A,B{ //假设可以运行  
   
 public static void main(String args[]){  
   C obj=new C();  
   obj.msg();// 不知道将要调用哪个msg()方法  
 }  
}  

万物之父 Object

如图:

我们知道 Java 是面向对象的语言,而每个类都继承 Object。
定义类的时候,如果没有使用关键字 exyends ,Java 会自行继承 extends Object :

class Animal{
    // code
}
// 等价于下面代码中,Java会自动帮你extends Object
class Animal extends Object{
    // code
}

关键字 this、super

this

指到自己类别的成员。
class Human{
    String name;
    int age;
    Human(String str){
        this.name = str;
    }
    String getName(){
        return this.name;
    }
}

上述程式中, this.name 意思是 自己这个类别的成员name ,当然在这个情况 不写也无所谓 ,但继承关系越 复杂 的情况下这样写法可以大大增加代码的 可读性

调用构造方法 this(.); 

如果写了很多构造提供多元的建构物件方式,构造之间彼此可以互相调用

class Human{
    String name;
    int age;
    static int totalCount = 0;
    Human(){
        name = "untitled";
        age = -1;
        totalCount++;
    }
    Human(String str){  
        this();                          
        this.name = str;
    }
    Human(String str,int a){
        this(str);  
        this.age = a;
    }
    void printInfo(){
        System.out.println(name+" 年齡:"+age+" 总人数:"+totalCount);
    }
}

上述程式中, this() 表示调用无参构造方法, this(String) 表示调用带有一个字串参数的构造方法,以此类推。

这样写的好处是,各构造之间有功能扩充的效果,已经写好的程式可以被充分的再利用,要修改某个环节也比较不会出错。

tips: 

this(.) 只能放在方法体内第一行!!!

Human(String str){  // 编译错误,要把方法体内两行位置互换
    this.name = str;
    this();        // 要等构造初始化完成,才能做最后的决定
}

好,那用定义好的构造来测试一下程序:

class TestHuman {
    public static void main(String[] args) {
        Human h1 = new Human();
        h1.printInfo();
        Human h2 = new Human("铅华");
        h2.printInfo();
        Human h3 = new Human("小花", 18);
        h3.printInfo();
    }
}

执行结果:

untitled 年齡:-1 目前總人數:1
铅华 年齡:-1 目前總人數:2
小花 年齡:18 目前總人數:3

super

指到父类别,使用方法跟 this 相似,一样要放到第一行。
使用代码测试一下:

class Parent {
    int money = 100;

    public Parent(int money) {
        this.money -= money;
    }

    static class Child extends Parent {
        public Child(int money) {
            super(money);
            System.out.println("儿子花了爸爸" + money + "元钱后剩余" + this.money);
        }
    }

    public static void main(String[] args) {
        new Child(10);
    }
}

执行结果:

儿子花了爸爸10元钱后剩余90

层层初始化

举个例子:

class A{
    A(){
        System.out.println("A的构造....");
    }
}
class B extends A{
    B(){
        System.out.println("B的构造....");
    }
}
class C extends B{
    C(){
        System.out.println("C的构造....");
    }
}

新建一个 C 对象试一试:

class Test {
    public static void main(String[] args) {
        C c = new C();
    }
}

执行结果:

A的构造....
B的构造....
C的构造....

示意图:

我们要建构的是 C ,而 C 是 B 的延伸,所以要先有 B ,而 B 是 A 的延伸,所以要先有 A ,而 A 是 Object 的延伸,所以要先有 Object ,于是就从最顶端的父类别一直建构下来。
好,现在我知道需要从父类别初始化下来,但构造呢?一个类别可以定义无数个构造,他怎么知道我要用哪个构造方法来构造我的对象?到底是以什么机制来构造父类的?
嗯,回想一下,当初在定义类的时候,如果没有定义任何构造方法,Java 会帮你定义一个不带参数不做任何事的构造方法,现在同样的老招又来一次!
只要你的构造方法中 没有调用其他构造方法 ,就会在 第一行 偷偷帮你加上去一个 super();  有多偷偷呢?你连看都看不到! !但他就是存在于最后的程序代码中。

以上的程式来说,就像这样:

class A{
    A(){
        super();  // 这行不写的话,Java会帮你加上,但你看不到
        System.out.println("这里是A的构造方法");
    }
}
class B extends A{
    B(){
        super();  // 这行不写的话,Java会帮你加上,但你看不到
        System.out.println("这里是B的构造方法");
    }
}
class C extends B{
    C(){
        super();  // 这行不写的话,Java会帮你加上,但你看不到
        System.out.println("这里是C的构造方法");
    }
}

好的,现在知道他会自动帮我调用 super(); 来建构父类别,但是如果我不想用这个· 无参构造 呢?我辛苦设计那么多 构造方法 ,他只会帮我调用 无参构造 ,太惨了吧!
嗯嗯,没错就是这么惨,所以如果要调用 有参数的super(.); 你就要自己写!
观察下述代码,想想执行结果:

class ClassA {
    ClassA() {
        System.out.println("这里是A的构造方法...");
    }
}
class ClassB extends ClassA {
    ClassB() {
        System.out.println("这里是B的构造方法...");
    }

    ClassB(String str) {
        this();
        System.out.println("B:hello " + str);
    }
}
public class ClassC extends ClassB{
    ClassC() {
        this("tina");
        System.out.println("这里是C的构造方法...");
    }

    ClassC(String str) {
        super(str);
        System.out.println("C: hello "+ str);
    }

    public static void main(String[] args) {
        new ClassC();
    }
}

执行结果:

这里是A的构造方法...
这里是B的构造方法...
B:hello tina
C: hello tina
这里是C的构造方法...

如果跟你想的不一样,在重新看一下上面的描述再想想,哪里不懂可以查看原文问我。这里是重要的 继承理念 。

存取修饰符 protected

在存取修饰符的章节提过,现在刚好提到继承再拿出来讨论。
protected 是个关键字,开放的最大权限为 不同包的子类别 可以存取。
假设 Animal  与 Dog  位在不同 package ,先看 Animal 的代码:

 package A;
 public class Animal {
     public String name;  // 4个属性刚好4种权限范围都做测试
     protected int height;
     int weight;
     private int age;
     // ↓这个修饰子一定要public或protected,不然不同类别的Dog不能用他来构造对象
     public Animal(String str,int h,int w,int a){
         this.name = str;
         this.height = h;
         this.weight = w;
         this.age = a;
     }
 }

再看 Dog 的代码:

 package B;
 import A.Aminal;
 public class Dog extends Animal{
     String color;
     public Dog(String str,int h,int w,int a,String c){
         super(str,h,w,a);
         this.color = c;
     }
     public void printInfo(){
         System.out.println(name);    // OK, public 不同包也可以存取
         System.out.println(height);  // OK, protected 允许不同包的子类别存取
         System.out.println(weight);  // 编译错误,预设只有同包可以存取
         System.out.println(age);     // 编译错误,private 只有自身类中能存取
         System.out.println(color);   // OK, 当前类成员当然ok
     }
 }

重写的存取修饰符限制

上面的例子有提到过重写(override),这边再详细讨论一下,以及一些限制。

在继承中关系,父类别定义了一些方法,子类别觉得不适用的话可以 覆盖 掉父类别的方法,然后 重写 属于自己的方法。
举个例子:

class A{
    void printInfo(){
        System.out.println("hello, I am A.");
    }
}
class B extends A{
    void printInfo(){
        System.out.println("hello, I am B.");
    }
}
class C extends A{
}

测试代码:

class Test {
    public static void main(String[] args) {
        B b = new B();
        b.printInfo();
        C c = new C();
        c.printInfo();
    }
}

执行结果:

hello, I am B.
hello, I am A.

上述程式中, B 与 C 都是继承 A ,表示拥有了 A 所有的成员,但 B 重写了 printInfo() 方法,而 C 没有。所以在调用的时候, 对象B 会使用 B 类别重写的方法,而 对象C 因为 C 类没有自己定义重写,所以会使用到父类 A 所定义的 printInfo() 。
好,那来谈谈 重写 的 限制 。

要重写父类方法必须满足几个条件:

  • 父类方法不能用 final 修饰。
  • 子类重写的方法名称、回传型态、参数个数顺序需相同。
  • 子类重写的方法,其修饰符权限不能小于父类方法。

第一点,用final修饰的方法无法被重写。
这是关键字 final 修饰方法的特性,详细内容于后面讨论。

class A{
    //       (↓关键字 final)
    public final void printInfo(){
        System.out.println("hello, this is A.");
    }
}
class B extends A{
    // 编译错误  ↓ 利用final修飾的方法不能被重写。
    public void printInfo(){
        System.out.println("hello, this is B;");
    }
}

A类  的 printInfo() 方法利用 关键字 final  修饰,所以任何继承他的子类别都不能重写这个方法,否则会产生编译错误: Cannot override the final method from A .
第二点,方法名称、回传型态、参数个数必须相同。
嗯,如果不一样的话,就是自己再定义一个新方法了阿! !跟重写有什么关系 XD

class A{
    public void printInfo(){
        System.out.println("hello, this is A.");
    }
}
class B extends A{
    public void printInfo2(){
        System.out.println("hello Tina, nice to meet you <3");
    }
}

恩,就是多定义一个方法,没什么好说的,这根本不是重写。

第三点,子类方法修饰符权限不能小于父类方法。
简单来说,如果父类说这个方法是对 全世界公开(public) 的方法,你要重写就不能 占为己有(private) 。
tips: 存取修饰符的开放权限从大到小: public  -> protected  -> (no modifier)  -> private 
如果父类说此方法是 protected ,那子类重写时的修饰符必须是 public 或 protected 。
如果父类说此方法是 private ,那子类重写时的修饰符必须是 public 或 protected 或 (no modifier) 或 private 。
关键是权限的开放范围不得小于重写对象。

class A{
    // 注意修饰符是(no modifier)
    void printInfo(){
        System.out.println("hello, this is A.");
    }
}
class B extends A{
    // ↓ 编译错误,子类重写方法修饰符权限小于父类方法
    private void printInfo(){
        System.out.println("hello, this is B.");
    }
}

A类 中的 printInfo() 方法修饰子是 (no modifier) ,依据重写的开放权限规则, B类 继承了 A类 想重写 printInfo() ,重写的开放权限必须为 public 或 protected 或 ( no modifier) ,重点就是不能小于重写对象,否则会发生编译错误: Cannot reduce the visibility of the inherited method from A .

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
Java基础之005-继承
Java基础之005-继承                                        35岁学习Java 1 继承 1.1 继承的概述 1)         多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要继那个类即可。
1085 0
线程 - Java 多线程编程(上)
线程 - Java 多线程编程(上)
80 0
线程 - Java 多线程编程(下)
线程 - Java 多线程编程(下)
30 0
Java多线程那些事,对Java并发编程2w余字的总结,超详细(从入门到完全掌握)
Java多线程那些事,对Java并发编程2w余字的总结,超详细(从入门到完全掌握)
95 0
java多线程中的死锁、活锁、饥饿、无锁都是什么鬼?
死锁、活锁、饥饿是关于多线程是否活跃出现的运行阻塞障碍问题,如果线程出现了这三种情况,即线程不再活跃,不能再正常地执行下去了。
77 0
五分钟带你玩转多线程(一)java多线程基础知识简介
线程概念 进程:是一个执行中的程序,如打开网易云音乐,网易云音乐就是一个进程 线程:是进程的组成,一个进程包含多个线程,是jvm最小调度单元。如网易云音乐听歌是一个线程,评价是一个线程。
56 0
Java的并发编程中的多线程问题到底是怎么回事儿?
原创: Hollis 在我之前的一篇《再有人问你Java内存模型是什么,就把这篇文章发给他。》文章中,介绍了Java内存模型,通过这篇文章,大家应该都知道了Java内存模型的概念以及作用,这篇文章中谈到,在Java并发编程中,通常会遇到三个问题,即原子性问题、一致性问题和有序性问题。
1060 0
18
文章
2
问答
文章排行榜
最热
最新
相关电子书
更多
低代码开发师(初级)实战教程
立即下载
阿里巴巴DevOps 最佳实践手册
立即下载
冬季实战营第三期:MySQL数据库进阶实战
立即下载