详解Java中散列和HashMap


前言:看了《Java编程思想》第十七章容器的深入研究中讲散列和散列码这一节,内容十分精彩。现根据该书内容加以自己的描述认真总结一下,其中很多内容直接搬运自于书中,例子是以Map为例展开的,这里分享给大家。
这篇文章博客园格式看着更舒服 https://www.cnblogs.com/snailvan/p/10671171.html

1 一个关于HashMap高效查找的问题?

我们知道Map是保存每一条记录的是键值对<key,value>的形式,要查询value只要知道key就可以了value=map.get(key)。我们是如何快速实现查找的呢?假设我们自己实现一个MyMap类来实现键值关联的这种数据

  1. 假设我们用一个 n × 2 n\times 2 n×2的二维数组Object[][] pairs = new Object[n][2];实现这样的结构,第i个元素的key = pairs[i][0];value = pairs[i][1];在我们根据key查找value时需要先查找key的位置,由于key在数组中是随机存储的,所以查找key是一个线性搜索过程,的平均时间复杂度将会是O(n)。另外存在一个显著的问题就是数组的长度是固定的,这很不灵活。
  2. 我们也可以用两个List来实现该结构,List<K> keys = new ArrayList<K>();List<V> values = new ArrayList<V>();这样我们就可以用value = values.get(keys.indexOf(key));来访问,这虽然解决了数组长度固定的问题,但是查找key的位置复杂度依然是O(n)。

看来查询速度瓶颈位于键的查询,而HashMap的查找时间复杂度是O(1)。这是怎么做到的呢?原因是HashMap使用了散列。

2 散列的查找思想

散列将键的信息通过散列函数映射到数组的索引中,以便能够实现快速的查找。

2.1 散列码

我们知道Map中的key是一个Object(称作键对象),首先我们通过键对象生成一个整数,将其作为数组的下标,这个数字就是散列码。这个散列码是通过Object根类的public native int hashCode();方法产生,所以所有的类都可以产生散列码。通常在实现类中我们会根据需要覆盖此方法,实现我们自己的计算方式。

2.2 存储与查询

存储 当我们要向Map中存入一个Entry<key,value>时,我们首先根据它的键对象计算出对应的散列码,并将该Entry<key,value>存到数组对应于散列码的索引位置。当然不同的Entry<key,value>的key计算出的散列码可能是相同的,这就相当于在数组的同一个位置存入了多个Entry<key,value>,重复了如之奈何?不必担心,后面继续说明。

查找 查找时我们根据提供的键对象key用散列函数计算出它在数组中对应的索引位置,再在索引位置保存的多个对象中进行一次线性查询(通过equals()方法比较两个对象key是否相等)找到该key对应的value。

从查找的过程看我们通过计算散列码找到索引位置后进行了一次线性搜索,并通过equals()方法精确定位到了我们要找的Entry<key,value>,所以散列码是可以重复的,即我们的同一个散列码可以对应多个key。这样我们既解决了数组长度固定的问题,又解决了查询速度问题。

形象的比喻 其实我们可以把保存对象的数组索引看做是n=array.length个槽位(或者叫桶),在保存的时候我们根据计算哈希函数将Entry<key,value>分配到了不同的槽位中,每个槽位的数据条数相比于集合数据总条数很少,这样在找到槽位进行接下来的线性搜索只对很少的部分进行线性搜索,这就是查询速度如此之快的原因。

总结 散列码不需要是唯一的,但是通过hashCode()和equals()必须能够完全确定对象身份。对于散列函数的设计我们值的注意的是:

  1. 更应该关注其生成散列码的速度;
  2. 生成的散列码能尽量让数据均匀分布在每一个槽位中,以提高整体查询速度;
  3. 映射值的范围只能是数组索引的整数范围。

3 一个naive的实现–SimpleHashMap

声明:下面两段代码来自《Java编程思想》,只在main函数测试中稍作了修改。

3.1 编写MyEntry关联对象

Map里面保存的都是关联对象,这样<键对象,值对象>一组在Java中有一个Entry接口,我们定义自己的MapEntry实现该接口,并实现其中的getKey(),getValue(),setValue()方法。从2.2中可知,我们必须覆盖Object根类的hashCode()和equals()方法,以完成我们自己的散列码计算和相等比较。

import java.util.Map.Entry;

public class MapEntry<K, V> implements Entry<K, V> {
	private K key;
	private V value;

	public MapEntry(K key, V value) {
		this.key = key;
		this.value = value;
	}

	@Override
	public K getKey() {
		return key;
	}

	@Override
	public V getValue() {
		return value;
	}

	@Override
	public V setValue(V v) {
		V result = value;
		value = v;
		return result;
	}

	@Override
	public int hashCode() {
		return (key == null ? 0 : key.hashCode())
				^ (value == null ? 0 : value.hashCode());
	}

	@Override
	public boolean equals(Object o) {
		if (!(o instanceof MapEntry)) {
			return false;
		}
		MapEntry me = (MapEntry) o;
		return (key == null ? me.getKey() == null : key.equals(me.getKey()))
				&& (value == null ? me.getValue() == null : value.equals(me
						.getValue()));
	}

	@Override
	public String toString() {
		return key + "=" + value;
	}
}

3.2 实现SimpleHashMap

设计好了我们要在Map中保存的关联对象,我们就可以编写一个简单的SimpleHashMap来实际体会一下。详细解释在代码注释中。

import java.util.AbstractMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;

public class SimpleHashMap<K, V> extends AbstractMap<K, V> {
	static final int SIZE = 997; // 选一个质数作为槽位数
	LinkedList<MapEntry<K, V>>[] buckets = new LinkedList[SIZE]; //初始化该数组

	public V put(K key, V value) {
		V oldValue = null;
		int index = Math.abs(key.hashCode()) % SIZE; // 计算index
		if (buckets[index] == null) { // 如果此槽位还是空的就初始化
			buckets[index] = new LinkedList<MapEntry<K, V>>();
		}
		LinkedList<MapEntry<K, V>> bucket = buckets[index]; // 得到该槽位的数据List
		MapEntry<K, V> pair = new MapEntry<K, V>(key, value); // 初始化一个MapEntry
		boolean found = false;
		ListIterator<MapEntry<K, V>> it = bucket.listIterator(); // 获取List的迭代器
		while (it.hasNext()) { // 遍历该槽位的list
			MapEntry<K, V> iPair = it.next();
			if (iPair.getKey().equals(key)) { 
				oldValue = iPair.getValue();
				it.set(pair); // 如果找到就替换原值
				found = true; // 设置找到标记为true
				break; // 退出循环
			}
		}
		if (!found) { // 如果没找到,直接将该值添加到该槽位
			buckets[index].add(pair);
		}
		return oldValue; // 返回旧值,如果原来没有这里返回的就是null
	}

	public V get(Object key) {
		int index = Math.abs(key.hashCode()) % SIZE; // 计算index
		if (buckets[index] == null) { // 如果该槽位为空,则找不到返回null
			return null;
		}
		for (MapEntry<K, V> iPair : buckets[index]) {// 不为空则遍历槽位列表
			if (iPair.getKey().equals(key)) {//如果找到
				return iPair.getValue();//返回该值
			}
		}
		return null;// 找不到返回 null
	}

	public Set<Entry<K, V>> entrySet() {
		Set<Entry<K, V>> set = new HashSet<Map.Entry<K, V>>();
		for (LinkedList<MapEntry<K, V>> bucket : buckets) {
			if (bucket == null)
				continue;
			for (MapEntry<K, V> mpair : bucket) {
				set.add(mpair);
			}
		}
		return set;
	}
	
	public static void main(String[] args) {
		SimpleHashMap<String, String> m = new SimpleHashMap<String, String>();
		m.put("山东","济南");
		m.put("四川", "成都");
		m.put("甘肃", "兰州");
		m.put("江苏", "南京");
		System.out.println(m);
		System.out.println(m.get("四川"));
		System.out.println(m.entrySet());
	}
}

我们已经按照散列的思想实现了HashMap中的put()和get(),其实该例子中entrySet()方法依然是采用的线性搜索,这里不考虑该方法的效率问题。

4 散列和HashMap

接下来我们来看一下Java中HashMap源码,体会一下他是怎么实现的呢。

该类中首先定义了5个全局常量:

// 默认初始容量16,必须是2的幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

// 最大容量,必须是2的幂,且最大为 1<<30
static final int MAXIMUM_CAPACITY = 1 << 30;

// 默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;


static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;

接着定义了一个内部类static class Node<K,V> implements Map.Entry<K,V>作为要保存键值节点,接着定义了几个静态工具方法。

4.1 HashMap的成员字段

接着就是该类的成员字段了:

/**存储数据的容器,该表在首次使用时初始化,并根据需要调整大小。长度始终是2的幂。*/
transient Node<K,V>[] table;

/**保存根据Map获取的Set集合*/
transient Set<Map.Entry<K,V>> entrySet;

/**Map中当前存储的键值对个数*/
transient int size;

/**此HashMap经过结构修改的次数结构修改是指更改HashMap 中映射数量或以其他方式修
改其内部结构(例如,再散列)的修改。 此字段用于在HashMap的Collection-views上
快速生成迭代器。*/
transient int modCount;

/**自动扩容阈值(threshold = capacity*loadFactor), capacity是当前桶位数,loadFactor是负载因
子,默认0.75f;也就是当HashMap数据条数达到桶位数的75%时,就会自动扩容*/
int threshold;

/** 负载因子(默认0.75f), loadFactor = size/capacity */
final float loadFactor;

当负载情况达到负载因子水平时,容器将自动增加其容量(桶位数),实现方式是使容量大致加倍,并重新将现有对象分布到新的桶位中(这被称为再散列)。

4.2 HashMap的构造函数

public HashMap() // 构造一个空的HashMap
public HashMap(int initialCapacity) // 构造一个指定初始桶位数的HashMap
public HashMap(int initialCapacity, float loadFactor) // 同时指定桶位数和负载因子
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) // 用已有的Map构造一个HashMap

4.3 方法分析

接下来我们就跟着HashMap中的put()方法,顺藤摸瓜看一看HashMap是怎样计算散列码的和怎样保存的。最后我们看一下get()方法,和entrySet(),以对比我们实现的SimpleHashMap和Java本身的HashMap的差距。

4.3.1 保存数据

首先看一下put方法:

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

该方法直接return了putVal()的返回值,putVal第一个参数用hash()工具方法生成散列值:

static final int hash(Object key) {
    int h;
    /*如果key为空,则返回hash值为0;否则计算当前键对象的hashCode()
    (该hashCode()方法一般被我们覆盖过),接着该值与他无符号右移
    16位的结果按位与运算,返回该结果*/
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

下面看看putVal()中的详细情况:

/**
 * @param hash  key的散列值
 * @param key   键对象
 * @param value 要保存的值对象
 * @param onlyIfAbsent 如果为true,不要替换存在的值对象
 * @param evict 如果为false,则表处于创建模式
 * @return 返回旧值,如果不存在返回null
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    // tab是桶位数组,p是某个桶位的根结点
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    if ((tab = table) == null || (n = tab.length) == 0) // 如果为空或长度为0
        n = (tab = resize()).length; // resize()并返回数组长度
    if ((p = tab[i = (n - 1) & hash]) == null) // 如果该hash值对应的索引处为空
        tab[i] = newNode(hash, key, value, null); // 初始化一个结点
    else { // 否则不为空
        Node<K,V> e; K k;
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            // p是树结构结点的话,就将其转成TreeNode执行putTreeVal(),插入结点
            // 实际HashMap中还定义了个TreeNode的内部类,也就是说该槽位节点时一个树的根结点
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) { // 在该槽位一直向下找,直到桶位的next为空,证明元素不存在
                    p.next = newNode(hash, key, value, null); // 初始化一个结点
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash); // 
                    break;
                }
                // 如果找到了已经存在,就不添加直接跳出循环
                if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k)))) 
                    break;
                p = e;
            }
        }
        if (e != null) { // 存在映射了
            V oldValue = e.value;
            // 如果onlyIfAbsent为true或者oldValue为空则替换
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue; // 返回oldValue
        }
    }
    ++modCount;// HashMap结构修改计数加1
    if (++size > threshold) // 如果此时保存的键值对数量超过了阈值
        resize(); // 再散列
    afterNodeInsertion(evict);
    return null;
}

putVal()方法涉及到调用其他众多方法,这里不再追查了。从putVal()方法中我们获得了很多信息,从编码风格上源码中似乎喜欢比较简洁的写法,比如像if ((tab = table) == null || (n = tab.length) == 0)p = tab[i = (n - 1) & hash]等等,喜欢直接把tab,n,i初始化揉在判断课索引里。还有个特点是喜欢灵活使用for循环,像for (int binCount = 0; ; ++binCount)或for(;;)这种。另外我们的保存数据的容器为transient Node<K,V>[] table;的Node数组,从源码中看,我们的数组中每个Node节点不一定是链表结构的起始节点,而且可以是树结构的根节点。

4.3.2 扩容

在putVal()中我们注意到resize()方法,我们看看HashMap是怎样扩容(再散列)的:

final Node<K,V>[] resize() {
    // 1. 初始化必要的变量
    Node<K,V>[] oldTab = table; // 暂存旧容器
    int oldCap = (oldTab == null) ? 0 : oldTab.length; // 旧容量
    int oldThr = threshold; // 旧阈值
    int newCap, newThr = 0; // 初始化新的容量和阈值
    
    // 2. 计算新阈值和新容量
    if (oldCap > 0) { // 如果旧容量大于0
        
        if (oldCap >= MAXIMUM_CAPACITY) { // 旧容量大于最大允许容量
            threshold = Integer.MAX_VALUE; // 阈值设为最大
            return oldTab; // 无法再扩容,直接返回旧容器
        }else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)// 否则新容量加倍
            newThr = oldThr << 1; // 新容量没超过最大容量,阈值加倍
        
    }else if (oldThr > 0)
        newCap = oldThr; // 用旧阈值代替新容量
    else {               // 否则用默认值初始化新容量和新阈值
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    
   if (newThr == 0) { // 上面分支计算完,如果新阈值为0
        float ft = (float)newCap * loadFactor; // 计算阈值
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    
    threshold = newThr; // 得到最终阈值更新
    @SuppressWarnings({"rawtypes","unchecked"})
    // 3. 再散列过程
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // 根据新容量建一个新容器
    table = newTab; // 用新容器替代原来的容器
    if (oldTab != null) { // 如果旧容器非空
        for (int j = 0; j < oldCap; ++j) { // 遍历旧容器的每一个桶位
            Node<K,V> e;
            if ((e = oldTab[j]) != null) { // 该桶位赋值给e,如果非空
                oldTab[j] = null;
                if (e.next == null) //e.next为空,该桶位只有一个节点
                    newTab[e.hash & (newCap - 1)] = e; // 直接存给新桶
                else if (e instanceof TreeNode) // 如果是树结构的根节点
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else {
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    // 遍历桶中的链表
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) { // 索引值不变
                            if (loTail == null) 
                                loHead = e; // 头结点
                            else
                                loTail.next = e; // 连接新元素
                            loTail = e; // 尾结点指向下一位
                        }
                        else { // 索引值改变
                            if (hiTail == null)
                                hiHead = e; // 头结点
                            else
                                hiTail.next = e; // 连接新元素
                            hiTail = e; // 尾结点指向下一位
                        }
                    } while ((e = next) != null);
                    // 保存到(j)位置,即原位置
                    if (loTail != null) {
                        loTail.next = null; // 尾结点设为null
                        newTab[j] = loHead; // 数组前半段
                    }
                    // 保存到(oldCap + j)位置,即后半段
                    if (hiTail != null) {
                        hiTail.next = null; // 尾结点设为null
                        newTab[j + oldCap] = hiHead; // 数组后半段
                    }
                }
            }
        }
    }
    return newTab;
}

在计算新的索引值时,值的注意的是数组扩容后是之前的2倍(newCap = 2 × \times ×oldCap),我们计算新位置时用的是e.hash &(newCap - 1)= e.hash &(2* × \times ×oldCap -1)。以oldCap = 8,newCap = 16为例:

  • 新的索引为 e.hash & (newCap -1) = e.hash & 15 = e.hash&1111。如果e.hash & oldCap = e.hash & 8 = e.hash & 1000 == 0证明e.hash第 2 3 2^3 23位是0,所以新的索引e.hash&1111 = e.hash;否则证明 2 3 2^3 23位是1,则新的下标e.hash&1111 = e.hash + oldCap

4.3.3 查询

public V get(Object key) {
    Node<K,V> e;
    // 调用getNode
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}
/**
 * @param hash  key的散列值
 * @param key   键对象
 */
final Node<K,V> getNode(int hash, Object key) {
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    // 在判断时 first = tab[(n - 1) & hash]已经根据计算找到对应的桶位
    if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
        if (first.hash == hash && // 检查第一个结点是要找的么
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
        if ((e = first.next) != null) {
            if (first instanceof TreeNode) // 是树节点就getTreeNode搜索并返回
                return ((TreeNode<K,V>)first).getTreeNode(hash, key);
            do { // 是链表就继续向下搜索
                if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))//找到
                    return e;// 返回
            } while ((e = e.next) != null);
        }
    }
   return null; // 没找到返回null
}

4.3.4 遍历

遍历方法我直接搜了个网址HashMap遍历的四种方法,列举方法比较详尽,这里我就不详细说了。

一般遍历都是采用遍历map.entrySet(),或者用map.entrySet().iterator()返回一个迭代器。差异在后者采用迭代器的方式能够通过Itertor中的remove()方法删除一个entry。另外还有两种只遍历map.keySet()和map.values()的方法。但是通过map.keySet()得到key在去调用map.get(key) 遍历values是效率底下的,不推荐使用。

接下来我们看看HashMap中的entrySet()方法:

public Set<Map.Entry<K,V>> entrySet() {
    Set<Map.Entry<K,V>> es;
    return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}

该方法直接返回了一个内部类EntrySet的引用,查看该内部类并没有显示的定义任何构造函数,即调用的是默认的无参构造函数,所以该方法仅仅是返回了一个EntrySet引用。

观察EntrySet中继承了AbstractSet, AbstractSet又是继承自AbstractCollection, 查看AbstractCollection我们看到了public abstract Iterator<E> iterator();EntrySet中实现的itertor()来源就是这里,搞了半天迭代器是来源是这么追溯的EntrySet–>AbstractSet–>AbstractCollection–>Collection–>Iterable,Iterable中的iterator()是一切iterator的来源。

public final Iterator<Map.Entry<K,V>> iterator() {
    return new EntryIterator();
}

在遍历时,像这样:

Map<Integer, Integer> map = new HashMap<Integer, Integer>();
for(Map.Entry<Integer, Integer> entry : map.entrySet()){
	System.out.println("key = " + entry.getKey() + ", value = " + entry.getValue())

当我们调用增强for循环时,会自动调用map.entrySet()返回引用的类的iterator()方法,该方法又返回一个内部类的引用EntryIterator():

final class EntryIterator extends HashIterator implements Iterator<Map.Entry<K,V>> {
    public final Map.Entry<K,V> next() { return nextNode(); }
}

该类继承了HashIterator实现了Iterator接口。在调用增强for循环时会调用hasNext()和next()方法,就来自这儿。

next()方法返回nextNode();,该方法是内部类HashIterator中的方法,它返回了容器中的下一个节点,方法如下:

// 下面current,next,expectedModeCount,index都是HashIterator在new EntryIterator()被子类初始化的。
public final boolean hasNext() {
    return next != null;// next是下一个entry
}
final Node<K,V> nextNode() {
    Node<K,V>[] t;
    Node<K,V> e = next;
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
    if (e == null)
        throw new NoSuchElementException();
    // 先遍历链表next = (current = e).next,在遍历下一个桶位next = t[index++]链表
    // 这里t=table直接获取外部类容器了,所以数据是这么获取到的
    if ((next = (current = e).next) == null && (t = table) != null) { 
        do {} while (index < t.length && (next = t[index++]) == null);
    }
    return e;
}

所以这里我们可以总结一下增强for循环遍历集合:编译器会自动执行集合的iterator()方法返回Iterator实例, 然后根据泛型声明一个指定对象类型的对象,然后进行while循环,使用Iterator的hasNext()方法和next()方法进行取值。

当然我们也可以这样遍历:

Iterator<Map.Entry<Integer, Integer>> entries = map.entrySet().iterator();
while (entries.hasNext()) {
	Map.Entry<Integer, Integer> entry = entries.next();
	System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());

这样相当于直接显示调用的EntrySet的iterator()方法得到Iterator实例,然后进行显示的遍历。

5 如何编写hashCode()和equals()函数

equals()和hashCode()都是Object根类的方法,下面是Object根类中两个方法:

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

从上面代码,我们可以看出,equals()方法默认比较的是两个对象的地址,如果需要比较两个对象的内容需要我们自己去覆盖该方法。hashCode()则是一个native方法,它的具体实现不是由Java语言实现的,该方法返回的是一个与Object对象地址等信息相关的整数。

5.1 覆盖equals()

equals()方法设计是专门用来比较两个对象的,如果不覆盖原始的Object.equals()方法,则比较的是对象地址,这和==有点类似。如果我们需要定义自己的类,并实现内容的比较,则需要自己覆盖此方法。Java集合类中对equals()就是覆盖了的,另外在自定义对象基于比较(如排序)中也是需要覆盖此方法的。

那么如何写覆盖一个equals()方法呢?需要遵循下面的规则(摘自《Java编程思想》):

  1. 自反性。对任意的x,x.equals(x)一定返回true;
  2. 对称性。对任意x,y,如果y.equals(x)返回true,则x.equals(y)也返回true;
  3. 传递性。对任意x、y、z,如果x.equals(y)返回ture,y.equals(z)返回true,则x.equals(z)一定返回true;
  4. 一致性。对任意x,y,如果对象中用于等价比较的信息没有改变,那么无论调用x.equals(y),多少次,返回结果应该保持一致。
  5. 对任何不是null的x,x.equals(null)一定返回false。

5.2 覆盖hashCode()

如果要使用自己的类作为HashMap的键,必须重写equals()和hashCode()方法。事实上,所有与hash有关的集合类都是覆盖过Object.hashCode()方法的。

编写hashCode()我们需要明确,多个对象是可以有相同散列码的,但是无论何时同一个对象调用hashCode()都应该生成同样的值,并且好的hashCode()方法产生散列码应当尽量分布均匀。《Java编程思想》中引用了《Effective Java Programming Language Guide》一书中的指导:

  1. 给int变量赋予某个非零值常量,例如17;
  2. 为对象内每个有意义的域f(即每个可以做equals()操作的域)计算出一个int散列码c:
域类型计算
booleanc=(f?0:1)
byte,char,short,intc=(int)f
longc=(int)(f^(f>>>32))
floatc=Float.floatToIntBits(f)
doublec=long l = Double.doubleToLongBits(f)
Object,其equals()调用这个域的equals()c=f.hashCode()
数组对每个元素应用上述规则
3. 合并计算得到的散列码: result := 37*result + c; 4. 返回result; 5. 检查hashCode()最后生成的散列码,确保相同对象有相同的散列码。

结语

终于写完了,估计内容会有一些地方不正确,希望读到的网友指出来,我好改过来,免得误导大家。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值