HashMap的内部实现-容器深入研究

HashMap键值对早已烂熟于心,但突然被问到HashMap内部是怎么实现的,我能说我不知道吗。。。

想想自己写一个的话也是可以实现的。

定义两个list分别存放key和value,然后实现一下put,get,set等方法即可。

import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

//HashMap也是继承的这个类AbstractMap
public class SlowMap<K, V> extends AbstractMap<K, V> {

	private List<K> keys = new ArrayList<K>();
	private List<V> values = new ArrayList<V>();
	
	public V put(K key, V value) {
		V oldValue = get(key);
		if(keys.contains(key)) {
			values.set(keys.indexOf(key), value);
		} else {
			keys.add(key);
			values.add(value);
		}
		return oldValue;
	}
	
	//public V get(K key)//属于覆盖 要和AbstractMap中get保持一致
	public V get(Object key) {
		if(keys.contains(key)) {
			//根据下标去values中去对应的value
			return values.get(keys.indexOf(key));
		}
		return null;
	}
	
	//继承了就必须要实现的方法
	@Override
	public Set<java.util.Map.Entry<K, V>> entrySet() {
		Set<java.util.Map.Entry<K, V>> set = new HashSet<java.util.Map.Entry<K, V>>();
		Iterator<K> ki = keys.iterator();
		Iterator<V> vi = values.iterator();
		
		while(ki.hasNext()) {
			//这里需要Map.Entry<K,V> 但是这个东西是个接口
			//所以需要自己实现一个
			set.add(new MapEntry<K, V>(ki.next(), vi.next()));
		}
		return set;
	}
	
	public static void main(String[] args) {
		SlowMap<String, String> map = new SlowMap<String, String>();
		map.put("1", "test1");
		map.put("2", "test2");
		map.put("3", "test3");
		map.put("4", "test4");
		map.put("5", "test5");
		
		System.out.println(map.get("4"));
		
		//在没有实现entrySet的时候会报错java.lang.NullPointerException
		//应该是toString里面调用了这个方法,然后再处理了一下格式
		System.out.println(map);
		
		//这个打印的格式是MapEntry里面的
		System.out.println(map.entrySet());
	}
}

HashMap本身就是继承AbstractMap的,所以这里也实现为继承AbstractMap。但是继承这个的话就必须实现entrySet方法,这个方法是返回HashMap的Set集下面会细说。

先说一个重要的put和get方法

put中先查看keys中是否包含该key,如果有就替换它的value,没有就添加 并且将value添加到values中

get中看keys中是否包含该key,如果有根据key的下标去values中找对应的value,没有则返回null(HashMap中没有也是返回null)

注:keys和values之间是按插入顺序联系起来的,下标相同的就是对应的 “键值对”


put和get实现之后就可以测试了,其他的都很正常。但是直接打印map(自定义HashMap的实例),报了空指针,检查之后发现是因为没有实现entrySet这个方法,貌似toString中有调用这个方法,所有才出现了错误。

看方法中需要返回Set<Map.Entry>,但是这个MapEntry是个接口,所以我们需要自己实现一下,就像下面这样

import java.util.Map;

public class MapEntry<K, V> implements Map.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() {
		// TODO Auto-generated method stub
		return key;
	}

	@Override
	public V getValue() {
		// TODO Auto-generated method stub
		return value;
	}

	@Override
	public V setValue(V arg0) {
		// TODO Auto-generated method stub
		value = arg0;
		return value;
	}
	
	public int hashCode() {
		return (key == null ? 0 : key.hashCode())^
				(value == null ? 0 : value.hashCode());
	}
	public boolean equals(Object o) {
		if(!(o instanceof MapEntry)) {
			return false;
		}
		MapEntry entry = (MapEntry) o;
		
		//想看懂这段需要点基本功啊...
		//保证key和value与entry的key vaule都相等才行
		return (key == null ?
				entry.getKey() == null : key.equals(entry.getKey())) &&
			   (value == null ?
			    entry.getValue() == null : value.equals(entry.getValue()));
//		if(key == entry.getKey() && value == entry.getValue())
//			return true;
//		if(key == null || value == null || entry.getKey() == null || entry.getValue() == null)
//			return false;
//		return false;
	}
	public String toString() {
		return key + " = " + value;
	}
}

这里主要的就是实现hashCode和equals这两个方法,HashMap就是依靠他们来定位键值对的,所以很重要。

hashcode中用到了冒号运算符,如果相等就取冒号前面的值反之则去后面的。这个以还比较简单,看一下equals就不是那么简单了。。。

按照&&符号拆成两个,第一个如果key等于null,值就由entry.getKey()是否等于null决定,否则就有key是否等于entry.getKey()决定。

第二个和第一个类似,然后两个同时为true是才返回true。

也就是说必须满足两个key相同,两个value相同才行,都为null也可以


输出结果

test4
{1=test1, 5=test5, 4=test4, 3=test3, 2=test2}
[1 = test1, 5 = test5, 4 = test4, 3 = test3, 2 = test2]

这样就算完成一个HashMap了,但是这样的方式并不快,HashMap是以快著称的。这里的问题在于键的查询,这里对键的保存没有什么特定的顺序,所以只能线性查询(最慢的查询方式)。

HashMap用的是散列,它使得查询得以快速进行。散列将键存储在某处,以便能够快速找到。存储一组元素最快的数组结构是数组,所以可以使用它来保存键的信息。因为数组必须确定大小,所以这里不能保存键本身,键的数量是不确定的。这里是通过键生成一个数字(不超过数组大小)作为数组下标,这个数字就是散列码。就是hashCode()需要干的活。

也就是说现在直接通过hashCode()(下标)去找的话,可以找到很多个键,那怎么确定呢。这里的设计是,数组中存放的是键的list,确定list之后再通过键进行线性查找。(类似分块处理)

修改后的代码

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

public class SimpleHashMap<K, V> extends AbstractMap<K, V> {

	private final static int SIZE = 997;
	@SuppressWarnings("unchecked")
	LinkedList<MapEntry<K, V>>[] buckets = new LinkedList[SIZE];
	
	
	public V put(K key, V value) {
		V oldValue = get(key);
		//计算下标,保证不超过数组的大小
		int index = Math.abs(key.hashCode() % SIZE);
		
		if(buckets[index] == null) {
			//数组中每个元素都是list
			buckets[index] = new LinkedList<MapEntry<K, V>>();
		}
		
		LinkedList<MapEntry<K, V>> bucket = buckets[index];
		MapEntry<K, V> pair = new MapEntry<K, V>(key, value);
		boolean found = false;
		ListIterator<MapEntry<K, V>> iterator = bucket.listIterator();
		while (iterator.hasNext()) {
			MapEntry<K, V> type = iterator.next();
			if(type.getKey().equals(key)){ 
				oldValue = type.getValue();
				iterator.set(pair);
				found = true;
				break;
			}
		}
		if(!found) {
			buckets[index].add(pair);
		}
		return oldValue;
	}
	
	//public V get(K key)//属于覆盖 要和AbstractMap中get保持一致
	public V get(Object key) {
		int index = Math.abs(key.hashCode() % SIZE);
		if(buckets[index] == null) {
			return null;
		}
		for (MapEntry<K, V> pair : buckets[index]) {
			if(pair.getKey().equals(key)) {
				return pair.getValue();
			}
		}
		return null;
	}
	
	@Override
	public Set<java.util.Map.Entry<K, V>> entrySet() {
		Set<java.util.Map.Entry<K, V>> set = new HashSet<java.util.Map.Entry<K, V>>();
		for(LinkedList<MapEntry<K, V>> bucket : buckets) {
			if(bucket == null) {
				continue;
			} else {
				for(MapEntry<K, V> pair : bucket) {
					set.add(pair);
				}
			}
		}
		return set;
	}
	
	public static void main(String[] args) {
		SimpleHashMap<String, String> map = new SimpleHashMap<String, String>();
		map.put("1", "test1");
		map.put("2", "test2");
		map.put("3", "test3");
		map.put("4", "test4");
		map.put("5", "test5");
		
		System.out.println(map.get("4"));
		
		//在没有实现entrySet的时候会报错java.lang.NullPointerException
		//应该是toString里面调用了这个方法,然后再处理了一下格式
		System.out.println(map);
		
		//这个打印的格式是MapEntry里面的
		System.out.println(map.entrySet());
	}

}
put和get方法都变成了先通过下标查找对应的list,然后再从list中查看或添加对应的键值对。entrySet则是遍历数组以及list得到所有的键值对


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值