首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【JavaSE】抽象类abstract && 接口interface && Comparable && Comparator && Object 类

【JavaSE】抽象类abstract && 接口interface && Comparable && Comparator && Object 类

原创
作者头像
lirendada
发布2026-03-14 14:29:18
发布2026-03-14 14:29:18
740
举报
文章被收录于专栏:JavaJava

Ⅰ. 抽象类 -- abstract

一、概念

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

使用抽象类相当于多了一层编译器的检验

二、抽象类的语法

Java 中,一个类如果被 abstract 修饰则称为抽象类抽象类中被 abstract 修饰的方法称为抽象方法,并且抽象方法不用给出具体的实现体。

说白了抽象类就是在多态的基础上,不让抽象类实例化出对象,并且重写的接口也不能有方法实现,然后强制性让子类去重写接口才能实例化子类对象,但同样也是可以用抽象类的引用指向子类的空间来达到多态的效果,形成一个更高层次的抽象感觉!

【抽象类和普通类的区别】

  1. 抽象类」不能实例化对象,而「普通类」可以。
  2. 抽象类」中可以包含非抽象类方法和抽象方法,而「普通类」只能包含非抽象类方法。
  3. 抽象类」不加访问限定符时候默认为 default,和「普通类」是一样的。
代码语言:javascript
复制
abstract class Base {
    // 可以包含静态和非静态成员变量、方法,以及构造方法
    public int a = 10;
    static public int b = 20;
    public Base() {
        System.out.println("Base()"); // 构造方法
    }
    public void func() {
        System.out.println("func()"); // 非静态方法
    }
    public static void func2() {
        System.out.println("static func2()"); // 静态方法
    }

    // 抽象方法:(不能有函数体)
    abstract protected void eat();
}

class Derived extends Base {
    public Derived() {
        System.out.println("Derived()");
    }

    // 重写抽象类函数,其返回值可以比抽象类函数的返回值权限高一些
    @Override
    public void eat() {
        System.out.println("Derived eat()");
    }
}

public class test1 {
    public static void main(String[] args) {
        Base tmp = new Derived();
        System.out.println(tmp.a + Base.b);
        tmp.eat(); // 访问的是子类重写的方法
        tmp.func();
        Base.func2();
    }
}

// 执行结果:
Base()
Derived()
30
Derived eat()
func()
static func2()

三、抽象类的特性

  1. 抽象类不能直接实例化对象
  2. 抽象类」也是类,可以包含普通方法和属性静态方法和属性,甚至构造方法
  3. 「抽象方法」不能被 privatestaticfinal 修饰。(这和多态规则是类似的)
  4. 抽象方法和非抽象方法的默认的访问限定符为 default。(但不能显式写 default
  5. 子类继承「抽象类」之后要重写父类中的抽象方法,否则子类也必须得是抽象类才行

Ⅱ. 接口 -- interface

接口是公共的行为规范标准,大家在实现时,只要符合规范标准就可以通用,不需要在乎类型。在 Java 中,接口可以看成是多个类的公共规范,是一种引用数据类型

接口的定义格式与定义类的格式基本相同,class 关键字换成 interface 关键字,就定义了一个接口

【接口类的特性和规定】

  • 接口不能直接实例化出对象
  • 接口的命名一般以大写字母 I 开头。
  • 阿里编码规范中约定,接口中的方法和属性不要加任何修饰符号,保持代码的简洁性。
  • 接口虽然不是类,但是接口编译完成后字节码文件的后缀格式也是 .class
  • 和抽象类一样,如果子类没有实现接口中的所有的抽象方法,则该子类必须设置为抽象类,否则会报错!
  • 成员变量
    • 默认被 public static final 修饰,所以必须初始化
    • 不能被 public 外的访问限定符修饰。(这里说的不准确,具体可以看下面的总结)
  • 成员方法
    • 默认被 public abstract 修饰,所以可以简洁的写出接口。(也就是只能为 public abstract,如果是其他修饰符都会报错)
    • 不能被 public 外的访问限定符修饰。(这里说的不准确,具体可以看下面的总结)
    • 不能存在「构造方法」和「静态代码块
    • 接口可以有「静态方法」,但是该静态方法必须要有方法体。
    • 一般接口没有方法体,但是在 JDK 1.8 中是可以有的,需要显式使用 default 修饰该方法。
代码语言:javascript
复制
interface IUSB {
    // 成员变量:
    public int a;                   // ❌必须初始化
    public int b = 10;              // ✅
    protected int c = 20;           // ❌不能被public外的访问限定符修饰
    int d = 30;                     // ✅推荐这种写法
    private int e = 40;             // ❌不能被public外的访问限定符修饰
    public static int f = 50;       // ✅
    public static final int g = 60; // ✅

    // 成员方法:
    public abstract void method(); // ✅public abstract是固定搭配,可以不写
    void method1();                // ✅推荐这种写法
    private void method2();        // ❌不能被public外的访问限定符修饰
    protected void method3();      // ❌不能被public外的访问限定符修饰
    public static void method4() {
        // ✅静态方法必须有方法体
    }
    default public void method5() {
        // ✅普通方法有方法体的话,必须使用default修饰该方法
    }
    
    public IUSB {} // ❌不能存在构造方法
}

一、接口的使用 -- implements

接口不能直接使用,必须要有一个子类来 "实现" 该接口,实现接口中的所有抽象方法。(这跟前面所说的抽象类是类似的)

这里要用到 implements 关键字来表示实现该接口,注意不是继承关系!

代码语言:javascript
复制
interface IUSB {
    void method(); 
}

class mouse implements IUSB {
    // 对接口的方法进行重写
    @Override
    public void method() {
        System.out.println("mouse重写接口类的方法");
    }
}

下面我们就用一个文件一个类或者接口的编程方式,写一个 USB 接口,然后用鼠标类和键盘类来实现该接口的方法,接着用电脑类来向上转型,达到屏蔽底层实现的效果,代码如下所示:

代码语言:javascript
复制
package JavaSE.interfaceTest.USB;

// IUSB.java
public interface IUSB {
    void openService();  // 打开USB的服务接口
    void closeService(); // 关闭USB的服务接口
}

// Mouse.java
public class Mouse implements IUSB{
    @Override
    public void openService() {
        System.out.println("打开mouse的接口服务");
    }
    @Override
    public void closeService() {
        System.out.println("关闭mouse的接口服务");
    }

    // 鼠标自己包含的方法
    public void click() {
        System.out.println("点击鼠标");
    }
}

// KeyBoard.java
public class KeyBoard implements IUSB{
    @Override
    public void openService() {
        System.out.println("打开keyBoard的服务");
    }
    @Override
    public void closeService() {
        System.out.println("关闭keyBoard的服务");
    }

    // 键盘自己包含的方法
    public void input() {
        System.out.println("键盘输入内容……");
    }
}

// Computer.java
public class Computer {
    public void usbMap(IUSB usb) {
        // 调用usb的启动服务接口,此时不关心是哪个具体的子类实现的
        usb.openService();

        // 根据具体类型,进行向下转型调用子类独有的方法
        if(usb instanceof Mouse) {
            Mouse m = (Mouse)usb;
            m.click();
        } else if(usb instanceof KeyBoard) {
            KeyBoard k = (KeyBoard)usb;
            k.input();
        }

        // 调用usb的关闭服务接口,此时不关心是哪个具体的子类实现的
        usb.closeService();
    }
}

///////////////////////////////////////////////////////////////////////////

// Main.java
public class Main {
    public static void main(String[] args) {
        Computer cptr = new Computer();

        cptr.usbMap(new Mouse());
        System.out.println("-----------------");
        cptr.usbMap(new KeyBoard());
    }
}

二、接口与抽象类的区别

接口

抽象类

关键字使用

interface 和 implements

abstract 和 extends

谁可使用

任意类型均可实现接口

只能由子类重写方法

构造方法

没有构造方法

有构造方法

普通方法有无定义和实现

只有定义,没有方法的实现

既有定义,又可以有实现

类型拓展

一个类可实现多个接口

不支持多继承

成员变量

只能被 public static final 修饰,并且必须赋初值,不能被修改

与普通类的成员变量一样

成员方法

所有的成员方法都是 public abstract 修饰的

抽象方法被 abstract 修饰,其不能被 private、static、final、synchronized 和 native 等修饰,并且不能有方法体

静态代码块

不支持

可以使用

因为上面表格空间有限,这里补充一下访问限定符的总结:

  1. 「方法」的访问控制符
    1. 普通类方法
      • 四个访问控制符都可以使用
    2. 抽象类方法
      • JDK 1.8以前:抽象类的方法默认访问权限为 protected(即可以是 publicprotected
      • JDK 1.8抽象类的方法默认访问权限为 default(即可以是 publicprotected 或者不写)
    3. 接口方法
      • JDK 1.8以前:接口中的方法默认,也必须是 public 的(即只能用 public
      • JDK 1.8接口中的方法默认 public 的,也可以是 default(即可以是 publicdefault
      • JDK 1.9:接口中的方法可以是 private 的(即可以是 publicdefaultprivate

2. 「变量」的访问控制符

  1. 普通类变量
    • 四个访问控制符都可以使用
  2. 抽象类变量
    • 四个访问控制符都可以使用
  3. 接口变量
    • 接口变量默认且仅为 public static final

三、一个类实现多个接口

代码语言:javascript
复制
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 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 + "正在蹬腿游泳");
    }
}

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

简单地说,就是当继承一个类之后,该子类还要其它的行为规范,那么就可以实现不同的行为规范来实现,而这些行为规范可能不属于当前父类所应该拥有的,比如说不是所有动物都会跑或者飞

此外需要注意的是继承需要放在实现接口之前,不能颠倒顺序

四、接口之间的继承

Java 中,类和类之间是单继承的,一个类可以实现多个接口,接口与接口之间可以多继承。即:用接口可以达到类似多继承的目的,使用的是继承的关键字 extends。如下所示:

代码语言:javascript
复制
interface A {
    void funcA();
}
interface B {
    void funcB();
}
interface C extends A, B { // C接口继承了A和B的接口
    void funcC();
}

// Test类必须实现三个接口
class Test implements C{
    @Override
    public void funcA() {
        System.out.println("重写A");
    }
    @Override
    public void funcB() {
        System.out.println("重写B");
    }
    @Override
    public void funcC() {
        System.out.println("重写C");
    }
}

五、常用接口

Comparable

这个接口用来进行比较大小,通常用于自定义类型数组的排序!

在类中实现 Comparable<类型> 接口之后,重写 compareTo() 方法即可,如下所示:

代码语言:javascript
复制
class Student implements Comparable<Student> { // 要实现Comparable接口!!!
    public int age;
    public int score;
    public String name;

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

    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", score=" + score +
                ", name='" + name + '\'' +
                '}';
    }

    @Override
    public int compareTo(Student o) {
        return this.age - o.age; // 重写该接口,比较年龄大小进行返回!!!
    }
}

public class Test {
    public static void main(String[] args) {
        Student[] s = new Student[]{
                new Student(18, 100, "liren"),
                new Student(24, 86, "yt"),
                new Student(16, 95, "lt")
        };
        Arrays.sort(s);
        System.out.println(Arrays.toString(s));
    }
}

// 结果:
[Student{age=16, score=95, name='lt'}, Student{age=18, score=100, name='liren'}, Student{age=24, score=86, name='yt'}]

这里需要注意就是字符串的比较需要调用字符串本身实现的 compareTo(),而不是 equals(),因为 equals() 函数返回值是布尔类型,不能用做比较大小,只能比较相不相同,并且 sort() 源码也要求的是返回整型来进行比较~

compareTo() 的规则是这样的:

  • 返回 负数 → 当前对象 < 参数对象 → 当前对象排在前面
  • 返回 → 两个对象相等 → 顺序不变
  • 返回 正数 → 当前对象 > 参数对象 → 当前对象排在后面

Comparator

虽然上面通过实现 Comparable 接口我们实现了自定义类型的排序,但它存在一个问题:不灵活

因为对 compareTo() 函数进行重写,只能对其中一种类型进行排序,相当于写死了,如果我们在一个程序中需要对 Student 类进行不同字段的排序,很显然这种操作的局限性是很大的,办不到这种需求!

所以我们就要引入一个新的接口:Comparator,通常称之为「比较器」。

Comparator 的使用是类似的,首先要实现 Comparator 接口,然后重写 compare() 方法

代码语言:javascript
复制
class Student { 
    public int age;
    public int score;
    public String name;

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

    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", score=" + score +
                ", name='" + name + '\'' +
                '}';
    }
}

// 年龄比较器
public class AgeComp implements Comparator<Student>  {
    @Override
    public int compare(Student o1, Student o2) {
        return o1.age - o2.age;
    }
}

// 分数比较器
public class ScoreComp implements Comparator<Student> {
    @Override
    public int compare(Student o1, Student o2) {
        return o1.score - o2.score;
    }
}

public class Test {
    public static void main(String[] args) {
        Student s1 = new Student("zhangsan", 18, 60);
        Student s2 = new Student("lisi", 15, 99);
        Student[] s = {s1, s2};

        // 使用年龄对比排序(注释部分是类似源码原理,调用compare(a, b)后返回整型与0比较)
        AgeComp ac = new AgeComp();
        /*if(ac.compare(s1, s2) > 0)
            System.out.println("zhangsan大!");
        else
            System.out.println("lisi大!");*/
        Arrays.sort(s, ac);
        System.out.println(Arrays.toString(s));

        // 使用分数对比排序
        ScoreComp sc = new ScoreComp();
        Arrays.sort(s, sc);
        System.out.println(Arrays.toString(s));
    }
}

Cloneable && 深浅拷贝

Clonable 用于拷贝对象,虽然用的少,但是面试经常问

Object 类中存在一个 clone() 方法,调用这个方法可以创建一个对象的 "拷贝"。其声明如下所示:

代码语言:javascript
复制
protected native Object clone() throws CloneNotSupportedException;

想合法调用 clone 方法,假设当前要拷贝的类为 Person 类型,则有以下规则:

  1. Person 实现 Cloneable 接口(相当于做一个标记,告诉编译器这个类对象可以被克隆,尽管该接口是空接口)
  2. Person 重写 clone() 方法(因为 Object 中的 clone() 方法是 protected 修饰的,非 Person 类中要调用拷贝方法的话必须调用 Person 类中提供的 clone(),不然会有访问权限问题)
  3. 使用时候需要向下转型(因为 Person 类重写 clone() 方法后返回值仍然是 Object,所以在使用 clone() 的时候需要强制转型为 Person 类型才能接收对象)
代码语言:javascript
复制
class Money {
    public double money = 13.14;
}

public class Person implements Cloneable { // 实现Cloneable接口
    public String name;
    Money m = new Money();

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", m=" + m.money +
                '}';
    }

    // 重写拷贝方法
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

注意事项:如果非 Person 类需要用到 Person 类的 clone() 方法,但没有实现 Cloneable 接口,则必须在使用了 clone() 方法的方法中处理 CloneNotSupportedException 异常,如下图所示:

但是这里就涉及到深浅拷贝的问题了,因为 Money 也是一个自定义类型,其对象也是开辟在堆上的,但是因为重写的 clone() 接口中调用的是 Object.clone(),其只做了将当前对象的资源原封不动拷贝到另一个对象中,所以 p1p2 中的 Money 对象指的是同一个内存空间的对象,这就是浅拷贝!如下面代码所示:

代码语言:javascript
复制
public class Test {
    public static void main(String[] args) throws CloneNotSupportedException {
        Person p1 = new Person();
        Person p2 = (Person)p1.clone();
        System.out.println(p1.toString());
        System.out.println(p2.toString());
        System.out.println("==================");

        p1.m.money = 20.1;
        System.out.println(p1.toString());
        System.out.println(p2.toString());
    }
}

// 运行结果:
Person{name='null', m=13.14}
Person{name='null', m=13.14}
==================
Person{name='null', m=20.1}
Person{name='null', m=20.1}

可以看到我们修改了 p1moneyp2 也跟着变了,这在逻辑上是不成立的,所以我们得用深拷贝来解决问题

首先我们也要给 Money 类实现 Cloneable 接口以及重写 clone() 方法:

代码语言:javascript
复制
public class Money implements Cloneable {
    public double money = 13.14;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

然后修改一下 Person 类的 clone() 方法,借助 this 引用完成 Money 类的拷贝:

代码语言:javascript
复制
public class Person implements Cloneable { // 实现Cloneable接口
    ...

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Person p = (Person)super.clone(); // 先进行Person的拷贝
        p.m = (Money)this.m.clone();      // 此时this指向p1,拷贝一份给p.m,才是深拷贝
        return p;
    }
}

// 运行结果:
Person{name='null', m=13.14}
Person{name='null', m=13.14}
==================
Person{name='null', m=20.1}
Person{name='null', m=13.14}

Ⅲ. Object 类

ObjectJava 默认提供的一个类,被定义在 Java.lang 包中,这个包默认会自动导入的,如下所示:

JavaObject 类是所有类的父类,也就是说所有类都默认会继承 Object。即所有类的对象都可以使用 Object 的引用进行接收。

所以我们要学习 Object 类中的所有方法,其实也不多,就是如下图所示的几个,也有很多是我们接触过的:

比如我们之前学过了 toString()clone(),下面我们来学习一下 equalsI() 以及 hashCode() 方法,剩下的方法则会在后面的学习过程不断的接触!

一、对象比较的方法 -- equals()

Java 中,当用 == 进行比较时:

  1. 如果 == 左右两侧是基本类型变量,比较的是变量中是否相同。
  2. 如果 == 两侧是引用类型变量,比较的是引用变量地址是否相同。

所以如果要比较对象中内容,必须重写 Object 中的 equals() 方法,因为 equals() 方法默认也是按照地址比较的,如下所示:

代码语言:javascript
复制
// Object类中的equals方法
public boolean equals(Object obj) {
    return (this == obj); // 使用引用中的地址直接来进行比较
}

下面还是用 Person 类为例子,重写 Person 类中的 equals() 方法,再运行看效果:

代码语言:javascript
复制
class Person {
    public String name;

    public Person(String name) {
        this.name = name;
    }

    @Override
    public boolean equals(Object obj) {
        if(obj == null)                
            return false;
        if(obj == this)
            return true;
        if(!(obj instanceof Person))
            return false;
        Person p = (Person)obj; // 需要向下转型!!!
        return this.name.equals(p.name);
    }
}

public class Test {
    public static void main(String[] args) {
        Person p1 = new Person("liren");
        Person p2 = new Person("liren");

        System.out.println(p1.equals(p2));
    }
}

// 运行结果:
true

二、获取对象内存位置的方法 -- hashCode()

主要应用场景:

  1. 哈希表数据结构中
    1. HashMapHashSetHashtable 等集合类使用 hashCode() 来确定对象的存储位置
    2. 这些集合首先调用 hashCode() 计算哈希值,然后根据哈希值决定对象存储在哪个桶中
  2. 对象比较优化
    1. 在比较两个对象是否相等时,可以先比较 hashCode()
    2. 如果哈希值不同,可以立即确定对象不相等,避免昂贵的 equals() 比较
  3. 作为对象的唯一标识
    1. 在某些情况下,hashCode() 可以作为对象的唯一标识符使用
    2. 但要注意哈希冲突的可能性

下面我们有个需求:我们认为两个名字、年龄相同的对象,应该是同一个对象才对,也就是在内存位置上应该是相同才对,但是如果不重写 hashCode() 方法,我们得到的两个对象的内存位置是不同的,如下所示:

代码语言:javascript
复制
class Person {
    public int age;
    public String name;

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

public class Test {
    public static void main(String[] args) {
        Person p1 = new Person(18, "liren");
        Person p2 = new Person(18, "liren");

        System.out.println(p1.hashCode());
        System.out.println(p2.hashCode());
        System.out.println(p1 == p2);
    }
}

// 运行结果:
1735600054
21685669
false

可以看到两个对象虽然属性相同,但是内存空间是不同的,在本质上不是同一个对象!

但是如果我们重写 Person 类中的 hashCode() 方法的话就不一样了,这里我们需要借助 Objects 类中的 hash() 方法,根据 Person 类的属性来生成哈希值,那么相同的属性就能得到相同的哈希值,最后通过 hashCode() 拿到的就是相同的内存位置,表示它们是同一个对象!所以最后的代码如下所示:

代码语言:javascript
复制
import java.util.Objects; // 需要导入util包
class Person {
    public int age;
    public String name;

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

    @Override
    public int hashCode() {
        return Objects.hash(age, name); // 通过hash()根据属性生成哈希值
    }
}

public class Test {
    public static void main(String[] args) {
        Person p1 = new Person(18, "liren");
        Person p2 = new Person(18, "liren");

        System.out.println(p1.hashCode());
        System.out.println(p2.hashCode());
        System.out.println(p1 == p2);
    }
}

// 运行结果:
102982637
102982637
false

注意事项:虽然我们重写了 hashCode() 方法让其具有相同属性的对象得到相同的哈希值,来让我们觉得它们是同一个对象,但是它们在内存上实际的地址还是不同的,这可以通过上面代码中的 p1 == p2 来判断:无论上面的哈希值是否相同,都不能作为判断是否为同一个对象的标准,因为这个哈希值只是根据属性得到的,而不是通过内存地址得到的

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Ⅰ. 抽象类 -- abstract
    • 一、概念
    • 二、抽象类的语法
    • 三、抽象类的特性
  • Ⅱ. 接口 -- interface
    • 一、接口的使用 -- implements
    • 二、接口与抽象类的区别
    • 三、一个类实现多个接口
    • 四、接口之间的继承
    • 五、常用接口
      • ① Comparable
      • ② Comparator
      • ③ Cloneable && 深浅拷贝
  • Ⅲ. Object 类
    • 一、对象比较的方法 -- equals()
    • 二、获取对象内存位置的方法 -- hashCode()
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档