PS: 以下内容有参考其他内容,也有自己的总结。
目录
集合 | 有序否 | 是否允许元素重复 | ||
List | ArrayList | 按照存入的顺序建立索引 | 是 | |
LinkedList | ||||
Vector | ||||
Set | HashSet | 会对存入的元素进行排序 | 否 | |
SortedSet | TreeSet | |||
Map | HashMap | 按照存入的顺序建立索引 | 使用key-value来映射和存储数据,key必须唯一,value可以重复 | |
SortedMap | TreeMap | |||
HashTable |
说明:
1)Map虽然也属于集合体系,但是和Collection不同,Map是key和value的映射集合,其中key列就是一个集合。
2)SortedSet和SortedMap接口对元素按照指定规则排序,SortedMap是对key列进行排序。
3)因为TreeSet是SortedSet接口的子类,所以此接口所有类都是可以排序的。
一、List
先看看List的基本类基本体系结构:
1).Vector:基于Array的List,其实就是封装了Array所不具备的一些功能,方便我们使用,它不可能不受Array的限制。性能也就不可能超越Array。所以在可能的情况下,能用Array就用Array。另外很重要的一点就是Vector是线程同步的(通过synchronized关键字),这个也是Vector和ArrayList的唯一区别。
Stack:这个类从Vector派生而来,并且增加了方法实现栈,一种先进后出的存储结构。该类也是线程同步的(synchronized)
2).ArrayList:通Vector一样是一个基于Array上的链表,但是不同的是ArrayList不是同步的,所以在性能上要比Vector优越一些,但是当运行在多线程环境中的时候,需要自己管理同步问题。
3).LinkedList:不同于前面两种List,它不是基于Array的,所以不受Array的性能限制。它的每一个节点(Node)都包含两方面的内容:①节点本身的数据(data);②下一个节点的信息(nextNode)。所以当对LinkedList做添加、删除动作的时候就不用像基于Array的List一样,必须进行大量的数据移动,只要更改nextNode的相关信息就可以了。这就是LinkedList的优势。
总结:
所有的List中只能容纳单个不同类型的对象组成的表,而不是key-value键值对。
所有的List中可以有相同的元素。例如Vector中可以有[tom,mary,david,mary]。
所有的List中可以有null元素,例如[tom,null,mary]。
基于Array的List(Vector、ArrayList)适合查询,而LinkedList适合添加、删除操作。
二、Set
先看看set的类结构体系:
1).Set的实现基础是Map(HashMap),这个在Set的源码中可以非常明显的看出来
2).Set中的元素是不能重复的,如果使用add(Object obj)方法添加已经存在的对象,则会覆盖前面的对象
3).HashSet:虽然Set和List都实现了Collection接口,但是它们的实现方式却不大一样。List基本上是以Array为基础,但是Set则是在HashMap的基础上来实现的,这个就是Set和List的根本区别。HashSet的存储方式是把HashMap中的key作为Set的对应存储项。看看HashSet的add(Object obj)方法的实现就可以一目了然了。
这个也是为什么在Set中不能像在List中一样有重复项的根本原因,因为HashMap的key是不能重复的。
4).LinkedHashSet:HashSet的一个子类,一个链表。
5).TreeSet:SortedSet的子类,它不同于HashSet的根本就是TreeSet是可以排序的,它是通过SortedMap来实现(内部是一个TreeMap)。以升序排序其中的元素。
如下两个例子:
private static void testTreeSet() {
//打印结果是有序的(会对存入的元素排序)
Set<String> set = new TreeSet<>();
set.add("3t3t3");
set.add("gerg");
set.add("jopij");
set.add("abc");
set.add("2134fwe");
for (String s : set){
System.out.println(s);
}
}
执行结果:
2134fwe
3t3t3
abc
gerg
jopij
private static void testHashSet() {
//打印结果是无序的,也不是添加进去的顺序,而是打乱的
Set<String> set = new HashSet<>();
set.add("3t3t3");
set.add("gerg");
set.add("jopij");
set.add("abc");
set.add("2134fwe");
for (String s : set){
System.out.println(s);
}
}
执行结果:
gerg
abc
3t3t3
jopij
2134fwe
三、Map
先来看看Map的类体系结构:
1).HashTable:实现一个映射,所有的键必须非空。为了能高效的工作,定义键的类必须实现hashCode()方法。这个类是前面java实现的一个继承,并且通常能在实现映射的其他类中更好的使用。
并且HashTable是线程安全的。
2).HashMap:实现一个映射,允许存储空对象(null或空字符串""),而且允许键值空(null或空字符串"")(由于键必须是唯一的,当然只能有一个)。
3).LinkedHashMap:继承自HashMap,但是内部维护了一个双向链表,可以保持插入的顺序。
4).TreeMap:实现一个映射,对象是按键升序排序的。
看下面例子:
private static void testHashTable() {
Map<String, String> map = new Hashtable<>();
map.put("re", "f");
map.put("ghe", "a");
map.put("ht", "h");
map.put("ab", "b");
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println(entry.getKey() + ":" + entry.getValue());
}
}
运行结果:
ghe:a
ht:h
re:f
ab:b
private static void testHashMap() {
Map<String, String> map = new HashMap<>();
map.put("re", "f");
map.put("ghe", "a");
map.put("ht", "h");
map.put("ab", "b");
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println(entry.getKey() + ":" + entry.getValue());
}
}
运行结果:
ab:b
re:f
ghe:a
ht:h
private static void testLinkedHashMap() {
Map<String, String> map = new LinkedHashMap<>();
map.put("re", "f");
map.put("ghe", "a");
map.put("ht", "h");
map.put("ab", "b");
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println(entry.getKey() + ":" + entry.getValue());
}
}
运行结果(保持了存入时的顺序):
re:f
ghe:a
ht:h
ab:b
private static void testTreeMap() {
Map<String, String> map = new TreeMap<>();
map.put("re", "f");
map.put("ghe", "a");
map.put("ht", "h");
map.put("ab", "b");
for (Map.Entry<String, String> entry : map.entrySet()) {
System.out.println(entry.getKey() + ":" + entry.getValue());
}
}
运行结果(有序):
ab:b
ghe:a
ht:h
re:f
需要高度注意,Map类集合K/V能不能存储null值的情况,如下表:
集合类 | key | value | super | 说明 |
TreeMap | 不允许为null (如果为null,会空指针异常,代码有判空) | 允许为null | AbstractMap | 线程不安全 |
HashTable | 不允许为null(如果为null,会空指针异常,会调用key的hashCode方法,如果为null的话,调的时候会空指针) | 不允许null (如果为null,会空指针异常,有判空) |
| 线程安全 |
HashMap | 允许为null | 允许为null | AbstractMap | 线程不安全 |
ConcurrentHashMap | 不允许为null (如果为null,会空指针异常,因为代码里面有是否为null的判断) | 不允许为null (如果为null,会空指针异常,因为代码里面有是否为null的判断) | AbastractMap | 分段锁技术,效率比较高 |
四、Collections
Collections是针对集合类的一个帮助类。提供了一系列静态方法,实现对各种集合的搜索、排序、线程安全化等操作。
相当于对Array进行类似操作的类Arrays。
如:
Collections.max(Collection coll); //取coll中最大的元素
Collections.sort(List list); //对list中元素排序
五、集合使用注意事项
1. Collection没有get()方法来取得某个元素。只能通过iterator()遍历元素。
2. 一般使用ArrayList,用LinkedList构造堆栈stack、队列queue。
3. Map用put(k, v)/get(k),还可以使用containsKey(k)/containsValue(v)来检查其中是否包含有某个key/value(还是应该尽量少使用containsValue,该方法会遍历整个map,而且value对象需要重写hashCode和equals方法,在比较value的时候是通过==和equals方法比较的)。
4. HashMap会利用对象的hashCode来快速找到key.
hashing哈希码就是将对象的信息经过一些转变,变成一个独一无二的int值,这个值存储在一个array中。我们都知道所有存储结构中,array查找速度是最快的。所以,可以加速查找。
Map中元素可以将key序列、value序列单独取出来。使用keySet()抽取key序列,将map中的所有keys生成一个Set。使用values()抽取value序列,将map中的所有values生成一个Collection。为什么一个生成Set,一个生成Collection,那是因为key总是唯一的,value允许重复。
5. ArrayList的初始容量
在使用的时候,如果能确定元素个数的话,在new的时候可以指定容量,如果不能确定具体数量但是知道元素数量不少的话也可以适当的指定一个初始容量,以期尽量减少扩容,ArrayList的扩容是比较耗时的(通过数组复制进行扩容),而且元素越多扩容耗时越多(当然如果元素数量不多的话可以不考虑,指定容量可能会造成空间浪费)。如下例子:
private static void testtest() {
long startTime = System.currentTimeMillis();
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 1000000; i ++) {
list.add(i);
}
System.out.println(System.currentTimeMillis() - startTime);
startTime = System.currentTimeMillis();
List<Integer> list1 = new ArrayList<>(10000);
for (int i = 0; i < 1000000; i ++) {
list1.add(i);
}
System.out.println(System.currentTimeMillis() - startTime);
}
执行结果:
74
16
很明显第二种方法效率要高
6.
ArrayList的subList结果不可强转成Arraylist,否则会抛出ClassCastException异常:java.util.RandomAccessSubList cannot be cast to java.util.ArrayList;
这是因为subList返回的是ArrayList的内部类SubList,是一个内存片段,并不是ArrayList,而是ArrayList的一个视图,对于SubList子列表的所有操作最终会反射到原列表(如果将字列表中的一个元素删除掉,那么该元素在父列表中也不存在了)。
在subList场景中,高度注意对原集合元素个数的修改,会导致子列表的遍历、增加、删除均产生ConcurrentModificationException异常。
使用集合转数组的方法,必须使用集合的toArray(T[] array),传入的是类型完全一样的数组,大小就是list.size()。
说明:使用toArray带参数的方法,入参分配的数组空间不够大时,toArray方法内部将重新分配内存空间,并返回新的数组地址;如果数组元素大于实际所需,下标为[list.size()]的数组元素将被置为null,其他数组元素保持原值,因此最好将方法入参数组大小定义与集合元素个数一致。
说明:直接使用toArray无参数方法存在问题,此方法返回值只能是Object[]类,若强转其他类型数组,将抛出ClassCastException异常。不过可以遍历Object数组,遍历出来的元素一个一个的强转即可。
使用工具类Arrays.asList()把数组转换成集合时:
(1)在使用asList时不要将基本数据类型当做参数。
例如:
int[] ints = {1,2,3,4,5};
List list = Arrays.asList(ints);
System.out.println("list.size:" + list.size());
输出:list.sieze:1
这是因为Arrays.asList()方法把ints当成了一个对象处理了,将这一个对象放到list里面,所以list的大小是1,list里面的这一个元素就是ints。有兴趣的同学可以看看源码,Arrays.asList()方法返回的List是Arrays的一个内部类,转换的时候其实没有做什么操作,只是把传入的数组封装到了Arrays的一个叫ArrayList的内部类里面了,转换后的List做的一系列操作其实都是这个内部类在数组上操作的。
(2)不要试图改变asList返回的列表,不能使用其修改集合相关方法,它的add、remove、clear方法会抛出UnsupportedOperationException异常。有的同学就要问了,那为什么又能调这些方法呢,如果看了源码的同学肯定知道,因为因为该方法返回的是Arrays的一个叫ArrayList的内部类,这个内部类继承了AbstractList,父类里面有这些方法,所以能调,调的时候又为什么报错呢,那是因为内部类没有实现这些方法。
以下是曾经在一本书上看到的一段话,感觉讲的挺形象的:
说明:asList返回的对象是一个Arrays内部类,并没有实现集合的修改方法。Arrays.asList体现的是适配器模式,只是转换接口,后台数组仍是数组。asList返回的列表只不过是一个披着list的外衣,它并没有list的基本特性(变长)。该list是一个长度不可变的列表,传入参数的数组有多长,其返回的列表就只能是多长。所以:不要试图改变asList返回的列表。
使用entrySet遍历Map类集合K/V,而不是keySet方式进行遍历。
说明:keySet其实是遍历了两次,一次是转为Iterator对象,另一次是从hashMap中取出key所对应的value。而entrySet只是遍历了一次就把key和value都放到了entry中,效率更高。
代码如下:
private static void testMap() {
Map<Integer, Integer> map = getMap();
long startTime = System.currentTimeMillis();
Iterator<Integer> it = map.keySet().iterator();
while (it.hasNext()) {
Integer key = it.next();
Integer value = map.get(key);
}
long endTime = System.currentTimeMillis();
System.out.println("低效率耗时:" + (endTime-startTime) + "毫秒");
System.out.println("-------------------");
long startTime1 = System.currentTimeMillis();
Iterator<Map.Entry<Integer, Integer>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Integer, Integer> entry = iterator.next();
Integer key = entry.getKey();
Integer value = entry.getValue();
}
long endTime1 = System.currentTimeMillis();
System.out.println("高效率耗时:" + (endTime1-startTime1) + "毫秒");
}
//10W个元素的map
private static Map<Integer,Integer> getMap() {
int count = 100000;
Map<Integer, Integer> map = new HashMap<>(count);
for (int i = 1; i <= count; i ++) {
map.put(i, i);
}
return map;
}
运行结果:
map元素越多,差距越大,每一次运行结果可能不一样,但是第一种方式耗时肯定比第二种方式多。