切换语言为:繁体

基于 Java 谈谈「面向对象」

  • 爱糖宝
  • 2024-05-15
  • 2080
  • 0
  • 0

一、面向对象概述

【概念】

面向对象(Object Oriented),对Java语言来说,一切皆是对象。把现实世界中的对象抽象地体现在编程世界中,一个对象代表了某个具体的操作。一个个对象最终组成了完整的程序设计,这些对象可以是独立存在的,也可以是从别的对象继承过来的。对象之间通过相互作用传递信息,实现程序开发。

【对象】

所谓对象就是真实世界中的实体,对象与实体是一一对应的,也就是说现实世界中每一个实体都是一个对象,它是一种具体的概念。

【特点】

  1. 对象具有属性和行为。

  2. 对象具有变化的状态。

  3. 对象具有唯一性。

  4. 对象都是某个类别的实例。

  5. 一切皆为对象,真实世界中的所有事物都可以视为对象。

【例如】

现实生活中我们需要购物,其中会有顾客、商家、商品等实体,顾客有姓名、住址等属性,还有下单、付款、退单等操作。这里的顾客只是抽象的描述成为顾客“类”,但是在你执行购物的时候每一个顾客就是一个具体的对象,比如王老板去下单了螺狮粉。

二、面向对象核心特性

1、封装

【概念】

封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问。但是可以提供一些可以被外界访问的方法来操作属性。

【优点】

优点

说明

提高代码安全性

保护类中的信息,阻止外部代码随意访问内部属性。

接口的统一性

通过封装类和方法实现标准化的接口,提升开发效率。

降低模块耦合

当一个模块的逻辑发生变化时,只要它的接口不变,就不会影响其他系统的使用。

【特点】

Java 语言的基本封装单位是类,通过封装可以隐藏内部的复杂实现逻辑,然后提供外部访问的方法供需要者使用。

2、继承

【概念】

继承是指子类拥有父类的全部特征和行为,这是类之间的一种关系。Java 只支持单继承,通过继承可以复用父类的代码,以其为基础可以增加新的功能和数据,也可使用父类的功能。

【优点】

优点

说明

提升开发效率

减少创建类的工作量,子类直接拥有父类的方法和属性

提高代码复用性

复用父类功能,减少代码冗余,提高程序运行效率

提高代码的可扩展性

更好的实现父类的方法,方便功能扩展

【注意事项】

  1. Java使用extends关键字实现继承;

  2. Java只允许单继承,但可以多层继承;

  3. 子类无法继承private 成员;

  4. 构造方法不能被继承;

  5. 子类通过super使用父类成员,通过this使用本类成员;

3、多态

【概念】

即“一个方法,多个实现”。多态体现在父类中定义的属性和方法被子类继承后,可以具有不同的属性或表现方式。多态性允许一个接口被多个同类使用,弥补了单继承的不足。

简单说,就是设定好某个行为,但是不同对象去完成这个行为,得到的结果是不同的。

【分类】

分类

说明

编译时多态

主要是指方法的重载,通过编译之后会变成两个不同的方法。

运行时多态

当传递不同类对象时,会动态调用对应类中的方法。

【实现条件】

  1. 继承:在多态中必须存在有继承关系的子类和父类;

  2. 重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法;

  3. 父类引用指向子类对象:只有这样该引用才既能可以调用父类的方法,又能调用子类的方法。

【特点】

  1. 子类和父类存在同名的成员变量时,访问的是父类的成员变量;

  2. 子父类存在同名的非静态成员方法时,访问的是子类中重写的方法;

  3. 子父类存在同名的静态成员变量成员方法时,访问的是父类的成员函数;

  4. 不能访问子类独有的方法(需要进行引用类型转换)。

【引用类型转换】

分类

格式

说明

向上转型

父类 变量名 = new 子类();

将子类的引用赋给父类对象,是自动转换的

向下转型

子类 变量名 = (子类)父类变量名;

将父类对象转化成子类对象,需要进行强转

【例如】

public class Animal {
    public String name = "animal";
    public void eat() {
        System.out.println("午餐吃肉");
    }
}


public class Dog extends Animal{
    public String name = "dog";
    
    public void run(){
        System.out.println("狗跑的飞起");
    }
    
    @Override
    public void eat() {
        System.out.println("狗吃狗粮");
    }
}
public class FuXing {
    public static void main(String[] args) {
        // 父类类型 对象 = new 子类类型()--向上转型
        Animal dog = new Dog();
        // 多态中成员变量特点--编译运行看左边
        System.out.println(dog.name);
        // 成员方法特点--编译看左边,运行看右边
        dog.eat();
        // 不能访问子类独由的方法(须进行强转,否则报错)--向下转型
        ((Dog) dog).run();
        //输出结果:animal 狗吃狗粮 狗跑的飞起
    }
}


三、instanceof关键字

【概述】

在Java中可以使用instanceof关键字判断一个对象是否为一个类(或接口、抽象类、父类)的实例,它可以帮助我们避免出现ClassCastException这样的异常。

【语法格式】

boolean result = obj instanceof Class;

其中,obj是一个对象,Class表示一个类或接口。obj是class类(或接口)的实例或者子类实例时,结果result返回true,否则返回false。

【用法】

public class FuXing {
    public static void main(String[] args) {
        // 1.声明一个class类的对象,判断obj是否为class类的实例对象(很普遍的一种用法)
        String name = "fuxing";
        System.out.println(name instanceof String); // true
        // 2.声明一个class接口实现类的对象obj,判断obj是否为class接口实现类的实例对象
        ArrayList arrayList = new ArrayList();
        System.out.println(arrayList instanceof List);  // true
        // 3.obj是class类的直接或间接子类
        Animal a1 = new Animal();
        Dog dog = new Dog();
        Animal a2 = new Dog();
        System.out.println(a1 instanceof Dog);  // false --因为Animal是Dog的父类
        System.out.println(dog instanceof Dog);  // true
        System.out.println(a2 instanceof Dog);  // true
    }
}

四、对象、类和接口

1、对象

【概述】

在面向对象中,类和对象是最基本、最重要的组成单元。类实际上是表示一个客观世界某类群体的一些基本特征抽象。对象就是表示一个个具体的东西。所以说类是对象的抽象,对象是类的具体。

【对象创建】

分类

创建方式

格式

显式

创建

对象

  1. 使用 new 关键字

父类类型 变量名 = new 子类类型();

  1. 调用java.lang.Class或java.lang.reflect.Constuctor类的newlnstance()实例方法

Class 类对象名称 = Class.forName(要实例化的类全称);

类名 对象名 = (类名)Class类对象名称.newInstance();

  1. 调用对象的clone()方法

要实例化的类必须继承java.lang.Cloneable 接口

类名对象名 = (类名)已创建好的类对象名.clone();

隐含

创建

对象

  1. String name = "fuxing",其中的"fuxing"就是一个String对象,由Java虚拟机隐含地创建;

  2. 字符串的"+"运算符运算的结果为一个新的String对象;

  3. 当Java虚拟机加载一个类时,会隐含地创建描述这个类的 Class 实例。

【示例】

public class Student implements Cloneable {   
    // 实现 Cloneable 接口
    private String Name;    // 学生名字
    private int age;    // 学生年龄
    public Student(String name,int age) {    
        // 构造方法
        this.Name = name;
        this.age = age;
    }
    public Student() {
        this.Name = "name";
        this.age = 0;
    }
    public String toString() {
        return"学生名字:"+Name+",年龄:"+age;
    }
    public static void main(String[] args)throws Exception {       
        // 使用new关键字创建对象
        Student student1 = new Student("小刘",22);
        System.out.println(student1);
       
        // 调用 java.lang.Class 的 newInstance() 方法创建对象
        Class c1 = Class.forName("Student");
        Student student2 = (Student)c1.newInstance();
        System.out.println(student2);
        
        // 调用对象的 clone() 方法创建对象
        Student student3 = (Student)student2.clone();
        System.out.println(student3);
    }
}

【创建对象步骤】

  1. 给对象分配内存;

  2. 将对象的实例变量自动初始化为其变量类型的默认值;

  3. 初始化对象,给实例变量赋予正确的初始值;

【匿名对象】

每次new都相当于开辟了一个新的对象,并开辟了一个新的物理内存空间。如果一个对象只需要使用唯一的一次,就可以使用匿名对象,匿名对象还可以作为实际参数传递。匿名对象就是没有明确的给出名字的对象,是对象的一种简写形式。一般匿名对象只使用一次,而且匿名对象只在堆内存中开辟空间,而不存在栈内存的引用。

例如,直接使用了new Animal("Dog")语句,这实际上就是一个匿名对象,与之前声明的对象不同,此处没有任何栈内存引用它,所以此对象使用一次之后就等待被 GC(垃圾收集机制)回收。

【注意事项】

  1. 每个对象都是相互独立的,在内存中占有独立的内存地址;

  2. 每个对象都具有自己的生命周期;

  3. 当一个对象的生命周期结束时,对象就变成了垃圾,由 Java 虚拟机自带的垃圾回收机制处理。

【对象销毁】

对象使用完之后需要对其进行清除。对象的清除是指释放对象占用的内存。在创建对象时,用户必须使用 new 操作符为对象分配内存。不过,在清除对象时,由系统自动进行内存回收,不需要用户额外处理。Java语言的内存自动回收称为垃圾回收(Garbage Collection)机制,简称 GC,详情请看垃圾回收。

【空对象null】

null是一种特殊的引用数据类型,一个引用变量(当变量指向一个对象时,这个变量就被称为引用变量)没有通过new分配内存空间,这个对象就是空对象,Java使用关键字null表示空对象,当试图调用一个空对象的属性或方法时,会抛出空指针异常NullPointerException。

2、类

2.1 类的定义

【概述】

类是描述了一组有相同特性(属性)和相同行为(方法)的一组对象的集合,所有的 Java 程序都是基于类的。

【创建步骤】

  1. 声明类。编写类的最外层框架,声明一个名称为Animal的类;

  2. 编写类的属性。类中的属性和方法统称为类成员。其中,类的属性就是类的数据成员。通过在类的主体中定义变量来描述类所具有的特征(属性),这里声明的变量称为类的成员变量;

  3. 编写类的方法。类的方法描述了类所具有的行为,是类的方法成员。

2.2 抽象类

【概述】

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

【抽象方法】

如果一个方法使用abstract来修饰,则说明该方法是抽象方法,抽象方法只有声明没有实现。

需要注意的是abstract关键字只能用于普通方法,不能用于static方法或者构造方法中,且不能使用private修饰,因为抽象方法必须被子类重写,而如果使用了private声明,则子类是无法重写的。

【抽象方法特征】

  1. 抽象方法没有方法体;

  2. 抽象方法必须存在于抽象类中;

  3. 子类重写父类时,必须重写父类所有的抽象方法。

【使用规则】

  1. 抽象类和抽象方法都要使用abstract关键字声明;

  2. 如果一个方法被声明为抽象的,那么这个类也必须声明为抽象的。而一个抽象类中,可以有0~n个抽象方法,以及0~n个具体方法;

  3. 抽象类不能实例化,也就是不能使用 new 关键字创建对象。

  4. 抽象类继承普通类的属性和方法;

  5. 具体类继承抽象类, 要重写抽象类的抽象方法;

  6. 抽象类继承抽象类, 可以重写也可以选择不重写其中的抽象方法。

【与普通类区别】

  1. 普通类不能包含抽象方法,抽象类可以;

  2. 普通类可以直接实例化,抽象类不能;

  3. 抽象类中不一定非要有抽象方法,且抽象类不能被final修饰 (定义抽象类就是让其他类继承的,如果修饰final就不能被继承,这样就会产生矛盾)。

2.3 内部类

2.3.1 简介

【概念】

如果在类(命名为Outer)的内部再定义一个类(命名为Inner),此时Inner类就称为内部类(或称为嵌套类),而Outer类则称为外部类(或称为宿主类)。

【分类】

实例内部类、静态内部类、局部内部类、匿名内部类

【特点】

  1. 内部类仍然是一个独立的类,在编译之后内部类会被编译成独立的.class文件,但是前面冠以外部类的类名和$符号。

  2. 内部类不能用普通的方式访问。内部类是外部类的一个成员,因此内部类可以自由地访问外部类的成员变量,无论是否为private的。

  3. 内部类声明成静态的,就不能随便访问外部类的成员变量,仍然是只能访问外部类的静态成员变量。

  4. 外部类只有两种访问级别:public 和默认;内部类则有 4 种访问级别:public、protected、 private 和默认。

【示例】

public class Outer {
    public class Inner {
        public int getSum(int x,int y) {
            return x + y;
        }
    }
    public static void main(String[] args) {
        Outer.Inner ti = new Outer().new Inner();
        int i = ti.getSum(2,3);
        System.out.println(i);    // 输出5
    }
}

2.3.2 实例内部类

【概念】

实例内部类是指没有用static修饰的内部类,有的地方也称为非静态内部类。

【特点】

  1. 在外部类的静态方法和外部类以外的其他类中,必须通过外部类的实例创建内部类的实例;

  2. 在实例内部类中,可以访问外部类的所有成员;

  3. 在外部类中不能直接访问内部类的成员,而必须通过内部类的实例去访问。如果类 A包含内部类B,类B中包含内部类C,则在类 A 中不能直接访问类C,而应该通过类B的实例去访问类C;

  4. 外部类实例与内部类实例是一对多的关系,也就是说一个内部类实例只对应一个外部类实例,而一个外部类实例则可以对应多个内部类实例;

  5. 在实例内部类中不能定义static成员,除非同时使用final和static修饰。

【示例】

public class Outer {
    public int a = 100;
    static int b = 100;
    final int c = 100;
    private int d = 100;
    
    public class Inner {
        // 2.在实例内部类中,可以访问外部类的所有成员。
        public int getSum() {
            return a + b + c + d;
        }
    }
    public static void main(String[] args) {
        // 1.在外部类的静态方法和外部类以外的其他类中,必须通过外部类的实例创建内部类的实例。
        Outer.Inner ti = new Outer().new Inner();
        int i = ti.getSum();
        System.out.println(i);    // 输出400
    }
}

2.3.3 静态内部类

【概念】

指使用static修饰的内部类。

【特点】

  1. 在创建静态内部类的实例时,不需要创建外部类的实例;

  2. 静态内部类中可以定义静态成员和实例成员。外部类以外的其他类需要通过完整的类名访问静态内部类中的静态成员,需要通过静态内部类的实例访问静态内部类中的实例成员;

  3. 静态内部类可以直接访问外部类的静态成员,如果要访问外部类的实例成员,则需要通过外部类的实例去访问。

【示例】

public class Outer {
    int o = 0;    // 实例变量
    static int p = 0;    // 静态变量
    static class Inner {
        // 2.1静态内部类中可以定义静态成员和实例成员。
        int a = 0;    // 实例变量a
        static int b = 0;    // 静态变量 b
        // 3.静态内部类可以直接访问外部类的静态成员,如果要访问外部类的实例成员,则需要通过外部类的实例去访问。
        Outer o = new Outer;
        int a2 = o.o;    // 访问实例变量
        int b2 =p;    // 访问静态变量
    }
}
class OtherClass {
    // 1.在创建静态内部类的实例时,不需要创建外部类的实例。
    Outer.Inner oi = new Outer.Inner();
    // 2.2外部类以外的其他类需要通过完整的类名访问静态内部类中的静态成员,如果要访问静态内部类中的实例成员,则需要通过静态内部类的实例。
    int a2 = oi.a;    // 访问实例成员
    int b2 = Outer.Inner.b;    // 访问静态成员
}

2.3.4 局部内部类

【概念】

局部内部类是指在一个方法中定义的内部类。

【特点】

  1. 局部内部类与局部变量一样,不能使用访问控制修饰符(public、private 和 protected)和 static 修饰符修饰;

  2. 静局部内部类只在当前方法中有效;

public class Outer {
    Inner i = new Inner();    // 编译出错
    Outer.Inner ti = new Outer.Inner();    // 编译出错
    Outer.Inner ti2 = new Outer().new Inner();    // 编译出错
    public void method() {
        class Inner{
        
        }
        Inner i = new Inner();
    }
}
  1. 局部内部类中不能定义 static 成员;

  2. 局部内部类中还可以包含内部类,子内部类同特点1;

  3. 在局部内部类中可以访问外部类的所有成员。

  4. 局部内部类中只可以访问当前方法中 final 类型的参数与变量。(Java8以后可以不加,系统默认加,但不能在被重新赋值)

public class Outer {
    int a = 0;
    int d = 0;
    public void method() {
        int b = 0;
        final int c = 0;
        final int d = 10;
        class Inner {
            int a2 = a;    // 访问外部类中的成员
            // int b2 = b;    // 编译出错
            int c2 = c;    // 访问方法中的成员
            int d2 = d;    // 访问方法中的成员
            int d3 = Test.this.d;    //访问外部类中的成员
        }
        Inner i = new Inner();
        System.out.println(i.d2);    // 输出10
        System.out.println(i.d3);    // 输出0
    }
    public static void main(String[] args) {
        Outer o = new Outer();
        o.method();
    }
}

2.3.5 匿名内部类

【概念】

匿名类是指没有类名的内部类,必须在创建时使用new语句来声明类。

【实现方式】

  1. 通过子类继承父类,重写其方法;

public class Out {
    void show() {
        System.out.println("调用 Out 类的 show() 方法");
    }
    public static void main(String[] args) {
        // 通过匿名内部类创建了一个Out类的子类对象,该子类对象只使用一次。
        Out anonymousClass = new Out() {
            @Override
            void show() {
                System.out.println("子类覆盖父类方法");
            }
        };
        anonymousClass.show();
    }
}
  1. 实现一个接口(可以是多个),实现其方法。

public interface Out {
    void show();
}


public class AnonymousClass {
    public static void main(String[] args) {
        // 通过匿名内部类创建了一个实现了Out接口的类对象,该类对象只使用一次。
        Out anonymousClass = new Out() {
            @Override
            public void show() {
            }
        };
        anonymousClass.show();
    }
}


【特点】

  1. 匿名类和局部内部类一样,可以访问外部类的所有成员。

  2. 如果匿名类位于一个方法中,则匿名类只能访问方法中final类型的局部变量和参数。(Java8以后可以不加,系统默认加,但不能在被重新赋值)

public class Out {
	void show() {
        System.out.println("调用 Out 类的 show() 方法");
    }
}
public static void main(String[] args) {
    int a = 5;
    final int b = 10;
    Out anonymousClass = new Out() {
        void show() {
            // JDK1.8 之前编译出错
            // System.out.println("调用了匿名类的 show() 方法"+a);    
            System.out.println("调用了匿名类的 show() 方法"+b);   
        }
    };
    anonymousClass.show();
}
  1. 匿名类中允许使用非静态代码块进行成员初始化操作。

Out anonyInter = new Out() {
    int i; {    // 非静态代码块
        i = 10;    //成员初始化
    }
    public void show() {
        System.out.println("调用了匿名类的 show() 方法"+i);
    }
};
  1. 匿名类的非静态代码块会在父类的构造方法之后被执行。

3、接口

3.1 简介

【概念】

接口是 Java 中最重要的概念之一,它可以被理解为一种特殊的类,不同的是接口的成员没有执行体,是由全局常量和公共的抽象方法所组成。定义方式与类基本相同,不过接口定义使用的关键字是interface。

【特点】

  1. 具有public访问控制符的接口,允许任何类使用。没有指定public的接口,其访问将局限于所属的包;

  2. 方法的声明不需要其他修饰符,在接口中声明的方法,将隐式地声明为公有的public和抽象的abstract;

  3. 在Java接口中声明的变量其实都是常量,接口中的变量声明,将隐式地声明为public、static和final,即常量,所以接口中定义的变量必须初始化;

  4. 接口没有构造方法,不能被实例化。

3.2 接口和抽象类的对比

【联系】

  1. 接口和抽象类都不能被实例化,主要用于被其他类实现和继承。

  2. 接口和抽象类都可以包含抽象方法,实现接口或继承抽象类的普通子类都必须实现这些抽象方法。

【区别】

对比维度

抽象类

接口

设计目的

  1. 体现一种模板设计的思想;

  2. 为了代码复用;

  1. 对类进行约束;

  2. 规定调用方式和标准;

使用方法

实现方式

子类使用 extends继承抽象类

子类使用implements实现接口

访问修饰符

可以用public、protected和default修饰

默认且修饰符只能是public

方法

完全可以包含普通方法

不能为普通方法提供方法实现

变量

普通成员变量、静态常量均可定义

只能定义静态常量

构造方法

有,但不是用于创建对象,而是让其子类调用这些构造方法来完成属于抽象类的初始化操作

没有构造方法

初始化块

可以包含初始化块

不能包含初始化块

main 方法

可以包含且能运行

不支持

运行速度

较快

较慢

继承/实现数量

一个类只能有一个直接父类

一个类可以实现多个接口

应用场景

  1. 如果拥有一些方法并且想让它们有默认实现,则使用抽象类。

  2. 如果基本功能在不断改变,那么就需要使用抽象类。

如果想实现多重继承,那么必须使用接口

五、常见类总结

1、Object类

1.1 简介

【概述】

Java 类库中的一个特殊类,也是所有类的父类,也就是说,Java允许把任何类型的对象赋给Object类型的变量。当一个类被定义后,如果没有指定继承的父类,那么默认父类就是Object类。

【常用方法】

/**
 * 用于返回当前运行时对象的Class对象
 */
public final native Class<?> getClass()
    
/**
 * 用于返回对象的哈希码
 */
public native int hashCode()
    
/**
 * 用于比较2个对象的内存地址是否相等
 */
public boolean equals(Object obj)
    
/**
 * 用于创建并返回当前对象的一份拷贝
 */
protected native Object clone() 
    
/**
 * 返回类的名字实例的哈希码的16进制的字符串。
 */
public String toString()
    
/**
 * 唤醒一个在此对象监视器上等待的线程
 * 如果有多个线程在等待只会任意唤醒一个
 */
public final native void notify()
    
/**
 * 唤醒一个在此对象监视器上等待的所有线程
 */
public final native void notifyAll()
    
/**
 * 停线程的执行。
 * sleep没有释放锁,而wait释放了锁 ,timeout是等待时间。
 */
public final native void wait(long timeout) 
    
/**
 * 多了 nanos 参数,这个参数表示额外时间(以毫微秒为单位)。 
 * 所以超时的时间还需要加上 nanos 毫秒。。
 */
public final void wait(long timeout, int nanos) 
    
/**
 * 跟之前的2个wait方法一样
 * 只不过该方法一直等待,没有超时时间这个概念
 */
public final void wait() 
    
/**
 * 实例被垃圾回收器回收的时候触发的操作
 */
protected void finalize()
1.2 equals()介绍

【概述】

用于比较2个对象的内存地址是否相等。

【示例】

public boolean equals(Object obj) {
     return (this == obj);
}

【使用场景】

  1. 类没有重写equals()方法:等价于通过==比较这两个对象(如示例)。

  2. 类重写了equals()方法:一般我们都重写equals()方法来比较两个对象中的属性是否相等,若它们的属性相等,则返回 true。

【与==的区别】

方向

==

equals()

比较内容

基本类型和引用类型的数据均可比较

只能判断引用类型的数据

  1. 对于基本数据类型,比较的是值

  2. 对于引用数据类型,比较的是对象的内存地址

  1. 一般比较的是对象的内存地址

  2. 重写之后可能比较的是对象的值

1.3 HashCode()介绍

【概述】

hashCode()的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。

【作用域】

hashCode()在散列表中才有用,在其它情况下没用。在散列表中hashCode()的作用是获取对象的散列码,进而确定该对象在散列表中的位置。

【哈希冲突】

对象Hash的前提是实现equals()和hashCode()两个方法,那么HashCode()的作用就是保证对象返回唯一hash值,但当两个对象计算值一样时,这就发生了碰撞冲突。当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突。

【与equals()关系】

  1. 使用equals方法中进行比较的参数没有修改,那么多次调用的hashCode()方法返回的哈希值应该是相同的。

  2. 通过equals方法比较是相等的,要求这两个对象的hashCode方法返回的值也应该是相等的。

  3. 如果两个对象通过equals方法比较是不同的,不要求hashCode()方法也一定返回不同的值,但是最好返回不同值,以提高哈希表性能。

1.4  为什么重写 equals() 时必须重写 hashCode()

如果插入某个没有重写hashCode方法的对象,会导致两个相等的实例o1和o2返回了不同的哈希码。

会违反了“两个相同对象的hashCode()返回的值也应该是相等”这一约定。

一般对于HashSet、HashMap等基于hash值的类就会出现问题。下面将举例说明:

以 HashMap 为例,添加对象时,会用当前散列表中的元素 p 和添加对象比较。

  1. p 为空:则直接插入;

  2. 两者 Hash 值相同:通过 equals() 继续进行比较,相同就替换,不相同就插入。

这样避免了频繁调用 equals() 问题,详情见HashMap的底层原理。

public class Point {
	private final int x, y;
    
	public Point(int x, int y) {
	    this.x = x;
	    this.y = y;
	}
	@Override
	public boolean equals(Object obj) {
	    if (this == obj) return true;
	    if (!(obj instanceof Point)) return false;
	    Point point = (Point) obj;
	    return x == point.x && y == point.y;
	}
    
	public static void main(String[] args) {
		Point p1 = new Point(1, 2);
		Point p2 = new Point(1, 2);
		System.out.println(p1.equals(p2));// true
		
		Map<Point, String> map = new HashMap<>();
		map.put(p1, "p1");
        String result = map.get(p2);
        System.out.println(result); // null
	}
}

根据上述代码,你可能觉得result应该返回字符串 p1, 但是却返回null, 这是因为Point类并没有重写hashCode方法,导致两个相等的实例p1和p2返回了不同的哈希码,违反了hashCode的约定,key不同,自然获取不到。

@Override
public int hashCode() {
	int result = Integer.hashCode(x);
    result = 31 * result + Integer.hashCode(y);
    return result;
}

重写hashCode方法后,你会发现result返回的就是字符串p1了, 因为hashCode()重写后,相等的Point实例计算出的哈希值是相同的。

注意事项:

  1. 当你重写hashCode方法后,请一定问一下自己是否满足相等的实例有相同的哈希码这一条件。

  2. 重写的hashCode()中涉及到的属性应与equals()中保持一致,不要试图从哈希码计算中排除重要的属性来提高性能。

此外Objects类有一个静态方法Objects.hash,它接受任意数量的对象并为它们返回一个哈希码。其质量与根据这个项目中的上面编写的方法相当。

@Override
public int hashCode() {
    return Objects.hash(x, y);
}

参考:

  1. 《Java Guide》

  2. 《C语言中文网--Java教程》

  3. 《为啥子重写equals方法时一定要重写hashCode方法》

0条评论

您的电子邮件等信息不会被公开,以下所有项均必填

OK! You can skip this field.