Map及散列HashCode

Map数据结构在平时开发当中经常使用到,其中HashMap因为其查询和插入非常高效快速而更受开发者热捧。

在本文当中我们通过设计HashMap来

  1. 明白HashMap高效查找和插入的原因
  2. 我们自己的类作为HashMap的键需要注意什么?
  3. hashCode及equals方法的作用及应用

接下来我们来分析Java中Map的设计及如果我们使用自己类作为Map的键需要考虑哪些条件呢?

场景一:我们自己实现Map类型:

Map的底层是关联数组,我们可以使用两个ArrayList(一个存储Key,一个存储Value)来设计Map,我们设计MyMap继承自AbstractMap,重写put,get,entrySet等常用方法。

如果我们自己实现Map类型,就需要同时定义Map.Entry接口的实现,这样才能实现Map接口的entrySet方法,返回Map结构的Entry节点。
我们自己实现Map类型代码如下:

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

public class MyMap<K, V> extends AbstractMap<K, V>{
	private List<K> keyList = new ArrayList<K>();
	private List<V> valueList = new ArrayList<V>();
	

public V put(K key,V value){
	V oldval = get(key);//获取原先的值
	if(oldval != null){//原先的key在map存在就更新
		valueList.set(keyList.indexOf(key), value);
	}else{
		//原先的key在map不存在就插入
		keyList.add(key);
		valueList.add(value);
	}
	
	return oldval;
}

public V get(Object key){
	if(!keyList.contains(key)){
		return null;
	}
	return valueList.get(keyList.indexOf(key));
}

@Override
public Set<Map.Entry<K, V>> entrySet() {
	Set<Map.Entry<K, V>> set = new HashSet<Map.Entry<K, V>>();
	for (int i = 0;i < keyList.size();i++) {
		set.add(new MapEntry<K, V>(keyList.get(i),valueList.get(i)));
	}
	return set;
}

public static void main(String[] args) {
	MyMap<String,Integer> map = new MyMap<String,Integer>();
	map.put("Huawei", 1000);
	map.put("Xiaomi", 400);
	map.put("OPPO", 500);
	map.put("VIVO", 500);
	map.put("Nokia", 100);
	map.put("APPLE", 1000);
	
	System.out.println(map.keySet());
	System.out.println(map.values());
	System.out.println(map.entrySet());
	System.out.println(map.get("APPLE"));
	
	/**
	 * [OPPO, APPLE, Nokia, Huawei, Xiaomi, VIVO]
		[1000, 1000, 500, 500, 100, 400]
		[Huawei = 1000, OPPO = 500, Xiaomi = 400, APPLE = 1000, VIVO = 500, Nokia = 100]
		1000
	 * */
	}
}
//实现Map.Entry<K, V>接口产生自己的MapEntry<K, V>
class MapEntry<K, V> implements Map.Entry<K, V>{
	private K k;
	private V v;
public MapEntry(K k, V v) {
	this.k = k;
	this.v = v;
}

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

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

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

public boolean equals(Object e1){
	if(!(e1 instanceof MapEntry)){
		return false;
	}
	MapEntry<K, V> me = (MapEntry<K, V>)e1;
	return (me.getKey() == null ? this.getKey() == null : me.getKey().equals(this.getKey()))
			&& (me.getValue() == null ? this.getValue() == null : me.getValue().equals(this.getValue()));
}

public int  hashCode(){
	return(k==null ? 0 : k.hashCode()) ^ (v==null ? 0 : v.hashCode());
}
public String toString(){
	return k + " = "+ v;
}
}

那么虽然可以正常存储数据,但是数组的查询都是线性查找,效率非常低。那么HashMap的高效查询原因在于哪里?

场景二:我们自己的类要作为HashMap的键

接下来我们再看一个场景,假设我们有自己的类要作为HashMap的键,那需要注意什么?

import java.util.HashMap;

public class Mobile {
	private String band;
	private int price;
public Mobile(String band,int price){
	this.band = band;
	this.price=price;
}
public String getBand() {
	return band;
}
public int getPrice() {
	return price;
}
public String toString(){
	return band + " the price is " +price;
}

public static void main(String[] args) {
	HashMap<Mobile, Integer> mobiles = new HashMap<>();
	mobiles.put(new Mobile("Huawei",1000), 10);
	mobiles.put(new Mobile("OPPO",600), 40);
	mobiles.put(new Mobile("VIVO",800), 30);
	mobiles.put(new Mobile("Xiaomi",700), 20);
	mobiles.put(new Mobile("Huawei",1000), 20);//再次插入同样的对象但是还可以插入
	
	System.out.println(mobiles);
	System.out.println(mobiles.get(new Mobile("Huawei",1000)));//找不到插入的对象
	
	/*
	 * {VIVO the price is 800=30, Huawei the price is 1000=20, Huawei the price is 1000=10, OPPO the price is 600=40, Xiaomi the price is 700=20}
		null
	 * */
}
}

可以看出我们自己创建的类作为HashMap的键的时候,出现了问题,插入的数据找不到了。问题的关键在于我们没有实现类的hashcode及equals方法

hashCode与equals

HashMap其高效的查询是因为其底层的散列算法hashCode来快速定位数据,散列算法在其他数据结构如:LinkedHashMap,HashSet,LinkedHashSet也有使用

散列算法的查询过程
  1. 存储一组元素最快的数据结构是数组,使用一个数组保存键的信息,而不是键的本身,具体是将键的散列值(实现hashCode方法)作为数组的下标
  2. 虽然数组是固定存储大小的,但是我们允许不同的键通过散列产生相同的下标,这会产生冲突,而数组里面存储的是链表对象来解决冲突,可以存储具体的值;
  3. 综上,查询的过程是先将对键计算散列码,根据散列码跳到数组对应的下标,根据数组下标找到对应的链表list再遍历list(实现equals方法)找到相应的值

这就是HashMap如此高效查询的原因

设计hashCode方法的注意点:
  1. 设计hashCode的关键点在于无论何时,对同一个对象调用hashCode应该产生同样的值,不能在put的时候产生一个值,在get()的时候产生另外一个值,所以我们不能使用对象中容易改变的值来hashCode
  2. hashCode()不应该使用对象具有唯一性的对象信息,尤其是使用this的值(对象的在堆中的内存地址),这样无法生成一个新的键,使之与put()原始的键值对应的键相同,应该使用对象具有识别意义的信息
  3. 一个好的hashCode算法能够产生均匀的散列码,将键信息均匀地分布在不同的数组下标里面,这样防止数组里某个下标的链表存在过多的元素导致使用equals遍历时间过长

在《Effective Java》对于好的hashCode有基本的指导意见,如下:
在这里插入图片描述
实用的hashCode()生成速度快并且有意义,这样可以快速定位对象,equals()则对比对象的基本特征是否一样,通过hashCode()及equals()可以完全确定对象的身份。

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

下面我们改进上面的Map类型及Mobile2

import java.util.HashMap;

public class Mobile2 {
	private String band;
	private int price;
	
	public Mobile2(String band,int price){
		this.band = band;
		this.price=price;
	}
	public String getBand() {
		return band;
	}
	public int getPrice() {
		return price;
	}
	public String toString(){
		return band + " the price is " +price;
	}
	
	//重写Object继承而来的hashCode方法,默认的Object的hashCode根据对象的内存地址来计算,实用的hashCode根据对象的内容来生成
	public int hashCode(){
		int result = 17;
		result = 37 * result + band.hashCode();
		result = result + price;
		return result;
	}
	
	//重写Object继承而来的equals方法,默认的Object的equals比较对象的内存地址,实用的equals比较对象的关键属性
	/*Object的equals方法:
	 *public boolean equals(Object obj) {
        return (this == obj);
    }* */
	public boolean equals(Object obj){
		if(!(obj instanceof Mobile2))
			return false;
		
		Mobile2 m2 = (Mobile2)obj;
		return (m2.getBand() == null ? this.band == null : m2.getBand().equals(this.band))
				&& (m2.getPrice() == 0 ? this.price == 0 : m2.getPrice() == this.price);
	}
	
	public static void main(String[] args) {
		HashMap<Mobile2, Integer> mobiles = new HashMap<>();
		mobiles.put(new Mobile2("Huawei",1000), 10);
		mobiles.put(new Mobile2("OPPO",600), 40);
		mobiles.put(new Mobile2("VIVO",800), 30);
		mobiles.put(new Mobile2("Xiaomi",700), 20);
		mobiles.put(new Mobile2("Huawei",1000), 20);//再次插入同样的对象但是还可以插入
		
		System.out.println(mobiles);
		System.out.println(mobiles.get(new Mobile2("Huawei",1000)));//找不到插入的对象
		
		/*{Xiaomi the price is 700=20, OPPO the price is 600=40, VIVO the price is 800=30, Huawei the price is 1000=20}
			20
		 * */
	}
}

改进的HashMap代码如下:

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

public class MyHashMap<K, V> extends AbstractMap<K, V>{
	static final int SIZE = 997;
	
	//创建存放List的数组
	private LinkedList<MapEntry<K, V>> [] buckets = new LinkedList [997];
	
	public V put(K key,V value){
		V oldVal = null ;
		
		//对键进行hashCode计算
		int index = Math.abs(key.hashCode())  % SIZE;
		if(buckets[index] == null){
			buckets[index] = new LinkedList<MapEntry<K, V>>();
		}
		
		//查看key是否已经存在,如果存在则更新里面的value
		boolean isFound = false;
		LinkedList<MapEntry<K, V>> bucket = buckets[index];
		for (MapEntry<K, V> mapEntry : bucket) {
			if(mapEntry.getKey().equals(key)){
				isFound = true;
				oldVal = mapEntry.getValue();
				mapEntry.setValue(value);
				break;
			}
		}
		
		//不存在则把key和value存放到桶里面
		if(!isFound){
			MapEntry<K, V> entry = new MapEntry<K, V>(key, value);
			bucket.add(entry);
		}
		
		return oldVal;
	}
	
	public V get(Object key){
		V val = null;
		//对键进行hashCode计算
		int index = Math.abs(key.hashCode()) % SIZE;
		
		LinkedList<MapEntry<K, V>> bucket = buckets[index];
		if(bucket != null){
			for (MapEntry<K, V> mapEntry : bucket) {
				if(mapEntry.getKey().equals(key)){
					val = mapEntry.getValue();
					break;
				}
			}
		}
		
		return val;
	}
	
	@Override
	public  Set<Map.Entry<K, V>> entrySet() {
		Set<Map.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> mapEntry : bucket) {
				set.add(mapEntry);
			}
		}
		
		return set;
	}
	
	public static void main(String[] args) {
		MyHashMap<Mobile2, Integer> mobileMap = new MyHashMap<Mobile2, Integer>();
		mobileMap.put(new Mobile2("Huawei",1000), 10);
		mobileMap.put(new Mobile2("OPPO",600), 40);
		mobileMap.put(new Mobile2("VIVO",800), 30);
		mobileMap.put(new Mobile2("Xiaomi",700), 20);
		mobileMap.put(new Mobile2("Huawei",1000), 20);//更新之前插入的对象
		
		System.out.println(mobileMap.entrySet());
		System.out.println(mobileMap.get(new Mobile2("Huawei",1000)));//查找插入的对象
		
		/**[OPPO the price is 600 = 40, Xiaomi the price is 700 = 20, VIVO the price is 800 = 30, Huawei the price is 1000 = 20]
			20
		 * */
	}
}

综上所示,我们通过自己设计自己的HashMap了解了:

1.HashMap高效查找的原因及查找过程

2.明白了HashCode及equals方法的作用

3.我们用自己的类作为HashMap的键时需要注意重写hashCode及equals方法

以上部分内容来自《Java编程思想》

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值