【Java源码分析】HashTable源码分析

类的定义

public class Hashtable<K,V>
extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable {}

注意前面虽然说HashTable和HashMap是非常像的,但是这两个类的父类是不一样的。前者是字典类的子类,后者是抽象Map的子类。

  1. HashTable 也是将key映射到value的集合类,key不允许为null,并且作为key的类是必须实现hashCode()和equals()方法
  2. 影响HashTable的性能的两个因素也是容量capacity和装载因子load-factor。关于这一点和HashMap是一样的,默认装载因子也是0.75
  3. 初始化容量用于控制空间利用率和扩容之间的平衡,如果初始容量很小,那么后续可能引发多次扩容(扩容会重新分配空间,再hash,并拷贝数据,所以比较耗时);如果初始容量很大,可能会有点浪费存储空间;所以和HashMap一样,最好预估一个合理的初始大小
  4. 迭代器创建并迭代的过程中同样是不允许修改HashTable结构的(比如增加或者删除数据),否则出现fail-fast,同时fail-fast虽然抛出了ConcurrentModificationException但是它并不能保证线程安全性
  5. HashTable是线程安全的如果程序本身不是多线程环境下运行,那么建议使用HashMap,如果是高并发环境下,建议使用java.util.concurrent.ConcurrentHashMap,只有在一般并发环境下建议使用HashTable

重要的成员变量

private transient Entry<K,V>[] table; // 基于数组实现
private transient int count; // 实际存放的实体个数
private int threshold; // 阈值,用于判断是否需要扩容
private float loadFactor; // 装载因子

构造函数

public Hashtable() {
    this(11, 0.75f);
}

public Hashtable(int initialCapacity) {
    this(initialCapacity, 0.75f);
}

public Hashtable(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal Load: "+loadFactor);

    if (initialCapacity==0)
        initialCapacity = 1;
    this.loadFactor = loadFactor;
    table = new Entry[initialCapacity];
    threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    useAltHashing = sun.misc.VM.isBooted() &&
            (initialCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
}

public Hashtable(Map<? extends K, ? extends V> t) {
    this(Math.max(2*t.size(), 11), 0.75f);
    putAll(t);
}

构造函数和HashMap一样,不同点在于默认容量不一样,HashTable的默认容量是11

hash函数

transient final int hashSeed = sun.misc.Hashing.randomHashSeed(this);
private int hash(Object k) {
    if (useAltHashing) {
        if (k.getClass() == String.class) {
            return sun.misc.Hashing.stringHash32((String) k);
        } else {
            int h = hashSeed ^ k.hashCode();

            // This function ensures that hashCodes that differ only by
            // constant multiples at each bit position have a bounded
            // number of collisions (approximately 8 at default load factor).
            h ^= (h >>> 20) ^ (h >>> 12);
            return h ^ (h >>> 7) ^ (h >>> 4);
         }
    } else  {
        return k.hashCode();
    }
}

hash的计算和HashMap是一样的

判断是否存在给定值

public synchronized boolean contains(Object value) {
    if (value == null) {
        throw new NullPointerException();
    }

    Entry tab[] = table;
    for (int i = tab.length ; i-- > 0 ;) {
        for (Entry<K,V> e = tab[i] ; e != null ; e = e.next) {
            if (e.value.equals(value)) {
                return true;
            }
        }
    }
    return false;
}

public boolean containsValue(Object value) {
    return contains(value);
}

可以看到这里一旦发现value是null就直接抛出异常,这样直接说明HashTable是不允许null-value的。另外该方法虽然是contains(),但是实际功能和containsValue()是一样的,从containsValue()的实现可以看出

给定的key是否存在

public synchronized boolean containsKey(Object key) {
    Entry tab[] = table;
    int hash = hash(key);
    int index = (hash & 0x7FFFFFFF) % tab.length;
    for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            return true;
        }
    }
    return false;
}

当且仅当给定的key是存在的时候才返回true,但是注意一点的是这里所说的是否存在是取决于equals()方法是如何实现的。这和开头所强调的存入HashTable的key必须实现hashCode和equals方法是一致的.这里(hash & 0x7FFFFFFF)的目的应该是为了保证hash是一个正值,毕竟只有第32位为0其他都是1

使用给定的key查找

public synchronized V get(Object key) {
    Entry tab[] = table;
    int hash = hash(key);
    int index = (hash & 0x7FFFFFFF) % tab.length;
    for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            return e.value;
        }
    }
    return null;
}

同HashMap,因为这里的底层实现结构都是一样的,都是基于数组,数组的每个下标对应一个单链表。如果对应的key在HashTable中存在着对应的value那么返回找到的value,否则返回Null

扩容

protected void rehash() {
    int oldCapacity = table.length;
    Entry<K,V>[] oldMap = table;

    // overflow-conscious code
    int newCapacity = (oldCapacity << 1) + 1;
    if (newCapacity - MAX_ARRAY_SIZE > 0) {
        if (oldCapacity == MAX_ARRAY_SIZE)
            // Keep running with MAX_ARRAY_SIZE buckets
            return;
        newCapacity = MAX_ARRAY_SIZE;
    }
    Entry<K,V>[] newMap = new Entry[newCapacity];

    modCount++;
    threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    boolean currentAltHashing = useAltHashing;
    useAltHashing = sun.misc.VM.isBooted() &&
            (newCapacity >= Holder.ALTERNATIVE_HASHING_THRESHOLD);
    boolean rehash = currentAltHashing ^ useAltHashing;

    table = newMap;

    for (int i = oldCapacity ; i-- > 0 ;) {
        for (Entry<K,V> old = oldMap[i] ; old != null ; ) {
            Entry<K,V> e = old;
            old = old.next;

            if (rehash) {
                e.hash = hash(e.key);
            }
            int index = (e.hash & 0x7FFFFFFF) % newCapacity;
            e.next = newMap[index];
            newMap[index] = e;
        }
    }
}

当实际装入的键值对的个数超过了table的长度也就是容量capacity和装载因子的乘积,就会进行扩容,扩容过程是自动的,但是比较耗时。扩增后的容量是最初的容量的2倍加1,这里和HashMap对比就很容易理解,HashMap的初始容量是偶数,而且要求容量一直是偶数,那么扩容的时候就直接扩大到原来的2倍。相应的,HashTable的初始容量是奇数11,2倍之后变成偶数,所以加1。不过这里没有要求HashTable的容量必须是奇数,但无论如何扩容后容量都会变成奇数

添加键值对

public synchronized V put(K key, V value) {
    // Make sure the value is not null
    if (value == null) {
        throw new NullPointerException();
    }

    // Makes sure the key is not already in the hashtable.
    Entry tab[] = table;
    int hash = hash(key);
    int index = (hash & 0x7FFFFFFF) % tab.length;
    for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            V old = e.value;
            e.value = value;
            return old;
        }
    }

    modCount++;
    if (count >= threshold) {
        // Rehash the table if the threshold is exceeded
        rehash();

        tab = table;
        hash = hash(key);
        index = (hash & 0x7FFFFFFF) % tab.length;
    }

    // Creates the new entry.
    Entry<K,V> e = tab[index];
    tab[index] = new Entry<>(hash, key, value, e);
    count++;
    return null;
}

注意键和值都必须不能为NULL添加过程中如果发现给定的key已经存在,那么替换旧值并返回旧值,否则将新的键值对加入到HashTable,其中如果添加过程中发现实际键值对数目已经超过阈值,那么就进行扩容

删除键值对

public synchronized V remove(Object key) {
    Entry tab[] = table;
    int hash = hash(key);
    int index = (hash & 0x7FFFFFFF) % tab.length;
    for (Entry<K,V> e = tab[index], prev = null ; e != null ; prev = e, e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            modCount++;
            if (prev != null) {
                prev.next = e.next;
            } else {
                tab[index] = e.next;
            }
            count--;
            V oldValue = e.value;
            e.value = null;
            return oldValue;
        }
    }
    return null;
}

如果找到了给定key对应的键值对,那么就做删除操作,如果没有找到,那么就什么也不做。实际的删除过程就是单链表的节点删除

清空操作

public synchronized void clear() {
    Entry tab[] = table;
    modCount++;
    for (int index = tab.length; --index >= 0; )
        tab[index] = null;
    count = 0;
}

和HashMap的清空操作一样,只是将每个slot置为NULL,剩下的交给GC

比较两个HashTable是否相等

public synchronized boolean equals(Object o) {
    if (o == this)
        return true;

    if (!(o instanceof Map))
        return false;
    Map<K,V> t = (Map<K,V>) o;
    if (t.size() != size())
        return false;

    try {
        Iterator<Map.Entry<K,V>> i = entrySet().iterator();
        while (i.hasNext()) {
            Map.Entry<K,V> e = i.next();
            K key = e.getKey();
            V value = e.getValue();
            if (value == null) {
                if (!(t.get(key)==null && t.containsKey(key)))
                    return false;
            } else {
                if (!value.equals(t.get(key)))
                    return false;
            }
        }
    } catch (ClassCastException unused)   {
        return false;
    } catch (NullPointerException unused) {
        return false;
    }

    return true;
}

首先比较大小是否相等,如果大小相等,那么就依次比较实体集合中每个实体的是否是一样的键值对,如果所有的键值对都一样,那么就返回true,否则返回false ,注意由于HashTable中数据存储并不是有序的,所以实际比较的时候使用的是get()根据指定的key获取对应的value

获取HashTable的hashCode

public synchronized int hashCode() {
    /*
     * This code detects the recursion caused by computing the hash code
     * of a self-referential hash table and prevents the stack overflow
     * that would otherwise result.  This allows certain 1.1-era
     * applets with self-referential hash tables to work.  This code
     * abuses the loadFactor field to do double-duty as a hashCode
     * in progress flag, so as not to worsen the space performance.
     * A negative load factor indicates that hash code computation is
     * in progress.
     */
    int h = 0;
    if (count == 0 || loadFactor < 0)
        return h;  // Returns zero

    loadFactor = -loadFactor;  // Mark hashCode computation in progress
    Entry[] tab = table;
    for (Entry<K,V> entry : tab)
        while (entry != null) {
            h += entry.hashCode();
            entry = entry.next;
        }
    loadFactor = -loadFactor;  // Mark hashCode computation complete

    return h;
}

实际计算HashTable的hashCode是根据每个实体的hashCode来计算的,所有实体的hashCode()之和就是HashTable的hashCode。loadFractor扮演了一个标志位的角色,标志计算的开始和结束。关于这里h += entry.hashCode()中h是否会溢出的问题,上面再hash()函数中已经看到了每次求hash的时候都会与0x7fffffff,也就是说hash永远为正,不会溢出

序列化和反序列化

private void writeObject(java.io.ObjectOutputStream s)
        throws IOException {
    Entry<K, V> entryStack = null;

    synchronized (this) {
        // Write out the length, threshold, loadfactor
        s.defaultWriteObject();

        // Write out length, count of elements
        s.writeInt(table.length);
        s.writeInt(count);

        // Stack copies of the entries in the table
        for (int index = 0; index < table.length; index++) {
            Entry<K,V> entry = table[index];

            while (entry != null) {
                entryStack =
                    new Entry<>(0, entry.key, entry.value, entryStack);
                entry = entry.next;
            }
        }
    }

    // Write out the key/value objects from the stacked entries
    while (entryStack != null) {
        s.writeObject(entryStack.key);
        s.writeObject(entryStack.value);
        entryStack = entryStack.next;
    }
}

private void readObject(java.io.ObjectInputStream s)
     throws IOException, ClassNotFoundException
{
    // Read in the length, threshold, and loadfactor
    s.defaultReadObject();

    // set hashSeed
    UNSAFE.putIntVolatile(this, HASHSEED_OFFSET,
            sun.misc.Hashing.randomHashSeed(this));

    // Read the original length of the array and number of elements
    int origlength = s.readInt();
    int elements = s.readInt();

    // Compute new size with a bit of room 5% to grow but
    // no larger than the original size.  Make the length
    // odd if it‘s large enough, this helps distribute the entries.
    // Guard against the length ending up zero, that‘s not valid.
    int length = (int)(elements * loadFactor) + (elements / 20) + 3;
    if (length > elements && (length & 1) == 0)
        length--;
    if (origlength > 0 && length > origlength)
        length = origlength;

    Entry<K,V>[] table = new Entry[length];
    threshold = (int) Math.min(length * loadFactor, MAX_ARRAY_SIZE + 1);
    count = 0;
    useAltHashing = sun.misc.VM.isBooted() &&
            (length >= Holder.ALTERNATIVE_HASHING_THRESHOLD);

    // Read the number of elements and then all the key/value objects
    for (; elements > 0; elements--) {
        K key = (K)s.readObject();
        V value = (V)s.readObject();
        // synch could be eliminated for performance
        reconstitutionPut(table, key, value);
    }
    this.table = table;
}

注意在反序列化过程中,并不会重建一个和序列化时候一模一样的HashTable,相反,会根据实际的大小和装载因子等数据创建一个更加合理大小的数组来存放数据,也就是上面加注释的求取新的length的代码。首先需要保证的是新的length是不能超过原来的length,否则没有意义,另外是保证这个新的length是奇数,而且比原来增加5%.另外这里也说明了为什么capacity或者说length需要时奇数,主要是为了更均匀的映射。HashMap中设置为偶数也是为了均匀Hash

时间: 2024-10-11 12:51:52

【Java源码分析】HashTable源码分析的相关文章

【源码】Hashtable源码剖析

注:以下源码基于jdk1.7.0_11 上一篇分析了HashMap的源码,相信大家对HashMap都有了更深入的理解.本文将介绍Map集合的另一个常用类,Hashtable. Hashtable出来的比HashMap早,HashMap 1.2才有,而Hashtable在1.0就已经出现了.HashMap和Hashtable实现原理基本一样,都是通过哈希表实现.而且两者处理冲突的方式也一样,都是通过链表法.下面我们就详细介绍下这个类. 首先看类声明: public class Hashtable<

java集合框架09——HashTable和源码分析

上一章我们学习了HashMap的源码,这一节我们来讨论一下HashTable,HashTable和HashMap在某种程度上是类似的.我们依然遵循以下步骤:先对HashTable有个整体的认识,然后学习它的源码,深入剖析HashTable. 1.HashTable简介 首先看一下HashTable的继承关系 java.lang.Object ? java.util.Dictionary<K, V> ? java.util.Hashtable<K, V> public class H

Java入门系列之集合Hashtable源码分析(十一)

前言 上一节我们实现了散列算法并对冲突解决我们使用了开放地址法和链地址法两种方式,本节我们来详细分析源码,看看源码中对于冲突是使用的哪一种方式以及对比我们所实现的,有哪些可以进行改造的地方. Hashtable源码分析 我们通过在控制台中实例化Hashtable并添加键值对实例代码来分析背后究竟做了哪些操作,如下: public static void main(String[] args) { Hashtable hashtable = new Hashtable(); hashtable.p

JAVA的HashTable源码分析

Hashtable简介 Hashtable同样是基于哈希表实现的,同样 每个元素是一个key-value对,其内部也是通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长. Hashtable也是JDK1.0引入的类,是线程安全的,能用于多线程环境中. Hashtable同样 实现了Serializable接口,它支持序列化,实现了Cloneable接口,能被克隆 . HashTable源码剖析 Hashtable的源码的很多实现都与HashMap差不多,源码如下(加入了比较详细的注

关于java中ReentrantLock类的源码分析以及总结与例子

一,官方描述 关于ReentrantLock的官方描述,英文的就不贴出来了,这里我只贴出我自己翻译的描述: reentrant是一个跟synchronized具有相同行为和语义的持有锁来访问方法和语句的互斥锁,但是reentrant还拥有被扩展的能力. ReentrantLock会被线程拥有并且持续锁定,不会解锁.线程调用lock()方法返回后,则成功持有锁,否则这个锁正在被另一个线程所持有,只能等待另一个线程释放锁,如果当前线程拥有了锁,则调用lock()方法会立即返回,这个状态可以通过isH

Java中arraylist和linkedlist源码分析与性能比较

Java中arraylist和linkedlist源码分析与性能比较 1,简介 在java开发中比较常用的数据结构是arraylist和linkedlist,本文主要从源码角度分析arraylist和linkedlist的性能. 2,arraylist源码分析 Arraylist底层的数据结构是一个对象数组,有一个size的成员变量标记数组中元素的个数,如下图: * The array buffer into which the elements of the ArrayList are sto

Java并发系列[2]----AbstractQueuedSynchronizer源码分析之独占模式

在上一篇<Java并发系列[1]----AbstractQueuedSynchronizer源码分析之概要分析>中我们介绍了AbstractQueuedSynchronizer基本的一些概念,主要讲了AQS的排队区是怎样实现的,什么是独占模式和共享模式以及如何理解结点的等待状态.理解并掌握这些内容是后续阅读AQS源码的关键,所以建议读者先看完我的上一篇文章再回过头来看这篇就比较容易理解.在本篇中会介绍在独占模式下结点是怎样进入同步队列排队的,以及离开同步队列之前会进行哪些操作.AQS为在独占模

Java并发系列[5]----ReentrantLock源码分析

在Java5.0之前,协调对共享对象的访问可以使用的机制只有synchronized和volatile.我们知道synchronized关键字实现了内置锁,而volatile关键字保证了多线程的内存可见性.在大多数情况下,这些机制都能很好地完成工作,但却无法实现一些更高级的功能,例如,无法中断一个正在等待获取锁的线程,无法实现限定时间的获取锁机制,无法实现非阻塞结构的加锁规则等.而这些更灵活的加锁机制通常都能够提供更好的活跃性或性能.因此,在Java5.0中增加了一种新的机制:Reentrant

Java集合专题总结(1):HashMap 和 HashTable 源码学习和面试总结

2017年的秋招彻底结束了,感觉Java上面的最常见的集合相关的问题就是hash--系列和一些常用并发集合和队列,堆等结合算法一起考察,不完全统计,本人经历:先后百度.唯品会.58同城.新浪微博.趣分期.美团点评等都在1.2--面的时候被问过无数次,都问吐了&_&,其他公司笔试的时候,但凡有Java的题,都有集合相关考点,尤其hash表--现在总结下. Java集合概述 HashMap介绍 HashMap源码学习 关于HashMap的几个经典问题 HashTable介绍和源码学习 Hash

Java 集合系列 10 Hashtable详细介绍(源码解析)和使用示例

java 集合系列目录: Java 集合系列 01 总体框架 Java 集合系列 02 Collection架构 Java 集合系列 03 ArrayList详细介绍(源码解析)和使用示例 Java 集合系列 04 LinkedList详细介绍(源码解析)和使用示例 Java 集合系列 05 Vector详细介绍(源码解析)和使用示例 Java 集合系列 06 Stack详细介绍(源码解析)和使用示例 Java 集合系列 07 List总结(LinkedList, ArrayList等使用场景和