1.简介
集合分三大接口:Collection、Map、Iterator,集合框架的接口和类在java.util包中
2.Collection
Collection主要用于存储单个对象,Collection的两大接口List和Set。
2.1 List
List接口: ArrayList、Vector、LinkedList
2.1.1 ArrayList
ArrayList的用法
// 可以储存多个不同类型的对象,我们可以限定List只能存储什么类型的元素
List<String> list = new ArrayList<String>();
list.add("array1");
list.add("array2");
list.add("array3");
list.add(null); // 可以存储null
list.add(null);
int size = list.size(); // 把size()拿出来存储变量保存在栈内要比在循环里面调用方法的效率高
for(int i = 0; i < size; i++) {
System.out.print(list.get(i) + " "); // array1 array2 array3 null null
}
// 是否存在array2
System.out.println(list.contains("array2")); // true
// 移除对象
list.remove("array3");
size = list.size();
for(int i = 0; i < size; i++) {
System.out.println(list.get(i) + " "); // array1 array2 null null
}
ArrayList实现了List接口,是顺序容器,即元素存放的数据与放进去的顺序相同,允许放入null元素,底层通过数组实现。除该类未实现同步外,其余跟Vector大致相同。每个ArrayList都有一个容量(capacity),表示底层数组的实际大小,容器内存储元素的个数不能多于当前容量。当向容器中添加元素时,如果容量不足,容器会自动增大底层数组的大小。这里的数组是一个Object数组,以便能够容纳任何类型的对象。
自动扩容:每当向数组中添加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度,如果超出,数组将会进行扩容,以满足添加数据的需求。
数组进行扩容时,会将老数组中的元素重新拷贝一份到新的数组中,每次数组容量的增长大约是其原容量的1.5倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。
2.1.2 Vector
ArrayList不是线程安全的,而Vector则是线程安全的,因为它的方法用了synchronized来操作。Vector也是用动态数组实现的,默认容量为10,也是Object类型。
当其增量为零是扩充大小是原来的两倍,否则是一倍加增量。
2.1.3 LinkedList
和ArrayList、Vector不同,LinkedList采用双向链表的结构来实现。因为链表的缘故所以增加删除速度比较快,查找的话相比于数组就慢些。
2.2 Set
List是有序的而Set不是并且不支持存储重复元素。Set接口主要有TreeSet、HashSet、LinkedHashSet
2.2.1 TreeSet
基于**TreeMap(二叉树)**实现,支持有序性操作,例如根据一个范围查找元素的操作。
存储自定义对象需要实现Comparable接口(有序的基础),否则会报错。
实现Comparable接口的compareTo方法
但是结果只存了两个对象,因为person2和person3的年龄相同,认为是同一个对象。
所以:TreeSet可以实现两个功能:去掉重复元素和排序
2.2.2 HashSet
基于哈希表(HashMap)实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。
添加元素存储在HashMap的键。而value使用固定的对象。
HashSet保证元素唯一性的源码分析:
首先进入HashSet的add方法,底层是调用了HashMap的put方法
点击进入put方法
再进入hash方法,可以看到该方法是计算key的hash值,然后再作为参数传递到putVal方法
点击进入putVal方法
第一个if判断哈希表是否为空,为空则初始化哈希表;如果不为空,则判断该哈希值在哈希表中对应数组位置是否为空,为空的话直接插入;如果不为空,则判断key值是否相同,相同的替换掉旧节点,不同则判断是否树化,树化就按树的方式进行存储,没有树化就按链表的方式进行存储,保证了元素的唯一性。
可以看出,HashSet的唯一性则是通过重写hashCode()方法和equal()方法实现的。所以如果自定义类要实现唯一性,必须重写Object公共父类的hashCode()方法和equal()方法
String重写了这些方法:
2.2.2 LinkedHashSet
具有 HashSet 的查找效率,且内部使用双向链表维护元素的插入顺序。
- LinkedHashSet的底层使用LinkedHashMap存储元素。
- LinkedHashSet是有序的,它是按照插入的顺序排序的。
3. Map
Map是键值对映射的数据结构,一个映射不能包含重复的键并且存取的顺序不能保证,允许存null键和null值。
3.1 HashMap
HashMap基于哈希表实现。由于要保证键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。
基本用法:
Map<String, String> map = new HashMap<>();
map.put("key1", "value1");
map.put("key2", "value2");
System.out.println(map.get("key1")); // value1
System.out.println(map.get("key3")); // null
// 遍历1
Set<Map.Entry<String, String>> set = map.entrySet();
Iterator<Map.Entry<String, String>> iterator = set.iterator();
while (iterator.hasNext()) {
Map.Entry<String, String> entry = iterator.next();
String key = entry.getKey();
String value = entry.getValue();
System.out.println("key = " + key + " value = " + value);
}
//遍历2
Iterator<String> iter = map.keySet().iterator();
while (iter.hasNext()) {
String key = iter.next();
String value = map.get(key);
System.out.println("key = " + key + " value = " + value);
}
HashMap存储结构:JDK1.7和 JDK1.8之后的 HashMap 存储结构。在JDK1.7及之前,是用数组加链表的方式存储的。当链表的长度特别长的时候,查询效率将直线下降,查询的时间复杂度为 O(n),因此,JDK1.8 把它设计为达到一个特定的阈值之后,就将链表转化为红黑树。
红黑树的特点:
- 每个节点只有两种颜色:红色或者黑色
- 根节点必须是黑色
- 每个叶子节点(NIL)都是黑色的空节点
- 从根节点到叶子节点,不能出现两个连续的红色节点
- 从任一节点出发,到它下边的子节点的路径包含的黑色节点数目都相同
HashMap 结构示意图:
常量:
//默认的初始化容量为16,必须是2的n次幂
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
//最大容量为 2^30
static final int MAXIMUM_CAPACITY = 1 << 30;
//默认的加载因子0.75,乘以数组容量得到的值,用来表示元素个数达到多少时,需要扩容。
//为什么设置 0.75 这个值呢,简单来说就是时间和空间的权衡。
//若小于0.75如0.5,则数组长度达到一半大小就需要扩容,空间使用率大大降低,
//若大于0.75如0.8,则会增大hash冲突的概率,影响查询效率。
static final float DEFAULT_LOAD_FACTOR = 0.75f;
//刚才提到了当链表长度过长时,会有一个阈值,超过这个阈值8就会转化为红黑树
static final int TREEIFY_THRESHOLD = 8;
//当红黑树上的元素个数,减少到6个时,就退化为链表
static final int UNTREEIFY_THRESHOLD = 6;
//链表转化为红黑树,除了有阈值的限制,还有另外一个限制,需要数组容量至少达到64,才会树化。
//这是为了避免,数组扩容和树化阈值之间的冲突。
static final int MIN_TREEIFY_CAPACITY = 64;
//存放所有Node节点的数组
transient Node<K,V>[] table;
//存放所有的键值对
transient Set<Map.Entry<K,V>> entrySet;
//map中的实际键值对个数,即数组中元素个数
transient int size;
//每次结构改变时,都会自增,fail-fast机制,这是一种错误检测机制。
//当迭代集合的时候,如果结构发生改变,则会发生 fail-fast,抛出异常。
transient int modCount;
//数组扩容阈值
int threshold;
//加载因子
final float loadFactor;
//普通单向链表节点类
static class Node<K,V> implements Map.Entry<K,V> {
//key的hash值,put和get的时候都需要用到它来确定元素在数组中的位置
final int hash;
final K key;
V value;
//指向单链表的下一个节点
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
}
//转化为红黑树的节点类
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
//当前节点的父节点
TreeNode<K,V> parent;
//左孩子节点
TreeNode<K,V> left;
//右孩子节点
TreeNode<K,V> right;
//指向前一个节点
TreeNode<K,V> prev; // needed to unlink next upon deletion
//当前节点是红色或者黑色的标识
boolean red;
TreeNode(int hash, K key, V val, Node<K,V> next) {
super(hash, key, val, next);
}
}
构造函数:
//默认无参构造,指定一个默认的加载因子
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR;
}
//可指定容量的有参构造,但是需要注意当前我们指定的容量并不一定就是实际的容量,下面会说
public HashMap(int initialCapacity) {
//同样使用默认加载因子
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
//可指定容量和加载因子,但是笔者不建议自己手动指定非0.75的加载因子
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
//这里就是把我们指定的容量改为一个大于它的的最小的2次幂值,如传过来的容量是14,则返回16
//注意这里,按理说返回的值应该赋值给 capacity,即保证数组容量总是2的n次幂,为什么这里赋值给了 threshold 呢?
//先卖个关子,等到 resize 的时候再说
this.threshold = tableSizeFor(initialCapacity);
}
//可传入一个已有的map
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
//把传入的map里边的元素都加载到当前map
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { // pre-size
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
threshold = tableSizeFor(t);
}
else if (s > threshold)
resize();
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
//put方法的具体实现,后边讲
putVal(hash(key), key, value, false, evict);
}
}
}
resize() 扩容机制
当HashMap中的元素个数超过数组大小(数组长度)*loadFactor(负载因子)时,就会进行数组扩容,loadFactor的默认值(DEFAULT_LOAD_FACTOR)是0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中的元素个数超过16×0.75=12(这个值就是阈值或者边界值threshold值)的时候,就把数组的大小扩展为2×16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预知元素的个数能够有效的提高HashMap的性能。
3.2 LinkedHashMap
LinkedHashMap是继承于HashMap,是基于HashMap和双向链表来实现的。HashMap是无序的,当我们希望有顺序地去存储key-value时,就需要使用LinkedHashMap了。TreeSet就是用到了LinkedHashMap而HashSet用到了HashMap。
3.2 TreeMap
TreeMap中默认的排序为升序,如果要改变其排序可以自己写一个Comparator。
下面代码按照key降序。