HashMap和HashTable都是Map接口的典型实现类,但HashTable是一个古老的Map实现类,自Java1.0就有了,但Java1.0还没有Map接口,HashTable只包含了两个方法,elements()(类似于Map接口定义的values()方法)和keys()(类似于Map接口定义的keySet()方法)。
import java.util.*;
import static java.lang.System.*;
public class NullInHashMap
{
public static void main(String[] args)
{
var hm = new HashMap();
// 试图将两个key为null的key-value对放入HashMap中
hm.put(null, null);
hm.put(null, null);
// 将一个value为null的key-value对放入HashMap中
hm.put("a", null);
// 输出Map对象
out.println(hm);
}
}
为了成功的在HashMap、Hashtable中存储、获取对象,用作key的对象必须实现hashCode()方和equals()方法,与HashSet集合一样,HashMap、Hashtable也不能保证其中key-value对的顺序;并且判断两个key相等的标准也是:两个key通过equals()方法比较返回true,两个key的hashCode值也相等。
import java.util.*;
import static java.lang.System.*;
class A
{
int count;
public A(int count)
{
this.count = count;
}
// 根据count的值来判断两个对象是否相等。
public boolean equals(Object obj)
{
if (obj == this)
return true;
if (obj != null && obj.getClass() == A.class)
{
var a = (A) obj;
return this.count == a.count;
}
return false;
}
// 根据count来计算hashCode值。
public int hashCode()
{
return this.count;
}
}
class B
{
// 重写equals()方法,B对象与任何对象通过equals()方法比较都返回true
public boolean equals(Object obj)
{
return true;
}
}
public class HashtableTest
{
public static void main(String[] args)
{
var ht = new Hashtable();
ht.put(new A(60000), "自动化平台测试开发");
ht.put(new A(87563), "SpringBoot实战");
ht.put(new A(1232), new B());
out.println(ht);
// 只要两个对象通过equals比较返回true,
// Hashtable就认为它们是相等的value。
// 由于Hashtable中有一个B对象,
// 它与任何对象通过equals比较都相等,所以下面输出true。
out.println(ht.containsValue("测试字符串"));
// 只要两个A对象的count相等,它们通过equals比较返回true,且hashCode相等
// Hashtable即认为它们是相同的key,所以下面输出true。
out.println(ht.containsKey(new A(87563)));
// 下面语句可以删除最后一个key-value对
ht.remove(new A(1232));
out.println(ht);
}
}
程序定义了A类和B类,其中A类判断两个A对象相等的标准是count实例变量,只要两个A对象的count变量相等,则通过equals()方法比较它们返回ture,它们的hashCode()值也相等;
Hashtable判断value相等的标准时:value与另一个对象通过equals()方法比较返回true即可
与HashSet类似的是,如果使用可变对象作为HashMap、HashTable的key,并且程序修改了作为key的可变对象,则也可能出现与HashSet类似的情形:程序再也无法准确访问到Map中被修改过的key
import java.util.*;
import static java.lang.System.*;
public class HashMapErrorTest
{
public static void main(String[] args)
{
var ht = new HashMap();
// 此处的A类与前一个程序的A类是同一个类
ht.put(new A(60000), "Spring实战");
ht.put(new A(87563), "SpringBoot实战");
// 获得Hashtable的key Set集合对应的Iterator迭代器
var it = ht.keySet().iterator();
// 取出Map中第一个key,并修改它的count值
var first = (A) it.next();
first.count = 87563;
// 输出{A@1560b=Spring实战, A@1560b=SpringBoot实战}
out.println(ht);
// 只能删除没有被修改过的key所对应的key-value对
ht.remove(new A(87563));
out.println(ht);
// 无法获取剩下的value,下面两行代码都将输出null。
out.println(ht.get(new A(87563)));
out.println(ht.get(new A(60000)));
}
}
因此尽量不要使用可变对象作为HashMap、HashTable的key,如果使用可变对象则尽量不要修改作为key的对象
HashSet有一个LinkedHashSet子类,HashMap也有一个LinkedHashMap子类,LinkedHashMap使用双向链表来维护key-value对的次序,该链表负责维护Map的迭代顺序,该顺序与key-value的插入顺序保持一致, 它可以记住key-value对的添加顺序
LinkedHashMap需要维护元素的插入顺序,因此性能略低于HashMap,但因为它以链表来维护内部顺序,所以在迭代访问Map里的全部元素时将有较好的性能
import java.util.*;
import java.lang.System.*;
public class LinkedHashMapTest
{
public static void main(String[] args)
{
var scores = new LinkedHashMap();
scores.put("语文", 80);
scores.put("英文", 82);
scores.put("数学", 76);
// 调用forEach方法遍历scores里的所有key-value对
scores.forEach((key, value) -> System.out.println(key + "-->" + value));
}
}
Properties类是Hashtable类的子类,用于处理属性文件,例如windows平台的ini文件。Properties类可以把Map对象和属性文件关联起来,从而可以把Map对象中的键值对写入属性文件中,也可以把属性文件中的“属性名=属性值”加载到Map中
因为属性文件中的属性名和属性值只能是字符串类型,因此Properties里的key和value也都是字符串类型
该类提供了如下方法来修改Properties:
import java.util.*;
import java.io.*;
import static java.lang.System.*;
public class PropertiesTest
{
public static void main(String[] args)
throws Exception
{
var props = new Properties();
// 向Properties中增加属性
props.setProperty("username", "yeeku");
props.setProperty("password", "123456");
// 将Properties中的key-value对保存到a.ini文件中
props.store(new FileOutputStream("a.ini"),
"comment line");
// 新建一个Properties对象
var props2 = new Properties();
// 向Properties中增加属性
props2.setProperty("gender", "male");
// 将a.ini文件中的key-value对追加到props2中
props2.load(new FileInputStream("a.ini"));
out.println(props2);
}
}
此外,Properties可以把键值对以XML文件的形式保存起来,也可以从XML文件中加载键值对
正如Set接口派生出SortedSet子接口,SortedSet接口有一个TreeSet实现类一样,Map接口也派生出一个SortedMap子接口,SortedMap接口也有一个TreeMap实现类,TreeMap就是一个红黑树数据结构,每个键值对作为红黑树的一个节点。
TreeMap存储键值对(节点)时,需要根据key对节点进行排序,TreeMap可以保证所有的键值对处于有序状态,它的排序方式:
类似于TreeSet中判断两个元素相等的标准,TreeMap中判断两个key相等标准是:两个key通过compareTo()方法返回0,TreeMap即可认为两个key是相等的
如果使用自定义类坐位TreeMap的key,则重写该类的equals(0方法和compareTo()方法时应保持一致的返回结果:两个key通过equals()方法比较返回true时,他们通过compareTo()方法比较应该返回0,如果两个方法的结果不一致,TreeMap和Map接口的规则就会冲突
TreeMap提供了一系列根据key顺序访问键值对的方法:
import java.util.*;
import static java.lang.System.*;
class R implements Comparable
{
int count;
public R(int count)
{
this.count = count;
}
public String toString()
{
return "R[count:" + count + "]";
}
// 根据count来判断两个对象是否相等。
public boolean equals(Object obj)
{
if (this == obj)
return true;
if (obj != null && obj.getClass() == R.class)
{
var r = (R) obj;
return r.count == this.count;
}
return false;
}
// 根据count属性值来判断两个对象的大小。
public int compareTo(Object obj)
{
var r = (R) obj;
return count > r.count ? 1 :
count < r.count ? -1 : 0;
}
}
public class TreeMapTest
{
public static void main(String[] args)
{
var tm = new TreeMap();
tm.put(new R(3), "SpringBoot实战");
tm.put(new R(-5), "Spring实战");
tm.put(new R(9), "Spring");
out.println(tm);
// 返回该TreeMap的第一个Entry对象
out.println(tm.firstEntry());
// 返回该TreeMap的最后一个key值
out.println(tm.lastKey());
// 返回该TreeMap的比new R(2)大的最小key值。
out.println(tm.higherKey(new R(2)));
// 返回该TreeMap的比new R(2)小的最大的key-value对。
out.println(tm.lowerEntry(new R(2)));
// 返回该TreeMap的子TreeMap
out.println(tm.subMap(new R(-1), new R(4)));
}
}
定义了一个R类,该类重写了equals()方法,并实现了Comparable接口,所以可以使用该R对象作为TreeMap的key,该TreeMap使用自然排序
WeakHashMap与HashMap的用法基本相似,区别在于HashMap的key保留了对实际对象的强引用,只要该HashMap对象不被销毁,那么该HashMap的所有key所引用的对象就不会被垃圾回收,HashMap也不会自动删除这些key所对应的键值对。
WeakHashMap的key只保留了对实际对象的若引用,这意味着这些key所引用的对象可能被垃圾回收,当垃圾回收了该key所对应的实际对象后,WeakHashMap会自动删除该key对应的键值对,并且WeakHashMap也可能自动删除这些key所对应的键值对
import java.util.*;
import static java.lang.System.*;
public class WeakHashMapTest
{
public static void main(String[] args)
{
var whm = new WeakHashMap();
// 将WeakHashMap中添加三个key-value对,
// 三个key都是匿名字符串对象(没有其他引用)
whm.put(new String("语文"), new String("良好"));
whm.put(new String("数学"), new String("及格"));
whm.put(new String("英文"), new String("中等"));
//将 WeakHashMap中添加一个key-value对,
// 该key是一个系统缓存的字符串对象,该key是一个字符串直接量
whm.put("java", new String("中等"));
// 输出whm对象,将看到4个key-value对。
out.println(whm);
// 通知系统立即进行垃圾回收
gc();
runFinalization();
// 通常情况下,将只看到一个key-value对。
out.println(whm);
}
}
如果需要使用WeakHashMap的key来保留对象的弱引用,则不要让该key所引用的对象具有任何强引用,否则将失去使用WeakHashMap的意义
在IdentityHashMap中,当且仅当两个key严格相等(key1=key2)时,IdentityHashMap才认为两个key相等,而对于普通的HashMap来说只要key1和key2通过equals()比较返回true,且他们的hashCode值相等即可
IdentityHashMap提供了与HashMap基本相似的方法,也允许使用null作为key和value,同时它也不保证键值对之间的顺序,更不能保证顺序不变
import java.util.*;
import static java.lang.System.*;
public class IdentityHashMapTest
{
public static void main(String[] args)
{
var ihm = new IdentityHashMap();
// 下面两行代码将会向IdentityHashMap对象中添加两个key-value对
ihm.put(new String("语文"), 89);
ihm.put(new String("语文"), 78);
// 下面两行代码只会向IdentityHashMap对象中添加一个key-value对
ihm.put("java", 93);
ihm.put("java", 98);
System.out.println(ihm);
}
}
EnumMap是一个与枚举类一起使用的Map实现,EnumMap中的所有key都必须是单个枚举类的枚举值。创建EnumMap时必须显式或隐式指定它对应的枚举类
import java.util.*;
import static java.lang.System.*;
enum Season
{
SPRING, SUMMER, FALL, WINTER
}
public class EnumMapTest
{
public static void main(String[] args)
{
// 创建EnumMap对象,该EnumMap的所有key都是Season枚举类的枚举值
var enumMap = new EnumMap(Season.class);
enumMap.put(Season.SUMMER, "夏日炎炎");
enumMap.put(Season.SPRING, "春暖花开");
out.println(enumMap);
}
}
对于HashSet及其子类而言,他们采用hash算法来决定集合中元素的存储位置,并通过hash算法来控制集合的大小;对于HashMap、Hashtable及其子类而言,他们采用hash算法来决定Map中key的存储,并通过hash算法来增加key集合的大小
hash表里可以存储元素位置的被称为“桶(bucket)”,在通常情况下,单个bucket里存储一个元素,此时具有最好的性能【hash算法可以根据hashCode值计算出bucket的存储位置,接着从bucket中取出元素】
hash表的状态是open的在发生“hash冲突”的情况下,单个bucket会存储多个元素,这些元素以链表形式存储,必须按顺序搜索,如图所示
HashSet、HashMap、HashTable都使用hash算法来决定其元素的存储,因此HashSet、HashMap的hash表包含如下属性:
HashSet、HashMap和HashTable的构造器允许指定一个负载极限,默认值为0.75,表明当hash表的3/4已经被填满的时候,hash表就会发生rehashing,这个数值是在时间和空间成本上的一种折中。
如果一开始就知道HashSet、HashMap及HashTable会保存很多记录,则可以在创建时就使用较大的初始化容量,如果初始化容量
始终大于HashSet、HashMap及HashTable所包含的最大记录
除以负载极限
就不会发生rehashing