在Java中集合的概念与数学中集合的概念相似的,即表示一组对象的组合。当然在日常的应用中,它也很常见和使用,比如数据库“连接池”就是许多数据库连接这种对象的一个集合。
下图是Java中所有相关集合类和接口的框架图。从图中可以看出整个集合类都是继承自Collection接口,一个Collection代表一组对象元素。Collection接口实现了Iterator接口,Iterator接口用于遍历集合中所有的元素。
下面详细介绍集合中常见的类:
1、Collection
Collection接口用于表示任何对象或元素的组合。想要尽可能以常规方式处理一组元素时,就使用这一接口。Collection在前面的大图也可以看出,它是List和Set 的父类。并且它本身也是一个接口。
它定义了作为集合所应该拥有的一些方法。如下:
- boolean add(Object element)
- boolean remove(Object element)
Collection 接口还支持查询操作:
- int size()
- boolean isEmpty()
- boolean contains(Object element)
- Iterator iterator()
组操作 :Collection 接口支持的其它操作,要么是作用于元素组的任务,要么是同时作用于整个集合的任务。
- boolean containsAll(Collection collection)
- boolean addAll(Collection collection)
- void clear()
- void removeAll(Collection collection)
- void retainAll(Collection collection)
以上所有的方法名根据名称就可理解方法的作用。另外,因为Collection的实现基础是数组,所以有转换为Object数组的方法:
- Object[] toArray()
- Object[] toArray(Object[] a)
其中第二个方法返回包含此 collection 中所有元素的数组;返回数组的运行时类型与指定数组的运行时类型相同。如果指定的数组能容纳该 collection,则返回包含此collection 元素的数组。否则,将分配一个具有指定数组的运行时类型和此collection大小的新数组。
注意:
集合必须只有对象,集合中的元素不能是基本数据类型。这也是包装器(Integer等)类存在的其中一个原因。
2、Iterator
任何容器类,都必须有某种方式可以将东西放进去,然后由某种方式将东西取出来。毕竟,存放事物是容器最基本的工作。对于ArrayList,add()是插入对象的方法,而get()是取出元素的方式之一。ArrayList很灵活,可以随时选取任意的元素,或使用不同的下标一次选取多个元素。
如果从更高层的角度思考,会发现这里有一个缺点:要使用容器,必须知道其中元素的确切类型。初看起来这没有什么不好的,但是考虑如下情况:如果原本是ArrayList ,但是后来考虑到容器的特点,你想换用Set ,应该怎么做?或者你打算写通用的代码,它们只是使用容器,不知道或者说不关心容器的类型,那么如何才能不重写代码就可以应用于不同类型的容器?
所以迭代器(Iterator)的概念,也是出于一种设计模式就是为达成此目的而形成的。所以Collection不提供get()方法。如果要遍历Collectin中的元素,就必须用Iterator。
迭代器(Iterator)本身就是一个对象,它的工作就是遍历并选择集合序列中的对象,而客户端的程序员不必知道或关心该序列底层的结构。此外,迭代器通常被称为“轻量级”对象,创建它的代价小。但是,它也有一些限制,例如,某些迭代器只能单向移动。
Collection接口的 iterator() 方法返回一个 Iterator。Iterator 和您可能已经熟悉的 Enumeration 接口类似。使用 Iterator接口方法,您可以从头至尾遍历集合,并安全的从底层 Collection 中除去元素。
下面,我们看一个对于迭代器的简单使用:
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
public class IteratorDemo {
public static void main(String[] args) {
Collection collection = new ArrayList();
collection.add("s1");
collection.add("s2");
collection.add("s3");
Iterator iterator = collection.iterator();//得到一个迭代器
while(iterator.hasNext()) {//遍历
Object element = iterator.next();
System.out.println("iterator= " + element);
}
if(collection.isEmpty())
System.out.println("collectionis Empty!");
else
System.out.println("collectionis not Empty! size="+collection.size());
Iterator iterator2 = collection.iterator();
while(iterator2.hasNext()) {//移除元素
Object element = iterator2.next();
System.out.println("remove:"+element);
iterator2.remove();
}
Iterator iterator3 = collection.iterator();
if(!iterator3.hasNext()) {//察看是否还有元素
System.out.println("还有元素");
}
if(collection.isEmpty())
System.out.println("collectionis Empty!");
//使用collection.isEmpty()方法来判断
}
}
程序的运行结果为:
iterator = s1
iterator = s2
iterator = s3
collection is not Empty! size=3
remove: s1
remove: s2
remove: s3
还有元素
collection is Empty!
可以看到,Java的Collection的Iterator 能够用来,:
1)使用方法 iterator() 要求容器返回一个Iterator.第一次调用Iterator 的next() 方法时,它返回集合序列的第一个元素。
2)使用next() 获得集合序列的中的下一个元素。
3)使用hasNext()检查序列中是否元素。
4)使用remove()将迭代器新返回的元素删除。
注意:
方法删除由next方法返回的最后一个元素,在每次调用next时,remove方法只能被调用一次。
3、List
List就是列表的意思,它是Collection 的一种,即继承了 Collection 接口,以定义一个允许重复项的有序集合。该接口不但能够对列表的一部分进行处理,还添加了面向位置的操作。List 是按对象的进入顺序进行保存对象,而不做排序或编辑操作。它除了拥有Collection接口的所有的方法外还拥有一些其他的方法:
- void add(int index, Object element) :添加对象element到位置index上
- boolean addAll(int index, Collection collection) :在index位置后添加容器collection中所有的元素。
- Object get(int index) :取出下标为index的位置的元素
- int indexOf(Object element) :查找对象element 在List中第一次出现的位置
- int lastIndexOf(Object element) :查找对象element 在List中最后出现的位置
- Object remove(int index) :删除index位置上的元素
- Object set(int index, Object element) :将index位置上的对象替换为element 并返回老的元素。
实现List接口的常用类有ArrayList,LinkedList,Vector,Stack。
一、ArrayList
ArrayList提供快速的基于索引的成员访问,对尾部成员的增加和删除支持较好。
ArrayList实现了可变大小的数组。它允许所有元素,包括null。ArrayList没有同步。size,isEmpty,get,set方法运行时间为常数。但是add方法开销为分摊的常数,添加n个元素需要O(n)的时间。其他的方法运行时间为线性。
每个ArrayList实例都有一个容量(Capacity),即用于存储元素的数组的大小。这个容量可随着不断添加新元素而自动增加,但是增长算法并没有定义。当需要插入大量元素时,在插入前可以调用ensureCapacity方法来增加ArrayList的容量以提高插入效率。
二、LinkedList
对列表中任何位置的成员的增加和删除支持较好,但对基于索引的成员访问支持性能较差。
LinkedList实现了List接口,允许null元素。此外LinkedList提供额外的get,remove,insert方法在 LinkedList的首部或尾部。这些操作使LinkedList可被用作堆栈(stack),队列(queue)或双向队列(deque)。
注意LinkedList没有同步方法。如果多个线程同时访问一个List,则必须自己实现访问同步。一种解决方法是在创建List时构造一个同步的List:
List list =Collections.synchronizedList(new LinkedList(...));
LinkedList可以当成栈(stack)、队列(queue)、双向队列(deque)使用。因为它实现了addFirst(),addLast(),getFirst(),getLast(),removeFirst()和removeLast()等方法。
三、Vector
Vector非常类似ArrayList,但是Vector是同步的。由Vector创建的Iterator,虽然和ArrayList创建的 Iterator是同一接口,但是,因为Vector是同步的,当一个Iterator被创建而且正在被使用,另一个线程改变了Vector的状态(例 如,添加或删除了一些元素),这时调用Iterator的方法时将抛出ConcurrentModificationException,因此必须捕获该异常。
四、Stack和Queue
Stack继承自Vector,实现一个后进先出的堆栈。Stack提供5个额外的方法使得Vector得以被当作堆栈使用。基本的push和pop方法,还有peek方法得到栈顶的元素,empty方法测试堆栈是否为空,search方法检测一个元素在堆栈中的位置。Stack刚创建后是空栈。
Queue是继承Collection的一个接口。用以支持队列的常见操作。Queue使用时要尽量避免Collection的add()和remove()方法,而是要使用offer()来加入元素,使用poll()来获取并移出元素。它们的优点是通过返回值可以判断成功与否,add()和remove()方法在失败的时候会抛出异常。
六、ListIterator
ListIterator接口继承 Iterator 接口以支持添加或更改底层集合中的元素,还支持双向访问。
以下源代码演示了列表中的反向循环。请注意 ListIterator 最初位于列表尾之后(list.size()),因为第一个元素的下标是0。
List list = ...;
ListIterator iterator =list.listIterator(list.size());
while (iterator.hasPrevious()) {
Object element = iterator.previous();
//Process element
}
4、Set
Java中的Set和正好和数学上直观的集(set)的概念是相同的。Set最大的特性就是不允许在其中存放的元素是重复的。根据这个特点,我们就可以使用Set 这个接口来实现前面提到的关于商品种类的存储需求。Set可以被用来过滤在其他集合中存放的元素,从而得到一个没有包含重复新的集合。
按照定义,Set 接口继承 Collection 接口,而且它不允许集合中存在重复项。所有原始方法都是现成的,没有引入新方法。具体的 Set 实现类依赖添加的对象的 equals() 方法来检查等同性。
- public int size() :返回set中元素的数目,如果set包含的元素数大于Integer.MAX_VALUE,返回Integer.MAX_VALUE
- public boolean isEmpty() :如果set中不含元素,返回true
- public boolean contains(Object o) :如果set包含指定元素,返回true
- public Iterator iterator():返回set中元素的迭代器,元素返回没有特定的顺序,除非set是提高了该保证的某些类的实例
- public Object[] toArray() :返回包含set中所有元素的数组
- public Object[] toArray(Object[] a) :返回包含set中所有元素的数组,返回数组的运行时类型是指定数组的运行时类型
- public boolean add(Object o) :如果set中不存在指定元素,则向set加入
- public boolean remove(Object o) :如果set中存在指定元素,则从set中删除
- public boolean removeAll(Collection c) :如果set包含指定集合,则从set中删除指定集合的所有元素
- public boolean containsAll(Collection c) :如果set包含指定集合的所有元素,返回true。如果指定集合也是一个set,只有是当前set的子集时,方法返回true
- public boolean addAll(Collection c) :如果set中中不存在指定集合的元素,则向set中加入所有元素
- public boolean retainAll(Collection c) :只保留set中所含的指定集合的元素(可选操作)。换言之,从set中删除所有指定集合不包含的元素。如果指定集合也是一个set,那么该操作修改set的效果是使它的值为两个set的交集
- public boolean removeAll(Collection c) :如果set包含指定集合,则从set中删除指定集合的所有元素
- public void clear() :从set中删除所有元素
一、HashSet
HashSet是为优化查询速度而设计的Set。HashSet可以无序地遍历成员,成员可为任意Object子类的对象,但如果覆盖了equals方法,同时注意修改hashCode方法。
二、TreeSet
TreeSet可以外部有序地遍历成员;附加实现了SortedSet, 支持子集等要求顺序的操作。成员要求实现Comparable接口,或者使用Comparator构造TreeSet。成员一般为同一类型。
TreeSet用了红黑树这种数据结构来为元素排序。
三、LinkedHashSet
LinkedhashSet可以外部成员的插入顺序遍历成员。成员与HashSet成员类似。
具有可预知迭代顺序的Set接口的哈希表和链接列表实现。此实现与HashSet的不同之外在于,后者维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,即按照将元素插入到set中的顺序(插入顺序)进行迭代。注意,插入顺序不受在set中重新插入的元素的影响。
5、Map
数学中的映射关系在Java中就是通过Map来实现的。它表示,里面存储的元素是一个对(pair),我们通过一个对象,可以在这个映射关系中找到另外一个和这个对象相关的东西。
Map接口不是 Collection 接口的继承。而是从自己的用于维护键-值关联的接口层次结构入手。按定义,该接口描述了从不重复的键到值的映射。
我们可以把这个接口方法分成三组操作:改变、查询和提供可选视图。
改变操作允许您从映射中添加和除去键-值对。键和值都可以为 null。但是,您不能把 Map 作为一个键或值添加给自身。
- Object put(Object key,Object value):用来存放一个键-值对Map中
- Object remove(Object key):根据key(键),移除一个键-值对,并将值返回
- void putAll(Map mapping) :将另外一个Map中的元素存入当前的Map中
- void clear() :清空当前Map中的元素
查询操作允许您检查映射内容:
- Object get(Object key) :根据key(键)取得对应的值
- boolean containsKey(Object key) :判断Map中是否存在某键(key)
- boolean containsValue(Object value):判断Map中是否存在某值(value)
- int size():返回Map中键-值对的个数
- boolean isEmpty() :判断当前Map是否为空
最后一组方法允许您把键或值的组作为集合来处理。
- public Set keySet() :返回所有的键(key),并使用Set容器存放
- public Collection values() :返回所有的值(Value),并使用Collection存放
- public Set entrySet() :返回一个实现Map.Entry 接口的元素 Set
一、Hashtable
Hashtable继承Map接口,实现一个key-value映射的哈希表。任何非空(non-null)的对象都可作为key或者value。
添加数据使用put(key, value),取出数据使用get(key),这两个基本操作的时间开销为常数。
Hashtable通过initial capacity和load factor两个参数调整性能。通常缺省的loadfactor 0.75较好地实现了时间和空间的均衡。增大loadfactor可以节省空间但相应的查找时间将增大,这会影响像get和put这样的操作。
使用Hashtable的简单示例如下,将1,2,3放到Hashtable中,他们的key分别是”one”,”two”,”three”:
Hashtable numbers =new Hashtable();
numbers.put(“one”, new Integer(1));
numbers.put(“two”, new Integer(2));
numbers.put(“three”, new Integer(3));
要取出一个数,比如2,用相应的key:
Integer n =(Integer)numbers.get(“two”);
System.out.println(“two = ” + n);
由于作为key的对象将通过计算其散列函数来确定与之对应的value的位置,因此任何作为key的对象都必须实现hashCode和equals方法。hashCode和equals方法继承自根类Object,如果你用自定义的类当作key的话,要相当小心,按照散列函数的定义,如果两个对象相 同,即obj1.equals(obj2)=true,则它们的hashCode必须相同,但如果两个对象不同,则它们的hashCode不一定不同,如 果两个不同对象的hashCode相同,这种现象称为冲突,冲突会导致操作哈希表的时间开销增大,所以尽量定义好的hashCode()方法,能加快哈希 表的操作。
如果相同的对象有不同的hashCode,对哈希表的操作会出现意想不到的结果(期待的get方法返回null),要避免这种问题,只需要牢记一条:要同时复写equals方法和hashCode方法,而不要只写其中一个。
Hashtable是同步的。
二、HashMap
能满足用户对Map的通用需求。键成员可为任意Object子类的对象,但如果覆盖了equals方法,同时注意修改hashCode方法。
HashMap和Hashtable类似,不同之处在于HashMap是非同步的,并且允许null,即null value和null key。,但是将HashMap视为Collection时(values()方法可返回Collection),其迭代子操作时间开销和HashMap的容量成比例。因此,如果迭代操作的性能相当重要的话,不要将HashMap的初始化容量设得过高,或者load factor过低。
三、TreeMap
支持对键有序地遍历,使用时建议先用HashMap增加和删除成员,最后从HashMap生成TreeMap; 附加实现了SortedMap接口,支持子Map等要求顺序的操作。键成员要求实现Comparable接口,或者使用Comparator构造TreeMap键成员一般为同一类型。
四、WeakHashMap类
WeakHashMap是一种改进的HashMap,它对key实行“弱引用”,如果一个key不再被外部所引用,那么该key可以被GC回收。
WeakHashMap 是 Map 的一个特殊实现,它只用于存储对键的弱引用。当映射的某个键在 WeakHashMap 的外部不再被引用时,就允许垃圾收集器收集映射中相应的键值对。使用 WeakHashMap 有益于保持类似注册表的数据结构,其中条目的键不再能被任何线程访问时,此条目就没用了。
五、LinkedHashMap
保留键的插入顺序,用equals 方法检查键和值的相等性。成员可以为任意Object子类的对象,但如果覆盖了equals方法,同时注意修改hashCode方法。
六、IdentifyHashMap
Map的一种特性实现,关键属性的hash码不是由hashCode()方法计算,而是由System.identityHashCode方法计算,使用==进行比较而不是equals()方法。
6、Arrays和Collections
一、Arrays
java.util里面有一个Arrays类,它包括了一组可用于数组的static方法,这些方法都是一些实用工具。其中有四个基本方法:用来比较两个数组是否相等的equals();用来填充的fill();用来对数组进行排序的sort();以及用于在一个已排序的数组中查找元素的binarySearch()。所有这些方法都对primitive和Object进行了重载。此外还有一个asList()方法,它接受一个数组,然后把它转成一个List容器。
虽然Arrays还是有用的,但它的功能并不完整。举例来说,如果它能让我们不用写for循环就能直接打印数组,那就好了。此外,正如你所看到的fill()只能用一个值填数组。所以,如果你想把随即生成的数字填进数组的话,fill()是无能为力的。
1)查询有序数组binarySearch()
一旦数组排完序,你就能用Arrays.binarySearch()进行快速查询了。但是切忌对一个尚未排序的数组使用binarySearch();因为这么做的结果是没意义的。
如果Arrays.binarySearch()找到了,它就返回一个大于或等于0的值。否则它就返回一个负值,而这个负值要表达的意思是,如果你手动维护这个数组的话,这个值应该插在哪个为止。这个值就是:
-(插入点)-1
“插入点”就是,在所有“比要找的那个值”更大值中,最小的那个值的下标,或者,如果数组中所有的值都比要找的值小,它就是a.size()。
如果数组里面有重复元素,那它不能保证会返回哪一个。这个算法不支持重复元素,不过它也不报错。所以,如果你需要的是一个无重复元素的有序序列的话,那么可以考虑使用本章前面所介绍的TreeSet和LinkedHashSet。这两个类会帮你照看所有细节。只有在遇到性能瓶颈的时候,你才应该用手动维护的数组来代替这两个类。
如果排序的时候用到了Comparator(针对对象数组,primitive数组不允许使用Comparator),那么binarySearch()的时候,也必须使用同一个Comparator(用这个方法的重载版)。
复制一个数组arraycopy()
Java标准类库提供了一个System.arraycopy()的static方法。相比for循环,它能以更快的速度拷贝数组。System.arraycopy()对所有类型都作了重载。
对象数组和primitive数组都能拷贝。但是如果你拷贝的是对象数组,那么你只拷贝了它们的reference--对象本身不会被拷贝。这被成为浅拷贝(shallow copy)。
数组的比较equals()
为了能比较数组是否完全相等,Arrays提供了经重载的equals()方法。当然,也是针对各种primitive以及Object的。两个数组要想完全相等,他们必须有相同数量的元素,而且数组的每个元素必须与另一个数组的相对应的位置上的元素相等。元素的相等姓,用equals()判断。(对于 primitive,它会使用其wrapper类的equals();比如int使用Integer.equals()。)。
数组元素的比较
Java里面有两种能让你实现比较功能的方法。一是实现java.lang.Comparable接口,并以此实现类“自有的”比较方法。这是一个很简单的接口,它只有一个方法compareTo()。这个方法能接受另一个对象作为参数,如果现有对象比参数小,它就会返回一个负数,如果相同则返回零,如果现有的对象比参数大,它就返回一个正数。。
现在假设,有人给你一个没有实现Comparable接口的类,或者这个类实现了Comparable接口,但是你发现它的工作方式不是你所希望的,于是要重新定义一个新的比较方法。Java没有强求你一定要把比较代码塞进类里,它的解决方案是使用“策略模式(strategy design pattern)”。有了策略之后,你就能把会变的代码封装到它自己的类里(即所谓的策略对象strategy object)。你把策略对象交给不会变的代码,然后用它运用策略完成整个算法。这样,你就可以用不同的策略对象来表示不同的比较方法,然后把它们都交给同一个排序程序了。接下来就要“通过实现Comparator接口”来定义策略对象了。这个接口有两个方法compare()和equals()。但是除非是有特殊的性能要求,否则你用不着去实现equals()。因为只要是类,它就都隐含地继承自Object,而Object里面已经有了一个 equals()了。所以你尽可以使用缺省的Object的equals(),这样就已经满足接口的要求了。
Collections类里专门有一个会返回与对象自有的比较法相反的Comparator的方法。它能很轻易地被用到CompType上面。
Collections.reverseOrder()返回了一个Comparator的reference。
compare()方法会根据第一个参数是小于,等于还是大于第二个参数,分别返回负整数,零或是正整数。
数组的排序sort()
有了内置的排序方法之后,你就能对任何数组排序了,不论是primitive的还是对象数组的,只要它实现了Comparable接口或有一个与之相关的Comparator对象就行了。
Java标准类库所用的排序算法已经作了优化--对primitive,它用的是“快速排序(Quicksort)”,对对象,它用的是“稳定合并排序(stable merge sort)”。所以除非是prolier表明排序算法是瓶颈,否则你不用为性能担心。
数组部分的总结
总而言之,如果你要持有一组对象,首选,同时也是效率最高的选择,应该是数组。而且,如果这是一组primitive的话,你也只能用数组。还有一些更为一般的情况,也就是写程序的时候还不知道要用多少对象,或者要用一种更复杂方式来存储对象情况。为此,Java提供了“容器类(container class)”。其基本类型有List,Set和Map。
它们还有一些别的特性。比方说Set所持有的对象,个个都不同,Map则是一个“关联性数组(associative array)”,它能在两个对象之间建立联系。此外,与数组不同,它们还能自动调整大小,所以你可以往里面放任意数量的对象。
7、比较
6.1 Vector和ArrayList
Vector的方法是同步的(Synchronized),是线程安全的(thread-safe),而ArrayList的方法不是,由于线程的同步必然要影响性能。因此,ArrayList的性能比Vector好。
当Vector或ArrayList中的元素超过它的初始大小时,Vector会将它的容量翻倍,而ArrayList只会增加50%的大小。这样ArrayList有利于节约内存空间。
6.2 ArrayList和LinkedList
1.ArrayList是基于数组,LinkedList是基于链表实现。
2.对于访问get和set,ArrayList要优于LinkedList,因为LinkedList要移动指针。
3.对于新增和删除操作add和remove,LinkedList比较占优势,因为ArrayList要移动数据。
事实上也不一定,比如ArrayList在末尾插入和删除数据就不设计到数据移动,不过还是有这么个建议:随机访问比较多的话一定要用ArrayList而不是LinkedList,如果需要频繁的插入和删除应该考虑用LinkedList来提高性能。
6.3 HashSet和TreeSet
6.4 HashMap和Hashtable
Hashtable和HashMap它们的性能方面的比较类似Vector和ArrayList,比如Hashtable的方法是同步的,而HashMap的不是。
6.5 Comparable接口和Comparator接口
Comparable接口强行对实现它的每个类的对象进行整体排序。此排序被称为该类的自然排序,类的 compareTo 方法被称为它的自然比较方法。实现此接口的对象列表(和数组)可以通过Collections.sort(和 Arrays.sort)进行自动排序。实现此接口的对象可以用作有序映射表中的键或有序集合中的元素,无需指定比较器。
Comparator比较函数强行对某些对象 collection 进行整体排序。可以将Comparator 传递给 sort 方法(如 Collections.sort),从而允许在排序顺序上实现精确控制。还可以使用 Comparator 来控制某些数据结构(如TreeSet 或 TreeMap)的顺序。