8.8 操作集合的工具类Collections

Collections提供了什么功能

Java提供了一个操作SetListMap等集合的工具类:Collections,该工具类里提供了大量方法对集合元素进行排序查询修改等操作,还提供了将集合对象设置为不可变对集合对象实现同步控制等方法。

8.8.1 排序操作

常用排序方法

Collections提供了如下常用的类方法用于对List集合元素进行排序。

方法 描述
static void reverse(List<?> list) 反转方法:反转指定List集合中元素的顺序。
static void shuffle(List<?> list) 随机打乱方法:对List集合元素进行随机排序(shuffle方法模拟了“洗牌”动作)。
static <T extends Comparable<? super T>> void sort(List<T> list) 自然升序排序方法:根据元素的自然顺序对指定List集合的元素按升序进行排序
static <T> void sort(List<T> list, Comparator<? super T> c) 定制排序方法:根据指定Comparator产生的顺序对List集合元素进行排序。
static void swap(List<?> list, int i, int j) 交换两个元素的方法:将指定List集合中的i处元素和j处元素进行交换。
static void rotate(List<?> list, int distance) 循环移动方法,
  • distance为正数时,将list集合的后distance个元素“整体”移到前面;
  • distance为负数时,将list集合的前distance个元素“整体”移到后面。
该方法不会改变集合的长度。

实例

下面程序简单示范了利用Collections工具类来操作List集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.util.*;

public class SortTest
{
public static void main(String[] args)
{
ArrayList nums = new ArrayList();
nums.add(2);
nums.add(-5);
nums.add(3);
nums.add(0);
// 输出:[2, -5, 3, 0]
System.out.println(nums);
// 将List集合元素的次序反转
Collections.reverse(nums);
// 输出:[0, 3, -5, 2]
System.out.println(nums);
// 将List集合元素的按自然顺序排序
Collections.sort(nums);
// 输出:[-5, 0, 2, 3]
System.out.println(nums);
// 将List集合元素的按随机顺序排序
Collections.shuffle(nums);
// 每次输出的次序不固定
System.out.println(nums);
}
}

一次运行效果如下:

1
2
3
4
[2, -5, 3, 0]
[0, 3, -5, 2]
[-5, 0, 2, 3]
[3, -5, 2, 0]

8.7 HashSet和HashMap的性能选项

对于HashSet及其子类而言,它们采用hash算法来决定集合中元素的存储位置,并通过hash算法来控制集合的大小;
对于HashMapHashtable及其子类而言,它们采用hash算法来决定Mapkey的存储,并通过hash算法来增加key集合的大小。

hash存储示意图

hash表里可以存储元素的位置被称为”桶(bucket)”,在通常情况下,单个”桶”里存储一个元素,此时有最好的性能:
hash算法可以根据hashCode值计算出”桶”的存储位置,接着从”桶”中取出元素。但hash表的状态是open的:在发生”hash冲突”的情况下,单个桶会存储多个元素,这些元素以链表形式存储,必须按顺序搜索。如图8.8所示是hash表保存各元素,且发生”hash冲突”的示意图。
这里有一张图片

hash表中的属性

因为HashSetHashMapHashtable都使用hash算法来决定其元素(HashMap则只考虑key)的存储,因此HashSetHashMaphash表包含如下属性:

hash表属性 描述
容量(capacity) hash表中桶的数量。
初始化容量(initial capacity) 创建hash表时桶的数量。HashMapHashSet都允许在构造器中指定初始化容量
尺寸(size) 当前hash表中记录的数量。
负载因子(load factor) 负载因子等于”size除以capacity“。
  • 负载因子为0,表示空的hash表,
  • 负载因子为0.5表示半满的hash表,依此类推。
轻负载的hash表具有冲突少适宜插入查询的特点(但是使用Iterator迭代元素时比较慢)。

负载极限

除此之外,hash表里还有一个”负载极限”,”负载极限”是一个0到1的数值,”负载极限”决定了hash表的最大填满程度。当hash表中的负载因子达到指定的”负载极限”时,hash表会自动成倍地增加容量(桶的数量),**并将原有的对象重新分配,放入新的桶内,这称为rehashing**。
HashSetHashMapHashtable的构造器允许指定一个负载极限,HashSetHashMapHashtable默认的”负载极限”为0.75,这表明默认情况下,当该hash表的4分之3已经被填满时,hash表会发生rehashing

负载极限如何取舍

“负载极限”的默认值(0.75)是时间和空间成本上的一种折中:
较高的”负载极限”可以降低hash表所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的操作(HashMaget()put()方法都要用到查询);
较低的”负载极限”会提高查询数据的性能,但会增加hash表所占用的内存开销。

程序员可以根据实际情况来调整HashSetHashMap的”负载极限”值

如果开始就知道HashSetHashMapHashtable会保存很多记录,则可以在创建时就使用较大的初始化容量,如果初始化容量始终大于HashSetHashMapHashtable所包含的最大记录数除以”负载极限”,就不会发生rehashing。使用足够大的初始化容量创建HashSetHashMapHashtable时,可以更高效地增加记录,但将初始化容量设置太高可能会浪费空间,因此通常不要将初始化容量设置得过高。

8.6.9 各Map实现类的性能分析

Hashtable比HashMap慢

对于Map的常用实现类而言,虽然HashMapHashtable的实现机制几乎一样,但由于Hashtable是一个古老的、线程安全的集合,因此HashMap通常比Hashtable要快。

TreeMap比Hashtable慢

TreeMap通常比HashMapHashtable要慢(尤其在插入、删除key-value对时更慢),因为TreeMap采用红黑树来管理key-value对(红黑树的每个节点就是一个key-value对)。

TreeMap会自动排序

使用TreeMap有一个好处: TreeMap中的key-value对总是处于有序状态,无须专门进行排序操作。

如何快速查找TreeMap中的key

  • TreeMap被填充之后,就可以调用keySet()方法,取得由key组成的Set,
  • 然后使用toArray()方法生成key的数组,
  • 接下来使用Arrays类的binarySearch方法就可以在已排序的数组中快速地查询key对象。

一般用HashMap

对于一般的应用场景,程序应该多考虑使用HashMap,因为HashMap正是为快速查询设计的(HashMap底层其实也是采用数组来存储key-value对)。

需要自动排序用TreeMap

但如果程序需要一个总是排好序的Map时,则可以考虑使用TreeMap.

LinkedHashMap按插入顺序排序

LinkedHashMapHashMap慢一点,因为它需要维护链表来保持Mapkey-value时的添加顺序

IdentityHashMap使用==判断key是否相等

IdentityHashMap性能没有特别出色之处,因为它釆用与HashMap基本相似的实现,只是它使用==运算符来判断元素相等,而不是使用equals()方法来判断元素相等。

EnumMap只能放入枚举值

EnumMap的性能最好,但它只能使用同一个枚举类的枚举值作为key

8.6.8 EnumMap实现类

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

EnumMap特征

EnumMap具有如下特征:

存储结构 数组

EnumMap在内部以数组形式保存,所以这种实现形式非常紧凑、高效

按枚举值的定义顺序排序

EnumMap根据key的自然顺序(即枚举值在枚举类中的定义顺序)来维护key-value对的顺序。当程序通过keySetentrySet()values()等方法遍历EnumMap时可以看到这种顺序。

key不可以为null,value可以为null

EnumMap**不允许使用null作为key,但允许使用null作为value**。

  • 如果试图使用null作为key时将抛出NullPointerException异常。
  • 如果只是查询是否包含值为nullkey,或只是删除值为nullkey,都不会抛出异常

创建EnumMap时必须指定枚举类

与创建普通的Map有所区别的是,创建EnumMap时必须指定一个枚举类,从而将该EnumMap和指定枚举类关联起来。

程序 EnumMap示例

下面程序示范了EnumMap的用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.util.*;

enum Season
{
SPRING,SUMMER,FALL,WINTER
}
public class EnumMapTest
{
public static void main(String[] args)
{
// 创建EnumMap对象,该EnumMap的所有key都是Season枚举类的枚举值
EnumMap enumMap = new EnumMap(Season.class);
enumMap.put(Season.SUMMER , "夏日炎炎");
enumMap.put(Season.SPRING , "春暖花开");
System.out.println(enumMap);
}
}

上面程序中创建了一个EnumMap对象,创建该EnumMap对象时指定它的key只能是Season枚举类的枚举值。如果向该EnumMap中添加两个key-value对后,这两个key-value对将会以Season枚举值的自然顺序排序
编译、运行上面程序,看到如下运行结果:

1
{SPRING=春暖花开, SUMMER=夏日炎炎}

总结

  • 创建EnumMap是就需要传入枚举的class对象
  • EnumMap中的key只能是Season枚举类的枚举值.
  • 放入EnumMap中的枚举值对按其在枚举类中定义的顺序排序

8.6.7 IdentityMap实现类

两个key是同一个对象的引用时IdentityMap才认为这两个key相等

IdentityMap这个Map实现类的实现机制与HashMap基本相似,不过,IdentityMap在处理两个key相等时比较独特:

  • IdentityHashMap中,当且仅当两个key严格相等时, IdentityHashMap才认为两个key相等;
    • 所谓严格相等即:key1==key2。也就是说,只有key1key2是同一个对象的引用,IdentityHashMap才认为两个key相等.

普通的HashMap通过equals和hashCode来判断相等

对于普通的HashMap而言,只要key1key2通过equals()方法比较返回true,且它们的hashCode值相等即可。

IdentityHashMap违反了Map的通常规范

IdentityHashMap是一个特殊的Map实现!此类实现Map接口时,它有意违反了Map的通常规范: IdentityHashMap要求两个key严格相等时才认为两个key相等。

IdentityHashMap的key和value可以为null

IdentityHashMap提供了与HashMap基本相似的方法,也允许使用null作为keyvalue

IdentityHashMap无序

HashMap相似: IdentityHashMap也不保证key-value对之间的顺序,更不能保证它们的顺序随时间的推移保持不变。

程序 IdentityHashMap示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.*;

public class IdentityHashMapTest
{
public static void main(String[] args)
{
IdentityHashMap ihm = new IdentityHashMap();
// 下面两行代码将会向IdentityHashMap对象中添加两个key-value对
// 虽然作为key的这两个字符串通过equals()方法比较相等
// 但不是同一个对象,所以IdentityHashMap认为这两个key不相等,可以添加
ihm.put(new String("语文") , 89);
ihm.put(new String("语文") , 78);
// 下面两行代码只会向IdentityHashMap对象中添加一个key-value对
// 字符串直接量只会创建一次,所以这两个对象是同一个对象.
// IdentityHashMap认为这两个key相等,所以只会添加一次.
ihm.put("java" , 93);
ihm.put("java" , 98);
System.out.println(ihm);
}
}

编译、运行上面程序,看到如下运行结果:

1
{语文=89, java=98, 语文=78}

上面程序试图向IdentityHashMap对象中添加4个key-value对,

  • 前2个key-value对中的key是新创建的字符串对象,它们通过==运算符比较不相等,所以IdentityHashMap会把它们当成2个key来处理;
  • 后2个key-value对中的key都是字符串直接量,而且它们的字符序列完全相同,Java使用常量池来管理字符串直接量,所以它们通过一比较返回true, IdentityHashMap会认为它们是同一个key,因此只有第一次可以添加成功

8.6.6 WeakHashMap实现类

WeakHashMapHashMap的用法基本相似。

WeakHashMap和HashMap的区别

WeakHashMapHashMap的区别在于:

  • HashMapkey保留了对实际对象的强引用,这意味着:
    • 只要该HashMap对象不被销毁,该HashMap的所有key所引用的对象就不会被垃圾回收, HashMap也不会自动删除这些key所对应的key-value;
  • Weak HashMapkey只保留了对实际对象的弱引用,这意味着:
    • 如果WeakHashMap对象的key所引用的对象没有被其他强引用变量所引用,则这些key所引用的对象可能被垃圾回收, WeakHashMap也可能自动删除这些key所对应的key-value

WeakHashMap特点

WeakHashMap中的每个key对象只持有对实际对象的弱引用,因此,当垃圾回收了该key所对应的实际对象之后, WeakHashMap会自动删除该key对应的key-value

实例

看如下程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.*;

public class WeakHashMapTest {
public static void main(String[] args) {
WeakHashMap 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是一个系统缓存的字符串对象。
whm.put("java", new String("中等")); / ①
// 输出whm对象,将看到4个key-value对。
System.out.println(whm);
// 通知系统立即进行垃圾回收
System.gc();
System.runFinalization();
// 通常情况下,将只看到一个key-value对。
System.out.println(whm);
}
}

编译、运行上面程序,看到如下运行结果:

1
2
{英文=中等, java=中等, 数学=及格, 语文=良好}
{java=中等}

从上面运行结果可以看出,当系统进行垃圾回收时,删除了WeakHashMap对象的前三个key- value对。这是因为添加前三个key- value对时,这三个key都是匿名的字符串对象,WeakHashMap只保留了对它们的弱引用,这样垃圾回收时会自动删除这三个key-value对。
WeakHashMap对象中第4个组key-value对(第①号代码行)的key("java")是一个字符串直接量,(系统会自动保留对该字符串对象的强引用),所以垃圾回收时不会回收它.

使用WeakHashMap要注意什么

如果需要使用WeakHashMapkey来保留对象的弱引用,则不要让该key所引用的象具有任何强引用,否则将失去使用WeakHashMap的意义

8.6.5 SortedMap接口和TreeMap实现类

正如Set接口派生出SortedSet子接口有一个TreeSet实现类一样,Map接口也派生的SortedMap子接口也有一个TreeMap实现类.

TreeMap数据结构 红黑树

TreeMap就是一个红黑树数据结构,每个key-value对即作为红黑树的一个节点。 TreeMap存储key-value对(节点)时,需要根据key对节点进行排序。 TreeMap可以保证所有的key-value对处于有序状态

自然排序 定制排序

TreeMap也有两种排序方式:

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

TreeMap判断两个key相等的标准

类似于TreeSet中判断两个元素相等的标准, TreeMap中判断两个key相等的标准是:
如果两个key通过compareTo()方法返回0, 那么TreeMap就认为这两个key是相等的

理解Comparable接口的compareTo方法

  • 如果对象.compareTo(参数)返回0,则表示该对象和参数相等.
  • 如果对象.compareTo(参数)返回正数,则表示该对象大于参数.
  • 如果对象.compareTo(参数)返回负数,则表示该对象小于参数.

为了便于记忆可以把Comparable接口的compareTo方法理解为减去的意思,也就是说对象.compareTo(参数)意思为对象减去参数.

  • 如果对象减去参数等于0,则两者相等.
  • 如果对象减去参数大于0,则对象大于参数,
  • 如果对象减去参数小于0则对象小于参数。

理解Comparator接口的compare方法

同样的Comparator接口的compare(参数1,参数2)可以理解为前面的参数减去后面的参数的计算结果.也就是参数1减去参数2,所以:

  • 如果参数1减去参数2等于0时,两者相等,
  • 如果参数1减去参数2大于0,则参数1大于参数2,
  • 如果参数1减去参数2小于0,则参数1小于参数2.

作为key的自定义类的要求

如果使用自定义类作为TreeMapkey,且想让TreeMap良好地工作,则重写该类的equals()方法和compareTo()方法时应保持一致的返回结果:
**两个key通过equals方法比较返回true时,它们通过compareTo()方法比较应该返回0**。如果equals()方法与compareTo()方法的返回结果不一致, TreeMapMap接口的规则就会冲突。

Java使用Map来实现的Set

再次强调:SetMap的关系十分密切,Java源码就是先实现了HashMapTreeMap等集合,然后通过包装一个所有的value都为nullMap集合实现了Set集合类。

Set和Map的关系

再次强调:SetMap的关系十分密切,Java源码就是先实现了HashMapTreeMap等集合,然后通过包装一个所有的value都为nullMap集合来实现Set集合类

返回key或Entry的方法

TreeSet类似的是, TreeMap中也提供了一系列根据key顺序访问key-value对的方法。

查找最大最小的key

方法 描述
K firstKey() 返回该Map中的最小key值,如果该Map为空,则返回null
K lastKey() 返回该Map中的最大key值,如果该Map为空或不存在这样的key,则都返回null

查找给定key的前一个或后一个key

方法 描述
K higherKey(K key) 返回该Map中大于参数key的最小key值。如果该Map为空或不存在这样的key-value对,则都返回null
K lowerKey(K key) 返回该Map中小于参数key的最大key值。如果该Map为空或不存在这样的key,则都返回null

查找最大最小key对应的Entry

方法 描述
Map.Entry<K,​V> firstEntry() 返回该Map中最小key所对应的key-value对,如果该Map为空,则返回null
Map.Entry<K,​V> lastEntry() 返回该Map中最大key所对应的 key-value对,如果该Map为空或不存在这样的key-value对,则都返回null

查找给定key的前一个或后一个key对应的Entry

方法 描述
Map.Entry<K,​V> higherEntry(K key) 返回该Map中大于参数key的最小key所对应的key-value对。如果该Map为空,则返回null
Map.Entry<K,​V> lowerEntry(K key) 返回该Map中小于参数key的最大key所对应的key-value对。如果该Map为空或不存在这样的 key-value对,则都返回null

截取子Map的方法

截取指定区间

方法 描述
SortedMap<K,​V> subMap(K fromKey, K toKey) 返回该Map的子Map,其key的范围是从fromKey(包括)到toKey(不包括)。subXXX()方法遵循前闭后开原则。
NavigableMap<K,​V> subMap(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive) 返回该Map的子Map,其key的范围是从 fromKeytoKey,是否包括fromKey取决于第二个参数,是否包括toKey取决于第四个参数.

向前截取

方法 描述
SortedMap<K,​V> headMap(K toKey) 返回该Map的子Map,其key的范围是小于toKey的所有key,不包括toKey.
NavigableMap<K,​V> headMap(K toKey, boolean inclusive) Returns a view of the portion of this map whose keys are less than (or equal to, if inclusive is true) toKey.

向后截取

方法 描述
SortedMap<K,​V> tailMap(K fromKey) 返回该Map的子Map,其key的范围是大于fromKey(包括)的所有key
NavigableMap<K,​V> tailMap(K fromKey, boolean inclusive) 返回该Map的子Map,其key的范围是大于fromKey的所有key,子Map是否包括第一个fromKey取决于第二个参数.

表面上看起来这些方法很复杂,其实它们很简单。因为TreeMap中的key-value对是有序的,所以增加了访问第一个、最后一个、前一个、后一个、key-value对的方法,并提供了几个从TreeMap中截取子TreeMap的方法。

实例

下面以自然排序为例,介绍TreeMap的基本用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import java.util.*;

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)
{
R r = (R)obj;
return r.count == this.count;
}
return false;
}
// 根据count属性值来判断两个对象的大小。
public int compareTo(Object obj)
{
R r = (R)obj;
return count > r.count ? 1 :
count < r.count ? -1 : 0;
}
}
public class TreeMapTest
{
public static void main(String[] args)
{
TreeMap tm = new TreeMap();
tm.put(new R(3) , "轻量级Java EE企业应用实战");
tm.put(new R(-5) , "疯狂Java讲义");
tm.put(new R(9) , "疯狂Android讲义");
System.out.println(tm);
// 返回该TreeMap的第一个Entry对象
System.out.println(tm.firstEntry());
// 返回该TreeMap的最后一个key值
System.out.println(tm.lastKey());
// 返回该TreeMap的比new R(2)大的最小key值。
System.out.println(tm.higherKey(new R(2)));
// 返回该TreeMap的比new R(2)小的最大的key-value对。
System.out.println(tm.lowerEntry(new R(2)));
// 返回该TreeMap的子TreeMap
System.out.println(tm.subMap(new R(-1) , new R(4)));
}
}

上面程序中定义了一个R类,该类重写了equals方法,并实现了Comparable接口,所以可以使用该R对象作为TreeMapkey,该TreeMap使用自然排序。运行上面程序,看到如下运行结果:

1
2
3
4
5
6
{R[count:-5]=疯狂Java讲义, R[count:3]=轻量级Java EE企业应用实战, R[count:9]=疯狂Android讲义}
R[count:-5]=疯狂Java讲义
R[count:9]
R[count:3]
R[count:-5]=疯狂Java讲义
{R[count:3]=轻量级Java EE企业应用实战}

8.6.4 使用Properties读写属性文件

Properties类是Hashtable类的子类,正如它的名字所暗示的,该对象在处理属性文件时特别方便( Windows操作平台上的.ini文件就是一种属性文件)。

Properties功能

Properties类可以把Map对象和属性文件关联起来,从而可以把Map对象中的key-value对写入属性文件中,也可以把属性文件中的”属性名=属性值“加载到Map对象中

key和value都是String

由于属性文件里的属性名、属性值只能是字符串类型,所以Properties里的keyvaue都是字符串类型。所以Properties相当于一个keyvalue都是String类型的Map

Properties类方法

获取属性值

方法 描述
String getProperty(String key) 获取Properties中指定属性名对应的属性值,类似于Mapget(Object key)方法。
String getProperty(String key, String defaultValue) 该方法与前一个方法基本相似。该方法多一个功能,如果Properties中不存在指定的key时,则该方法返回第二个参数作为默认值。

设置属性值

方法 描述
Object setProperty(String key, String value) 设置属性值,类似于Hashtableput()方法。

读取属性文件

方法 描述
void load(InputStream inStream) 从属性文件(以输入流表示)中加载key-value对,把加载到的key-value对追加到Properties里(PropertiesHashtable的子类,它不保证key-value对之间的次序)。
void load(Reader reader) Reads a property list (key and element pairs) from the input character stream in a simple line-oriented format.

写入属性文件

方法 描述
void store(OutputStream out, String comments) Properties中的key-value对输出到指定的属性文件(以输出流表示)中。
void store(Writer writer, String comments) Writes this property list (key and element pairs) in this Properties table to the output character stream in a format suitable for using the load(Reader) method.

实例 Properties读写文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.*;
import java.io.*;

public class PropertiesTest
{
public static void main(String[] args)
throws Exception
{
Properties 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对象
Properties props2 = new Properties();
// 向Properties中增加属性
props2.setProperty("gender" , "male");
// 将a.ini文件中的key-value对追加到props2中
props2.load(new FileInputStream("a.ini") ); //②
System.out.println(props2);
}
}

上面程序示范了Properties类的用法,其中
①代码处将Properties对象中的key-value对写入a.ini文件中;
②代码处则从a.ini文件中读取key-value对,并添加到props2对象中。
编译、运行上面程序,该程序输出结果如下:

1
{password=123456, gender=male, username=yeeku}

上面程序还在当前路径下生成了一个a.ini文件,该文件的内容如下:

1
2
3
4
#comment line
#Thu Jul 11 17:42:17 CST 2019
password=123456
username=yeeku

读写XML

Properties可以把key-value对以XML文件的形式保存起来,也可以从XML文件中加载key-value对,相关方法如下。

读取XML

方法 描述
void loadFromXML(InputStream in) 将指定输入流上的XML文档表示的所有属性加载到此Properties表中。

写入XML

方法 描述
void storeToXML(OutputStream os, String comment) 把属性表中的键值对保存到到XML文件中
void storeToXML(OutputStream os, String comment, String encoding) 把属性表中的键值对保存到XML文件中,并指定编码
void storeToXML(OutputStream os, String comment, Charset charset) Emits an XML document representing all of the properties contained in this table, using the specified encoding.

实例 Properties读写XML

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package map.test.properties;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.InvalidPropertiesFormatException;
import java.util.Properties;

public class PropertiesXMLTest
{
public static void main(String[] args)
{
Properties props = new Properties();
// 向Properties中增加属性
props.setProperty("username", "yeeku");
props.setProperty("password", "123456");
// 将Properties中的key-value对保存到a.ini文件中
try
{
props.storeToXML(new FileOutputStream("a.xml"), "这是注释");
} catch (FileNotFoundException e1)
{
e1.printStackTrace();
} catch (IOException e1)
{
e1.printStackTrace();
}
// 新建一个Properties对象
Properties props2 = new Properties();
// 向Properties中增加属性
props2.setProperty("gender", "male");
// 将a.ini文件中的key-value对追加到props2中
try
{
props2.loadFromXML(new FileInputStream("a.xml"));
} catch (InvalidPropertiesFormatException e)
{
e.printStackTrace();
} catch (FileNotFoundException e)
{
e.printStackTrace();
} catch (IOException e)
{
e.printStackTrace();
}
System.out.println(props2);
}
}

运行效果:

1
{password=123456, gender=male, username=yeeku}

生成XML文件内容如下:

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>这是注释</comment>
<entry key="password">123456</entry>
<entry key="username">yeeku</entry>
</properties>

8.6.3 LinkedHashMap实现类

HashSet有一个LinkedHashSet子类, HashMap也有一个LinkedHashMap子类;

LinkedHashMap数据结构 双向链表

LinkedHashMap也使用双向链表来维护key-value对的次序(其实只需要考虑key的次序),该链表负责维护Map的迭代顺序。

LinkedHashMap有序

LinkedHashMap的迭代顺序与key-value对的插入顺序保持一致

LinkedHashMap可以避免对HashMapHashtable里的key-value对进行排序(只要插入key-value对时保持顺序即可),同时又可避免使用TreeMap所增加的成本。
LinkedHashMap需要维护元素的插入顺序,因此性能略低于HashMap的性能;但因为它以链表来维护内部顺序,所以在迭代访问Map里的全部元素时将有较好的性能。

程序 LinkedHashMap示例

下面程序示范了LinkedHashMap的功能:迭代输出LinkedHashMap的元素时,输出的顺序与添加key-value对的顺序一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.util.*;

public class LinkedHashMapTest
{
public static void main(String[] args)
{
LinkedHashMap 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));
}
}

运行结果:

1
2
3
语文-->80
英文-->82
数学-->76

上面程序中最后一行代码使用Java 8Map新增的forEach()方法来遍历Map集合。编译、运行上面程序,即可看到LinkedHashMap的功能: LinkedHashMap可以记住key-value对的添加顺序

8.6.2 Java 8改进的HashMap和Hashtable实现类

HashMapHashtable都是Map接口的典型实现类,它们之间的关系完全类似于ArrayListVector的关系:Hashtable是一个古老的Map实现类,它从JDK1.0起就已经出现了,当**Hashtable出现时,Java还没有提供Map接口**,所以Hashtable包含了两个烦琐的方法:

  • elements()方法(这个类似于Map接口定义的values()方法)
  • keys()方法(该方法类似于Map接口定义的keySet()方法)

现在很少使用这两个方法。

Java 8改进了HashMap的实现,使用HashMap存在key冲突时依然具有较好的性能。

Hashtable和HashMap的典型区别

除此之外, HashtableHashMap存在两点典型区别。

Hashtable线程安全 HashMap线程不安全

  • Hashtable是一个线程安全Map实现,但HashMap线程不安全的实现,所以HashMapHashtable的性能高一点;但如果有多个线程访问同一个Map对象时,使用Hashtable实现类会更好。

Hashtable的key和value不能为null HashMap的key和value可以为null

  • Hashtable不允许使用null作为keyvalue,如果试图把null值放进Hashtable中,将会引发NullPointerException异常;但HashMap可以使用null作为keyvalue

由于HashMap里的key不能重复,value可以重复,所以HashMap里最多只有一个key-value对的key可以为null,但可以有无数多个key-value对的value可以为null

下面程序示范了用null值作为HashMapkeyvalue的情形。

实例 null作为HashMap的key和value的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.*;

public class NullInHashMap
{
public static void main(String[] args)
{
HashMap hm = new HashMap();
// 试图将两个key为null的key-value对放入HashMap中
hm.put(null , null);
hm.put(null , null); // 1
// 将一个value为null的key-value对放入HashMap中
hm.put("a" , null); // 2
// 输出Map对象
System.out.println(hm);
}
}

上面程序试图向HashMap中放入三个key-value对,其中1代码处无法将key-value对放入,因为Map中已经有一个key-value对的keynull值,所以无法再放入keynull值的key-value对。2代码处可以放入该key-value对,因为一个HashMap中可以有多个valuenull。编译、运行上面程序,看到如下输出结果:

1
{null=null, a=null}

尽量少用Hashtable

Hashtable的类名上就可以看出它是一个古老的类,它的命名甚至没有遵守Java的命名规范:每个单词的首字母都应该大写。也许当初开发Hashtable的工程师也没有注意到这一点,后来大量Java程序中使用了Hashtable类,所以这个类名也就不能改为HashTable了,否则将导致大量程序需要改写。与Vector类似的是,尽量少用Hashtable实现类

即使有线程安全要求也不要使用Hashtable

即使需要创建线程安全的Map实现类,也无须使用Hashtable实现类,可以通过后面介绍的Collections工具类把HashMap变成线程安全的。

HashMap中作为key对象要满足什么条件

为了成功地在HashMapHashtable中存储、获取对象,用作key的对象必须实现hashCode()方法和equals()方法。

HashMap判断两个key相等的标准是什么

HashSet集合不能保证元素的顺序一样, HashMapHashtable也不能保证其中key-value对的顺序。类似于HashSet, HashMapHashtable判断两个key相等的标准也是:两个key通过equals方法比较返回true,并且两个keyhashCode值也相等

HashMap判断两个value相等的标准是什么

除此之外, HashMapHashtable中还包含一个containsValue()方法,用于判断是否包含指定的value那么HashMapHashtable如何判断两个value相等呢? HashMapHashtable判断两个value相等的标准更简单:只要两个对象通过equals方法比较返回true即可

程序示例

下面程序示范了Hashtable判断两个key相等的标准和两个value相等的标准。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
import java.util.*;

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)
{
A 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)
{
Hashtable ht = new Hashtable();
ht.put(new A(60000) , "疯狂Java讲义");
ht.put(new A(87563) , "轻量级Java EE企业应用实战");
ht.put(new A(1232) , new B());
System.out.println(ht);
// 只要两个对象通过equals比较返回true,
// Hashtable就认为它们是相等的value。
// 由于Hashtable中有一个B对象,
// 它与任何对象通过equals比较都相等,所以下面输出true。
System.out.println(ht.containsValue("测试字符串")); // ① 输出true
// 只要两个A对象的count相等,它们通过equals比较返回true,且hashCode相等
// Hashtable即认为它们是相同的key,所以下面输出true。
System.out.println(ht.containsKey(new A(87563))); // ② 输出true
// 下面语句可以删除最后一个key-value对
ht.remove(new A(1232)); //③
System.out.println(ht);
}
}

上面程序定义了A类和B类,其中

  • A类判断两个A对象相等的标准是count实例变量:只要两个A对象的count变量相等,则通过equals方法比较它们返回true,它们的hashCode值也相等;
  • 而B对象则可以与任何对象相等。

Hashtable判断value相等的标准是: value与另外一个对象通过equals()方法比较返回true即可。上面程序中的ht对象中包含了一个B对象,它与任何对象通过equals()方法比较总是返回true,所以在①代码处返回true在这种情况下,不管传给ht对象的containtsValue()方法参数是什么,程序总是返回true

根据Hashtable判断两个key相等的标准,程序在②处也将输出true,虽然两个A对象虽然不是同个对象,但它们**通过equals方法比较返回tue,且hashCode值相等, Hashtable就认为它们是同一个key**。类似的是,程序在③处也可以删除对应的key-value对。

equals方法和hashCode方法判断标准要一致

当使用自定义类作为HashMapHashtablekey时,如果重写该类的equals()方法和hashCode()方法,则应该保证两个方法的判断标准一致,也就是当两个key通过equals方法比较返回true时,两个keyhashCode()返回值也应该相同。因为HashMapHashtable保存key的方式与HashSet保存集合元素的方式完全相同,所以HashMapHashtablekey的要求与HashSet对集合元素的要求完全相同。

可变对象作为HashMap的key时可能无法正确访问

HashSet类似的是,如果使用可变对象作为HashMapHashtablekey,并且程序修改了作为key的可变对象,则也可能出现与HashSet类似的情形:程序再也无法准确访问到Map中被修改过的key

看下面程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.util.*;

public class HashMapErrorTest
{
public static void main(String[] args)
{
HashMap ht = new HashMap();
// 此处的A类与前一个程序的A类是同一个类
ht.put(new A(60000) , "疯狂Java讲义");
ht.put(new A(87563) , "轻量级Java EE企业应用实战");
// 获得Hashtable的key Set集合对应的Iterator迭代器
Iterator it = ht.keySet().iterator();
// 取出Map中第一个key,并修改它的count值
A first = (A)it.next();
first.count = 87563; // ①
// 输出{A@1560b=疯狂Java讲义, A@1560b=轻量级Java EE企业应用实战}
System.out.println(ht);
// 只能删除 没有被修改过 的key所对应的key-value对
ht.remove(new A(87563));

System.out.println(ht);
// 无法获取剩下的value,下面两行代码都将输出null。
System.out.println(ht.get(new A(87563))); // ② 输出null
System.out.println(ht.get(new A(60000))); // ③ 输出null
}
}

该程序使用了前一个程序定义的A类实例作为key,而A对象是可变对象。当程序在①处修改了A对象后,实际上修改了HashMap集合中元素的key,这就导致该key不能被准确访问。当程序试图删除count87563的A对象时,只能删除没被修改的key所对应的key-value对。程序②和③处的代码都不能访问”疯狂Java讲义“字符串,这都是因为它对应的key被修改过的原因。

尽量不要使用可变对象作为HashMap的key

HashSet类似的是,尽量不要使用可变对象作为HashMapHashtablekey,如果确实需要使用可变对象作为HashMapHashtablekey,则尽量不要在程序中修改作为key的可变对象。