Java HashMap的一种简单实现 以及HashMap、HashSet、HashTable等的主要区别

哈希表

散列表,也就是哈希表,是一种可以用常数时间执行插入、删除和查找的技术,但元素间的排序关系往往是得不到支持的。

一般来说,对字符串的哈希是最频繁的,在Java,hashCode作为一个java.lang.Object下的方法,对所有的Object都是可用的,其声明为:

public native int hashCode();

另外还有一个也声明为native的hash方法:

public static native int identityHashCode(Object x);

执行以下代码段,结果将返回true

	//ss是一个类的实例
	int i = ss.hashCode();
	int ii = System.identityHashCode(ss);
	System.out.println(i == ii);

所常用的String哈希源码:

		//String类的hash方法
	    public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return

LinkedHashMap中对key的Hash方法(不是hashCode):

	//LinkedHashMap中的hash
	    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

在哈希表中,一个优秀的哈希方法可以充分利用分配的内存,且不容易造成冲突。

解决冲突的方法:

1、分离链接

将散列值相同的元素合并为一个链表放进数组,即整个哈希表就是一个链表数组

2、开放地址

即允许同散列值的元素占用还未占用的数组位置,至于分配位置的方法,主要有三种:

2.1、线性探测法:即寻找紧挨着的下一个空闲位置

2.2、平方探测法:假设某元素分配的位置i已被占用,则该元素尝试占用位置i^2

2.3、双散列:即对被占用的位置i再进行一次散列(二次散列方法一般不同于第一次使用的散列方法),拿到一个新的位置

但以上三种方法各自存在不同的缺陷或者性能问题,一个通病是在面对大量数据的时候,可能需要重复扩容,而扩容往往伴随着对所有元素重新散列(reHash)分配位置的操作,是不小的性能开销。

另外,还有完美散列、布谷鸟散列、跳房子散列等改进的算法。

完美散列:通过为非空的bucket创建N^2 (N > 1)size的二级散列表,并用不同的散列函数对同二级表的元素进行散列

布谷鸟散列:基本思路为创建N张散列表,并用N个散列函数对待插入的元素进行散列,这样该元素就能拿到在N个表中共N个期望的插入位置,并从第一张表开始尝试插入,若第一张表的期望位置已被占用,则1、替换原本在该位置的元素,让该元素换到其他空位;2、继续查找下一张表。布谷鸟散列可以通过并行实现,通过多线程提升效率

跳房子散列:线性探测的一种改良版本,在线性探测中,若发生冲突,则不断尝试寻找下一个空位,而跳房子散列则限制了尝试的次数,当尝试寻找空bucket的次数达到上限时,回头替换某个限度内的元素,被替换的元素继续进行有限次数的尝试。这样一来,使得查找等操作可以在常数的时间内完成。

例如:
a    b    c    d    _    _    对应0-5个bucket
设尝试的限度为三次,假设f的散列值与a相同,它的期望位置是0,但由于0已被占用,只好再进行两次尝试,直到发现c所在的位置2也被占用,则不再继续尝试,选择替换b,让b重新进行三次尝试:
结果为:
a    f    c    d    b    _
这样一来可以保证在进行查找操作候可以在常数时间内完成。


分离链接法如果采用单链表实现(文末会给出一个自己写的简单实现),则在面对大量数据的时候会导致所有基本操作的性能普遍下降,但可以对这个问题进行优化,目前想到的方法主要有三个:

初始化时根据数据量选择合适的初始化数组长度

当数据量慢慢增大时选择扩容(reHash),同时重新散列位置(用于数据量不可预见的增大)

同散列值的元素改用双链表(LinkedList就是一种双链表)模型储存

 

关于Java中的HashMap、HashSet、LinkedHashMap、HashTable以及ConcurrentHashMap的主要区别

HashMap:

使用分离链接法实现,线程不安全,在进行reHash时会发生线程竞争问题,key和Value可空

HashSet:

先看一段源码:

	//hashset源码
	private transient HashMap<E,Object> map;

    /**
     * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
     * default initial capacity (16) and load factor (0.75).
     */
    public HashSet() {
        map = new HashMap<>();
    }

HashSet其实是基于HashMap实现的,存取速度比较高,线程不安全,可空

LinkedHashMap:

使用了双链表结构维持了插入顺序的结构,基本上就是在HashMap的基础上对插入顺寻使用双链表进行了维护,线程不安全,可空,有序性对比例子:

	//hashmap和linkedhashmap顺序对比
		HashMap<String,Integer> s = new HashMap<String,Integer>();
		for(int i=0 ;i<10 ;i++) {
			s.put("aaa"+i, i);
		}
		System.out.println(s);
		LinkedHashMap<String,Integer> ss = new LinkedHashMap<String,Integer>();
		for(int i=0 ;i<10 ;i++) {
			ss.put("aaa"+i, i);
		}
		System.out.println(ss);
		

结果:

HashTable:

线程安全版本的HashMap,对整个表进行了加锁,key和value不能为null

ConcurrentHashMap:

线程安全,分段锁同步的实现,安全性不如HashTable,但性能和可扩展性优于hashTable,不可空

一种基于分离链接模型HashMap的简单实现:

package com.ryo.structure.hash;

/**
 * <p>分离链接模型的哈希表
 * <p>这里的实现是将散列到同一位置的元素组成一个单链表<br>默认长度为103,
 * 但也可以通过向构造函数传入一个大于103的整数创建一个自定义大小的哈希表,
 * 传入小于103的整数将默认创建103大小的表<br>未实现Entry
 * <br>未实现根绝负载因子扩容的功能,使用可变长度的初始化来解决这个问题
 * @author shiin
 * @param <K>	key
 * @param <V>	value
 */
@SuppressWarnings("unchecked")
public class SCHashMap<K ,V> implements HashMap<K ,V>{
	private static final int DEFAULT_CAPACITY = 103;
	
	private HashNode<K ,V>[] map; 
	private int size;
	private int currentCapacity;
	
	public SCHashMap() {
		this(DEFAULT_CAPACITY);
	}
	
	public SCHashMap(int capacity) {
		if(capacity < DEFAULT_CAPACITY)
			capacity = DEFAULT_CAPACITY;
		map = new HashNode[capacity];
		size = 0;
		currentCapacity = capacity;
	}

	@Override
	public boolean contains(K key) {
		HashNode<K ,V> node = map[hash(key)];
		while(node != null) {
			if(node.key.equals(key)) {
				return true;
			}
			node = node.next;
		}
		return false;
	}

	@Override
	public V get(K key) {
		HashNode<K ,V> node = map[hash(key)];
		while(node != null) {
			if(node.key.equals(key)) {
				return node.value;
			}
			node = node.next;
		}
		return null;
	}

	@Override
	public int put(K key, V value) {
		int index = hash(key);
		if(map[index] == null) {
			map[index] = new HashNode<K ,V>(key ,value);
		}
		else {
			HashNode<K ,V> node = map[index];
			HashNode<K ,V> prev = null;
			while(node != null) {
				if(node.key.equals(key)) {	
					node.value = value;
					return 1;
				}
				prev = node;
				node = node.next;
			}
			node = new HashNode<K ,V>(key ,value);
			prev.next = node;
		}
		size++;
		return 1;
	}

	@Override
	public int remove(K key) {
		HashNode<K ,V> node = map[hash(key)];
		HashNode<K ,V> prev = null;
		while(node != null) {
			if(node.key.equals(key)) {
				if(prev == null)
					map[hash(key)] = node.next;
				else
					prev.next = node.next;
				node = null;//GC
				size--;
				return 0;
			}
			prev = node;
			node = node.next;
		}
		return 0;
	}

	@Override
	public int size() {
		return this.size;
	}

	@Override
	public void clearAll() {
		map = new HashNode[currentCapacity];
		size = 0;
	}

	@Override
	public boolean isEmpty() {
		return size == 0;
	}
	
	/**
	 * 对key的散列方法
	 * @param key	键值
	 * @return	散列得到的存储位置
	 */
	private int hash(K key) {
		if(key == null)
			return 0;
		int hash = key.hashCode() % currentCapacity;
		if(hash < 0)
			hash += currentCapacity;
		return hash;
	}
	
	private static class HashNode<K ,V>{
		K key;
		V value;
		HashNode<K ,V> next;
		
		HashNode(K key ,V value ,HashNode<K ,V> next){
			this.key = key;
			this.value = value;
			this.next = next;
		}
		
		HashNode(K key,V value){
			this(key ,value ,null);
		}
	}

}

猜你喜欢

转载自blog.csdn.net/my_dearest_/article/details/80041041