前言
看完本章文,你会学习到 包,继承,多态思想,抽象类,接口,具体看目录即可。
本文全文 3.7w字数可收藏下拉慢慢看。
一、包
包 (package) 是组织类的一种方式.
为什么要组织类??
你在公司的一个工程上面创建了一个TestDemo类,要是你同事也创建一个叫做TestDemo类,就会发现创建不了。
为什么呢??我们试着打开src路径的文件夹看一下里面有什么?
原来是有一个TestDemo的文件,这样我们就明白了为什么不可以了,因为文件夹里面不可以出现2个相同文件的文件夹。
所以为了解决上面的问题,我们就引入了一个 包 这个概念,所以也就是说,在直观来看包就是一个文件夹而已。
我们平时已经使用过包,比如:使用Scanner的时候,必须导入包含Scanner这个类的一个包(如果,没有导入就会报错),类似C语言的头文件。
导入包中的类
上面说到没有导入Scanner的包就会报错,那么我们怎么知道要导入什么包呢?这里提供一下方法
- 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()也要在第一行,所以水火不相容
在第一行不报错
- 为什么呢?? 因为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的关系。猫属动物,动物并不只是猫。
向上转型发生的时机:
- 直接赋值
- 方法的传参
函数的参数是Animal,传过去了一个animal ,传过去的animal还是指向animal所指的对象 new Dog(),这也叫做上向转型。
还可以暴力点:这也是向上转型
- 方法的返回:
为什么不报错呢?? 因为他们是父子类关系
还有一种情况:return的是Dog,使用Animal接收
下一步
静态绑定和动态绑定
什么是绑定?将一个方法调用同一个方法所在的类关联在一起就是绑定,绑定分为静态绑定
和动态绑定
两种:
- 静态绑定:即在编译时,编译器就能准确判断应该调用哪个方法,绑定是在运行前完成,也叫
前期绑定
- 动态绑定:程序在运行期间由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)的区别
重载:
- 方法名相同
- 方法的参数列表不同(数据的个数+数据的类型)
- 返回值不做要求
- 并不一定是在同一个类里面
重写:顾名思义重新写一份
- 方法名相同
- 方法的返回值相同
- 方法的参数列表相同
我们来看看刚刚的eat代码:
Animal类
Dog类
会发现他们的方法名和返回都是一样的,只有访问修饰符我们等等说。
有一个图很形象的表达出来他们的区别:
重写的注意事项:
- 要重写父类的方法,那个方法不可以被final修饰
- 父类方法不可以使用private修饰,子类的访问修饰符权限一定大于等于父类的权限
- 方法不能被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();
Shape shape2 = new Cycle();
Shape shape3 = new Rect();
drawMap(shape1);
drawMap(shape2);
drawMap(shape3);
}
// 打印单个图形
}
在这个代码中, 分割线上方的代码是 类的实现者 编写的, 分割线下方的代码是 类的调用者 编写的
.
当类的调用者在编写 drawMap 这个方法的时候, 参数类型为 Shape (父类), 此时在该方法内部并不知道, 也不关注当前的 shape 引用指向的是哪个类型(哪个子类)的实例. 此时 shape 这个引用调用 draw 方法可能会有多种不同的表现(和 shape 对应的实例相关), 这种行为就称为 多态
使用多态的好处是什么?
1.类调用者对类的使用成本进一步降低.
- 封装是让类的调用者不需要知道类的实现细节
- 多态能让类的调用者连这个类的类型是什么都不必知道, 只需要知道这个对象具有某个方法即可
2 能够降低代码的 "圈复杂度", 避免使用大量的 if - else
例如我们现在需要打印的不是一个形状了, 而是多个形状. 如果不基于多态, 实现代码如下:
public static void drawShapes() {
Rect rect = new Rect();
Cycle cycle = new Cycle();
Flower flower = new Flower();
String[] shapes = {"cycle", "rect", "cycle", "rect", "flower"};
for (String shape : shapes) {
if (shape.equals("cycle")) {
cycle.draw();
} else if (shape.equals("rect")) {
rect.draw();
} else if (shape.equals("flower")) {
flower.draw();
}
}
}
如果使用使用多态, 则不必写这么多的 if - else 分支语句, 代码更简单
public static void drawShapes() {
// 我们创建了一个 Shape 对象的数组.
Shape[] shapes = {new Cycle(), new Rect(), new Cycle(), //都是继承了shapes
new Rect(), new Flower()};
for (Shape shape : shapes) {
shape.draw();
}
}
什么叫 "圈复杂度" ?
圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解. 而如果有很
多的条件分支或者循环语句, 就认为理解起来更复杂.
因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 "圈复杂度". 如果一
个方法的圈复杂度太高, 就需要考虑重构.
不同公司对于代码的圈复杂度的规范不一样. 一般不会超过 10
3) 可扩展能力更强.
class Triangle extends Shape {
@Override
public void draw() {
System.out.println("△");
}
}
对于类的调用者来说(drawShapes方法), 只要创建一个新类的实例就可以了, 改动成本很低.
而对于不用多态的情况, 就要把 drawShapes 中的 if - else 进行一定的修改, 改动成本更高
四、抽象类
语法规则
在刚才的打印图形例子中, 我们发现, 父类 Shape 中的 draw 方法好像并没有什么实际工作, 主要的绘制图形都是由Shape 的各种子类的 draw 方法来完成的. 像这种没有实际工作的方法, 我们可以把它设计成一个 抽象方法(abstractmethod), 包含抽象方法的类我们称为 抽象类(abstract class)
语法规则
abstract class Shape {
abstract public void draw();
}
- 在 draw 方法前加上 abstract 关键字, 表示这是一个抽象方法. 同时抽象方法没有方法体(没有 { }, 不能执行具体代码
- 对于包含抽象方法的类, 必须加上 abstract 关键字表示这是一个抽象类.
#### 注意事项
- 抽象类不能直接实例化
- 抽象方法不能是 private 和final
final
- 抽象类中可以包含其他的非抽象方法, 也可以包含字段. 这个非抽象方法和普通方法的规则都是一样的, 可以被重写,也可以被子类直接调用
如果想要不重写方法 可以使用abstract
但是如果还有一个B继承了Reat,那么这个还是要重写第一个抽象类的方法
抽象类的作用
上面说到抽象类不可以被继承,那么它还有什么作用呢?
抽象类存在的最大意义就是为了被继承.
抽象类本身不能被实例化, 要想使用, 只能创建该抽象类的子类. 然后让子类重写抽象类中的抽象方法.
有些同学可能会说了, 普通的类也可以被继承呀, 普通的方法也可以被重写呀, 为啥非得用抽象类和抽象方法呢?
确实如此. 但是使用抽象类相当于多了一重编译器的校验.
使用抽象类的场景就如上面的代码, 实际工作不应该由父类完成, 而应由子类完成. 那么此时如果不小心误用成父类了,
使用普通类编译器是不会报错的. 但是父类是抽象类就会在实例化的时候提示错误, 让我们尽早发现问题.
很多语法存在的意义都是为了 "预防出错", 例如我们曾经用过的 final 也是类似. 创建的变量用户不去修改, 不就
相当于常量嘛? 但是加上 final 能够在不小心误修改的时候, 让编译器及时提醒我们.
充分利用编译器的校验, 在实际开发中是非常有意义的.
五、接口
接口是抽象类的更进一步. 抽象类中还可以包含非抽象方法, 和字段. 而接口中包含的方法都是抽象方法, 字段只能包含
静态常量.
语法规则及语法实现
- 使用 interface定义一个接口(一般定义接口使用 I 开头)
- 接口里的方法 不能有普通方法,方法一定是抽象方法
- 普通方法
- 接口当中方法默认是public abstract的(去掉没关系)
- 在JDK1.8中,接口可以有普通方法,但前提是要被default修饰
5.接口当中的变量
,默认是public static final 修饰 (可以去掉)
- 接口同样不可以进行实例化
- Cycle 使用 implements 继承接口. 此时表达的含义不再是 "扩展", 而是 "实现",改类需要实现我们接口当中的抽象方法
扩展(extends) vs 实现(implements)
扩展指的是当前已经有一定的功能了, 进一步扩充功能.
实现指的是当前啥都没有, 需要从头构造出来
- 在调用的时候同样可以创建一个接口的引用, 对应到一个子类的实例
- 一个类可以使用implements 实现多个接口,每个接口之间使用逗号分割开。(需要重写)
为什么可以实现多个接口?
因为我们Java 不可以多继承。
- 接口和接口之间的关系,使用extends关键字来维护。意为:扩展。该接口扩展了其他接口的功能
这个时候要是有其他类来implementsD,需要重写D,A,B的方法(这个时候E的功能就很强大)
- 实现多个接口
有的时候我们需要让一个类同时继承自多个父类. 这件事情在有些编程语言通过 多继承 的方式来实现的.
然而 Java 中只支持单继承, 一个类只能 extends 一个父类. 但是可以同时实现多个接口, 也能达到多继承类似的效果.
现在我们通过类来表示一组动物
另外我们再提供一组接口, 分别表示 "会飞的", "会跑的", "会游泳的"
接下来我们创建一个具体的动物
猫, 是会跑的. (注意先继承后实现接口)
青蛙, 既能跑, 又能游(两栖动物)
上面的代码展示了 Java 面向对象编程中最常见的用法: 一个类继承一个父类, 同时实现多种接口
猫是一种动物, 具有会跑的特性.
青蛙也是一种动物, 既能跑, 也能游泳
为什么要搞这么多接口呢?不可以全部放在Animal类里面吗??
答:因为动物不一定都会飞,不一定多会游泳,我们可以继承他们都有的特征,具体一下行为可以使用接口来实现,而且接口,就是给我们达到多继承的效果的。
这样设计有什么好处呢? 时刻牢记多态的好处, 让程序猿忘记类型. 有了接口之后, 类的使用者就不必关注具体类型, 而
只关注某个类是否具备某种能力
比如:只要实现了我的接口 都可以发生向上转型,都可以发生动态绑定,都可以发生多态
接口使用实例
comparable 接口
刚才的例子比较抽象, 我们再来一个更能实际的例子.
给对象数组排序
给定一个学生类
想对这个数组进行排序
按以前的方法能不能直接使用sort排序呢??
当然是不可以的,仔细思考, 不难发现, 和普通的整数不一样, 两个整数是可以直接比较的, 大小关系明确. 而两个学生对象的大小关系怎么确定?
那么comparable是什么东西呢?根据一路找下来,我们发现这个是个接口
但是我们当前的student类和这个comparable没有关系,一想到这个是个接口,那么我们是不是可以把这个student类实现那个接口呢?
只需重写方法就OK了
那么我们这里写什么呢??写一个比较规则
有人会问这个是什么东西啊??
我们可以换一个思路写:
然后去看一下底层:大于0发生交换
那么我们在来看,如果他们this的大,那么一定是大于0的数字,(不一定是1),如果小肯定是个负数,
所以来看一下结果:按照年龄就排序了
反过来就是降序
所以这个comparable接口有什么用,其实就是对于 sort 方法来说, 需要传入的数组的每个对象都是 "可比较" 的, 需要具备 compareTo 这样的能力. 通过重写 compareTo 方法的方式, 就可以定义比较规则.(让类也有可比较的能力)
让哪一个类具备了这样的能力(可比较性)转换的都是student类
为什么可以转换呢? 还是因为实现了接口,就有了可以向上转型,所以不会报错
上面的列子就说明了,一旦一个类实现了接口就具备了某种能力
那么这个接口有什么不好的地方呢??
这样想,要是我们不想按照年龄排序,需要按照名字排序呢?那么我们的规则就需要修改一下:
ε=(´ο`*)))唉,大家就要说了 为什么不是减 而是compareTo了,第一因为name是字符串类型,第二我们点进去看看,还是len1-len2
现在来运行一下:根据姓名排了
那我们想分数排序:修改规则
运行结果:
大家发现了没有,这玩意一点不灵活啊,老是要修改规则 麻烦死了
对类的侵入性比较强(每次如果更换了比较方式,那么都需要修改类的内部)
为了解决comparable接口的缺陷 我们有一个新的接口 Comparator接口
Comparator接口
现在我们来这个接口来比较,假如我们想使用age来比较,我们来写一个类,让这个类实现Comparator
怎么用呢??
把刚刚那个类实例化,然后当做sort参数,对于sort理解比较深,其实平时也就是使用一个参数,或者两个参数
我们来看一下sort的源码:是一个接口类型的参数
那么我们传过去一个实现的接口是不是也可以
运行结果:
好了,现在我们换一个场景,使用name排序,那么也是一样的写一个类:
main
结果:
我们一般把Comparator叫做比较器
那么他们的区别在哪里呢?
- Comparable重写的是compareTo
- Comparator重写的是compare
- Comparator对类的侵入性较弱
Clonable 接口
Java 中内置了一些很有用的接口, Clonable 就是其中之一.
Object 类中存在一个 clone 方法, 调用这个方法可以创建一个对象的 "拷贝". 但是要想合法调用 clone 方法, 必须要先
实现 Clonable 接口, 否则就会抛出 CloneNotSupportedException 异常
Clonable接口里面什么都没有(空接口)
这里存在一个面试问题:这个空接口有什么用??
空接口有个术语叫做(标记接口),实现的类证明可以被克隆的
重写克隆方法代码如下:
main:
运行结果:
我们可以打印一下他们的地址:确认是2个不一样的
克隆的深拷贝和浅拷贝
首先我们先来看一段代码:
Main函数:
输出结果:
我们来分析一下main的内存空间样子,为什么不会影响person,因为下面是2个空间使用并不会影响到
接下来我们来修改一下代码:
main方法:一步一步来
先看这个内存图:
如果我修改了12.5 是不是person 和 person2都被修改了?(因为克隆只是克隆了蓝框框 它们里面所指money 还是那一个)
main函数:
运行结果:
但是我们写要的跟刚刚一样:改为person2的money值 不影响person1的值。(现在这个是浅拷贝)目的:达到深拷贝
我们有个思路:把money也克隆一份 变成黄色的部分那样 让一个另外一个person指向黄色部分,这样修改黑色部分就不会受影响了,这样也达到了我们深拷贝的效果
其实最终结果就是克隆对象里面的对象;
那么我们怎么做呢??
money要克隆,我们让它实现克隆接口 重写克隆方法
在person类里面写
看数字1 和 2 ,对应的就是上面的一二行代码
运行结果:
总结:抽象类和接口都是 Java 中多态的常见使用方式. 都需要重点掌握. 同时又要认清两者的区别
核心区别: 抽象类中可以包含普通方法和普通字段, 这样的普通方法和字段可以被子类直接使用(不必重写), 而接口中不
能包含普通方法, 子类必须重写所有的抽象方法.
以上就是全部内容了如果有帮助可以点个大拇指,收藏一下