Java Language——集合框架

Java 集合类位于 java.util 包下,JDK1.5 之后还在 java.util.concurrent 包下提供了一些多线程支持的集合类。Java 集合主要由两个接口派生而出:Collection 和 Map。Collection 的父接口是 Iterable(迭代器),所以 Collection 的子接口全部可以使用 Iterable 遍历集合。

Collection 的子接口包括 List、Set 和 Queue 接口。

从 Java 源码来看,Java 先实现了 Map,然后通过包装一个所有 value 都为 null 的 Map 就实现了 Set 集合。

Java Language——集合框架_第1张图片

集合 API 可以参考 JDK 文档:http://tool.oschina.net/apidocs/apidoc?api=jdk-zh

1.List集合

多线程 底层 说明 动态扩容
ArrayList 线程不安全 数组 有序可重复 默认大小10,每次扩容容量为原来的1.5倍
LinkedList 线程不安全 双向循环链表,既可以作队列,可以作栈
Vector 线程安全 数组 方法都加上了synchronized,保证了线程安全 每次扩容容量为原来的2倍

Vector 是线程安全的,所以 ArrayList 性能相对 Vector 会好很多。

1.ArrayList

List<Integer> list = new ArrayList<Integer>();
list.add(2); // 虽然集合里不能放基本类型的值, 但Java支持自动装箱

集合里不能放基本类型的值,但 Java 支持自动装箱,所以 list.add(2); 也是没问题的。

ArrayList 线程不安全的,底层是数组实现的,可以自增扩容。

public class ArrayList<E> extends AbstractList<E> implements List<E>, 
        RandomAccess, // 支持快速随机访问
        Cloneable, // 能被克隆
        java.io.Serializable { // 支持序列化
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    transient Object[] elementData; // 数组
    public ArrayList() {
    	this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
	}
	public ArrayList(int initialCapacity) {
        this.elementData = new Object[initialCapacity];
	}
    // 省略部分代码
}

调用无参构造方法 new 对象时,实际上只是将 elementData 指向一个空数组而已。调用带参数的构造方法时,就是初始化一个参数 initialCapacity 大小的新数组。

不管是上面哪种情况,调用 add() 操作时,都会涉及动态扩容,无参构造函数创建的 ArrayList 第一次 执行 add() 操作时,就会动态扩容出一个长度为 10 的数组。

ArrayList 的动态扩容原理:其实就是创建新的长度的数组,然后把老数组数据 copy 到新数据,再覆盖掉老数组的过程。

ArrayList 每次扩容容量为原来的 1.5 倍,需要注意的是 Vector 每次扩容容量是原来的 2 倍。

通过源码也可以看到,ArrayList 并不是线程安全的,其方法里即没用到锁,也没用到 CAS 操作。

2.LinkedList

有序可重复,既可以被当作栈(先进后出)使用,也可以当成队列(先进先出)使用。

LinkedList<Integer> linkedList = new LinkedList<Integer>();
linkedList.offer(1); // 将元素加入队列的尾部
linkedList.offerFirst(2); // 将元素添加到队列的头部
Integer element1 = linkedList.peekLast(); // 访问、并不删除队列的最后一个元素
Integer element2 = linkedList.pollLast(); // 访问、并删除队列的最后一个元素

linkedList.push(3); // 将元素加入栈的顶部
Integer element3 = linkedList.peekFirst(); // 访问、并不弹出栈顶的元素
Integer element4 = linkedList.pop(); // 将栈顶的元素弹出

// 以List的方式 (按索引访问的方式) 来遍历集合元素
for (int i = 0; i < linkedList.size(); i++) {
    Integer element = linkedList.get(i);
}

LinkedList 线程不安全的,底层是双向循环链表实现的,可以自增扩容。

public class LinkedList<E> extends AbstractSequentialList<E>
        implements List<E>, Deque<E>, Cloneable, java.io.Serializable {
    transient int size = 0;
    transient Node<E> first; // 头节点
    transient Node<E> last; // 尾节点
    public LinkedList() {
    }
    private static class Node<E> { // 节点, 内部类
        E item; // 元素
        Node<E> next; // 下一个节点
        Node<E> prev; // 上一个节点
        Node(Node<E> prev, E element, Node<E> next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }
    // 省略部分代码
}

通过源码也可以看到,LinkedList 并不是线程安全的,其方法里即没用到锁,也没用到 CAS 操作。

对于遍历 List 集合元素,ArrayList 最好使用随机访问方法(get)来遍历,这样性能最好;LinkedList 则最好用迭代器(Iterator)来遍历集合元素。

2.Set集合

多线程 底层 说明
HashSet 线程不安全 数组 无序,集合元素值允许为null
TreeSet 线程不安全 红黑树 有序,会按照自然排序,集合元素值允许为null

1.HashSet

Set<Integer> set = new HashSet<Integer>();
Iterator it = set.iterator(); // 使用Iterator遍历集合元素
while (it.hasNext()) {
    Integer i = (Integer) it.next();
}

HashSet 判断两个元素相等的标准:两个对象通过 equals() 方法比较相等;两个对象的 hashCode() 返回值也相等。

通过阅读 HashSet 源码,可以看到,HashSet 是通过 HashMap 实现的。

public class HashSet<E> extends AbstractSet<E>
        implements Set<E>, Cloneable, java.io.Serializable {
    private transient HashMap<E, Object> map;
    public HashSet() {
        map = new HashMap<>();
    }
    // 省略部分代码
}

简而言之,HashSet 就是一个 value 为空对象的 HashMap。

2.TreeSet

TreeSet 采用红黑树的数据结构来存储集合元素,支持两种排序方法:自然排序和定制排序,默认自然排序

TreeSet<Integer> treeSet = new TreeSet<Integer>();
Integer first = treeSet.first(); // 获取第一个元素
Integer last = treeSet.last(); // 获取最后一个元素
SortedSet headSet = treeSet.headSet(25); // 获取小于25的子集, 不包含25
SortedSet tailSet = treeSet.tailSet(10); // 获取大于10的子集, 如果Set中包含10,子集中还包含10
SortedSet subSet = treeSet.subSet(-10, 20); // 获取大于等于-10, 小于20的子集

通过阅读 TreeSet 源码,可以看到,TreeSet 是通过 TreeMap 实现的。

public class TreeSet<E> extends AbstractSet<E>
        implements NavigableSet<E>, Cloneable, java.io.Serializable {
    private transient NavigableMap<E, Object> m;
    TreeSet(NavigableMap<E, Object> m) {
        this.m = m;
    }
    public TreeSet() {
        this(new TreeMap<E, Object>());
    }
    // 省略部分代码
}

简而言之,TreeSet 就是一个 value 为空对象的 TreeMap。

3.Map集合

多线程 底层 说明 动态扩容
HashMap 线程不安全 hash 表 无序,key 不允许null,value 可以null,为快速查询而设计的 默认大小16,如果超过当前大小 * 扩容因子 0.75 就会扩容,扩容 2 倍后重排
LinkedHashMap 线程不安全 hash 表 继承 HashMap
TreeMap 线程不安全 红黑树 自然有序,key 不允许null,value 可以null,速度略慢于 HashMap
Hashtable 线程安全 hash 表
Properties 线程安全 hash 表 继承自 Hashtable,可以把Map对象和属性文件关联起来
IdentityHashMap 线程不安全 数组 根据对象内存地址计算 hash

Map 用于保存具有映射关系的数据(key-value),key 不允许重复,value 可以重复。从 Java 源码来看,Java 先实现了Map,然后通过包装一个所有 value 都为 null 的 Map 就实现了 Set 集合。

速度对比:HashMap(最快) > Hashtable > TreeMap

1.HashMap

hash(哈希、散列)算法的功能:能保证快速查找被检索的对象,hash 算法的价值在于速度,当查询某个元素时,hash 算法可以直接根据该元素的 hashCode 值计算出该元素的存储位置,从而快速定位。

Map<String, Object> map = new HashMap<String, Object>();
boolean b = map.containsKey("C"); // 判断是否包含指定key
boolean b1 = map.containsValue(90)); // 判断是否包含指定value
for (String key : map.keySet()) { // 遍历key-value
    // 获取指定key对应的value
    Object value = map.get(key);
}
map.remove("C"); // 删除key-value

Java8 也为 Map 新增了很多方法,例如 replace() 方法替换 value 值等,新特性建议多阅读 API。

打开 HashMap 源码,可以看到:

transient Set<K>        keySet;
transient Collection<V> values;

Map 里的 key 是通过 Set 组织起来的,通过 Set 意味着它是有去重功能的,这也是为什么 key 不允许重复的原因,而 value 是通过 Collection 组织起来的。

HashMap、HashTable、ConcurrentHashMap 的区别?

HashMap JDK1.8 前采用了数组 + 链表实现的,数组的特点是查询快增删慢,链表的特点是查询慢增删快,HashMap 结合了两者的优势,同时 HashMap 的操作是非 synchronized 的,因此效率比较高。

HashMap 默认初始大小是 16。

Java Language——集合框架_第2张图片

最坏情况下,hash() 计算后总是会命中同一个数组元素,那么 HashMap 的性能将会从原先的 O(1) 变成 O(n)。

针对这个问题,JDK1.8 及以后变成了数组 + 链表 + 红黑树实现,使用了一个 TREEIFY_THRESHOLD 常量来控制是否将链表转换为红黑树来存储它们,这意味着可以将最坏情况下的性能从 O(n) 提高到 O(logn)。

Java Language——集合框架_第3张图片

通过 JDK1.8 HashMap 源码可以看出,HashMap 是通过一个 table 数组和链表组成的复合结构。

transient Node<K,V>[] table; // table数组
transient Set<Map.Entry<K,V>> entrySet;

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key; // 键
    V value; // 值
    Node<K,V> next; // 下一个Node
    // 省略部分代码
}
// hash算法
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

Node 是由 hash 值,键值对以及指向的下一个 Node 组成的,一个 table 数组元素可以理解为一个 “桶”。

HashMap put() 方法的逻辑:

  1. 若 table 数组未被初始化,则进行初始化操作;
  2. 对 key 计算 hash 值,依据 hash 值计算 table 数组下标;
  3. 若未发生碰撞,即该下标的 “桶” 还没有存储头节点,则直接放入 “桶” 的头节点中;
  4. 若发生碰撞,即该下标的 “桶” 已经存储了头节点,则执行链表的添加操作;
  5. 若链表的长度超过树化阈值 (TREEIFY_THRESHOLD,默认 8)则被改造成红黑树,而当删除操作导致低于最低树化阈值(UNTREEIFY_THRESHOLD,默认 6)的时候,红黑树则转回成链表,保证更高的性能;
  6. 若键值对已经存在,则用新值替换旧值;
  7. 若某个 “桶” 满了(默认容量 16 * 扩容因子 0.75 = 12,则默认超过 12 就会扩容),就需要 resize(扩容 2 倍后重排)。

这里树化阈值为什么默认是 8 呢?

如何有效减少碰撞,提升 HashMap 性能?

  • 扰动函数:促使元素位置分布均匀,较少碰撞机率;
  • 使用 final 对象,并采用合适的 equals() 和 hashCode() 方法。

扩容?

扩容的问题?

  • 多线程环境下,调整大小会存在条件竞争,容易造成死锁;
  • 原先 HashMap 的键值对需要重新移动到新的 HashMap 里,这将是一个非常耗时的过程。

HashTable 是早起 Java 类库提供的哈希表的实现,HashTable 的方法都加的 synchronized 修饰,是线程安全的,但是也导致了多线程环境下串行化的执行,性能较差,所以已经很少使用了。HashTable 的源码相对简单,也没有树化逻辑,有时间可以阅读一下。

Map<String, Object> hashMap = new HashMap<String, Object>(); 
Map<String, Object> safeHashMap = Collections.synchronizedMap(hashMap);

其实 HashMap 也可以通过 Collections.synchronizedMap() 方法对自身方法进行 synchronized 加锁处理,但是跟 HashTable 一样,在多线程环境下是串行化的执行,性能较差。

为了提升 HashMap 多线程下的性能,JDK1.5 引入了 ConcurrentHashMap 这个类。

ConcurrentHashMap 是出自 JUC 包(java.util.concurrent)的,ConcurrentHashMap 和 HashMap 非常类似,包括属性参数。

早期的 ConcurrentHashMap 是通过分段锁技术来实现的,将 HashMap 的 table 数组逻辑上拆分成多个子数组,默认会分成 16 段,每个段配一把锁,这样多个线程如果操作的是不同的段,则不会被阻塞,理论上会比 HashTable 的效率提升 16 倍。

JDK 1.8 ConcurrentHashMap 的实现取消了分段锁,而 CAS + synchronized 使锁更细化。同时对结构也做了进一步优化,跟 JDK 1.8 的 HashMap 一样,由数据 + 链表 + 红黑树实现,synchronized 只锁定当前链表或者红黑树的首节点,这样只要 hash 不冲突,就不会产品线程安全问题,效率得到了进一步的提高。

Java Language——集合框架_第4张图片

ConcurrentHashMap put() 方法的逻辑:

  1. 若 table 数组未被初始化,则进行初始化操作;
  2. 对 key 计算 hash 值,依据 hash 值计算 table 数组下标;
  3. 若未发生碰撞,即该下标的 “桶” 还没有存储头节点,则使用 CAS 操作放入 “桶” 的头节点中,添加失败则循环重试;
  4. 检查到内部正在扩容,就帮助它一块扩容;
  5. 若发生碰撞,即该下标的 “桶” 已经存储了头节点,则使用 synchronized 锁住头节点(链表或者红黑树),如果是链表结构则执行链表的添加操作,如果是红黑树结构则执行树添加操作;
  6. 若链表的长度超过树化阈值 (TREEIFY_THRESHOLD,默认 8)则被改造成红黑树,而当删除操作导致低于最低树化阈值(UNTREEIFY_THRESHOLD,默认 6)的时候,红黑树则转回成链表,保证更高的性能;
  7. 若键值对已经存在,则用新值替换旧值;
  8. 若某个 “桶” 满了(默认容量 16 * 扩容因子 0.75 = 12,则默认超过 12 就会扩容),就需要 resize(扩容 2 倍后重排)。

ConcurrentHashMap size() 方法和 mappingCount() 方法的异同,两者计算是否准确?

多线程环境下 ConcurrentHashMap 如何扩容?

2.TreeMap

底层红黑树,每个 key-value 对即作为红黑树的一个节点。TreeMap 支持两种排序方式:自然排序和定制排序,TreeMap 的使用方法这里不再详细给出,建议查看 API 文档学习。

4.Queue集合

多线程 底层 说明
ArrayDeque 线程不安全 数组
LinkedList 线程不安全 双向循环链表

1.ArrayDeque

Deque 接口实现了 Queue 接口,代表双端队列,Deque 接口里定义了一些双端队列的方法,这些方法允许从两端来操作队列的元素。Deque 接口提供了一个典型的实现类 ArrayDeque,ArrayDeque 是基于数组实现的双端队列,ArrayDeque 不仅可以当成 “栈” 使用,而且还可以当成 “队列” 使用。

把 ArrayDeque 当成 “栈” 使用:

ArrayDeque<Integer> stack = new ArrayDeque<Integer>();
// 将元素push入"栈"
stack.push(2);
// 访问第一个元素, 但并不将其pop出"栈"
Integer element = stack.peek();
// pop出第一个元素
Integer element1 =stack.pop();

把 ArrayDeque 当成 “队列” 使用:

ArrayDeque<Integer> queue = new ArrayDeque<Integer>();
// 将元素加入"队列"
queue.push(2);
// 访问队列头部的元素, 但并不将其poll出"队列"
Integer element = queue.peek();
// poll出第一个元素
Integer element1 =queue.poll();

5.Collections

Collections 工具类提供了大量方法对集合元素进行查询、排序、修改等操作:

// 对Collection集合进行查找
Collections.max(collection); // 获取集合最大元素
Collections.min(collection); // 获取集合最小元素
Collections.frequency(collection , 1); // 判断1在集合中出现的次数

// 同步控制, 创建线程安全的集合对象
Collection c = Collections.synchronizedCollection(new ArrayList());
List l = Collections.synchronizedList(new ArrayList());
Set s = Collections.synchronizedSet(new HashSet());
Map m = Collections.synchronizedMap(new HashMap());

// 对List集合元素进行排序
Collections.reverse(list); // 将元素的次序反转
Collections.sort(list); // 将元素的按自然顺序排序
Collections.shuffle(list); // 将元素的按随机顺序排序

// 对List集合元素进行替换
Collections.replaceAll(list , 0 , 1); // 将List中的0使用1来代替
Collections.binarySearch(list , 1); // 使用二分法搜索指定的List集合, 以获得List集合中的索引, 只有排序后的List集合才可用二分法查询

// 设置不可变集合, 如果试图改变, 将引发UnsupportedOperationException异常
List unList = Collections.emptyList(); // 创建一个空的、不可改变的List对象
Set unSet = Collections.singleton(2); // 创建一个只有一个元素, 且不可改变的Set对象
Map<String, Object> map = new HashMap<String, Object>();
map.put("C" , 100);
Map unMap = Collections.unmodifiableMap(map); // 返回普通Map对象对应的不可变版本

除了使用 Collections 工具类创建线程安全的集合对象外,java.util.concurrent 包还提供了 HashMap 等集合线程安全的高效实现:ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、ConcurrentLinkedDeque。这些集合使用复杂的算法,通过允许并发地访问数据结构的不同部分来使竞争极小化。


常见的问题:

1、TreeMap、HashMap、LinkedHashMap 的区别?

这三种集合都是非线程安全的。

HashMap 键值无序,但是结果一般会按照自然结果排序,底层 hash 表;
LinkedHashMap 是 HashMap 的子类,增加了一个链表,用于记录插入时的顺序,所以可以保证插入的顺序,底层 hash 表;

TreeMap 键值有序,底层红黑树,默认按键的自然顺序排序,原理:按照实现的 Comparable 接口的 compareTo 这个方法的比较结果进行存储的;

注意:LinkedHashMap 记录的是插入顺序,TreeMap 记录的是自然顺序。

2、ArrayList 和 LinkedList 性能对比?

由于数组以一块连续内存区保存所有数组元素,所以内部以数组作为底层实现的集合在随机访问时性能最好;而内部以链表作为底层实现的集合在执行插入、删除操作时有较好的性能。总体 ArrayList 性能优于 LinkedList,大部分考虑使用 ArrayList。

  • 对于遍历List集合元素,ArrayList最好使用随机访问方法(get)来遍历,这样性能最好;LinkedList则最好用迭代器(Iterator)来遍历集合元素;
  • 如果要经常插入删除大量数据的List,建议使用LinkedList。

3、HashSet、TreeSet 性能?

HashSet 性能总是比 TreeSet 好,特别是添加、查询等操作,因为 TreeSet 需要额外的红黑树算法来维护集合元素次序。只有当需要一个保持排序的 Set 时才用 TreeSet。

你可能感兴趣的:(Java)