❤️Java图文深入解析 继承多态接口(超详细,小白一看就会)❤️

Posted 意愿三七

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了❤️Java图文深入解析 继承多态接口(超详细,小白一看就会)❤️相关的知识,希望对你有一定的参考价值。

前言

看完本章文,你会学习到 包,继承,多态思想,抽象类,接口,具体看目录即可。

本文全文 3.7w字数可收藏下拉慢慢看。


一、包

包 (package) 是组织类的一种方式.

为什么要组织类??

你在公司的一个工程上面创建了一个TestDemo类,要是你同事也创建一个叫做TestDemo类,就会发现创建不了。
为什么呢??我们试着打开src路径的文件夹看一下里面有什么?

原来是有一个TestDemo的文件,这样我们就明白了为什么不可以了,因为文件夹里面不可以出现2个相同文件的文件夹。

所以为了解决上面的问题,我们就引入了一个 这个概念,所以也就是说,在直观来看包就是一个文件夹而已

我们平时已经使用过包,比如:使用Scanner的时候,必须导入包含Scanner这个类的一个包(如果,没有导入就会报错),类似C语言的头文件。

导入包中的类

上面说到没有导入Scanner的包就会报错,那么我们怎么知道要导入什么包呢?这里提供一下方法

  1. IDea的提示第一个

    2.打开帮助手册进行查询(推荐多使用帮助手册)

    这里也是把帮助手册的下载链接放在这里,需要的小伙伴可以自己去下载: 帮助手册下载地址

这里可以看见: Scanner.class是存放在java.util.Scanner;

所以import导入会发现就是部分的文件存放路径。

这里我们可以尝试一下这个导包

import java.util.*;

* 代表通配符

代表导入util 下所有的类。

那就有个问题了,这么多类,我们没有使用到岂不是浪费了???

诶,老乡这你就不懂了

这里在java中是你使用到谁才会导谁,比如现在就使用到了Scanner ,它只会导入Scanner,随用随取

还有个问题我们平时使用的String也没有导入包啊 怎么可以正常使用呢?

这里有个小知识,在lang下面的不需要导入自己手动导入了 ,但是这些文件夹下面的可不一定了,这里先不提起

我们发现还有一种导包的方法:比如下面需要使用Date这个方法

这里我们可不可以不使用import,当然可以!!


但是在日常使用,大家也没有看见多少人这样导包吧,所以还是尽量使用import

但是下面有个情况:使用util* 和java.sql.*就会报错

为什么呢??我们打开帮助手册:

发现有两个下面都有…

这个情况下我们可以使用

java.util.Date date = new java.util.Date();

静态导入

该节了解即可:

我们平时写输出

但是大家要是不想写system那就使用下面的方法即可省略

为什么呢??因为可以发现System是一个类 所以可以这样。

也不推荐这样写,大家也不这样写。

但是这样有个好处就是看下面代码:


总是Math调用看情况不好,所以这个情况我们可以使用以下代码:

这样是不是简洁许多呢?但是这样不好,代码太简洁,可读性变差了。


将类放到包中

好了上面讲了那么多,其实就是证明文件在文件夹下面,现在我们需要解决一开始的问题,回到根本我们要开始创建package了

大家注意一定是在scr这个文件夹下创建,因为java程序只会找scr这里的东西

包名是有讲究的:一般采用公司的域名倒着写的方式

比如 www.baidu.com 我们写成:最后你可以取个有意义的名称com.baidu.TestDemo1

有些同学不一样,这里我们可以设置一下,看起来更有层次感

注意包名要小写,且最好是公司域名的逆置

在最下面的com里面创建一个class,对应的文件夹也存在这个class,避免了文件名冲突。

大家可以看见这个使用包创建的文件,这个java文件会使用package关键字跟上当前java文件的全路径,注意不要删除掉。

我们在src上面输入类名 编译器也会方便让我们区分


包的访问权限控制

我们已经了解了类中的 public 和 private. private 中的成员只能被类的内部使用

但是要是都不包含private 和public呢?我们在com包下创建了TetsDemo2并且里面 三个属性:其中sex是没有被任何访问符修饰,这个时候sex就是包访问权限:也就是当前com包下所有成员可以访问的到

当前com包下访问会发现有sex

我们试试去src的TestDemo下访问看看:只有age 没有sex


总结:包访问权限只能在同一个文件下的类里面访问。


二、继承

我们经常听见继承封装多态他们其实都是面向对象的一个基本特征(不是某一个语言的特征,上面向对象的特征,不单单指某一个语言)


1.什么是继承??
继承的根本出发点是若干类存在相似点,共享相同的属性和发法,这样来相似处能够提前出来重用,不必重复编写代码。

来代码举例: 只需要看注释的字

public class Animal {    //动物类
 public String name; 	//名称
 
 public Animal(String name) { 
 this.name = name; 
 } 
 
 public void eat(String food){  // 动物的行为 
 System.out.println(this.name + "正在吃" + food); 
 } 
} 
// Cat.java 
class Cat {   		//猫类型
 public String name; 
 
 public Cat(String name) { 
 this.name = name; 
 } 
 
 public void eat(String food) {   //猫的吃东西行为
 System.out.println(this.name + "正在吃" + food); 
 } 
} 
// Bird.java 
class Bird {      			//鸟类
 public String name; 
 
 public Bird(String name) { 
 this.name = name; 
 } 
 
 public void eat(String food) {   // 鸟的吃东西行为
 System.out.println(this.name + "正在吃" + food); 	
 } 
 
 public void fly() { 			// 鸟的飞行行为
 System.out.println(this.name + "正在飞 ︿( ̄︶ ̄)︿"); 
 } 
}

大家可以看见上面,我们定义了动物类型 动物有名字,和可以吃东西的行为,而我们的Cat类型,和Bird类型也会有名字,和吃东西的行为,这样的话代码就有重复的了,让我们的代码变的冗余了,我们可以看见既然动物了有这些相似的,那么我们可以直接继承Animal类,让它的东西给其他的类使用,这样就完成了我们继承的一个过程了。是不是十分简单。


小总结:

这三个类都具备一个相同的 eat 方法, 而且行为是完全一样的.
这三个类都具备一个相同的 name 属性, 而且意义是完全一样的.
从逻辑上讲, Cat 和 Bird 都是一种 Animal (is - a 语义)
此时我们就可以让 Cat 和 Bird 分别继承 Animal 类, 来达到代码重用的效果

此时, Animal 这样被继承的类, 我们称为 父类 , 基类 或 超类

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


语法规则

基本语法:

class 子类 extends 父类 { 
 
}

注意:

  • 使用 extends 指定父类

  • Java 中一个子类只能继承一个父类 (而C++/Python等语言支持多继承)

  • 子类会继承父类的所有 public 的字段和方法(其实private也可以被继承就是不可以被访问)

  • 对于父类的 private 的字段和方法, 子类中是无法访问的

  • 子类的实例中, 也包含着父类的实例. 可以使用 super 关键字得到父类实例的引用

    • 啥是super???超人?超类。 刚刚好像上面讲到了超类
      我们来看看代码的例子: 为什么呢?我们来探究原理:

解析: 我们要记住子类继承父类,需要先帮助父类来构造

在Cat构造前,我们已经继承了Animal,Animal先有 而且参数是String类型的我们父类也是String类型的所以使用Super就先给父类赋值了,就符合了上面说的话了。

使用快捷键 Alt+enter

我们以前不写为什么不报错呢??因为会默认生成一个没有参数的方法

super和this的区别

  • super关键字:我们可以通过super关键字来实现对父类成员的访问,用来引用当前对象的父类。

  • this关键字:指向自己的引用。

  • super()和this()不可以一起使用

    • 为什么呢?? 因为super要在第一行this()也要在第一行,所以水火不相容

      在第一行不报错

大家有没有发现为什么子类没有的字段方法可以被调用出来字段:

我们来画个内存图看看:age是Dog类的字段,下面的是的name是继承父类的

现在我们来修改代码一下看看:使用继承

class Animal { 
 public String name; 
 public Animal(String name) { 
 this.name = name; 
 } 
 public void eat(String food) { 
 System.out.println(this.name + "正在吃" + food); 
 } 
} 
class Cat extends Animal { 
 public Cat(String name) { 
 // 使用 super 调用父类的构造方法. 
 super(name); 
 } 
} 
class Bird extends Animal { 
 public Bird(String name) { 
 super(name); 
 } 
 public void fly() { 
 System.out.println(this.name + "正在飞 ︿( ̄︶ ̄)︿"); 
 } 
} 
public class Test { 
 public static void main(String[] args) { 
 Cat cat = new Cat("小黑"); 
 cat.eat("猫粮"); 
 Bird bird = new Bird("圆圆"); 
 bird.fly(); 
 } 
}

protected 关键字

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

  • 对于类的调用者来说, protected 修饰的字段和方法是不能访问的
  • 对于类的 子类同一个包的其他类 来说, protected 修饰的字段和方法是可以访问的
  • 对于不同包(使用super,不可以通过实例化)
// Animal.java 
public class Animal { 
 protected String name; 
 public Animal(String name) { 
 this.name = name; 
 } 
 
 public void eat(String food) { 
 System.out.println(this.name + "正在吃" + food); 
 } 
 
} 
// Bird.java 
public class Bird extends Animal { 
 public Bird(String name) { 
 super(name); 
 } 
 
 public void fly() { 
 // 对于父类的 protected 字段, 子类可以正确访问
 System.out.println(this.name + "正在飞 ︿( ̄︶ ̄)︿"); 
 } 
 
} 
// Test.java 和 Animal.java 不在同一个 包 之中了. 
public class Test { 
 public static void main(String[] args) { 
 Animal animal = new Animal("小动物"); 
 System.out.println(animal.name); // 此时编译出错, 无法访问 name 
 } 
}

小结: Java 中对于字段和方法共有四种访问权限

  • private: 类内部能访问, 类外部不能访问
  • 默认(也叫包访问权限): 类内部能访问, 同一个包中的类可以访问, 其他类不能访问.
  • protected: 类内部能访问, 子类和同一个包中的类可以访问, 其他类不能访问.
  • public : 类内部和类的调用者都能访问

范围表:default (什么修饰符都不写)

什么时候下用哪一种呢?

我们希望类要尽量做到 “封装”, 即隐藏内部实现细节, 只暴露出 必要 的信息给类的调用者.

因此我们在使用的时候应该尽可能的使用 比较严格 的访问权限. 例如如果一个方法能用 private, 就尽量不要用public.

另外, 还有一种 简单粗暴 的做法: 将所有的字段设为 private, 将所有的方法设为 public. 不过这种方式属于是对访问权限的滥用, 还是更希望同学们能写代码的时候认真思考, 该类提供的字段方法到底给 “谁” 使用(是类内部自己用, 还是类的调用者使用, 还是子类使用)


更加复杂的继承

要是父类有个字段,我们子类继承父类也有一个一样的字段,那么输出是谁的呢?

输出 3 ,3 ,1 , 满足就近原则

要是父类,还继承了个父类,然后子类输出的是谁的呢?
这个情况子类还是输出的是继承的父类,而不是父类的父类也是满足就近原则的。

上面的情况可以衍生出继承很多。

// Animal.java 
public Animal { 
 ... 
} 
// Cat.java 
public Cat extends Animal { 
 ... 
} 
// ChineseGardenCat.java 
public ChineseGardenCat extends Cat { 
 ... 
} 
// OrangeCat.java 
public Orange extends ChineseGardenCat { 
 ... 
} 
......

如刚才这样的继承方式称为多层继承, 即子类还可以进一步的再派生出新的子类.

注意:

时刻牢记, 我们写的类是现实事物的抽象. 而我们真正在公司中所遇到的项目往往业务比较复杂, 可能会涉及到一系列复杂的概念, 都需要我们使用代码来表示, 所以我们真实项目中所写的类也会有很多. 类之间的关系也会更加复杂.

但是即使如此, 我们并不希望类之间的继承层次太复杂. 一般我们不希望出现超过三层的继承关系. 如果继承层次太多, 就需要考虑对代码进行重构了.

如果想从语法上进行限制继承, 就可以使用 final 关键字


final 关键字

曾经我们学习过 final 关键字, 修饰一个变量或者字段的时候, 表示 常量 (不能修改).

final int a = 10; 
a = 20; // 编译出错

final 关键字也能修饰类, 此时表示被修饰的类就不能被继承.

final public class Animal { 
 ... 
} 
public class Bird extends Animal { 
 ... 
} 
// 编译出错
Error:(3, 27) java: 无法从最终com.bit.Animal进行继承


组合

什么叫组合??
组合其实也是经常被大家忽略,其实它也是面向对象的思想。
和继承类似, 组合也是一种表达类之间关系的方式, 也是能够达到代码重用的效果

例如表示一个学校:

public class Student { 
 ... 
} 
public class Teacher { 
 ... 
} 
public class School { 
 public Student[] students; 
 public Teacher[] teachers; 
}

组合并没有涉及到特殊的语法(诸如 extends 这样的关键字), 仅仅是将一个类的实例作为另外一个类的字段.
这是我们设计类的一种常用方式之一

组合表示 has - a 语义
在刚才的例子中, 我们可以理解成一个学校中 “包含” 若干学生和教师.

继承表示 is - a 语义
在上面的 “动物和猫” 的例子中, 我们可以理解成一只猫也 “是” 一种动物.

大家要注意体会两种语义的区别.


三、多态

多态是面向对象的第三大特征,它的实现实质上是由向上转型(Upcasting)也称为(向上映射)和动态绑定的机制结合完成的,只有理解了这两个机制,才能明白多态的意义。


向上转型的概念及方法调用

子类的对象可以赋值给父类的对象,即子类对象可以向上转型为父类类型


可以简写:

向上转型是安全的,这是因为任何子类都继承了并接受了父类的方法,子类与父类的继承关系是is-a的关系。猫属动物,动物并不只是猫。


向上转型发生的时机:

  1. 直接赋值

  2. 方法的传参

函数的参数是Animal,传过去了一个animal ,传过去的animal还是指向animal所指的对象 new Dog(),这也叫做上向转型。

还可以暴力点:这也是向上转型

  1. 方法的返回:


为什么不报错呢?? 因为他们是父子类关系

还有一种情况:return的是Dog,使用Animal接收

下一步


静态绑定和动态绑定

什么是绑定?将一个方法调用同一个方法所在的类关联在一起就是绑定,绑定分为静态绑定动态绑定两种:

  1. 静态绑定:即在编译时,编译器就能准确判断应该调用哪个方法,绑定是在运行前完成,也叫前期绑定
  2. 动态绑定:程序在运行期间由JVM根据对象的类型自动判断应该调用哪个方法,也叫后期绑定

接下来使用代码列子来解释一下两种绑定:

静态绑定:有如下方法和成员变量

定义Dog类:有如下方法和成员变量

Dog对象调用类中的成员方法,这种调用方式是在代码里指定的,编译时编译器就知道dog调用的是Dog的eat()。


结果:


动态绑定:代码稍微修改了下 看下面图:

新增了一个Dog2:

main方面的改动:有一个数组,然后随机数随机向上转型

结果:

结论:这种在animal[ i ] ,eat并不能具体看出调用了哪一个类的eat(),编译时也无法知道s数组元素的具体类型。直到运行时根据随机数确定animal[ i ] 具体代表的子类对象,最终调用的是哪一个子类的eat方法, 这种在运行时才能把方法调用与方法所属类关联在一起的方式就是动态绑定

也可以这样来看: 动态绑定

  • 发生向上转型
  • 通过父类引用 来调用子类和父类的同名覆盖方法
  • 编译的时候,调用的是父类的方法(从反汇编来看),运行的时候,实际上调用的是子类方法

向上转型需要注意的地方:


我想Dog调用dogName调用不出来

为什么呢?·因为这个引用类型是Animal,所以只可以调用Animal特有的方法和属性。

结论:通过父类引用 调用方法或者成员变量的时候 , 只能调用父类的特有方法或者成员


但是上面的话我们下图的结果既然是Dog的eat方法,那岂不是和上面的冲突了?

其实这个是一个新知识点:


方法重写

针对刚才的 eat 方法来说:

子类实现父类的同名方法, 并且参数的类型和个数完全相同, 这种情况称为 覆写/重写/覆盖(Override)

1.我们先来看一下重载(overload)和重写(override)的区别

重载:

  1. 方法名相同
  2. 方法的参数列表不同(数据的个数+数据的类型)
  3. 返回值不做要求
  4. 并不一定是在同一个类里面

重写:顾名思义重新写一份

  1. 方法名相同
  2. 方法的返回值相同
  3. 方法的参数列表相同

我们来看看刚刚的eat代码:

Animal类

Dog类

会发现他们的方法名和返回都是一样的,只有访问修饰符我们等等说。

有一个图很形象的表达出来他们的区别:

重写的注意事项:

  1. 要重写父类的方法,那个方法不可以被final修饰
  2. 父类方法不可以使用private修饰,子类的访问修饰符权限一定大于等于父类的权限
  3. 方法不能被static修饰

注解

针对重写的方法, 可以使用 @Override 注解来显式指定

在要重写的方法上加一个@Override

有了这个注解能帮我们进行一些合法性校验. 例如不小心将方法名字拼写错了 (比如写成 aet), 那么此时编译器就会发现父类中没有 aet 方法, 就会编译报错, 提示无法构成重写


向下转型

向上转型是子类对象转成父类对象, 向下转型就是父类对象转成子类对象. 相比于向上转型来说, 向下转型没那么常见,但是也有一定的用途.

先来看一下什么是向下转型 首先我们来看代码:dog指向animal 调用eat

运行一下:报错了 ,说Animal不可以转换为 Dog

我们试着来修改一下代码如下:

运行一下:成功运行

前提:向下转型 一定要先发生向上转型

可以看出向下转型是不安全的,因为不知道那个Dog要转换的类有没有关系

这个时候我们可以使用一个关键字instanceof:instanceof 可以判定一个引用是否是某个类的实例. 如果是, 则返回 true. 这时再进行向下转型就比较安全了.

这样就不会报错了,所以正是因为这样的不安全,我们平时不经常使用,不过上面认识即可。


在构造方法中调用重写的方法(一个坑)

一段有坑的代码. 在父类构造方法调用eat

运行结果:

main函数:

运行结果: 为什么会这样呢?

解析:
因为在构造方法中发生了动态绑定,调用了eat ,它会去调用 Dog中的eat

在Animal里面是先eat在this.name = name ,所以上面的eat没有值

结论: “用尽量简单的方式使对象进入可工作状态”, 尽量不要在构造器中调用方法(如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成), 可能会出现一些隐藏的但是又极难发现的问题


理解多态

有了面的向上转型, 动态绑定, 方法重写之后, 我们就可以使用 多态(polypeptide) 的形式来设计程序了.
我们可以写一些只关注父类的代码, 就能够同时兼容各种子类的情况

class Shape { 
 public void draw() { 
 // 啥都不用干
	 } 
} 

class Cycle extends Shape { 
 @Override 
 public void draw() { 

 System.out.println("○"); 
	 } 
} 

class Rect extends Shape { 
 @Override 
 public void draw() { 
 System.out.println("□"); 
 } 
} 

class Flower extends Shape { 
 @Override 
 public void draw() { 
 System.out.println("♣"); 
 } 
} 
/我是分割线// 
// Test.java 
public class Test { 

 public static void drawShape(Shape shape) { 
 shape.draw(); 
 } 

 public static void main(String[] args) { 
 Shape shape1 = new Flower();<

以上是关于❤️Java图文深入解析 继承多态接口(超详细,小白一看就会)❤️的主要内容,如果未能解决你的问题,请参考以下文章

JavaSE系列Java面向对象之组合多态与接口

深入Java核心 Java中多态的实现机制

Python类和对象以及继承多态(超详细,小白也可以懂)

继承多态接口

java基础三 [深入多态,接口和多态](阅读Head First Java记录)

深入理解Java面向对象三大特性 封装 继承 多态