Java集合(四)-Map集合


Map集合

Map的key不允许重复,即两个key通过equals方法比较总是返回false。如果把所以的key放在一起来看,它们就是一个Set集合,就是没有顺序,key之间不能重复。而实际上Map确实包含了一个KeySet()方法,用于返回Map集合的所有key组成的Set集合。另外key集与Set集合里的元素的储存形式也很像,Map子类和Set子类在名字上也很相似,比如HashMapHashSet等接口和子类。Map和Set之间的关系非常密切,但如果把key-value中的value当作key的附庸,就可以当Set来看Map。

事实上,Map提供了一个Entry内部类来封装key-value对,而储存时只考虑key。从源码上看,Java先实现了Map,然后通过包装一个所有value都为null的Map就实现了Set集合。如果把所有value放在一起,它们又类似一个List集合,元素与元素之间通过索引来查找,只是Map不是通过整数索引查找,而是用一个对象作为索引。如果需要取出元素,则需要提供元素的key索引。

Map接口提供了大量的实现类,典型的如:HashMapHashtableLinkedHashMap等,其中包括一个内部类Entry,该类封装了一个key-value对,类中包含了三个方法:

  • Object getKey():返回该Entry里包含的Key
  • Object getValue():返回该Entry里包含的value值
  • Object setValue(V value):返回该Entry里包含的value值,并返回新设置的Value

Map集合最典型的用法就是成对添加和删除key-value对,判断该Map中是否包含指定key,添加新的value会覆盖原有的value。HashMap重写了toString()方法,实际上所有的实现类都重写了该方法调用方法后,总是返回:{key1=balue1,key2=value2...}格式。

Java8改进HashMapHashtable实现类

其实这种尴尬的关系不是第一次见了,它们的关系如同ArrayListVector的关系一样。Hashtable是一个古老的Map实现类,从它的名字就可以看出,没有遵循单词首字母大写的命名规范。它从JDK1.0就已经有了,那时还没有Map接口,所以它包含了两个现在很少用的方法,elements()类似value方法,和keys()方法。同时,Java8改进了HashMap的实现,让它在哈希冲突时依然保持良好的性能。

区别:

  • Hashtable是一个线程安全的实现,而HashMap不是,然而性能比Hashtable好。
  • Hashtable不允许null作为keyvalue,但HashMap可以。

其实区别了也没太大意义,因为Hashtable这样古老的类现在用的并不多,如果想要获得线程安全的Map类,可以通过Collections工具类把HashMap转化成线程安全的。

内部元素储存

为了成功在HashMapHashtable中储存获取对象,用作Key的对象必须实现hashCode()方法和equals()方法,道理和HashSet类似。与HashSet集合不能保证元素顺序一样,两个key相等的标准是通过equals方法返回truehashCode值也相等。而判断两个value相等的标准更松一点,即equals返回true即可,这种标准在containValue()方法中可以体现。

同样的,当使用自定义类作为HashMap,Hashtablekey时,如果重写该类的equals()方法,hashCode()方法也应该重写,另一方面讲,HashMapHashtable对key的要求和HashSet对元素的要求完全相同。同样的问题,如果key是可变对象,则修改了key将导致无法准确访问被修改的key,而无法访问对应的value值。

LinkedHashMap实现类(HashMap子类)

HashSet有一个LinkedHashSet子类,HashMap也有一个LinkedHashMap子类。同样,也是采用链表来维护Map的迭代顺序的,并且迭代顺序与插入顺序一致。只需在插入时保持应有的顺序即可,而避免对其排序所带来的成本。由于要维护顺序,所以性能上稍逊HashMap,然而像上次讲的那样,这也是优点,就是迭代起来比较方便。

LinkedHashMap weights = new LinkedHashMap();
weights.put("小明",110);
weights.put("菜鸡一号",130);
weights.forEach((key,value)->System.out.println(key+"->"+value));
//Java8新增forEach()方法遍历Map

Properties类(Hashtable的子类)

顾名思义,该类在处理属性文件时特别方便,Properties类可以把Map对象中的key-value写入属性文件,也可以从属性文件中加载键值对到Map对象中去。由于属性名和属性值只能是字符串,所以其中的key,value都是字符串类型,Properties相当于一个key,value都是String类型的Map。该类提供了如下三个方法来修改Properties里的key,value值,注意是“修改”,后面会介绍几个读写方法。

  • String getProperty(String key):获取Properties中指定属性名对应的属性值,类似get方法。
  • String getProperty(String key,String defaultValue):功能与上一个类似,但可以指定key不存在时的默认值。
  • Object setProperty(String key,String value):设置属性值,类似put方法

还有两个读写属性方法:

  • void load(InputStream in):从属性文件(输入流表示)中加载key-value对,并把对加到Properties里。
  • void store(OutputStream out,String comments):将ProPerties中的key-value对输出到指定属性文件中去。
Properties p = new Properties();
p.setProperty("username","菜鸡二号");
p.store(new FileOutputStream("a.ini"),"user");
Properties p1 = new Properties();
p1.load(new FileInputStream("a.ini"));

程序在当前路径下生成一个a.ini文件,内容如下:

#user
#Thu Oct 9
password=123456
username=菜鸡一号

SortedMap接口与TreeMap实现类

不难发现,该类与TreeSetSortedMap的关系一样,TreeMap也是一个红黑树结构,即每个key-value对作为一个节点,而在储存它们时需要进行排序。所以它也有两种排序方式:

  • 自然排序:key必须实现Comparable接口,且为同一类的对象,否则抛出ClassCastException异常。
  • 定制排序:在创建TreeMap时,传入一个Comparator对象,该对象负责对TreeMap中的所有key进行排序,但是不要钱实现Comparable接口。

类似TreeSet判断元素相等的标准一样,两个key通过compareTo()方法返回0,即为相等。如果equals()compareTo()方法返回结果不一致,TreeMap与Map接口就会冲突。同样,它也有相类似的方法访问key-value对。由于TreeMap本身是有序的,所以它增加了访问第一个、最后一个、前一个和后一个的key-value对方法,并提供了截取几个子TreeMap方法。

WeakHashMap实现类

用法HashMap类似,区别在于HashMapkey保留了对实际对象的强引用,这表示只要HashMap对象不被销毁,所有key所引用的对象就不会被垃圾回收。但WeakHashMap对象的key所引用的对象没有被其他强引用变量所引用,则引用的对象有可能被垃圾回收,而且WeakHashMap也可能自动删除这些key所对应的key-value

每个key只持有对实际对象的弱引用,因此,在垃圾回收该key所引用的实际对象后,集合也会自动删除该键值对。

WeakHashMap w = new WeakHashMap();
w.put(new String("语文"),new String("及格"));
w.put(new String("数学"),new String("优秀"));
w.put("java",new String("优秀"));
System.out.println(w);
//{语文=及格,java=优秀,数学=优秀}
Syste,.gc();
System.runFinalization();
System.out.println(w);
{java=中等}

从上面看出,系统进行垃圾回收时,删除了原先的两个键值对,这是由于这两个key都是匿名字符串对象,WeakHashMap只保留了它们的弱引用,所以垃圾回收时将自动删除这两个键值对。如果想使用key来保留对象的弱引用,则不要让key所引用的对象有任何强引用!

IdentityHashMap实现类

实现机制HashMap类似,但在处理两个key相等时比较独特。在IdentityHashMap中,当且仅当两个key严格相等时,即key1 == key2IdentityHashMap才认为两个key相等。而对于普通HashMap而言,只需要通过equals方法比较,且它们的hashCode值相等即可。

在提供的方法上,IdentityHashMap提供了与HashMap相类似的方法,也允许使用null作为keyvalue!同样,它也不能保证键值对之间的顺序,更不能保证它们的顺序以后不会变。

IdentityHashMap i = new IdentityHashMap();
i.put(new String("菜鸡"),88);
i.put(new String("菜鸡"),90);
i.put("java",100);
i.put("java",101);//字符串直接量
System.out.println(i);

输出结果为:

{java=101,菜鸡=90,菜鸡=88}

由于前两个是字符串对象,所以==比较后返回false,后两个是字符串直接量,Java用常量池管理字符串直接量,则将会返回true,最后一次添加为最终结果。这种问题在之前就讲过,为啥直接量包装成对象==比较会返回true,应该是在包装类那里提到过。

EnumMap实现类

这是一个和枚举类一起实现的Map,其中所有的key都必须是单个枚举类的枚举值。在创建EnumMap时,必须隐式或显示的指定它的对应的枚举类。特点有:

  • 在内部以数组的形式保存元素,十分高效
  • 有顺序,根据key的自然排序维护键值对。可以通过keySet(),entrySet(),values()方法遍历集合看到!
  • 不允许将null作为key,但允许作为value。如果只是查询或删除不会抛出异常!

需要注意的是,在创建EnumMap时,必须指定一个枚举值,从而将该EnumMap和指定枚举类关联起来。如果不理解可以参考一下EnumSet集合,类似的。

enum Names{Jason,Toray,Massa}
public class EnumTest{
    public static void main(String[] args){
        EnumMap e = new EnumMap(Names.class);
        e.put(Names.Jason,"就欧森");
        e.put(Names.Toray,"陀螺仪");
        System.out.println(e);
    }
}

输出为:

{Jason=就欧森,Toray=陀螺仪}

各Map实现类性能分析

在讲解Map集合类的过程中,经常性的提到对象之间的比较,value之间的比较及它们的标准;如果是有排序功能的集合,则还会提到实现Comparable接口,或者传入Comparator对象进行排序;而且还会提到是否能保存null值,或能不能把它作为keyvalue等。

提到性能,最主要还是集中在HashMap,Hashtable,TreeSet,TreeMap,LinkedHashSet,LinkedHashMap等Map集合类的分析·。然而在分析的时候并不是太难纠结,由于要维护了啥,所以性能略低;由于实现了啥,所以性能有所下降等。在使用TreeMap时有一个好处,key-value总是处于有序状态,无需专门排序,在被添入键值对后,就可以使用KeySet()方法取得key所组成的Set,然后就可以直接使用toArray()方法生成key的数组,接下来使用binarySearch()方法可以在以排序的数组中二分查找对象。EnumMap性能最好,但它只能使用同一个枚举类的枚举值作为key。

哈希表的存储

哈希表里可以储存元素的位置称为桶bucket,通常一个桶存放一个元素,此时有最好的性能。哈希算法根据哈希值计算出桶的位置,继而从中取出元素。但发生哈希冲突的时候,一个桶会储存多个元素,这些元素会以链表的形式储存,查找时按顺序搜索,由于HashMap,Hashtable,HashSet都使用哈希算法来决定元素的存储,所以其哈希表包含如下属性:

  • 容量capacity:哈希表中桶的数量
  • 初始化容量initial capacity:创建哈希表时桶的数量。HashMapHashSet都允许在构造器中指定初始化容量。
  • 尺寸size:当前哈希表的元素数量
  • 负载因子load factor:其等于size/capacity大小,若为0则表示空,0.5表示半满哈希表。

另外哈希表还有一个负载极限,它是一个0~1的数,决定了哈希表的最大填满程度。当负载因子到达负载极限时,哈希表会自动成倍增加容量,也就是桶的数量,将原有对象重新分配放入桶内(rehashing)。HashSet,HashMap,Hashtable允许在构造器指定一个负载极限,默认为0.75,这表明负载因子当达到0.75的时候,哈希表会重新分配。

那为啥是0.75呢?其实都是一种取舍后的办法,要时间还是要空间?哈希表是用来查询的,较高的负载极限可以降低内存开销,然而却增加查询数据时间开销;较低的负载极限可以降低时间开销,却增加的内存的开销。如果已经知道要保存大量数据,创建时指定较大的初始化容量就行了,从而可以避免rehashing


文章作者: 流浪舟
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 流浪舟 !
评论
 上一篇
Java工具类 Java工具类
公众号:菜鸡干Java 排序操作Collections提供了如下方法用于对List集合排序: void reverse(List list):反转顺序 void shuffle(List list):随机排序 void sort(Li
2020-10-12
下一篇 
Java集合(三)-List和Queue集合 Java集合(三)-List和Queue集合
公众号: 菜鸡干Java 欢迎关注 Java集合—List集合与Set集合不同,List集合是有序,可重复的,而且默认以添加顺序设置索引。List子接口是继承了Collection接口,则可以使用其中的方法。 特别的是List增加了根
2020-10-09
  目录