目录
在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。比如:


在打印图形例子中, 我们发现, 父类 Shape 中的 draw 方法好像并没有什么实际工作, 主要的绘制图形都是由 Shape的各种子类的 draw 方法来完成的. 像这种没有实际工作的方法, 我们可以把它设计成一个 抽象方法(abstract method), 包含抽象方法的类我们称为 抽象类(abstract class).
Shape这个类不可以表述成一个具体的对象, 所以我们可以把它定义成一个抽象类. 而抽象类的定义只需在class关键字前加上abstract即可, 此时这个类就是抽象类.
- abstract class Shape {
- //抽象方法
- public abstract void draw();
- }
并且我们可以看到, Shape中的draw()有没有具体的实现并不重要, 因为子类都重写了该方法, 那么如果不想在Shape中实现它, 给它加上abstract关键字, 这样就可以不写具体的实现了.
注意, 一旦把方法定义成抽象方法, 就必须是在抽象类中.
当然, 在这个类中是可以有其他的字段和方法的.
- abstract class Shape {
- public static int a = 10;
-
- //抽象方法
- public abstract void draw();
-
- public void func() {
-
- }
- }
其实抽象类本身里面的成员和普通类没有区别, 只不过它多了一个抽象方法.
1. 抽象类 使用abstract修饰类
2. 抽象类当中 可以包含普通类所能包含的成员
3. 抽象类和普通类不一样的是, 抽象类当中可以包含抽象方法
4. 抽象方法是使用abstract修饰的。这个方法 没有具体的实现
5. 不能实例化抽象类 new
6. 抽象类存在的最大的意义 就是为了被继承
7. 如果一个普通类 继承了一个抽象类 此时必须重写抽象类当中的方法
运行以下代码, 也能发生动态绑定.
- abstract class Shape {
- //抽象方法
- public abstract void draw();
- }
-
- class Rect extends Shape {
- @Override
- public void draw() {
- System.out.println("矩形!");
- }
- }
-
- class Cycle extends Shape {
- @Override
- public void draw() {
- System.out.println("画圆!");
- }
- }
-
- class Triangle 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 drawMap(Shape shape) {
- shape.draw();
- }
-
- public static void main(String[] args) {
- Rect rect = new Rect();
- drawMap(rect);
- drawMap(new Cycle());
- drawMap(new Triangle());
- drawMap(new Flower());
- }
-
- public static void main1(String[] args) {
- //Shape shape = new Shape(); //err
- Shape shape = new Rect();
- }
- }

8. 如果一个抽象类A 继承了一个抽象类B,此时A当中 不需要重写B中的抽象方法。但是如果A再被普通类继承,就需要重写。
- abstract class A extends Shape {
-
- }
- class B extends A {
- // 必须要重写, 否则报错
- @Override
- public void draw() {
- System.out.println("dsfsasa");
- }
- }
9. 抽象方法 不能是私有的. 也就是要满足重写的规则.
![]()
10. 抽象方法不能被final和static修饰, 因为抽象方法要被子类重写
![]()
11. 抽象类当中 可以有构造方法。为了方便子类能够调用,来初始化抽象类当中的成员
抽象类本身不能被实例化, 要想使用, 只能创建该抽象类的子类. 然后让子类重写抽象类中的抽象方法.
使用抽象类相当于多了一重编译器的校验, 如上面的代码, 实际工作不应该由父类完成, 而应由子类完成. 那么此时如果不小心误用成父类了, 使用普通类编译器是不会报错的. 但是父类是抽象类就会在实例化的时候提示错误, 让我们尽早发现问题.
很多语法存在的意义都是为了 "预防出错", 例如我们曾经用过的 final 也是类似. 创建的变量用户不去修改, 不就相当于常量嘛? 但是加上 final 能够在不小心误修改的时候, 让编译器及时提醒我们.
充分利用编译器的校验, 在实际开发中是非常有意义的.
在现实生活中,接口的例子比比皆是,比如:笔记本上的USB口,电源插座等。


电脑的USB口上,可以插:U盘、鼠标、键盘...所有符合USB协议的设备
电源插座插孔上,可以插:电脑、电视机、电饭煲...所有符合规范的设备
通过上述例子可以看出:接口就是公共的行为规范标准,大家在实现时,只要符合规范标准,就可以通用。
在Java中,接口可以看成是:多个类的公共规范,是一种引用数据类型。
接口可以认为是 行为的标准/规范
接口的定义格式与定义类的格式基本相同,将class关键字换成interface 关键字,就定义了一个接口。
1. 使用interface来修饰 接口
2. 接口当中的成员方法,不能有具体的实现。[ public ]
(1) 抽象方法:默认是public abstract的方法
(2) JDK1.8开始 允许有可以实现的方法,但是这个方法只能是由default修饰的
(3) 可以实现 有一个静态方法
3. 成员变量默认是public static final修饰的
4. 接口不能被实例化。
- interface ITest {
-
- int size = 10; // public static final
-
- void draw(); // public abstract, 必须被重写
-
- default public void func() {
- System.out.println("默认方法!");
- }
-
- public static void func2() {
- System.out.println("Fsafsaa");
- }
- }
5. 类和接口之间采用
implements 来实现多个接口
- class A implements ITest {
- @Override
- public void draw() {
- System.out.println("必须重写的!"); // 必须重写抽象方法
- }
-
- /*@Override
- public void func() {
- System.out.println("这个方法的重写 是可以选择的! ");
- }*/
- }
6. 子类重写抽象方法,必须加上public.
7. 接口中不能有静态代码块和构造方法
8.如果你不想实现接口的方法,那么就把这个类定义为抽象类。但是如果这个类 被其他类继承 那么必须重写
9.一个类 可以实现 多个接口。 使用implements用逗号隔开[可以解决多继承的问题]
我们把前面画图形的例子改成用接口实现.
- class Rect implements IShape {
- @Override
- public void draw() {
- System.out.println("矩形!");
- }
- }
-
- class Flower implements IShape {
- @Override
- public void draw() {
- System.out.println("花!");
- }
- }
-
- class Cycle implements IShape {
- @Override
- public void draw() {
- System.out.println("圆!");
- }
- }
-
- public class Test2 {
- public static void drawMap(IShape iShape) {
- iShape.draw();
- }
-
- public static void main(String[] args) {
- Rect rect = new Rect();
- drawMap(rect);
- drawMap(new Cycle());
- drawMap(new Flower());
- //IShape iShape = new Cycle();
- }
- }

注意:子类和父类之间是extends 继承关系,类与接口之间是 implements 实现关系。
在Java中,类和类之间是单继承的,一个类只能有一个父类,即Java中不支持多继承,但是一个类可以实现多个接口。
- interface IFLying {
- void flying();
- }
-
- interface ISwimming {
- void swimming();
- }
-
- interface IRunning {
- void running();
- }
-
- class Animal {
- public String name;
- public int age;
-
- public Animal(String name, int age) {
- this.name = name;
- this.age = age;
- }
-
- public void eat() {
- System.out.println("吃饭!");
- }
- }
-
- class Dog extends Animal implements IRunning, ISwimming {
-
- public Dog(String name, int age) {
- super(name, age);
- }
-
- @Override
- public void swimming() {
- System.out.println(name + " 正在游泳!");
- }
-
- @Override
- public void running() {
- System.out.println(name + " 正在跑!");
- }
-
- @Override
- public void eat() {
- System.out.println(name + "正在吃狗粮!");
- }
- }
-
- class Bird extends Animal implements IFLying {
-
- public Bird(String name, int age) {
- super(name, age);
- }
-
- @Override
- public void flying() {
- System.out.println(name + "正在飞!");
- }
-
- @Override
- public void eat() {
- System.out.println(name + "正在鸟粮!");
- }
- }
-
- class Duck extends Animal implements IFLying, IRunning, ISwimming {
-
- public Duck(String name, int age) {
- super(name, age);
- }
-
- @Override
- public void flying() {
- System.out.println(name + "正在飞!");
- }
-
- @Override
- public void swimming() {
- System.out.println(name + " 正在游泳!");
- }
-
- @Override
- public void running() {
- System.out.println(name + " 正在跑!");
- }
-
- @Override
- public void eat() {
- System.out.println(name + "正在吃鸭粮!");
- }
- }
注意:一个类实现多个接口时,每个接口中的抽象方法都要实现,否则类必须设置为抽象类。
上面的代码展示了 Java 面向对象编程中最常见的用法: 一个类继承一个父类, 同时实现多种接口.
在Java中,类和类之间是单继承的,一个类可以实现多个接口,接口与接口之间可以多继承。即:用接口可以达到多继承的目的。
- interface A1 {
- void func();
- }
-
- interface B1 {
- void func2();
- }
-
- interface D extends A1,B1 {
- void func3();
- }
- class E implements D {
-
- @Override
- public void func() {
-
- }
-
- @Override
- public void func2() {
-
- }
-
- @Override
- public void func3() {
-
- }
- }
-
- class C1 implements A1,B1 {
-
- @Override
- public void func() {
-
- }
-
- @Override
- public void func2() {
-
- }
- }
接口间的继承相当于把多个接口合并在一起.
核心区别: 抽象类中可以包含普通方法和普通字段, 这样的普通方法和字段可以被子类直接使用(不必重写), 而接口中不能包含普通方法, 子类必须重写所有的抽象方法.
| No | 区别 | 抽象类(abstract) | 接口(interface) |
|---|---|---|---|
| 1 | 结构组成 | 普通类+抽象方法 | 抽象方法+全局常量 |
| 2 | 权限 | 各种权限 | public |
| 3 | 子类使用 | 使用extends关键字继承抽象类 | 使用implements关键字实现接口 |
| 4 | 关系 | 一个抽象类可以实现若干接口 | 接口不能继承抽象类, 但是接口可以使用extends关键字继承多个父接口 |
| 5 | 子类限制 | 一个子类只能继承一个抽象类 | 一个子类可以实现多个接口 |
以下代码实现了对数组的简单的排序:
- class Student {
- public String name;
- public int age;
-
- public Student(String name, int age) {
- this.name = name;
- this.age = age;
- }
-
- @Override
- public String toString() {
- return "Student{" +
- "name='" + name + '\'' +
- ", age=" + age +
- '}';
- }
- }
-
- public class Test {
- public static void main1(String[] args) {
- int[] array = {1, 4, 2, 7, 3, 8, 5};
- Arrays.sort(array);
- System.out.println(Arrays.toString(array));
- }
- }

接下来我们需求变了, 我们不再排序简单的数据.
- public static void main(String[] args) {
- // 有一个学生数组, 数组中有三个学生
- Student[] students = new Student[3];
- students[0] = new Student("bit", 10);
- students[1] = new Student("hello", 40);
- students[2] = new Student("gbc", 5);
- // 现在想对数组排序
- Arrays.sort(students);
- System.out.println(Arrays.toString(students));
- }
根据年龄还是姓名进行排序?

可以看到,执行上述代码会直接报异常了, 这个异常是个类型转换异常(ClassCastException). 后面的意思是Student不能转化为Comparable.
通过点击灰色部分可以进到出错的位置, 首先可以知道是第29行出错, 再点击其中的第320行的灰色部分.

可以看到, 它是把传进来的数组强制转化成Comparable, 也就是说它认为底层在给数组排序的时候, 需要把下标的元素强转成Comparable. 那么又为什么要把好好的Student转化成Comparable?
首先, 如果是一个自定义类型, 是需要我们自己指定比较的规则的. 因为目前的Student是根据什么比较的我们根本看不出来, 我们需要指定要根据什么来进行比较, 比如姓名, 年龄...也就是说, 我们自定义的学生类, 需要具备可以比较的功能? 怎么具备呢?
假设现在不是学生, 如果现在有一个字符串数组, 要对数组进行排序, Arrays.sort()比较的是字符串:
- public static void main(String[] args) {
- String[] strings = {"abc", "hello", "bcd"};
- Arrays.sort(strings);
- System.out.println(Arrays.toString(strings));
- }

可以看到, 此时是按照了字母顺序排好序的. 那么代码层面看, 它是如何排好序的? 可以看String的源码.

会发现, String实现了Comparable接口, 那么我们就可以模仿这个来写.
所以, Student要可比较, 就要实现
Comparable接口, 只有实现这个接口, 才能让Student具备可比较的功能.
要实现Comparable接口, 我们就要看它的源码:

可以看到方法compareTo(), 那么这个方法是接口中的方法, 所以需要重写.
- @Override
- public int compareTo(Student o) {
- // 根据年龄比较
- if (this.age - o.age > 0) {
- return 1;
- } else if (this.age - o.age < 0) {
- return -1;
- } else {
- return 0;
- }
- }
再次执行前面的学生数组排序程序, 可以看到, 此时就根据年龄排成有序了.

实现Comparable接口使得Student可比较后, 我们可以使用compareTo()方法更直观的感受到Student具备了的可比较的功能.
- public static void main(String[] args) {
- Student student1 = new Student("bit", 10);
- Student student2 = new Student("hello", 40);
-
- if (student1.compareTo(student2) > 0) {
- System.out.println("student1 > student2");
- } else {
- System.out.println("student1 < student2");
- }
- }

总结: 如果我们以后 自定义的类型, 如果要比较大小, 那么必须要让这个类具备可以比较的功能. 此时可以选择实现Comparable接口.
但是我们会发现这样写有一个不好的地方, 更换了需求, 比如此时要根据姓名比较, 那么就意味着要修改重写的compareTo()的代码逻辑, 但是假设代码已经使用了很长一段时间, 此时贸然修改这部分代码, 会引起非常大的风险, 在下次项目上线的时候, 就很有可能会出现很大的bug.
Java提供了另一个接口
Comparator, 灵活性更强.
- class AgeComparator implements Comparator
{ -
- @Override
- public int compare(Student o1, Student o2) {
- return o1.age - o2.age;
- }
- }
可以看到, 我们重新写了另外一个类, 实现了Comparator接口, 而且并没有在Student类上实现该接口.
那么我们可以通过Comparator源码看到, 它有一个compare()方法.

所以我们重写compare()方法即可.
Arrays.sort()是可以传两个参数的, 此时第二个参数可以是实现了Comparator接口的一个对象
- public static void main(String[] args) {
- Student[] students = new Student[3];
- students[0] = new Student("bit", 10);
- students[1] = new Student("hello", 40);
- students[2] = new Student("gbc", 5);
-
- AgeComparator ageComparator = new AgeComparator();
- Arrays.sort(students, ageComparator);
- System.out.println(Arrays.toString(students));
- }

运行可以看到, 它就根据年龄从小到大进行排序了.
也可以更灵活一点, 如果要根据姓名进行比较:
- class NameComparator implements Comparator
{ -
- @Override
- public int compare(Student o1, Student o2) {
- return o1.name.compareTo(o2.name);
- }
- }
- public static void main(String[] args) {
- Student[] students = new Student[3];
- students[0] = new Student("bit", 10);
- students[1] = new Student("hello", 40);
- students[2] = new Student("gbc", 5);
-
- NameComparator nameComparator = new NameComparator();
- Arrays.sort(students, nameComparator);
- System.out.println(Arrays.toString(students));
- }

可以看到, 此时就根据姓名进行比较了.
于是我们可以知道, 要通过什么来进行排序, 都可以建一个类来实现Comparator接口.
那么我们会注意到, Comparable接口对类的侵入性非常强, 而Comparator对类的侵入性就比较弱.
Comparator又叫做比较器
- public static void main(String[] args) {
- Student student1 = new Student("bit", 10);
- Student student2 = new Student("hello", 40);
-
- AgeComparator ageComparator = new AgeComparator();
-
- if (ageComparator.compare(student1, student2) > 0) {
- System.out.println("student1 > student2");
- } else {
- System.out.println("student1 < student2");
- }
- }

我们也可以利用比较器写一个冒泡排序:
- public static void bubbleSort(Comparable[] array) {
- for (int i = 0; i < array.length - 1; i++) {
- for (int j = 0; j < array.length - 1 - i; j++) {
- if (array[j].compareTo(array[j + 1]) > 0) {
- Comparable tmp = array[j];
- array[j] = array[j + 1];
- array[j + 1] = tmp;
- }
- }
- }
- }
- public static void main(String[] args) {
- Student[] students = new Student[3];
- students[0] = new Student("bit", 10);
- students[1] = new Student("hello", 40);
- students[2] = new Student("gbc", 5);
- bubbleSort(students);
- System.out.println(Arrays.toString(students));
- }

- class Person {
- public int id;
-
- @Override
- public String toString() {
- return "Person{" +
- "id=" + id +
- '}';
- }
- }
-
- public class Test2 {
- public static void main(String[] args){
- Person person = new Person();
- }
- }
如上代码所示, 有一个person对象, 现在假设需要克隆一下这个person对象. 也就是说, 有一个一模一样的person对象. 那么此时上面代码的内存图如下所示.

现在要求把这个对象拷贝(克隆)一份, 就需要让Person实现Cloneable接口.

然后在Person中重写Object.clone()方法:
- @Override
- protected Object clone() throws CloneNotSupportedException {
- return super.clone();
- }
最后在main上声明异常, 并强转person.clone的结果:
- public class Test2 {
- public static void main(String[] args) throws CloneNotSupportedException {
- Person person = new Person();
- Person person2 = (Person) person.clone();
- System.out.println(person);
- System.out.println(person2);
- }
- }
于是当以上代码写完之后, 内存分布如下:

运行结果:

深拷贝是什么? 与之相对应的, 浅拷贝又是什么?
我们更改一下上面Cloneable的示例代码,
- class Money {
- public double m = 12.5;
- }
-
- class Person implements Cloneable {
- public int id;
- public Money money = new Money();
-
-
- @Override
- public String toString() {
- return "Person{" +
- "id=" + id +
- '}';
- }
-
- @Override
- protected Object clone() throws CloneNotSupportedException {
- return super.clone();
- }
- }
添加了Money类, 并在Person中加入成员属性, 那么此时的内存分布如下.

在main中代码不变的情况下, 其克隆的内存效果图如下.

同时:
- System.out.println("person:" + person.money.m);
- System.out.println("person2:" + person2.money.m);
运行结果:

那么可以看到, money是指向的同一个对象, 如果是这样就会有一个问题:
person2.money.m = 1999;

可以看到, person和person2的money都被改成了1999, 这是我们不希望看到的, 我们预期的是把money也拷贝一份.
以上这就是浅拷贝, 并没有把person中作为对象的成员属性也进行拷贝. 我们希望能够达到深拷贝, 如下图, 把money也拷贝了过去.

可以看到, 此时修改person2.money.m的值, 并不会影响原来person.money.m的值, 这就是深拷贝.
- class Money implements Cloneable {
- public double m = 12.5;
-
- @Override
- protected Object clone() throws CloneNotSupportedException {
- return super.clone();
- }
- }
注意此时如果只在Money中重写clone(), 依然是不符合预期结果的, 显然这个clone()并没有被调用.
- class Person implements Cloneable {
- public int id;
- public Money money = new Money();
-
- @Override
- protected Object clone() throws CloneNotSupportedException {
- //return super.clone();
- Person tmp = (Person) super.clone(); // 克隆了Person
- tmp.money = (Money) this.money.clone(); // 克隆了Money
- return tmp;
- }
-
- @Override
- public String toString() {
- return "Person{" +
- "id=" + id +
- '}';
- }
- }

Object是Java默认提供的一个类。Java里面除了Object类,所有的类都是存在继承关系的。默认会继承Object父类。即所有类的对象都可以使用Object的引用进行接收。
范例:使用Object接收所有类的对象
- class Person{
- }
-
- class Student{
- }
-
- public class Test {
- public static void main(String[] args) {
- function(new Person());
- function(new Student());
- }
-
- public static void function(Object obj) {
- System.out.println(obj);
- }
- }
执行结果:
Person@1b6d3586
Student@4554617c
所以在开发之中,Object类是参数的最高统一类型。但是Object类也存在有定义好的一些方法。如下:

对于整个Object类中的方法需要实现全部掌握。
在这里,我们主要熟悉这几个方法:toString()方法,equals()方法,hashcode()方法
在sout(对象)的时候, 底层就是调用的Object.toString(), 然后toString()中有调用到hashCode()

那么hashCode()是什么?
![]()
可以看到, hashCode()返回的是一个int类型的值, 然后把它变成十六进制(toHexString)
在public native int hashCode();的注释中可以看到:
Returns a hash code value for the object. This method is supported for the benefit of hash tables such as those provided by java.util.HashMap.
返回对象的 哈希代码值。支持此方法是因为可以使用诸如java.util.HashMap提供的哈希表之类的哈希表。
在Java中,==进行比较时:
a.如果==左右两侧是基本类型变量,比较的是变量中值是否相同
b.如果==左右两侧是引用类型变量,比较的是引用变量地址是否相同
c.如果要比较对象中内容,必须重写Object中的equals方法,因为equals方法默认也是按照地址比较的
- class Person {
- private String name;
- private int age;
-
- public Person(String name, int age) {
- this.age = age;
- this.name = name;
- }
- }
-
- public class Test6 {
- public static void main(String[] args) {
- Person person1 = new Person("张三", 18);
- Person person2 = new Person("张三", 18);
- }
- }

可以看到, 运行结果是false, 但是我们希望的结果的true.
进行调试:



那么怎么办?
我们预期是认为person1和person2是两个一样的人, 那么就需要在Person中自己重写equals()方法.
- @Override
- public boolean equals(Object obj) {
- if (obj == null) {
- return false;
- }
- if (this == obj) {
- return true;
- }
- // 不是Person类对象 是不是同种类型
- if (!(obj instanceof Person)) {
- return false;
- }
-
- Person per = (Person) obj;
- if (this.name.equals(per.name) && this.age == per.age) {
- return true;
- }
- return false;
- }
再次运行程序:

如果以后写了自定义类型, 那么就需要注意重写
equals()方法
结论:比较对象中内容是否相同的时候,一定要重写equals方法。
前面有提到过hashCode, 这个方法是帮我们计算一个具体的对象位置.
- class Person {
- private String name ;
- private int age ;
- public Person(String name, int age) {
- this.age = age ;
- this.name = name ;
- }
- }
-
- public class Test6 {
- public static void main(String[] args) {
- Person person1 = new Person("张三",18);
- Person person2 = new Person("张三",18);
- System.out.println(person1.hashCode());
- System.out.println(person2.hashCode());
- }
- }

可以看到, 两个person的hash数值不一样. 但是在数据结构的哈希表中如果两个人都叫 张三 的话, 这个时候就要看hashCode能否把它俩放到同一个位置上.
像重写equals方法一样,我们也可以重写hashcode()方法。
- @Override
- public int hashCode() {
- //计算对象的位置
- return Objects.hash(name, age);
- }

1、hashcode方法用来确定对象在内存中存储的位置是否相同
2、事实上hashCode() 在散列表中才有用,在其它情况下没用。在散列表中hashCode() 的作用是获取对象的散列码,进而确定该对象在散列表中的位置。