Java中HashSet的存储原理

Java中HashSet的存储原理

1. 说明

HashSet,从字面意思我们大致可以看出它包含了两方面的内容:Hash和Set,即散列与集合。

实际确实如此,HashSet实现了Set接口,所以它符合Set集合使用的特征,集合中不允许有重复的元素,同时HashSet底层的实现是通过HashMap来实现元素保存操作的。

2. 分析

我们来分析一下HashSet的原码,下面这段是HashSet中存储对象的数据结构:

private transient HashMap<E,Object> map;

public HashSet() {
    map = new HashMap<E,Object>();
}

这里可以看到,HashSet类维护了一个HashMap引用作为自己的成员变量,并在其构造方法中将其实例化。

HashMap表示的是关键字key到值value的映射,其中key唯一,即一个key某一时刻只能映射到一个value。

下面我们再来看HashSet中添加元素的方法:

private static final Object PRESENT = new Object();

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

我将add()方法中用到的一个参数PRESENT的定义也一并复制到一起来。大家可以看到,实际上add()方法的实现非常简单,直接调用HashMap的put()方法实现键-值对映射。在键-值对映射时,将e(待添加到集合中的元素)作为键,将PRESENT作为值传递到put()方法中。我们在调用add()方法的时候,其实不关心底层的实现,在这儿写的HashMap中保存的键值对映射,具体值是什么,其实是不重要的,所以定义了一个常量的Object,让所有的键映射到同一个值上,但这一点对于使用HashSet的人是不需要知道的。

那在add()方法中添加的元素是如何实现了唯一性(不重复)的呢,这还得从HashMap中去说。

和HashMap相关的一些基本概念,大家可以参考http://blog.csdn.net/zhliro/article/details/46885883,这不再赘述。

下面我们再转到HashMap中的put()方法来看看:

    public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }

        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

hash(key.hashCode())是获取key的散列码,indexFor(hash, table.length)是获取在底层存放数组中的索引位置,如果在该位置已经存放了元素,则会比较该位置上元素的散列码与关键字,如果这些都相同,则说明是同一个对象,那么不会重复添加,当然如果找到的位置上未存放有元素,或不是重复的元素,则调用addEntry()方法将元素保存起来。

那么我们仔细再来看下这段代码。在使用hash()方法获取散列码时,传递了key.hashCode()这样的参数,key是什么呢,key是在调用HashSet的add()方法时传递进来的要保存的对象。我们在判断是否重复元素时,会用到散列码,使用传递进来的对象的hashCode()方法计算出其散列码值,再使用hash()方法对该散列码重新运算得到一个新值,那么这个hashCode()方法是比较重要的。除了hashCode()这个方法外,还有一个方法:equals(),它主要是用来判断两个对象是否是相同的对象。为什么又会调用到这个方法呢?我们说,当两个对象散列码相同的时候,不一定是相同的两个对象,仅只能说明关键字使用散列函数运算后得到的存储位置一致,至于是否为相同对象,还得通过equals()方法判断。

如果我们要从HashSet中获取元素,如何来获取呢。因为HashSet底层是以HashMap的方式实现的,所以在HashSet中不会维护所存放元素的顺序,我们要获取所保存的元素,那就只能使用HashMap中的相应方法来实现了,好在调用什么方法,在HashSet中已经帮我们封装了实现细节。

public Iterator<E> iterator() {
    return map.keySet().iterator();
}

这儿我们用到了迭代器(Iterator),在迭代器中遍历到的元素是些什么呢,看具体的实现,应该是map.keySet(),也就是在HashMap中保存的所有键集合,通过前面存储的分析,我们知道所有键的集合就是我们所在HashSet中保存的所有元素。

示例

学生类:

package com.hash.demo2;

/**
 * 学生类
 * 
 * @author 小明
 *
 */
public class Student {
    private int id; // 编号
    private String name; // 姓名
    private int age; // 年龄
    private String sex; // 性别

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

    // 省略getter与setter
    // ……
}

测试:

package com.hash.demo2;

import java.util.HashSet;

/**
 * HashSet测试
 * @author 小明
 *
 */
public class HashSetDemo {

    public static void main(String[] args) {
        HashSet<Student> students = new HashSet<Student>();

        /* 创建两个属性值都相同的对象 */
        Student stu1 = new Student(1, "张三", 28, "男");
        Student stu2 = new Student(1, "张三", 28, "男");
        /* 将两个对象添加到HashSet中保存 */
        students.add(stu1);
        students.add(stu2);
        // 打印集合大小
        System.out.println("集合中存放元素个数:" + students.size());
    }
}

这时打印的结果为:

集合中存放元素个数:2

但是我们创建的两个Student对象所有属性都一致,一般我们会认为这是两个相同的对象,HashSet中不会存放重复元素,但为什么这两个相同的对象又都能存放到集合中呢?我们之前分析底层存储时说到,在HashMap中关键字会使用其hashCode()判断是否具有相同的散列码。因为两个Student对象都是独立实例化出来的,在堆中存放的位置不一样,所以hashCode()得到的结果也不一致(大家可以自行测试,打印出两个对象的hashCode值),那么在HashSet中保存这两个元素时,就会认为它们不是相同的元素,所以都能保存下来,集合长度为2。

那我们在Student类中重写hashCode()方法:

@Override
public int hashCode() {
    /* 我们先自定义一个规则来生成对象的散列码 */
    int result = 10;
    result += this.age * 2 + this.id;
    return result;
}

再来执行测试,结果为:

集合中存放元素个数:2

集合中存放元素的个数仍然为2。我们不是重写了hashCode()方法,经过hashCode()方法运算后返回的结果都为67,散列码都相同了,为什么还是存放了重复元素呢。前边已经解释过了,散列码相同,只能说明两个元素存放的位置是在同一个位置上面,而至于是不是相同的两个元素,还得通过equals()方法比较才知道。

那么接下来,我们再重写一下equals()方法吧:

@Override
public boolean equals(Object obj) {
    Student stu = (Student) obj;
    return id == stu.getId() && name.equals(stu.getName())
            && age == stu.getAge() && sex.equals(stu.getSex());
}

再来执行测试,结果为:

集合中存放元素个数:1

现在结果就是我们想要的结果了:HashSet中不重复保存元素。

每次如果我们都自定义hashCode()的实现与equals()的实现,会比较麻烦,并且在考虑比较条件的时候可能会不完全,所以可以使用Eclipse工具提供的方式,直接生成hashCode()和equals()方法。

关于hashCode有一个常规协定:

  • 在Java应用程序执行期间,在对同一对象多次调用hashCode方法时,必须一致地返回相同的整数,前提是将对象进行equals比较时所用的信息没有被修改。从某一应用程序的一次执行到同一应用程序的另一次执行,该整数无需保持一致。
  • 如果根据equals(Object)方法,两个对象是相等的,那么对这两个对象中的每个对象调用hashCode方法都必须生成相同的整数结果。
  • 如果根据equals(java.lang.Object)方法,两个对象不相等,那么对这两个对象中的任一对象上调用hashCode方法不要求一定生成不同的整数结果。但是,程序员应该意识到,为不相等的对象生成不同整数结果可以提高哈希表的性能。

所以我们要重写equals()方法,一般都有必要重写hashCode()方法,以维护hashCode()方法的常规协定。

总结

下面简要总结一下HashSet的存储原理

根据每个对象的哈希码值(调用hashCode()获得)用固定的算法算出它的存储索引,把对象存放在一个叫散列表的相应位置(表元)中,可能会有以下两种情况:

  1. 如果对应的位置没有其它元素,就只需要直接存入。
  2. 如果该位置有元素了,会将新对象跟该位置的所有对象进行比较(调用equals()),以查看是否已经存在了:还不存在就存放,已经存在就直接使用。

如果我们要取元素,则根据对象的哈希码值计算出它的存储索引,在散列表的相应位置(表元)上的元素间进行少量的比较操作就可以找出它。

完整示例

学生类:

package com.hash.demo2;

/**
 * 学生类
 * 
 * @author 小明
 *
 */
public class Student {
    private int id; // 编号
    private String name; // 姓名
    private int age; // 年龄
    private String sex; // 性别

    public Student() {
        super();
    }

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

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + age;
        result = prime * result + id;
        result = prime * result + ((name == null) ? 0 : name.hashCode());
        result = prime * result + ((sex == null) ? 0 : sex.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Student other = (Student) obj;
        if (age != other.age)
            return false;
        if (id != other.id)
            return false;
        if (name == null) {
            if (other.name != null)
                return false;
        } else if (!name.equals(other.name))
            return false;
        if (sex == null) {
            if (other.sex != null)
                return false;
        } else if (!sex.equals(other.sex))
            return false;
        return true;
    }

    @Override
    public String toString() {
        return "Student [id=" + id + ", name=" + name + ", age=" + age
                + ", sex=" + sex + "]";
    }
}

测试类:

package com.hash.demo2;

import java.util.HashSet;
import java.util.Iterator;

/**
 * HashSet测试
 * 
 * @author 小明
 *
 */
public class HashSetDemo {

    public static void main(String[] args) {
        HashSet<Student> students = new HashSet<Student>();

        /* 创建两个属性值都相同的对象 */
        Student stu1 = new Student(1, "张三", 28, "男");
        Student stu2 = new Student(1, "张三", 28, "男");
        /* 将两个对象添加到HashSet中保存 */
        students.add(stu1);
        students.add(stu2);
        // 打印集合大小
        System.out.println("集合中存放元素个数:" + students.size());
        /* 遍历HashSet中的元素 */
        Iterator<Student> it = students.iterator();
        while (it.hasNext()) {
            Student stu = it.next();
            System.out.println(stu);
        }
    }
}
  • 5
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值