当前位置:首页 » 《休闲阅读》 » 正文

Java之继承和多态

8 人参与  2024年04月23日 13:25  分类 : 《休闲阅读》  评论

点击全文阅读


继承

一 、继承相关基础

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 { // 将汽车中包含的:轮胎、发送机、车载系统全部继承下来 } 组合和继承都可以实现代码复用,应该使用继承还是组合,需要根据应用场景来选择,一般建议: 能用组合尽量用组合。

多态

待更新


点击全文阅读


本文链接:http://zhangshiyu.com/post/99072.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1