Java面向对象系列[v1.0.0][HashMap]

HashMap和HashTable都是Map接口的典型实现类,但HashTable是一个古老的Map实现类,自Java1.0就有了,但Java1.0还没有Map接口,HashTable只包含了两个方法,elements()(类似于Map接口定义的values()方法)和keys()(类似于Map接口定义的keySet()方法)。

HashMap和Hashtable

  • HashTable是一个线程安全的Map实现,但HashMap是线程不安全的,因此HashMap比HashTable的性能好一些,但多个线程访问同一个Map对象时,使用HashTable实现类会更好
  • HashTable不允许使用null作为key和value,如果试图把null放进HashTable中,将会引发NullPointerException异常,但HashMap可以使用null作为key或value
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的对象

LinkedHashMap实现类

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读写属性文件

Properties类是Hashtable类的子类,用于处理属性文件,例如windows平台的ini文件。Properties类可以把Map对象和属性文件关联起来,从而可以把Map对象中的键值对写入属性文件中,也可以把属性文件中的“属性名=属性值”加载到Map中
因为属性文件中的属性名和属性值只能是字符串类型,因此Properties里的key和value也都是字符串类型
该类提供了如下方法来修改Properties:

  • String getProperty(String key):获取Properties中指定属性名对应的属性值,类似于Map的get(Object key)方法
  • String getProperty(String key, String defaultValue):该方法与前一个方法基本相似,只是如果Properties中不存在指定的key时,则该方法指定默认值
  • Object setProperty(String key, String value):设置属性值,类似于Hashtable的put()方法
    void load(InputStream inStream):从属性文件(以输入流表示)中加载键值对,把加载到的键值对追加到Properties里(PropertiesHashtable的子类,它不保证键值对的次序)
    void store(OutputStream out, String comments):将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文件中加载键值对

SortedMap接口和TreeMap实现类

正如Set接口派生出SortedSet子接口,SortedSet接口有一个TreeSet实现类一样,Map接口也派生出一个SortedMap子接口,SortedMap接口也有一个TreeMap实现类,TreeMap就是一个红黑树数据结构,每个键值对作为红黑树的一个节点。

TreeMap存储键值对(节点)时,需要根据key对节点进行排序,TreeMap可以保证所有的键值对处于有序状态,它的排序方式:

  • 自然排序:TreeMap的所有key必须实现Comparable接口,而且所有的key应该是同一个类的对象,否则将会抛出ClassCastException异常
  • 定制排序:创建TreeMap时,传入一个Comparator对象,该对象负责对TreeMap中的所有key进行排序,采用定制排序时不要求Map的key实现Comparable接口

类似于TreeSet中判断两个元素相等的标准,TreeMap中判断两个key相等标准是:两个key通过compareTo()方法返回0,TreeMap即可认为两个key是相等的
如果使用自定义类坐位TreeMap的key,则重写该类的equals(0方法和compareTo()方法时应保持一致的返回结果:两个key通过equals()方法比较返回true时,他们通过compareTo()方法比较应该返回0,如果两个方法的结果不一致,TreeMap和Map接口的规则就会冲突

TreeMap提供了一系列根据key顺序访问键值对的方法:

  • Map.Entry firstEntry():返回该Map中最小key所对应的key-value对,如果该Map为空,则返回null
  • Object firstKey():返回该Map中最小key值,如果该Map为空,则返回null
  • Map.Entry lastEntry():返回该Map中最大key所对应的键值对,如果Map为空或不存在这样的键值对,则返回null
  • Object lastKey():返回该Map中的最大key值,如果该Map为空或不存在这样的key,则返回null
  • Map.Entry higherEntry(Object key):返回该Map中位于key后一位的键值对(即大于指定key的最小key所对应的键值对),如果该Map为空,则返回null
  • Object higherKey(Object key):返回该Map中位于key后一位的key值,如果该Map为空或者不存在这样的键值对,则返回null
  • Map.Entry lowerEntry(Object key):返回该Map中位于key前一位的键值对(即小于指定key的最大key所对应的键值对),如果该Map为空或不存在这样的键值对,则返回null
  • Object lowerKey(Object key):返回该Map中位于key前一位的key值,如果该Map为空或者不存在这样的key,则返回null
  • NavigableMap subMap(Object fromKey, boolean fromInclusive, Object toKey, boolean toInclusive):返回该Map的子Map,其key的范围从fromKey(是否包括取决于第二个参数)到toKey(是否包括取决于第四个参数)
  • SortedMap subMap(Object fromKey, Object toKey):返回该Map的子Map,其key的范围是从fromKey(包括)到tokey(不包括)
  • SortedMap tailMap(Object fromKey):返回该Map的子Map,其key的范围是大于fromKey(包括)的所有key
  • NavigableMap tailMap(Object fromKey, boolean inclusive):返回该Map的子Map,其key的范围是大于fromKey(是否包括取决于第二个参数)的所有key
  • SortedMap headMap(Object toKey):返回该Map的子Map,其key的范围是小于toKey(不包括)的所有key
  • NavigableMap headMap(Object toKey, boolean inclusive):返回该Map的子Map,其key的范围是小于toKey(是否包括取决于第二个参数)的所有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实现类

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实现类

在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);
	}
}

  • 前两个键值对的key是新创建的字符串对象,通过==比较不相等,因此被当成两个键值对处理
  • 后两个键值对的key是字符串直接量,且字符顺序完全相同,Java用常量池来管理字符串直接量,所有通过==比较相等,被当成同一个key处理

EnumMap实现类

EnumMap是一个与枚举类一起使用的Map实现,EnumMap中的所有key都必须是单个枚举类的枚举值。创建EnumMap时必须显式或隐式指定它对应的枚举类

  • EnumMap在内部以数组形式保存,所以这种实现形式非常紧凑高效
  • EnumMap根据key的自然顺序(即枚举值在枚举类中的定义顺序)来维护键值对的顺序,当通过keySet()、entrySet()、value()等方法遍历EnumMap时可以看到这种顺序
  • EnumMap不允许使用null作为key,可以使用null作为value,如果试图使用null作为key将抛出NullPointerException异常,如果只是查询是否包含值为null的key,或只是删除值为null的key则不会抛异常
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);
	}
}

各Map实现类性能

  • 通常情况下HashMap要比HashTable要快
  • TreeMap通常比HashMap、HashTable要慢,尤其在插入和删除键值对时,因为TreeMap底层采用红黑树来管理键值对,HashMap底层采用数组来存储键值对,但TreeMap总是处于有序状态,不同专门排序,可以调用keySet()取得有key组成的Set,然后使用toArray()方法生成key的数组,然后使用Arrays的binarySerach()方法在已排序的数组中快速查询对象
  • LinkedHashMap比HashMap慢一点,因为它要维护链表来保持Map中键值对添加时的顺序
  • IdentityHashMap性能没有特别出色之处,它跟HashMap的实现基本相似,只是它用==而不是equals()方法来判断元素相等
  • EnumMap的性能最好,但他只能使用同一个枚举类的枚举值作为key

HashSet和HashMap的性能选项

对于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表包含如下属性:

  • 容量(capacity):hash表中bucket的数量
  • 初始化容量(initial capacity):创建hash表时bucket的数量,HashMap和HashSet都允许在构造器中指定初始化容量
  • 尺寸(Size):当前hash表中记录的数量
  • 负载因子(load factor):负载因子等于Size/Capacity,当其为0的时候,表示空的hash表,0.5的时候表示半满的hash表,以此类推【轻负载的hash表具有冲突少,适宜插入与查询的特点,但用Iterator迭代元素时比较慢】
  • 负载极限:它是一个0到1的数值,这个数值决定了hash表最大填满程度,当hash表中的负载因子达到指定的负载极限时,hash表会自动成倍的增加容量(bucket的数量),并将原有的对象重新分配,放入新的bucket,这就是rehashing。

HashSet、HashMap和HashTable的构造器允许指定一个负载极限,默认值为0.75,表明当hash表的3/4已经被填满的时候,hash表就会发生rehashing,这个数值是在时间和空间成本上的一种折中。

  • 较高的负载极限:可以降低hash表所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的操作,例如HashMap的get()和put()方法都要用到查询
  • 较低的负载极限:可以提高查询数据的性能,但会增加hash表所占用的内存空间

如果一开始就知道HashSet、HashMap及HashTable会保存很多记录,则可以在创建时就使用较大的初始化容量,如果初始化容量始终大于HashSet、HashMap及HashTable所包含的最大记录除以负载极限就不会发生rehashing

发布了207 篇原创文章 · 获赞 124 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/dawei_yang000000/article/details/105233230