我们在不考虑线程安全使用的时候,HashMap居多,因为具有的api都很直接很方便,在面试的时候这些集合属性也是面试的焦点,但是也多半是问一下线程安全方面的问题,对比之间的不同,这篇文章不会主要去讲述线程安全,而是从基础的角度出发,对这三种集合属性进行一下介绍。
简介
这三者都是一些常见的集合属性的具体实现,以键值对的方式进行存储和数据的操作。
HashTable是早期Java类库提供的一个哈希表的实现,本身就是同步的,使用的时候对于性能开销很大,无法插入null值。
HashMap是应用十分广泛的一个哈希实现,对数据的存储和操作十分方便,而且在初学的时候,也会大量使用到,所以看起来是十分入门的一个Map的实现方法。
但和HashTable的主要区别在于,HashMap是异步的,支持null值的插入,且get put操作性能很强。
TreeMap是基于红黑树的Map实现,但是get put时间复杂度比较高。
扩展
我们从上面的介绍可以看出,HashMap看起来是最好的Map实现方法,时间复杂度又低,性能高,而且消耗资源还少。
但是这句话只限于单线程,我们很清楚HashMap属于线程不安全的实现,所以程序中难免会出现无限循环占用CPU、size不准确等奇怪的问题产生。
接下来我们对此进行一下Map的细节了解:
Map整体架构
首先,我们要对Map的相关类型有一个整体的了解,Map虽然通常被包括在Java集合框架中,但是其本身并不是狭义上的集合类型(Collection),具体可以看这张图(from 极客时间):
HashTable是比较特别的方法,它作为Vector、Stack的早期集合相关类型,扩展了Dictionary类,在结构上与HashMap有明显不同。
而其他的Map实现,就比如说HashMap则是扩展了AbstractMap,里面包含了很多通用方法的抽象类。不同的Map,在类图结构上就能体现出来,设计的目的也体现在不同的接口上。
我们一般使用Map的也就是get/put 而对顺序没有特殊的要求,所以才会让HashMap成为最常用的Map实现方法。但是HashMap的性能更多还是依赖于哈希码的有效性,所以要张蔚一个hashCode与equals的一些基本约定,比如:
- equals相当,hashCode也一定要相等
- 重写了hashCode也要重写equals
- hashCode保持一致性,状态改变返回的哈希值也仍然要一致
- equals的对称、反射、传递等特性
虽然我们创建一个实现顺序存储的Map实现可以用LinkedHashMap和TreeMap都可以保证某种顺序,但是二者还是有一定的不同:LinkedHashMap通常提供的是遍历顺序符合插入顺序,它的实现是通过条目(键值对)维护双向链表。注意,通过特定构造函数,我们可以创建反映访问顺序的实例,所谓的put、get、compute等,都算作“访问”。
这种行为适用于一些特定应用场景,构建一个空间占用敏感的资源池,希望可以自动将最不常被访问的对象释放掉,这就可以利用LinkedHashMap提供的机制来实现:
package jike_Time;
import java.util.LinkedHashMap;
import java.util.Map;
public class linkedHashMapSample {
public static void main(String[] args) {
//实现自定义删除策略,否则行为就和普遍Map没有区别
LinkedHashMap<String, String> linkedHashMap = new LinkedHashMap<String,String>(16, 0.75F, true){
@Override
protected boolean removeEldestEntry(Map.Entry<String, String> eldest) {
return size() > 3;
}
};
linkedHashMap.put("project1", "Valhalla");
linkedHashMap.put("project2", "Panama");
linkedHashMap.put("project3", "Loom");
linkedHashMap.forEach((k,v)->{
System.out.println(k + ":" + v);
});
//模拟访问
linkedHashMap.get("project1");
linkedHashMap.get("project2");
linkedHashMap.get("project3");
System.out.println("Iterate over should be not affected:");
linkedHashMap.forEach((k,v)->{
System.out.println(k + ":" + v);
});
//触发删除
linkedHashMap.put("project4", "Mission Control");
System.out.println("Oldest entry should be removed:");
linkedHashMap.forEach((k, v) ->{
System.out.println(k + ":" + v);
});
}
}
/**
* output:
* project1:Valhalla
* project2:Panama
* project3:Loom
* Iterate over should be not affected:
* project1:Valhalla
* project2:Panama
* project3:Loom
* Oldest entry should be removed:
* project2:Panama
* project3:Loom
* project4:Mission Control
*/
我们可以发现,当我们实现条件(存储单元超过3就自动删除第一个)就可以类似队列做到先进先出。
而对于TreeMap,它的整体顺序是由键的顺序关系决定的,通过Comparator或Comparable(自然顺序)来决定。
类似于刚才提到的HashCode与equals的约定,为了避免模棱两可的情况,自然顺序同样需要符合一个约定,就是compareTo的返回值需要和equals一致,否则就会出现模棱两可的情况。
我们可以分析TreeMap的put方法:
public V put(K var1, V var2) {
TreeMap.Entry var3 = this.root;
if (var3 == null) {
this.compare(var1, var1);
this.root = new TreeMap.Entry(var1, var2, (TreeMap.Entry)null);
this.size = 1;
++this.modCount;
return null;
} else {
Comparator var6 = this.comparator;
int var4;
TreeMap.Entry var5;
if (var6 != null) {
do {
var5 = var3;
var4 = var6.compare(var1, var3.key);
if (var4 < 0) {
var3 = var3.left;
} else {
if (var4 <= 0) {
return var3.setValue(var2);
}
var3 = var3.right;
}
} while(var3 != null);
} else {
if (var1 == null) {
throw new NullPointerException();
}
Comparable var7 = (Comparable)var1;
do {
var5 = var3;
var4 = var7.compareTo(var3.key);
if (var4 < 0) {
var3 = var3.left;
} else {
if (var4 <= 0) {
return var3.setValue(var2);
}
var3 = var3.right;
}
} while(var3 != null);
}
TreeMap.Entry var8 = new TreeMap.Entry(var1, var2, var5);
if (var4 < 0) {
var5.left = var8;
} else {
var5.right = var8;
}
this.fixAfterInsertion(var8);
++this.size;
++this.modCount;
return null;
}
}
从代码底层可以看出,如果不遵守约定,就会被当做同一个。