Java中的集合主要分为以下集合类:Map、List、Set、Queue和JUC包下供多线程环境下使用的以上几种集合类,JUC包下的相关类我在另一篇JUC下集合类相关的Blog详细介绍过,本篇Blog重点介绍Java单线程环境下的几种集合类。我们知道数组是很常用的一种的数据结构,我们用它可以满足很多的功能,但有时我们会遇到如下这样的问题:
- 我们需要该容器的长度是不确定的,在添加数据时也不确定需要添加多少。
- 我们需要它能自动排序,添加数据进行是有序的
- 我们需要存储以键值对方式存在的数据。
这些数组类都满足不了,于是我们使用与数组类似的数据结构——集合类,集合类在Java中有很重要的意义,保存临时数据,管理对象,泛型,Web框架等,很多都大量用到了集合类。以下是一张集合类图:
Collection接口是集合类的根接口,Java中没有提供这个接口的直接的实现类。但是却让其被继承产生了三个接口,就是Set、List和Queue。我们重点关注Set和List这两个集合类。Map是Java.util包中的另一个接口,它和Collection接口没有关系,是相互独立的,但是都属于集合类的一部分
List集合
实现List接口的类有三个,ArrayList&Vector&LinkedList,它们都有插入元素有序、可重复的特征
ArrayList集合类
ArrayList集合的类继承关系如下图所示
基本使用方法如下:
public static void main(String[] args) { /* 新建一个ArrayList */ ArrayList<String> list = new ArrayList<>(); System.out.println("初始化大小:" + list.size()); /* 添加元素 */ list.add("tml1"); list.add("tml2"); list.add("tml3"); list.add("tml4"); System.out.println("当前容量:" + list.size()); /* 将ArrayList的大小和实际所含元素的大小设置一致 */ list.trimToSize(); System.out.println("遍历--------------"); /* 遍历 */ for (String string : list) { System.out.println(string); } System.out.println("插入后遍历-----------"); /* 在指定位置插入元素 */ list.add(2, "tml5"); for (String string : list) { System.out.println(string); } System.out.println("清空后遍历-----------"); /* 清空list */ list.clear(); /* 遍历 */ for (String string : list) { System.out.println(string); } }
打印结果如下:
初始化大小:0 当前容量:4 遍历-------------- tml1 tml2 tml3 tml4 插入后遍历----------- tml1 tml2 tml5 tml3 tml4 清空后遍历-----------
LinkedList集合类
LinkedList类的继承关系如下图所示,可以看到其实现了队列Queue的接口。
Vector集合类
Vector集合的继承关系如下图所示:
List下集合类对比
ArrayList和Vector的特征比较
- vector是线程同步的,所以它也是线程安全的,而arraylist线程不安全。如果不考虑到线程安全因素,一般用arraylist效率比较高。
- 如果集合中的元素的数目大于目前集合数组的长度时,vector增长率为目前数组长度的100%,而arraylist增长率为目前数组长度的50%。如果在集合中使用数据量比较大的数据,用vector有一定的优势
- 如果查找一个指定位置的数据,vector和arraylist使用的时间是相同的,如果频繁的访问数据,这个时候使用vector和arraylist都可以。而如果移动一个指定位置会导致后面的元素都发生移动,这个时候就应该考虑到使用linkedlist,因为指针结构,移动一个指定位置的数据时其它元素不需要移动。
ArrayList和LinkedList的特征比较
- 相同点:都实现了Collection接口
- 不同点:ArrayList基于数组,具有较高的查询速度,而LinkedList基于双向循环链表,具有较快的添加或者删除的速度,二者的区别,其实就是数组和列链表的区别。
ArrayList&Vector&LinkedList性能比较
- ArrayList 和Vector是采用数组方式存储数据,此数组元素数大于实际存储的数据以便增加和插入元素,都允许直接序号索引元素,但是插入数据要涉及到数组元素移动等内存操作,所以索引数据快,插入数据慢
- Vector由于使用了synchronized方法(线程安全)所以性能上比ArrayList要差
- LinkedList使用双向链表实现存储,按序号索引数据需要进行向前或向后遍历,查询性能较差,但是插入数据时只需要记录本项的前后项即可,所以插入速度较快。
依据自己的需求选择合适的集合使用。
Set集合
实现Set接口的类有四个,HashSet&TreeSet&LinkedHashSet,它们都有插入元素无序、不可重复的特征,
HashSet集合类
HashSet内部使用Map保存数据,即将HashSet的数据作为Map的key值保存,这也是HashSet中元素不能重复的原因。而Map中保存key值前,会去判断当前Map中是否含有该key对象,内部是先通过key的hashCode,确定有相同的hashCode之后,再通过equals方法判断是否相同。HashSet集合的类继承关系如下图所示:
TreeSet集合类
TreeSet的继承关系实现如下图所示:
LinkedHashSet集合类
LinkedHashSet的继承关系实现如下图所示
Set下集合类对比
HashSet&TreeSet&LinkedHashSet比较三者的联系和区别如下:
- HashSet,为快速查找而设计的Set。存入HashSet的对象必须实现hashCode()和equals(),底层由HashMap实现。如果覆盖了equals方法,需要重写hashCode方法
- TreeSet是 提供排序功能的Set,底层为树结构 。相比较HashSet其查询速度低,如果只是进行元素的查询,我们一般使用HashSet
- LinkedHashSet,具有HashSet的查询速度,且内部使用链表维护元素的顺序(插入的次序),于是在使用迭代器遍历Set时,结果会按元素插入的次序显示,底层由LinkedHashMap实现
我们可以看的出,Set的很多实现方式都是通过组合的方式使用了Map,举个例子:
HashSet(int initialCapacity, float loadFactor, boolean dummy) { map = new LinkedHashMap<>(initialCapacity, loadFactor); }
Map集合
Map是Java.util包中的另一个接口,它和Collection接口没有关系,是相互独立的,但是都属于集合类的一部分。Map包含了key-value对。Map不能包含重复的key,但是可以包含相同的value,Map集合中存储的是键值对,键不能重复,值可以重复。根据键得到值,对map集合遍历时先得到键的set集合,对set集合进行遍历,得到相应的值。
HashMap集合类
在插入值的过程中HashMap可以插入null的key或value,插入的时候,检查是否已经存在相同的key,如果不存在,则直接插入,如果存在,则用新的value替换旧的value
HashMap有两种遍历方式,第一种方式:KeySet,第二种方式entrySet
KeySet遍历
将Map中所有的键存入到set集合中。因为set具备迭代器。所有可以迭代方式取出所有的键,再根据get方法。获取每一个键对应的值。 keySet迭代后只能通过get取key 。取到的结果会乱序,因为取得数据行主键时,使用了HashMap.keySet
方法,而这个方法返回的Set结果,里面的数据是乱序排放的。
Map map = new HashMap(); map.put("key1","lisi1"); map.put("key2","lisi2"); map.put("key3","lisi3"); map.put("key4","lisi4"); //获取迭代器 Iterator it = map.keySet().iterator(); //通过迭代器获取遍历set,然后取值 while(it.hasNext()){ Object key = it.next(); System.out.println(map.get(key)); }
返回结果为
lisi1 lisi2 lisi3 lisi4
entrySet遍历
返回此映射中包含的映射关系的 Set 视图(一个关系就是一个键-值对),就是把(key-value)作为一个整体一对一对地存放到Set集合当中的。Map.Entry表示映射关系。entrySet迭代后可以e.getKey,e.getValue两种方法来取key和value。返回的是Entry接口。
public static void main(String[] args) { Map map = new HashMap(); map.put("key1","lisi1"); map.put("key2","lisi2"); map.put("key3","lisi3"); map.put("key4","lisi4"); //将map集合中的映射关系取出,存入到set集合 Iterator it = map.entrySet().iterator(); while(it.hasNext()){ Map.Entry e =(Map.Entry) it.next(); System.out.println("键"+e.getKey () + "的值为" + e.getValue()); } }
返回结果为:
键key1的值为lisi1 键key2的值为lisi2 键key3的值为lisi3 键key4的值为lisi4
LinkedHashMap集合类
以下为LinkedHashMap集合类的继承关系图:
TreeMap集合类
以下为TreeMap集合类的继承关系图:
HashTable集合类
以下为HashTable集合类的继承关系图:
Map下集合类对比
HashMap和HashTable的联系和区别如下
- 二者都实现了Map接口,因此具有一系列Map接口提供的方法。
- 继承不同,
Hashtable extends Dictionary
,而HashMap extends AbstractMap
- 同步级别不同,Hashtable 中的方法是同步的,而HashMap中的方法在缺省情况下是非同步的。在多线程并发的环境下,可以直接使用Hashtable,但是要使用HashMap的话就要自己增加同步处理了。
- 使用Collections类的synchronizedMap方法包装一下。方法如下:
public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m)
返回由指定映射支持的同步(线程安全的)映射 - 使用ConcurrentHashMap,它使用分段锁来保证线程安全
- 对key、value的null值要求不同
- Hashtable中key和value都不允许出现null值
- HashMap中,key可以为null,这样的键只有一个;可以有一个或多个key所对应的value为null。当get方法返回null值时,既可以表示 HashMap中没有该键,也可以表示该键所对应的值为null。因此,在HashMap中不能由get方法来判断HashMap中是否存在某个键, 而应该用containsKey方法来判断
- 遍历方式的内部实现不同。Hashtable、HashMap都使用了 Iterator。而由于历史原因,Hashtable还使用了Enumeration的方式
- 哈希值的使用不同,HashTable直接使用对象的hashCode。而HashMap重新计算hash值,而且用与代替求模
- 内部实现方式的数组的初始大小和扩容的方式不同,HashTable中hash数组默认大小是11,增加的方式是 old*2+1。HashMap中hash数组的默认大小是16,而且一定是2的指数。
- Hashtable有contains方法而HashMap没有contains方法
这些就是一些比较突出的不同点,实际上他们在实现的过程中会有很多的不同,如初始化的大小、计算hash值的方式等等。毕竟这两个类包含了很多方法,有很重要的功能,综合推荐使用HashMap,因为它提供了比HashTable更多的方法,以及较高的效率,如果大家需要在多线程环境中使用,那么做一下同步即可。
HashMap和TreeMap的联系和区别如下
HashMap具有较高的速度(查询),TreeMap则提供了按照键进行排序的功能。
HashMap和LinkedHashMap的联系和区别如下
LinkedHashMap具有HashMap的查询速度,且内部使用链表维护元素的顺序(插入的次序),于是在使用迭代器遍历Map时,结果会按元素插入的次序显示