在Java类库的集合中,集合可以分为四个大种类,分别是列表(list),无重复元素列表(集合set),队列(queue),映射(map)。其中,前三种集合都实现了Collection接口,而映射实现了Map接口
下面是这些集合的具体类以及他们之间的继承层次
ArrayList:一种可以动态增长和缩减的索引序列
LinkedList:一种可以在任何位置高效的插入的删除操作的有序序列
ArrayDeque:一种用循环数组实现的双端队列
HashSet:一种没有重复元素的无序集合
TreeSet:一种有序集
EnumSet:一种包含枚举类型值的集
LinkedHashSet:一种可以记住元素插入次序的集
PriorityQueue:一种允许高效删除最小元素的集合
HashMap:一种存储键/值关联的数据结构
TreeMap:一种键值有序排列的顺序表
EnumMap:一种键值属于枚举类型的映射表
LinkedHashMap:一种可以记住键/值项添加次序的映射表
WeakHashMap:一种其值无用武之地后可以被垃圾回收器回收的映射表
IdentityHashMap:一种用==而不是equals比较键值的映射表
下面是他们之间的继承关系:
1.链表(LinkedList)
数组和数组列表有一个重大缺陷,就是从数组的中间位置插入或删除一个元素要付出很大代价,因为要移动大量元素,而使用链表则会解决这个问题,链表中的每个对象都存储在单独的结点中,每个结点中还存储着这个元素的前面和后面结点的引用,这样,在插入或者删除元素的时候只需要修改引用即可达到目的。因此改动集合的效率大大提高。但是,作为代价,链表的随机访问能力相当弱,进行随机访问会付出很大的代价。
我们都知道在Java中有Iterator接口,而这个接口是所有集合迭代器,所以只提供了最通用的方法,而这个接口的其中一个子接口叫做ListIterator就提供了有关顺序表的方法,其中就包括下面要说的add和remove方法。
在java中,使用add(Object)的方法在链表中添加对象,使用该链表对象调用此方法时是在此表后面添加对象,如果是该对象的迭代器使用此方法,就是在此迭代器的next()方法返回的元素后面添加对象,如果多次调用,则继续在新添加对象的后面再添加一个对象。使用remove()方法删除next()方法返回的对象。
另外ListIterator还有两个方法用来反向处理链表
E previous()
boolean hasPrevious()
除了上面这些添加和删除操作,还有一个set方法用于取代调用next或previous方法返回的上一个元素。
在下面的代码示例中,先添加3个元素,然后再将第二个删除,最后再在第一个元素后面连续添加两个元素
List<String> staff=new LinkedList<>();
staff.add("Amy");
staff.add("Bob");
staff.add("Carl");
System.out.println("原始数据"+staff);
ListIterator<String> iter=staff.listIterator();
String first=iter.next();
String second=iter.next();
iter.remove();
System.out.println("删除第二个对象后的数据"+staff);
iter.add("Erica");
iter.add("Gloria");
System.out.println("又添加两个对象后的数据"+staff);
运行结果:
下面的示例中再链表中添加6个元素,然后将其倒数第二个对象删除,最后再在其倒数第三个对象的前面连续插入两个对象。
List<String> staff=new LinkedList<>();
staff.add("于理想");
staff.add("张琛");
staff.add("张艳杰");
staff.add("张哲鑫");
staff.add("闫智");
staff.add("孙崇伟");
System.out.println("原始数据"+staff);
ListIterator<String> iter=staff.listIterator(staff.size());
String lastFirst=iter.previous();
String lastSecond=iter.previous();
iter.remove();
System.out.println("删除倒数第二个对象后的数据"+staff);
iter.add("闫勇");
iter.add("赵堃");
System.out.println("又添加两个对象后的数据"+staff);
运行结果:
下面代码中,将链表中的第二个元素设置为另一个值:
List<String> list=new LinkedList<>();
list.add("于理想");
list.add("张琛");
list.add("张艳杰");
list.add("张哲鑫");
list.add("闫智");
list.add("孙崇伟");
System.out.println("原始数据:"+list);
ListIterator<String> iter=list.listIterator();
String first=iter.next();
String second=iter.next();
iter.set("张艳杰");
System.out.println("修改后的数据:"+list);
下面有两点需要注意:
1、我们不得不注意一个问题就是修改与遍历的同步性,如果在某个迭代器修改集合时,另一个迭代器对其进行遍历,一定会出现混乱的情况。考虑下面一种情况:一个迭代器指向另一个迭代器刚刚删除的元素前面,这个迭代器就应该是无效的,并且不能够再被使用,在链表的迭代器的设计中,如果迭代器发现它的集合被另一个迭代器修改了,或者被该集合自身的方法修改了,就会抛出一个ConcurrentModificationException。
下面是一段示例代码:
List<String> list=new LinkedList<>();
list.add("于理想");
list.add("张琛");
list.add("张艳杰");
list.add("张哲鑫");
list.add("闫智");
list.add("孙崇伟");
System.out.println("原始数据:"+list);
ListIterator<String> iter1=list.listIterator();
ListIterator<String> iter2=list.listIterator();
iter1.next();
iter1.remove();
iter2.next();//会抛出异常
运行结果:
为了避免发生异常,Java核心技术上提到了使用下述简单规则来设计程序:可以根据需要给容器附加很多的迭代器,但是这些迭代器只能读取列表。另外,再单独附加一个既能读又能写的的迭代器。在这些集合的迭代器中,所有的迭代器都单独维护一个集合的大小的数值,因此当调用此迭代器的时候,会首先检查当前列表长度是否与原来维护的长度相等,如果不相等,则抛出上面所说的异常,因为迭代器检查的是集合长度,因此,set方法的调用不会引发上述异常。
2、Collection接口中声明了许多对链表操作的方法,其中大部分的实现是在LinkedList的超类AbstractCollection中实现的,这些实用方法有toString,contains等,其中也包括了许多有争议的方法,比如get(i)
值得注意的是,迭代器也可以返回当前的位置,而且这个方法效率非常高,因为迭代器是随时存储着元素的位置的。返回的时候使用nextIndex方法,可以返回调用next方法返回的元素在集合中的位置。
下面,最后一个综合程序综合反映了Java的链表的使用。
package Collection;
import java.util.*;
public class LinkedListTest {
public static void main(String[] args) {
List<String> a=new LinkedList<>();
a.add("Amy");
a.add("Carl");
a.add("Erica");
List<String> b=new LinkedList<>();
b.add("bob");
b.add("Doug");
b.add("Frances");
b.add("Gloria");
ListIterator<String> aIter=a.listIterator();
Iterator<String> bIter=b.iterator();
while(bIter.hasNext()) {
if(aIter.hasNext()) {
aIter.next();
}
aIter.add(bIter.next());
}
System.out.println(a);
bIter=b.iterator();
while(bIter.hasNext()) {
bIter.next();
if(bIter.hasNext()) {
bIter.next();
bIter.remove();
}
}
System.out.println(b);
a.removeAll(b);
System.out.println(a);
ListIterator<String> uIter=a.listIterator();
uIter.next();
uIter.add("bob");
uIter.add("Frances");
System.out.println(a);
}
}
运行结果:
2.ArrayList
ArrayList类应该是使用最多的一种集合了,这种集合本质是一种动态再分配的对象数组,对于这个类的最大好处就是提供了快速随机访问的功能,但是插入或删除,特别是在集合的前半部分操作的效率是非常低的。同样这个类也实现了List接口,其余操作与LinkedList相似。
和ArrayList类似的集合还有一个叫做Vector,这个集合与ArrayList的最大的区别就是这个集合是线程同步,建议在需要线程同步的时候才使用这个集合,如果不需要线程同步却使用Vector的话,效率会比ArrayList低,因为这个类会在线程同步上耗费时间。
3.散列集
散列集的目的是提高数据的查找效率,作为代价,数据是无序存放的。如果想要理解这散列集具体的原理,可以参考有关数据结构的书籍,这里只介绍这种集合在Java中如何使用。这种集合在Java中被封装为HashSet类,下面是一个关于使用它的示例:程序将读取输入的所有单词,并且将它们添加到散列集中,然后遍历散列集再次进行读入单词的输入,注意输入的单词顺序与输出的单词顺序的差别。(必须提到的是,所有类都可以使用hashCode方法生成散列码,当然也可以重定义)
在Java中,散列表用链表数组实现。每个列表被称为“桶”。
代码
package Collection;
import java.util.*;
public class SetTest {
public static void main(String[] args) {
Set<String> words=new HashSet<String>();//HashSet继承自Set
long totalTime=0;
try (Scanner in=new Scanner(System.in)){
while(in.hasNext()) {
String word=in.next();
long callTime=System.currentTimeMillis();
words.add(word);
callTime=System.currentTimeMillis()-callTime;
totalTime+=callTime;
}
}
Iterator<String> iter=words.iterator();
for(int i=1;i<=20 && iter.hasNext();i++) {
System.out.println(iter.next());
}
System.out.println("......");
System.out.println(words.size()+" distinct words."+totalTime+"milliseconds.");
}
}
运行结果:
4.树集
TreeSet类与散列表类非常相似,不过树集是一个有序集合,意思就是可以在存入数据的时候自动按某种次序排列好,但是,作为代价,查找的效率会比散列表慢一些,但是会比顺序表快,而且有一个重要限制就是存入的类必须实现Comparable接口才可以。
从JavaSE6.0起,TreeSet类实现了NavigableSet接口,这个接口增加了几个便于定位元素以及反向遍历的方法。
下面示例中创建了两个Item对象的树集。第一个按照部件编号排序,这是Item对象的默认顺序。第二个通过使用一个定制的比较器来按照描述信息排序:
代码:
package Collection;
import java.util.*;
public class TreeSetTest {
public static void main(String[] args) {
SortedSet<Item> parts=new TreeSet<>();
parts.add(new Item("Toaster",1234));
parts.add(new Item("Widget",4562));
parts.add(new Item("Modem",9912));
System.out.println(parts);
NavigableSet<Item> sortedByDescription=new TreeSet<>(
Comparator.comparing(Item::getDescription)
);
sortedByDescription.addAll(parts);
System.out.println(sortedByDescription);
}
}
class Item implements Comparable<Item>{
private String description;
private int partNumber;
public Item(String aDescription,int aPartNumber) {
description=aDescription;
partNumber=aPartNumber;
}
public String getDescription() {
return description;
}
public String toString() {
return "[description="+description+",patrNumber="+partNumber+"]";
}
public boolean equals(Object otherObject) {
if(this==otherObject) {
return true;
}
if(otherObject==null) {
return false;
}
if(getClass()!=otherObject.getClass()) {
return false;
}
Item other=(Item)otherObject;
return Objects.equals(description, other.description) && partNumber==other.partNumber;
}
public int hashCode() {
return Objects.hash(description,partNumber);
}
@Override
public int compareTo(Item arg0) {
int diff=Integer.compare(partNumber, arg0.partNumber);
return diff!=0?diff:description.compareTo(arg0.description);
}
}
运行结果:
5.队列与双端队列
队列可以让人们有效地在尾部添加一个元素,在头部删除一个元素,有两个端头的队列称为双端队列,队列不支持在中间插入或者删除元素,JavaSE6中引入Deque接口,并由ArrayDeque与LinkedList实现。
6.优先级队列
优先级队列中的元素可以按照任何顺序插入,却总是按照排序的顺序进行检索,优先级队列使用了一种称为堆的数据结构,堆是一种可以自我调整的二叉树,对树执行添加和删除操作,可以让最小的元素移动到根,而不必花费时间对元素进行排序。
优先级队列中的对象由于需要进行比较所以必须实现Comparable接口,可选的,在优先级队列的构造器中也可以提供一个Comparator对象
优先级队列的应用实例典型的有任务调度和带有vip服务的酒店排队等,任务调度具有优先级,优先级越高的最先被调度。酒店排队中,不管非vip用户来的多早,服务顺序都要晚于vip用户。
示例代码:
package Collection;
import java.time.*;
import java.util.*;
public class PriorityQequeTest {
public static void main(String[] args) {
PriorityQueue<LocalDate> pq=new PriorityQueue<>();
pq.add(LocalDate.of(1906, 12, 9));
pq.add(LocalDate.of(1815, 12, 10));
pq.add(LocalDate.of(1903, 12, 3));
pq.add(LocalDate.of(1910, 6, 22));
System.out.println("遍历该队列");
for(LocalDate date:pq) {
System.out.println(date);
}
System.out.println("删除元素");
while(!pq.isEmpty()) {
System.out.println(pq.remove());
}
}
}
运行结果: