Java集合总结
如何使用Java类库设计传统的数据结构
Java集合类库将接口和实现分离。Java类库中也有以Abstract开头的类,如AbstractQueue,这些类是为类库实现者而设计的。
集合类的基本接口是Collection。
for each循环可以与任何实现了Iterable接口的对象一起工作
元素访问的顺序取决于集合类型
Java中链表实际上都是双向链表(doubly linked),每个结点还存储着前驱结点的引用
使用链表的唯一理由是尽可能减少在列表中插入、删除元素的代价
ListIterator是Iterator的子接口,用在List及子类中,描述集合的位置,用add()可以添加元素,previous()、hasPrevious()可以向前遍历List,next()、hasNext()可以向后遍历List
ListIterator的add()在迭代器位置之前添加一个元素。
多次调用add(),元素会依次添加到ListIterator当前位置之前
ListIterator的remove(),在next()调用之后会删除迭代器位置左侧的元素,在previous()调用之后会删除迭代器位置右侧的元素,且不能连续调用两次
ListIterator的set()方法会用一个新元素取代用next或previous方法返回的上一个元素
一个迭代器在修改集合时,另外的迭代器读写改集合会发生ConcurrentModificationException异常。一个简单的方法可以检测到并发修改的问题,集合可以跟踪改写操作的次数,每个迭代器都维护一个独立的计数值。在每个迭代器方法的开始处检查自己改写操作的计数值是否与集合的改写操作的计数值是否一致
ArrayList类不是同步的,Vector类是同步的
HashSet
HashSet可以快速查找元素,但无法控制元素的顺序
散列表为每个对象计算一个散列码(hash code),由对象的实例域产生的一个整数
Java中,散列表用链表数组实现,每个链表成为桶(bucket)。要表中对象的位置,需要先计算对象的散列码,对桶数取余,得到的结果就是这个对象桶数的索引。如果该桶没有其他元素,直接插入桶中就行。如果桶被占满,这种现象被成为散列冲突(hash collision)。这时就需要与桶中所有对象进行比较,查看该对象是否已经存在。如果散列码是合理且随机分布的,桶数足够大,需要比较的次数就会少。
TreeSet是个有序集合。可以以任意顺序将元素插入到集合中,对集合进行遍历时排序,当前使用红黑树这中树结构进行排序。迭代器总是以排好序的顺序访问每个元素
要使用TreeSet,必须能够比较元素
队列可以在尾部添加元素,在头部删除元素。双端队列可以在头部或尾部同时添加删除元素,不支持在队列中间添加元素。有Deque(double ended queue的缩写)接口,其有ArrayDeque、LinkedList类
优先级队列(PriorityQueue)可以按任意顺序插入,总是按排序顺序进行检索。使用的场景是任务调度,优先级最高的排在前。
优先级队列并没有对所有元素进行排序,而是使用了数据结构堆(heap),堆是一个可以自我调整的二叉树,执行添加、删除,最小元素会移动到根部
映射数据结构用来存放键值对
HashMap和TreeMap都实现了Map接口,HashMap快一些,对键进行了散列
更新映射项,当键不存在且首次更新时的解决方法如下:
1.counts.put(word, counts.getOrDefault(word, 0) + 1)
2.counts.putIfAbsent(word, 0);
counts.put(word, counts.get(word) + 1)
3.counts.merge(word, 1, Integer::sum)
映射视图的keySet是不是HashSet或TreeSet,而是实现了Set接口的其他类,可以像集合一样使用KeySet。对KeySet的迭代器调用remove()方法,会删除映射中的键和值
LinkedHashSet与LinkedHashMap可以记住元素项的顺序,当元素插入到表中,就会并入双向链表中
视图
Java中集合相关的接口和类有很多,除了我们常用的,还有很多实现了Collection、Map接口的类做视图(View)类用。如Map的keySet()方法返回的KeySet类。视图类可以分为以下几种:
- 集合包装器
- 子范围(subrange)视图
- 同步视图
- 受查视图
集合包装器
如Arrays.asList()返回List包装器,可以访问底层数组,可以调用get()和set(),但不能改变数组大小
子范围(subrange)视图
如List.subList(10, 20)来获得一个子范围视图。可以将任何操作作用于子范围视图,并且能自动地反映个整个列表的情况。比如删除整个子范围,则元素自动从原集合中删除
对于有序集和映射,使用排列顺序而不是元素位置建立子范围。
不可修改视图(unmodifiable views)
对现有集合增加了运行时检查,如果发现对集合进行修改,就会抛出异常。
不可修改视图并不是集合本身不可修改,仍可通过集合的原始引用对集合进行修改。
由于视图只是包装了接口而不是实际的集合对象,只能访问接口中定义的方法
同步视图
当多个线程访问集合,就必须确保集合不会被破坏。
类设计者使用视图机制来确保常规集合的线程安全,而不是实现线程安全的集合类。如Collections.synchronizedMap()可以将任何一个Map转成具有同步方法的Map
受查视图
对泛型类型发生问题时提供调试支持。
生成受查视图
ArrayList<String> strings = new ArrayList(); List<String> strs = Collections.checkedList(strings, String.class);
此时的视图的add()将检测插入的对象是否属于给定的类,如果不属于,则抛出ClassCastException异常
算法
使用泛型集合接口作为方法的参数,带来的好处是算法只需要实现一次。比如计算集合最大元素的这个问题,可以创建一个方法接收Collection接口,然后使用迭代器迭代元素进行比较,如下,这样就可以使用这个方法来计算链表、数组列表最大元素了。
public static void max(Collection<T> c) { }
排序算法
使用Collections排序
Collections.sort(List<T> list) Collections.sort(List<T> list, Comparator<? super T> c)
List接口排序,Java8引入
List.sort(Comparator<? super E> c)
ArrayList排序
ArrayList.sort(Comparator<? super E> c)
Java中sort()方法是将所有元素转入一个数组,对数据进行排序,再将数组复制回表。
sort()使用的是归并排序,不是快排。
传递给sort()的List必须是可修改的,即要支持set()方法
二分查找算法
Collections类有两个方法实现了对List的二分法查找。List集需要先排好序,否则需要提供排序的Comparator。
Collections.binarySearch(List<? extends Comparable<? super T>> list, T key) Collections.binarySearch(List<? extends T> list, T key, Comparator<? super T> c)
如果binarySearch()返回的值大于等于0,标识匹配对象的索引。
如果返回负数,表示没有匹配的元素,但是可以计算出应该将element插入到集合的哪个位置,以保持集合的有序性。插入的位置是-i-1。
如果为binarySearch()提供了LinkedList,则改为线性查找。