面向对象编程-2
- 一.抽象类
-
- 1.什么是抽象类
- 2.语法规则
- 3.抽象类的作用
- 二.接口
-
- 1.什么是接口
- 2.语法规则
- 3.实现多个接口
- 4.接口之间的继承
- 三.接口的使用实例
-
- 1. Comparable 接口
- 2.Comparator 接口
- 3.Clonable 接口
- 四.总结
- 五.写在最后
一.抽象类 1.什么是抽象类 首先我们来回顾一下上一篇文章提到的一个例子:打印图形
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("?");
}
}
//---------------------------------------public class Test {
public static void main(String[] args) {
Shape shape1 = new Flower();
Shape shape2 = new Cycle();
Shape shape3 = new Rect();
drawMap(shape1);
drawMap(shape2);
drawMap(shape3);
}
// 打印单个图形
public static void drawShape(Shape shape) {
shape.draw();
}
}
我们发现, 父类
Shape
中的 draw 方法
好像并没有什么实际工作,主要的绘制图形都是由 Shape 的各种子类的 draw 方法来完成的。像这种
没有实际工作
的方法, 我们可以把它设计成一个 抽象方法(abstractmethod)
,包含抽象方法的类我们称为 抽象类(abstract class)
2.语法规则 那么,抽象类到底怎么写呢?请看代码:
abstract class Shape {
abstract public void draw();
}
- 在 draw 方法前加上
abstract
关键字, 表示这是一个抽象方法。 同时抽象方法没有方法体
(没有 { },不能执行具体代码) - 对于包含抽象方法的类, 必须加上 abstract 关键字表示这是一个抽象类
- 抽象类不能直接实例化:
Shape shape = new Shape();
// 编译出错
Error:(30, 23) java: Shape是抽象的;
无法实例化
- 抽象方法不能是 private 的:
abstract class Shape {
abstract private void draw();
}
// 编译出错
Error:(4, 27) java: 非法的修饰符组合: abstract和private
- 抽象类中可以包含其他的非抽象方法,也可以包含字段。这个非抽象方法和
普通方法
的规则都是一样的,可以被重写,也可以被子类直接调用:
abstract class Shape {
abstract public void draw();
void func() {
System.out.println("func");
}
}
class Rect extends Shape {
}
public class Test {
public static void main(String[] args) {
Shape shape = new Rect();
shape.func();
}
}
// 执行结果
func
3.抽象类的作用 抽象类存在的最大意义就是为了
被继承
抽象类本身
不能被实例化
,要想使用,只能创建该抽象类的子类,然后让子类重写
抽象类中的抽象方法。那大家可能有一个疑问,普通的类也可以被继承, 普通的方法也可以被重写呀,为啥非得用抽象类和抽象方法呢?确实如此,但是使用抽象类相当于多了一重
编译器的校验
:使用抽象类的场景就如上面的代码, 实际工作不应该由父类完成, 而应由子类完成。
那么此时如果不小心误用成父类了,使用普通类编译器是不会报错的。 但是父类是抽象类就会在实例化的时候提示错误,让我们尽早发现问题。
很多语法存在的意义都是为了 “二.接口 1.什么是接口 接口是预防出错
”,例如我们曾经用过的 final 也是类似。 创建的变量用户不去修改, 不就相当于常量嘛? 但是加上 final 能够在不小心误修改的时候,让编译器及时提醒我们。
充分利用编译器的校验, 在实际开发中是非常有意义的。
抽象类
的更进一步。抽象类中还可以包含 非抽象方法 和字段。而接口中包含的方法都是抽象方法
, 字段只能包含静态常量
2.语法规则 在刚才的打印图形的示例中,我们的父类 Shape 并没有包含别的非抽象方法,也可以设计成一个接口:
interface IShape {
void draw();
}
class Cycle implements IShape {
@Override
public void draw() {
System.out.println("○");
}
}
public class Test {
public static void main(String[] args) {
IShape shape = new Rect();
shape.draw();
}
}
- 使用
interface
定义一个接口 - 接口中的方法一定是
抽象方法
,因此可以省略 abstract - 接口中的方法一定是
public
,因此可以省略 public - Cycle 使用
implements
继承接口。此时表达的含义不再是 “扩展”, 而是 “实现” - 在调用的时候同样可以创建一个
接口的引用
,对应到一个子类的实例 - 接口不能
单独
被实例化 - 从
jdk1.8
开始,接口中的普通方法可以有具体实现,但这个方法必须是default
修饰的。
- 扩展指的是当前已经有一定的功能了,进一步
扩充功能
- 实现指的是当前啥都没有,需要
从头构造
出来
接口中只能包含
抽象方法
。 对于字段来说, 接口中只能包含静态常量
(final static):interface IShape {
void draw();
public static final int num = 10;
}
其中的 public, static, final 的关键字都可以省略.省略后的 num 仍然表示 public 的静态常量总结:
- 我们创建接口的时候, 接口的命名一般以
大写字母 I
开头 - 接口的命名一般使用 “
形容词
” 词性的单词 - 阿里编码规范中约定,接口中的方法和属性不要加任何
修饰符号
,保持代码的简洁性
interface IShape {
abstract void draw() ;
// 即便不写public,也是public
}
class Rect implements IShape {
void draw() {
System.out.println("□") ;
//权限更加严格了,所以无法重写
}
}
文章图片
3.实现多个接口 有的时候我们需要让一个类同时继承多个父类。这件事情在有些编程语言通过
多继承
的方式来实现的。然而 Java 中只支持
单继承
, 一个类只能 extends 一个父类。但是可以同时实现多个接口
,也能达到多继承类似的效果。现在我们通过类来表示一组动物:
class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
}
另外我们再提供一组接口,分别表示 “会飞的” “会跑的” “会游泳的” :
interface IFlying {
void fly();
}
interface IRunning {
void run();
}
interface ISwimming {
void swim();
}
接下来我们创建几个具体的动物
猫,是会跑的 :
class Cat extends Animal implements IRunning {
public Cat(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.name + "正在用四条腿跑");
}
}
鱼,是会游的 :
class Fish extends Animal implements ISwimming {
public Fish(String name) {
super(name);
}
@Override
public void swim() {
System.out.println(this.name + "正在用尾巴游泳");
}
}
【Java|Java-抽象类与接口】青蛙,既能跑,又能游 :
class Frog extends Animal implements IRunning, ISwimming {
public Frog(String name) {
super(name);
}
@Override
public void run() {
System.out.println(this.name + "正在往前跳");
}
@Override
public void swim() {
System.out.println(this.name + "正在蹬腿游泳");
}
}
PS : IDEA 中使用还有一种神奇的动物,水陆空三栖,叫做 “鸭子” :ctrl + i
快速实现接口
class Duck extends Animal implements IRunning, ISwimming, IFlying {
public Duck(String name) {
super(name);
}
@Override
public void fly() {
System.out.println(this.name + "正在用翅膀飞");
}
@Override
public void run() {
System.out.println(this.name + "正在用两条腿跑");
}
@Override
public void swim() {
System.out.println(this.name + "正在漂在水上");
}
}
上面的代码展示了 Java 面向对象编程中最常见的用法 : 一个类继承一个父类,同时实现多种接口
继承表达的含义是
is - a
语义,而接口表达的含义是 具有 xxx 特性
猫是一种动物,具有会跑的特性这样设计有什么好处呢?
青蛙也是一种动物,既能跑,也能游泳
鸭子也是一种动物, 既能跑, 也能游,还能飞
时刻牢记多态的好处,让我们忘记类型.有了接口之后,类的使用者就不必关注
具体类型
, 而只关注某个类是否具备某种能力
例如, 现在实现一个方法, 叫 “散步”:
public static void walk(IRunning running) {
System.out.println("我带着伙伴去散步");
running.run();
}
在这个 walk 方法内部,我们并不关注到底是哪种动物,只要参数是
会跑的
, 就行:Cat cat = new Cat("小猫");
walk(cat);
Frog frog = new Frog("小青蛙");
walk(frog);
// 执行结果
我带着伙伴去散步
小猫正在用四条腿跑
我带着伙伴去散步
小青蛙正在往前跳
甚至参数可以不是 “动物”,只要会跑!
class Robot implements IRunning {
private String name;
public Robot(String name) {
this.name = name;
}
@Override
public void run() {
System.out.println(this.name + "正在用轮子跑");
}
}
Robot robot = new Robot("机器人");
walk(robot);
// 执行结果
机器人正在用轮子跑
4.接口之间的继承 接口可以继承一个接口,达到复用的效果.使用
extends
关键字:interface IRunning {
void run();
}
interface ISwimming {
void swim();
}
// 两栖的动物, 既能跑, 也能游
interface IAmphibious extends IRunning, ISwimming { }
class Frog implements IAmphibious {
}
通过接口继承创建一个新的接口 IAmphibious 表示 “两栖的”
此时实现接口创建的 Frog 类, 就继续要实现 run 方法,也需要实现 swim 方法,接口间的继承相当于把多个接口
合并
在一起文章图片
三.接口的使用实例 1. Comparable 接口 刚才的例子比较抽象, 我们再来一个更能实际的例子,给对象数组排序 :
给定一个学生类
class Student {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
} @Override
public String toString() {
return "[" + this.name + ":" + this.score + "]";
}
}
再给定一个学生对象数组, 对这个对象数组中的元素进行排序(按分数降序):
Student[] students = new Student[] {
new Student("张三", 95),
new Student("李四", 96),
new Student("王五", 97),
new Student("赵六", 92),
};
按照我们之前的理解, 数组我们有一个现成的 sort 方法,我们来试试能否直接用sort方法进行排序:
文章图片
仔细思考, 不难发现学生和普通的整数不一样, 两个整数是可以
直接比较
的, 大小关系明确. 而两个学生对象的大小关系怎么确定? 需要我们额外指定
让我们的 Student 类实现
Comparable
接口, 并实现其中的 compareTo 方法:class Student implements Comparable {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
@Override
public String toString() {
return "[" + this.name + ":" + this.score + "]";
}
@Override
public int compareTo(Object o) {
Student s = (Student)o;
if (this.score > s.score) {
return -1;
} else if (this.score < s.score) {
return 1;
} else {
return 0;
}
}
}
在 sort 方法中会自动调用
compareTo
方法. compareTo 的参数是 Object
, 其实传入的就是 Student
类型的对象然后比较当前对象和参数对象的大小关系(按分数来算):
- 如果当前对象应排在参数对象之前, 返回小于 0 的数字
- 如果当前对象应排在参数对象之后, 返回大于 0 的数字
- 如果当前对象和参数对象不分先后, 返回 0
文章图片
这时候结果就符合我们预期了( ̄▽ ̄)*
compareTo其实就是一个比较规则 , 如果我们想
自定义比较类型
的话 , 一定要实现可以比较
的接口 . 但是 , Comparable接口有个很大的缺点 , 那就是对类的侵入性很强 , 所以我们一般不轻易改动2.Comparator 接口 刚才我们提到了Comparable接口对类的侵入性很强 , 那么有没有一个比较灵活的接口供我们使用呢? 答案是肯定的 , 那就是Comparator接口
我们先来写一个用年龄进行比较的比较器:
class AgeComparator implements Comparator{
@Override
public int compare(Student o1,Student o2) {
return o1.age - o2.age;
}
}
再来写一个用姓名进行比较的比较器:
class NameComparator implements Comparator{
@Override
public int compare(Student o1, Student o2) {
return o1.name.compareTo(o2.name);
}
}
这时候,我们实例化这两个比较器,并且在sort方法中传入要
排列的数组
和我们写的比较器对象
:class Student implements Comparable{
public int age;
public String name;
public Student(int age, String name, double score) {
this.age = age;
this.name = name;
}@Override
public String toString() {
return "Student{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
public class Test {public static void main(String[] args) {
Student[] students = new Student[3];
students[0] = new Student(12,"af");
students[1] = new Student(6,"be");
students[2] = new Student(18,"zhangsan");
System.out.println("按年龄排序:");
AgeComparator ageComparator = new AgeComparator();
Arrays.sort(students,ageComparator);
System.out.println(Arrays.toString(students));
System.out.println("---------------------------");
System.out.println("按姓名排序:");
NameComparator nameComparator = new NameComparator();
Arrays.sort(students,nameComparator);
System.out.println(Arrays.toString(students));
}
}
运行结果:
文章图片
所以 Comparator接口 只需要根据自己的需求重新写比较器就 ok 了, 灵活很多, 而不是像Comparable接口直接就写死了
3.Clonable 接口 Java 中内置了一些很有用的接口 ,
Clonable
就是其中之一Object 类中存在一个 clone 方法, 调用这个方法可以创建一个对象的 “
拷贝
”. 但是要想合法调用 clone 方法, 必须要先实现 Clonable 接口
, 否则就会抛出 CloneNotSupportedException
异常我们来看一个例子 :
- 实现Clonable接口
- 别忘了要抛出异常
- 重写Object的clone方法
class Person implements Cloneable{
public int age;
@Override
public String toString() {
return "Person{" +
"age=" + age +
'}';
}@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}public class TestDemo {
public static void main(String[] args) throws CloneNotSupportedException{
Person person = new Person();
person.age = 99;
Person person2 = (Person) person.clone();
System.out.println(person2);
}
}
运行结果:
文章图片
此时内存如下:
文章图片
这时候,我们再来加一个Money类,并且在Person类中实例化它:
class Money implements Cloneable{
public double m = 12.5;
}
}
class Person implements Cloneable{
public int age;
public Money money = new Money();
@Override
public String toString() {
return "Person{" +
"age=" + age +
'}';
}@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
我们在person2中拷贝一份money的值,这时候修改person2中的money,那么person1的money是否改变呢?
public class TestDemo {public static void main(String[] args) throws CloneNotSupportedException{
Person person = new Person();
Person person2 = (Person) person.clone();
System.out.println(person.money.m);
System.out.println(person2.money.m);
System.out.println("-------------------------");
person2.money.m = 13.5;
System.out.println(person.money.m);
System.out.println(person2.money.m);
}
}
答案是不会改变!
文章图片
那么是否说明Clonable接口就是只能实现浅拷贝呢?
答案也是
否
, 决定深浅拷贝的并不是 方法的用途 , 而是代码的实现
!我们来看看此时的内存分布图:
文章图片
要想实现深拷贝,我们拷贝person的时候就要把
person对象里的money
也拷贝一份,让person2的money
指向 新拷贝出来的money
,这时候咱们就实现了深拷贝文章图片
具体的操作实现只需要将Money类
重写clone方法
(方便克隆),然后将Person中的clone方法进行修改 ,将money也进行拷贝即可具体代码如下 :
class Money implements Cloneable{
public double m = 12.5;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class Person implements Cloneable{
public int age;
public Money money = new Money();
@Override
public String toString() {
return "Person{" +
"age=" + age +
'}';
}@Override
protected Object clone() throws CloneNotSupportedException {
Person tmp = (Person) super.clone();
tmp.money = (Money) this.money.clone();
return tmp;
//return super.clone();
}
}
我们来测试一下 :
public class TestDemo {public static void main(String[] args) throws CloneNotSupportedException{
Person person = new Person();
Person person2 = (Person) person.clone();
System.out.println(person.money.m);
System.out.println(person2.money.m);
System.out.println("-------------------------");
person2.money.m = 13.5;
System.out.println(person.money.m);
System.out.println(person2.money.m);
}
文章图片
这样就成功实现了深拷贝 !
四.总结
- 抽象类和接口都是 Java 中
多态
的常见使用方式 - 抽象类中可以包含
普通方法和普通字段
, 这样的普通方法和字段可以被子类直接使用(不必重写), 而接口中不能包含普通方法
, 子类必须重写所有的抽象方法
文章图片
五.写在最后 今天又失眠了,突然想到与其把时间浪费在刷视频打游戏上,不如把时间拿来学习一下提升自己.这篇文章从十二点写到将近四点,总算完成了,希望对大家有所帮助,如有错误,望uu们斧正
寒假也快接近尾声了,祝大家都有所进步,天天开心(?????)
文章图片
推荐阅读
- 面试|面试官(看到你熟练性能调优,可以说一下你对MySQL索引的理解())
- 单细胞测序|经验总结 | R语言批量读取目录下的文件然后按照行名对其进行整合成为data.frame
- 笔记|R语言画图 | 如何看已知基因list的细胞类型特异性表达()
- 拓端tecdat|拓端tecdat|MATLAB用Lasso回归拟合高维数据和交叉验证
- 拓端tecdat|拓端tecdat|Python用Markowitz马克维兹有效边界构建最优投资组合可视化分析四只股票
- 拓端tecdat|拓端tecdat|R语言多变量广义正交GARCH(GO-GARCH)模型对股市高维波动率时间序列拟合预测
- 拓端tecdat|拓端tecdat|R语言样条曲线、分段线性回归模型piecewise regression估计个股beta值分析收益率数据
- 拓端数据tecdat|拓端tecdat|R语言数量生态学冗余分析RDA分析植物多样性物种数据结果可视化
- 拓端tecdat|拓端tecdat|R语言GJR-GARCH和GARCH波动率预测普尔指数时间序列和Mincer Zarnowitz回归、DM检验、JB检验