`

hash表(续)

 
阅读更多

    在这一篇里我主要说一下自己对于系统自带各种hash函数的看法。

    1)hashMap:

         hashMap是一种基于Map的结构,通过键值Key存储对应的Value,构造原理与我的hash代码相同(通过hash函数的计算,得出index,存入数据),但是具体的实现存在一些差异。首先,这里插入数据时需要指定key与Value值,put方法得到用户输入的key后将Key.hashCode通过位运算转换为int(hashCode返回的是对象存储的物理位置,两个对象如果不相同,那么他们的hashCode必然不同),然后对所得的整数与数组长度进行计算得出index插入位。

        其实看源码会发现,这里实际实现存储的还是一个数组(此数组定义使用了关键字transient,即串行化时不会包含此变量,我对于串行化的理解就是将对象进行存储或者传输,然后可以再读取所存储的字节重新构建一个与原始对象状态相同的对象,也就是反串行化。比如定义了String name以及transient String pwd,我们实例化一个具有这两个属性的对象a("艾","123"),然后将对象a通过流对象写出到磁盘,再读入后打印,我们会发现pwd属性变为了空。我觉得transient关键字就像一种保护措施,比如银行等机构要将用户操作存入磁盘或者备份,又不能将密码等私人信息写入,便可以使用此方法使操作如往常一样执行,同时也避免了信息泄露),数组中存储了定义的Entry<K,V> e,计算得到index后便找到数组table中的这一位,对内部存储的链表结构进行遍历查找(e = e.next),如果hash(由hashCode转换得出,所以除非两个元素相同才会有相同的hash)与key都相同,便修改已存储的值,如果未找到相同元素且找到了e = null,即将相应元素插入这个位置。get方法的实现与put同一原理,先根据输入的key计算hash值,得出index,找到对应数组节点,在节点内的链表结构中遍历寻找所求数据。

         再看源码的过程中,我也找到了几个使得代码高效的方法:

/**
     * Applies a supplemental hash function to a given hashCode, which
     * defends against poor quality hash functions.  This is critical
     * because HashMap uses power-of-two length hash tables, that
     * otherwise encounter collisions for hashCodes that do not differ
     * in lower bits. Note: Null keys always map to hash 0, thus index 0.
     */
    static int hash(int h) {
        // 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);
    }

    /**
     * Returns index for hash code h.
     */
    static int indexFor(int h, int length) {
        return h & (length-1);
    }

 

           可以看出在将hashCode转换为int值的过程中,直接采用了位运算,在计算index时,也使用了&与运算,比如3&7=3,因为3的二进制表达为0011,7的二进制表达为0111,0011&0111与运算=0011。这些直接进行的位运算极大的提高了机器的操作效率,尤其是大量数据处理,比起我的直接对int进行操作确实高明了很多。还有一点是在将原有table中的元素转换入新的table时,采用了读取后直接转换存入的方法,我之前程序中是将所有的元素读出放入一个list,再遍历该list,将元素计算放入新的数组中,毋庸置疑这也导致了很大的时间上的消耗。这里,我将系统的reHash部分实现贴出来:

/**
     * Rehashes the contents of this map into a new array with a
     * larger capacity.  This method is called automatically when the
     * number of keys in this map reaches its threshold.
     *
     * If current capacity is MAXIMUM_CAPACITY, this method does not
     * resize the map, but sets threshold to Integer.MAX_VALUE.
     * This has the effect of preventing future calls.
     *
     * @param newCapacity the new capacity, MUST be a power of two;
     *        must be greater than current capacity unless current
     *        capacity is MAXIMUM_CAPACITY (in which case value
     *        is irrelevant).
     */
    void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable);
        table = newTable;
        threshold = (int)(newCapacity * loadFactor);
    }

    /**
     * Transfers all entries from current table to newTable.
     */
    void transfer(Entry[] newTable) {
        Entry[] src = table;
        int newCapacity = newTable.length;
        for (int j = 0; j < src.length; j++) {
            Entry<K,V> e = src[j];
            if (e != null) {
                src[j] = null;
                do {
                    Entry<K,V> next = e.next;
                    int i = indexFor(e.hash, newCapacity);
                    e.next = newTable[i];
                    newTable[i] = e;
                    e = next;
                } while (e != null);
            }
        }
    }

          transfer代码中遍历了原来的table,读取每一个位置内部存储的元素,计算后放入新的table中,并清除原有数组中此位置的元素,还采用了e = e.next以及do、while方法寻找index链表内部的全部存储元素,要比我的递归查询速度快,最后将新的table赋给原有table。

         看过了源码后,我将之前所写的代码里中间部分的list过度去除,数据操作的速度提升了一些,但还是和系统的有差距,问题应该在计算部分,我的代码中采用 存入值 mod 数组长度 的方法,着实不是很明白这里位运算的操作原理,是不是与运算在加快处理速率的基础上还能够提升数据存储的均衡性呢?求讲解~~~还有一点很深的感触就是java的源码真的写得很精简,用最少的行数实现了功能的最大化。起初阅读源码觉得有点不习惯,等慢慢理解了之后回过头看自己的代码,真的觉得差距很大,里面定义了各种其实不必要的变量或者过渡的操作,这属于代码质量的问题,好的代码习惯或者风格都是在平时点点滴滴积累起来的,以后一定要注意自己代码的“美观度”啊~~

         在这里也提一下,记得原来在数据结构的课程中也了解过hash结构,跟我所写的hash有一点点的区别,其中包含了一个二次线性再散列的方法,主要用来处理当某一个元素位存储了过多的元素,如果此时再插入一个元素在这个位置,势必要进行很多次的遍历比较是否为相同元素,花费过高的时间代价。此时可以使用一个线性处理,比如index = 2 * index + a(这里只是打个比方,具体的函数可以自己去查阅),将元素存放在其他位置,这个过程也可以称之为散列,这样就可以规避一些过高花费的操作。当然,实际中这个问题也可以通过一些其它方式来解决,比如设计合理的装载因子以及hash函数,使得数组的负载处在一个均衡状态,在遍历花费还未对搜索时间造成可估计影响之前,经由判断调用reHash,重构数组。

     2)HashSet:

          HashSet实现了一个Set接口,其内部实现实际上就是一个HashMap,所以在这里我们不再赘述它的实现。在我对几个系统Hash类进行测试的时候也发现,HashSet打印出的数据是无序的,完全与Set的特性相一致,其余均为有序的存储。

     3)HashTable:

          常拿来和HashMap作对比的类,那么先说一下两者的区别吧:HashMap是一个实现了map接口的实现类(HashSet实现了Set接口),而HashTable是继承了Dictionary的子类;在HashMap中,允许使用null作为键,可是这样的键只有一个,可以有一个或多个键所对应的值为null,因此当get()方法返回null值时,即可以表示HashMap中没有该键,也可以表示该键所对应的值为null,在HashMap中不能由get()方法来判断HashMap中是否存在某个键,而应该用containsKey()方法来判断。HashTable中不允许出现null的键值;第三个区别是最重要的,也正是这个区别使得两个类存在了是否可以使用null值做键的差别—同步。如果阅读HashTable的源码,你一定会发现,除了HashMap中出现过的关键字transient被用来定义属性外,所有的方法都被一个关键字修饰,那就是Synchronized。

         这个被称作同步的关键字主要是被用来保证在多线程的情况下,并发的操作会被控制使得每次只有一个线程执行操作,引用一个专业点的说法就是:当两个并发线程访问同一个对象object中的这个synchronized同步代码块时,一个时间内只能有一个线程得到执行,另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块(在这里要声明一下:synchronized关键字无论加在方法上还是对象上,它锁定的都是对象,而不是把一段代码或者函数当做锁,而且每个对象只有一个锁与之相关联),当然,此时其他线程可以访问代码中的其他非Synchronized方法,但是要注意其他线程对object中所有其它synchronized同步代码块的访问将被阻塞,也就是说,当一个线程访问object的一个synchronized同步代码块时,它就获得了这个object的对象锁。结果,其它线程对该object对象所有同步代码部分的访问都被暂时阻塞。也正是因为这一点,使用Synchronized关键字可能会导致庞大的系统开销,所以在使用的时候要慎重。

          说完了两者的区别,我们来看一下HashTable的内部实现。HashTable也是基于map结构的存储,它的实现思路与我Myhash非常相像,比如在这段实现put的代码中,直接取key值的hashCode,& 0x7FFFFFFF实现了对这个32位值的首位取正数,然后将这个值与当前数组的长度取模,得到插入位置index:

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 = key.hashCode();
	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;
            index = (hash & 0x7FFFFFFF) % tab.length;
	}

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

           get的原理与put相同,此处不再赘述。现在我们来看看Table中的reHash部分:

protected void rehash() {
	int oldCapacity = table.length;
	Entry[] oldMap = table;

	int newCapacity = oldCapacity * 2 + 1;
	Entry[] newMap = new Entry[newCapacity];

	modCount++;
	threshold = (int)(newCapacity * loadFactor);
	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;

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

       可以看出,在Hashtable中新建数组的长度是原来的两倍加1,相对于Hashmap中的do、while循环,它的新值存放通过潜逃的for循环、old = old.next、old !=null控制实现,其他的方法比较好理解,在这里也不再说,想要研究的同学自己去查看吧~

       在这里我再补充一点,相信大家再看代码的时候会注意到,HashMap以及HashTable都使用到了Entry<K,V>这个类来初始化数组索引位的对象,这与他们是基于map存储数据的特点相关,这里的Entry<K,V>是接口Map.Entry<K,V> 的实现类,接口内部定义了属性int hash、K key、 V value、Entry<K,V> next,还有一些方法名,不同的类实现的方法肯定会存在差异,但是接口的使用保证了外部输入、结果返回的一致性。

        就写到这里了,文章里的都是自己的一些认识,很可能存在问题,欢迎大家指正~~

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics