一、简介
除了List,平时在项目中用的比较多的还有Map,Map是存放键值对的数据结构,键不允许重复,如果键重复会覆盖之前的值。比较常用到的有: HashMap和HashTable。本文将会模仿Map的源码实现两个自定义的Map(一个是没有用hashcode的效率低的版本,一个是用hashcode优化版),只是帮助理解Map底层实现原理,实际项目中没有必要去自定义Map。
二、实现原理
Map是存放键值对的数据结构,键不允许重复,如果新增的时候已经存放相同键的元素,那么会覆盖之前相同键的值。Map底层是通过操作Entry[]数组来实现的,Entry里面主要就是存放我们的键值对。
三、自定义Map(效率低,没有使用hashcode)
Entry类:存放键值对的对象
public class CustomEntry {
/**
* 键
*/
private Object key;
/**
* 值
*/
private Object value;
public CustomEntry() {
}
public CustomEntry(Object key, Object value) {
this.key = key;
this.value = value;
}
public Object getKey() {
return key;
}
public void setKey(Object key) {
this.key = key;
}
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
package wsh.version1;
/**
* 自定义Map(版本一,效率较低,需要循环遍历找相同的key)
*
* @author weishihuai
* @date 2018/9/27 21:25
* 说明:
* 1. Map是存放键值对(key-value)的数据结构
* 2. Map键值不允许重复,如果重复会覆盖之前的值
* 3. 根据键值可以找到对应的值
*/
public class CustomMap {
private CustomEntry[] array = new CustomEntry[1000];
private int size;
/**
* 往Map中新增元素
* 原理: 新建Entry对象,往数组中添加元素即可
*
* @param key 键
* @param value 值
*/
public void put(Object key, Object value) {
CustomEntry customEntry = new CustomEntry(key, value);
//判断是否存在相同的键,如果有相同的直接覆盖之前的值,size不用改变。
for (int i = 0; i < size; i++) {
if (array[i].getKey().equals(key)) {
array[i].setValue(value);
return;
}
}
array[size] = customEntry;
size++;
}
/**
* 根据键找到相应的值
* 原理: 循环遍历entry数组中的key集合,找到返回键对应的值,未找到返回null
*
* @param key 键
* @return 键对应的值
*/
public Object get(Object key) {
for (int i = 0; i < size; i++) {
if (array[i].getKey().equals(key)) {
return array[i].getValue();
}
}
return null;
}
/**
* 判断是否存放指定key的元素
* 原理: 循环遍历所有的key集合,找到返回true,找不到返回false
*
* @param key 键
* @return 是否包含对应的键的元素
*/
public boolean containsKey(Object key) {
for (int i = 0; i < size; i++) {
if (array[i].getKey().equals(key)) {
return true;
}
}
return false;
}
/**
* 判断是否包含指定值的元素
* 原理: 循环遍历所有的value集合,找到返回true,找不到返回false
*
* @param value 值
* @return 是否包含指定值的元素
*/
public boolean containsValue(Object value) {
for (int i = 0; i < size; i++) {
if (array[i].getValue().equals(value)) {
return true;
}
}
return false;
}
/**
* 返回Map中元素的个数
*
* @return Map的元素个数
*/
public int size() {
return size;
}
/**
* 判断Map是否为空
*
* @return map是否为空
*/
public boolean isEmpty() {
return size == 0;
}
}
四、部分方法详解
【a】Entry类 主要属性就是键值对
public class CustomEntry {
/**
* 键
*/
private Object key;
/**
* 值
*/
private Object value;
}
【b】 put(Object key, Object value) {} 往Map插入新的元素
1. 实现原理: 通过传递的参数新建Entry对象,往数组中添加元素即可。
2. 注意需要判断是否存在相同的键,如果有相同的直接覆盖之前的值,size不用改变。
//判断是否存在相同的键,如果有相同的直接覆盖之前的值,size不用改变。
for (int i = 0; i < size; i++) {
if (array[i].getKey().equals(key)) {
array[i].setValue(value);
return;
}
}
这里是直接遍历整个数组查找是否有相同key的元素。
【c】get(Object key) {} 根据键取出对应的值
1. 实现原理: 循环遍历Entry数组中的key集合,找到返回键对应的值,未找到返回null。
2. 注意查找是否有相同key采用的是equals方法。
【d】containsKey(Object key) {} 判断Map是否存放某个键
1. 实现原理: 循环遍历所有的key集合,找到返回true,找不到返回false。
【e】containsValue(Object value) {} 判断是否包含指定值的元素
1. 实现原理: 循环遍历所有的value集合,找到返回true,找不到返回false。
【f】isEmpty(){} 判断Map是否为空
1. 实现原理: return size == 0;
通过上面的代码可以看到,都是循环整个数组去遍历,如果数组过大,显然效率并不是很好,那么通过查看HashMap的源码可以看到,有一种优化方案,就是使用hashcode和equals()方法来优化,这样就不用整个数组遍历。
测试:
package wsh.version1;
/**
* 测试自定义Map
*
* @author weishihuai
* @date 2018/9/27 21:30
*/
public class TestCustomMap {
public static void main(String[] args) {
CustomMap customMap = new CustomMap();
/***********************************put(key,value)*************************************/
customMap.put("aaa", "zhangsan");
customMap.put("bbb", "lisi1");
customMap.put("bbb", "lisi2");
customMap.put("ccc", "wangwu");
/*******************************get(key)***********************************************/
//zhangsan
System.out.println(customMap.get("aaa"));
/************************************size()********************************************/
//3
System.out.println(customMap.size());
/**********************************get(key)********************************************/
//lisi2
System.out.println(customMap.get("bbb"));
/*************************************containsKey(key)*********************************/
//true
System.out.println(customMap.containsKey("aaa"));
//false
System.out.println(customMap.containsKey("123"));
/************************************containsValue(value)******************************/
//true
System.out.println(customMap.containsValue("zhangsan"));
//false
System.out.println(customMap.containsValue("123"));
/************************************isEmpty()*****************************************/
//false
System.out.println(customMap.isEmpty());
}
}
五、HashMap简介
HashMap也是存放键值对的结构,HashMap底层是通过数组+链表的方式实现的,通过键的hashcode确定在数组的某个位置,如果多个hashcode相同,那么这些都会存放到数组里面对应的一条链表上,链表里面存放是我们的Entry对象,即键值对。画了个简图:
六、自定义HashMap(效率高,使用hashcode)
package wsh.version2;
import java.util.LinkedList;
/**
* 自定义Map(通过hashcode/equals方法实现,效率高)
*
* @author weishihuai
* @date 2018/10/1 16:05
* <p>
* 说明:
* Map底层结构是通过(数组 + 链表)实现,只是帮助理解一下Map大概实现原理,没必要自定义Map
*/
public class CustomMap {
private LinkedList[] array = new LinkedList[1000];
private int size;
/**
* 往Map中插入键值对元素
*
* @param key 键
* @param value 值
*/
public void put(Object key, Object value) {
CustomEntry customEntry = new CustomEntry(key, value);
int hashCode = key.hashCode();
//根据hashCode计算出在bucket[]数组中的位置
int index = hashCode % 1000;
if (null == array[index]) {
//空链表
LinkedList linkedList = new LinkedList();
linkedList.add(customEntry);
array[index] = linkedList;
} else {
//不是空链表,拿出链表进行遍历,如果键值相同,值会被覆盖
LinkedList list = array[index];
for (int i = 0; i < list.size(); i++) {
CustomEntry customEntry2 = (CustomEntry) list.get(i);
if (customEntry2.getKey().equals(key)) {
//键值重复直接覆盖
customEntry2.setValue(value);
return;
}
}
array[index].add(customEntry);
}
size++;
}
/**
* 根据key找到对应的值
*
* @param key 键
* @return 值
* <p>
* 原理: 根据key的hashCode计算出位置,如果该位置对应的链表不为空,循环遍历链表,找到对应的key,返回对应的值。如果未找到则返回null
*/
public Object get(Object key) {
int index = key.hashCode() % 1000;
if (null != array[index]) {
LinkedList list = array[index];
for (int i = 0; i < list.size(); i++) {
CustomEntry customEntry = (CustomEntry) list.get(i);
if (customEntry.getKey().equals(key)) {
return customEntry.getValue();
}
}
}
return null;
}
public int size() {
return size;
}
}
七、部分方法详解
【a】 put(Object key, Object value) {} 往HashMap中插入新的键值对
1. 实现原理:通过 key.hashCode()计算出hashcode, 然后根据某种规则算出一个索引值,这个索引对应在bucket[]数组汇总的位置
a. 如果索引对应元素是一个空链表,那么新建一个LinkedList,把键值对存放到链表中。
b. 如果索引对应元素不是一个空链表,那么直接通过数组【index】取出链表对象,遍历链表,如果键值相同,值会被覆盖,并将键值对加入到已有的链表中。
【b】get(Object key) {} 根据key找到对应的值
1. 实现原理: 根据key的hashCode计算出位置,如果该位置对应的链表不为空,循环遍历链表,找到对应的key,返回对应的值。如果未找到则返回null。
八、测试
package wsh.version2;
/**
* 测试自定义Map
*
* @author weishihuai
* @date 2018/10/01 16:30
*/
public class TestCustomMap {
public static void main(String[] args) {
CustomMap customMap = new CustomMap();
customMap.put("name", "zhangsan");
customMap.put("name", "lisi");
//lisi
System.out.println(customMap.get("name"));
//1
System.out.println(customMap.size());
}
}
九、总结
以上通过两个示例以及一些常用方法的讲解,相信可以帮助大家理解一下HashMap底层大概实现的原理,这里只实现了部分方法,有些方法有些问题暂时没有考虑。本文是作者在学习HashMap底层实现原理的一些总结以及方法讲解,仅供大家学习参考,一起学习一起进步。