1、JDK1最早的集合类:Vector、Stack、Hashtable、BitSet、Enumeration
2、JDK1.2后,设计了新的集合接口,并且接口和实现分离。接口如List、Map等
例如链表,可以用循环数组,也可以用传统链表实现:
List<String> lst = new ArrayList<String>(); List<String> lst = new LinkedList<String>();
3、此外还有以Abstract开头的抽象类,如AbstractList,在List基础上实现了基本的方法,如果要自定义新的实现,用它们会更容易。
4、Collection是所有集合对象的父接口,有先看两个方法:
public Interface Collection<E> { boolean add(E element); Iterator<E> iterator(); }
第一个add就是向集合中添加元素,如果成功返回true。
第二个iterator用于迭代元素使用,返回的Iterator也是一个接口:
public Interface Iterator<E> { E next(); boolean hasNext(); void remove(); }
这个结构在Java中太常见了:
先调用hasNext方法,如果返回true,则表示有元素,访问next()获取下一个元素。否则表示没下一个元素了。
以ArrayList为例:
import java.util.*; public class AA { public static void main(String [] args) { ArrayList<String> lst = new ArrayList<String>(); lst.add("1"); lst.add("2"); lst.add("3"); Iterator<String> itr = lst.iterator(); while(itr.hasNext()) { System.out.println(itr.next()); } } }
在JDK5之后,支持了foreach,因此有了更优雅的实现:由for循环隐式处理了iterator:
ArrayList<String> lst = new ArrayList<String>(); lst.add("1"); lst.add("2"); lst.add("3"); for(String e: lst) { System.out.println(e); }
要注意的是:ArrayList等访问次序是固定的,而HashSet等访问次序是随机的,但一定能保证每个元素被访问到且只被访问到一次。
5、刚才的Iterator接口还有一个remove方法,显然是用于删除的:
在调用一次remove前,必须调用一次next() (才能定位到第一个元素啊,切记!)
因此如果连续删除两个remove(),必须
itr.remove();
itr.next();
itr.remove();
才可以!
6、Collection还有一些可能有用的方法:
public Interface Collection { //判断是否含元素? boolean contains(Object o); //集合是否为空 boolean isEmpty(); // 是否等于另外一个集合 boolean equals(Object o); // 删除所有与集合c中不同的元素,如果集合有改变,返回true boolean retainAll(Collection<?> c) //转化为数组 <T> T[] toArray(T[] a); ...... }
7、前面说了,直接实现这些方法可能会很困难,好在我们有AbstractCollection,它已经帮助我们实现了很多无聊的方法。
8、除了以Map结尾的具体集合类来自接口Map,其他都源自于接口Collection。
9、常用的集合类:
ArrayList: 一种可以动态增长和缩减的索引序列(动态数组,类似于C++中的vector);
LinkedList: 一种可以在任何位置进行高效的插入和删除操作的有序序列(类似于C++中list);
ArrayDeque: 一种用循环数组实现的双端队列(类似于C++中的deque);
HashSet:一种没有重复元素的无序集合(C++的标准库中并未提供hashset集合,但是Windows的VC和Linux平台下的gcc均各自提供了hashset容器);
TreeSet: 一种有序集(类似于C++中的set);
EnumSet: 一种包含枚举类型值的集;
LinkedHashSet: 一种可以记住元素插入次序的集,在其内部由LinkedList负责维护插入的次序,HashSet来维护Hash;
PriorityQueue: 优先队列!
HashMap:一种存储键值对关联的数据结构(C++的标准库中并未提供hashmap集合,但是Windows的VC和Linux平台下的gcc均各自提供了hashmap容器);
TreeMap:一种键值有序排列的映射表(类似于C++中的map);
EnumMap:一种键值属于枚举类型的映射表;
LinkedHashMap:一种可以记住键值项插入次序的映射表;
WeakHashMap: 一种当key不被除了map之外的对象持有时,释放对应的key和value(被垃圾回收的Map)??
IdentityHashMap: 一种用==比较而不是用equals比较的HashMap
10、ArrayList相当于可变的数组,在中间插入、删除元素会耗费很大,此时可以用LinkedList实现。它使用纯双向链表结构,在任意位置插入、删除都是O(1)的。
11、ArrayList和LinkedList都继承自List接口。对于LinkedList来说,除了可以直接add添加到尾部之外,还可以调用它的listIterator以获取ListIterator,然后调用它的add()方法,插入到任意位置!
这个Iterator是ListIterator接口,除了add之外还有一对方法:
E previous() ;
boolean hasPrevious();
用于反向遍历链表。
例子:
List<String> lst = new LinkedList<String>(); lst.add("1"); lst.add("2"); lst.add("3"); ListIterator<String> itr = lst.listIterator(1); itr.add("100"); for(String e: lst) { System.out.println(e); }
ListIterator还支持set(E e),将e取代当前itr上的数值。
12、如果某个迭带器对集合进行了修改,另一个迭带器正在遍历,那么会抛出异常ConcurrentModificationException,这是很显然的。
13、链表是不支持随机访问的,即如果要访问第i个,那么要从链表头一直找到第i-1个,这是很低效率的。尽管如此,Java还是实现了LinkedList的get(int i)方法,使用时候要非常的谨慎!
14、链表迭带器ListIterator还可以返回当前、前一个、下一个的index数值:
int previousIndex()
和
int nextIndex()
下面这个例子将创建一个链表,然后再逐一打印、删除链表中的元素。
import java.util.*; public class LL { public static void main(String [] args) { List<String> lst = new LinkedList<String>(); lst.add("1"); lst.add("2"); lst.add("3"); ListIterator<String> itr = lst.listIterator(); while(itr.hasNext()) { System.out.println("current value:"+itr.next()); itr.remove(); } System.out.println(lst.size()); } }
15、ArrayList的set和get提供高效的随机访问(都是O(1)),它的内部是一个可重新动态分配的数组。同时,在获取随机访问性能的同时,在任意位置的删除的代价会很昂贵。
16、散列表(Hash Table)用于在一个集合中快速的找到某个元素,Hash桶中的每个元素最多只能出现一次!
17、Java的散列表的散列值一般由类的hashCode()产生。如果要自定义类,就要负责这个类的hashCode()。同时hashCode()需要与equals()保持一致,如果equals返回true,则它们的HashCode必须一样!
18、Java的散列表是桶(Bucket)+数组(Array)实现的。
(1)公有N个桶,每个桶实际是一个链表(例如LinkedList)
(2)元素到来时,计算hashCode,用hashCode % N,即为这个元素应在的桶号
(3) 如果桶为空,直接放到链表中,如果不是空(此时称为冲突),要依次比较这个桶内的所有元素,确定没有重复的,再放入。
19、显然,桶的数量是固定的,当放入元素过多时,就会经常发生碰撞。因此,Java对哈希表引入了装填因子的概念:即桶数/元素数。默认为0.75,当装填因子大于它时(即元素过多),就会把桶数量×2,然后对已经有的元素重新做散列!
20、这个0.75是比较合理的,一般不要改变它。顺便说一句,Java的哈希表得益于散列表,性能是很好的。此外,初始桶的数量是2^4=16.
21、利用散列表的原理,Java提供了很多实现,如HashSet,用于快速检查元素是否存在于集合中。下面的例子,统计stdin输入了多少个不同的单词:
import java.util.*; public class WordCount { public static void main(String [] args) { Set<String> words_set = new HashSet<String>(); // Input from System.in, read al words Scanner scan = new Scanner(System.in); while(scan.hasNext()) { // Unique words using HashSet String word = scan.next(); words_set.add(word); } // Print all words and total number of words for(String word: words_set) { System.out.print(word); System.out.print(" "); } System.out.println(); System.out.println(words_set.size()); } }
22、TreeSet比HashSet略有改进,它的元素之间是有大小序的(不是add的顺序)。因此它的插入比HashSet慢。可以直接用其他的Collection构造一个TreeSet:
public TreeSet(Collection<? extends E> c)
23、TreeSet的大小比较按照Comparable<E>接口的compareTo进行。因此,放到TreeSet中的元素必须实现了Comparable接口。也可以通过Comparator接口实现:
public Interface Comparator<T> { int compare(T o1, T o2); }
24、从JDK6后,TreeSet额外实现了NavigableSet接口,更好的支持了反向遍历,比较等。
25、JDK6之后,引入了双端队列接口Deque,在头和尾插入、删除元素非常高效。ArrayDeque和LinkedList都实现了双端队列:
下面两个都是在队列尾部插入元素。区别是空间不够时,第一个抛出异常,第二个返回false。
boolean add(E element) boolean offer(E element)
下面两个方法都是从队列头部删除,同理,如果队列为空,第一个抛出异常,第二个返回null。
E remove() E pool()
下面方法是返回队列头部元素,并不删除。当空时,也是第一个异常,第二个null。
E element() E peek()
26、对于ArrayDeque,是有容量限制的:
public ArrayDeque(int numElements)
如果不指定,用默认构造函数,则容量为16。
27、PriorityQueue是优先级队列(继承自java.util.AbstractQueue类),内部用堆实现,也是通过Comparable接口比较大小。
下面的例子,从System.in中读取若干的树,放到优先队列中,最后输出排序的数组。比起传统的先收集、再排序方法,优点是可以边I/O边计算。
import java.util.*; public class PriorityQueueTest { public static void main(String [] args) { //Input and put into PriorityQueue PriorityQueue<Integer> pq = new PriorityQueue<Integer>(); Scanner scan = new Scanner(System.in); while(scan.hasNextInt()) { pq.add(scan.nextInt()); } //Output System.out.println("Result:"); for(Integer e: pq) { System.out.println(e); } } }
28、映射表(Map)的设计目标:根据一个key(如姓名),快速的找到value(如员工的详细信息)。Java提供了Map接口,两个常用的Map是HashMap和TreeMap。
Map的一个最主要方法是put:
public Interface Map { V put(K key, V value); }
同一个key只能有一个value,如果同一个key调用了两次put,第二次的会替换掉第一次的。
remove方法用于删除key以及对应的value:
public InterfaceMap<K, V> { V remove(Object key); }
remove返回的是被删除的key对应的value。
29、遍历Map中的所有key:
Set<K> keySet()
遍历所有的Value:
Collection<V> values()
同时遍历key和value:
Set<Map.Entry<K,V>> entrySet()
注意上面这个Set内部实际是Map.Entry,两个主要方法:
K getKey() //和 V getValue()
下面的例子添加、删除、替换、遍历Map:
import java.util.*; public class MapTest { public static void main(String [] args) { // Put into map Map<String, Integer> map = new HashMap<String, Integer>(); map.put("li", 1); map.put("liu", 2); map.put("wang", 3); // Remove map.remove("wang"); // Replace map.put("li", 10); // Iterator for(Map.Entry<String, Integer> entry: map.entrySet()) { System.out.print(entry.getKey()); System.out.print(":"); System.out.println(entry.getValue()); } } }
30、弱引用的散列表:WeakHashMap,如果一个value对应的key已经没有其他引用了,WeakHashMap将负责销毁它。而其他的Map是不会自动销毁的(除非你自己去remove它)。他的内部使用了弱引用WeakReference。
31、链接散列表LinkedHashSet和LinkeHashMap,他们让集合内部保持插入的顺序(insert-mode)或者访问顺序(access-mode),这个在构造函数的第三个参数选项,默认是插入顺序。
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder);
32、按照访问顺序的模式可以实现LRU等:启用accessOrder后,每次get或者put的元素会被移到队列尾部。再配合initialCapacity和removeEldestEntry,就是一个LRU了。
33、EnumSet是枚举Enum集合的Set类,Set的T类型必须是enum。EnumMap的Key的T类型必须是enum,Value类型随意。
34、IdentityHashMap是特殊的Map,它的key散列值不是来自hashCode(),而是System.identityHashCode,方法是内存地址。因此在比较两个IdentityHashMap时,应当使用==而不是equals。
35、框架(Framework)包含了一系列可以完成高级功能的超类。Collection的基础方法:add(E elem),Map接口的基本方法:V put(K key, V value)和V get (K key)。
36、List接口在Collection的基础上额外添加了:add(index, elem)、E get(index)和void remove(index),主要是为了数组的随机访问操作。
37、为了防止使用LinkedList的随机访问操作带来的低效率,可以用RandomAccess接口进行instanceof判断:
if (c instanceof RandomAcces) { //use random access } else { //don't use random access, may be very slow }
ArrayList和Vector都实现了上述接口,而相反,LinkedList就没有实现RA接口。
38、ListIterator接口具有add方法,即在List只添加到尾部的add方法基础上,可以再任意可iter的位置上进行add。
39、Java中的具体类如HashMap等都是根据AbstractXXX抽象类实现的。具体集合接口和类结构见书592页。
40、JDK1还遗留了一些集合类:Vector、Stack、Hashtable、Properties。他们也被集成到了现有的集合类结构中。如Vector继承自AbstractList。
41、视图(views),如Map的keySet()方法,获得其他实现了集合接口和操作的对象。
42、java.util.Arrays/java.util.Collections提供了一系列static方法:
(1)将数组转化为List:
Card [] cards = new Card[50]; List<Card> cardList = Arrays.asList(cards);
在JDK5之后,asList还可以接受可变参数:
List<String> cardList = Arrays.asList("Li", "Ao", "an");
(2)创建N个重复的实例:
List<String> lst = Collections.nCopies(100, "STRING");
43、子范围,其实Java也支持类似Python的slice,只不过需要调用函数:
public Interface List { List<E> subList(int fromIndex, int toIndex); }
对于SortedMap和SortedSet等,可以根据大小(equals)选择子集:
public class SortedSet { SortedSet<E> subSet(E fromElement, E toElement); }
44、Collections还可以创建不可修改的视图(不可变的List、Map等):
传入一个List,返回一个不可变的List:
public Class Collections { public static <T> List<T> unmodifiableList(List<? extends T> list); }
类似的还有Map、Set、SortedSet、SorteMap、Collection等。
45、多线程视图:如果多线程使用,可以用Collections包装成同步Map、List等:
如Map:
public Class Collections { public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m); }
包装好后的Map在内部确保了外部多线程get和set没有问题。
类似的还可以包装List、Set等。
46、上述创建的这些视图,当发生无法完成的操作时,会抛出UnsupportedOperationException,如只读的,你却set了。
47、集合的交:可用retainsAll实现。
48、集合到数组的转换:用toArray:
Object[] toArray(); <T> T[] toArray(T[] a);
49、集合的接口方式,可以方便的根据不同集合类型的特性,高效的实现各种算法如:ArrayList可以随机访问,而LinkedList就最好不要随机访问。
50、Collections.sort提供了排序算法,数之间的比较可以通过Comparable实现,也可以单独的Compator类来传入(反向排序也可以这么搞):
public class Collections { //要求元素实现了Comparable接口 public static <T extends Comparable<? super T>> void sort(List<T> list); //也可以单独传入比较器Comparator public static <T> void sort(List<T> list, Comparator<? super T> c); }
51、Java中排序算法是归并排序,缺点是反复拷贝可能略微耗空间、有点是排序是稳定的。
52、Collections.shuffle可以讲List中的元素随机化,原来不止是Python有。
class Collections { public static void shuffle(List<?> list); }
下面的例子讲元素随机化再排序:
import java.util.*; public class SortTest { public static void main(String [] args) { // Add List<Integer> lst = new ArrayList<Integer>(); for(int i=0; i< 20; i++) { lst.add(i); } // Random and print Collections.shuffle(lst); for(Integer e: lst) { System.out.print(e); System.out.print(" "); } System.out.println(); // Sort and Pring Collections.sort(lst); for(Integer e: lst) { System.out.print(e); System.out.print(" "); } System.out.println(); } };
53、如果List是有序的,我们可以利用Collections.binarySearch(c, elem),这个要求实现了Comparable,也可以自己传入compartor。
注意,二分查找返回的是index,如果index<0,表示没有找到,但是可以用返回的负值计算如果插入这个e,应该插入的位置:
insertPos = -i - 1,还是比较先进的。。
// Collections public static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key);
54、Collections还提供了一些简单的算法:
Collections.min
Collections.max
Collections.copy(List to , List from)
Collections.fill(List l, T value)
Collections.replaceAll(List l, T old, T new) // 将list中所有的old替换为new
Collections.indexOfSubList(List l, List s)
Collections.swap(List l, int i, int j) //交换
Collections.reverse(List l) // 翻转List
Collections.rotate(List l, int d) //将i移动到 (i+d) % .lsize()
Collections.frequency(Collection c, Oject c) // 返回c中与o相同的对象数量
Collections.disjoint(Collection c1, Collection c2) // 如果两个集合完全没有交集,返回true
55、JDK1遗留了很多集合类:
Hashtable:和HashMap类一样,但它直接是线程安全的。
Vector:也是同步的,类似LinkedList
56、Eumeration也是遗留的类,对元素进行遍历。有时候JDK的其他类也用到了他们。主要是hasMoreElements和nextElement.
57、Properties:键、值表,实际上也可以用于配置文件!
Properties()
Properties(Properties default) // 提供属性的默认值
String getProterty(String key, String defaultVal) // 返回键对应的
void load(InputStream in)
void store(OutputStream out, String commentString)
58、Properties内部使用的是类似ini的格式。
59、JDK1提供的Stack完成的栈的功能,Stack。方法:
E push(E item)
E pop()
E peek() //返回栈顶但不弹出,空返null
60、Java也有BitSet类,get(i),set(i)、clear、xor等和C++的基本相同。
61 、据说用Java的BitSet类做筛法求素数,200万内,Java以105毫秒秒杀C++的360毫秒。
有时候不要对Java的速度有偏见,都到了JDK7的时代了,很多优化已经远领先于C++了。
本章完毕。