公众号:菜鸡干Java
文章目录
Map集合
Map
的key不允许重复,即两个key通过equals
方法比较总是返回false
。如果把所以的key
放在一起来看,它们就是一个Set
集合,就是没有顺序,key
之间不能重复。而实际上Map确实包含了一个KeySet()
方法,用于返回Map集合的所有key组成的Set
集合。另外key集与Set集合里的元素的储存形式也很像,Map子类和Set子类在名字上也很相似,比如HashMap
与HashSet
等接口和子类。Map和Set之间的关系非常密切,但如果把key-value中的value
当作key
的附庸,就可以当Set来看Map。
事实上,Map提供了一个Entry
内部类来封装key-value
对,而储存时只考虑key
。从源码上看,Java先实现了Map
,然后通过包装一个所有value
都为null
的Map就实现了Set
集合。如果把所有value
放在一起,它们又类似一个List
集合,元素与元素之间通过索引来查找,只是Map
不是通过整数索引查找,而是用一个对象作为索引。如果需要取出元素,则需要提供元素的key
索引。
Map
接口提供了大量的实现类,典型的如:HashMap
,Hashtable
,LinkedHashMap
等,其中包括一个内部类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改进HashMap
和Hashtable
实现类
其实这种尴尬的关系不是第一次见了,它们的关系如同ArrayList
和Vector
的关系一样。Hashtable
是一个古老的Map
实现类,从它的名字就可以看出,没有遵循单词首字母大写的命名规范。它从JDK1.0
就已经有了,那时还没有Map
接口,所以它包含了两个现在很少用的方法,elements()
类似value方法,和keys()
方法。同时,Java8改进了HashMap
的实现,让它在哈希冲突时依然保持良好的性能。
区别:
Hashtable
是一个线程安全的实现,而HashMap
不是,然而性能比Hashtable
好。Hashtable
不允许null
作为key
和value
,但HashMap
可以。
其实区别了也没太大意义,因为Hashtable
这样古老的类现在用的并不多,如果想要获得线程安全的Map
类,可以通过Collections
工具类把HashMap
转化成线程安全的。
内部元素储存
为了成功在HashMap
和Hashtable
中储存获取对象,用作Key
的对象必须实现hashCode()
方法和equals()
方法,道理和HashSet
类似。与HashSet
集合不能保证元素顺序一样,两个key
相等的标准是通过equals
方法返回true
,hashCode
值也相等。而判断两个value
相等的标准更松一点,即equals
返回true即可,这种标准在containValue()
方法中可以体现。
同样的,当使用自定义类作为HashMap
,Hashtable
的key
时,如果重写该类的equals()
方法,hashCode()
方法也应该重写,另一方面讲,HashMap
与Hashtable
对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实现类
不难发现,该类与TreeSet
和SortedMap
的关系一样,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
类似,区别在于HashMap
的key
保留了对实际对象的强引用,这表示只要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 == key2
,IdentityHashMap
才认为两个key相等。而对于普通HashMap
而言,只需要通过equals
方法比较,且它们的hashCode
值相等即可。
在提供的方法上,IdentityHashMap
提供了与HashMap
相类似的方法,也允许使用null
作为key
和value
!同样,它也不能保证键值对之间的顺序,更不能保证它们的顺序以后不会变。
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
值,或能不能把它作为key
,value
等。
提到性能,最主要还是集中在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
:创建哈希表时桶的数量。HashMap
和HashSet
都允许在构造器中指定初始化容量。 - 尺寸
size
:当前哈希表的元素数量 - 负载因子
load factor
:其等于size/capacity
大小,若为0则表示空,0.5表示半满哈希表。
另外哈希表还有一个负载极限
,它是一个0~1的数,决定了哈希表的最大填满程度。当负载因子到达负载极限时,哈希表会自动成倍增加容量,也就是桶的数量,将原有对象重新分配放入桶内(rehashing)。HashSet
,HashMap
,Hashtable
允许在构造器指定一个负载极限,默认为0.75
,这表明负载因子
当达到0.75的时候,哈希表会重新分配。
那为啥是0.75
呢?其实都是一种取舍后的办法,要时间还是要空间?哈希表是用来查询的,较高的负载极限
可以降低内存开销,然而却增加查询数据时间开销;较低的负载极限
可以降低时间开销,却增加的内存的开销。如果已经知道要保存大量数据,创建时指定较大的初始化容量就行了,从而可以避免rehashing
。