【Java学习笔记】HashSet中加入自定义的类的对象

简介:

这个话题还是从一个有问题的代码中引申出来的,原代码如下:

import java.util.*; 
class TreeSetTest 

    public static void main(String[] args) 
    { 
        HashSet hs=new HashSet(); 
        Student st1=new Student(1,"zhao1");     
        Student st2=new Student(1,"zhao1");     
        hs.add(st1); 
        hs.add(st2); 
        System.out.println(hs); 
    } 

class Student   

    public Student(int num,String name) 
    { 
        this.num=num; 
        this.name=name; 
    } 
    public int hashCode() 
    { 
        return new Integer(num).hashCode(); 
    } 
    public boolean equals(Student st) 
    { 
        if (name==st.name) return true; 
        else return false; 
    } 
    public String toString() 
    { 
        return "student "+num+" name:"+name; 
    } 
    int num; 
    String name; 
}

为什么st1和st2两个对象内容完全一样,却还能插入到一个set中呢,set不是不能有重复的对象吗?

这段程序有两个主要问题,就要先从Java中两个面向对象的基本含义说起了:

JAVA中的重载overload: 
只要是一个类以及其父类里有的两个函数有相同的名字但是不同的参数列表 (包括参数类型,参数个数,参数顺序3项中的一项或多项)。重载可以在单个类或者两个具有继承关系的类中出现。 是实现类的多态性的一种重要方式。

JAVA中的覆盖override : 
覆盖只会在类继承的时候才会出现,覆盖要求两个函数的名字和参数列表都完全一样。

在HashSet判断是不是重复元素时是使用了equals方法,不过请注意自定义的这个类实际继承了Object类,而Object类中equals方法的定义如下:

public boolean equals(Object o)

这么说,这段程序中定义的equals方法是对Object中的equals方法的重载,而不是覆盖,那么在HashSet判断重复元素时,实际调用的就是Object.equals 方法,自然是true。

所以该程序第一个需要修改的地方就是equals方法:我们要的是覆盖不是重载,为了防止这样问题,可以加上annotation让Eclipse自己去判断。


public boolean equals(Object st) 
    { 
        Student tempStudent= (Student) st; 
        if (name==tempStudent.name) return true; 
        else return false; 
    }

另外,该程序段在自定义类的hashCode方法和equals并不一致,前者是用num作为hashCode方法的依据,而后者是用name作为判断是不是相同的依据。

1)利用HashSet/HashMap/Hashtable类来存储数据时,都是根据存储对象的hashcode值来进行判断是否相同的,在 hashCode中仅在两个对象有着相同hashCode()的时候才会调用equals方法去比较,因为hashset内部采用对某个数字n进行取余的 方式对哈希码进行区域划分,也就是说即使哈希码不同,他们也可能被划分在同一个区域。在添加数据时,首先计算hashcode(String 对象的哈希码根据以下公式计算: s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] 注:使用 int 算法,这里 s[i] 是字符串的第 i 个字符,n 是字符串的长度,^ 表示求幂。(空字符串的哈希值为 0)),发现在一个区域的才会使用equals方法去一一比较同一区域的对象是否相同,否则直接插入。

|           |            |

|  区域1  |   区域2  |  ……

|           |            |

这个区域在实现时采用链表的方法。

当调用了 HashSet 的 add 方法存放对象 obj , HashSet 会首先调用 obj 的 hasCode 方法得到该对象的哈希码, HashSet 会使用一个算法把它的哈希码转换成一个数组下标,该下标“标记”了 obj 的位置。如果这个位置上的链表中没有元素,那么就把 obj 对象添加到链表上。如果这个位置上的链表中已经有了元素,则遍历这个链表,调用 obj 的 equals 方法,判断 obj 是否和其中的某个元素重复,如果没有重复的元素,那么就将 obj 添加到链表上;如果有重复的元素,则不会讲 obj 对象存入 HashSet 中。

也就是说,根据哈希表的定义,为了保障相同的对象被放到相同的哈希区域,则必须满足条件:有equals() 返回true=> hashCode() 返回true。 因为先判断的是hashCode的值,换句话说,equals的值为true是hashCode值为true 的充分非必要条件。这样的话,就不会出现两个实际相同的对象,仅仅因为不在同一个哈希区域而被错误的加入到哈希集合中的情况发生了。

2)并且由于链表的缺点在于查询速度慢,所以在我们定义自己的hashCode()和equals()时,为了照顾到哈希表的性能 ,也要遵循“equals返回false时,hashCode也为false”

综合上述1)和2)两点,若hashCode方法和equals不一致则hashCode()和equals()结果没有任何关系,也就是说 equals返回true时,hashcode()也可能是false的,这个与哈希表定义中不允许相同的元素的定义不符合,也不符合哈希表性能优化的需 要。

得出的结论是:建议hashCode和equals方法的判断依据最好是一个,也就是所谓的两个方法兼容。

注意:当一个对象被存储进Hashset中以后,就不能修改这个对象中那些参与计算哈希值的字段了,否则,对象修改后的哈希值与最初存储进 hashset对象的哈希值就不同了,在这种情况下,即使在contains方法使用该对象的当前引用作为参数去检索hashset集合,也将返回找不到 对象的结果,这会导致无法从hashset集合中单独删除当前对象,从而造成内存泄露。

实例代码如下:

package testhashcode; 
/** 
 * @author gnuhpc 
 *         email: warmbupt@gmail.com 
 *         blog:  http://blog.csdn.net/gnuhpc 
 * @date 2010-1-13 
 */ 
import java.util.*; 
class TreeSetTest 

    public static void main(String[] args) 
    { 
        HashSet<Student> hs=new HashSet<Student>(); 
        Student st1=new Student(1,"zhao");     
        Student st2=new Student(2,"qian"); 
        Student st3=new Student(3,"sun"); 
        hs.add(st1); 
        hs.add(st2); 
        hs.add(st3); 
        System.out.println(hs); 
st1.num=4; //可以试着注释掉这一行看一看结果 
        hs.remove(st1); 
        System.out.println(hs); 
    } 

class Student   

    public Student(int num,String name) 
    { 
        this.num=num; 
        this.name=name; 
    } 
    public int hashCode() 
    { 
        return new Integer(num).hashCode(); 
    } 
    @Override 
    public boolean equals(Object st) 
    { 
        Student tempStudent= (Student) st; 
        if (num==tempStudent.num) return true; 
        else return false; 
    } 
    public String toString() 
    { 
        return "student "+num+" name:"+name; 
    } 
    int num; 
    String name; 
}




本文转自gnuhpc博客园博客,原文链接:http://www.cnblogs.com/gnuhpc/archive/2012/12/17/2822239.html,如需转载请自行联系原作者

相关文章
|
21天前
|
Java
java8中List对象转另一个List对象
java8中List对象转另一个List对象
36 0
|
1天前
|
Java
Java中如何克隆一个对象?
【4月更文挑战第13天】
9 0
|
2天前
|
Java 编译器
Java Character 类
4月更文挑战第13天
|
2天前
|
Java API 数据库
深入解析:使用JPA进行Java对象关系映射的实践与应用
【4月更文挑战第17天】Java Persistence API (JPA) 是Java EE中的ORM规范,简化数据库操作,让开发者以面向对象方式处理数据,提高效率和代码可读性。它定义了Java对象与数据库表的映射,通过@Entity等注解标记实体类,如User类映射到users表。JPA提供持久化上下文和EntityManager,管理对象生命周期,支持Criteria API和JPQL进行数据库查询。同时,JPA包含事务管理功能,保证数据一致性。使用JPA能降低开发复杂性,但需根据项目需求灵活应用,结合框架如Spring Data JPA,进一步提升开发便捷性。
|
3天前
|
存储 Java
Java基础教程(7)-Java中的面向对象和类
【4月更文挑战第7天】Java是面向对象编程(OOP)语言,强调将事务抽象成对象。面向对象与面向过程的区别在于,前者通过对象间的交互解决问题,后者按步骤顺序执行。类是对象的模板,对象是类的实例。创建类使用`class`关键字,对象通过`new`运算符动态分配内存。方法包括构造函数和一般方法,构造函数用于对象初始化,一般方法处理逻辑。方法可以有0个或多个参数,可变参数用`类型...`定义。`this`关键字用于访问当前对象的属性。
|
7天前
|
Java Shell
Java 21颠覆传统:未命名类与实例Main方法的编码变革
Java 21颠覆传统:未命名类与实例Main方法的编码变革
10 0
|
7天前
|
Java
Java 15 神秘登场:隐藏类解析未知领域
Java 15 神秘登场:隐藏类解析未知领域
11 0
|
7天前
|
Java
Java配置大揭秘:读取自定义配置文件的绝佳指南
Java配置大揭秘:读取自定义配置文件的绝佳指南
11 0
Java配置大揭秘:读取自定义配置文件的绝佳指南
|
7天前
|
存储 Java 编译器
对象的交响曲:深入理解Java面向对象的绝妙之处
对象的交响曲:深入理解Java面向对象的绝妙之处
36 0
对象的交响曲:深入理解Java面向对象的绝妙之处
|
9天前
|
安全 Java
append在Java中是哪个类下的方法
append在Java中是哪个类下的方法
21 9