目录
一、前言
大家好,本篇博文是对Map接口常用实现类之一Hashtable类的源码分析,顺便讲一下它的子类Properties,考虑到Hashtable的使用频率,up不会像HashMap那样讲得很细致,但是底层的东西该说都会说的,比一般地方讲得还是要细点。
注意 : ①解读源码需要扎实的基础,比较适合希望深究的同学; ②不要眼高手低,看会了不代表你会了,自己能跟着过一遍才算有收获; ③点击文章的侧边栏目录或者前面的目录可以进行跳转。 良工不示人以朴,up所有文章都会适时改进。大家有问题都可以在评论区讨论交流,或者私信up。 感谢阅读!
二、Hashtable详解
1.简介
Hashtable是Map接口的一个实现类,地位上与HashMap平起平坐。Hashtable也属于java.base模块,java.util包下,如下图所示 :
我们再来看看Hashtable的类图,如下 :
2.特点
1° Hashtable中保存的也是key-value键值对,不过特别的是,Hashtable键值对中的"key"和"value"都不能为null,否则会抛出nullPointerException异常。
2° Hashtable中的一些常用方法在用法上基本和HashMap中的一致,比如put方法。
3°Hashtable中的成员方法用了synchronized关键字修饰,因此Hashtable是线程安全的,而HashMap是线程不安全的。
3.底层实现
0°
为了通过Debug来给大家分析Hashtable的源码,up以Hashtable_Demo类作为演示类来进行断点调试。Hashtable_Demo类代码如下 : (main函数第一行设置断点)
packagecsdn.knowledge.api_tools.gather.map; importjava.util.Hashtable; /*** @author : Cyan_RA9* @version : 21.0*/publicclassHashtable_Demo { publicstaticvoidmain(String[] args) { Hashtablehashtable=newHashtable(); hashtable.put(1, 141); hashtable.put(2, 141); hashtable.put(3, 141); hashtable.put(4, 141); hashtable.put(5, 141); hashtable.put(6, 141); hashtable.put(7, 141); hashtable.put(8, 141); hashtable.put(9, "这是集合第9个元素捏"); System.out.println(hashtable); } }
1°
Hashtable底层维护了Hashtable$Entry类型的数组table,用于存放键值对,如下图所示 :
通过查看Hashtable的源码能够找到这个Entry类型,如下图所示 :
可以看到,Hashtable$Entry其实就是Hashtable的一个静态内部类,并且同HashMap中的内部类HashMap$Node类一样,也实现了Map接口中的Entry内部接口。
2°
利用无参构造初始化Hashtable类对象时,会将底层的table数组初始化为长度等于11的数组。同样我们可以通过追溯Hashtable无参构造的源码来找到依据,如下 :
可以看到,Hashtable的无参构造,其实底层调用的是本类的一个有参构造,并且通过传入的实参我们可以得到以下信息——table数组的初始容量 = 11;加载因子 = 0.75。这俩就不用我多说了,都是老面孔了。
我们可以进入追进入这个有参构造中看看,如下图所示 :
别的不说,直接看最后两行,将new出的长度 = 11的Entry类型的数组赋值给了table数组,这条语句解释了table数组的初始容量为什么是11。
同时,threshold(临界值)被赋值为8。这个8是怎么来的?其实和我们之前讲得HashMap底层一样,threshold = table数组的长度 * 加载因子 = 11 * 0.75 = 8.25,因为此处进行了int类型的强制向下转型,所以最后赋值给threshold变量的是8,初始临界值 = 8。
关于"临界值有什么用"的问题up在之前的源码分析中讲过很多次了,这里只回顾一点——在HashMap底层中,当table数组中元素的个数超过临界值,就要对table数组进行扩容。
3°
对于Hashtable底层的table数组,其扩容时采用了"2n + 1"的机制。具体解释我们可以结合源码来看,如下图所示 :
先向table数组中加入8个元素,可以看到此时集合中键值对的个数count = 8,等于此时table数组的临界值threshold。此时,如果我们向集合中添加第9个元素,table数组就会进行扩容操作。我们可以通过Debug,来追一下添加第9个元素的put方法。首先我们进入put方法,如下图所示 :
在put方法中我们发现,真正完成添加元素的操作是在addEntry方法中进行的,因此,我们继续追入addEntry方法,如下图所示 :
观察addEntry方法的源码,我们可以清楚地看到Hashtable类和HashMap类在扩容时明显的两点不同——
①具体代码的执行顺序不同 :
HashMap底层是先完成添加元素的操作,再进行临界值的判断——如果当前元素加入集合后,会使得集合中元素的个数大于临界值threshold,就会跳入resize方法对table数组进行扩容。
而Hashtable则是先进行临界值的判断,再完成添加元素的操作——如果当前集合中元素的个数已经达到或超过当前的临界值,就会先跳入rehash方法对table数组进行扩容操作,等扩容成功后,再回来把当前键值对加入table数组中。
②对临界值的判断条件不同 :
HashMap底层对于临界值的判断条件是——如果当前集合中元素的个数大于当前临界值threshold,就进入resize方法对table数组进行扩容。
PS : 大家可能对HashMap底层的一些东西忘了,up将图放下面 :
而Hashtable对于临界值的判断条件却是——如果当前集合中元素的个数大于等于(达到或超过)当前临界值threshold,就进入rehash方法对table数组进行扩容。
当然,以上两点只是内在底层的一些不同,如果从外在宏观上来看,从结果上来看,其实这俩还是一样的😂,都是在添加超过临界值的那个元素后,table数组完成了扩容。
4°
说了这么多,咱们还不知道Hashtable到底是怎么扩容的呢。
前面我们说到,对Hashtable底层的table数组执行扩容操作的是rehash方法,那么其扩容机制到底是个甚呢?接下里,我们就追入rehash方法瞧瞧,如下所示 :
可以看到,有一条非常关键的语句"int newCapacity = (oldCapacity << 1) + 1;",它可以明确地告诉我们——扩容后新数组的长度newCapacity,就等于旧数组的长度左移一位再加1,相当于旧数组的长度 * 2 再 + 1。因此我们上文才说——Hashtable底层的table数组在扩容时采取了"2n + 1"的扩容机制。所以,扩容后table数组的长度就应该 = 11 * 2 + 1 = 23,对应的临界值 = (int) 23 * 0.75 = 17。如下图所示 :
4.HashMap VS Hashtable
HashMap和Hashtable的具体对比图如下 :
三、Properties详解
1.简介
Properties是Hashtable的子类,因此Properties也间接实现了Map接口。Properties类也是采用key-value键值对的形式来存放数据。
2.特点
1° 作为Hashtable的子类,Properties的特点与Hashtable类似,比如不允许null,以及线程同步等等。
2° Properties可以从___.properties文件对应的文件输入流中加载数据到Properties类对象,并进行数据的读取和修改。(PS : ___.properties文件通常指的是配置文件,例如数据库中保存用户信息的配置文件)。
3.具体使用(可以不看)
1° 关于Properties类对象进行元素添加删除等基本操作,up这里就不演示了。我们直接来看看Properties特别的地方,即——对___.properties文件中的属性列表进行读取和修改的操作。
2° 鉴于此部分内容涉及到一些异常类,File类,IO流等基础知识,如果你还没有学完这些内容,建议你先去把这些核心基础搞定(可以去参考up之前写得关于这些内容的万字详解系列博文),搞定之后再回来看这个。当然,如果你已经是学过这些知识了,看博文就是图个查缺补漏,那接下来的代码对你来说就不成问题。
我们先创建一个properties文件,并保存好该文件的路径,如下图所示 :
该properties文件内容如下 :
我们的目的就是实现——在控制台打印出demp1.properties文件中的属性类别,并将demo1.properties文件中的属性列表读取到demo2.properties文件中,并且再向新的demo2.properties文件中追加我们希望的新的数据。
up以Properties_Demo类为演示类,代码如下 :(代码中的注释要看)
packagecsdn.knowledge.api_tools.gather.map; importjava.io.*; importjava.util.Iterator; importjava.util.Properties; /*** @author : Cyan_RA9* @version : 21.0*/publicclassProperties_Demo { publicstaticvoidmain(String[] args) throwsIOException { //演示 : Properties类的特有功能//1.创建Properties类对象Propertiesproperties=newProperties(); //2.利用字节输入流读取demo1.properties文件中的内容InputStreaminputStream=newBufferedInputStream(newFileInputStream("D:\\JAVA\\IDEA\\file\\demo1.properties")); //3.利用Properties类的load方法,将文件中的属性列表加载到Properties类对象中。properties.load(inputStream); //4.利用迭代器遍历key的集合。Iterator<String>iterator=properties.stringPropertyNames().iterator(); while (iterator.hasNext()) { Stringkey=iterator.next(); //5.根据获得的key来获取对应的value。System.out.println(key+":"+properties.getProperty(key)); } //6.利用字节输出流保存属性到demo2.properties文件(PS : true表示追加开启)FileOutputStreamfileOutputStream=newFileOutputStream("D:\\JAVA\\IDEA\\file\\demo2.properties", true); properties.setProperty("platform", "CSDN"); properties.store(fileOutputStream, "demo2 for properties,just a demonstration."); /*store方法可以将当前Properties类对象中保存的属性列表存储到目的地文件中。传入的字符串可以作为注释出现在新的文件的头部。*///7.释放资源inputStream.close(); fileOutputStream.close(); } }
运行结果 :
在目的地文件的路径下,我们可以看到新的properties文件已经成功生成,如下图所示 :
打开demo2.properties文件,如下图所示 :
可以看到,不仅原本的数据都保存了下来,新的属性列表也成功追加了进来。
四、完结撒❀
🆗,以上就是我们Hashtable及其子类Properties讲解的全部内容了。内容本身还是没什么难度的,主要就是给大家过一下,让大家知道——噢,Map接口的这个实现类是这么个回事。更多精彩内容,关注up不迷路。感谢阅读!
System.out.println("END----------------------------------------------------------------------------");