Java Collections
1.集合(Collections)
Java中集合类是Java编程中使用最频繁、最方便的类。集合类作为容器类可以存储任何类型的数据,当然也可以结合泛型存储指定的类型(不过泛型仅仅在编译期有效,运行时是会被擦除的)。集合类中存储的仅仅是对象的引用,并不存储对象本身。集合类的容量可以在运行期间进行动态扩展,并且还提供很多很方便的方法,如求集合的并集、交集等。
2.集合类结构
Java中的集合包含多种数据结构,如链表、队列、哈希表等。从类的继承结构来说,可以分为两大类,一类是继承自Collection接口,这类集合包含List、Set和Queue等集合类。另一类是继承自Map接口,这主要包含了哈希表相关的集合类。下面我们看一下这两大类的继承结构图:
3.集合类的关系
3.1.List
- ArrayList: 提供快速的基于索引的成员访问,对尾部成员的增加和删除支持较好,成员可以为任意的Object子类的对象
- LinkedList: 链表,对列表中任何位置的元素的增加和删除支持较好,但对基于索引的元素访问支持性较差;
3.2.Set
- HashSet: 元素不能重复,外部无序地遍历元素,成员可以为任意的Object子类的对象,但是如果重写了equals方法,同时需要修改hashCode方法;
- TreeSet: 元素不能重复,外部有序地遍历成员,附加实现了SortedSet,支持子集等要求顺序的操作,成员要求实现caparable接口,或者使用Comparator构造TreeSet,成员一般为同一种类型;
- LinkedHashSet: 元素不能重复,外部按元素的插入顺序遍历元素,元素类型和HashSet类似;
3.3.Map
- HashMap: 保存键值对成员,基于键找值操作,compareTo或compare方法对键排序,HashMap能满足用户对Map的通用需求,其键成员为任意Object子类的对象,但是如果覆盖了equals方法,同时需要修改hashCode方法;
- TreeMap: 保存键值对成员,基于键找值操作,compareTo或compare方法对键排序,TreeMap 支持对键有序地遍历,使用时建议先用HashMap增加和删除成员,最后从HashMap生成TreeMap;附加实现了SortedMap接口,支持子Map等要求顺序的操作,键成员要求实现caparable接口或者使用Comparator构造TreeMap,键成员一般为同一类型;
- LinkedHashMap:基于键找值操作,compareTo或compare方法对键排序,LinkedHashMap 留键的插入顺序,用equals 方法检查键和值的相等性,成员可为任意Object子类的对象,但如果覆盖了equals方法,同时注意修改hashCode方法;
- IdentifyHashMap:基于键找值操作,compareTo或compare方法对键排序,使用== 来检查键和值的相等性,成员使用的是严格相等;
- WeekHashMap:基于键找值操作,compareTo或compare方法对键排序,其行为依赖于垃圾回收线程,没有绝对理由则少用;
4 List, Set和Queue
4.1. List
我们用的比较多List包括ArrayList和LinkedList,这两者的区别也很明显,从其名称上就可以看出。ArrayList的底层的通过数组实现,所以其随机访问的速度比较快,但是对于需要频繁的增删的情况,效率就比较低了。而对于LinkedList,底层通过链表来实现,所以增删操作比较容易完成,但是对于随机访问的效率比较低。
我们先看下两者的插入效率:
import java.util.ArrayList;
import java.util.LinkedList;
/**
* Created by david.tian on 17/08/2017.
*/
public class ListTest {
public static void main(String[] args){
long start = System.currentTimeMillis();
LinkedList<Integer> linkedList = new LinkedList<Integer>();
for(int i =0; i<100000; i++){
linkedList.add(0,i);
}
long end = System.currentTimeMillis();
System.out.println(end-start);
ArrayList<Integer> arrayList = new ArrayList<Integer>();
for(int i=0; i<100000; i++){
arrayList.add(0,i);
}
System.out.println(System.currentTimeMillis() - end);
}
}
下面是本地执行的结果:
15
818
可以看出,在这种情况下,LinkedList的插入效率远远高于ArrayList,当然这是一种比较极端的情况。我们再来比较一下两者随机访问的效率:
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Random;
/**
* Created by david.tian on 17/08/2017.
*/
public class ListReadTest {
public static void main(String[] args){
Random random = new Random();
LinkedList<Integer> linkedList = new LinkedList<Integer>();
for(int i=0; i<100000; i++){
linkedList.add(i);
}
ArrayList<Integer> arrayList = new ArrayList<Integer>();
for(int i=0; i<100000; i++){
arrayList.add(i);
}
long start = System.currentTimeMillis();
for(int i=0; i< 100000; i++){
int j = random.nextInt(i+1);
int k = linkedList.get(j);
}
long end = System.currentTimeMillis();
System.out.println(end-start);
/** ArrayList **/
for(int i =0; i < 100000; i++){
int j = random.nextInt(i+1);
int k = arrayList.get(j);
}
System.out.println(System.currentTimeMillis()-end);
}
}
下面是我本机执行的结果:
3708
7
很明显可以看出,ArrayList的随机访问效率比LinkedList高出好几个数量级。通过这两段代码,我们应该能够比较清楚的知道LinkedList和ArrayList的区别和适应的场景。
4.2. Queue
一般可以直接使用LinkedList完成,从上述类图也可以看出,LinkedList继承自Deque,所以LinkedList具有双端队列的功能。PriorityQueue的特点是为每个元素提供一个优先级,优先级高的元素会优先出队列。
4.3. Set
Set与List的主要区别是Set是不允许元素重复的,而List则可以允许元素重复的。判断元素的重复需要根据对象的hash方法和equals方法来决定。这也是我们通常要为集合中的元素类重写hashCode方法和equals方法的原因。我们还是通过一个例子来看一下Set和List的区别,以及hashcode方法和equals方法的作用:
import java.util.*;
/**
* Created by david.tian on 17/08/2017.
*/
public class SetTest {
public static void main(String[] args){
Person p1 = new Person("david",20,"WX");
Person p2 = new Person("david",20,"WX");
Person p3 = new Person("david",20,"BJ");
ArrayList<Person> arrayList = new ArrayList<Person>();
arrayList.add(p1);
System.out.println(">>>>>>>>>>>>>>p1 added to ArrayList>>>>>>>>>>>>>>>>>>");
arrayList.add(p2);
System.out.println(">>>>>>>>>>>>>>p2 added to ArrayList>>>>>>>>>>>>>>>>>>");
arrayList.add(p3);
System.out.println(">>>>>>>>>>>>>>p3 added to ArrayList>>>>>>>>>>>>>>>>>>");
System.out.println(">>>>show arrayList size: " +arrayList.size()+ "<<<<<<<<");
System.out.println("===================I'm the separator======================");
Set<Person> sp = new HashSet<Person>();
sp.add(p1);
System.out.println(">>>>>>>>>>>>>>p1 added to Set>>>>>>>>>>>>>>>>>>");
sp.add(p2);
System.out.println(">>>>>>>>>>>>>>p2 added to Set>>>>>>>>>>>>>>>>>>");
sp.add(p3);
System.out.println(">>>>>>>>>>>>>>p3 added to Set>>>>>>>>>>>>>>>>>>");
System.out.println(">>>>show Set size: " +sp.size()+ "<<<<<<<<");
}
}
从结果看出,元素加入List的时候,不执行额外的操作,并且可以重复。而加入Set之前需要先执行hashCode方法,如果返回的值在集合中已存在,则要继续执行equals方法,如果equals方法返回的结果也为真,则证明该元素已经存在,会将新的元素覆盖老的元素,如果返回hashCode值不同,则直接加入集合。这里记住一点,对于集合中元素,hashCode值不同的元素一定不相等,但是不相等的元素,hashCode值可能相同。
HashSet和LinkedHashSet的区别在于后者可以保证元素插入集合的元素顺序与输出顺序保持一致。而TresSet的区别在于其排序是按照Comparator来进行排序的,默认情况下按照字符的自然顺序进行升序排列。
5.Map
Map类型的集合最大的优点在于其查找效率比较高,理想情况下可以实现O(1)的时间复杂度。Map中最常用的是HashMap,LinkedHashMap与HashMap的区别在于前者能够保证插入集合的元素顺序与输出顺序一致。这两者与TreeMap的区别在于TreeMap是根据键值进行排序的,当然其底层的实现也有本质的区别,如HashMap底层是一个哈希表,而TreeMap的底层数据结构是一棵树。我们现在看下TreeMap与LinkedHashMap的区别:Map类型的集合最大的优点在于其查找效率比较高,理想情况下可以实现O(1)的时间复杂度。Map中最常用的是HashMap,LinkedHashMap与HashMap的区别在于前者能够保证插入集合的元素顺序与输出顺序一致。这两者与TreeMap的区别在于TreeMap是根据键值进行排序的,当然其底层的实现也有本质的区别,如HashMap底层是一个哈希表,而TreeMap的底层数据结构是一棵树。我们现在看下TreeMap与LinkedHashMap的区别:
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.TreeMap;
/**
* Created by david.tian on 17/08/2017.
*/
public class MapTest {
public static void main(String[] args){
Map<String,String> treeMap = new TreeMap<String,String>();
Map<String,String> linkedMap = new LinkedHashMap<String,String>();
treeMap.put("c", null);
treeMap.put("b", null);
treeMap.put("a", null);
for(Iterator<String> iterator = treeMap.keySet().iterator(); iterator.hasNext();){
System.out.println("TreeMap = " + iterator.next());
}
System.out.println("-----------------分割线-------------------");
linkedMap.put("c",null);
linkedMap.put("b",null);
linkedMap.put("a",null);
for(Iterator<String> iterator = linkedMap.keySet().iterator(); iterator.hasNext();){
System.out.println("LinkedHashMap = " + iterator.next());
}
}
}
运行上述代码,执行结果如下:
TreeMap = a
TreeMap = b
TreeMap = c
-----------------分割线-------------------
LinkedHashMap = c
LinkedHashMap = b
LinkedHashMap = a
从运行结果可以很明显的看出这TreeMap和LinkedHashMap的区别,前者是按字符串排序进行输出的,而后者是根据插入顺序进行输出的。细心的读者可以发现,HashMap与TreeMap的区别,与之前提到的HashSet与TreeSet的区别是一致的,在后续进行源码分析的时候,我们可以看到HashSet和TreeSet本质上分别是通过HashMap和TreeMap来实现的,所以它们的区别自然也是相同的。HashTable现在已经很少使用了,与HashMap的主要区别是HashTable是线程安全的,不过由于其效率比较低,所以通常使用HashMap,在多线程环境下,通常用CurrentHashMap来代替。