先来看一下java中的集合族谱
其中我们常用的有几个:ArrayList, LinkedList, HashSet, HashMap
一、Collection集合
它的子接口有两个:(1)List,(2)Set ,二者的区别:
List允许存放重复的元素,而Set不允许存放重复的元素,集合的这一特点和数学中的集合一致;
List中的元素是有序的,所以可通过下标来操作元素;而Set中的元素是没有先后顺序的,也就不能通过下标来操作元素。
(1) List子接口有2个类:(a)ArrayList和(b)LinkedList
来看看二者的不同之处:
(a)ArrayList:
查询速率很快。ArrayList类的底层数据结构是数组,具有数组的特性,所以ArrayList在内存中的地址是连续的。那么想要获取ArrayList的某个元素,只要知道这个ArrayList内存地址的首部,再通过偏移量(也就是下标),就能很快定位到这个元素(通过二分查找)。所以他的查询速度远超过LinkedList。但是ArrayList的写入性能很差。
写入速率很慢。由于ArrayList有容量的概念,每当ArrayList存储的容量达到预先分配的size(例如超过75%的容量),那么ArrayList会自动扩容。扩容意味着在原来的ArrayList容量的基础上新增一定的容量,那么就需要新创建一个更大容量的ArrayList,再把原先的ArrayList中所有元素拷贝到这个新的ArrayList里去,这一点就导致了当写入ArrayList的数据量很大时,写入性能明显地比LinkedList差很多。
(b)LinkedList:
LinkedList的底层数据结构是链表,他的特点是寻址困难,但是容易插入和删除元素。从它的源码可以看到有Node,Node相当于一个javabean,每个node不仅存储元素本身,还存有2个引用。这俩引用分别指向该元素的后一个元素、该元素的前一个元素;首元素仅持有下一个元素的引用,末元素仅持有前一个元素的引用。
所以,LinkedList的写入性能很高。因为node的特性,在往LinkedList集合的指定位置插入新元素时,只需要把要插入的元素放到LinkedList的指定位置,再把该元素加上两个引用即可,一个引用是指向他后面的元素,另一个引用指向前一个元素,整个过程不涉及扩容的问题(LinkedList没有容量概念)。
(2)Set子接口有2个类:HashSet和TreeSet
这两个类的父类都是集合,也就具有了集合的唯一性:无重复元素。那么如何保证存到集合里的每个元素都是唯一的呢?HashSet和TreeSet有不同的机制:
(a) HashSet通过hashCode()方法和equals()方法来保证;
(b) TreeSet通过Comparator.compare()或者comparable.compareTo()方法中的来保证元素的是否相等来保证的。这种集合的另一个特性是能对元素进行排序。
(a)HashSet元素唯一性机制概述:
首先比较哈希值是否相同(也就是比较将要添加的元素的哈希值是否与原有元素的哈希值),如果不同就直接把此元素添加进去;
如果hash code相同,就判断equals()的返回值是true或false,是true就表明有相同的元素了,就不添加,false表明还没有相同的元素,允许添加。
(b)TreeSet实现元素排序的机制概述:
由于TreeSet底层的数据结构是红黑树,所以TreeSet中的元素是以二叉树的形式存放的。TreeSet能实现2种排序:自然排序和比较器排序。
自然排序能让元素自动具备可比性。例如我们存放的元素数据类型是Integer的话,那么他会自动地按照数字大小进行自然排序。但是,如果我们定义了自己的数据类型,想在存放到TreeSetI里的时候自动进行自然排序,实现途径:首先在自定义类实现Comparable接口,再重写compareTo()方法,在该方法里写上我们想要的排序规则。
比较器排序能让集合具备可比性。实现途径:让TreeSet构造方法接收Comparable的实现类对象,具体来说,在创建TreeSet实例的时候实现Comparator接口的compare()方法, 在方法内写上想要的排序规则即可,写好之后,TreeSet会对添加到其中的元素自动排序。
二、Map:映射关系的集合
Map可以看成是数学里面的一元函数,xèf(x) 一个x在y轴的f(x)值最多一个。很多高级语言都有这种类型的数据结构,Python中的叫Diction(字典),Java中的叫Map(映射)。
Map就是key è value之间的映射关系,一个key对应一个value, 且key不可重复。如果key重复就会造成混乱,举例来说一个宾馆的房间号不能重复,一个房间号只能对应一间,每一间客房是唯一的那么要求房间号也是唯一的,如果房间号有3个6号,服务员就不知道客人住的是哪个房间了。
Map在集合体系中是与Collection并列存在的。
Map有几个地方不同于Collection:
首先,存储的元素类型不一样:Map存储的是键值对,也就是双列的;而Collection是单列的。
第二,Map集合的数据结构只针对键有效,与值无关;Collection的数据结构是对元素有效。
第三,Map新增元素使用的是put()方法,Collection使用add()方法。
第四,没有直接的方法获取Map集合元素,而是通过转换成Set集合后通过迭代获取元素(通过遍历Map键集合获取每一个值,或通过遍历键值对对象获取键和值)。
实现Map接口的类:(1)HashMap (2)Hashtable (3)TreeMap
(1)HashMap键是哈希表结构,所以能保证键的唯一性
(2)Hashtable是很早以前用的,现在已经被HashMap替代了。他和HashMap的区别在于:
Hashtable是线程安全的,效率低,不允许null键和null值;
HashMap线程不安全,效率高。允许null键和null值。
(3)TreeMap的键也是唯一的,比HashMap多的功能是能对键排序,排序原理与TreeSet相同。
三、集合框架中的工具类:Collections、Arrays
Collections提供了查找、找极值、对List进行排序等功能;
Arrays提供的功能:把数组转成List集合、对数组排序、对数组进行二分查找。
--------------<顺便说两句>--------------
HashMap的实现原理
这里重点说一下HashMap因为他很常用。
1,底层的数据结构
HashMap是一个以链表为元素的一维数组,所以HashMap结合了两大优点:数组的高效寻址性能(查询容易)和链表的高效写入性能!
从上图中可以看出,HashMap底层就是一个数组结构,数组中的每一项又是一个链表。当新建一个HashMap的时候,就会初始化一个数组。
1 /**
2 * The table, resized as necessary. Length MUST Always be a power of two.
3 */
4 transient Entry[] table;
5
6 static class Entry<K,V> implements Map.Entry<K,V> {
7 final K key;
8 V value;
9 Entry<K,V> next;
10 final int hash;
11 ……
12 }
可以看出,Entry就是数组中的元素,每个 Map.Entry 其实就是一个key-value对,它还持有一个指向下一个元素的引用(next),这就构成了链表。
当系统决定存储HashMap中的key-value对时,完全没有考虑Entry中的value,仅仅只是根据key来计算并决定每个Entry的存储位置。我们完全可以把 Map 集合中的 value 当成 key 的附属,当系统决定了 key 的存储位置之后,value 随之保存在那里即可。
2,HashMap的扩容问题
当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。为了提高查询的效率,就要对HashMap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,这是一个常用的操作,而HashMap的数组扩容是最消耗性能的,因为原数组中的数据必须重新计算其在新数组中的位置,并拷贝过去,这就是扩容(resize)。
那么HashMap什么时候进行扩容呢?当HashMap中的元素个数超过数组大小*loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过16*0.75=12的时候就把数组的大小扩展为 2*16=32,即扩大一倍,再重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作。所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。可以通过HashMap构造器来预设初始容量和Loadfactor(负载因子, 默认0.75)。
3,快速失败策略
java.util.HashMap不是线程安全的,因为他的实现方法里没有添加synchronized关键字来确保线程同步,这意味着如果在使用迭代器的过程中有其他线程修改了HashMap,将会抛出ConcurrentModificationException。这就是快速失败策略(fast-fail)。
--------------<全文结束>-----------