0


Java-继承与多态

面向对象编程-1

一.包

1.什么是包

包 (package)

是组织类的一种方式,使用包的主要目的是保证类的唯一性。

2.导入包中的类

Java中已经提供了很多现成的类供我们使用,比如我们可以使用

java.util.Date

这种方式引入

java.util

这个包中的

Date 类

。代码如下:

publicclassTest{publicstaticvoidmain(String[] args){java.util.Date date =newjava.util.Date();// 得到一个毫秒级别的时间戳System.out.println(date.getTime());}}

但是这种写法往往比较麻烦,所以我们可以使用

import

语句导入包。代码如下:

importjava.util.Date;publicclassTest{publicstaticvoidmain(String[] args){Date date =newDate();// 得到一个毫秒级别的时间戳System.out.println(date.getTime());}}

如果需要使用

java.util

中的其他类, 可以使用

import java.util.*

。这里的

*

可以理解为通配符,用它就可以使用包中的所有类。代码如下:

importjava.util.*;publicclassTest{publicstaticvoidmain(String[] args){Date date =newDate();// 得到一个毫秒级别的时间戳System.out.println(date.getTime());}}

注意:Java是用到包中的哪个类就导入哪个类

但是 我们更建议显示的指定要导入的类名,否则还是容易出现冲突的情况。例如:

importjava.util.*;importjava.sql.*;publicclassTest{publicstaticvoidmain(String[] args){// util 和 sql 中都存在一个 Date 这样的类, 此时就会出现歧义, 编译出错Date date =newDate();System.out.println(date.getTime());}}// 编译出错Error:(5,9) java: 对Date的引用不明确
  java.sql 中的类 java.sql.Date 和 java.util 中的类 java.util.Date 都匹配

这种情况下我们就需要完整的包名:

importjava.util.*;importjava.sql.*;publicclassTest{publicstaticvoidmain(String[] args){java.util.Date date =newjava.util.Date();System.out.println(date.getTime());}}

注意事项:

  • import 和 C++ 的 #include 差别很大。C++ 必须通过 #include 来引入其他文件内容, 但是 Java 不需要,import 只是为了写代码的时候更方便。import 更类似于 C++ 的 namespace 和 using
  • import java.util.*导入包下的所有类,Java是用到哪个类再去拿哪个类,而不是像include一样导入所有的文件

3.静态导入

使用

import static

可以导入包中的静态方法和字段。代码如下:

importstaticjava.lang.System.*;publicclassTest{publicstaticvoidmain(String[] args){
        out.println("hello");}}

使用这种方式可以更方便的写一些代码, 例如:

importstaticjava.lang.Math.*;publicclassTest{publicstaticvoidmain(String[] args){double x =30;double y =40;// 静态导入的方式写起来更方便一些. // double result = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));double result =sqrt(pow(x,2)+pow(y,2));System.out.println(result);}}

4.将类放入包中

基本规则:

  • 在文件的最上方加上一个 package 语句指定该代码在哪个包中。
  • 包名需要尽量指定成唯一的名字, 通常会用公司的域名的颠倒形式(例如 com.bit.demo1 )
  • 包名要和代码路径相匹配。例如创建 com.bit.demo1 的包, 那么会存在一个对应的路径 com/bit/demo1 来存储代码
  • 如果一个类没有 package 语句, 则该类被放到一个默认包
  • 包名必须小写

5.包的访问权限控制

我们已经了解了类中的 public 和 private, private 中的成员只能被

类的内部

使用

如果某个成员不包含 public 和 private 关键字, 此时这个成员可以在

包内部的其他类

使用, 但是不能在包外部的类使用

下面的代码给了一个示例。 Demo1 和 Demo2 是同一个包中, Test 是其他包中:
Demo1.java

packagecom.bit.demo;publicclassDemo1{int value =0;}

Demo2.java

packagecom.bit.demo;publicclassDemo2{publicstaticvoidMain(String[] args){Demo1 demo =newDemo1();System.out.println(demo.value);}}// 执行结果, 能够访问到 value 变量10

Test.java

importcom.bit.demo.Demo1;publicclassTest{publicstaticvoidmain(String[] args){Demo1 demo =newDemo1();System.out.println(demo.value);}}// 编译出错Error:(6,32) java: value在com.bit.demo.Demo1中不是公共的; 无法从外部程序包中对其进行访问

6.常见的系统包

  1. java.lang:系统常用基础类(String、Object),此包从JDK1.1后自动导入。
  2. java.lang.reflect:java 反射编程包
  3. java.net:进行网络编程开发包。
  4. java.sql:进行数据库开发的支持包。
  5. java.util:是java提供的工具程序包。(集合类等) 非常重要
  6. java.io:I/O编程开发包

二.继承

1.什么是继承

简单来说,继承的意义就是

实现代码的复用

代码中创建的类, 主要是为了抽象现实中的一些事物(包含属性和方法)

有的时候客观事物之间就存在一些关联关系, 那么在表示成类和对象的时候也会存在一定的关联

例如, 设计一个类表示动物:

publicString name;publicAnimal(String name){this.name = name;}publicvoideat(String food){System.out.println(this.name +"正在吃"+ food);}}classCat{publicString name;publicCat(String name){this.name = name;}publicvoideat(String food){System.out.println(this.name +"正在吃"+ food);}}classBird{publicString name;publicBird(String name){this.name = name;}publicvoideat(String food){System.out.println(this.name +"正在吃"+ food);}publicvoidfly(){System.out.println(this.name +"正在飞 ︿( ̄︶ ̄)︿");}}

这个代码我们发现其中存在了大量的冗余代码

仔细分析, 我们发现

Animal

Cat

以及

Bird

这几个类中存在一定的关联关系:

  • 这三个类都具备一个相同的 eat 方法, 而且行为是完全一样的
  • 这三个类都具备一个相同的 eat 方法, 而且行为是完全一样的
  • 这三个类都具备一个相同的 name 属性, 而且意义是完全一样的
  • 从逻辑上讲, Cat 和 Bird 都是一种 Animal (is - a 语义)

此时我们就可以让 Cat 和 Bird 分别

继承

Animal 类, 来达到代码重用的效果

2.继承的语法规则

基本语法:

class 子类 extends 父类 {}
  • 使用 extends 指定父类
  • Java 中一个子类只能继承一个父类 (而C++/Python等语言支持多继承)
  • 子类会继承父类的所有 public 的字段和方法
  • 对于父类的 private 的字段和方法, 子类中是无法访问的
  • 子类的实例中, 也包含着父类的实例。 可以使用 super 关键字得到父类实例的引用

这时候,我们再把上面的代码改一下,通过

extends

关键字实现继承,将

Cat

Bird

继承

Animal

类, Cat 在定义的时候就不必再写 name 字段和 eat 方法:

classAnimal{publicString name;publicAnimal(String name){this.name = name;}publicvoideat(String food){System.out.println(this.name +"正在吃"+ food);}}classCatextendsAnimal{publicCat(String name){// 使用 super 调用父类的构造方法. super(name);}}classBirdextendsAnimal{publicBird(String name){super(name);}publicvoidfly(){System.out.println(this.name +"正在飞 ︿( ̄︶ ̄)︿");}}publicclassTest{publicstaticvoidmain(String[] args){Cat cat =newCat("小黑"); 
         cat.eat("猫粮");Bird bird =newBird("圆圆"); 
         bird.fly();}}

像Animal这种被继承的类,我们称为

父类、基类 或者 超类

,对于像 Cat 和 Bird 这样的类,我们称为

子类 或者 派生类

。和现实中的儿子继承父亲的财产类似, 子类也会继承

父类的字段和方法

, 以达到代码重用的效果

这时候,如果我们把 name 改成 private, 那么此时子类就不能访问了:

publicBird(String name){super(name);}publicvoidfly(){System.out.println(this.name +"正在飞 ︿( ̄︶ ̄)︿");}}// 编译出错Error:(19,32) java: name 在 Animal 中是 private 访问控制

3.protected关键字

刚才我们发现, 如果把字段设为 private, 子类不能访问。但是设成 public, 又违背了我们 “封装” 的初衷。两全其美的办法就是

protected

关键字

  • 对于类的调用者来说, protected 修饰的字段和方法是不能访问的
  • 对于类的 子类同一个包的其他类 来说, protected 修饰的字段和方法是可以访问的
// Animal.java publicclassAnimal{protectedString name;publicAnimal(String name){this.name = name;}publicvoideat(String food){System.out.println(this.name +"正在吃"+ food);}}// Bird.java publicclassBirdextendsAnimal{publicBird(String name){super(name);}publicvoidfly(){// 对于父类的 protected 字段, 子类可以正确访问System.out.println(this.name +"正在飞 ︿( ̄︶ ̄)︿");}}// Test.java 和 Animal.java 不在同一个 包 之中了 publicclassTest{publicstaticvoidmain(String[] args){Animal animal =newAnimal("小动物");System.out.println(animal.name);// 此时编译出错, 无法访问 name }}

小结:

Java 中对于字段和方法共有

四种

访问权限:

  1. private: 类内部能访问, 类外部不能访问
  2. 默认(也叫包访问权限): 类内部能访问, 同一个包中的类可以访问, 其他类不能访问
  3. protected: 类内部能访问, 子类和同一个包中的类可以访问, 其他类不能访问
  4. public : 类内部和类的调用者都能访问在这里插入图片描述什么时候下用哪一种呢? 我们希望类要尽量做到 封装, 即隐藏内部实现细节, 只暴露出 必要 的信息给类的调用者。 因此我们在使用的时候应该尽可能的使用 比较严格 的访问权限. 例如如果一个方法能用 private, 就尽量不要用 public 。 另外, 还有一种 简单粗暴 的做法: 将所有的字段设为 private, 将所有的方法设为 public。不过这种方式属于是对访问权限的滥用,还是更希望同学们能写代码的时候认真思考, 该类提供的字段方法到底给 “谁” 使用(是类内部自己用, 还是类的调用者使用, 还是子类使用)

4.更复杂的继承关系

刚才我们的例子中, 只涉及到 Animal, Cat 和 Bird 三种类。但是如果情况更复杂一些呢?
针对 Cat 这种情况, 我们可能还需要表示更多种类的猫:
在这里插入图片描述
这个时候使用继承方式来表示, 就会涉及到更复杂的体系:

// Animal.java publicAnimal{...}// Cat.java publicCatextendsAnimal{...}// ChineseGardenCat.java publicChineseGardenCatextendsCat{...}// OrangeCat.java publicOrangeextendsChineseGardenCat{...}......

如刚才这样的继承方式称为

多层继承

,即子类还可以进一步的再派生出新的子类。 一般我们不希望出现

超过三层

的继承关系。 如果继承层次太多,就需要考虑对代码进行重构。
如果想从语法上进行限制继承, 就可以使用

final

关键字。

5.final关键字

final关键字总共有三种用法:
在这里插入图片描述

三.组合

组合和继承类似,也是一种表达

类之间关系

的方式,也是能够达到

代码重用

的效果,例如表示一个学校:

publicclassStudent{...}publicclassTeacher{...}publicclassSchool{publicStudent[] students;publicTeacher[] teachers;}

组合并没有涉及到特殊的语法(诸如 extends 这样的关键字),仅仅是将一个类的

实例

作为另外一个类的

字段

。这是我们设计类的一种常用方式之一。

组合和继承的区别:

  • 组合表示 has - a 语义

在刚才的例子中, 我们可以理解成一个学校中 “

包含

” 若干学生和教师

  • 继承表示 is - a 语义

在上面的 “动物和猫” 的例子中, 我们可以理解成一只猫也 “

” 一种动物

四.多态

1.向上转型

刚才例子中,我们写了这样一段代码:

Bird bird =newBird("圆圆");

这段代码也可以这样写:

Bird bird =newBird("圆圆");Animal bird2 = bird;// 或者写成下面的方式Animal bird2 =newBird("圆圆");

此时

bird2

是一个父类

(Animal) 的引用

,指向一个子类

(Bird) 的实例

。这种写法称为

向上转型

在这里插入图片描述
向上转型发生的时机主要有三种:

  1. 直接赋值
  2. 方法传参
  3. 方法返回

直接赋值的方式我们已经演示了,接下来我们具体演示一下其他两种:

方法传参:

publicclassTest{publicstaticvoidmain(String[] args){Bird bird =newBird("圆圆");feed(bird);}publicstaticvoidfeed(Animal animal){ 
         animal.eat("谷子");}}// 执行结果
圆圆正在吃谷子

此时形参

animal

的类型是

Animal

,实际上对应到

Bird

的实例

方法返回:

publicclassTest{publicstaticvoidmain(String[] args){Animal animal =findMyAnimal();}publicstaticAnimalfindMyAnimal(){Bird bird =newBird("圆圆");return bird;}}

此时方法

findMyAnimal

返回的是一个 Animal 类型的引用, 但是实际上对应到 Bird 的实例

2.动态绑定

当子类和父类中出现

同名方法

的时候,再去调用会出现什么情况呢?

对前面的代码稍加修改, 给 Bird 类也加上同名的 eat 方法,并且在两个 eat 中分别加上不同的日志:

classAnimal{protectedString name;publicAnimal(String name){this.name = name;}publicvoideat(String food){System.out.println("我是一只小动物");System.out.println(this.name +"正在吃"+ food);}}classBirdextendsAnimal{publicBird(String name){super(name);}publicvoideat(String food){System.out.println("我是一只小鸟");System.out.println(this.name +"正在吃"+ food);}}publicclassTest{publicstaticvoidmain(String[] args){Animal animal1 =newAnimal("圆圆"); 
         animal1.eat("谷子");Animal animal2 =newBird("扁扁"); 
         animal2.eat("谷子");}}// 执行结果
我是一只小动物
圆圆正在吃谷子
我是一只小鸟
扁扁正在吃谷子

此时,我们发现:

  • animal1 和 animal2 虽然都是 Animal 类型的引用, 但是 animal1 指向 Animal 类型的实例,animal2 指向Bird 类型的实例
  • 针对 animal1 和 animal2 分别调用 eat 方法, 发现 animal1.eat() 实际调用了父类的方法,而animal2.eat() 实际调用了子类的方法

首先我们找到class文件所在目录,按住shift,右键点击Test,点击PowerShell,发现编译时调用的方法并不能确定

真正调用的方法


在这里插入图片描述
因此, 在 Java 中,调用某个类的方法,究竟执行了哪段代码 (是父类方法的代码还是子类方法的代码) , 要看究竟这个

引用指向

的是父类对象还是子类对象。 这个过程是程序

运行时决定

的(而不是编译期), 因此称为

动态绑定

3.方法重写

子类实现父类的同名方法,并且

参数的类型

个数

完全相同,这种情况称为

覆写/重写/覆盖(Override)

注意事项:

  1. 重写和重载完全不一样,不要混淆:在这里插入图片描述
  2. 普通方法可以重写, static 修饰的静态方法不能重写
  3. 重写中子类的方法的访问权限不能低于父类的方法访问权限
  4. 重写的方法返回值类型不一定和父类的方法相同(可以是协变类型,返回值构成继承关系)
  5. 另外, 针对重写的方法, 可以使用 @Override 注解来显式指定

4.向下转型

向上转型是

子类对象

转成

父类对象

,向下转型就是

父类对象

转成

子类对象

。相比于向上转型来说,向下转型没那么常见,但是也有一定的用途
在这里插入图片描述
要想细究向下转型,我们来看一段代码:

publicclassAnimal{protectedString name;publicAnimal(String name){this.name = name;}publicvoideat(String food){System.out.println("我是一只小动物");System.out.println(this.name +"正在吃"+ food);}}classBirdextendsAnimal{publicBird(String name){super(name);}publicvoideat(String food){System.out.println("我是一只小鸟");System.out.println(this.name +"正在吃"+ food);}publicvoidfly(){System.out.println(this.name +"正在飞");}}

接下来我们在Test里让圆圆吃谷子:

Animal animal =newBird("圆圆"); 
animal.eat("谷子");// 执行结果
圆圆正在吃谷子

接下来我们尝试让圆圆飞起来:

animal.fly();// 编译出错
找不到 fly 方法

究竟圆圆为啥不能飞呢?

编译过程中, animal 的类型是

Animal,

此时编译器只知道这个类中有一个 eat 方法,

没有 fly 方法

。虽然 animal 实际引用的是一个 Bird 对象, 但是编译器是以

animal 的类型

来查看有哪些方法的。
对于

Animal animal = new Bird("圆圆")

这样的代码,

  • 编译器检查有哪些方法存在, 看的是 Animal 这个类型
  • 执行时究竟执行父类的方法还是子类的方法, 看的是 Bird 这个类型

那么想实现刚才的效果, 就需要

向下转型

// (Bird) 表示强制类型转换Bird bird =(Bird)animal; 
bird.fly();// 执行结果
圆圆正在飞

但是这样的向下转型有时是不太可靠的, 例如:

Animal animal =newCat("小猫");Bird bird =(Bird)animal; 
bird.fly();// 执行结果, 抛出异常Exception in thread "main"java.lang.ClassCastException:Cat cannot be cast toBird 
 at Test.main(Test.java:35)

请注意,这里不是所有动物都是Bird!

animal

本质上引用的是一个

Cat 对象

,是不能转成 Bird 对象的, 运行时就会抛出异常。

所以, 为了让向下转型更安全, 我们可以先判定一下看看 animal 本质上是不是一个 Bird 实例, 再来转换:

Animal animal =newCat("小猫");if(animal instanceofBird){Bird bird =(Bird)animal; 
     bird.fly();}
instanceof

可以判定

一个引用

是否是

某个类的实例

。 如果是, 则返回 true. 这时再进行向下转型就比较安全了

5.super关键字

前面的代码中由于使用了重写机制, 调用到的是子类的方法。 如果需要在子类内部调用父类方法怎么办? 可以使用

super 关键字

,表示对父类实例的引用。

常见用法:

  1. super():调用父类的构造方法
  2. super.func():调用父类的普通方法
  3. super.data:调用父类的成员属性
super

this

的区别:

  1. 代表的事物不同: super代表的是父类空间的引用 this代表的是所属函数的调用者对象
  2. 使用前提不同: super必须要有继承关系才可以使用 this不需要继承关系也可以使用
  3. 调用事物不同: super调用的是父类的构造方法 this调用的是所属类的构造方法

在这里插入图片描述

6.构造方法中调用重写方法

这是一段有坑的代码,我们来看一看:
我们创建两个类,

B 是父类

D 是子类

。D 中重写

func 方法

,并且在 B 的构造方法中

调用 func
classB{publicB(){// do nothing func();}publicvoidfunc(){System.out.println("B.func()");}}classDextendsB{privateint num =1;@Overridepublicvoidfunc(){System.out.println("D.func() "+ num);}}publicclassTest{publicstaticvoidmain(String[] args){D d =newD();}}// 执行结果D.func()0

这里调用了子类的方法,说明又发生了动态绑定:

  • 构造 D 对象的同时, 会调用 B 的构造方法
  • B 的构造方法中调用了 func 方法,此时会触发动态绑定, 会调用到 D 中的 func
  • 此时 D 对象自身还没有构造, 此时 num 处在未初始化的状态,值为 0

7.理解多态

了解了

向上转型

动态绑定

方法重写

之后,我们就可以使用

多态(polypeptide)

的形式来设计程序了。我们可以写一些

只关注父类

的代码, 就能够同时

兼容各种子类

的情况。

说再多不如上一段代码来理解:

classShape{publicvoiddraw(){// 啥都不用干}}classCycleextendsShape{@Overridepublicvoiddraw(){System.out.println("○");}}classRectextendsShape{@Overridepublicvoiddraw(){System.out.println("□");}}classFlowerextendsShape{@Overridepublicvoiddraw(){System.out.println("♣");}}/我是分割线// publicclassTest{publicstaticvoidmain(String[] args){Shape shape1 =newFlower();Shape shape2 =newCycle();Shape shape3 =newRect();drawMap(shape1);drawMap(shape2);drawMap(shape3);}// 打印单个图形publicstaticvoiddrawShape(Shape shape){ 
         shape.draw();}}

在这个代码中, 分割线上方的代码是

类的实现者

编写的,分割线下方的代码是

类的调用者

编写的。
当类的调用者在编写

drawMap

这个方法的时候, 参数类型为

Shape (父类)

, 此时在该方法内部并不知道, 也不关注当前的 shape 引用指向的是哪个类型(哪个子类)的实例。
此时 shape 这个引用

调用 draw 方法

可能会有多种不同的表现(和 shape 对应的实例相关), 这种行为就称为

多态

我们为什么要使用多态?有什么好处吗?

  1. 类调用者对类的使用成本进一步降低。 封装是让类的调用者不需要知道类的实现细节。 多态能让类的调用者连这个类的类型是什么都不必知道,只需要知道这个对象具有某个方法即可。 因此, 多态可以理解成是封装的更进一步, 让类调用者对类的使用成本进一步降低。
  2. 能够降低代码的 “圈复杂度”, 避免使用大量的 if - else
  3. 可扩展能力更强。如果要新增一种新的形状, 使用多态的方式代码改动成本也比较低

本文转载自: https://blog.csdn.net/weixin_55752048/article/details/122792304
版权归原作者 ViolentAsteroid 所有, 如有侵权,请联系我们删除。

“Java-继承与多态”的评论:

还没有评论