Java---包,继承,组合,多态,抽象类,接口(万字详解)

Java---包,继承,组合,多态,抽象类,接口(万字详解),第1张

Java---包,继承,组合,多态,抽象类,接口(万字详解)

 文章前提:

我们已经知道 Java 是面向对象进行编程的,那么具体的怎样去面向对象进行编程,本篇博客将进行深入探讨

 文章目录:

1.包

- 1.1 导入包中的类
- 1.2 静态导入
- 1.3 将类放到包中
- 1.4 包的访问权限控制
- 1.5 常见的系统包

2.继承

- 2.1 什么是继承
- 2.2 语法规则
- 2.3 protected 关键字
- 2.4 final 关键字

3.组合

4.多态
- 4.1 向上转型
- 4.2 动态绑定
- 4.3 使用多态的好处
- 4.4 向下转型

5.抽象类

6.接口

- 6.1 语法规则
- 6.2 实现多个接口
- 6.3 接口使用实例
- 6.4 接口间的继承
- 6.5 Clonable 接口


1.包 1.1 导入包中的类

在我们打印数组的时候会使用到一个打印方法:

public static void main(String[] args) {
        int[] array = {1,2,3,4,5};
        System.out.println(Arrays.toString(array));
    }

当我们写入 Arrays.toString 的时候需要导入一个包:

import java.util.Arrays;


 我们可以发现这些包在项目中都是可以找到的

 其实这些包在安装 jdk 的时候就已经安装好了(jdk 官方写好的)

❗️ 以后实现部分功能的时候可以直接引入包

当导入 Date 包(或者其他包)的时候,如果没有写 import 语句,也可以这样使用 (不推荐很麻烦)

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

❓ 那 impor 又有怎样的功能呢 

导入一个具体的类,不能导入一个具体的包

不可这样写❌(这样就是导入一个具体的包,无法实现)

import java.util;

也可以通过这样的方法(通配符)导入这个包底下所有的类

import java.util.*;

❓ 但是有一个问题,通配符难道是一次性把所有的类都导入了吗

不是,java 在处理中需要谁才导入谁

❓ 在通配符的使用过程中会出现一个问题

import java.util.*;
import java.sql.*;

 这两个类中都有 Date 包,编译器分辨不清,导致程序出错

这种情况下需要用完整的类名 

java.util.Date date = new java.util.Date();
1.2 静态导入

 静态导入很少使用,了解即可

import static java.lang.System.*;

导入一个这样的静态包后,在打印中就可以这样使用

out.println("12345");

 不提倡上面的代码样式

1.3 将类放到包中

基本规则

  • 在文件的最上方加上一个 package 语句指定该代码在哪个包中.
  • 包名需要尽量指定成唯一的名字, 通常会用公司的域名的颠倒形式(例如 com.bit.demo1 ).
  • 包名要和代码路径相匹配. 例如创建 com.bit.demo1 的包, 那么会存在一个对应的路径 com/bit/demo1 来存储代码
  • 如果一个类没有 package 语句, 则该类被放到一个默认包中

❓ 当再另外一个包中引入其他类,在同名的时候,编译器默认还是使用的当前类


❓ 如何解决呢 

后面的博客会涉及到,这里需要用到后面的知识

❗️ 包名必须是小写的,不能是大写的

1.4 包的访问权限控制

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

如果某个成员不包含 public 和 private 关键字, 此时这个成员可以在包内部的其他类使用, 但是不能在包外部的类使用.

❓ 什么情况可以称为包的访问权限:当你的成员变量不加任何的访问修饰限定词的时候,默认就是一个包的访问权限,只可以在当前包中使用

下面的代码给了一个示例. Test 和 TestDemo 是在同一个包中的可以互相调用 

public class Test {
        int val = 10;
}

public class TestDemo {
    public static void main(String[] args) {
        Test test = new Test();
        System.out.println(test.val);
    }
}

❗️ 如果在不同包中,即是引入另外一个包,也无法使用


代码在调用 Test 的 val 的时候报错(这就是包的访问权限的原因)

- 1.5 常见的系统包
  1. java.lang:系统常用基础类(String、Object),此包从JDK1.1后自动导入。
  2. java.lang.reflect:java 反射编程包;
  3. java.net:进行网络编程开发包。
  4. java.sql:进行数据库开发的支持包。
  5. java.util:是java提供的工具程序包。(集合类等) 非常重要
  6. java.io:I/O编程开发包。

2.继承 2.1 什么是继承

面向对象的基本特征(已学):

  1. 封装(体现安全性)不必要公开的数据成员使用 private 关键字进行修饰。
  2. 继承:具体下面讲解
  3. 组合:具体下面讲解
  4. 多态:具体下面讲解

 继承:本质上是对每一个类共性的抽取

2.2 语法规则

使用关键字 extern 进行处理,意义:可以对代码重用

class 子类 extends 父类 { 
 
} 

实例使用:

class Animal{
    public int age;
    public String name;
    public void eat(){
        System.out.println("eat()");
    }
}
class Dog extends Animal{
    
}
class Bird extends Animal{
    public String wing;
    public void fly(){
        System.out.println("fly()");
    }
}
public class TestDemo1 {
    public static void main(String[] args) {
        Dog dog = new Dog();
        System.out.println(dog.name);
    }

}

 Dog 和 Bird 继承了 Animal 的属性(两种的共同属性)

仔细分析, 我们发现 Animal 和 Dog t以及 Bird 这几个类中存在一定的关联关系: 

  • 这三个类都具备一个相同的 eat 方法, 而且行为是完全一样的.
  • 这三个类都具备一个相同的 name 属性, 而且意义是完全一样的.
  • 从逻辑上讲, Cat 和 Bird 都是一种 Animal (is - a 语义).

 继承中的 Dog(Bird) 可以被称为子类/派生类,Animal 可以称为父类/基类/超类,通过 extern 可以继承父类的属性(和现实中的儿子继承父亲的财产类似, 子类也会继承父类的字段和方法, 以达到代码重用的效果)


由于 Dog (Bird) 继承了父类 Animal 的属性,所以这里可以通过 (.)访问父类的属性 

public static void main(String[] args) {
        Dog dog = new Dog();
        System.out.println(dog.name);
    }

❗️ 注意事项

  • 使用 extends 指定父类.
  • Java 中一个子类只能继承一个父类 (而C++/Python等语言支持多继承).
  • 子类会继承父类的所有 public 的字段和方法.
  • 对于父类的 private 的字段和方法, 子类中是无法访问的.
  • 子类的实例中, 也包含着父类的实例. 可以使用 super 关键字得到父类实例的引用
  • 子类在构造的时候,要先帮助父类构造

看最后一个注意事项,当为父类提供一个构造方法时,程序会报错

public Animal(String name, int age){
        this.name = name;
        this.age = age;
    }

❌ 报错 


 解决方式:子类帮助父类构造,即(子类先调用父类的构造方法)

class Dog extends Animal{
    public Dog(String name, int age){
        super(name,age);
    }
}

 使用 super 关键字,先行调用父类,super 为显示调用构造方法

那为什么不写构造方法的时候程序不会报错,因为当不写构造方法的时候,编译器默认生成一个没有参数的构造方法,如下方的构造方法 

public Dog(){
        super();
    }

super 用法总结:

  • super(); 调用父类的构造方法
  • super.func(); 调用父类的方法
  • super.name; 调用父类的成员属性
  • 不能出现在静态方法中,它是依赖对象的
  • super 一定出现在构造方法的第一行,其他方法中不可以

 内存图解释继承 


❗️ 当父类和子类成员属性同名的时候,优先调用子类的属性,如果需要调用父类的成员属性(方法),可以使用 super 关键字指定

2.3 protected 关键字

先看一张表:


☝️ 总结上表:

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

❗️ 注意事项:

  • 我们希望类要尽量做到 “封装”, 即隐藏内部实现细节, 只暴露出 必要 的信息给类的调用者.
  • 因此我们在使用的时候应该尽可能的使用 比较严格 的访问权限. 例如如果一个方法能用 private, 就尽量不要用public.
  • 另外, 还有一种 简单粗暴 的做法: 将所有的字段设为 private, 将所有的方法设为 public. 不过这种方式属于是对访问权限的滥用, 还是更希望大家能写代码的时候认真思考, 该类提供的字段方法到底给 “谁” 使用(是类内部自己用, 还是类的调用者使用, 还是子类使用).

更复杂的继承关系


❓ 这样的继承关系又怎样进行描述呢

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

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

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

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

  • 如果想从语法上进行限制继承, 就可以使用 final 修饰

2.4 final 关键字

如果一个类不想被继承,可以把该类设置为被 final 修饰

总结 final 修饰:

  • final 修饰常量,不可被修改

  • final 修饰类,不可被继承

  • final 修饰方法,方法不可重写,属性不可覆盖


3.组合

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

  • 组合表示 has - a 语义

在刚才的例子中, 我们可以理解成一个学校中 “包含” 若干学生和教师.

  • 继承表示 is - a 语义

在上面的 “动物和猫” 的例子中, 我们可以理解成一只猫也 “是” 一种动物.


4.多态

 多态(字面意思):一种事物多种形态

理解多态就需要理解:向上转型,即(父类对象引用子类对象)

4.1 向上转型
public static void main(String[] args) {
        Animal animal = new Dog("haha",19);

❓ 什么情况下会发生向上转型

  1. 直接赋值
  2. 作为函数的参数
public static void func(Animal animal){
        
    }
    public static void main(String[] args) {
        Dog dog = new Dog("haha",19);
        func(dog);
    }

3.作为返回值

public static Animal func2(){
        Dog dog = new Dog("haha",19);
        return dog;
    }
4.2 动态绑定

先看两个代码: 

public static void main(String[] args) {
        Animal animal = new Dog("haha",19);
        animal.eat();
    }

eat()

我们可以看到这里调用的时父类的方法

再看一个代码: (当子类中也有同名的 eat()方法)

class Dog extends Animal{
    public Dog(String name, int age){
        super(name,age);
    }
    public void eat(){
        System.out.println("狼吞虎咽的eat()");
    }
}
 public static void main(String[] args) {
        Animal animal = new Dog("haha",19);
        animal.eat();
    }

狼吞虎咽的eat()

❓ 我们看到这里调用的是子类的方法,这是为什么呢

 因为这里发生了动态绑定

❓ 利用 javap -c 打开文件的汇编代码,可以看到这里调用的还是 Animal 的 eat 方法,这是为什么呢

 在编译的时候不能够确定此时到底调用谁的方法,在运行的时候才知道调用谁的方法,称其为运行时绑定--------即我们的动态绑定

动态绑定的两个前提:

  1. 父类引用 引用子类对象
  2. 通过这个父类引用调用父类和子类同名的覆盖方法

❓ 那么什么是同名的覆盖

 同名的覆盖又被叫做重写,重写要满足以下几种情况

  1. 方法名相同
  2. 参数列表相同(个数+类型)
  3. 返回值相同
  4. 父子类的情况下

❗️ 重写的注意事项:

  1. 方法不可以是静态的,静态的方法不可以重写
  2. 子类的访问修饰限定符范围一定要大于等于父类的访问修饰限定符
  3. private 方法不能重写
  4. 被 final 修饰的(关键字/方法)不可以被重写
  5. 协变类型也可以构成重写

 协变类型(很少提,科普一下)

public Animal eat(){
        System.out.println("eat()");
        return null;
    }
    public Dog eat(){
        System.out.println("狼吞虎咽的eat()");
        return null;
    }

❗️ 在父类引用子类的时候有一个注意事项:

public static void main(String[] args) {
        Animal animal = new Bird("haha",19,"fei");
        animal.wing;
    }


程序在这里报错了,所以可以得出一个结论,通过父类引用只可以访问父类自己的成员

❓ 既然有了动态绑定,那有没有静态绑定呢,看这样一个代码

class Bird extends Animal{
    public Bird(String name, int age,String wing){
        super(name,age);
    }
    public String wing;
    public void fly(){
        System.out.println("fly()");
    }
    public void func(int a){
        System.out.println(a);
    }
    public void func(int a,int b){
        System.out.println(a);
    }
    public void func(int a,int b,int c){
        System.out.println(a);
    }
}


 public static void func(Animal animal){
        Bird bird = new Bird("haha",19,"fei");
        bird.func(10);
    }

我们看他的汇编代码:


 我们可以看到这里在编译的时候已经规定好了调用哪个 func 方法,这就是静态绑定

 静态绑定:根据你给定的参数个数和类型,判断调用哪个方法(又被称为编译式多态)

4.3 使用多态的好处

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

利用一个代码加以说明

class Shape{
    public void draw(){
        System.out.println();
    }
}
class sanjiao extends Shape{
    @Override
    public void draw() {
        System.out.println("△");
    }
}
class fangpian 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) {
        drawmap(new fangpian());
        drawmap(new sanjiao());
    }
}

☝️ 在这个代码中, 分割线上方的代码是 类的实现者 编写的, 分割线下方的代码是 类的调用者 编写的,

 当类的调用者在编写 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 分支语句, 代码更简单,利用 foreach 进行打印

public static void drawShapes() { 
 // 我们创建了一个 Shape 对象的数组. 
 Shape[] shapes = {new Cycle(), new Rect(), new Cycle(), 
 new Rect(), new Flower()}; 
 for (Shape shape : shapes) { 
 shape.draw(); 
 } 
} 

❓ 什么叫 “圈复杂度”

圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解. 而如果有很多的条件分支或者循环语句, 就认为理解起来更复杂.因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 “圈复杂度”. 如果一个方法的圈复杂度太高, 就需
进行重构

  1. 可扩展能力更强.

对于类的调用者来说(drawShapes方法), 只要创建一个新类的实例就可以了, 改动成本很低. 而对于不用多态的情况, 就要把 drawShapes 中的 if - else 进行一定的修改, 改动成本更高

4.4 向下转型

通过下面一个代码了解向下转型(不推荐使用向下转型)

public static void main(String[] args) {
        Animal animal = new Bird("haha",19,"fei");
        Bird bird = (Bird)animal;
        System.out.println(bird.wing);
    }


❗️ 向下转型不安全,我们知道向上转型(鸟一定是动物),但是动物不一定是鸟,所以, 为了让向下转型更安全, 我们可以先判定一下看看 animal 本质上是不是一个 Bird 实例, 再来转换,instanceof 可以判定一个引用是否是某个类的实例. 如果是, 则返回 true. 这时再进行向下转型就比较安全了.

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

一段有坑的代码. 我们创建两个类, B 是父类, D 是子类. D 中重写 func 方法. 并且在 B 的构造方法中调用 func

class B { 
 public B() { 
 // do nothing 
 func(); 
 } 
 public void func() { 
 System.out.println("B.func()"); 
 } 
} 
class D extends B { 
 private int num = 1; 
 @Override 
 public void func() { 
 System.out.println("D.func() " + num); 
 } 
} 
public class Test { 
 public static void main(String[] args) { 
 D d = new D(); 
  } 
} 
// 执行结果
D.func() 0
  • 构造 D 对象的同时, 会调用 B 的构造方法.
  • B 的构造方法中调用了 func 方法, 此时会触发动态绑定, 会调用到 D 中的 func
  • 此时 D 对象自身还没有构造, 此时 num 处在未初始化的状态, 值为 0.

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


5.抽象类

❓ 当上面的代码中我们可以看到其实 Shape 类中的 draw 除了可以做一个判断子类有没有重写 draw 方法,没有其他作用,那我们应该怎么简化呢

class Shape{
    public void draw();
}

❌ 直接删除方法会导致程序报错


 这就需要用到抽象类以及抽象方法

abstract class Shape{
    abstract public void draw();//抽象方法
}

 使用关键字 abstract 定义抽象类

❗️ 注意:抽象类无法实例化对象(程序报错)


总结抽象类:

  1. 包含抽象方法的类,叫做抽象类
  2. 什么是抽象方法,即没有具体实现的方法,被 abstract 修饰
  3. 抽象类不可以实例化
  4. 由于不能被实例化,所以抽象类只能被继承(最大的作用)
  5. 抽象类当中也可以包括和普通一样的成员和方法(静态的也可以)
  6. 一个普通类继承了一个抽象类,那么这个普通类需要重写抽象类的所有抽象方法
  7. 一个抽象类A如果继承一个抽象类B,那么这个抽象类A可以不实现抽象父类B的抽象方法
  8. 结合第7点当A类被一个普通类继承后,A和B这两个抽象类当中的抽象方法必须被重写
  9. 抽象类不能被 final 修饰,抽象方法也不可以被 final 修饰

6.接口 6.1语法规则

❗️ 接口的注意事项:

  • 接口是用 interface 来修饰的
  • 接口当中的普通方法不能有具体的实现,非要实现需要加关键字 default(不可以被重写)
  • 接口中可以有静态方法
  • 里面的所以方法都是 public 的
  • 抽象方法默认是一个 public abstract 的
  • 接口不可以被通过关键字 new 来进行实例化的
  • 类和接口之间的关系是通过 implements 来实现的
  • 当一个类实现了一个接口那么就必须重新接口当中的抽象方法
  • 接口中的成员变量默认是被 public static final 所修饰的,一定在定义的时候初始化
  • 当一个类实现一个接口之后,重写这个方法,这个方法前面必须加上 pubilc
  • 一个类可以通过通过关键字 static 继承一个抽象类或一个普通类,但是只能继承一个类,也可以通过 implements 来实现多个接口,接口之间使用(,)隔开
  • 接口B和接口C可以使用 extends 关键字来进行 *** 作,此时译为:拓展,C接口通过 extends 拓展B的功能,当一个类 D 通过 implements 来实现这个接口 B 的时候,不仅需要重写 B 的抽象方法,还有重写从C拓展的抽象方法

❓ 接口在什么情况下使用呢,看下面一种情况

class Animal{
    public int age;
    public String name;
    public Animal(int age, String name){
        this.age = age;
        this.name = name;
    }
}
interface Flying{
    void fly();
}
class Bird extends Animal implements Flying{
    public Bird(int age, String name){
        super(age,name);
    }

    @Override
    public void fly() {
        System.out.println("fly");
    }
}

 当鸟飞的属性不是所有的动物都具有的时候,飞的属性不可以写在 Animal 类中,如果单独定义一个类,Bird 已经继承了一个类,无法再继承一个类,所以这里可以使用到接口。

6.2实现多个接口

 有的时候我们需要让一个类同时继承自多个父类. 这件事情在有些编程语言通过 多继承 的方式来实现的.

 然而 Java 中只支持单继承, 一个类只能 extends 一个父类. 但是可以同时实现多个接口, 也能达到多继承类似的效果.现在我们通过类来表示一组动物.

class Animal{
    public int age;
    public String name;
    public Animal(int age, String name){
        this.age = age;
        this.name = name;
    }
}

再实现多个接口表示“会飞的”,“会跑的”

interface Flying{
    void fly();
}
interface Running{
    void run();
}

那么对于鸟这个类可以继承 Animal 的共有属性,还可以拓展多个接口(鸟的特有属性)

class Bird extends Animal implements Flying,Running{
    public Bird(int age, String name){
        super(age,name);
    }

    @Override
    public void fly() {
        System.out.println(this.name+"正在fly");
    }

    @Override
    public void run() {
        System.out.println(this.name+"正在run");
    }
}

☝️ 上面的代码展示了 Java 面向对象编程中最常见的用法: 一个类继承一个父类, 同时实现多种接口

 继承表达的含义是 is - a 语义, 而接口表达的含义是 具有 xxx 特性

❓ 这样设计有什么好处呢? 时刻牢记多态的好处, 让程序猿忘记类型. 有了接口之后, 类的使用者就不必关注具体类型, 而只关注某个类是否具备某种能力.

例如:实现一个方法,叫做“散步” 

public static void walk(Running running){
        running.run();
    }

 在这个 walk 方法内部, 我们并不关注到底是哪种动物, 只要参数是会跑的, 就行

public static void main(String[] args) {
        Bird bird = new Bird(19,"小鸟");
        walk(bird);
    }

执行结果:

小鸟正在run

甚至参数可以不是 “动物”, 只要会跑!

class Robort implements Running{
    @Override
    public void run() {
        System.out.println("机器人在跑");
    }
}
walk(new Robort());

执行结果:

机器人在跑

6.3 接口使用实例

在这里介绍两个常用的接口:

我们知道一个整型的数组进行排序是可以借助如下的方法进行排序的 

public class Test {
    public static void main(String[] args) {
        int[] arr = {14,2,65,16,7,1,96};
        Arrays.sort(arr);
        System.out.println(Arrays.toString(arr));
    }
}

❓ 那么如果待排序的是一个 Student 这个类呢

class Student{
    public int age;
    public String name;
    public double score;
    public Student(int age, String name, double score) {
        this.age = age;
        this.name = name;
        this.score = score;
    }
}

如果直接进行排序会发现程序报错

public class Test {
    public static void main(String[] args) {
        Student[] students = new Student[3];
        students[0] = new Student(19,"zhangsan",88.5);
        students[1] = new Student(9,"lisi",99.5);
        students[2] = new Student(20,"wangwu",55.5);
        Arrays.sort(students);
        System.out.println(Arrays.toString(students));
    }
}

Exception in thread “main” java.lang.ClassCastException: Demo2.Student cannot be cast to java.lang.Comparable
at java.util.ComparableTimSort.countRunAndMakeAscending(ComparableTimSort.java:320)
at java.util.ComparableTimSort.sort(ComparableTimSort.java:188)
at java.util.Arrays.sort(Arrays.java:1246)
at Demo2.Test.main(Test.java:34)

☝️ 程序会发生上图的异常,这是因为 Arrays.sort 没有规定根据什么去进行排序,根据年龄,分数,还是姓名?所以这里会报错,这里正式引出两个常用接口

1️⃣ Comparable

在 sort 的原码中,我们发现在比较过程中,元素类型都被强制转换为 Comparable 类型


所以这里需要我们告诉 Arrays.sort 以怎样的方式去进行比较

利用 Comparable 接口来让 Arrays.sort 知道比较的是学生

class Student implements Comparable

重写 compareTo 方法后 Arrays.sort 就知道了是按照什么去排序的,就可以进行排序了

public int compareTo(Student o) {
        return this.age - o.age;
    }

❗️ 所以如果自定义数据类型进行大小的比较,一定要实现可以比较的接口

❗️ 这里的 Comparable 有一个很不友好的地方,每一个 compareTo 方法需要根据不同需求去改,这个接口对类的侵入性很强,一旦写好了不可以轻易改动,那怎么办,第二种接口可以解决 

2️⃣ 通过比较器进行比较( Comparator)


 这里需要重写这里的 compare 方法,根据比较的需求重写该方法,可以比较出数值大小即可

按照三种比较方式重写的 compare 方法 

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);
    }
}
class ScoreComparator implements  Comparator{
    @Override
    public int compare(Student o1, Student o2) {
        return (int)(o1.score - o2.score);
    }
}

在使用的时候:(把排序的规则也写进 Arrays.sort 即可)

ScoreComparator scoreComparator = new ScoreComparator();
 Arrays.sort(students,scoreComparator);

 这样写的好处是:更灵活,对类的侵入性很小

❗️ 这两个接口的使用,取决业务,但一般推荐比较器

6.4 接口间的继承

 接口可以继承一个接口, 达到复用的效果. 使用 extends 关键字.(这里的 extend 可以译为拓展)

interface IRunning { 
 void run(); 
} 
interface ISwimming { 
 void swim(); 
} 
// 两栖的动物, 既能跑, 也能游
interface IAmphibious extends IRunning, ISwimming { 
} 
class Frog implements IAmphibious { 
 ... 
} 

通过接口继承创建一个新的接口 IAmphibious 表示 “两栖的”. 此时实现接口创建的 Frog 类, 就继续要实现 run 方法,也需要实现 swim 方法.

接口间的继承相当于把多个接口合并在一起.

6.5 Clonable 接口

 Java 中内置了一些很有用的接口, Clonable 就是其中之一.
Object 类中存在一个 clone 方法, 调用这个方法可以创建一个对象的 “拷贝”. 但是要想合法调用 clone 方法, 必须要先实现 Clonable 接口, 否则就会抛出 CloneNotSupportedException 异常 

class Animal implements Cloneable { 
 private String name; 
 @Override 
 public Animal clone() { 
 Animal o = null; 
 try { 
 o = (Animal)super.clone(); 
 } catch (CloneNotSupportedException e) {
 e.printStackTrace(); 
 } 
 return o; 
 } 
} 
public class Test { 
 public static void main(String[] args) { 
 Animal animal = new Animal(); 
 Animal animal2 = animal.clone(); 
 System.out.println(animal == animal2); 
 } 
} 
// 输出结果
// false 

欢迎分享,转载请注明来源:内存溢出

原文地址:https://54852.com/zaji/5522273.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2022-12-13
下一篇2022-12-13

发表评论

登录后才能评论

评论列表(0条)

    保存