1.集合接口
1.1集合的接口与实现分离
Java SE 5.0开始,集合类是带有类型参数的泛型类。
队列通常有两种实现方式:使用循环数组(SE 6中ArrayDeque类);使用链表(LinkedList类)。
一旦构建了集合,就不需要知道究竟使用了哪种实现。
//可使用接口类型存放集合的引用:
Queue<Customer> expressLane = new CircularArrayQueue<Customer>(100);
循环数组要比链表更高效,多数人优先选择循环数组。但循环数组容量有限,若要收集的对象数量没有上限,最好使用链表来实现。
1.2Java类库中的集合接口和迭代器接口
Java类库中,集合类的基本接口是Collection接口。
该接口基本方法:
public interface Collection<E>{
boolean add(E element);
Iterator<E> iterator();//返回实现Iterator接口的对象,可依次访问元素
...
}
1.2.1迭代器
Iterator接口包含3个基本方法:
public interface Iterator<E>{
E next();
boolean hasNext();
void remove();//删除上次访问的对象
}
//调用next时,迭代器越过下一个元素,并返回刚刚越过的那个元素的引用。
“for each”循环可与任何实现了Iterable接口的对象一起工作,该接口仅有一个方法:
public interface Iterable<E>{
Iterator<E> iterator();
}
Collection接口扩展了Iterable接口。
1.2.2删除元素
Iterator接口的remove方法将会删除上次调用next方法时返回的元素。
//删除字符串集合中第一个元素:
Iterator<String> it = c.iterator();
it.next();//越过第一个元素
it.remove();//删除第一个元素
!!如果调用remove之前没有调用next是不合法的,会抛出IllegalStateException异常
1.2.3泛型实用方法
2.具体的集合
2.1链表
Java中所有链表都是双向链接的。
List<String> staff = new LinkedList<String>();
staff.add("Amy");
staff.add("Bob");
staff.add("Carl");
Iterator iter = staff.iterator();
String first = iter.next();//访问第一个元素"Amy"
String second = iter.next();//访问第二个元素"Bob"
iter.remove();//删除上次访问的元素"Bob"
链表与泛型集合间有一个重要区别,链表是一个有序集合。
只有对自然有序的集合使用迭代器添加元素(依赖于位置的add方法)才有实际意义。
无序元素集合(例如:set集).
使用子接口ListIterator,包含add方法:
interface ListIterator<E> extends Iterator<E>{
void add(E element);//不返回boolean类型值,在迭代器位置之前添加一个元素
E previous();//反向遍历链表,类似next
boolean hasPrevious();
...
}
LinkedList类的listIterator方法返回实现了ListIterator接口的迭代器对象:
ListIterator<String> iter = staff.listIterator();
当用一个刚刚由Iterator方法返回,并指向链表表头的迭代器调用add操作时,新添加的元素将变成新表头。当迭代器越过链表的最后一个元素时(hasNext()返回false),添加的元素将变成列表的新表尾。如果链表有n个元素,则有n+1个位置可以添加新元素。
链表迭代器若发现它的集合被另一个迭代器修改了,或被该集合自身的方法修改了,就会抛出一个ConcurrentModificationException异常。
List<String> list = ...;
ListIterator<String> iter1 = list.listIterator();
ListIterator<String> iter2 = list.listIterator();
iter1.next();
iter1.remove();
iter2.next();//throws ConcurrentModificationException
!!为了避免发生并发修改的异常,可遵循以下简单规则:根据需要给容器附加许多只能读取列表的迭代器,
另外,再单独附加一个既能读又能写的迭代器。
//链表只负责跟踪对列表的结构性修改(例:添加、删除元素),set操作不被视为结构性修改。
for(int i=0;i<list.size();i++)
list.get(i);
//每次查找一个元素都要从列表头部重新开始,没有缓存,效率极低!!
//get方法优化:索引大于size()/2时,从列表尾端开始搜索元素
2.2数组列表
List接口用于描述一个有序集合,并且集合中每个元素的位置十分重要。
2种访问元素协议:
(1)迭代器
(2)get()和set()方法随机访问元素(不适用于链表,对数组很有用)
ArrayList类实现了List接口,封装了一个动态再分配的对象数组。
//不需要同步时使用ArrayList,而不要使用Vector.
2.3散列集
若不在意元素的顺序,可以有几种能够快速查找元素的数据结构。
缺点:无法控制元素出现的次序。他们按照有利于操作目的的原则组织数据。
散列表(hash table)数据结构可以快速的查找所需要的对象。散列表为每个对象计算一个整数,称为散列码。
自定义类要实现这个类的hashCode方法,且应与equals方法兼容(a.equals(b)为true,a与b有相同散列码)。
HashSet():构造一个空散列表
HashSet(Collection<? extends E> elements):构造一个散列集,并将集合中的所有元素添加到散列集中;
HashSet(int initialCapacity):构造一个空的具有指定容量(桶数)的散列集。
HashSet(int initialCapacity,float loadFactor):构造一个指定容量和装填因子(0.0-1.0间的数值,
当大于这个百分比时,散列表进行再散列)的空散列集。
2.4树集
TreeSet类比散列集有所改进。树集是一个有序集合,对集合遍历时,每个值将自动按照排序后的顺序呈现。
排序是用树结构完成的(当前是红黑树)。迭代器总是以排好序的顺序访问每个元素。
SortedSet<String> sorter = new TreeSet<String>();
sorter.add("Bob");
sorter.add("Amy");
sorter.add("Carl");
for(String s:sorter)
System.out.println(s);
//打印顺序:Amy->Bob->Carl
将一个元素添加到树中要比添加到散列表中慢,但与将元素添加到数组或链表的正确位置相比还是快很多。
若树中包含n个元素,查找新元素位置比较次数:log2(n);
2.5对象的比较
SortedSet<Item> sortByDescription = new TreeSet<Item>(
new Comparator<Item>(){ //不是Comparable接口
public int compare(Item a,Item b){
String descrA = a.getDescription();
String descrB = b.getDescription();
return descrA.compareTo(descrB);
}
}
);
树的排序是整体排序,任意两个元素必须是可比的。
Java SE 6起,TreeSet类实现了NavigableSet接口。接口增加了几个便于定位元素及反向遍历的方法。
2.6队列与双端队列
队列可在尾部添加元素,头部删除元素;
双端队列可在头部和尾部同时添加或删除元素。
Java SE 6中引入Deque接口,由ArrayDeque和LinkedList类实现。
2.7优先级队列
优先级队列使用了一个优雅且高效的数据结构,称为“堆”。堆是一个可以自我调整的二叉树。
与TreeSet一样,一个优先级队列既可以保存实现了Comparable接口的类对象,也可保存在构造器中提供比较器的对象。
使用优先级队列的典型示例是任务调度。每一个任务有一个优先级,任务以随机顺序添加到队列中。每启动一个新任务,都将优先级最高的任务从队列删除。
2.8映射表
映射表用来存放键/值对。
Java类库为映射表提供了两个通用的实现:HashMap和TreeMap。两个类都实现了Map接口。
散列映射表(HashMap):对键进行散列;
树映射表(TreeMap):用键的整体顺序对元素进行排序,并将其组织成搜索树。
键必须是唯一的。若对同一个键两次调用put方法,第二个值就会取代第一个值。实际上,put将返回用这个键参数存储的上一个值。
集合框架并没有将映射表本身视为一个集合。然而,可以获得映射表的视图。
3个视图:
键集:Set<K> keySet();
值集合:Collection<K> values();
键/值对集:Set<Map.Entry<K,V>> entrySet();//条目集的元素是静态内部类Map.Entry的对象
//枚举映射表中所有键
Set<String> sets = map.keySet();
for(String key:keys){
...
}
//枚举各个条目,同时查看键与值
for(Map.Entry<String,Employee> entry:staff.entrySet()){
String key = entry.getKey();
Employee value = entry.getValue();
...
}
2.9专用集与映射表类
2.9.1弱散列映射表
2.9.2链接散列集和链接映射表
2.9.3枚举集与枚举映射表
2.9.4标识散列映射表
3.集合框架
框架是一个类的集,包含很多超类。Java集合类库构成了集合类的框架。
Java SE 1.4引入了一个标记接口RandomAccess.接口没有任何方法,可用来检测集合是否支持高效的随机访问:
if(c instanceof RandomAccess){
...
}
else{
...
}
//ArrayList类和Vector类都实现了RandomAccess接口。
有关Set接口:集的equals方法定义两个集相等的条件是它们包含相同的元素但顺序不必相同。
3.1视图与包装器
通过使用视图,可以获得其他的实现了集合接口和映射表接口的对象。
3.1.1轻量级集包装器
Arrays类的静态方法asList将返回一个包装了普通Java数组的List包装器。
这个方法可以将数组传递给期望得到列表或集合变元的方法,如:
Card[] cardDeck = new Card[52];
List<Card> cardList = Arrays.asList(cardDeck);
//返回的对象不是ArrayList,而是视图对象
//返回实现List接口的不可修改的对象
List<String> settings = Collections.nCopies(100,"DEFAULT");//Collections类
//创建一个包含100个字符串的List,每个串都被设置为"DEFAULT"
3.1.2子范围
可为很多集合建立子范围视图
//取出第10-19个元素:
List group2 = staff.subList(10,20);
group2.clear();//删除整个子范围
//对于有序集和映射表,可以使用排序顺序而不是元素位置建立子范围
SortedSet接口声明方法:
SortedSet<E> subSet(E from,E to);
SortedSet<E> headSet(E to);
SortedSet<E> tailSet(E from);
Java SE 6引入的NavigableSet接口声明方法:可指定是否包括边界
NavigableSet<E> subSet(E from,boolean fromInclusive,E to,boolean toInclusive);
NavigableSet<E> headSet(E to,boolean toInclusive);
NavigableSet<E> tailSet(E from,boolean fromInclusive);
SortedMap接口方法:
SortedMap<K,V> subMap(K from,K to);
SortedMap<K,V> headMap(K to);
SortedMap<K,V> tailMap(K from);
Java SE 6引入的NavigableMap接口声明方法:可指定是否包括边界
NavigableMap<K,V> subMap(K from,boolean fromInclusive,K to,boolean toInclusive);
NavigableMap<K,V> headMap(K to,boolean toInclusive);
NavigableMap<K,V> tailMap(K from,boolean fromInclusive);
3.1.3不可修改的视图
不可修改的视图并不是集合本身不可修改。
3.1.4同步视图
若由多个线程访问集合,就必须确保集不会被意外的破坏。
类库设计者使用视图机制来确保常规集合的线程安全,而不是实现线程安全的集合类。
//静态synchronizedMap方法将任何一个映射表转换成具有同步访问方法的Map:
Map<String,Employee> map = Collections.synchronizedMap(new HashMap<String,Employee>());
3.1.5被检验视图
用来对泛型类型发生问题时提供调试支持。
ArrayList<String> strings = new ArrayList<String>;
ArrayList rawList = strings;//只产生警告
rawList.add(new Date());//成功添加一个Date对象,运行时检测不到错误
//安全列表,add方法检测插入的对象是否属于给定的类
List<String> safeStrings = Collections.checkedList(strings,String.class);
ArrayList rawList = safeStrings;
rawList.add(new Date());//抛出ClassCastException异常
3.2批操作
使用类库中的批操作避免频繁的使用迭代器。
//找出两个集的交
Set<String> result = new HashSet<String>(a);
result.retainAll(b);
//批操作应用于视图,删除指定员工
Map<String,Employee> staffMap = ...;
Set<String> terminatedIDs = ...;//待删除员工的ID集
staffMap.removeAll(terminatedIDs);
3.3集合与数组间的转换
String[] values = ...;
HashSet<String> staff = new HashSet<String>(Arrays.asList(values));//转换为集合
Object[] values = staff.toArray();//产生对象数组,不能类型转换
String[] values = staff.toArray(new String[0]);//返回的数组与所创建数组一样
values = staff.toArray(new String[staff.size()]);//指定数组大小
4.算法
泛型集合接口有一个很大的优点,即:算法只需要实现一次。
4.1排序与混排
Collections类中的sort方法可对实现了List接口的集合进行排序:
List<String> staff = new LinkedList<String>();
Collections.sort(staff);//假定列表元素实现了Comparable接口
Collections.sort(staff,Collections.reverseOrder());//静态方法返回比较器,降序排序
Java中,将列表的所有元素转入一个数组,并使用一种归并排序的变体对数组进行排序,然后将排序后的序列复制回列表。时间复杂度:O(nlog2(n)).
集合类库中使用的归并排序算法比快速排序慢一些,但有一个主要的优点:稳定(不需要交换相同的元素)。
Collections类有一个算法shuffle,可以随机的混排列表中元素的顺序:
时间复杂度:O(n*a(n)).a(n)是访问元素的平均时间
ArrayList<Card> cards = ...;
Collections.shuffle(cards);
//如果提供的列表没有实现RandomAccess接口,shuffle方法将元素复制到数组中,然后打乱数组中元素的顺序,
最后将打乱顺序后的元素复制回列表。
4.2二分查找
Collections类的binarySearch方法实现了这个算法。集合(实现List接口)必须是排好序的。
i = Collections.binarySearch(c,element);//c:集合;element:待查找元素
i = Collections.binarySearch(c,element,comparator);//comparator:比较器
//返回正值或0:匹配对象的索引
//返回负值:没有匹配的元素。但可利用其确定插入新元素element的位置(-i-1),保持有序性
只有采用随机访问,二分查找才有意义。如果为binarySearch(检查列表参数是否实现RandomAccess接口,实现则采用二分查找,否则线性查找)方法提供一个链表,它将自动变为线性查找。
4.3简单算法
4.4编写自己的算法
尽可能的使用接口,而不要使用具体的实现。
例:
void fillMenu(JMenu menu,ArrayList<JMenuItem> items){
for(JMenuItem item:items){
menu.addItem(item);
}
}
//调用程序必须在ArrayList中提供选项
void fillMenu(JMenu menu,Collection<JMenuItem> items){
for(JMenuItem item:items){
menu.addItem(item);
}
}
//可以用ArrayList或LinkedList,甚至Arrays.asList包装器包装的数组调用这个方法