Java基础-Collection类详解

来源:互联网 发布:家里的网络突然没网了 编辑:程序博客网 时间:2024/05/16 07:07

之前做的一些项目里也用过一些集合类,但是java中对集合类的具体实现却不是很了解,在网上找了一些资料,觉得这篇文章写的很全面,于是转载过来学习一下。

原文链接:http://my.oschina.net/xianggao/blog/90189

在Java2中,有一套设计优良的接口和类组成了Java集合框架Collection,使程序员操作批量的数据或对象元素极为方便。这些接口和类有很多对抽象数据类型操作的API,而这是我们常用的且在数据结构中熟知的,例如Map,Set,List等。并且Java用面向对象的设计对这些数据结构和算法进行了封装,这就极大的减化了程序员编程时的负担。程序员可以以这个集合框架为基础,定义更高级别的数据抽象,比如栈、队列和线程安全的集合等,从而满足自己的需要。

线性表,链表,哈希表是常用的数据结构,在进行Java开发时,JDK已经为我们提供了一系列相应的类来实现基本的数据结构。这些类均在java.util包中。本文试图通过简单的描述,向读者阐述各个类的作用以及如何正确使用这些类。

一、Conllection类介绍

QQ截图20121118172448

1. Collection接口

Collection是最基本的集合接口,一个Collection代表一组Object,即Collection的元素(Elements)。一些Collection允许相同的元素而另一些不行。一些能排序而另一些不行。Java SDK不提供直接继承自Collection的类,Java SDK提供的类都是继承自Collection的“子接口”如List和Set

所有实现Collection接口的类都必须提供两个标准的构造函数:a) 无参数的构造函数用于创建一个空的Collection;b) 有一个Collection参数的构造函数用于创建一个新的Collection,这个新的Collection与传入的Collection有相同的元素。后一个构造函数允许用户复制一个Collection。

如何遍历Collection中的每一个元素?不论Collection的实际类型如何,它都支持一个iterator()的方法,该方法返回一个迭代子,使用该迭代子即可逐一访问Collection中每一个元素。典型的用法如下:

Iterator it = collection.iterator(); // 获得一个迭代子 
while(it.hasNext()) { 
      Object obj = it.next(); // 得到下一个元素 
}

由Collection接口派生的两个接口是List和Set。

主要方法:

boolean add(Object o) 添加对象到集合

boolean remove(Object o) 删除指定的对象

int size() 返回当前集合中元素的数量

boolean contains(Object o) 查找集合中是否有指定的对象

boolean isEmpty() 判断集合是否为空

Iterator iterator() 返回一个迭代器

boolean containsAll(Collection c) 查找集合中是否有集合c中的元素

boolean addAll(Collection c) 将集合c中所有的元素添加给该集合

void clear() 删除集合中所有元素

void removeAll(Collection c) 从集合中删除c集合中也有的元素

void retainAll(Collection c) 从集合中删除集合c中不包含的元素

2. List接口

List是有序的Collection,使用此接口能够精确的控制每个元素插入的位置。用户能够使用索引(元素在List中的位置,类似于数组下标)来访问List中的元素,这类似于Java的数组。

和下面要提到的Set不同,List允许有相同的元素。除了具有Collection接口必备的iterator()方法外,List还提供一个listIterator()方法,返回一个 ListIterator接口,和标准的Iterator接口相比,ListIterator多了一些add()之类的方法,允许添加,删除,设定元素, 还能向前或向后遍历。

实现List接口的常用类有LinkedList,ArrayList,Vector和Stack。

主要方法:

void add(int index,Object element)在指定位置上添加一个对象

boolean addAll(int index,Collection c)将集合c的元素添加到指定的位置

Object get(int index)返回List中指定位置的元素

int indexOf(Object o)返回第一个出现元素o的位置.

Object removeint(int index)删除指定位置的元素

3. LinkedList类

LinkedList实现了List接口,采用双向链表方式存储数据,允许null元素。此外LinkedList提供额外的get,remove,insert方法在 LinkedList的首部或尾部。这些操作使LinkedList可被用作堆栈(stack),队列(queue)或双向队列(deque)。

注意:LinkedList没有同步方法。如果多个线程同时访问一个List,则必须自己实现访问同步。一种解决方法是在创建List时构造一个同步的List:

List list = Collections.synchronizedList(new LinkedList(...));

4. ArrayList类

ArrayList实现了可变大小的数组,采用数组方式存储数据,它允许所有元素,包括null。

size,isEmpty,get,set方法运行时间为常数。但是add方法开销为分摊的常数,添加n个元素需要O(n)的时间。其他的方法运行时间为线性。

每个ArrayList实例都有一个容量(Capacity),即用于存储元素的数组的大小。这个容量可随着不断添加新元素而自动增加,但是增长算法并没有定义。当需要插入大量元素时,在插入前可以调用ensureCapacity方法来增加ArrayList的容量以提高插入效率。

和LinkedList一样,ArrayList也是非同步的(unsynchronized)。

主要方法:

Boolean add(Object o)将指定元素添加到列表的末尾

Boolean add(int index,Object element)在列表中指定位置加入指定元素

Boolean addAll(Collection c)将指定集合添加到列表末尾

Boolean addAll(int index,Collection c)在列表中指定位置加入指定集合

Boolean clear()删除列表中所有元素

Boolean clone()返回该列表实例的一个拷贝

Boolean contains(Object o)判断列表中是否包含元素

Boolean ensureCapacity(int m)增加列表的容量,如果必须,该列表能够容纳m个元素

Object get(int index)返回列表中指定位置的元素

Int indexOf(Object elem)在列表中查找指定元素的下标

Int size()返回当前列表的元素个数

5. Vector类

Vector非常类似ArrayList,采用数组方式存储数据,但是Vector是同步的。由Vector创建的Iterator,虽然和ArrayList创建的 Iterator是同一接口,但是,因为Vector是同步的,当一个Iterator被创建而且正在被使用,另一个线程改变了Vector的状态(例如,添加或删除了一些元素),这时调用Iterator的方法时将抛出ConcurrentModificationException,因此必须捕获该异常。

6. Stack 类

Stack继承自Vector,实现一个后进先出的堆栈。

Stack提供5个额外的方法使得Vector得以被当作堆栈使用。基本的push和pop方法,还有peek方法得到栈顶的元素,empty方法测试堆栈是否为空,search方法检测一个元素在堆栈中的位置。

Stack刚创建后是空栈。

7. Set接口

Set是一种不包含重复的元素的Collection,即任意的两个元素e1和e2都有e1.equals(e2)=false,Set最多有一个null元素。很明显,Set的构造函数有一个约束条件,传入的Collection参数不能包含重复的元素。

注意:必须小心操作可变对象(Mutable Object)。如果一个Set中的可变元素改变了自身状态导致Object.equals(Object)=true将导致一些问题。

8. Map接口

Map没有继承Collection接口,Map提供key到value的映射。一个Map中不能包含相同的key,每个key只能映射一个 value。

Map接口提供3种集合的视图,Map的内容可以被当作一组key集合,一组value集合,或者一组key-value映射。

主要方法:

boolean equals(Object o)比较对象

boolean remove(Object o)删除一个对象

put(Object key,Object value)添加key和value

9. Hashtable类

Hashtable继承Map接口,实现一个key-value映射的哈希表。任何非空(non-null)的对象都可作为key或者value。Hashtable是同步的。

添加数据使用put(key, value),取出数据使用get(key),这两个基本操作的时间开销为常数。

Hashtable通过initial capacity和load factor两个参数调整性能。通常缺省的load factor 0.75较好地实现了时间和空间的均衡。增大load factor可以节省空间但相应的查找时间将增大,这会影响像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方法,而不要只写其中一个。

10. HashMap类

HashMap和Hashtable类似,不同之处在于HashMap是非同步的,并且允许null,即null value和null key。但是将HashMap视为Collection时(values()方法可返回Collection),其迭代子操作时间开销和HashMap的容量成比例。因此,如果迭代操作的性能相当重要的话,不要将HashMap的初始化容量设得过高,或者load factor过低。

11. WeakHashMap类

WeakHashMap是一种改进的HashMap,它对key实行“弱引用”,如果一个key不再被外部所引用,那么该key可以被GC回收。

二、集合类数据结构

1. List集合

1) ArrayList 顺序存储

ArrayList维护着一个对象数组。如果调用new ArrayList()后,它会默认初始一个size=10的数组。 每次add操作都要检查数组容量,如果不够,重新设置一个初始容量1.5倍大小的新数组,然后再把每个元素copy过去(使用System.arraycopy())。 在数组中间插入或删除,都要移动后面的所有元素。

数据结构图:

 image

2) LindedList 链式存储

LinkedList的实现是一个双向链表。每个节点除含有元素外,还包含向前,向后的指针。 新建一个LinkedList,生成一个头节点(header,就是一个头指针),它的元素为null。

数据结构图:

image

它自包含,next和previous指针都指向自己。 执行add(Object obj)方法后,会生成一个新节点:

数据结构图:

image

Header节点的next指向链表的第一个节点,previous指向链表的最后一个节点,在这里都是first。再增加一个对象,它的形状像下面这样:

数据结构图:

image

现在是一个标准的双向链表形状。每个节点都有自己的next和previous指针。

增加节点,只会对链表的指针进行操作,速度快;LinkedList实现了Deque,所以它有双向队列的特征,在链表两端可增删数据;使用index查找对象时,会以index和size/2比较,从前或从后向中间搜索;ListIterator可向前或向后进行迭代;

3) 比较ArrayList和LinkedList的数据结构,就可以得出:

1. ArrayList是实现了基于动态数组的数据结构,LinkedList基于双向指针链表的数据结构;

2. 对ArrayList和LinkedList而言,在列表末尾增加一个元素所花的开销都是固定的。对ArrayList而言,主要是在内部数组中增加一项,指向所添加的元素,偶尔可能会导致对数组重新进行分配;而对LinkedList而言,这个开销是统一的,分配一个内部Entry对象;

3. 在ArrayList的中间插入或删除一个元素意味着这个列表中剩余的元素都会被移动;而在LinkedList的中间插入或删除一个元素的开销是固定的;

4. LinkedList不支持高效的随机元素访问;

5. ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相等的空间;

可以这样说:

1. 当操作是在一列数据的后面添加数据而不是在前面或中间,并且需要随机地访问其中的元素时,使用ArrayList会提供比较好的性能;

2. 当你的操作是在一列数据的前面或中间添加或删除数据,并且按照顺序访问其中的元素时,就应该使用LinkedList了;

4) 浅谈数组链表与指针链表

数组链表访问快,复杂度O(1),但是添加删除慢,复杂度O(n); 
指针链表访问慢,复杂度是O(n),但是添加删除快,复杂度O(1);

至于选择哪种数据结构,只不过一般有习惯而已,比如二叉树,一般都是用指针实现,你想用数组实现也没有任何问题.而且有的时候算法需要数组实现. 你需要了解一个数据结构特点,进行算法复杂度分析,就能够针对你的应用程序选择合适的方法.

2. Map集合

1) HashMap

HashMap的结构是一个散列桶,初始化时生成如下结构:

image

每个bucket包含一个Entry(map自定义的一种结构,包含一个往后的指针)的链表。 在put(key, value)后,它的结构如下:

未命名

将key的hashcode再次散列,然后用这个hash和length-1进行按位与操作,得到bucket的index,然后检查当前bucket的链表,有没有这个key,如果有替换value,没有则跟在链表的最后。

允许key和value都可以是null。Index=0的bucket存key=null的value,也可以是其它hashcode为0的项。

初始容量必须为2的幂次(我的理解是,在生成index的时候有这样的代码:hash ^ (length - 1)),length – 1的二进制代码为全1,则容易进行hash的设计)。

如果两个key散列后的index一样的话,第一个key生成的Entry先存在桶中,第二个key生成的Entry会将第一个Entry设为自己的next,串起来。(如图中,先put(yy, “first”),会将这个Entry设为bucket的第一项,后put(xx,”second”),则生成新Entry,它的next为key为yy的Entry,生成一个链表), 在put操作中,会比较threshold(capacity * load_factor,一个临界值),如果size > threshold的话,生成一个当前bucket两倍数量的buckets,然后把现有的数据重新散列到新bucket中。

对HashMap迭代时,返回数据的顺序是:index从0到length-1,循环遍历每个bucket,把不为null的数据取出,每个bucket内的顺序由链表的顺序决定。而不是由插入数据决定。

2) LinkedHashMap

上面说过,Map的迭代不由插入顺序决定。如果要保持这种顺序呢?就要新增加一种结构来保持。

未命名

LinkedHashMap是HashMap的子类,增加一个双向链表,用来存储每个新加入的节点。在遍历时,按链表的顺序进行。其实差不多就是上面HashMap和LinkedList的和吧。

3. Set

1) HashSet

HashSet使用HashMap来保持元素。Key = 元素,value是一个公有的对象,对每个元素都一样,在HashMap里面key是惟一的,当然很适合于构造set集合。等同于用HashMap包装了层,显示Set自己的特性。

三、ArrayList、Vector与LinkedList分析

1. 同步性

Vector是同步的。这个类中的一些方法保证了Vector中的对象是线程安全的。而ArrayList则是非同步的,因此ArrayList中的对象并不是线程安全的。因为同步的要求会影响执行的效率,所以如果你不需要线程安全的集合那么使用ArrayList是一个很好的选择,这样可以避免由于同步带来的不必要的性能开销。

2. 数据增长

从内部实现机制来讲ArrayList和Vector都是使用数组(Array)来控制集合中的对象。当你向这两种类型中增加元素的时候,如果元素的数目超出了内部数组目前的长度它们都需要扩展内部数组的长度,Vector缺省情况下自动增长原来一倍的数组长度,ArrayList是原来的50%,所以最后你获得的这个集合所占的空间总是比你实际需要的要大。所以如果你要在集合中保存大量的数据,那么使用Vector有一些优势,因为你可以通过设置集合的初始化大小来避免不必要的资源开销。

3. 使用模式

在ArrayList和Vector中,从一个指定的位置(通过索引)查找数据或是在集合的末尾增加、移除一个元素所花费的时间是一样的,这个时间我们用O(1)表示。但是,如果在集合的其他位置增加或移除元素那么花费的时间会呈线形增长:O(n-i),其中n代表集合中元素的个数,i代表元素增加或移除元素的索引位置。为什么会这样呢?以为在进行上述操作的时候集合中第i和第i个元素之后的所有元素都要执行位移的操作。这一切意味着什么呢?

这意味着,你只是查找特定位置的元素或只在集合的末端增加、移除元素,那么使用Vector或ArrayList都可以。如果是其他操作,你最好选择其他的集合操作类。比如,LinkedList集合类在增加或移除集合中任何位置的元素所花费的时间都是一样的O(1),但它在索引一个元素的使用却比较慢O(i),其中i是索引的位置.使用ArrayList也很容易,因为你可以简单的使用索引来代替创建iterator对象的操作。LinkedList也会为每个插入的元素创建对象(会重新分配空间),所以你要明白它也会带来额外的开销。

ArrayList底层采用数组完成,而LinkedList则是采用一般的双向链表(double-linked list)完成,其内每个对象除了数据本身外,还有两个引用,分别指向前一个元素和后一个元素。如果我们经常在List的开始处增加元素,或者在List中进行插入和删除操作,我们应该使用LinkedList,否则的话,使用ArrayList将更加快速。当执行搜索操作时,采用ArrayList 比较好。

最后,在《Practical Java》一书中Peter Haggar建议使用一个简单的数组(Array)来代替Vector或ArrayList。尤其是对于执行效率要求高的程序更应如此。因为使用数组(Array)避免了同步、额外的方法调用和不必要的重新分配空间的操作。

4. 集合类的区别

1. ArrayList: 元素单个,效率高,多用于查询 ; 
2. Vector: 元素单个,线程安全,多用于查询 ; 
3. LinkedList:元素单个,多用于插入和删除 ; 
4. HashMap: 元素成对,元素可为空 ; 
5. HashTable: 元素成对,线程安全,元素不可为空 ;

四、Collections和Arrays帮助类

在 Java集合类框架里有两个类叫做Collections(注意,不是Collection!)和Arrays,这是JCF里面功能强大的工具,但初学者往往会忽视。按JCF文档的说法,这两个类提供了封装器实现(Wrapper Implementations)、数据结构算法和数组相关的应用。

想必大家不会忘记上面谈到的“折半查找”、“排序”等经典算法吧,Collections类提供了丰富的静态方法帮助我们轻松完成这些在数据结构课上烦人的工作:

binarySearch:折半查找。 
sort:排序,这里是一种类似于快速排序的方法,效率仍然是O(n * log n),但却是一种稳定的排序方法。 
reverse:将线性表进行逆序操作,这个可是从前数据结构的经典考题哦! 
rotate:以某个元素为轴心将线性表“旋转”。 
swap:交换一个线性表中两个元素的位置。 
……

Collections还有一个重要功能就是“封装器”(Wrapper),它提供了一些方法可以把一个集合转换成一个特殊的集合:

unmodifiableXXX:转换成只读集合,这里XXX代表六种基本集合接口:Collection、List、Map、Set、SortedMap和SortedSet。如果你对只读集合进行插入删除操作,将会抛出UnsupportedOperationException异常。 
synchronizedXXX:转换成同步集合。 
singleton:创建一个仅有一个元素的集合,这里singleton生成的是单元素Set,singletonList和singletonMap分别生成单元素的List和Map。 
空集:由Collections的静态属性EMPTY_SET、EMPTY_LIST和EMPTY_MAP表示。

五、总结

大多数情况下,从性能上来说ArrayList最好,但是当集合内的元素需要频繁插入、删除时LinkedList会有比较好的表现,但是它们三个性能都比不上数组,另外Vector是线程同步的。所以:

如果能用数组的时候(元素类型固定,数组长度固定),请尽量使用数组来代替List; 
如果没有频繁的删除插入操作,又不用考虑多线程问题,优先选择ArrayList; 
如果在多线程条件下使用,可以考虑Vector; 
如果需要频繁地删除插入,LinkedList就有了用武之地; 
如果你什么都不知道,用ArrayList没错。

要特别注意对哈希表的操作,作为key的对象要正确复写equals和hashCode方法。

最后,编程时尽量返回接口而非实际的类型,如返回List而非ArrayList,这样如果以后需要将ArrayList换成LinkedList时,客户端代码不用改变。这就是针对接口编程。

0 0
原创粉丝点击