疯狂Java讲义(八)----第四部分

2. Java8增强的Map集合

  (4) 使用Properties 读写属性文件

        Properties类是 Hashtable类的子类,正如它的名字所暗示的,该对象在处理属性文件时特别方便(Windows 操作平台上的 ini文件就是一种属性文件)。Properties类可以把Map对象和属性文件关联起来,从而可以把 Map对象中的key-value对写入属性文件中,也可以把属性文件中的“属性名=属性值”加载到 Map对象中。由于属性文件里的属性名、属性值只能是字符串类型,所以Properties里的 key、value都是字符串类型。该类提供了如下三个方法来修改Properties里的 key、value值。

  • 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):从属性文件(以输入流表示)中加载key-value对,把加载到的key-value对追加到Properties里(Properties是Hashtable 的子类,它不保证 key-value对之间的次序)。
  • void store(OutputStream out, String comments):将Properties中的key-value对输出到指定的属性文件(以输出流表示)中

        上面两个方法中使用了InputStream类和OutputStream类,它们是Java IO体系中的两个基类,关于这两个类的详细介绍请参考第15章。

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

        Properties可以把key-value对以XML文件的形式保存起来,也可以从XML 文件中加载 key-value对,用法与此类似,此处不再赘述。

  (5) SortedMap接口和TreeMap实现类

        正如Set接口派生出SortedSet子接口,SortedSet 接口有一个TreeSet 实现类一样,Map接口也派生出一个SortedMap子接口,SortedMap接口也有一个TreeMap实现类。
        TreeMap就是一个红黑树数据结构每个key-value对即作为红黑树的一个节点。TreeMap存储key-value对(节点)时,需要根据key对节点进行排序。TreeMap可以保证所有的key-value对处于有序状态。TreeMap也有两种排序方式。

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

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

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

  • Map.Entry firstEntry():返回该Map中最小key所对应的key-value对,如果该Map为空,则返回null。
  • Object firstKey():返回该Map中的最小key值,如果该Map为空,则返回null。
  • Map.Entry lastEntry():返回该Map中最大key所对应的key-value对,如果该Map为空或不存在这样的key-value对,则都返回null.
  • Object lastKey():返回该Map中的最大key值,如果该Map为空或不存在这样的 key,则都返回null。
  • Map.Entry higherEntry(Object key):返回该Map中位于key后一位的key-value对(即大于指定key的最小key所对应的key-value对)。如果该Map为空,则返回null。
  • Object higherKey(Object key):返回该Map中位于key后一位的key值(即大于指定key的最小key 值)。如果该Map为空或不存在这样的key-value对,则都返回null。
  • Map.Entry lowerEntry(Object key):返回该Map中位于key前一位的 key-value对(即小于指定key的最大key所对应的 key-value对)。如果该Map为空或不存在这样的 key-value对,则都返回null。
  • Object lowerKey(Object key):返回该Map中位于key前一位的 key值(即小于指定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

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

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

  (6) WeakHashMap 实现类

        WeakHashMap与HashMap的用法基本相似。与HashMap的区别在于,HashMap的 key 保留了对实际对象的强引用,这意味着只要该HashMap对象不被销毁,该HashMap 的所有key 所引用的对象就不会被垃圾回收,HashMap也不会自动删除这些 key所对应的 key-value对;WeakHashMap 的 key只保留了对实际对象的弱引用,这意味着如果WeakHashMap对象的key所引用的对象没有被其他强引用变量所引用,则这些key 所引用的对象可能被垃圾回收,WeakHashMap也可能自动删除这些key所对应的key-value对。
        WeakHashMap 中的每个key对象只持有对实际对象的弱引用,因此,当垃圾回收了该key 所对应的实际对象之后,WeakHashMap会自动删除该key对应的key-value对。看如下程序。

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

  (7) ldentityHashMap 实现类

        这个 Map实现类的实现机制与 HashMap基本相似,但它在处理两个 key 相等时比较独特:在IdentityHashMap中,当且仅当两个key严格相等(keyl == key2)时,IdentityHashMap 才认为两个key相等;对于普通的HashMap而言,只要keyl和key2通过equals()方法比较返回true,且它们的hashCode值相等即可。

        IdentityHashMap提供了与HashMap基本相似的方法,也允许使用null作为key和 value。与HashMap相似: IdentityHashMap也不保证key-value对之间的顺序,更不能保证它们的顺序随时间的推移保持不变。

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

  (8) EnumMap 实现类

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

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

        与创建普通的Map有所区别的是,创建EnumMap时必须指定一个枚举类,从而将该EnumMap和指定枚举类关联起来。
        下面程序示范了EnumMap的用法。

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

  (9) 各Map 实现类的性能分析

        对于 Map 的常用实现类而言,虽然HashMap和 Hashtable的实现机制几乎一样,但由于Hashtable是一个古老的、线程安全的集合,因此 HashMap通常比 Hashtable要快
        TreeMap通常比HashMap、Hashtable要慢(尤其在插入、删除 key-value对时更慢),因为TreeMap底层采用红黑树来管理key-value对(红黑树的每个节点就是一个 key-value对)。
        使用TreeMap有一个好处:TreeMap中的key-value对总是处于有序状态,无须专门进行排序操作当TreeMap被填充之后,就可以调用keySet(),取得由key组成的Set,然后使用toArray()方法生成 key的数组,接下来使用Arrays的 binarySearch()

方法在已排序的数组中快速地查询对象
        对于一般的应用场景,程序应该多考虑使用HashMap,因为 HashMap正是为快速查询设计的(HashMap 底层其实也是采用数组来存储key-value对)。但如果程序需要一个总是排好序的Map时,则可以考虑使用TreeMap。
        LinkedHashMap 比 HashMap慢一点,因为它需要维护链表来保持 Map 中 key-value时的添加顺序。IdentityHashMap性能没有特别出色之处,因为它采用与HashMap基本相似的实现,只是它使用--而不是equals()方法来判断元素相等。EnumMap的性能最好,但它只能使用同一个枚举类的枚举值作为key。

3. HashSet和 HashMap的性能选项

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

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

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

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

        除此之外,hash表里还有一个“负载极限”,“负载极限”是一个0~1的数值,“负载极限”决定了hash表的最大填满程度。当hash表中的负载因子达到指定的“负载极限”时,hash表会自动成倍地增加容量(桶的数量),并将原有的对象重新分配,放入新的桶内,这称为rehashing
        HashSet和 HashMap、Hashtable的构造器允许指定一个负载极限,HashSet和HashMap、Hashtable默认的“负载极限”为0.75,这表明当该hash表的3/4已经被填满时,hash表会发生rehashing。
        “负载极限”的默认值(0.75)是时间和空间成本上的一种折中:较高的“负载极限”可以降低 hash表所占用的内存空间,但会增加查询数据的时间开销,而查询是最频繁的操作(HashMap的get()与 put()方法都要用到查询);较低的“负载极限”会提高查询数据的性能,但会增加 hash表所占用的内存开销。程序员可以根据实际情况来调整HashSet和 HashMap的“负载极限”值
        如果开始就知道HashSet和HashMap、Hashtable 会保存很多记录,则可以在创建时就使用较大的初始化容量,如果初始化容量始终大于HashSet和 HashMap、Hashtable所包含的最大记录数除以“负载极限”,就不会发生rehashing。使用足够大的初始化容量创建HashSet和 HashMap、Hashtable时,可以更高效地增加记录,但将初始化容量设置太高可能会浪费空间,因此通常不要将初始化容量设置得过高。

4. 操作集合的工具类:Collections

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

  (1) 排序操作

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

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

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

        上面代码示范了Collections类常用的排序操作。下面通过编写一个梭哈游戏来演示List集合、Collections工具类的强大功能。

  (2) 查找、替换操作

        Collections还提供了如下常用的用于查找、替换集合元素的类方法。

  • int binarySearch(List list, Object key):使用二分搜索法搜索指定的List集合,以获得指定对象在List集合中的索引。如果要使该方法可以正常工作,则必须保证List 中的元素已经处于有序状态
  • Object max(Collection coll):根据元素的自然顺序,返回给定集合中的最大元素
  • Object max(Collection coll, Comparator comp):根据Comparator指定的顺序,返回给定集合中的最大元素
  • Object min(Collection coll):根据元素的自然顺序,返回给定集合中的最小元素
  • Object min(Collection coll, Comparator comp):根据Comparator 指定的顺序,返回给定集合中的最小元素
  • void fill(List list,Object obj):使用指定元素obj替换指定List集合中的所有元素
  • int frequency(Collection c,Object o):返回指定集合中指定元素的出现次数
  • int indexOfSubList(List source, List target):返回子List对象在父List对象中第一次出现的位置索引;如果父List中没有出现这样的子List,则返回口-1。
  • int lastIndexOfSubList(List source, List target):返回子List对象在父List对象中最后一次出现的位置索引;如果父List中没有出现这样的子List,则返回口-1。
  • boolean replaceAll(List list, Object oldVal, Object newVal):使用一个新值newVal替换List对象的所有旧值 oldVal

下面程序简单示范了Collections工具类的用法。

  (3) 同步控制

        Collections类中提供了多个synchronizedXxx()方法,该方法可以将指定集合包装成线程同步的集合,从而可以解决多线程并发访问集合时的线程安全问题
        Java中常用的集合框架中的实现类HashSet、TreeSet、ArrayList、ArrayDeque、LinkedList、HashMap和TreeMap 都是线程不安全的。如果有多个线程访问它们,而且有超过一个的线程试图修改它们,则存在线程安全的问题。Collections提供了多个类方法可以把它们包装成线程同步的集合。
        下面的示例程序创建了4个线程安全的集合对象。

        在上面示例程序中,直接将新创建的集合对象传给了Collections 的synchronizedXxx方法,这样就可以直接获取List、Set和 Map的线程安全实现版本。

  (4) 设置不可变集合

        Collections提供了如下三类方法来返回一个不可变的集合。

  • emptyXxx():返回一个空的、不可变的集合对象,此处的集合既可以是List,也可以是SortedSet、Set,还可以是Map、SortedMap等。
  • singletonXxx():返回一个只包含指定对象(只有一个或一项元素)的、不可变的集合对象,此处的集合既可以是List,还可以是 Map。
  • unmodifiableXxx():返回指定集合对象的不可变视图,此处的集合既可以是List,也可以是Set、SortedSet,还可以是Map、SorteMap等。

        上面三类方法的参数是原有的集合对象,返回值是该集合的“只读”版本。通过Collections 提供的三类方法,可以生成“只读”的Collection或 Map。看下面程序。

        上面程序的三行粗体字代码分别定义了一个空的、不可变的 List对象,一个只包含一个元素的、不可变的Set对象和一个不可变的Map对象。不可变的集合对象只能访问集合元素,不可修改集合元素。所以上面程序中①②③处的代码都将引发UnsupportedOperationException异常。

  (5) Java 9新增的不可变集合

        Java9终于增加这个功能了——以前假如要创建一个包含6个元素的Set集合,程序需要先创建Set集合,然后调用6次add()方法向Set集合中添加元素。Java 9对此进行了简化,程序直接调用Set、List、Map的of()方法即可创建包含N个元素的不可变集合,这样一行代码就可创建包含N个元素的集合。
        不可变意味着程序不能向集合中添加元素,也不能从集合中删除元素。
        如下程序示范了如何创建不可变集合。

        上面粗体字代码示范了如何使用集合元素创建不可变集合,其中 Set、List 比较简单,程序只要为它们的of()方法传入N个集合元素即可创建Set、List集合。
        从上面粗体字代码可以看出,创建不可变的 Map集合有两个方法:使用of()方法时只要依次传入多个key-value对即可;还可使用ofEntries()方法,该方法可接受多个 Entry对象,因此程序显式使用Map.entry()方法来创建Map.Entry对象

5. 烦琐的接口:Enumeration

        Enumeration接口是 lterator迭代器的“古老版本”,从JDK 1.0开始,Enumeration接口就已经存在了( Iterator 从 JDK 1.2才出现)。Enumeration接口只有两个名字很长的方法。

  • boolean hasMoreElements():如果此迭代器还有剩下的元素,则返回true。
  • Object nextElement():返回该迭代器的下一个元素,如果还有的话(否则抛出异常)。

        通过这两个方法不难发现,Enumeration接口中的方法名称冗长,难以记忆,而且没有提供Iterator的remove()方法。如果现在编写Java程序,应该尽量采用lterator迭代器,而不是用Enumeration迭代器。
        Java之所以保留Enumeration接口,主要是为了照顾以前那些“古老”的程序,那些程序里大量使用了Enumeration接口,如果新版本的Java里直接删除Enumeration接口,将会导致那些程序全部出错。在计算机行业有一条规则:加入任何规则都必须慎之又慎,因为以后无法删除规则
        实际上,前面介绍的Vector(包括其子类Stack)、Hashtable两个集合类,以及另一个极少使用的BitSet,都是从JDK 1.0遗留下来的集合类,而Enumeration接口可用于遍历这些“古老”的集合类。对于ArrayList、HashMap等集合类,不再支持使用Enumeration迭代器。
        下面程序示范了如何通过Enumeration接口来迭代Vector和 Hashtable。

        上面程序使用Enumeration迭代器来遍历Vector和Hashtable集合里的元素,其工作方式与Iterator迭代器的工作方式基本相似。但使用Enumeration迭代器时方法名更加冗长,而且Enumeration迭代器只能遍历Vector、Hashtable这种古老的集合,因此通常不要使用它。除非在某些极端情况下,不得不使用 Enumeration,否则都应该选择Iterator迭代器。

6. 本章小结

        本章详细介绍了Java集合框架的相关知识。本章从Java的集合框架体系开始讲起,概述了Java集合框架的4个主要体系:Set、List、Queue和 Map,并简述了集合在编程中的重要性。本章详细介绍了Java8对集合框架的改进,包括使用Lambda表达式简化集合编程,以及集合的Stream编程等。本章细致地讲述了Set、List、Queue、Map接口及各实现类的详细用法,并深入分析了各种实现类实现机制的差异,并给出了选择集合实现类时的原则。本章从原理上剖析了Map结构特征,以及 Map结构和Set、List之间的区别及联系。本章最后通过梭哈游戏示范了Collections 工具类的基本用法。

猜你喜欢

转载自blog.csdn.net/indeedes/article/details/121061617