从源码角度简析 Hashtable、HashMap 和 LinkedHashMap

注意:此文原文均摘自 Sun jdk

Hashtable 与 HashMap

不同点

先看类的定义——

这里写图片描述

这里写图片描述

除了接口的实现是相同的,我们可以看到继承的类是不同的,我们不妨打开 Dictionary 抽象类看一下

这里写图片描述

我们可以看到红色箭头指向的地方,大致翻译一下就是 —— 注意:这个类已经过时了,新的实现应该去实现 Map 接口,而不是继承这个类。所以事实上继承或不继承这个类并没有多大影响,Hashtable 实现了 Map 接口 ——

这里写图片描述

这里写图片描述

我们可以看到,Dictionary 类中的方法在 Map 接口中是有相同意义的方法的。接下来就是 HashMap 的父类 AbstractMap 抽象类 ——

这里写图片描述

翻译过来就是:

此类提供 Map 接口的骨干实现,以最大限度地减少实现此接口所需的工作。

要实现不可修改的映射,编程人员只需扩展此类并提供 `entrySet` 方法的实现即可,该方法将返回映射的映射关系 set 视图。通常,返回的 set 将依次在 `AbstractSet` 上实现。此 set 不支持 `add` 或 `remove` 方法,其迭代器也不支持 `remove` 方法。

要实现可修改的映射,编程人员必须另外重写此类的 `put` 方法(否则将抛出 `UnsupportedOperationException`),`entrySet().iterator()` 返回的迭代器也必须另外实现其 `remove` 方法。

按照 `Map` 接口规范中的建议,编程人员通常应该提供一个 void(无参数)构造方法和 map 构造方法。

此类中每个非抽象方法的文档详细描述了其实现。如果要实现的映射允许更有效的实现,则可以重写所有这些方法。

此类是 Java Collections Framework 的成员。

所以 AbstractMap 就是实现了一些 Map 接口的方法,方便子类复用。

所以说,HashtableHashMap 在类结构上是基本没有任何差异的,那么具体的实现呢?
其一:Hashtable 的键值都不可为空,而 HashMap 键值对皆可为空 ——

这里写图片描述

这里写图片描述

其二:Hashtable 相比于 HashMap 线程更安全,因为它所有的方法都添加了 synchronized 关键字(这里笔者想提到一点就是,线程安全并不意味着在高并发的情况下就能够得到正确的结果,毕竟它只是能保证任一时刻只有一个线程访问而不是保证线程访问的顺序)。这里笔者就不截图了,大家可以戳一下 Hashtable 源码查看一下。

其三:初始容量不同,假设我们并未在初始化 HashMapHashtable 指定自定义容量,那么它们的初始化容量是多少呢?

这里写图片描述

这里写图片描述

Hashtable 初始化容量为11,而 HashMap 初始化容量为16(虽然 HashMap 无参构造函数中并未明显显示出来,但是注释中已经透露)。

其四:扩容机制不同——这里所指的扩容机制不同是指其扩容后的大小与原大小的比例不同,但是它们的触发条件都是一样的,当当前元素个数超过原大小的0.75倍时,将会扩容当前数组大小,源码笔者在这里就不展示了,我们可以通过以下 demo 来看到扩容效果 ——

HashMap 部分:

public static void main(String[] args) {
    Map<String, String> hashMap = new HashMap<>();
    Class<? extends Map> hashMapClass = hashMap.getClass();
    try {
        Field count = hashMapClass.getDeclaredField("table");
        count.setAccessible(true);
        hashMap.put("1", "1");
        hashMap.put("2", "1");
        hashMap.put("3", "1");
        hashMap.put("4", "1");
        hashMap.put("5", "1");
        hashMap.put("6", "1");
        hashMap.put("7", "1");
        hashMap.put("8", "1");
        hashMap.put("9", "1");
        hashMap.put("10", "1");
        hashMap.put("11", "1");
        hashMap.put("12", "1");
        System.out.println(((Object[]) count.get(hashMap)).length);
        hashMap.put("13", "1");
        System.out.println(((Object[]) count.get(hashMap)).length);
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
}

输出:
>> 16
>> 32

Hashtable 部分:

public static void main(String[] args) {
    Map<String, String> hashTable = new Hashtable<>();
    Class<? extends Map> hashTableClass = hashTable.getClass();
    try {
        Field count = hashTableClass.getDeclaredField("table");
        count.setAccessible(true);
        hashTable.put("1", "1");
        hashTable.put("2", "1");
        hashTable.put("3", "1");
        hashTable.put("4", "1");
        hashTable.put("5", "1");
        hashTable.put("6", "1");
        hashTable.put("7", "1");
        hashTable.put("8", "1");
        System.out.println(((Object[]) count.get(hashTable)).length);
        hashTable.put("9", "1");
        System.out.println(((Object[]) count.get(hashTable)).length);
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
}

输出:
>> 11
>> 23

其五:索引算法不同。我们知道,对于基于 Hash 算法的数据结构,索引算法是一道关键点,采用好的索引算法不仅能够快速计算出索引位置,而且能够避免 Hash 冲突——

HashMap 部分首先是将 key 的 hash 值进行再 hash ——

int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

接着,其索引算法基于上述 hash 值 index = (n - 1) & hash

Hashtabl 部分是 (key.hashCode & 0x7FFFFFFF) % tab.length

相同点

数据结构

解决完不同点,我们来看看相同点,共同实现的几个接口就没有什么好介绍的了,我们来看看它们的数据结构,HashMapHashtable 都是基于哈希表实现的,那么什么是哈希表?

这里写图片描述
这里写图片描述

这里写图片描述
这里写图片描述

打开 HashMapHashtable 类我们都能看到一个数组,这两个数组的实质是一样的,画一张图会更清晰 ——

这里写图片描述

HashMapHashtable 就是基于这样的数组实现的。数组中的每个值实际上都是一个单链表,这个链表中的每个元素的 hash 值是相同的!hash 值也就对应数组中的索引!新进入的结点会放在表头的位置!

这样说可能还有点抽象,我们来举个例子,假如我们 HashMapHashtable 的 hash 值计算方法就是元素的 key 对数组长度求余(事实上肯定不是这样的),即 hash == key % arrays.length(),数组初始长度为5,现在我们需要插入5个键值对,代码如下:

Map<Integer, String> hashtable = new Hashtable<Integer, String>();
hashtable.put(1, "张");
hashtable.put(2, "李");
hashtable.put(3, "王");
hashtable.put(4, "刘");
hashtable.put(5, "赵");

那么 hashtable 会怎么做呢?我们不妨查看一下 Hashtableput() 方法 ——

这里写图片描述
这里写图片描述
这里写图片描述

put() 方法有两个英文注释已经解释得很清楚了,第一步是确保 value 不为空,否则抛出空指针异常;第二步是假设这个 key 已经存在 hashtable 中了,那么就替换原来的 value;第三步就是 value 不为空,并且是一个新的 key,那么就添加到 hashtable 中我们可以看到 // Creates the new entry 下面的那四行代码,可以看出来,先将数组索引位置的 entry 赋给了一个新的值,然后又创建一个指向该值的新的 entry,故新值就是新的表头了。

对于键值对 (1, "张)",我们求出该键值对的 hash 值等于 1 % 5 等于4,同理 (2, "李")(3, "王")(4, "刘")(5, "赵)" 的 hash 值分别是3,2,1,0,它们经过的都是上述的第三步,最后得到的 hash 表如下 ——

这里写图片描述

那现在加入我们再插入一个 (6, "周") 的键值对会怎样呢?老规矩——先计算 hash 值,发现 hash 值为1,于是对数组索引为1的那个单链表进行遍历,但是发现没有 key 相等的键值对,于是也是进入第三步,插在了包含键值对为 (4, "刘") 的那条单链表的头部,如下图 ——

这里写图片描述

HashMapHashtableput() 方法同理,此处就不再扩展了。

索引算法

此处不做扩展,可见hashmap和hash算法研究

LinkedHashMap

首先查看类的定义 ——

这里写图片描述

LinkedHashMap 是继承自 HashMap 的。我们再看看官方文档的描述 ——

这里写图片描述
这里写图片描述

注意:以上截图只是节选部分重要文档。

大致翻译如下:

`Map` 接口的哈希表和链接列表实现,具有可预知的迭代顺序。此实现与 `HashMap` 的不同之处在于,后者维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序通常就是将键插入到映射中的顺序(插入顺序)。注意,如果在映射中重新插入 键,则插入顺序不受影响。(如果在调用 `m.put(k, v)` 前 `m.containsKey(k)` 返回了 true,则调用时会将键 k 重新插入到映射 m 中。)
提供特殊的构造方法来创建链接哈希映射,该哈希映射的迭代顺序就是最后访问其条目的顺序,从近期访问最少到近期访问最多的顺序(访问顺序)。这种映射很适合构建 LRU 缓存。调用 `put` 或 `get` 方法将会访问相应的条目(假定调用完成后它还存在)。`putAll` 方法以指定映射的条目集迭代器提供的键-值映射关系的顺序,为指定映射的每个映射关系生成一个条目访问。任何其他方法均不生成条目访问。特别是,collection 视图上的操作不影响底层映射的迭代顺序。
可以重写 `removeEldestEntry(Map.Entry)` 方法来实施策略,以便在将新映射关系添加到映射时自动移除旧的映射关系。

从以上文字我们节选出以下几条信息:

  • LinkedHashMap 内部维护的是双链表,且此链表定义了插入的顺序
  • LinkedHashMap 通过特殊的构造方法,可以将输出的值从按照插入的顺序改成符合 LRU 的顺序

LinkedHashMap 内部维护的是双链表,且此链表定义了插入的顺序

打开 LinkedHashMap 类,我们查看它的结点类型,如图——

这里写图片描述

我们可以看到,LinkedHashMap 的结点是继承自 HashMap 结点的,我们再看下 HashMap 的结点 ——

这里写图片描述

所以对于 LinkedHashMap 的结点来说,它是有三个指针的,而 LinkedHashMap 又没有复写父类的 put() 方法,所以说,相比于 HashMap 来说,LinkedHashMap 就是元素多了两个指针,分别指向插入时前一个元素,和插入时后一个元素。当然,口说无凭,我们继续查看 LinkedHashMap 类,类中有一个方法的名字看起来就很特别 ——

这里写图片描述

源代码很简单我此处就不做扩展了。那么何时会调用该方法呢?其实解决这个问题很简单,自己对对象进行 debug 就可以了 ——

这里写图片描述

此处为了方便我是对第二个进行 put 的元素进行 debug 查看流程,接下来:

这里写图片描述

进入父类的 put() 方法,再进入 putVal() 方法,再第二个 if 分支中进入 newNode() 方法中 ——

这里写图片描述

关键时刻到了,此时由于 LinkedHashMap 重写了父类 newNode() 方法,所以调用的就是 LinkedHashMapnewNode() 方法,也就是 java 中的多态。看到这里也就不向下扩展了,我们可以看到第四行就是我们想要的答案 —— 调用了 linkNodeLast() 方法!有一句话叫做“一图胜千言”,抽象理解完了 LinkedHashMap 的数据结构,我再上一张图来加深印象,上图之前也要配合代码,代码如下 ——

Map<Integer, String> map = new LinkedHashMap<Integer, String>();
map.put(1, "张");
map.put(2, "李");
map.put(3, "王");
map.put(4, "刘");
map.put(5, "赵");

这里依然为了简单起见,我们假设 hashCode() 方法所求的 hash 值就是 key 对数组的长度求余,也就是 keyhash == key % arrays.length()。数据结构图如下 ——

这里写图片描述

如果在此之后再插入一个键值对(6, 周) 又会怎样呢?

这里写图片描述

比起插入前,有以下几点更改 ——

  • 第二个双向链表的表头变成了 (6, 周) 这个键值对
  • tail 尾结点指针指向了 (6, 周)
  • 原尾结点 (5, 赵) 的 after 指针从指向 null 变成了指向 (6, 周)

HashMap 通过特殊的构造方法,可以将输出的值从按照插入的顺序改成符合 LRU 的顺序

这里写图片描述

LinkedHashMap 共有五个构造函数,那么上述中特殊的构造函数到底是指哪一个呢?答案就是最后一个,看构造函数上方的注释,对于参数 accessOrder 的解释如下:

the ordering mode   - <tt>true</tt> for access-order, <tt>false</tt> for insertion-order

翻译成中文就是:

排序的模式,如果是 true 的话就是访问顺序,如果是 false 的话就是插入顺序。

注释已经说得很清楚了,那么我们就不妨上代码试验一下,如果设置成 true 会是什么样的呢?代码如下:

public class Test extends Inner {
    public static void main(String[] args) {
        Map<String, String> map = new LinkedHashMap<String, String>(16, .75F, true);
        map.put("1", "1");
        map.put("2", "2");
        map.put("3", "3");
        map.put("4", "4");
        map.forEach((k, v) -> System.out.println(k + "    " + v));
        System.out.println("--------------------");

        map.get("4");
        map.get("2");
        map.get("3");
        map.forEach((k, v) -> System.out.println(k + "    " + v));
    }
}

打印结果如下:

这里写图片描述

我们可以发现,再调用了 get() 方法后,map 的输出顺序发生了改变,越是后调用的值越是越后输出,我们不妨看一下 LinkedHashMapget() 方法源码 ——

这里写图片描述

在第五行,如果 accessOrder 的值为 true 的话,我们就会进入 afterNodeAccess() 方法,继续跟踪进入该方法,源码如下 ——

这里写图片描述

我们可以很清楚的看到,原有尾结点会被设成 e 结点的 before 结点,而 e 结点又会被复制给 tail 结点变成新的尾结点,同时 e 结点的 after 结点赋空,所以最新使用的结点会在双链表的最末端,而最久未使用的那个结点会存在双链表的始端,所以如果在空间不足的情况下,就可以删除前面的结点了,这就是 LRU 算法的思想。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
POJOGenerator(POJO代码生成器 v1.3.3) 本POJO代码生成器采用Java的Swing技术编码实现,是绿色免费工具,可以自由传播。 由于本工具的内部实现较烂,所以还请反编译高手手下留情,让我留几分颜面。^_^ 由于本人只用过Oracle、DB2、MySQL、MS SQL Server这四款数据库产品,所以制作 成exe可执行文件时只添入了这四款数据库的驱动支持。如果您需要使用这款工具从 其它数据库中生成POJO,那么您可以联系我(Email:CodingMouse@gmail.com), 我会添加其它数据库的驱动支持后通过电子邮件发送给您。 简单的使用说明: 1、先将压缩档解压到任意文件夹,必须保留配置文件cmsdk4j.cfg.xml和generator .cfg.xml与可执行文件POJOGenerator.exe在同一目录,否则无法运行。 2、可以预先在配置档cmsdk4j.cfg.xml中设定您的数据库服务器配置,配置档中已经 提供了默认的配置信息,您仅需在此基础上修改部分参数(如:IP地址、端口号、 用户名、密码、数据库名等),这些参数将作为生成器的预设数据库连接配置参数。 3、可以预先在配置档generator.cfg.xml中设定您的数据类型映射方案,配置档中已经 提供了MS SQL Server/MySQL/DB2和Oracle两种映射方案,当然,可能有不太完整的地方 ,您可以根据实际情况稍作修改即可。需要注意的一点是ref属性表示引用同一映射方案 的另一映射,这样您便可以简化同一映射数据类型的配置;而import属性是指定需要在 最终生成的源代码中作为类最开始的package类型导入声明部分的导入类型名称,因此, 这个名称是完整带包名的类名称,否则不能正确生成最终代码。配置档中提供的默认配 置如果不能满足你的需要,也可以自行根据实际情况进行修改。最后,需要大家注意的 一点就是由于最终生成的代码要调用包装类型的equals和hashCode方法,因此,配置的 数据类型必须是包装类型,如果用基本类型生成的POJO代码是无法通过编译的。 4、所有配置档仅在工具启动初始读取一次并缓存到内存中,因此,如果您是在工具运行 时修改的配置档,请重新启动本工具以使新的配置生效。并且,所有配置档的XML结构均 不能修改,只能修改其节点间的文本值或属性值,以及添加新的标签组,否则会导致本 工具无法工作。选择“界面皮肤方案”后,默认会在当前目录生成名为skin.dat的文件, 这是一个Properties属性文件,用于保存您最后选择的皮肤名称,以便下次打开此工具 时加载您所选择的皮肤来渲染工具UI界面。 5、所有最终代码生成效果都可以在左边的代码预览区域中查看,可点击滑动箭头显示出 被隐藏的POJO代码卡片。点击“写入磁盘文件”按钮即可将POJO代码的Java源码文件写入 到指定文件夹中。POJO代码的equals方法重写完全符合《Core Java》所述规范,同时, 其中的hashCode方法重写则参考了Netbeans中JavaBean转换器的写法。为保障原有代码安 全,通常更好的做法是将最终代码生成后拷贝到您的项目对应文件夹中。最好不要直接指 向您的项目文件夹,因为本工具会直接覆盖掉指定目录中同名的文件。最终生成的代码文 件以.java为扩展名。 6、从1.3版开始生成的POJO代码目录中可自动添加一个名为pojo.ntf.xml的POJO映射通 知档,其中,ID列名默认使用主键名称(若为复合主键则采用次序排首位的主键列名) ,而Oracle环境下的sequence对象名称则为“seq_表名_id”格式的默认名称,请根据 实际情况修改。该配置档用于CmSdk4j-Core框架的ORM映射,不需要则请不要勾选此项或 在生成后直接删除即可。 7、目前1.3.3版与1.3版差异不大,仅修改了POJO类名与成员变量名的大小写处理策略。 即目标数据库服务器为Oracle时,才将表名除首字母外全部小写处理成POJO类名,同理, 成员变量名也只在Oracle数据库情况下才全小写处理。其余数据库如:DB2、MySQL、 MS SQL Server则直接处理为除首字母大写外,其余全部保留原始大小写。其中,对于 表名的处理还直接去掉了空格符和下划线,并且若为Oracle数据库时,下划线亦作为首 字母大写的分隔标志,如:HRM_HUMAN_RESOURCE,最终生成的POJO类名将直接去掉串中 的下划线,并以下划线作为首字母大写的起始,即:HrmHumanResource + POJO类名后缀。 同理,成员变量名的处理也是采用了相同的处理策略。最终处理效果详见生成写入到磁盘 的pojo.ntf.xml配置档。 8、此小工具一直均只写来自用,以便与自己的O/R Mapping简易版工具配套使用,目前 1.3.3这个版本已经能满足自己的需要,同时为了方便预览POJO代码生成的效果,特意添 加了语法着色功能,其着色色调搭配和关键字字典数据来源于EmEditor这款带语法着色的 纯文本编辑器,并且该色调搭配方案也被多款JS版本的语法着色器采用,色调可读性较高。 此小工具虽然GUI、功能这些都相对较弱,但自用已经足够。因此,后期可能就不再考虑 功能更新了,请见谅! 如果您有好的建议,请发送留言到作者博客:http://blog.csdn.net/CodingMouse 或发送邮件到:CodingMouse@gmail.com 本工具已经打包成exe可执行文件,便于在Window环境下运行,但仍需要你的机器上 安装至少1.6版本的jre环境(受打包工具的jre版本不兼容限制影响)。 By CodingMouse 2010年5月22日
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值