1.JAVA集合类
最常用的集合有List, Set 和 Map
List ------------
LinkedList
------------
ArrayList
------------Vector
|-------
Stack
Set------------HashSet
|-------
LinkedHashSet
-------------SortedSet
|------
TreeSet
对于它们的数据是否可以重复出现,是否有序,有如下特点:
2.List
2.1 ArrayList
List的本质是array。 它在插入操作时会自动增加其大小,如下图的JDK 1. 6提供的源码:
private transient Object[] elementData; //elementData是一个数组
public void ensureCapacity(int minCapacity) {
modCount++;
int oldCapacity = elementData.length;
if (minCapacity > oldCapacity) {
Object oldData[] = elementData;
int newCapacity = (oldCapacity * 3)/2 + 1; //每次自动增加适量长度
if (newCapacity < minCapacity)
newCapacity = minCapacity;
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity); //拷贝旧数组到新数组,增加长度
}
}
public void add(int index, E element) { //时间复杂度: O(n)
if (index > size || index < 0)
throw new IndexOutOfBoundsException(
"Index: "+index+", Size: "+size);
ensureCapacity(size+1); // Increments modCount!!增加数组长度
System.arraycopy(elementData, index, elementData, index + 1,
size - index); //再拷贝一次,留下index的位置给要插入的数据
elementData[index] = element;
size++;
}
public E remove(int index) {
RangeCheck(index);
modCount++;
E oldValue = (E) elementData[index];
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // Let gc do its work
return oldValue;
}
/**
* Returns the element at the specified position in this list.
*
* @param index index of the element to return
* @return the element at the specified position in this list
* @throws IndexOutOfBoundsException {@inheritDoc}
*/
public E get(int index) {
RangeCheck(index);
return (E) elementData[index];
}
结构特点:ArrayList的特性跟数组很相似。
元素有序可重复。
查找操作:继承数组的特点,查找很快。
修改操作:每次插入数据,必须移动其它数据,所以它在指定位置的插入操作较慢O(n),且浪费资源。
线程安全:
非线程同步。
适用情况:适用于无平凡增删的情况。
2.2 linkedList
LinkedList 以Node作为单元。Node 为泛型,可以传入任意数据类型
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
private transient Entry<E> header = new Entry<E>(null, null, null);
private transient int size = 0;
/**
* Constructs an empty list.
*/
public LinkedList() {
header.next = header.previous = header;
}
private E remove(Entry<E> e) {
if (e == header)
throw new NoSuchElementException();
E result = e.element;
e.previous.next = e.next;
e.next.previous = e.previous;
e.next = e.previous = null;
e.element = null;
size--;
modCount++;
return result;
}
/**
* Returns the indexed entry.
*/
private Entry<E> entry(int index) {
if (index < 0 || index >= size)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+size);
Entry<E> e = header;
if (index < (size >> 1)) {
for (int i = 0; i <= index; i++)
e = e.next;
} else {
for (int i = size; i > index; i--)
e = e.previous;
}
return e;
}
对于Linkedlist, 如果要查找指定位置的元素,它没有ArrayList快,必须从头或从尾一个一个地遍历。时间花费较大O(n).但是对于修改操作,如插入删除,同样花费O(n)先去找到这个index对应的元素,然后花费常量时间去修改链接。迭代器的使用会使LinkedList快捷很多。
结构特点:链表。
元素有序可重复。
查找操作:查找速度不如ArrayList, 每次查找都得从头或从尾遍历。
修改操作:速度快于ArrayList,不必移动其它元素。
线程安全:
非线程同步。
适用情况:适用于有平凡增删的情况。
2.3 Vector( Stack)
Vector 非常类似于Arraylist. 但是最重要的区别是:Vector是
线程同步的,也就是说Vector适合多线程操作。Stack继承自Vector,提供了一些pop,push, peek等用于Stack操作的方法。Stack也是线程安全的。
结构特点:ArrayList。
元素有序可重复。
查找操作:查找速度快。
修改操作:不如linkedList
线程安全:
线程同步。
适用情况:适用于一些需要stack的情况。
String str = "abcd";
Vector<String> ads = new Vector<String>(); //a normal way to create a vector
ads.add(str);
Vector<String> ads3 = new Vector<String>(ads); //create a copy of ads to ads3, this way is quick and fast and useful
System.out.print(ads3.get(0).toString());
3.Set
HashSet将元素存储在哈希表(hash table)中,它是最高效的Set实现,但是它无法确定迭代顺序。TreeSet将元素存储在红黑树中,元素按照值进行排序,它比HashSet稍慢。LinkedHashSet是HashSet的链表实现,它保持元素插入的顺序,但是访问性能比HashSet和TreeSet差。
这里有一个简单但是有用的Set使用场景。假设你有一个Collection c,你想创建另外一个Collection,但必须去除重复元素(只保留一个)。下面一行代码就实现了你的要求:
Collection<Type> noDups = new HashSet<Type>(c);
这里有另外一个变种实现,能保证原始集合中元素的顺序:
Collection<Type> noDups = new LinkedHashSet<Type>(c);
3.1 set集合的批量操作(bulk operation)
批量操作尤其适用于Set。当执行批量批量操作相当于执行集合代数意义上的运算。假设s1和s2都是Set。下面是各种批量操作:
s1.containsAll(s2) — 如果s2是s1的子集,返回true,否则返回false
s1.addAll(s2) — 得到的是s1和s2的并集
s1.retainAll(s2) — 得到的是s1和s2的交集
s1.removeAll(s2) — 得到的是s1和s2的差集(s1-s2,即所有s1中有但是s2中没有的元素的集合)
为了计算两个集合的并、交、差集而不修改这两个集合,调用者必须先拷贝一份,然后再调用bulk opertaion,比如下面:
Set<Type> union = new HashSet<Type>(s1);
union.addAll(s2);
Set<Type> intersection = new HashSet<Type>(s1);
intersection.retainAll(s2);
Set<Type> difference = new HashSet<Type>(s1);
difference.removeAll(s2);
3.2 HashSet
hashSet的底层是一个map, 是最高效的set,但是无法保证集合中元素的顺序。
package set;
import java.util.*;
public class TestHashSet {
/**
* 创建一个散列集存储字符串,实现部分方法
* @author Sun1956
*/
public static void main(String[] args) {
//创建一个HashSet
Set<String> set1 = new HashSet<String>();
//添加字符串到set1中
set1.add("London");
set1.add("Paris");
set1.add("New York");
set1.add("San Francisco");
set1.add("BeiJing");
//使用迭代器遍历set1
Iterator<String> iterator = set1.iterator();
System.out.println("Display the elements in set1:");
while(iterator.hasNext()) {
System.out.printf(iterator.next() + " ");
}
System.out.println("\nset1 is " + set1);
System.out.println(set1.size() + " elements in set1");
//remove方法:删除一个字符串
set1.remove("London");
System.out.println("\nset1 is " + set1);
System.out.println(set1.size() + " elements in set1");
//创建set2
Set<String> set2 = new HashSet<String>();
//add method
set2.add("London");
set2.add("ShangHai");
set2.add("Paris");
System.out.println("\nset2 is " + set2);
System.out.println(set2.size() + " elements in set2");
//contains方法:检查是否包含一个字符串
System.out.println("\nIs Taipei in set2? "
+ set2.contains("Taipei"));
//addAll方法 :将另一个set的所有元素添加进来(Union 操作)
set1.addAll(set2);
System.out.println("\nAfter adding set2 to set1, set1 is " + set1);
//removeAll方法 : 删除另一个集合中的元素全部删除(Difference,差集)
set1.removeAll(set2);
System.out.println("After removing set2 from set1, set1 is " + set1);
//retainAll方法 : (得到交集)
set1.retainAll(set2);
System.out.println("Show the common elements with set2 "
+ "that in set1, set1 is " + set1);
}
}
Result:
Display the elements in set1:
San Francisco New York Paris London BeiJing
set1 is [San Francisco, New York, Paris, London, BeiJing]
5 elements in set1
set1 is [San Francisco, New York, Paris, BeiJing]
4 elements in set1
set2 is [Paris, London, ShangHai]
3 elements in set2
Is Taipei in set2? false
After adding set2 to set1, set1 is [San Francisco, New York, Paris, London, ShangHai, BeiJing]
After removing set2 from set1, set1 is [San Francisco, New York, BeiJing]
Show the common elements with set2 that in set1, set1 is []
San Francisco New York Paris London BeiJing
set1 is [San Francisco, New York, Paris, London, BeiJing]
5 elements in set1
set1 is [San Francisco, New York, Paris, BeiJing]
4 elements in set1
set2 is [Paris, London, ShangHai]
3 elements in set2
Is Taipei in set2? false
After adding set2 to set1, set1 is [San Francisco, New York, Paris, London, ShangHai, BeiJing]
After removing set2 from set1, set1 is [San Francisco, New York, BeiJing]
Show the common elements with set2 that in set1, set1 is []
3.3 LinkedHashSet
LinkedHashSet 的最大优点在于,它能保持元素的插入顺序,有些情况非常适合使用LinkedHashSet: 保持List或Array里元素的顺序. 具体实例可以有: 若从数据库里取出一个List的数组, 当然可能通过SQL里的关键字distinct而在数据库里就排重,但可能出于什么特殊的考虑而没有调用distinct,这样在Java里想排重就自然 地想到了Set,而初次想到的当然也就是HashSet, 但写出来后测试时一看结果, 不对呀, 原来从数据库里取出数据明明是已Order好的, 怎么显示出的却乱了, 转念一想可不是嘛: 在用Set的HashSet排重时,它并不关心你原来数据的顺序.这时LinkedHashSet就可救火了.
3.4 TreeSet
TreeSet 的最大优点在于,它可以实现可排序的Set, 比linkedset强大(只能按输入顺序排序)。它就成了 List (有序,可重复) 和 Set(无序,不可重复) 的中间类型。 在特定的时候需要使用TreeSet。
package set;
import java.util.*;
public class TestTreeSet {
/**
* 使用TreeSet改写上一个程序,按照字母顺序显示字符串
* @author Sun1956
*/
public static void main(String[] args) {
//created a hash set
Set<String> set = new HashSet<String>();
//Add
set.add("London");
set.add("Paris");
set.add("San Francisco");
set.add("Beijing");
set.add("New York");
//create a tree set
TreeSet<String> treeSet = new TreeSet<String>(set);
//按字母顺序排序
System.out.println("Sorted tree set: " + treeSet);
System.out.println("first() : " + treeSet.first()); //返回第一个元素
System.out.println("last() : " + treeSet.last()); //返回最后一个
System.out.println("headSet() : "
+ treeSet.headSet("New York")); //返回小于New York的所有元素
System.out.println("tailSet() : "
+ treeSet.tailSet("New York")); //返回大于等于New York的所有元素
System.out.println("lower(\"P\"): " + treeSet.lower("P")); //返回小于P的一个元素
System.out.println("higher(\"P\"): " + treeSet.higher("P")); //大于P
System.out.println("floor(\"P\"): " + treeSet.floor("P")); //小于等于P
System.out.println("ceiling(\"P\"): " + treeSet.ceiling("P")); //大于等于
System.out.println("pollFirst(): " + treeSet.pollFirst()); //删除treeSet中第一个
System.out.println("pollLast(): " + treeSet.pollLast()); //删除treeSet中最后一个
System.out.println("New tree set: " + treeSet); //输出删除后的treeSet
System.out.println();
}
}
Result:
Sorted tree set: [Beijing, London, New York, Paris, San Francisco]
first() : Beijing
last() : San Francisco
headSet() : [Beijing, London]
tailSet() : [New York, Paris, San Francisco]
lower("P"): New York
higher("P"): Paris
floor("P"): New York
ceiling("P"): Paris
pollFirst(): Beijing
pollLast(): San Francisco
New tree set: [London, New York, Paris]
first() : Beijing
last() : San Francisco
headSet() : [Beijing, London]
tailSet() : [New York, Paris, San Francisco]
lower("P"): New York
higher("P"): Paris
floor("P"): New York
ceiling("P"): Paris
pollFirst(): Beijing
pollLast(): San Francisco
New tree set: [London, New York, Paris]
4. Map
4.1 HashTable
HashTable 是
线程安全的Map,实现一个key-value映射的哈希表。任何非空(non-null)的对象都可作为key或者value。添加数据使用put(key, value),取出数据使用get(key),这两个基本操作的时间开销为常数。
由于作为key的对象将通过计算其散列函数来确定与之对应的value的位置,因此任何作为key的对象都必须实现hashCode和equals方法。hashCode和equals方法继承自根类Object,如果你用自定义的类当作key的话,要相当小心,按照散列函数的定义,如果两个对象相同,即obj1.equals(obj2)=true,则它们的hashCode必须相同,但如果两个对象不同,则它们的hashCode不一定不同,如果两个不同对象的hashCode相同,这种现象称为冲突,冲突会导致操作哈希表的时间开销增大,所以尽量定义好的hashCode()方法,能加快哈希表的操作。
如果相同的对象有不同的hashCode,对哈希表的操作会出现意想不到的结果(期待的get方法返回null),要避免这种问题,只需要牢记一条:要同时复写equals方法和hashCode方法,而不要只写其中一个。
4.2 HashMap
HashMap和Hashtable类似,不同之处在于HashMap是非同步的,并且允许null,即null value和null key。,但是将HashMap视为Collection时(values()方法可返回Collection),其迭代子操作时间开销和HashMap的容量成比例。因此,如果迭代操作的性能相当重要的话,不要将HashMap的初始化容量设得过高,或者load factor过低。
5. 总结
如果涉及到堆栈,队列等操作,应该考虑用List,对于需要快速插入,删除元素,应该使用LinkedList,如果需要快速随机访问元素,应该使用ArrayList。如果程序在单线程环境中,或者访问仅仅在一个线程中进行,考虑非同步的类,其效率较高,如果多个线程可能同时操作一个类,应该使用同步的类。要特别注意对哈希表的操作,作为key的对象要正确复写equals和hashCode方法。尽量返回接口而非实际的类型,如返回List而非ArrayList,这样如果以后需要将ArrayList换成LinkedList时,客户端代码不用改变。这就是针对抽象编程。
同步性
Vector是同步的。这个类中的一些方法保证了Vector中的对象是线程安全的。而ArrayList则是异步的,因此ArrayList中的对象并不是线程安全的。因为同步的要求会影响执行的效率,所以如果你不需要线程安全的集合那么使用ArrayList是一个很好的选择,这样可以避免由于同步带来的不必要的性能开销。
Vector是同步的。这个类中的一些方法保证了Vector中的对象是线程安全的。而ArrayList则是异步的,因此ArrayList中的对象并不是线程安全的。因为同步的要求会影响执行的效率,所以如果你不需要线程安全的集合那么使用ArrayList是一个很好的选择,这样可以避免由于同步带来的不必要的性能开销。
数据增长
从内部实现机制来讲ArrayList和Vector都是使用数组(Array)来控制集合中的对象。当你向这两种类型中增加元素的时候,如果元素的数目超出了内部数组目前的长度它们都需要扩展内部数组的长度,Vector缺省情况下自动增长原来一倍的数组长度,ArrayList是原来的50%,所以最后你获得的这个集合所占的空间总是比你实际需要的要大。所以如果你要在集合中保存大量的数据那么使用Vector有一些优势,因为你可以通过设置集合的初始化大小来避免不必要的资源开销。
使用模式
在ArrayList和Vector中,从一个指定的位置(通过索引)查找数据或是在集合的末尾增加、移除一个元素所花费的时间是一样的,这个时间我们用O(1)表示。但是,如果在集合的其他位置增加或移除元素那么花费的时间会呈线形增长:O(n-i),其中n代表集合中元素的个数,i代表元素增加或移除元素的索引位置。为什么会这样呢?以为在进行上述操作的时候集合中第i和第i个元素之后的所有元素都要执行位移的操作。这一切意味着什么呢?
这意味着,你只是查找特定位置的元素或只在集合的末端增加、移除元素,那么使用Vector或ArrayList都可以。如果是其他操作,你最好选择其他的集合操作类。比如,LinkList集合类在增加或移除集合中任何位置的元素所花费的时间都是一样的?O(1),但它在索引一个元素的使用缺比较慢-O(i),其中i是索引的位置.使用ArrayList也很容易,因为你可以简单的使用索引来代替创建iterator对象的操作。LinkList也会为每个插入的元素创建对象,所有你要明白它也会带来额外的开销。
最后,在《Practical Java》一书中Peter Haggar建议使用一个简单的数组(Array)来代替Vector或ArrayList。尤其是对于执行效率要求高的程序更应如此。因为使用数组(Array)避免了同步、额外的方法调用和不必要的重新分配空间的操作。