目录
一、抽象类和抽象方法
1.1 概念
1. 抽象类:
① 如果一个类没有足够的信息来描述一个对象(这个类通常是父类),那么这个类可以用abstract修饰成为抽象类。
②抽象类中可以正常像普通类一样去定义成员变量/方法、构造方法等,也可以发生多态。
③ 抽象类和普通类的区别在于:类的前面多了个修饰符号abstract;抽象类无法示例化具体的对象,只能被继承。(此点也说明了,一个类不能同时被abstrct 和 final同时修饰)
④ 继承抽象类的子类A必须要重写抽象类中的抽象方法,如果不重写,得将子类A也得写成抽象类,那么当子类B继承子类A后就得重写子类A的父类和子类A中所有没被重写的方法(出来混总是要还的)。
2. 抽象方法:
① 被abstract修饰的方法称为抽象方法,抽象方法可以不具体实现,如果具体实现了反而会报错 (父类中的方法通常被abstract修饰成抽象方法)。
② 如果一个类中有抽象方法,则这个类必须是抽象类,但如果一个类是抽象类,类中不一定有抽象方法。
③ 抽象方法不能被private、static、final修饰,且不能是构造方法,子类重写的抽象方法的访问修饰符一定大于抽象类中对应抽象方法的访问修饰符的权限 (因为抽象方法要满足能被重写的条件)
/*抽象方法*/
abstract class Father {
/*抽象类*/
public abstract void funcA();
}
/*抽象方法*/
abstract class Son extends Father {
/*抽象类*/
public abstract void funcB();
}
class GrandSon1 extends Son {
/*重写Father类中的抽象方法*/
@Override
public void funcA() {
System.out.println("我是爷爷的第一个孙子");
}
/*重写Son类中的抽象方法*/
@Override
public void funcB() {
System.out.println("我是爸爸的第一个儿子");
}
}
class GrandSon2 extends Son {
/*重写Father类中的抽象方法*/
@Override
public void funcA() {
System.out.println("我是爷爷的第二个孙子");
}
/*重写Son类中的抽象方法*/
@Override
public void funcB() {
System.out.println("我是爸爸的第二个儿子");
}
}
public class Test01 {
public static void funcDemo1(Father father) {//向上转型成Father类,只能调用Father类有的成员
father.funcA();
}
public static void funcDemo2(Son son) {//向上转型成Son类,由于Son类继承了Father类,
//此时能调用Father类和Son类有的成员,
//所以无论在抽象类有没有继承抽象类的情况下
//记住向上转型成最后一个抽象类即可
son.funcA();
son.funcB();
}
public static void main(String[] args) {
GrandSon1 grandSon1 = new GrandSon1();
GrandSon2 grandSon2 = new GrandSon2();
//Test01.funcDemo1(grandSon1);
//Test01.funcDemo1(grandSon2);
Test01.funcDemo2(grandSon1);
Test01.funcDemo2(grandSon2);
}
}
1.2 作用
1. 抽象类本身不能被实例化,要想使用只能创建该抽象类的子类,然后让子类重写抽象类中的抽象方法。有些人可能会说了, 普通的类也可以被继承呀, 普通的方法也可以被重写呀, 为啥非得用抽象类和抽象方法呢? 确实如此,但是使用抽象类相当于多了一重编译器的校验。使用抽象类的场景就如上面的代码, 实际工作不应该由父类的方法完成, 而应由子类重写的方法完成,那么如果不小心忘记在子类中重写父类中的方法了,在方法调用的过程中父类使用普通类编译器是不会报错的,但是父类是抽象类就会在子类继承父类后提示需要重写父类中的方法, 让我们尽早发现代码中的逻辑问题。
2. 很多语法存在的意义都是为了 "预防出错",例如我们曾经用过的 final 也是类似。创建的变量用户不去修改, 不就相当于常量嘛? 但是加上 final 能够在不小心误修改的时候, 让编译器及时提醒我们。充分利用编译器的校验, 在实际开发中是非常有意义的。
二、接口
2.1 概念及定义
概念:接口(英文:Interface),在JAVA编程语言中是一个抽象引用数据类型,是抽象方法的集合,接口用interface关键字来定义,和类一样,接口也会生成一个独立的字节码文件。
定义:
① 定义接口要使用interface关键字(接口并不是类)。
② 接口的命名一般以大写字母I开头,并且命名一般使用形容词性的单词。
③ 接口中的成员可以有成员变量和成员方法,但成员变量必须是public static final修饰的,成员方法必须是public abstract修饰的(也就是说,接口中的成员变量必须是公开可以使用的常量,常量名一般全大写;接口中的成员方法必须是抽象方法,不要写入具体的实现;例外:接口中用default、static修饰的方法是可以有具体的实现的),阿里编码规范中约定,接口中的方法和属性不要加任何的修饰符号,因此我们在写接口中的成员时可以不写前面的修饰,编译器会自动修饰。
④ 接口前面不要用abstract修饰,因为接口本身比抽象类更抽象,也因此接口也不能实例化具体的对象,只能被其他的类去实现。
⑤ 类实现接口的格式:关键字implements
2.2 特性
① 抽象类和接口都不能实例化对象,抽象类用来被类继承,接口用来被类实现。
② 当一个类实现一个接口时意味着要在这个类中重写接口中所有抽象方法,如果在这个类中不想重写,则这个类必须定义成抽象类,继承这个抽象类的类得重写接口和这个抽象类中的所有抽象方法(和抽象类一样)。
③ 接口也可以发生向上转型,动态绑定,多态(动态绑定是原理,多态是思想)。
④ 接口定义好后也会有一个单独的Java文件,编译完成后生成的字节码文件的后缀也是(.class)。
⑤ 接口中不能有静态代码块,构造代码块,构造方法等(这一点是和抽象类有区别的)。
⑥ 重写接口中的方法时,方法只能是public修饰的(因为重写方法的权限要大于等于重写前的权限,而接口中的成员方法默认是public的)。
⑦ 接口中被 default 或 static 修饰的方法要写清楚具体实现的语句,且被实现的接口中default成员方法可以不重写,也可以被重写,它并不会像接口中的抽象方法一样强制要重写;被static修饰的方法不具备重写的条件,所以不能被重写,通常使用它时,用接口名.即可。
2.3 实例:笔记本电脑
实现笔记本电脑使用USB鼠标、USB键盘的例子
1. USB接口:包含打开设备、关闭设备功能
2. 笔记本类:包含开机功能、关机功能、使用USB设备功能
3. 鼠标类:实现USB接口,并具备点击功能
4. 键盘类:实现USB接口,并具备输入功能
2.4 一个类可以实现多个接口
1. 在Java中,类和类之间只能是单继承的,也就是说一个类只能有一个父类,但一个类可以实现多个接口,从而间接达到实现多继承的目的。
实现格式:修饰符 class 类名 + extends + 类名 + implements + 接口名1,接口名2... {}
注意:继承一定写在接口实现的前面。
2. 一个类实现多个接口时,每个接口中的抽象方法都要实现,否则该类必须设置为抽象类。
3. 例如:根据我们当前掌握的知识当我们想要描述狗、鱼、鸭这三种动物时,我们会定义狗、鱼、鸭这三个类,并且由于它们之间共属于动物,我们可以定义一个父类Animal,从而达到代码复用的效果。但是在定义的过程中我们会发现一个问题,游泳、跑步、飞行这三个行为,如果我们把它们定义在父类Animal中并不稳妥,因为继承是对子类共性的抽取,并不是所有的动物都同时具备这三个行为;如果我们把它们定义在狗、鱼、鸭的各自类中,这又会造成代码冗余;如果我们把它们定义在一个新的类A中,当我们定义狗、鱼、鸭类时又会发现类是不支持多继承的,继承了Animal类就不能再继承A类了。在这种场景下,我们把游泳、跑步、飞行这三个行为定义在一个接口中最为合适,这样子类既可以继承Animal类也可以根据需要拥有游泳/跑步/飞行这三个行为,而且不会造成代码冗余。于是我们可以写下如下图代码,该代码展示了 Java 面向对象编程中最常见的用法:一个类继承一个父类, 同时实现了多个接口。
4. 使用接口的好处:让程序员忘记类型。例如,下图中用绿色方框框起来的方法,参数部分采用了向上转型传参数的方式,在调用该方法传参时程序员可以不用考虑传入的参数是什么类,只需要关心传入的参数的类中是否实现了对应的接口。
2.5 一个接口可以继承多个接口
1. 一个接口可以继承多个接口,从而达到复用的效果,关键字也是extends。
2. 接口的继承相当于把多个接口合并在一起。
3. 下图代码中接口C继承了接口A、B,实现接口C的TestNew类要重写接口A、接口B、接口C中所有的抽象方法,否则TestNew类得定义成抽象类。
4. 总之,如果你不重写抽象类/接口中得抽象方法,则继承抽象类/接口的类必须得写成抽象类。
2.6 Comparable<>接口
0. 实现了某个接口相当于该类具备了某种能力。
1. 关系运算符只能比较基本数据类型数据的大小,引用数据类型的数据比较大小要用到Comparable<>泛型接口中的compareTo方法,<>里面写上要比较的类类型。
2. 具体做法:为将要比较大小的引用数据类型的对象它对应的类实现Comparable<>泛型接口,在类中重写接口中的compareTo方法,compareTo方法的内部自定义根据类的哪一种成员变量进行比较。我们在比较该引用数据类型的对象的大小时,直接用对象的引用调用compareTo方法并传参即可,举例代码如下图所示。
3. Arrays.sort方法在排序装有引用数据类型的数组时,会先将数组中的元素强制转换成Comparable<>泛型类型,然后调用compareTo方法比较两个相邻数据的大小,最后根据比较的结果对数组中相邻两个元素进行位置调整。
这也就是说使用Arrays.sort方法对装有引用数据类型数据的数组进行排序时,也得先让对应引用数据类型的类实现Comparable<>泛型接口,并在类中重写了compareTo方法后才能正常进行排序,且值得注意的是,也就是说Arrays.sort排序的逻辑与compareTo内部实现比较大小的依据有着极大的关联,当compareTo内部根据年龄、姓名等比较大小时可能会造成不同的排序结果。
4. 使用Comparable<>泛型接口中的compareTo方法来比较两个引用数据类型的大小的局限性在于,一旦把compareTo中的内容写下了之后,以后在比较该引用数据类型对象的大小时调用compareTo方法只能按照compareTo方法中依据的东西判断大小了。
5. 写一个自己的sort方法,实现和Arrays.sort方法一样的功能(采用冒泡排序):
补充说明:mySort的形参部分之所以那样写是因为可以限制只有实现了Comparable<>泛型接口的类才能成功传参 (如果一个类没有继承/实现某个类/抽象类/接口,那么它们之间是没办法相互转换的,强制转换也不行)
6. 关于上图红色方框中mySort的形参部分,写成 Comparable<Student>[] comparables也行,但后面的if语句部分会报错,因为重写的toString方法的参数部分要求是Student类型的,但你传了Comparable<Student>类型的,Student类实现了Comparable,它们两者是父子级的关系,此时相当于你将父类赋值给了子类,是因为没有强制类型转换而报的错,修改代码如下:
2.7 Comparator<>接口
1. 针对Comparable接口的局限性,在比较某个类实例化的两个对象的大小时,我们可以采用实现Comparator接口对里面的compare方法进行重写,而不去采用实现Comparable接口。
2. 具体做法(又称定义比较器进行比较):重新定义新的类(A)去实现Comparator<>泛型接口,并重写compare方法,<>里面写上要比较的类类型,使用时直接new一个A类的对象a,使用a调用compare方法并传参即可;我们可以按照这种方式写多个比较器来达到目的;如果Comparator接口由比较对象的类实现,则会达不到想要的效果,如下图二所示。
3. 注意:实现Comparator接口时需要导包。
3. 我们在调用Arrays.sort方法时也可以将比较器作为参数传递给sort方法,使得sort方法在排序装有引用数据类型的数组时,比较相邻的两个元素的大小采用比较器中的compare方法,而不再采用Comparable中的compareTo方法。
2.8 Cloneable接口
1. 使用场景:想要克隆某个对象时。
2. 实现步骤:
① 给该对象对应的类实现Cloneable接口,但并不用重写Cloneable中的接口,因为这个接口里面并没有抽象方法,此时这个接口叫做空接口/标记接口,这种接口的意义在于只有实现了该接口才能证明你有该功能,除此之外没有什么特殊的意义。
注意:Cloneable接口不是泛型接口。
② 调用Object类中的clone方法。但我们不能直接调用,因为Object类中的clone方法被protected修饰了,此时是不能在不同包的非子类中使用的,
于是我们可以先在子类中重写Object的克隆方法。重写的内容:在这个重写的克隆方法内部用super调用Object类的clone方法,让其返回值直接返回原来克隆方法的返回值,因为我们调用克隆方法的目的本来就是得到克隆后对象的地址,在不同包的子类中是可以调用Object的clone方法的。
-》补充:在子类中快速重写Object类中方法的快捷方式
3. 由于clone方法的返回值是Object类型的,因此在接收clone方法的返回值时,要强制类型转换一下(Object类是所有类的父类,这里要发生向下转型)。
4. 当上面的步骤都做好后,代码运行时还会报错,此时是由于异常原因(重写的clone方法在参数列表后用throws关键字抛出了异常,在调用该方法的方法里面要对异常进行处理),此处我们的解决的方法可以是,在main方法参数列表的后面也加上clone方法参数列表后的“throws CloneNotSupportedException”即可,此时该异常会交给JVM去处理。
-》下图是示例代码:
-》上图代码在内存中的示意图:
2.9 浅拷贝和深拷贝
1. 浅拷贝:修改上文Cloneable接口中的代码,即给Student类增加一个成员变量(该成员变量是Money类创建的一个对象),并保持其他的代码不变。代码和代码在内存中的示意图如下图所示。
我们会发现一个问题,克隆出的student2中成员变量m指向的对象和student1中成员变量m指向的对象一样,当我们在修改student1/student2中m对象的money值时,student1和student2中m对象的money值都会变,这种现象被称为浅拷贝(并没有实现真正的拷贝效果)。
2. 深拷贝:为了实现真正的拷贝效果,我们可以修改上述代码中重写的clone方法来达到目的。具体操作是,在Student类重写的clone方法中先调用Object类中的clone方法,克隆一下this所指向的对象,并让临时的Student类对象tmp来接收,然后为Money类实现Cloneable接口并重写clone方法,接着克隆一下this中的m对象,并让tmp中的m接收,最后返回tmp即可达到真正的拷贝效果,代码和代码在内存中的分析如下图所示。
2.10 抽象类和接口的区别
1. 抽象类和接口都是 Java 中多态的常见使用方式。
2. 核心区别: 抽象类中可以包含普通方法和普通字段, 这样的普通方法和字段可以被子类直接使用(不必重写), 而接口中 不能包含普通方法, 子类必须重写所有的抽象方法。
3. 再次提醒: 抽象类存在的意义是为了让编译器更好的校验, 像 Animal 这样的类我们并不会直接使用, 而是使用它的子类. 万一不小心创建了 Animal 的实例, 编译器会及时提醒我们。
三、Object类
3.1 概念
1. Object类是所有类的父类。
2. 对于整个Object类中的方法需要实现全部掌握,本小节当中,我们主要来熟悉这几个方法:toString()方法,equals()方法,hashcode()方法。
3. 在ideal中如何看到Object类中所有的方法?
做法:双击shift,在出现的对话框中勾选Include non-project items,并在搜索框中搜索Object,点击第一个Object,然后在ideal页面的左下角点击Structure即可看到Object类中的所有的方法。
3.2 获取对象信息toString方法
如果要打印对象的信息,可以通过重写Object类中的toString()方法,在打印时:对于单个对象直接用sout打印对象的名称即可,对于装有多个对象的数组来说用sout打印Arrays.toString的返回值即可,具体示例代码如下图所示。
3.3 对象比较equals方法
1. Object类中的equals方法的功能是比较两个引用所指向的对象是否为同一个对象,如果是就返回true,否就返回false,并且Object类中的equals方法是用public修饰的哦~。
2. 也就是说当我们想比较两个对象中的内容是否相同时得重写Object类中的equals方法。并且ideal提供了快捷方式,具体做法:单机右键,Generate,点击equals() and hashCode(),然后一路next。
3.4 hashcode方法
1. toString方法的源码中有一个hashCode()方法,它可以帮我们算一个具体的对象位置(我们可以暂时理解为内存地址)。Integer.toHexString()方法,可以将这个地址以16进制输出。
2. hashCode()方法是一个native方法,底层是由C/C++代码写的,所以我们看不到。
3. 如果在某个业务场景中我们需要实现,如果对象的成员变量都一致,我们就认为是同一个人的话,我们可以通过重写hashCode方法来达到目的。ideal提供了快捷方式,具体做法:单机右键,Generate,点击equals() and hashCode(),然后一路next。
4. 重写hashCode前:
重写hashCode后:
本篇文章已完结,谢谢支持哟 ^^ !!!