【Java】抽象类和接口


一、抽象类

1.1 什么是抽象类

在Java中,抽象类是一种特殊的类,用来表示具有一般特征和行为的抽象概念。它不能够直接实例化,而是作为其他类的基类或者父类来使用。

抽象类通过在类声明中使用关键字abstract来定义。它可以包含抽象方法和非抽象方法。抽象方法是没有具体实现的方法,只有方法签名,用关键字abstract关键字修饰;非抽象方法则具有具体的实现。

抽象类的主要目的是提供一种通用的基类,将相关的子类归类在一起,以便共享通用的属性和行为。它可以定义一些通用方法,供子类继承和实现。

1.2 抽象类的语法

抽象类的基本语法如下面的代码所示:

abstract class AbstractClass {
    // 抽象方法
    public abstract void abstractMethod();

    // 非抽象方法
    public void nonAbstractMethod() {
        // 方法实现
    }
    
    // 构造方法
    public AbstractClass() {
        // 构造方法实现
    }
}

抽象类的语法要点如下:

  1. 使用关键字 abstract 来声明抽象类,将类定义为抽象类。抽象类不能被实例化

  2. 抽象类可以包含抽象方法和非抽象方法。

  3. 抽象方法使用 abstract 关键字进行声明,并且没有方法体。它们只有方法签名,用于定义方法的名称、参数列表和返回类型。子类必须实现(覆盖、重写)抽象方法,否则子类就应该声明为抽象类。

  4. 非抽象方法是具有实现的方法,可以在抽象类中直接定义方法体。子类可以直接继承非抽象方法的实现

  5. 抽象类可以包含构造方法。构造方法在子类实例化时调用父类的构造方法。

需要注意的是,如果一个类包含一个或多个抽象方法,那么该类必须被声明为抽象类。另外,抽象类可以被继承,但不能被标记为final,因为它需要被子类扩展和实现

以下是一个更完整的示例,展示了抽象类的语法和使用:

abstract class AbstractClass {
    // 抽象方法
    public abstract void abstractMethod();

    // 非抽象方法
    public void nonAbstractMethod() {
        System.out.println("This is a non-abstract method.");
    }

    // 构造方法
    public AbstractClass() {
        System.out.println("AbstractClass constructor");
    }
}

class ConcreteClass extends AbstractClass {
    // 实现抽象方法
    public void abstractMethod() {
        System.out.println("Abstract method implementation");
    }
}

public class Main {
    public static void main(String[] args) {
        ConcreteClass concrete = new ConcreteClass();
        concrete.abstractMethod();
        concrete.nonAbstractMethod();
    }
}

在上面的示例中,AbstractClass是一个抽象类,定义了一个抽象方法abstractMethod()和一个非抽象方法nonAbstractMethod()ConcreteClass继承了AbstractClass并实现了抽象方法。在Main类的main()方法中,我们实例化了ConcreteClass的对象,并调用了抽象方法和非抽象方法。

输出结果为:

AbstractClass constructor
Abstract method implementation
This is a non-abstract method.

以上是抽象类的基本语法和用法。通过使用抽象类,我们可以定义一些共享的行为和属性,并要求子类实现特定的方法,从而实现更灵活和可扩展的类设计。

1.3 抽象类的特性

抽象类具有以下特性:

  1. 无法实例化:抽象类不能直接实例化,也就是说,不能使用 new 关键字来创建抽象类的对象。只能通过继承抽象类并实现其中的抽象方法,创建其子类的对象。

  2. 可以包含抽象方法:抽象类可以包含一个或多个抽象方法。抽象方法是没有具体实现的方法,只有方法的声明,没有方法体。抽象方法使用 abstract 关键字进行声明,并且必须在抽象类中定义。子类必须实现(覆盖)抽象方法,否则子类也必须声明为抽象类。

  3. 可以包含非抽象方法:抽象类可以包含非抽象方法,这些方法具有具体的实现。子类可以直接继承和使用这些非抽象方法。

  4. 可以包含成员变量和常量:抽象类可以包含成员变量和常量。这些成员变量和常量可以被子类继承和使用。

  5. 可以包含构造方法:抽象类可以有构造方法,用于初始化抽象类的实例。子类在实例化时会调用父类的构造方法。

  6. 可以被继承:抽象类可以作为其他类的基类或父类,被其他类继承。子类继承抽象类后,必须实现(覆盖)抽象类中的所有抽象方法,除非子类自身也是抽象类。

  7. 用于抽象概念建模:抽象类常用于对具有相似特征和行为的类进行建模。它们可以捕捉类之间的共同特征,并提供一种通用的基类,以便共享通用的属性和行为。

  8. 不能被标记为 finalstatic:抽象类不能被标记为 finalstatic,因为它需要被其他类继承和实现。

抽象类的主要目的是作为类继承层次结构中的一个抽象概念,将相关的子类归类在一起,并为子类提供共享的行为和属性。通过使用抽象类,可以实现代码的重用性和可扩展性,同时还能够强制要求子类实现特定的方法。

1.4 抽象类的作用

抽象类在Java中有多种作用和用途,包括以下几个方面:

  1. 定义类层次结构:抽象类可以用于定义类的继承层次结构。它可以作为其他类的基类或父类,用于将一组相关的子类进行分类和组织。通过抽象类,可以将具有共同特征和行为的类进行归类,并将通用的属性和方法提取到抽象类中。

  2. 提供默认实现:抽象类可以包含非抽象方法的实现。这些非抽象方法提供了默认的行为,可以被子类继承和使用。通过在抽象类中定义通用的实现,可以减少子类中重复的代码,提高代码的重用性。

  3. 强制子类实现特定方法:抽象类可以包含抽象方法,这些方法没有具体的实现,只有方法签名。子类继承抽象类时,必须实现(覆盖)抽象方法,以提供特定的实现逻辑。通过抽象类,可以强制子类实现一些必要的方法,确保它们在正确的上下文中被调用。

  4. 提供模板方法:抽象类可以定义模板方法,这些方法提供了算法的骨架,而具体的实现可以在子类中完成。模板方法模式通过抽象类的模板方法和具体子类的实现方法,实现了算法的固定结构和可变部分的分离。这样可以方便地扩展和定制算法的实现。

  5. 提供共享的成员变量和常量:抽象类可以包含成员变量和常量,这些成员可以被子类继承和使用。通过在抽象类中定义共享的属性,可以避免在每个子类中重复定义相同的属性,提高代码的可维护性和一致性。

总的来说,抽象类提供了一种机制,用于定义类之间的共同特征和行为,并要求子类实现特定的方法。它促进了代码的重用性、可扩展性和模块化设计。通过使用抽象类,可以更好地组织和管理类的继承关系,并确保类在正确的上下文中被使用和实现。

二、接口

2.1 什么是接口

接口(Interface)是一种抽象的参照类型(Reference Type),它定义了一组方法的规范,但没有具体的实现。接口提供了一种契约机制,用于描述类应该具有的行为,而不涉及具体的实现细节。

在Java中,接口具有以下特点:

  1. 接口使用interface关键字来定义。接口可以包含方法的声明、常量的定义和默认方法的实现。
  2. 接口中的方法默认为抽象方法,即没有方法体。在接口中声明方法时,不需要使用abstract关键字进行标识。
  3. 接口中的常量默认为public static final,即常量的值不能够修改。
  4. 接口可以继承其他接口,使用extends关键字进行声明,一个接口可以继承多个接口,实现接口的继承层次结构。
  5. 一个类可以实现一个或多个接口,使用 implements 关键字进行声明。一个类实现接口时,必须实现接口中声明的所有方法,提供具体的实现。
  6. 接口可以用于实现多态性。通过接口引用,可以引用实现该接口的任意对象,并调用接口中的方法。

接口的主要目的是定义类应该具备的行为和能力,实现了接口的类必须提供接口中定义的方法的具体实现。通过使用接口,可以实现代码的解耦和模块化设计,提高代码的可维护性和可扩展性。接口还促进了面向接口编程的思想,实现了更松散耦合的系统架构。

2.2 接口的语法规则

接口(Interface)在Java中遵循以下语法规则:

  1. 声明接口
interface 接口名 {
    // 常量声明(可选)
    // 方法声明(抽象方法)
}

使用 interface 关键字声明一个接口,接口名应该符合标识符的命名规则。

  1. 声明方法
    接口中的方法默认为抽象方法,没有具体的实现。方法声明的语法如下:
[访问修饰符] 返回类型 方法名(参数列表);

其中,访问修饰符可以是 public 或者省略(默认为 public)。方法的返回类型可以是任意合法的数据类型,或者是另一个接口类型。

  1. 声明常量
    接口中的常量默认为 public static final,即常量的值不能修改。常量声明的语法如下:
[访问修饰符] static final 数据类型 常量名 =;

其中,访问修饰符可以是 public 或者省略(默认为 public)。常量的数据类型可以是任意合法的数据类型。

  1. 继承接口
    接口可以继承其他接口,使用 extends 关键字进行声明。一个接口可以继承多个接口,形成接口的继承层次结构。
interface 子接口名 extends 父接口1, 父接口2, ... {
    // ...
}

  1. 实现接口
    类可以实现一个或多个接口,使用 implements 关键字进行声明。一个类实现接口时,必须提供接口中声明的所有方法的具体实现。
class 类名 implements 接口1, 接口2, ... {
    // ...
}

如果一个类实现了接口,那么它必须实现接口中的所有抽象方法,否则类本身必须声明为抽象类。

2.3 接口的使用

接口不能直接使用,必须要有一个 实现类实现该接口,并且还有实现接口中的所有抽象方法。需要注意的是,子类和父类之间是extends继承关系,类与接口之间是 implements 实现关系。

以下是实现笔记本电脑使用USB鼠标、USB键盘的例子:

  1. USB接口:包含打开设备、关闭设备功能
  2. 笔记本类:包含开机功能、关机功能、使用USB设备功能
  3. 鼠标类:实现USB接口,并具备点击功能
  4. 键盘类:实现USB接口,并具备输入功能
// USB接口
interface USB {
	// 打开设备
    void openDevice();
    // 关闭设备
    void closeDevice();
}


// 鼠标类,实现USB接口
class Mouse implements USB {
    @Override
    public void openDevice() {
        System.out.println("打开鼠标");
    }

    @Override
    public void closeDevice() {
        System.out.println("关闭鼠标");
    }

    public void click() {
        System.out.println("鼠标点击");
    }
}

// 键盘类,实现USB接口
class KeyBoard implements USB {
    @Override
    public void openDevice() {
        System.out.println("打开键盘");
    }

    @Override
    public void closeDevice() {
        System.out.println("关闭键盘");
    }

    public void inPut() {
        System.out.println("键盘输入");
    }
}

// 笔记本类:使用USB设备
class Computer {
    public void powerOn() {
        System.out.println("打开笔记本电脑");
    }

    public void powerOff() {
        System.out.println("关闭笔记本电脑");
    }
	
    public void useDevice(USB usb) {
        usb.openDevice();
        if (usb instanceof Mouse) {
            Mouse mouse = (Mouse) usb;
            mouse.click();
        } else if (usb instanceof KeyBoard) {
            KeyBoard keyBoard = (KeyBoard) usb;
            keyBoard.inPut();
        }
        usb.closeDevice();
    }
}

// 测试类:
public class Test2 {
    public static void main(String[] args) {
        Computer computer = new Computer();
        computer.powerOn();
        // 使用鼠标设备
        computer.useDevice(new Mouse());
        // 使用键盘设备
        computer.useDevice(new KeyBoard());
        computer.powerOff();
    }
}

上述代码展示了一个使用USB设备的示例。

首先,定义了一个USB接口,其中包含了openDevice()closeDevice()两个方法,表示打开和关闭设备的操作。

接着,定义了鼠标类Mouse和键盘类KeyBoard,它们都实现了USB接口,并重写了接口中的方法。除此之外,鼠标类还有一个额外的方法click()表示鼠标的点击操作,键盘类也有一个额外的方法inPut()表示键盘的输入操作。

然后,定义了一个笔记本类Computer,它具有开机和关机的功能,以及一个useDevice()方法用于使用USB设备。useDevice()方法接收一个USB接口的实例作为参数,并根据具体的实例类型执行相应的操作。如果是鼠标设备,就调用鼠标的点击方法;如果是键盘设备,就调用键盘的输入方法。

最后,在测试类Test2中创建了一个笔记本电脑对象,并依次执行了打开电脑、使用鼠标设备、使用键盘设备和关闭电脑的操作。

运行测试类,输出如下结果:

打开笔记本电脑
打开鼠标
鼠标点击
关闭鼠标
打开键盘
键盘输入
关闭键盘
关闭笔记本电脑

通过接口的使用,我们可以通过统一的方法来操作不同的USB设备,提高了代码的灵活性和可扩展性。同时,也实现了多态性的特性,使得可以根据具体的设备类型进行不同的操作。

2.4 实现多个接口

在Java中,一个类可以实现多个接口,以获得多个接口所定义的行为和能力。实现多个接口的语法如下:

class MyClass implements Interface1, Interface2, Interface3 {
    // 实现接口中的抽象方法
    // ...
}

在上述示例中,MyClass 类同时实现了 Interface1Interface2Interface3 这三个接口。接口之间使用逗号进行分隔。

实现多个接口时,需要确保实现类提供了每个接口中声明的所有抽象方法的具体实现。如果某个接口的方法在实现类中没有被实现,则该实现类必须声明为抽象类。

实现多个接口使得类能够同时具备这些接口所定义的行为和能力。通过多态性,可以使用接口类型的引用来引用实现类的对象,并调用接口中定义的方法。这样可以在编程中实现灵活性和扩展性,同时允许代码以接口为基础进行设计和组织。

需要注意的是,当多个接口中存在同名的默认方法时,实现类必须重写这个方法来消除冲突。否则,编译器会报错,要求显式地在实现类中提供方法的具体实现。

class MyClass implements Interface1, Interface2 {
    // 实现接口1中的抽象方法
    // 实现接口2中的抽象方法
    
    // 重写接口1和接口2中的同名默认方法
}

实现多个接口是一种强大的工具,可以使类具备多重行为和能力,同时遵循面向接口编程的原则,提高代码的灵活性和可维护性。

2.5 接口间继承

在Java中,接口之间可以进行继承,这样一个接口可以继承自另一个或多个接口,形成接口的继承层次结构。接口继承的语法如下:

interface SubInterface extends SuperInterface {
    // ...
}

在上述示例中,SubInterface 接口继承了 SuperInterface 接口。这表示 SubInterface 接口继承了 SuperInterface 接口所声明的方法和常量,并且可以在 SubInterface 接口中添加新的方法和常量。

接口继承的特点如下:

  • 一个接口可以继承多个接口,使用逗号进行分隔。例如:interface SubInterface extends Interface1, Interface2 { ... }
  • 接口继承可以形成多层继承关系,类似于类的继承结构。
  • 继承的接口可以拥有自己的抽象方法和常量,并且可以在子接口中添加新的抽象方法和常量。
  • 子接口可以继承父接口的默认方法,并可以选择性地重写或继承这些默认方法。
  • 子接口继承了父接口的类型,可以作为父接口类型的引用。

通过接口的继承,可以实现更加抽象和通用的接口定义,促进代码的模块化和可复用性。接口继承还可以构建更丰富和灵活的接口层次结构,使得接口之间的关系更加清晰和易于管理。

2.6 Comparable 接口

现在有一个Student类,其包含的属性有名字name和年龄age,要求对其对象按照年龄进行升序排序:

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 +
                '}';
    }
}

再给定一个学生对象数组, 对这个对象数组中的元素按照年龄进行升序排序。

Student[] students = new Student[] {
	new Student("张三", 25),
	new Student("李四", 16),
	new Student("王五", 14),
	new Student("赵六", 20),
};

在学习数组的时候,我们可以使用Arrays中的sort方法对数组中的元素进行排序,那么是否也可以对学生数组进行排序呢?

Arrays.sort(students);
System.out.println(Arrays.toString(students));

运行代码可以发现抛出异常:ClassCastException,即不能将Student类型转换为java.lang.Comparable类型。

当我们思考一下,就会发现:如果是整数类型,当然可以直接比较出大小,而Student类型更加复杂,如果直接比较是按照什么来进行比较呢?因此需要我们自己指定比较的方式。

在Java中,如果需要实现类对象之间的比较,需要让该类实现Comparable接口,并实现其中的compateTo方法。Comparable 接口是在对象自身类中实现的,表示该类的对象具备自然顺序。

class Student implements Comparable<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 +
                '}';
    }

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

sort 方法中会自动调用 compareTo 方法 compareTo 的参数是 Student 类型的对象。然后比较当前对象和参数对象的大小关系(按年龄来算)。

  • 如果是当前对象的年龄减去比较对象的年龄,则实现的是升序排序;
  • 如果是比较对象的年龄减去当前对象的年龄,则实现的是降序排序。

当再次运行上面的代码逻辑的时候,我们就能够实现按照Student的年龄进行升序排序了。

但是,如果是使用Comparable接口实现对象之间的比较,如果要在后面改变对象之间的比较规则,并且如果项目已经发布一段时间,那么显然就很难修改了,因此Comparator 接口的作用就体现出来了。

2.7 Comparator 接口

Comparator 接口是一个独立的比较器,可以在对象类外部实现,用于对不同类的对象进行比较。通过实现其中的 compare() 方法来定义对象之间的比较规则。

  • compare() 方法接收两个参数,表示要比较的两个对象,返回一个整数值表示比较结果。
  • Comparator 接口常用于对某个类的对象进行临时的比较和排序,而不需要修改该类的代码。

对于自定义的类,如果需要按照不同的属性进行排序,可以通过实现 Comparator 接口来定义不同的比较器。例如,对上述的学生类,分别通过实现Comparator 接口,完成对姓名和年龄的比较:

class AgeComparator implements Comparator<Student>{

    @Override
    public int compare(Student o1, Student o2) {
        return o1.age - o2.age;
        // return o2.age - o1.age;
    }
}


class NameComparator implements Comparator<Student>{

    @Override
    public int compare(Student o1, Student o2) {
        return o1.name.compareTo(o2.name);
        // return o2.name.compareTo(o1.name);
    }
}

通过对年龄的比较,进行升序排序:

public static void main(String[] args) {
    Student[] students = new Student[] {
            new Student("张三", 25),
            new Student("李四", 16),
            new Student("王五", 14),
            new Student("赵六", 20),
    };

    AgeComparator ageComparator = new AgeComparator();
    Arrays.sort(students, ageComparator);
    
    System.out.println(Arrays.toString(students));
}

通过对姓名的比较进行排序:

public static void main(String[] args) {
    Student[] students = new Student[] {
            new Student("张三", 25),
            new Student("李四", 16),
            new Student("王五", 14),
            new Student("赵六", 20),
    };
    
    NameComparator nameComparator = new NameComparator();
    Arrays.sort(students, nameComparator);

    System.out.println(Arrays.toString(students));
}

2.8 Cloneable 接口和深拷贝

Object 类中存在一个 clone 方法,调用这个方法可以创建一个对象的 拷贝。但是要想合法调用 clone 方法,必须要先实现 Clonable 接口,否则就会抛出 CloneNotSupportedException 异常。

例如下面的代码:

class Person implements Cloneable{
    public int id;
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    @Override
    public String toString() {
        return "Person{" +
                "id=" + id +
                '}';
    }
}

在上面的代码中,Person类实现了Cloneable接口,并且完成了对clone方法的重写。

public static void main(String[] args) throws CloneNotSupportedException {
    Person person = new Person();

    Person person1 = (Person) person.clone();
    Person person2 = person;

    System.out.println(person == person1); // 运行结果:false
    System.out.println(person == person2); // 运行结果:true
}

通过运行这段代码,就能够发现是否完成了克隆的功能。

关于深浅拷贝的问题

在创建一个Money类,并且让其作为Student类的一个成员变量:

class Money {
    public double m = 0.00;

    @Override
    public String toString() {
        return "Money{" +
                "m=" + m +
                '}';
    }
}

class Person implements Cloneable {
    public int id;

    public Money money = new Money();

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

    @Override
    public String toString() {
        return "Person{" +
                "id=" + id +
                ", money=" + money +
                '}';
    }
}

然后再执行以下代码:

public static void main(String[] args) throws CloneNotSupportedException {
    Person person = new Person();
    Person person1 = (Person) person.clone();

    person.money.m = 10.00;
    person.id = 1;
    person1.money.m = 12.00;
    person1.id = 2;

    System.out.println(person);
    System.out.println(person1);
}

运行结果:

通过对比可以发现,其实对Money对象实现的是浅拷贝,其内存结构大概是:

即引用类型变量person和person1指向的Money对象为同一个,这就是浅拷贝。如果要实现对Money的深拷贝,Money对象也需要实现Cloneable接口。

class Money implements Cloneable {
    public double m = 0.00;

    @Override
    public String toString() {
        return "Money{" +
                "m=" + m +
                '}';
    }

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


class Person implements Cloneable {
    public int id;

    public Money money = new Money();

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Person tmp = (Person) super.clone();
        tmp.money = (Money) this.money.clone();
        return tmp;
    }

    @Override
    public String toString() {
        return "Person{" +
                "id=" + id +
                ", money=" + money +
                '}';
    }
}

此时,再次运行main方法,其结果如下:

由此,便实现了对Money的深拷贝。

2.9 抽象类和接口的区别

抽象类和接口是面向对象编程中的两个重要概念,它们有以下区别:

  1. 继承关系:

    • 抽象类使用类的继承机制,子类通过继承抽象类来获得共享的属性和方法,并可以进行扩展和重写。
    • 接口使用实现机制,类通过实现接口来声明自己能够提供某些行为,一个类可以实现多个接口。
  2. 构造方法:

    • 抽象类可以有构造方法,可以用于初始化共享的属性和执行一些公共逻辑。
    • 接口不能有构造方法,因为接口主要用于定义行为而不是实现。
  3. 成员变量和常量:

    • 抽象类可以包含实例变量、静态变量和常量。
    • 接口只能包含常量,即接口中的变量默认为 public static final
  4. 方法实现:

    • 抽象类可以包含抽象方法和具体方法的实现。
    • 接口只能包含抽象方法,即方法声明而没有具体实现。
    • Java 8 之后,接口也可以包含默认方法和静态方法的实现。
  5. 被实现/继承的数量:

    • 一个类可以实现多个接口,从而获得多个接口提供的行为。
    • 一个类只能继承一个抽象类。
  6. 设计目的:

    • 抽象类用于表示一种类的继承关系和层次结构,它体现了"is-a"的关系。
    • 接口用于表示一种类的能力或行为,它体现了"has-a"的关系。

选择抽象类还是接口取决于具体的需求和设计目的。如果需要共享一些通用的属性和方法,并且涉及到类的继承关系,可以选择抽象类。如果需要定义一组行为或能力,并且与类的继承关系无关,可以选择接口。有时候也可以将抽象类和接口结合使用,以满足不同的需求。

三、Object 类

3.1 认识 Object 类

Object是Java默认提供的一个类。Java里面除了Object类,所有的类都是存在继承关系的。默认会继承Object父类。即所有类的对象都可以使用Object的引用进行接收。

class A{}
class B{}
public class Test {
    public static void main(String[] args) {
        function(new A());
        function(new B());
    }
    public static void function(Object obj) {
        System.out.println(obj);
    }
}

运行结果:

3.2 获取对象信息

如果要打印对象中的内容,可以直接重写Object类中的toString()方法,Object类中的toString()方法实现:

public String toString() {
	return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

3.3 对象比较 equals 方法

在Java中,使用==进行比较时:

  1. 如果== 左右两侧是基本类型变量,比较的是变量中值是否相同。
  2. 如果==左右两侧是引用类型变量,比较的是引用变量地址是否相同。
  3. 如果要比较对象中内容,必须重写Object中的equals方法,因为equals方法默认也是按照地址比较的。

Object类中的equals方法:

public boolean equals(Object obj) {
	return (this == obj);  // 使用引用中的地址直接来进行比较
}

Student类重写equals方法:

class Student {
    public String name;
    public int age;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return age == student.age && Objects.equals(name, student.name);
    }
}

因此在比较对象中内容是否相同的时候,一定要重写equals方法。

3.4 hashcode 方法

回忆Object类中的toString方法:

public String toString() {
	return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

发现其中有一个hashCode()方法,它的作用是为我们算一个具体的对象位置,即内存地址。然后调用Integer.toHexString()方法,将这个地址以16进制输出。

继续查看hashCode源码:

public native int hashCode();

该方法是一个native方法,底层是由C/C++代码实现的。

Student类中,通常认为name和age相同的两个对象是同一个人,如果不重写hashCode方法,其结果如下:

class Student implements Comparable<Student>{
    public String name;
    public int age;

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

public class Test1 {

    public static void main(String[] args) {
        Student student1 = new Student("张三", 18);
        Student student2 = new Student("张三", 18);

        System.out.println(student1.hashCode());
        System.out.println(student2.hashCode());
    }
}

运行结果如下,表现为两个不同的对象:

Student类中重写hashCode方法:

@Override
public int hashCode() {
    return Objects.hash(name, age);
}

再次运行,此时就表现为一个对象了。

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
假设我们要设计一个动物园的程序,其中有多种动物,每种动物都有自己的特性和行为。这时候,我们可以使用面向对象的设计思想来实现这个程序。 首先,我们可以定义一个抽象类 Animal,它包含动物的基本属性和行为,例如: ```java public abstract class Animal { protected String name; protected int age; public Animal(String name, int age) { this.name = name; this.age = age; } public abstract void eat(); public abstract void sleep(); public abstract void makeSound(); } ``` 在这个抽象类中,我们定义了动物的基本属性 name 和 age,以及三个抽象方法 eat、sleep 和 makeSound,表示动物的基本行为。由于每种动物的具体实现不同,因此这些方法只是声明而不实现,留给子类去具体实现。 接下来,我们可以定义几个具体的动物子类,例如: ```java public class Lion extends Animal { public Lion(String name, int age) { super(name, age); } @Override public void eat() { System.out.println("Lion is eating meat."); } @Override public void sleep() { System.out.println("Lion is sleeping."); } @Override public void makeSound() { System.out.println("Roar!"); } } public class Elephant extends Animal { public Elephant(String name, int age) { super(name, age); } @Override public void eat() { System.out.println("Elephant is eating grass."); } @Override public void sleep() { System.out.println("Elephant is sleeping."); } @Override public void makeSound() { System.out.println("Trumpet!"); } } ``` 在这些子类中,我们重写了父类中的抽象方法,并实现了具体的行为。例如,Lion 类中的 eat 方法表示狮子吃肉,而 Elephant 类中的 eat 方法表示大象吃草。 最后,我们可以在主程序中创建一个动物园对象,添加各种不同的动物,例如: ```java public class Zoo { private List<Animal> animals; public Zoo() { animals = new ArrayList<>(); } public void addAnimal(Animal animal) { animals.add(animal); } public void showAnimals() { for (Animal animal : animals) { System.out.println(animal.name + " is " + animal.age + " years old."); animal.eat(); animal.sleep(); animal.makeSound(); System.out.println(); } } } public class Main { public static void main(String[] args) { Zoo zoo = new Zoo(); zoo.addAnimal(new Lion("Simba", 3)); zoo.addAnimal(new Elephant("Dumbo", 5)); zoo.showAnimals(); } } ``` 在主程序中,我们创建了一个动物园对象 zoo,添加了一只狮子和一头大象,然后展示了它们的基本信息和行为。运行程序,可以看到如下输出: ``` Simba is 3 years old. Lion is eating meat. Lion is sleeping. Roar! Dumbo is 5 years old. Elephant is eating grass. Elephant is sleeping. Trumpet! ``` 通过这个例子,我们可以看到抽象类接口的使用。Animal 类作为抽象类,定义了动物的基本属性和行为,并声明了一些抽象方法,留给子类去实现。而 Lion 和 Elephant 类作为具体的子类,继承了 Animal 类,并实现了它的抽象方法,以便具体实现狮子和大象的行为。最后,在 Zoo 类中,我们使用 Animal 类的对象来表示动物,从而可以添加不同种类的动物并展示它们的基本信息和行为。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

求知.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值