13 集合
实现方法时,不同的数据结构会导致性能有很大差异。
13.1 集合接口
Java集合类库将接口(interface)与实现(implementation)分离。
可以使用接口类型存放集合的应用,一旦改变了想法,可以轻松额使用另外一种不同的实现。
List<Integer> l = new ArrayList<>();
若想改为链表实现,只需上句为List<Integer> l = new LinkedList<>();
以Abstract开头的类,如AbstractQueue,是为类库实现者设计的。
如果想要实现自己的队列类,会发现扩展AbstractQueue类要比实现Queue接口中的方法轻松的多。
集合类的基本接口是Collection接口
两个基本方法:
public interface Collection<E>
{boolean add(E element);Iterator<E> iterator();
}
iterator方法返回一个实现了Iterator接口的对象。
Iterator接口包含三个方法:
public interface Iterator<E>
{E next();boolean hasNext();void remove();
}
Java迭代器被认为是在两个元素之间的。
当调用next时,迭代器就越过下一个元素,并返回刚刚越过的那个元素的引用。
remove方法将会删除上次调用next方法时返回的元素。
如果调用remove之前没有调用next将是不合法的。
由于Collection与Iterator都是泛型接口,可以编写任何集合类型实用方法。
13.2 具体的集合
Map结尾的类实现了Map接口;其他实现了Collection接口。
ArrayList | 顺序表 |
LinkedList | 双向链表 |
ArrayDeque | 循环数组实现的双端队列 |
HashSet | 没有重复元素的无序集合 |
TreeSet | 有序集 |
EnumSet | 包含枚举类型值的集 |
LinkedHashSet | 可以记住元素插入顺序的集 |
PriorityQueue | 允许高效删除最小元素的集合 |
HashMap |
|
TreeMap |
|
EnumMap | 键值属于枚举类型 |
LinkedHashMap | 可以记住K/V的添加次序 |
WeakHashMap | 其值无用武之地后可以被垃圾回收器回收 |
IdentityHashMap | 用==而不是equals比较键值的映射表 |
LinkedList:
interface ListIterator<E> extends Iterator<E>
{void add(E element);E previous();boolean hasPrevious();
}
set方法用一个新元素取代调用next或previous方法返回的上一个元素。
ListIterator<String> iter = list.listIterator();
String oldValue = iter.next(); //returns first element
iter.set(newValue); //sets first element to newValue
当某个迭代器修改集合时,另一个迭代器对其进行遍历,一定会出现混乱的状况。
例如:一个迭代器指向另一个迭代器刚刚删除的元素前面,现在这个迭代器就是无效的,并且不应该再使用。
链表迭代器的设计使它能够检测到这种修改。
如果迭代器发现它的集合被另一个迭代器修改了,或是被该集合自身的方法修改了,就会抛出一个ConcurrentModificationException。
例如:
ListIterator<String> iter1 = list.listIterator();
ListIterator<String> iter2 = list.listIterator();
iter1.next();
iter1.remove();
iter2.next(); //throws ConcurrentModificationException
链表不支持随机访问,但支持get(int index)。
get方法的小优化:如果index > size() / 2 就从列表尾端开始搜索元素。
for (int i = 0; i < list.size(); ++i)do something with list.get(i);//效率极低。出现此代码说明用错数据结构。
nextIndex() previousIndex() 返回索引值
list.listIterator(n)返回一个迭代器,这个迭代器指向索引为n的元素前面的位置。
ArrayList:
适用于get和set方法。
Vector类的所有方法都是同步的,如果由一个线程访问Vector,代码要在同步操作上耗费大量的时间。
ArrayList方法不是同步的。
散列集:
散列表(hash table)
散列表为每个对象计算一个整数,称为散列码(hash code)。
散列码是由对象的实例域产生的一个整数。具有不同数据域的对象将产生不同的散列码。
散列码要能够快速的计算出来。
在Java中,散列表用链表数组实现。每个列表被称为桶(bucket)。
散列码与桶数的余数是元素的桶的索引。
散列冲突(hash collision)
将桶数设置为预计元素个数的75%~150%,最好将桶数设置为一个素数,以防键的聚集。
标准库使用的桶数是2的幂,默认值为16。为表大小提供的任何值都将被自动的转换为2的下一个幂。
如果散列表太满,就需要再散列(rehashed)。
创建一个双倍桶数的表,将所有元素插入到这个新表中,然后丢弃原来的表。
装填因子(load factor)决定何时对散列表进行再散列。
对大多数应用程序来说,装填因子为0.75是比较合理的。
散列集迭代器依此访问所有的桶,所以访问顺序是随机的。
HashSet
树集:
有序集合(sorted collection)
红黑树(red-black tree)
迭代器以排好序的顺序访问每个元素
添加元素到树中要比添加到散列表中慢,但是,与将元素添加到数组或链表中相比还是快很多的。
TreeSet
对象的比较:
默认情况下,树集假定插入的元素实现了Comparable接口。
public interface Comparable<T>
{int compareTo(T other);
}
a.compareTo(b); //相等返回0;a在前返回负值;b在前返回正值。
使用Comparable接口定义排序有局限性。
对于一个给定的类,只能够实现这个接口一次。
如果在一个集合中需要按照部件编号进行排序,在另一个集合中却要按照描述信息进行排序,该如何?
如果一个类没有实现Comparable接口,又该如何?
可通过将Comparator对象传递给TreeSet构造器来告诉树集使用不同的比较方法。
pubic interface Comparator<T>
{int compare(T a, T b);
}
如:
class ItemComparator implements Comparator<Item>
{public int compare(Item a, Item b){ return a.getDescription().compareTo(b.getDescription()); }
}
ItemComparator comp = new ItemComparator();
SortedSet<Item> sortByDescription = new TreeSet<>(comp);
比较器是比较方法的持有器,不含数据,将这种对象称为函数对象(function object)。
函数对象通常动态定义,即定义为匿名内部类的实例。
SortedSet<Item> sortByDescription = new TreeSet<>(new
Comparator<Item>()
{public int compare(Item a, Item b)return a.getDescription().compareTo(b.getDescription());
}
队列与双端队列:
ArrayDeque
优先级队列:
priority queue
可以按照任意的顺序插入,却总是按照排序的顺序进行检索。
即,无论何时调用remove方法,总会获得当前优先级队列中最小的元素。
优先级队列使用的是堆(heap)。
映射表:
Map key/value
HashMap TreeMap
三种视图:
Set<K> keySet()
Collection<K> values()
Set<Map.Entry<K, V>> entrySet()
弱散列映射表:
WeakHashMap
如果一个值,对应键的唯一引用来自散列表条目时,这一数据结构将与垃圾回收器协同工作一起删除键/值对。
连接散列集和连接映射表:
LinkedHashSet LinkedHashMap 用来记住插入元素项的顺序。
链接散列映射表将用访问顺序而不是插入顺序,对映射表条目进行迭代。
标识散列映射表:
IdentityHashMap
键的散列不是用hashCode函数来计算的,而是用System.identityHashCode方法计算。
根据对象的内存地址来计算散列码。
两个对象进行比较时,IdentityHashMap使用的是”==”。
在实现对象遍历算法(如对象序列化)时,这个类非常有用,可以用来跟踪每个对象的遍历状况。
13.3 集合框架
框架(framework)是一个类的集,它奠定了创建高级功能的基础。
框架包含很多超类,这些超类拥有非常有用的功能、策略和机制。
框架使用者创建的子类可以扩展超类的功能,而不必重新创建这些基本的机制。
例如Swing就是一种用户界面的机制。
Java集合类库构成了集合类的框架,它为集合的实现定义了大量的接口和抽象类,并且对其中的某些机制给予了描述,例如,迭代协议。
如果想要实现用于多种集合类型的泛型算法,或者想要增加新的集合类型,必须了解框架。
集合有两个基本接口:Collection和Map
List是一个有序集合(ordered collection)
标记接口RandomAccess,检测一个特定的集合是否支持随机检索。
集合接口有大量地方法,这些方法可以通过更基本的方法加以实现。
抽象类提供了许多这样的例行实现。
如果实现了自己的集合类,就可能要扩展上面某个类,以便可以选择例行操作的实现。
Java类库支持下面几种具体类:
视图与包装器:
keySet方法返回一个实现Set接口的类对象,这个类的方法对原映射表进行操作。
这种集合称为视图。
1、轻量级包装器
Arrays类的静态方法asList将返回一个包装了普通Java数组的List包装器。
Card[] cardDeck = new Card[52];
List<Card> cardList = Arrays.asList(cardDeck);
改变数组大小的所有方法都会抛出一个UnsupportedOperationException异常。
List<String> names = Arrays.asList(“Amy”, “Bob”, “Carl”);
这个方法调用Collections.nCopies(n, anObject)方法。
2、子范围视图
subList方法
List group2 = staff.subList(10, 20); //含首不含尾
可以将任何操作应用于子范围。
对于有序集和映射表,可以使用排序顺序建立子范围。
SortedSet接口声明了3个方法:
SortedSet<E> subSet(E from, E to);
SortedSet<E> headSet(E to);
SortedSet<E> tailSet(E from);
SortedMap<K, V> subSet(K from, K to);
SortedMap<K, V> headSet(K to);
SortedMap<K, V> tailSet(K from);
3、不可修改视图
Collections.unmodiffiableCollection
Collections.unmodifiableList
Collections.unmodifiableSet
Collections.unmodifiableSortedSet
Collections.unmodifiableMap
Collections.unmodifiableSortedMap
每个方法都定义于一个接口。
这些视图对现有集合增加了一个运行时的检查。
如果发现试图对集合进行修改,就抛出一个异常。
4、同步视图
如果由多个线程访问集合,就必须确保集不会被意外的破坏。
类库的设计者使用视图机制来确保常规集合的线程安全,而不是实现线程安全的集合类。
Map<String, Employee> map = Collections.synchronizedMap(new HashMap<String, Employee>());
5、检查视图
List<String> sateStrings = Collections.checkedList(strings, String.class);
视图的add方法将检测插入的对象是否属于给定的类。如果不是,则抛出ClassCastException。
批操作:
bulk operation
result.retainAll(a); //保存了交集
result.removeAll(b);
result.addAll(b);
集合与数组之间的转换:
数组->集合:Arrays.asList包装器
集合->数组:toArray()方法
staff.toArray();//返回的是Object数组,不能强制类型转换;
staff.toArray(new String[0]);//返回的是String数组;
staff.toArray(new String[staff.size()]); //将元素一次复制到新数组中,并返回新数组
13.4 算法
泛型集合接口有一个很大的优点,即算法只需要实现一次。
排序:
Collections类中的sort方法。
这个方法假定元素实现了Comparable接口。
如果想采用其他方式对列表进行排序,可将Comparator对象作为第二个参数传递给sort方法。
降序:Collections.reverseOrder()方法
Java直接将所有元素转入一个数组,并使用归并排序的变体对数组进行排序,然后将排序后的序列复制回列表。
集合类库中使用的归并排序算法比快速排序要慢,快速排序是通用排序算法的传统选择。
归并排序的优点,稳定,即不需要交换相同的元素。
排序的列表必须是可修改的,但不必是可以改变大小的。
·如果列表支持set,则是可修改的;
·如果列表支持add和remove方法,则是可改变大小的。
混乱:
Collections的shuffle算法,随机的混排列表中元素的顺序。
如果没有实现RandomAccess接口,shuffle方法将元素复制到数组中,然后打乱数组元素的顺序,最后再将打乱顺序后的元素复制回列表。
二分查找:
i = Collections.binarySearch(c, element);
i = Collections.binarySearch(c, element, comparator);
二分查找对顺序表有意义,如果对链表采用二分查找,则自动变为顺序查找。
编写自己的算法:
如果编写自己的算法,应该尽可能的使用接口,而不要使用具体的实现。
13.5 遗留的集合
Java程序设计语言自问世以来就存在的集合:Hashtable Properties Vector Stack BitSet
Hashtable:
与HashMap类的作用一样,拥有相同的接口。
Hashtable的方法也是同步的。
枚举:
使用Enumeration接口对元素序列进行遍历。
两个方法:hasMoreElements和nextElement。
静态方法Collections.enumeration将产生一个枚举对象,枚举集合中的元素。
属性映射表:
property map
·键和值都是字符串;
·表可以保存到一个文件中,也可以从文件中加载
·使用一个默认的辅助表。
实现属性映射表的类称为Properties。
通常用于程序的特殊配置选项。
栈:
stack
位集:
BitSet类提供了一个便于读取、设置或清除各个位的接口。
与C++中的bitset功能一样。