一起看看Map容器吧
之前学习Java容器类时做的笔记, 做为从wiznote中转移的第一篇文章, 主要是探讨了一下map容器, 我们如何去写一个hashmap? Java自带的hashmap底层实现是怎么样的呢? 带着这些问题,看看我们下面的内容吧!
写一个自己的简单map
我们来深入看看Map容器,他是如何工作的,有哪些类型的Map容器,我们如何选择自己需要的Map容器。
通常map容器也被叫做映射表,或者是关联数组,因为他是用来存储一组相关联的数据,即一组键值对,在标准的java容器类中包含下面几种类型的MAP实现:
- HashMap
- TreeMap
- LinkedHashMap
- WeakHashMap
ConcurrentHashMap
它们根据一些实际需求,在查找,键值对的保存顺序,是否支持并发而有一些不同的实现。我们今天不对他们的全部进行探讨,选取其中用的较多的hashmap来看一下吧。
step 1
首先我们实现一个最简单的Map容器,他的底层是个数组,不支持扩展大小,查询效率也非常低,并且他不能对键值的唯一性保证。如下:
1 | public class MappingTable<K,V>{ |
这个map实现的比较简单,可以看到上面我们自己实现的容器,无论是存放,还是取值,都要进行一次线性的查找,如果是几十个数据量还好,但是如果一个Map集合中想要存放成千上万个元素呢?难道我们每次都要从头到尾遍历一遍?这效率想想就低的可怕哈,有没有一种可以直接访问的方法,所以java类库的Map实现就引入了HashCode(散列码)的方式来取代对键值的缓慢的线性查找。在根类Object中,有一个hashCode()方法,他是一个native方法,默认是根据对象的某些信息转换得到的,理论上是相对唯一的,所有java对象都可以生成自己的散列码,而HashMap就是通过对象的散列码进行快速查询的。下面我们继续看看散列是啥。
散列和散列码
在看散列前,我们先看一个关于Hashmap的例子。测试Java中的hashmap到底是依靠什么进行工作的,是通过equals()方法保证键值的唯一吗?我们自己编写的类,不覆写hashcode()方法,存放在hash类型的集合中,能保证唯一吗?来看看吧!
先来看一个例子,如果我们编写自己的类作为HashMap的键,不覆写他的hashCode()方法,看看HashMap能否正常工作。
射击运动员(键) — Shooter
他的射击靶数(值)— TargetNum
Shooter类:
1 | package ch17.deepincontainers; |
TargetNum类:
1 | package ch17.deepincontainers; |
TestHashMap类:
1 | package ch17.deepincontainers; |
看看下面的输出结果:
1 | Shooter#8=TargetNum is 0.04 hashcode: 621009875 |
结果显然是不能正常工作,不仅相同的键值被重复添加在map中呢,并且不能正常取值。在看hashmap的源码前,我们在进行下其他的猜想,其一,是不是没有覆写对象的equals方法导致添加了重复的键值,其二,没有覆写对象的hashcode方法导致我们无法取值,既然如此,我们在写两个类测试下。
class ShooterJustOverEquals:
1 | public class ShooterJustOverEquals extends Shooter { |
结果如下
1 | ShooterJustOverEquals:8=TargetNum is 3.04 hashcode: 621009875 |
可以看到,我们覆写了equls方法,但是没有起作用,我可以猜测是不是要结合hashcode方法才能生效呢?我们接下来继续试一把
ShooterOverEqualsAndHashcode:
1 | public class ShooterOverEqAndHashcode extends ShooterJustOverEquals { |
结果:
1 | ShooterOverEqAndHashcode#0=TargetNum is 6.34 hashcode: 0 |
这回我们看到,HashMap正常在工作呢,我们调转到hashmap的源码中去看看两个关键位置的操作
- put({key : value})
- get(key)
发现Hashmap的底层实现是这样的,我们用一张图来说明:
更进一步,如何写个HashMap容器
现在我们知道了散列的原理,那么实现一个简单的HashMap也就没那么困难呢,我们接下来也尝试实现一个简单的SimpleHashMap,在此之前,先和大家来看看一个标准的map容器是一个怎么样的大体结构,我们来仿照这个结构编写我们的hashmap
AbstractMap<K,V>这个抽象基类,这个基类包含了一个抽象方法public abstract Set<Map.Entry<K,V>> entrySet(),这个entrySet是用来存放键值对的集合的,我们实际的键值存放在Entry<K,V>中。这个Entry由我们自己提供,或者使用系统提供给我们的两个默认实现,SimpleEntry和SimpleImmutableEntry。
那么我们按照这个结构,编写一个自己的hash map容器类:
1 | package ch17.deepincontainers; |
我们只覆写了几个常用的方法,其目的是理解散列到底是如何工作的。到此为止,我们应该对Map容器有了进一步的了解,希望大家可以去看看HashMap的源码实现。接下来我们看看如何给自己的类覆写一个好的hashCode()方法,比如像我们上面那样为shoooter类覆写的hashcode不是理想的实现,不好的hash值可能会导致将所有的键值对散列到了集中的几个桶位,这样导致散列的不够平均,查询速度也会慢上很多。