写点什么

JAVA 面向对象 (九)-- 继承

用户头像
加百利
关注
发布于: 1 小时前

引言:java 的核心思想是面向对象,我们会将实际中客观存在的所有东西看做对象,进一步对对象的共同属性和方法抽取形成类,那么如果抽取出来的多个类有着相同的属性和方法,是否可以进一步抽取呢?就需要我们今天的继承知识点了。

一、JAVA 继承:

1.继承的概念:

继承是 java 面向对象编程技术的一块基石,因为它允许创建分等级层次的类。


继承就是子类继承父类的特征和行为,使得子类对象(实例)具有父类的实例域和方法,或子类从父类继承方法,使得子类具有父类相同的行为。


我们用实际生活中的例子解释:



兔子和羊属于食草动物类,狮子和豹属于食肉动物类。


食草动物和食肉动物又是属于动物类。


所以继承需要符合的关系是:is-a,父类更通用,子类更具体。


虽然食草动物和食肉动物都是属于动物,但是两者的属性和行为上有差别,所以子类会具有父类的一般特性也会具有自身的特性。

2.继承的语法:

通过 extends 这个关键字来实现继承,而且所有的类都是继承于 java.lang.Object,当一个类没有继承的关键字,则默认继承 object(这个类在 java.lang 包中,所以不需要 import)祖先类。


子类  extends 父类{  //属性和方法省略}
复制代码


如我们用上述例子来学习继承:


父类:


package cn.hz;
/** * @author hz * @version 1.0 * * 父类:动物类 */public class Animal { //属性和方法省略...}
复制代码


子类:(以食草动物为例,其他省略)


package cn.hz;
/** * @author hz * @version 1.0 * * 子类:食草动物,继承于动物父类 */public class Herbivore extends Animal { //属性和方法省略...}
复制代码

3.继承的使用:

当一个类 A 继承类 B 以后,可以直接通过 A 类创建该类对象,也可以通过父类的引用指向子类


子类A  extends 父类B{...}
复制代码


创建子类 A 对象方式一:


子类A 子类对象=new 子类A();
复制代码


例如上述例子,创建食草动物 a:


Herbivore a=new Herivore();
复制代码


创建子类 A 对象方式二:


父B 子类对象=new 子类A();
复制代码


例如上述例子,创建食草动物 a:


Animal a=new Herivore();
复制代码

4.继承的意义:

看完继承的概念和语法,可能有很多同学会有一个疑问?那我为什么要用继承呢?那我们就通过一个例子来具体看继承使用的好处。


还是以上述例子进行实现,开发动物类,其中动物分别为狗和企鹅,需求如下:


狗:属性(昵称,健康值,亲密度,品种),方法(打印信息,获取设置相关属性方法,构造方法)


企鹅:属性(昵称,健康值,亲密度,性别),方法(打印信息,获取设置相关属性方法,构造方法)


类图如下:



代码如下:


package cn.hz;
/** * @author hz * @version 1.0 * * 狗的类 */public class Dog { private String name; //昵称 private Integer health; //健康值 private Integer love; //亲密度 private String strain; //品种
public Dog() { }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Integer getHealth() { return health; }
public void setHealth(Integer health) { this.health = health; }
public Integer getLove() { return love; }
public void setLove(Integer love) { this.love = love; }
public String getStrain() { return strain; }
public void setStrain(String strain) { this.strain = strain; }
public void print(){ System.out.println("狗的基本信息:...省略"); }}
复制代码


package cn.hz;
/** * @author hz * @version 1.0 */public class Penguin { private String name; //昵称 private Integer health; //健康值 private Integer love; //亲密度 private String sex; //性别
public Penguin() { }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Integer getHealth() { return health; }
public void setHealth(Integer health) { this.health = health; }
public Integer getLove() { return love; }
public void setLove(Integer love) { this.love = love; }
public String getSex() { return sex; }
public void setSex(String sex) { this.sex = sex; }
public void print(){ System.out.println("企鹅信息:...省略"); }}
复制代码


通过对类图和代码进行比较分析,我们发现出现了上述写法中存在两个问题:


  • 代码存在大量冗余,狗和企鹅有着共同的属性 name,health,love 和打印方法,重复代码过多;

  • 代码扩展性不高,如果现在需要添加一个新的动物猫,需要将所有属性全部重新编写;


我们使用继承将上述代码进行优化


类图如下:



代码如下:


package cn.hz;
/** * @author hz * @version 1.0 * * 父类:动物类 */public class Animal { private String name; //昵称 private Integer health; //健康值 private Integer love; //亲密度
public Animal() { }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Integer getHealth() { return health; }
public void setHealth(Integer health) { this.health = health; }
public Integer getLove() { return love; }
public void setLove(Integer love) { this.love = love; } public void print(){ System.out.println("动物信息:...省略"); }}
复制代码


package cn.hz;
/** * @author hz * @version 1.0 * * 狗的类 */public class Dog { private String strain; //品种 public String getStrain() { return strain; } public void setStrain(String strain) { this.strain = strain; }}
复制代码


package cn.hz;
/** * @author hz * @version 1.0 */public class Penguin { private String sex; //性别
public void setSex(String sex) { this.sex = sex; }}
复制代码


经过优化,我们将狗和企鹅共同的属性进行抽取放入到父类,子类自己只编写自己特有的属性和方即可,这样代码的冗余问题得到解决,代码的扩展性也得到提高。

5.java 继承方式:

  • Java 不支持多继承,只支持单继承

  • java 支持多重继承,可以实现继承链,如上述食草动物继承动物,兔子继承食草动物


6.继承特性:

  • 子类拥有父类非 private 的属性、方法。

  • 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。

  • 子类可以用自己的方式实现父类的方法。

  • Java 的继承是单继承,但是可以多重继承,单继承就是一个子类只能继承一个父类,多重继承就是,例如 B 类继承 A 类,C 类继承 B 类,所以按照关系就是 B 类是 C 类的父类,A 类是 B 类的父类,这是 Java 继承区别于 C++ 继承的一个特性。

  • 提高了类之间的耦合性(继承的缺点,耦合度高就会造成代码之间的联系越紧密,代码独立性越差)。

二、继承综合案例:

由于面向对象阶段,概念性的东西很抽象,很多同学反馈学习的时候很明白,但是实际使用时就很蒙,所有本章节我们特意通过案例分析加深大家对继承的理解:


代码如下:B 类继承 A 类,创建 B 类的两个对象,最后执行结果是什么?为什么?


package cn.hz;
/** * @author hz * @version 1.0 */public class A { public A(){ System.out.println("A的构造方法"); } static{ System.out.println("A的静态块"); } { System.out.println("A的动态块"); }}
复制代码


package cn.hz;
/** * @author hz * @version 1.0 */public class B extends A { public B(){ System.out.println("B的构造方法"); } static{ System.out.println("B的静态块"); } { System.out.println("B的动态块"); }}
复制代码


package cn.hz;
/** * @author hz * @version 1.0 */public class TestAB { public static void main(String[] args) { //创建对象 B b=new B(); B b1=new B(); }}
复制代码


分析:


上述代码 B 继承 A,最后创建了两个 B 的对象,首先我们知道一个类创建对象需要先加载类,所有对于一个类含有静态块(对类的初始化),动态块(对象创建初始化),构造方法(对使用该构造方法创建对象初始化),他们的执行顺序是:


静态块--动态块-构造方法


而如果两个类存在继承关系,子类在创建对象时一定会调用父类对应的构造方法,而创建对象使用构造方法之前一定会先进行类的加载,所有父类静态块也会执行调用,但是一定注意创建对象时是父类动态块完成初始化然后执行构造方法以后,再通过子类动态块完成子类初始化再通过构造方法创建对象,即:


父类静态块-子类静态块--父类动态块-父类构造方法-子类动态块-子类构造方法


而结合代码我们发现创建了两个对象,这个时候需要注意了,当类加载完成以后,静态块完成初始化后期便不会再执行,但是动态块和构造方法是每创建一个对象都会执行一次,所有最终执行结果如下:



小结:


该案例重点是考察大家对于对象创建和继承的理解,大家可以就该案例进行变形演示进行总结分析。


三、方法重写:

通过继承我们可以解决代码冗余性问题,提高代码的可扩展性,但是我们在实际开发中发现在继承中父类定义的方法子类并不适用,而需要重写?那么什么是重写呢?重写时需要注意什么呢?

1.重写的定义:

重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。即外壳不变,核心重写!


重写的好处在于子类可以根据需要,定义特定于自己的行为。 也就是说子类能够根据需要实现父类的方法。


重写方法不能抛出新的检查异常或者比被重写方法申明更加宽泛的异常。例如: 父类的一个方法申明了一个检查异常 IOException,但是在重写这个方法的时候不能抛出 Exception 异常,因为 Exception 是 IOException 的父类,只能抛出 IOException 的子类异常。

2.重写的具体使用:

如上述例子,动物的类中定义一个打印方法,但是不同子类打印内容不同,所以需要对其进行重写,代码如下:


public class Animal{    //属性省略...  public void print(){    //代码省略...  }}
复制代码


public  class Dog extends Aniaml{  //属性省略     @Override    public void print() {       //子类具体实现    }}
复制代码


通过上述代码我们发现几个问题:


  • 父类的打印方法的方法实现体没有任何意义,是否可以省略?--(具体见抽象类抽象方法章节详细讲解)

  • 子类重写父类的同名方法,有什么要求?--(见后面重写规则)

3.重写规则:

  • 参数列表与被重写方法的参数列表必须完全相同。

  • 返回类型与被重写方法的返回类型可以不相同,但是必须是父类返回值的派生类(java5 及更早版本返回类型要一样,java7 及更高版本可以不同)。

  • 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为 public,那么在子类中重写该方法就不能声明为 protected。

  • 父类的成员方法只能被它的子类重写。

  • 声明为 final 的方法不能被重写。

  • 声明为 static 的方法不能被重写,但是能够被再次声明。

  • 子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为 private 和 final 的方法。

  • 子类和父类不在同一个包中,那么子类只能够重写父类的声明为 public 和 protected 的非 final 方法。

  • 重写的方法能够抛出任何非强制异常,无论被重写的方法是否抛出异常。但是,重写的方法不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异常,反之则可以。

  • 构造方法不能被重写。

  • 如果不能继承一个类,则不能重写该类的方法。


四、equals 方法重写:

1.为什么重写 equals 方法:

上述我们具体的讲解了方法的重写,我们来看实际生活中的一个例子:


定义一个学生类 Student,属性身份证号 id,姓名 name;如果两个人的身份证号和姓名相同,实际生活中则这两个人为同一个人,比较的话结果应该为 true;那么我们来看看如果在 java 中使用代码实现结果会如何?


package cn.hz;
/** * @author hz * @version 1.0 * 定义学生类 */public class Student { private Integer id; //属性:身份证号 private String name; //属性:姓名
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }}
复制代码


package cn.hz;
/** * @author hz * @version 1.0 */public class StudentTest { public static void main(String[] args) { //创建学生对象1 Student student1=new Student(); student1.setId(123456); student1.setName("张三"); //创建学生对象2 Student student2=new Student(); student2.setId(123456); student2.setName("张三"); System.out.println("两个学生对象是否为同一个:"+student1.equals(student2)); }}
复制代码


通过运行我们得出结果居然是 false:



很明显这样的结果并不符合我们实际,java 中结果为什么会是这样呢?通过分析我们知道 java 所有类都继承于 Object 父类,而我们使用的 equals 比较方法其实也是 Object 类的,通过 API 查找底层代码我们得知 Object 中 equals 源码如下:



本质上其实就是使用的==,==比较的是两个对象在堆中创建的地址,结果当然为 false,那么我们如何让我们 Student 类中的比较方法满足我们实际需求呢?这就需要我们重新父类 Object 中的 equals 方法。

2.重写 equals 方法的具体实现:

通过分析我们可以得知实际比较的几种情况:


  • 如果两个对象在堆内层地址一样,则肯定相等

  • 如果两个对象的类型不相同,则肯定不相等

  • 如果地址不同,类型相同,两个对象的所有属性都相同,则两个对象相等


代码实现如下:


package cn.hz;
/** * @author hz * @version 1.0 * 定义学生类 */public class Student { private Integer id; //属性:身份证号 private String name; //属性:姓名
public Integer getId() { return id; }
public void setId(Integer id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
/** * 重写equals方法 * @param obj * @return */ @Override public boolean equals(Object obj) { //如果两个对象地址相同,则两个对象对象内容一定相同; if(this==obj){ return true; } //如果两个对象的类型不同,则两个对象内容肯定不同, //instanceof表示判定左侧对象是否是右侧类型,返回值为boolean if(!(obj instanceof Student)){ return false; } //参数为object类型,已经判定类型,则可以确定obj为student类型,为了获取属性,obj转换为student Student o=(Student) obj; //如果两个对象的地址不同,类型相同,如果两个对象的属性值一样则为同一个对象,结果true if(this.id==o.id && this.name.equals(o.name) ){ return true; }else{ return false; } }
}
复制代码


package cn.hz;
/** * @author hz * @version 1.0 */public class StudentTest { public static void main(String[] args) { //创建学生对象1 Student student1=new Student(); student1.setId(123456); student1.setName("张三"); //创建学生对象2 Student student2=new Student(); student2.setId(123456); student2.setName("张三"); System.out.println("两个学生对象是否为同一个:"+student1.equals(student2)); }
}
复制代码


执行结果为 true,如下:


小结:

在实际生活中很多时候需要我们进行对象比较,equals 方法主要用于比较内容,==用于比较地址,为了让 equals 符合我们的实际需要,很多时候我们需要对 equals 方法进行重写。

用户头像

加百利

关注

还未添加个人签名 2021.06.08 加入

还未添加个人简介

评论

发布
暂无评论
JAVA面向对象(九)--继承