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 .

相关文章
|
4月前
|
Java 程序员
Java中的继承和多态:理解面向对象编程的核心概念
【8月更文挑战第22天】在Java的世界中,继承和多态不仅仅是编程技巧,它们是构建可维护、可扩展软件架构的基石。通过本文,我们将深入探讨这两个概念,并揭示它们如何共同作用于面向对象编程(OOP)的实践之中。你将了解继承如何简化代码重用,以及多态如何为程序提供灵活性和扩展性。让我们启程,探索Java语言中这些强大特性的秘密。
|
27天前
|
Java
在Java中,接口之间可以继承吗?
接口继承是一种重要的机制,它允许一个接口从另一个或多个接口继承方法和常量。
77 1
|
2月前
|
存储 缓存 Java
java基础:IO流 理论与代码示例(详解、idea设置统一utf-8编码问题)
这篇文章详细介绍了Java中的IO流,包括字符与字节的概念、编码格式、File类的使用、IO流的分类和原理,以及通过代码示例展示了各种流的应用,如节点流、处理流、缓存流、转换流、对象流和随机访问文件流。同时,还探讨了IDEA中设置项目编码格式的方法,以及如何处理序列化和反序列化问题。
89 1
java基础:IO流 理论与代码示例(详解、idea设置统一utf-8编码问题)
|
2月前
|
Java 开发者
在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口
【10月更文挑战第20天】在Java多线程编程中,创建线程的方法有两种:继承Thread类和实现Runnable接口。本文揭示了这两种方式的微妙差异和潜在陷阱,帮助你更好地理解和选择适合项目需求的线程创建方式。
27 3
|
2月前
|
Java
在Java多线程编程中,实现Runnable接口通常优于继承Thread类
【10月更文挑战第20天】在Java多线程编程中,实现Runnable接口通常优于继承Thread类。原因包括:1) Java只支持单继承,实现接口不受此限制;2) Runnable接口便于代码复用和线程池管理;3) 分离任务与线程,提高灵活性。因此,实现Runnable接口是更佳选择。
51 2
|
2月前
|
Java
Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口
【10月更文挑战第20天】《JAVA多线程深度解析:线程的创建之路》介绍了Java中多线程编程的基本概念和创建线程的两种主要方式:继承Thread类和实现Runnable接口。文章详细讲解了每种方式的实现方法、优缺点及适用场景,帮助读者更好地理解和掌握多线程编程技术,为复杂任务的高效处理奠定基础。
38 2
|
2月前
|
Java 开发者
Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点
【10月更文挑战第20天】Java多线程初学者指南:介绍通过继承Thread类与实现Runnable接口两种方式创建线程的方法及其优缺点,重点解析为何实现Runnable接口更具灵活性、资源共享及易于管理的优势。
44 1
|
3月前
|
Java 编译器
封装,继承,多态【Java面向对象知识回顾①】
本文回顾了Java面向对象编程的三大特性:封装、继承和多态。封装通过将数据和方法结合在类中并隐藏实现细节来保护对象状态,继承允许新类扩展现有类的功能,而多态则允许对象在不同情况下表现出不同的行为,这些特性共同提高了代码的复用性、扩展性和灵活性。
封装,继承,多态【Java面向对象知识回顾①】
|
2月前
|
Java 测试技术 编译器
Java零基础-继承详解!
【10月更文挑战第4天】Java零基础教学篇,手把手实践教学!
44 2
|
2月前
|
Java 编译器
在Java中,关于final、static关键字与方法的重写和继承【易错点】
在Java中,关于final、static关键字与方法的重写和继承【易错点】
28 5
下一篇
DataWorks