一、集合类简介
接口 | 简述 | 实现 | 操作特性 | 成员要求 |
Set | 成员不能重复 | HashSet | 外部无序地遍历成员 | 成员可为任意Object子类的对象,但如果覆盖了equals方法,同时注意修改hashCode方法。 |
TreeSet | 外部有序地遍历成员;附加实现了SortedSet, 支持子集等要求顺序的操作 | 成员要求实现caparable接口,或者使用 Comparator构造TreeSet。成员一般为同一类型。 | ||
LinkedHashSet | 外部按成员的插入顺序遍历成员 | 成员与HashSet成员类似 | ||
List | 提供基于索引的对成员的随机访问 | ArrayList | 提供快速的基于索引的成员访问,对尾部成员的增加和删除支持较好 | 成员可为任意Object子类的对象 |
LinkedList | 对列表中任何位置的成员的增加和删除支持较好,但对基于索引的成员访问支持性能较差 | 成员可为任意Object子类的对象 | ||
Map | 保存键值对成员,基于键找值操作,compareTo或compare方法对键排序 | HashMap | 能满足用户对Map的通用需求 | 键成员可为任意Object子类的对象,但如果覆盖了equals方法,同时注意修改hashCode方法。 |
TreeMap | 支持对键有序地遍历,使用时建议先用HashMap增加和删除成员,最后从HashMap生成TreeMap;附加实现了SortedMap接口,支持子Map等要求顺序的操作 | 键成员要求实现caparable接口,或者使用Comparator构造TreeMap。键成员一般为同一类型。 | ||
LinkedHashMap | 保留键的插入顺序,用equals 方法检查键和值的相等性 | 成员可为任意Object子类的对象,但如果覆盖了equals方法,同时注意修改hashCode方法。 | ||
IdentityHashMap | 使用== 来检查键和值的相等性。 | 成员使用的是严格相等 | ||
WeakHashMap | 其行为依赖于垃圾回收线程,没有绝对理由则少用 |
二、基本方法及使用
1.ArrayList
private transient Object[] elementData;
private int size;
public ArrayList(int paramInt)
{
if (paramInt < 0)
throw new IllegalArgumentException("Illegal Capacity: " + paramInt);
this.elementData = new Object[paramInt];
}
public ArrayList()
{
this(10);
}
此处elementData就是它底层用来存放数据的数组元素,仔细看一下,无论采用无参还有有参的构造
函数,最终都归结于一句话:this.elementData = new Object[paramInt];如果没有传入参数的话,会默认开辟一个10个字节大小的空间,可是当我们用的时候,我们写下如下的语句:
List<String> list = new ArrayList<String>( );
List<String> list = new ArrayList<String>(5);
当我们输出它的size值时:System.out.println(list.size());我们发现,输出的都是0.这让人貌似有一丝迷惑,明明是10或者5,这儿应该用清楚,elementData数组的长度并不是size的值,size是里面元素的个数,上面的10或者是5,意思是向内容开辟10个大小的空间,初始化的时候开辟一定数量的内存,但是里面并没有放任何对象,所以用size()计算得到的结果仍为0.
这时,我们又有新问题了,因为我们知道List是可以自动扩容的,这个功能就取决于如下的方法:
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);
}
}
ensureCapacity(int ensureCapacity)用来初始化或者扩大ArrayList的空间。
从上述代码中可以看出,数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。
ArrayList还给我们提供了将底层数组的容量调整为当前列表保存的实际元素的大小的功能。它可以通过trimToSize方法来实现。代码如下:
从上述代码中可以看出,数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。
这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。
此外,通过ensureCapacity(int ensureCapacity)方法可以提高ArrayList的初始化速度,请看下面的代码:
public class EnsureCapacityTest {
@SuppressWarnings("unchecked")
public static void main(String[] args) {
final int N = 1000000;
Object obj = new Object();
/*没用调用ensureCapacity()方法初始化ArrayList对象*/
ArrayList list = new ArrayList();
long startTime = System.currentTimeMillis();
for (int i = 0; i <= N; i++) {
list.add(obj);
}
long endTime = System.currentTimeMillis();
System.out.println("没有调用ensureCapacity()方法所用时间:"
+ (endTime - startTime) + "ms");
/*调用ensureCapacity()方法初始化ArrayList对象*/
list = new ArrayList();
startTime = System.currentTimeMillis();
// 预先设置list的大小
list.ensureCapacity(N);
for (int i = 0; i <= N; i++) {
list.add(obj);
}
endTime = System.currentTimeMillis();
System.out.println("调用ensureCapacity()方法所用时间:" + (endTime - startTime)
+ "ms");
}
}
输出:
没有调用ensureCapacity()方法所用时间:97ms
调用ensureCapacity()方法所用时间:37ms
很明显,使用ensureCapacity()能提高不少效率!
ArrayList还给我们提供了将底层数组的容量调整为当前列表保存的实际元素的大小的功能。它可以通过trimToSize方法来实现。代码如下:
public void trimToSize() {
modCount++;
int oldCapacity = elementData.length;
if (size < oldCapacity) {
elementData = Arrays.copyOf(elementData, size);
}
}
ArrayList基于数组实现,所以它具备数组的特点,即查询速度较快,但是修改、插入的速度却有点儿慢,但是,下面将要介绍的LinkedList就是来解决这个问题的,LinkedList基于链表,与ArrayList互补,所以实际开发中我们应该按照自己的需求来定到底用哪一个。
2.LinkedList
LinkedList底层采用双向循环列表实现,进行插入和删除操作时具有较高的速度,我们还可以使用LinkedList来实现队列和栈。
private static class Entry<E> {
E element;
Entry<E> next;
Entry<E> previous;
Entry(E element, Entry<E> next, Entry<E> previous) {
this.element = element;
this.next = next;
this.previous = previous;
}
}
这是LinkedList的原始存储模型,因为是双向循环列表,我们可以回忆一下数据结构中双向列表是什么情况:一个数据data,两个指针,一个指向前一个节点,名为previous,一个指向下一个节点,名为next,但是循环怎么体现了,来看下无参构造函数:
public LinkedList() {
header.next = header.previous = header;
}
头尾相等,就是说初始化的时候就已经设置成了循环的。仔细观察源码,不难理解,如果熟悉数据结构的读者,一定很快就能掌握她的原理。下面分析LinkedList的add()。
public boolean add(E e) {
addBefore(e, header);
return true;
}
private Entry<E> addBefore(E e, Entry<E> entry) {
Entry<E> newEntry = new Entry<E>(e, entry, entry.previous);//-------1---------
newEntry.previous.next = newEntry;
newEntry.next.previous = newEntry;
size++;
modCount++;
return newEntry;
}
我们先来观察下上面给出的Entity类,构造方法有三个参数,第二个是她的next域,第三个是她的previous域,所以上述代码1行处将传进来的entry实体,即header对象作为newEntry的next域,而将entry.previous即header.previous作为previous域。也就是说在header节点和header的前置节点之间插入新的节点。
三、比较(性能,功能方面)
1、HashMap和HashTable
相同点:二者都实现了Map接口,因此具有一系列Map接口提供的方法。
不同点:
1、HashMap继承了AbstractMap,而HashTable继承了Dictionary。
2、HashMap非线程安全,HashTable线程安全,到处都是synchronized关键字。
3、因为HashMap没有同步,所以处理起来效率较高。
4、HashMap键、值都允许为null,HashTable键、值都不允许有null。
5、HashTable使用Enumeration,HashMap使用Iterator。
这些就是一些比较突出的不同点,实际上他们在实现的过程中会有很多的不同,如初始化的大小、计算hash值的方式等等。毕竟这两个类包含了很多方法,有很重要的功能,所以其他不同点,请感兴趣的读者自己去看源码,去研究。笔者推荐使用HashMap,因为她提供了比HashTable更多的方法,以及较高的效率,如果大家需要在多线程环境中使用,那么用Collections类来做一下同步即可。
2、Set接口和List接口
相同点:都实现了Collection接口
不同点:
Set接口不保证维护元素的顺序,而且元素不能重复。List接口维护元素的顺序,而且元素可以重复。
3、ArrayList和LinkList
相同点:都实现了Collection接口
不同点:ArrayList基于数组,具有较高的查询速度,而LinkedList基于双向循环列表,具有较快的添加或者删除的速度,二者的区别,其实就是数组和列表的区别。上文有详细的分析。
4、SortedSet和SortedMap
二者都提供了排序的功能。 来看一个小例子:
public static void main(String[] args) {
SortedMap<String, Integer> map = new TreeMap<String, Integer>();
map.put("zgg", 1);
map.put("erqing", 3);
map.put("niu", 0);
map.put("abc", 2);
map.put("aaa", 5);
Set<String> keySet = map.keySet();
for (String string : keySet) {
System.out.print(map.get(string)+" ");
}
}
输出:5 2 3 0 1
从结果看得出:SortedMap具有自动排序功能
5、TreeMap和HashMap
HashMap具有较高的速度(查询),TreeMap则提供了按照键进行排序的功能。
6、HashSet和LinkedHashSet
HashSet,为快速查找而设计的Set。存入HashSet的对象必须实现hashCode()和equals()。
LinkedHashSet,具有HashSet的查询速度,且内部使用链表维护元素的顺序(插入的次序),于是在使用迭代器遍历Set时,结果会按元素插入的次序显示。
7、TreeSet和HashSet
TreeSet: 提供排序功能的Set,底层为树结构 。相比较HashSet其查询速度低,如果只是进行元素的查询,我们一般使用HashSet。
8、ArrayList和Vector
同步性:Vector是线程安全的,也就是说是同步的,而ArrayList是线程序不安全的,不是同步的。
数据增长:当需要增长时,Vector默认增长为原来一培,而ArrayList却是原来的一半
9、Collection和Collections
Collection是一系列单值集合类的父接口,提供了基本的一些方法,而Collections则是一系列算法的集合。里面的属性和方法基本都是static的,也就是说我们不需要实例化,直接可以使用类名来调用。下面是Collections类的一些功能列表:
生成单元素集合
Collections中的单元素集合指的是集合中只有一个元素而且集合只读。
Collections.singletonList——用来生成只读的单一元素的List
Collections.singletonMap——用来生成只读的单Key和Value组成的Map
Collections.singleton——用来生成只读的单一元素的Set
如下面的例子:
public static void main(String[] args) {
Map<Integer, Integer> map = Collections.singletonMap(1, 1);
//map.put(2, 2); ----------1-------------
System.out.println(map.size());
}
Collections.singletonMap(1, 1)生成一个单元素的map,如果加上1处的代码,会报异常。
Checked集合
Checked集合具有检查插入集合元素类型的特性,例如当我们设定checkedList中元素的类型是String的时候,如果插入其他类型的元素就会抛出ClassCastExceptions异常,Collections中提供了以下生成Checked集合的方法checkedCollection,checkedList,checkedMap,checkedSet,checkedSortedMap,checkedSortedSet
同步集合
Collections类提供一系列同步方法,为一些非线程安全的集合类提供同步机制。
查找替换
fill——使用指定元素替换指定列表中的所有元素。
frequency——返回指定 collection 中等于指定对象的元素数。
indexOfSubList—— 返回指定源列表中第一次出现指定目标列表的起始位置,如果没有出现这样的列表,则返回 -1。
lastIndexOfSubList——返回指定源列表中最后一次出现指定目标列表的起始位置,如果没有出现这样的列表,则返回-1。
max—— 根据元素的自然顺序,返回给定 collection 的最大元素。
min——根据元素的自然顺序 返回给定 collection 的最小元素。
replaceAll——使用另一个值替换列表中出现的所有某一指定值。
附一个小例子:
public static void main(String[] args) {
List<Integer> list = new ArrayList<Integer>();
list.add(1);
list.add(2);
for (Integer integer : list) {
System.out.println(integer);
}
/*找出最大值*/
int max = Collections.max(list);
System.out.println("最大的为:"+max);
/*用指定元素替换指定list中的元素*/
Collections.fill(list, 6);
System.out.println("替换后:");
for (Integer integer : list) {
System.out.println(integer);
}
/*找出某个list里某个元素的个数*/
int count = Collections.frequency(list, 6);
System.out.println("里面有6的个数:"+count);
}
输出:
1
2
最大的为:2
替换后:
6
6
里面有6的个数:2
集合排序
Collections还提供了集中对集合进行排序的方法。
reverse——对List中的元素进行转置
shuffle——对List中的元素随即排列
sort——对List中的元素排序
swap——交换List中某两个指定下标位元素在集合中的位置。
rotate——循环移动
public static void main(String[] args) {
List<Integer> list = new ArrayList<Integer>();
list.add(5);
list.add(2);
list.add(1);
list.add(9);
list.add(0);
System.out.println("排序前:");
for (Integer integer : list) {
System.out.print(integer+" ");
}
System.out.println();
/*排序*/
Collections.sort(list);
System.out.println("排序后");
for (Integer integer : list) {
System.out.print(integer+" ");
}
}
输出:
排序前:
5 2 1 9 0
排序后
0 1 2 5 9
下面是关于rotate(List<?> list, int distance)的一个例子:
- public static void main(String[] args) {
- List<Integer> list = new ArrayList<Integer>();
- list.add(5);
- list.add(2);
- list.add(1);
- list.add(9);
- list.add(0);
- System.out.println("原序列:");
- for (Integer integer : list) {
- System.out.print(integer+" ");
- }
- System.out.println();
- /*根据步长进行循环*/
- Collections.rotate(list, -1);
- System.out.println("循环后:");
- for (Integer integer : list) {
- System.out.print(integer+" ");
- }
- }
四、常见问题
1、Set集合如何保证对象不重复
这儿我采用HashSet来实现Set接口,先看个例子:
输出:
size:1
hello
说明,Set集合不允许有重复出现的对象,且最终的判断是根据equals()的。其实原理是这样的:HashSet的底层采用HashMap来存放数据,HashMap的put()方法是这样的:
当向HashMap中添加元素的时候,首先计算元素的hashcode值,然后根据1处的代码计算出Hashcode的值,再根据2处的代码计算出这个元素的存储位置,如果这个位置为空,就将元素添加进去;如果不为空,则看3-4的代码,遍历索引为i的链上的元素,如果key重复,则替换并返回oldValue值。
2、集合类排序问题
一种情况是集合类本身自带排序功能,如前面说过的TreeSet、SortedSet、SortedMap等,另一种就是本身不带排序功能,我们通过为需要排序的类实现Comparable或者Comparator接口来实现。
先来看两个例子,一个是实现Comparable的,一个是实现Comparator的,为了方便,我将类都写在了一个文件中。
下面是实现Comparator接口的:
通过上面的这两个小例子,我们可以看出,Comparator和Comparable用于不同的场景,实现对对象的比较从而进行排序。
总结为:
相同点:
1、二者都可以实现对象的排序,不论用Arrays的方法还是用Collections的sort()方法。
不同点:
1、实现Comparable接口的类,似乎是预先知道该类将要进行排序,需要排序的类实现Comparable接口,是一种“静态绑定排序”。
2、实现Comparator的类不需要,设计者无需事先为需要排序的类实现任何接口。
3、Comparator接口里有两个抽象方法compare()和equals(),而Comparable接口里只有一个方法:compareTo()。
4、Comparator接口无需改变排序类的内部,也就是说实现算法和数据分离,是一个良好的设计,是一种“动态绑定排序”。
5、Comparator接口可以使用多种排序标准,比如升序、降序等。
3、使用for循环删除元素陷阱
先来看看下面这个程序:
读者朋友们可以先猜猜这个程序输出什么?按我们的思路,应该是输不出什么,但是执行它,输出的却是:B。这是为什么呢?我们分部分析下这个程序,当地一步remove完后,集合内还剩2个元素,此时i为1,而list.size()的值为2,从0开始的话,i为1时,正好指向第二个元素,也就是说当remove完A后,直接就跳到C,将B漏了。