9.1 Java集合框架
- 与现代的数据结构类库的常见情况一样,Java集合类库也将接口(interface)与实现(implements分离
- 队列通常有两种实现方式:一种是使用循环数组(ArrayDeque),一种是使用链表(LinkedList)
- Java类库没有名为CircularArrayQueue和LinkedListQueue的类。如果需要一个循环数组队列,可以使用ArrayDeque,如果需要链表队列,可以使用LinkedList
- 一旦创建类集合就不需要知道究竟使用类哪种实现,可以使用接口类型,存放集合的引用:
Queue<ustomer> expressLane = new ArrayDeque<>(100);
- 接口本身并不能说明哪种实现的效率究竟如何。循环数组要比链表更有效,因此多数人优先选择循环数组。循环数组是一个有界集合,即容量有限,如果程序中要收集数量没有上限,就最好使用链表来实现。
- Java API中有一组以Abstract开头的类,例如AbstractQueue。这些类是为类库实现者而设计的,如果想要实现自己的队列类,会发现扩展AbstractQueue类比实现Queue接口中的方法轻松的多。
- 在Java类库中,集合类的基本接口是Collection接口。这个接口,有两个基本方法:add、iterator:
public interface Collection<E> { boolean add(E element); Iterator<E> iterator();//用于返回一个迭代器对象,用于遍历集合 ... }
- 迭代器(Iterator):
- Iterator接口包含4个方法:
public interface Iterator<E> { E next(); //返回集合中元素的引用 boolean hasNext(); void remove(); defalut void foEachRemaining(Consumer<? super E> acting); }
- 需要在调用next()之前用hasNext()判断
Collection<String. c = ...; Iterator<String> iter = c.iterator(); while(iter.hasNext()) { String element = iter.next(); do something here. } //用fo_each循环可以更加简练的完成循环操作 for(String element : c) { do something with element }
- 编译器简单的将“for_each”循环翻译为带有迭代器的循环
- for_each循环可以与任何实现了Iterator接口的对象一起工作,这个接口只包含一个抽象方法(iterator()):
public interface Iterator<E> { Iterator<E> iterator(); ... }
- Collection接口扩展了Iterator接口,因此,对于标准类库中的所有集合都可以使用“for_each”循环
- 在JavaSE8中,甚至不需要写循环。可以调用forEachRemaining方法提供一个lambda表达式。将对迭代器中每一个元素调用这个lambda表达式。知道没有元素为止:
iterator.forEachRemaining(element -> do something with element)
- 元素访问顺序取决于集合类型。如果对ArrayList进行迭代,迭代器将从索引0开始,没迭代一次,索引值加一。然而如果访问HashSet中的元素,每个元素将会按照某种随机的次序出现
- 应该将Java迭代器认为是位于两个元素之间。当调用next时,迭代器就越过下一个元素,返回刚才越过元素的引用
- Iterator的remove方法将会删除上次调用next方法时返回的元素,如果需要删除制定为止上的元素,依然需要调用next,更重要的是,next和remove的调用具有依赖性,如果调用remove之前没有调用next将是不合法的
- Iterator接口包含4个方法:
- Collection接口中的方法(所有集合类必须实现):
int size() boolean isEmpty() boolean contains(Object obj) boolean containsAll(Collection<?> c) boolean equals(Object other) boolean addAll(Collection<? extends E> from) boolean remove(Object obj) boolean removeAll(Collection<?> c) void clear() boolean retainAll(Collection<?> c) Object[] toArray() T[] toArray(T[] arrayToFill) - Java集合框架为不同类型的集合定义的大量接口:
- Collection:
- Map:
- Collection:
- 实际中有两种有序集合,其性能开销有极大差距。由数组支持的有序集合可以快速的随机访问,因此适合使用List方法并提供一个整数索引来访问。与之不同,链表尽管也是有序的,但是访问很慢,所以最好使用迭代器来遍历。
- Java使用RandomAccess标记接口来标记一个集合能不能支持随机访问
- Set接口等同于Collection接口,不过其方法有更严谨的定义。Set的add方法不允许增加重复的元素
- SortedSet和SortedMap(接口)会提供用于排序的比较器对象,这两个接口定义来可以得到集合子集视图的方法
- NavigableSet和NavigableMap(接口)包含一些用于搜索和遍历有序集和映射的方法。
SortedSet(接口) ---> NavigableSet(接口) ----> TreeSet(类)
- TreeSet和TreeMap是有序的,HashSet和HashMap是无序的,HashSet与HashMap性能更高
9.2 具体的集合
- 除了以Map结尾的类外,其他类都实现类Collection接口,而以Map结尾的类实现了Map接口:
集合类型 简要描述 ArrayList 动态增长数组 LinkedList 链表队列,增删快 ArrayDeque 循环数组队列,查找快 HashSet 无序集合(元素无重复),基于map TreeSet 有序集合,基于红黑树 EnumSet 包含枚举类型值的集 LinkedListSet 可以记住插入顺序 PriorityQueue 优先队列 HashMap 基于哈希的映射 TreeMap 基于红黑树的Map(有序) EnumMap 键值是枚举类型的映射 LinkedListmap 可记住插入顺序的Map WeakHashMap 键值保留弱引用的映射 IdentityHashMap 键值严格相等(==)
链表
- 在Java程序设计语言中,所有的链表都是双向链表—>即每个节点还存放着指向前驱节点的引用
- 链表与泛型集合之间有一个重要的区别。链表是一个有序集合,每个对象的位置都十分重要。(列表迭代器:ListIterator:previous和hasPrevious用来反响遍历链表)
- 如果链表有n个元素,有n+1个位置可以添加新元素,这些位置与迭代器的n+1个可能的位置想对应
- set()方法用一个新元素取代调用next或previous方法返回的上一个元素。(set()不会出现迭代器兵法修改的问题)
- 如果迭代器发现它的集合被另一个迭代器修改了,或是被该集合自身的方法修改了,就回抛出一个ConcurrentModificationException
List<String> list =... ListIterator<String> iter1 = list.listIterator(); ListIterator<String> iter2 = list.listIterator(); iter1.next(); iter1.remove(); iter2.next(); //throws ConcurrentModificationException
- 为了避免发生兵法修改的异常,请遵循以下简单规则:可以根据需要给容齐附加很多迭代器,但是这些迭代器只能读取列表,另外,再单独附加一个既能读也能写的迭代器
有一种简单的方法可以检测到并发修改的问题。集合可以跟踪改写操作的次数。每个迭代器都维护了一个独立的计数值。在每个迭代器方法的开始处检查计数值是否一致。
- 为了避免发生兵法修改的异常,请遵循以下简单规则:可以根据需要给容齐附加很多迭代器,但是这些迭代器只能读取列表,另外,再单独附加一个既能读也能写的迭代器
- 在Java类库中,还提供了许多理论上存在一定争议的方法。链表不支持快速的随机访问。如果要查看链表的第n个元素,就必须从头开始,越过n-1个元素,效率极低。
- 使用链表的唯一理由是尽可能的减少在链表中间插入或删除元素所付出的代价。如果列表只有少数几个元素,就完全可以使用ArrayList
数组列表
- ArrayList封装了一个动态再分配的对象数组。Vector类的所有方法都是同步的,但是,如果一个线程访问Vector,代码要在同步操作上耗费大量时间。
散列集
- 如果不在意元素的顺序,可以有继红快速查找元素的数据结构。其缺点是无法控制元素出现的次序,它们将按照有利于其操作目的的原则组织数据。
- 散列表为每个对象计算一个整数,称为散列码(hashcode)
- 在Java1中,散列表用链表数组实现。每个列表被称为桶(bucket)。
- Java集合类库提供了一个HashSet类,它实现类基于散列表的集
树集
- 树集是一个有序集合。可以以任意顺序将元素插入到集合中。在对集合进行遍历时,每个值自动的按照排序后的顺序呈现
- 排序是使用树结构实现的(红黑树)
- 将一个元素添加到树中要比添加到散列表中慢:
单词总数 不同的单词个数 HashSet TreeSet 28195 5909 5s 7s 466300 37545 75s 98s - 要使用树集,必须能够比较元素。这些元素必须实现comparable接口,或者构造集时必须提供一个Comparator
- 从javaSE 6起,TreeSet实现类navigableSet接口。这个接口增加了几个便于定位元素以及反响遍历的方法
队列与双端队列
- 队列可以让人们有效的在尾部添加一个元素,在头部删除一个元素。有两个端头的队列,即双端队列,可以让人们有效的在头部和尾部同时添加和删除元素。不支持在队列中间添加元素
- ArrayDeque和LinkedList都提供了双端队列
优先级队列(PriortyQueue)
- 优先级队列中的元素可以按照任意的顺序插入,总是按照顺序进行检索。
- 优先级
队列使用了一个优雅且高效的数据结构,称为堆。堆是一个可以自我挑战者的二叉树。
9.3 映射
- Java类库为映射提供了两个通用的实现:HashMao和TreeMap,这两个类都实现类Map接口。
- 与集一样,散列稍微快一点,如果不需要排列顺序的访问问题,就最好选择散列
- 要检索一个对象,必须使用一个键。如果在映射中没有找到与给定键的信息,get将返回null
- 键必须是唯一的。如果对同一个键两次调用put方法,第而个值就回取代第一个值。
- 要迭代处理映射的键和值:
scores.forEach(k, v) -> System.out.println("key:" + k + ", value:" + v);
- 映射视图:
- 有3种视图:键集、值集、键/值集
Set<k> keySet() Collection<V> values() Set<map.Entry<K, V>> entrySet()
- 有3种视图:键集、值集、键/值集
- WeakHashMap
- 当对键的唯一引用来自散列表时,这一数据结构将与垃圾回收器协同工作一起删除这个键值对
- LinkedHashSet和LinkedHashMap用来记住插入元素项的顺序。这样就可以避免在散列中的项从表面上看是随机哦爱咧的
9.4 视图与包装器(没看,不想看)
9.5 算法
- 事实上,标准的C++类库已经有几十种非常有用的算法每个算法都是在泛型集合上操作的。Java类库的算法没有如此丰富,但是,也包含类基本的排序,二分查找等实用算法
- Collections的sort方法可以对实现了List接口的集合进行排序
- Collections的shuffle可以对集合进行混淆
- Collections的binarySearch可以对集合进行二分查找
- 集合与数组的转换:
- 数组转集合(Arrays.asList()):
String[] values = ... HashSet<String> staff = new HashSet<String>(Arrays.asList(values));
- 集合转数组(变体的toArray方法):
toArray返回的是Object数组String [] values = staff.toArray(new String[0])
- 数组转集合(Arrays.asList()):
9.6 遗留的集合
- Hashtable类:与HashMap作用相同,但是是线程安全的
- 枚举(基本不用)
- 属性映射
- 栈(Stack是类不是接口,含有push,pop,peek方法)
- 位集(BitSet):称为位向量或位数组更合适