Java面向对象之继承

为什么要使用继承?

我们先来看一段代码:


Java面向对象之继承

从上述代码中我们发现,学生、老师、员工的中存在着重复的代码,代码重复就意味着维护的成本和难度增大,那么如何去解决代码重复这个问题呢,我们观察三个类,发现这个三个类中都有共同的属性:name、age,共同的方法:sleep此时我们就可以把他们共同的属性和方法单独新建一个类,类的名称就叫Person,这个可以怎么理解,学生、老师、员工都是人,他们都有名字、年龄,他们都要睡觉,Person类就是把他们的共性提取出来并把它封装起来。

Person类代码如下:

Java面向对象之继承

现在Person类提取出来了,那么怎么才能让它和学生、老师、员工的类有关系呢?这个时候就要使用继承了,他们继承关系图如下:

Java面向对象之继承

Person类就是父类(也有叫超类、基类、被拓展类),学生、老师、员工都继承于Person类,这些叫子类(也有叫拓展类),当子类集成父类之后,就具有父类的某些行为。由此可见,父类存放的是共性,而子类存放的是特性。从上面的列子可以看出,继承关系解决的是代码重复的问题。

什么是继承关系?

继承的定义如下:基于某个父类对对象的定义加以拓展,而产生新的子类定义,从这里我们可以发现,继承是先要有父类,然后在父类的基础上进行拓展产生子类,子类可以继承父类的某些定义,注意是某些,父类有些定义子类是继承不到的,比如某些方法,内部类,这个之后再说,先不管,子类也可以增加父类原来没有的定义,或者覆写父类中的某些特性。从面向对象的角度来说继承是一般到特殊的关系,比如说,狗是动物,狗就是子类,动物是父类。在java中,我们使用"extends"关键字来表示子类和父类之间的关系

语法格式如下:在定义子类的时候来表明自己拓展于哪一个类父类

public class 子类类名 extends 父类类名{

//编写自己特有的状态(即字段)和行为(即方法)

}

示例如下:改写上文的代码

Java面向对象之继承

在java中类与类之间的关系只允许单继承,不允许多继承,也就是说一个类它只能有一个之间的父类,不能出现一个类直接同时继承于多个父类,就像这样 【class Employee extends Person,SuperMan】,这种写法是错误的。也有编程语言是支持多继承的,比如C++是支持多继承的。java不支持多继承(java中通过实现接口来达到多继承的功能目的。一个类只能继承一个类,但是却可以实现多个接口!),但是java可以使用多重继承,也就是说一个类可以继承另外一个类,而另外一个类又可以继承别的类,比如A类继承B类,而B类又可以继承C类。.

在java中除了Object类之外,每一个类都有一个直接的父类,比如:class Student extends Person{},此时Student的直接父类就是Person,此时Person的父类又是谁呢,是Object类,因为Object类是java语言的根类,任何类都是Object的子类,相当于老祖宗,举个例子来说:class Person {}和class Person extends Object{} 是等价的。所以可以得出一个结论,Object要么是一个类的直接父类,要么就是一个类的间接父类。上面我们曾说过继承关系是解决代码重用的问题,其实非也,真正的作用是表达一个体系的问题

子类继承了父类的哪些成员?

我们先讨论一个问题即在继承中我们是写子类还是先写父类呢?这个需要看情况而定,一般的在开发的过程中,先编写多个自定义类,写的时候发现多个类中间存在共同的代码,此时可以抽取出一个父类。这是一种情况,还有一种情况,就是我们做开发大多是基于框架/组件来做的,我们是在别人的基础上继续做开发,打个比方就是别人提供了一个毛坯房,我们只需要在毛坯房的基础上进行装修,以后我们定义新的类去继承于框架中/组件中提供的父类。


子类继承父类,子类拥有父类的某些状态和行为,如此一来才会有代码的复用,那么我们来讨论一个问题即:子类到底继承了父类的那些成员(根据访问修饰符来判断):

  1. 如果父类中的成员使用public修饰,子类继承

  2. 如果父类中的成员使用protected修饰,子类也继承,即便是父类和子类不在同一个包中

  3. 如果父类和子类在同一个包中,此时子类可以继承父类缺省修饰符的成员

  4. 如果父类中的成员使用private修饰,子类无法继承,因为private修饰的成员只能在本类总访问

  5. 父类的构造器子类也不能继承,因为构造器必须与当前的类名相同

方法覆盖

子类继承父类后,可以获得父类的某些成员,可是当父类的某一个方法不适合子类的具体特征,那么怎么办呢,比如一般鸟可以飞行,但是鸵鸟也是属于鸟,但是鸵鸟不能飞行,所以当鸵鸟继承鸟的时候,飞行的行为也继承下来的,但是此时飞行的行为就不适合鸵鸟。这个时候就需要重方法的覆盖

未覆盖父类代码如下:

Java面向对象之继承

运行结果如下:

Java面向对象之继承

从上面的运行接过来看,很明显不符合常理,鸵鸟怎么可以会飞呢。此时就需要使用方法覆盖,那么什么是方法的覆盖呢,就是将父类的方法原封不动的拷贝过来,然后将方法体改了,方法的结构不变。代码如下:

Java面向对象之继承

运行的结果如下:

Java面向对象之继承

此时就符合常理了,那么也许会有人问,为什么调用的不是父类的fly方法,而是子类的fly方法呢?这是因为先找子类的方法,如果找到就执行,如果找不到就去父类中找,如果父类也没有就去Object根类中找,直到找到为止,一直找到根类为止,如果还没有找到就报错。只有方法才有覆盖的概念,覆盖的英文名称Override,当父类的某个行为不符合子类的特征的时候,此时就需要覆盖。

方法覆盖原则:

  1. 一同:方法的名称和参数列表必须相同

  2. 两小:

(1)子类方法的返回值类型要么和父类相同,要么比父类的返回类型小,也就是说子类的返回值更具体。

(2)子类方法声明抛出的异常类型和父类方法声明抛出的异常类型相同或是其子类

3.一大:子类方法的访问权限必须大于等于父类方法的访问权限

我们可以使用@Override注解来检查当前子类的方法是否覆盖了父类的方法,如果不是,就会报错。

方法重载和方法覆盖的区别

这两者本身没有什么联系,只是名字很像,方法覆盖的英文为Override,方法重载的英文为:Overload。

方法重载:解决同一类中相同的功能,方法名不同的问题。规则是:两同(方法名相同,同一个类)一不同(参数列表不同)

方法覆盖:解决子类继承父类之后,父类的某一个方法不满足子类的具体特征的问题。规则是:一同两小一大(详细见上文)

super关键字

假设现在有一个需求:在子类的方法中,去调用父类被覆盖的方法。

不使用super关键字调用父类被覆盖的方法代码如下:

Java面向对象之继承

运行结果如下:

Java面向对象之继承

我从运行的结果发现,此时say方法调用fly方法是子类中,不是父类中的fly方法,如果要调用父类中被覆盖的方法,就要使用super关键字,代码如下:

Java面向对象之继承

运行结果如下:

Java面向对象之继承

此时调用的就是父类中fly方法,由此可见super就表示当前对象的父类对象,当前对象就是this(谁调用this所在的方法,this就表示哪一个对象)

子类初始化过程:创建子类对象的过程

我们先看一段代码:

Java面向对象之继承

运行结果如下:

Java面向对象之继承

从运行结果我们发现,在创建子类对象时的执行顺序是:先进入子类构造器,然后在构造器中会先调用父类构造器,创建父类对象,然后在执行子类构造器的代码。其实在Fish类中无参构造器存在一个隐式的super(),代码如下:

Java面向对象之继承

运行结果如下:

Java面向对象之继承

从运行结果我们发现和上面的是一样的。值得注意的是super()必须作为构造器的第一个语句,这个是和this()一样的,否则就会报错。这里我们总结一下:在创建子类对象之前,会先创建父类对象,调用子类构造器之前,在子类构造器中,会先调用父类的构造器,默认调用的是父类的无参构造器,如果父类中不存在可以被子类访问的构造器,则不能存在子类。如果父类没有提供无参数构造器,子类必须显示通过super语句去调用父类带参数的构造器。

super的使用场景

在说super的使用场景前,我们先说一下隐藏的概念,所谓的隐藏就是"遮蔽"的意思,隐藏的三个场景:

  1. 满足继承的访问权限下,隐藏父类静态方法:若子类定义的静态方法的签名和超类中的静态方法签名相同,那么此时就隐藏父类方法,注意仅仅是静态方法,子类和父类存在一模一样的静态方法,注意这种情况不叫覆盖,非静态方法才叫覆盖。

  2. 满足继承的访问权限下,隐藏父类字段:若子类中定义的字段和超类中定义的字段名相同(不管类型),此时就隐藏父类字段,此时只能通过super关键字访问被隐藏的字段。(一般这种情况我们不讨论,因为破坏封装)

  3. 隐藏本类字段:若同类中某局部变量名和字段名相同,此时就是隐藏本类字段,此时只能通过this访问被隐藏的字段。

下面我们来说super的使用场景:

场景一:可以使用super解决子类隐藏父类的字段情况,但是一般情况下,我们不使用,因为破坏封装,但是我们要知道有这种现象。

范例:子类隐藏父类的字段

Java面向对象之继承

运行结果如下:

Java面向对象之继承

从运行结果中我们发现,打印出的是子类的字段,此时就把父类的字段隐藏了,如果要打印父类的字段就需要使用super关键字。代码如下:

Java面向对象之继承

运行结果如下:

Java面向对象之继承

场景二:在子类方法中,调用父类被覆盖的方法,此时必须使用super,这种情况我们使用的比较多

场景三:在子类的构造器中,调用父类构造器,此时必须使用super关键字,这种场景也比较多。

补充:static和super、this不能共存,因为static是类级别的,而super和this是对象级别的。

Object类和常用方法

Object类是java语言的根类,要么是一个类的直接父类,要么是一个类的间接父类。为什么Object是所有类的根类,到底什么是Object?Object本身是指对象的意思,我们发现所有的对象(包括数据)都具有共同的行为,所以我们抽象出一个类,就叫Object表示对象类,其他类都会继承于Object类,也就拥有Object类中的方法。所有对象都拥有Object中的方法,那为什么数组也可以拥有Object类中的方法呢,在java中的引用类型包括:数组、接口、类,引用类型有称之为对象类型,所谓的数组名称,应该指数组对象。我们学习Object类,除了要知道它是根类,还要学习它的方法。

Object类的常见方法:

  1. protected void finalize():当垃圾回收器确定不存在对该对象的更多引用时,由对象的垃圾回收器调用此方法。垃圾在回收某一个对象之前,会先调用该方法,该方法我们不要去调用,让垃圾回收器去调用

  2. Class> getClass(): 返回当前对象的真实类型

  3. int hashCode():返回该对象的哈希码值。hashCode决定了对象在哈希表中的存储位置 ,不同对像的hashCode是不一样的。

  4. boolean equals(Object obj):指示其他某个对象是否与此对象“相等”。 当前对象(this)和参数obj做比较,在Object类中的方法,本身和"=="相同,都是比较的内存地址,官方建议,每个类都应该覆写或覆盖equals()方法,不要比较内存地址,应该比较我们关心的数据。比如:两个字符串,只要内容相同,我们就认为是同一个字符串,因为我们关注的是内容,不是内存地址。注意这里是Object中的equals()方法,不是String类中的equals方法,String中的equals方法覆写了Object中的equals方法。

  5. String toString() 返回该对象的字符串表示。 把一个对象转换成字符串,打印对象的时其实打印的就是对象的toString()方法也就是说,System.out.println(obj对象)等价于System.out.println(obj对象.toString()),默认情况打印出来的就是对象的hashcode值所以要想打印出内容,就要覆写toString()方法,这也是官方建议的。

Java面向对象之继承

小生不足之处,还请大家多多指点,一起交流学习,共同进步,爱腻们哦。


分享到:


相關文章: