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

1. Queue集合

        Queue用于模拟队列这种数据结构,队列通常是指“先进先出”(FIFO)的容器。队列的头部保存在队列中存放时间最长的元素,队列的尾部保存在队列中存放时间最短的元素新元素插入(offer)到队列的尾部访问元素(poll)操作会返回队列头部的元素。通常,队列不允许随机访问队列中的元素。
        Queue接口中定义了如下几个方法。

  • void add(Object e):将指定元素加入此队列的尾部。
  • Object element():获取队列头部的元素,但是不删除该元素。
  • boolean offer(Object e):将指定元素加入此队列的尾部当使用有容量限制的队列时,此方法通常比 add(Object e)方法更好
  • Object peek():获取队列头部的元素,但是不删除该元素如果此队列为空,则返回null。
  • Object poll():获取队列头部的元素,并删除该元素。如果此队列为空,则返回null。
  • Object remove():获取队列头部的元素,并删除该元素。

        Queue接口有一个PriorityQueue 实现类。除此之外,Queue还有一个 Deque接口,Deque 代表一个“双端队列”,双端队列可以同时从两端来添加、删除元素,因此 Deque的实现类既可当成队列使用,也可当成栈使用。Java为 Deque提供了ArrayDeque和 LinkedList两个实现类。

  (1) PriorityQueue 实现类

        PriorityQueue是一个比较标准的队列实现类。之所以说它是比较标准的队列实现,而不是绝对标准的队列实现,是因为 PriorityQueue保存队列元素的顺序并不是按加入队列的顺序,而是按队列元素的大小进行重新排序因此当调用peek()方法或者poll()方法取出队列中的元素时,并不是取出最先进入队列的元素,而是取出队列中最小的元素从这个意义上来看,PriorityQueue已经违反了队列的最基本规则:先进先出((FIFO)。下面程序示范了PriorityQueue队列的用法。

        运行上面程序直接输出 PriorityQueue集合时,可能看到该队列里的元素并没有很好地按大小进行排序,但这只是受到PriorityQueue 的 toString()方法的返回值的影响。实际上,程序多次调用PriorityQueue集合对象的poll()方法,即可看到元素按从小到人的顺序“移出队列”。
        PriorityQueue不允许插入null元素,它还需要对队列元素进行排序,PriorityQueue的元素有两种排序方式。

  • 自然排序:采用自然顺序的PriorityQueue集合中的元素必须实现了Comparable接口,而且应该是同一个类的多个实例,否则可能导致ClassCastException异常。
  • 定制排序:创建PriorityQueue队列时,传入一个Comparator对象,该对象负责对队列中的所有元素进行排序。采用定制排序时不要求队列元素实现Comparable接口。

        PriorityQueue队列对元素的要求与TreeSet对元素的要求基本一致,因此关于使用自然排序和定制排序的详细介绍请参考8.3.3节。

  (2) Deque接口与ArrayDeque实现类

        Deque接口是Queue接口的子接口,它代表一个双端队列,Deque接口里定义了一些双端队列的方法,这些方法允许从两端来操作队列的元素。

  • void addFirst(Object e):将指定元素插入该双端队列的开头。
  • void addLast(Object e):将指定元素插入该双端队列的末尾。
  • lterator descendinglterator():返回该双端队列对应的迭代器,该迭代器将以逆向顺序来迭代队列中的元素。
  • Object getFirst():获取但不删除双端队列的第一个元素。
  • Object getLast():获取但不删除双端队列的最后一个元素。
  • boolean offerFirst(Object e):将指定元素插入该双端队列的开头。
  • boolean offerLast(Object e):将指定元素插入该双端队列的末尾。
  • Object peekFirst():获取但不删除该双端队列的第一个元素;如果此双端队列为空,则返回null。
  • Object peekLast():获取但不删除该双端队列的最后一个元素;如果此双端队列为空,则返回null。
  • Object pollFirst():获取并删除该双端队列的第一个元素;如果此双端队列为空,则返回null。
  • Object pollLast():获取并删除该双端队列的最后一个元素;如果此双端队列为空,则返回null。
  • Object pop()(栈方法): pop出该双端队列所表示的栈的栈顶元素。相当于removeFirst()。
  • void push(Object e)(栈方法):将一个元素 push进该双端队列所表示的栈的栈顶。相当于addFirst(e)。
  • Object removeFirst():获取并删除该双端队列的第一个元素。
  • Object removeFirstOccurrence(Object o):删除该双端队列的第一次出现的元素o。
  • Object removeLast():获取并删除该双端队列的最后一个元素。
  • boolean removeLastOccurrence(Object o):删除该双端队列的最后一次出现的元素o。

        从上面方法中可以看出,Deque不仅可以当成双端队列使用,而且可以被当成栈来使用,因为该类里还包含了pop(出栈)、push(入栈)两个方法。
        Deque的方法与Queue的方法对照表如表8.2所示。

        Deque接口提供了一个典型的实现类: ArrayDeque,从该名称就可以看出,它是一个基于数组实现的双端队列,创建 Deque时同样可指定一个numElements参数,该参数用于指定Object[]数组的长度;如果不指定numElements参数,Deque底层数组的长度为16

        下面程序示范了把ArrayDeque当成“栈”来使用。

        上面程序的运行结果显示了ArrayDeque作为栈的行为,因此当程序中需要使用“栈”这种数据结构时,推荐使用ArrayDeque,尽量避免使用Stack——因为Stack是古老的集合,性能较差。
        当然ArrayDeque也可以当成队列使用,此处ArrayDeque将按“先进先出”的方式操作集合元素。例如如下程序。

        上面程序的运行结果显示了ArrayDeque作为队列的行为。
        通过上面两个程序可以看出,ArrayDeque不仅可以作为栈使用,也可以作为队列使用

  (3) LinkedList 实现类

        LinkedList类是List 接口的实现类——这意味着它是一个List集合,可以根据索引来随机访问集合中的元素。除此之外,LinkedList还实现了Deque接口,可以被当成双端队列来使用,因此既可以被当成“栈”来使用,也可以当成队列使用。下面程序简单示范了LinkedList集合的用法。

        上面程序中粗体字代码分别示范了LinkedList作为 List集合、双端队列、栈的用法。由此可见,LinkedList是一个功能非常强大的集合类。
        LinkedList与 ArrayList、ArrayDeque 的实现机制完全不同,ArrayList、ArrayDeque内部以数组的形式来保存集合中的元素,因此随机访问集合元素时有较好的性能;而LinkedList 内部以链表的形式来保存集合中的元素,因此随机访问集合元素时性能较差,但在插入、删除元素时性能比较出色(只需改变指针所指的地址即可)。需要指出的是,虽然Vector也是以数组的形式来存储集合元素的,但因为它实现了线程同步功能(而且实现机制也不好),所以各方面性能都比较差。

  (4) 各种线性表的性能分析

        Java提供的List就是一个线性表接口,而ArrayList、LinkedList 又是线性表的两种典型实现:基于数组的线性表和基于链的线性表。Queue 代表了队列,Deque代表了双端队列(既可作为队列使用,也可作为栈使用),接下来对各种实现类的性能进行分析。
        初学者可以无须理会ArrayList 和 LinkedList 之间的性能差异,只需要知道LinkedList 集合不仅提供了List 的功能,还提供了双端队列、栈的功能就行。但对于一个成熟的Java程序员,在一些性能非常敏感的地方,可能需要慎重选择哪个List实现。
        一般来说,由于数组以一块连续内存区来保存所有的数组元素,所以数组在随机访问时性能最好,所有的内部以数组作为底层实现的集合在随机访问时性能都比较好;而内部以链表作为底层实现的集合在执行插入、删除操作时有较好的性能。但总体来说,ArrayList的性能比LinkedList的性能要好,因此大部分时候都应该考虑使用ArrayList
        关于使用List集合有如下建议。

  • 如果需要遍历List集合元素对于ArrayList、Vector集合,应该使用随机访问方法(get)来遍历集合元素,这样性能更好;对于LinkedList 集合,则应该采用迭代器(Iterator)来遍历集合元素
  • 如果需要经常执行插入、删除操作来改变包含大量数据的 List 集合的大小,可考虑使用
    LinkedList集合
    。使用ArrayList、Vector集合可能需要经常重新分配内部数组的大小,效果可能较差。
  • 如果有多个线程需要同时访问List集合中的元素,开发者可考虑使用Collections将集合包装成线程安全的集合

2. Java8增强的Map集合

        Map用于保存具有映射关系的数据,因此Map集合里保存着两组值,一组值用于保存Map里的 key,另外一组值用于保存Map里的valuekey和 value都可以是任何引用类型的数据Map的key不允许重复,即同一个Map对象的任何两个key通过equals方法比较总是返回false。
        key和 value之间存在单向一对一关系,即通过指定的key,总能找到唯一的、确定的value。从 Map中取出数据时,只要给出指定的key,就可以取出对应的value。如果把 Map的两组值拆开来看,Map里的数据有如图8.7所示的结构。


        从图8.7 中可以看出,如果把Map里的所有key放在一起来看,它们就组成了一个Set集合(所有的key没有顺序,key与 key之间不能重复)实际上Map确实包含了一个keySet()方法,用于返回Map里所有key组成的Set集合
        不仅如此,Map里key集和Set集合里元素的存储形式也很像,Map子类和Set子类在名字上也惊人地相似,比如Set接口下有HashSet、LinkedHashSet、SortedSet(接口)、TreeSet、EnumSet等子接口和实现类,而Map接口下则有HashMap、LinkedHashMap、SortedMap(接口)、TreeMap、EnumMap等子接口和实现类。正如它们的名字所暗示的,Map的这些实现类和子接口中 key集的存储形式和对应Set集合中元素的存储形式完全相同

        如果把Map里的所有value放在一起来看,它们又非常类似于一个List:元素与元素之间可以重复,每个元素可以根据索引来查找只是Map中的索引不再使用整数值,而是以另一个对象作为索引。如果需要从List集合中取出元素,则需要提供该元素的数字索引;如果需要从Map中取出元素,则需要提供该元素的 key索引。因此,Map有时也被称为字典,或关联数组。Map接口中定义了如下常用的方法。

  • void clear():删除该Map对象中的所有key-value对
  • boolean containsKey(Object key):查询Map中是否包含指定的key,如果包含则返回true。
  • boolean containsValue(Object value):查询Map中是否包含一个或多个value,如果包含则返回true。
  • Set entrySet():返回Map中包含的key-value对所组成的Set集合,每个集合元素都是Map.Entry(Entry是 Map的内部类)对象
  • Object get(Object key):返回指定 key所对应的value;如果此Map中不包含该key,则返回null
  • boolean isEmpty():查询该Map是否为空(即不包含任何key-value对),如果为空则返回true。
  • Set keySet():返回该Map 中所有key组成的Set集合。
  • Object put(Object key, Object value):添加一个key-value对,如果当前Map中已有一个与该key相等的key-value对,则新的key-value对会覆盖原来的key-value对,并且返回被覆盖的value。
  • void putAll(Map m):将指定Map中的key-value对复制到本Map 中。
  • Object remove(Object key):删除指定key所对应的key-value对,返回被删除key所关联的value,如果该key不存在,则返回null。
  • boolean remove(Object key, Object value):这是 Java8新增的方法,删除指定key、value所对应的 key-value对。如果从该Map中成功地删除该key-value对,该方法返回true,否则返回false。
  • int size():返回该Map里的key-value对的个数。
  • Collection values():返回该Map里所有value组成的Collection。

Map接口提供了大量的实现类,典型实现如HashMap和 Hashtable 等、HashMap 的子类LinkedHashMap,还有SortedMap子接口及该接口的实现类TreeMap,以及WeakHashMap 、IdentityHashMap等。下面将详细介绍Map接口实现类。
        Map中包括一个内部类Entry,该类封装了一个key-value对。Entry包含如下三个方法。

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

        Map集合最典型的用法就是成对地添加、删除 key-value对,接下来即可判断该Map中是否包含指定key,是否包含指定value,也可以通过Map提供的 keySet()方法获取所有key组成的集合,进而遍历Map中所有的key-value对。下面程序示范了Map的基本功能。

        上面程序中前5行粗体字代码示范了向Map中成对地添加key-value对。添加key-value对时,Map允许多个vlaue重复,但如果添加key-value对时Map中己有重复的key,那么新添加的value会覆盖该key原来对应的value,该方法将会返回被覆盖的 value
        程序接下来的2行粗体字代码分别判断了Map集合中是否包含指定key、指定value。程序中粗体字foreach循环用于遍历Map集合:程序先调用Map集合的keySet()获取所有的 key,然后使用foreach循环来遍历Map的所有key,根据key即可遍历所有的 value
        HashMap重写了toString()方法,实际上所有的Map实现类都重写了toString()方法,调用Map对象的toString()方法总是返回如下格式的字符串:{key1=value1,key2=value2...}。

  (1) Java 8为Map新增的方法

        Java8除为Map增加了remove(Object key , Object value)默认方法之外,还增加了如下方法。

  • Object compute(Object key,BiFunction remappingFunction):该方法使用remappingFunction根据原key-value对计算一个新value只要新value不为null就使用新value覆盖原value;如果原value不为 null,但新value为null则删除原key-value对;如果原value、新value同时为null,那么该方法不改变任何key-value对,直接返回null
  • Object computelfAbsent(Object key,Function mappingFunction):如果传给该方法的 key参数在Map中对应的value为null,则使用mappingFunction根据key计算一个新的结果,如果计算结果不为null,则用计算结果覆盖原有的value。如果原Map原来不包括该key,那么该方法可能会添加一组key-value对。
  • Object computeIfPresent(Object key, BiFunction remappingFunction):如果传给该方法的 key参数在 Map中对应的value不为null,该方法将使用remappingFunction根据原key、value计算一个新的结果,如果计算结果不为null,则使用该结果覆盖原来的value;如果计算结果为null,则删除原key-value对。
  • void forEach(BiConsumer action):该方法是Java 8为Map新增的一个遍历key-value对的方法,通过该方法可以更简洁地遍历Map 的key-value对。
  • Object getOrDefault(Object key, V defaultValue):获取指定key对应的value。如果该key 不存在,则返回defaultValue.
  • Object merge(Object key, Object value,BiFunction remappingFunction):该方法会先根据key参数获取该Map中对应的value。如果获取的value为null,则直接用传入的value覆盖原有的 value(在这种情况下,可能要添加一组 key-value对);如果获取的 value 不为 null,则使用remappingFunction函数根据原value、新value计算一个新的结果,并用得到的结果去覆盖原有的 value。
  • Object putIfAbsent(Object key, Object value):该方法会自动检测指定key对应的value是否为null,如果该key对应的value为null,该方法将会用新value代替原来的null 值。
  • Object replace(Object key, Object value):将 Map中指定 key对应的value替换成新value。与传统put()方法不同的是,该方法不可能添加新的 key-value对。如果尝试替换的 key在原Map中不存在,该方法不会添加 key-value对,而是返回null
  • boolean replace(K key, V oldValue, V newValue):将Map中指定key-value对的原value替换成新value。如果在Map中找到指定的key-value对,则执行替换并返回true,否则返回false。
  • replaceAll(BiFunction function):该方法使用BiFunction对原key-value对执行计算,并将计算结果作为该key-value对的 value值。

下面程序示范了Map常用默认方法的功能和用法。

        上面程序中注释已经写得很清楚了,而且给出了每个方法的运行结果,读者可以结合这些方法的介绍文档来阅读该程序,从而掌握 Map中这些默认方法的功能与用法。

  (2) Java 8改进的HashMap和 Hashtable实现类

        HashMap和 Hashtable都是Map接口的典型实现类,它们之间的关系完全类似于ArrayList和Vector的关系: Hashtable是一个古老的Map实现类,它从JDK 1.0起就已经出现了,当它出现时,Java还没有提供Map接口,所以它包含了两个烦琐的方法,即 elements()(类似于Map接口定义的values()方法)和keys()(类似于Map接口定义的keySet()方法),现在很少使用这两个方法(关于这两个方法的用法请参考8.9节)。
        Java8改进了HashMap 的实现,使用HashMap存在key冲突时依然具有较好的性能。

        除此之外,Hashtable和 HashMap存在两点典型区别。

  • Hashtable是一个线程安全的Map实现,但HashMap是线程不安全的实现,所以 HashMap 比Hashtable的性能高一点;但如果有多个线程访问同一个Map对象时,使用Hashtable实现类会更好。
  • Hashtable不允许使用null 作为 key和 value,如果试图把 null值放进Hashtable中,将会引发NullPointerException异常;但 HashMap可以使用null作为key或 value。

        由于HashMap里的 key不能重复,所以HashMap里最多只有一个key-value对的key为null,但可以有无数多个.key-value对的 value为null。下面程序示范了用null值作为HashMap的 key和value的情形。

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

        为了成功地在HashMap、Hashtable中存储、获取对象,用作 key的对象必须实现 hashCode()方法和equals()方法。
        与HashSet集合不能保证元素的顺序一样,HashMap、Hashtable也不能保证其中 key-value对的顺序。类似于HashSet,HashMap、Hashtable判断两个key相等的标准也是:两个 key通过 equals()方法比较返回true,两个key 的hashCode值也相等
        除此之外,HashMap、Hashtable中还包含一个containsValue()方法,用于判断是否包含指定的value。那么HashMap、Hashtable如何判断两个value相等呢?HashMap、Hashtable判断两个value相等的标准更简单:只要两个对象通过equals()方法比较返回true即可。下面程序示范了Hashtable判断两个key相等的标准和两个value 相等的标准。

        上面程序定义了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()方法比较返回true,且 hashCode值相等,Hashtable即认为它们是同一个key。类似的是,程序在③处也可以删除对应的key-value对。

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

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

  (3) LinkedHashMap 实现类

        HashSet有一个LinkedHashSet子类,HashMap也有一个LinkedHashMap子类;LinkedHashMap也使用双向链表来维护key-value对的次序(其实只需要考虑key 的次序),该链表负责维护Map的迭代顺序,迭代顺序与key-value对的插入顺序保持一致
        LinkedHashMap可以避免对HashMap、Hashtable里的 key-value对进行排序(只要插入key-value对时保持顺序即可),同时又可避免使用TreeMap所增加的成本。
        LinkedHashMap需要维护元素的插入顺序,因此性能略低于HashMap 的性能;但因为它以链表来维护内部顺序,所以在迭代访问Map里的全部元素时将有较好的性能。下面程序示范了LinkedHashMap的功能:迭代输出LinkedHashMap 的元素时,将会按添加key-value对的顺序输出。

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

  

猜你喜欢

转载自blog.csdn.net/indeedes/article/details/121058947
今日推荐