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

简介: 作者:gnuhpc 出处:http://www.cnblogs.com/gnuhpc/ 这个话题还是从一个有问题的代码中引申出来的,原代码如下: import java.util.*; class TreeSetTest {     public static void ...

作者:gnuhpc
出处:http://www.cnblogs.com/gnuhpc/

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

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/


               作者:gnuhpc
               出处:http://www.cnblogs.com/gnuhpc/
               除非另有声明,本网站采用知识共享“署名 2.5 中国大陆”许可协议授权。


分享到:

目录
相关文章
|
15小时前
|
Java
【JAVA基础篇教学】第五篇:Java面向对象编程:类、对象、继承、多态
【JAVA基础篇教学】第五篇:Java面向对象编程:类、对象、继承、多态
|
1天前
|
缓存 Java 程序员
关于创建、销毁对象⭐Java程序员需要掌握的8个编程好习惯
关于创建、销毁对象⭐Java程序员需要掌握的8个编程好习惯
关于创建、销毁对象⭐Java程序员需要掌握的8个编程好习惯
|
1天前
|
Java
从源码出发:JAVA中对象的比较
从源码出发:JAVA中对象的比较
9 3
|
1天前
|
Java 编译器 开发者
Java一分钟之-继承:复用与扩展类的特性
【5月更文挑战第9天】本文探讨了Java中的继承机制,通过实例展示了如何使用`extends`创建子类继承父类的属性和方法。文章列举了常见问题和易错点,如构造器调用、方法覆盖、访问权限和类型转换,并提供了解决方案。建议深入理解继承原理,谨慎设计类结构,利用抽象类和接口以提高代码复用和扩展性。正确应用继承能构建更清晰、灵活的代码结构,提升面向对象设计能力。
7 0
|
1天前
|
安全 Java 编译器
java中类与对象回顾总结-2
java中类与对象回顾总结
13 3
|
1天前
|
Java 编译器
java中类与对象回顾总结-1
java中类与对象回顾总结
13 3
|
1天前
|
Java
【Java多线程】面试常考 —— JUC(java.util.concurrent) 的常见类
【Java多线程】面试常考 —— JUC(java.util.concurrent) 的常见类
10 0
|
1天前
|
Java API 调度
【Java多线程】Thread类的基本用法
【Java多线程】Thread类的基本用法
5 0
|
1天前
|
SQL Java 数据库连接
JDBC Java标准库提供的一些api(类+方法) 统一各种数据库提供的api
JDBC Java标准库提供的一些api(类+方法) 统一各种数据库提供的api
8 0
|
2天前
|
Java
Java一分钟之-类与对象:面向对象编程入门
【5月更文挑战第8天】本文为Java面向对象编程的入门指南,介绍了类与对象的基础概念、常见问题及规避策略。文章通过代码示例展示了如何定义类,包括访问修饰符的适当使用、构造器的设计以及方法的封装。同时,讨论了对象创建与使用时可能遇到的内存泄漏、空指针异常和数据不一致等问题,并提供了相应的解决建议。学习OOP需注重理论与实践相结合,不断编写和优化代码。
25 1