后端学习 - 容器


一 简介

Java容器可大致分为 List、Queue、Set、Map 四种。

  • List:存储的元素是有序的、可重复的
  • Set:存储的元素是无序的、不可重复的
  • Queue:按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的
  • Map:使用键值对(key-value)存储,类似于数学上的函数 y=f(x),“x” 代表 key,“y” 代表 value,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值
    在这里插入图片描述

1 线程安全的容器

  1. Collections 工具类提供的 synchronizedXX() 方法,将容器包装成线程安全的【效率低,不推荐】
  2. 古老的线程安全集合类 VectorHashtable【效率低,不推荐】
  3. java.util.concurrent 包下的集合类【推荐】
    • Concurrent 开头的集合类:ConcurrentHashMap, ConcurrentSkipListMap...
    • CopyOnWrite 开头的集合类:CopyOnWriteArrayList, CopyOnWriteArraySet...

2 CopyOnWrite 写时复制机制

  • 适用于读多写少的场景,降低写性能换取读性能
    • 读操作无需加锁
    • 写操作时会执行内存复制,在新内存区域执行写操作(此时如果有读操作,读的是旧内存区域的值),然后再将原引用指向修改后的内存区域
  • 缺点是复制操作给内存造成较大压力,且无法保证实时可见性,有可能读到旧数据

二 底层数据结构总结

1 List

数据结构备注
ArrayListObject[]
VectorObject[]
LinkedList双向链表JDK1.6 之前为循环链表,JDK1.7 取消了循环,常用的Java栈、队列实现
  • 栈采用 LinkedListArrayDequeDeque(双端队列)接口
  • 队列使用 LinkedListQueue 接口
  • 栈和队列都是双端队列的特殊情况
// LinkedList节点,被定义为私有的静态内部类
private static class Node<E> {
	E item;
	Node<E> next;
	Node<E> prev;
}

2 Set

  • SetMap 的特殊形式,只有 key 有意义,其 value 均为相同的固定值
数据结构备注
HashSetHashMap存储的元素无序
LinkedHashSetLinkedHashMap存储的元素可以选择按插入顺序访问顺序排序,默认是插入顺序
TreeSetTreeMap存储的元素有序

3 Queue

数据结构备注
PriorityQueueObject[] 实现二叉堆存储的元素按照指定的权值和顺序组织,首个元素有序,其它元素无序常用的Java堆实现
ArrayDequeObject[] + 双指针

4 Map

数据结构备注
HashMap数组+链表/红黑树存储的元素无序
LinkedHashMap数组+链表/红黑树,且 Entry 存在双向的引用存储的元素可以选择按插入顺序访问顺序排序
TreeMap红黑树(自平衡的排序二叉树)存储的元素有序
HashTable数组+链表存储的元素无序
  • HashMap
    • JDK1.8 之前 HashMap数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了使用拉链法解决哈希冲突而存在的
    • JDK1.8 以后当链表长度大于阈值(默认为 8)且键值对个数大于64时,将链表转化为红黑树,以减少搜索时间
    • 如果键值对个数小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)
    • 相较于 AVL 树,红黑树减弱了对平衡的要求,降低了保持平衡需要的开销
  • AVL树:
    • 追求绝对平衡,任何节点的两个子树的高度差的绝对值不超过1
    • 实现相对复杂
    • 这种严格的平衡条件使得AVL树在节点增删时,需要进行多次旋转操作来维持树的平衡
  • 红黑树:
    • 追求大致平衡,确保从根到叶子的最长路径不会超过最短路径的两倍长
    • 实现相对简单
    • 这种宽松的平衡条件使得红黑树在节点增删时,只需要进行少量的旋转操作(最多三次旋转)就能达到平衡
// HashMap数据结构
class HashMap<K, V> extends ... {
	Entry<K, V>[] table;
	int size;  // 键值对个数
	int threshold;  // 扩容阈值 = table.length * loadFactor
	float loadFactor;  // 负载因子,默认0.75

	// HashMap Entry
	static class Entry<K, V> implements Map.Entry<K, V> {
		K key;
		V value;
		Entry<K, V> next;  // 用于拉链法解决哈希冲突
		int hash;  // key哈希值
	}
}

在这里插入图片描述

  • LinkedHashMap
    • 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成
    • 每个 Entry 增加了双向引用,构成一条双向链表,可以 保持键值对的插入顺序或访问顺序
    • 访问顺序指的是,对一个 key 执行 put/get 后,将键值对移动到链表末尾(便于实现 LRU)
    • 指定按访问顺序组织链表,需要调用构造方法 public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) 并指定 accessOrder = true
  • TreeMap
    • 存放的元素必须是全序的:构造时元素需要实现 Comparable 接口,或者指定 Comparator
    • 迭代时输出按键排序
// TreeMap数据结构
class TreeMap<K, V> extends... {
	Comparator<? super K> comparator;
	Entry<K, V> root;
	int size;

	// TreeMap Entry(红黑树节点)
	static final class Entry<K, V> implements Map.Entry<K, V> {
		K key;
		V value;
		Entry<K, V> left;
		Entry<K, V> right;
		Entry<K, V> parent;
		boolean color;  // 标志节点红或黑
	}
}*

三 Collection 的子接口 List

1 ArrayList 与 Vector

  • ArrayListVector 的底层都是 Object[] 存储的,即对数组进行封装
  • ArrayListList 的主要实现类,适用于频繁的查找工作,线程不安全
  • VectorList 的古老实现类,线程安全

2 ArrayList 与 LinkedList

  • 都是线程不安全的
  • ArrayList 底层使用 Object[] 存储(支持随机访问),LinkedList 使用双向链表存储(不支持随机访问
  • ArrayList 的空间浪费主要体现在列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)
  • 获取/插入/删除元素的时间复杂度不同,体现在数组与链表的区别

3 ArrayList 的 JDK 7/8 差异

  • JDK 7:ArrayList 的对象的创建类似于单例的饿汉式
ArrayList list = new ArrayList();//底层创建了长度是*10*的Object[]数组elementData
list.add(123);//elementData[0] = new Integer(123);
list.add(11);//如果此次的添加导致底层elementData数组容量不够,则扩容。

/*
默认情况下,扩容为原来的容量的1.5倍,同时需要将原有数组中的数据复制到新的数组中。 
结论:建议开发中使用带参的构造器:ArrayList list = new ArrayList(int capacity)
*/
  • JDK 8: ArrayList 的对象的创建类似于单例的懒汉式,延迟了数组的创建,节省内存
ArrayList list = new ArrayList();  // 底层Object[] elementData初始化为{}.并没有创建长度为10的数组
list.add(123);  // 第一次调用add()时,底层才创建了长度10的数组,并将数据123添加elementData[0]
// 后续的添加和扩容操作与jdk 7 无异

4 ArrayList 的构造方法与扩容机制*

参考链接

  1. 三种构造方法:无参、传入指定长度、传入 Collection
// 属性
private static final int DEFAULT_CAPACITY = 10;  // 默认的容量大小(常量)
private static final Object[] EMPTY_ELEMENTDATA = {};  // 定义的空数组(final修饰,大小固定为0)
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};  // 定义的默认空容量的数组(final修饰,大小固定为0)
transient Object[] elementData;   // 定义的不可被序列化的数组,实际存储元素的数
private int size;  // 数组中元素的个数


// 无参的构造方法
"""
容量此时是0,元素个数size为默认值0
"""
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;  // 只有调用无参构造器才会获得 DEFAULTCAPACITY_EMPTY_ELEMENTDATA
}


// 传容量的构造方法
"""
initialCapacity > 0时,容量为initialCapacity,元素个数size为默认值0
initialCapacity = 0时,容量为0,元素个数size为默认值0
initialCapacity < 0时,抛出异常
"""
public ArrayList(int initialCapacity) {
    if (initialCapacity > 0) {
        this.elementData = new Object[initialCapacity];
    } else if (initialCapacity == 0) {
        this.elementData = EMPTY_ELEMENTDATA;  // 调用有参构造器传入0,获得EMPTY_ELEMENTDATA(区别于无参构造器)
    } else {
        throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
    }
}


// 传入Collection元素列表的构造方法
"""
如果传入的Collection不包含元素,容量是0,元素个数size为0
如果传入的Collection包含元素,容量为传入序列的长度,元素个数size也为序列长度,此时的ArrayList是满的
"""
public ArrayList(Collection<? extends E> c) {
    // 将列表转化为对应的数组
    elementData = c.toArray();
    if ((size = elementData.length) != 0) {
        // 原对象数组的数组类型转化为 Object对象数组的数组类型
        if (elementData.getClass() != Object[].class)
            elementData = Arrays.copyOf(elementData, size, Object[].class);
    } else {
        // 赋予空数组
        this.elementData = EMPTY_ELEMENTDATA;
    }
}
  1. 扩容add & grow
// 单参数的add方法,用户使用的方法
public boolean add(E e) {
    modCount++;
    add(e, elementData, size);
    return true;
}


// 重载的多参数add方法
private void add(E e, Object[] elementData, int s) {
    // 判断元素个数是否等于当前容量,如果相等则调用无参的grow方法
    if (s == elementData.length)
        elementData = grow();
    elementData[s] = e;
    size = s + 1;
}


// 无参的grow方法
private Object[] grow() {
    return grow(size + 1);  // 传递参数为需求的最小容量,也就是当前容量+1
}


// 重载的有参grow方法
"""
if语句中不会处理 用默认 __无参构造方法__ 创建的数组的 __初始扩容__ 情况,其余扩容情况都是由if语句处理

ArraysSupport.newLength函数的作用是创建一个大小为oldCapacity + max(minimum growth, preferred growth)的数组
minCapacity是传入的参数,上面显示它的值是当前容量+1,那么minCapacity - oldCapacity的值就恒为1,minimum growth的值也就恒为1
oldCapacity >> 1 右移一位,也就是减半,preferred growth的值即为oldCapacity大小的一半(当oldCapacity为0时,右移后还是0)
"""
private Object[] grow(int minCapacity) {
    // 当前容量
    int oldCapacity = elementData.length;
    // 如果当前容量大于0 或者 数组不是DEFAULTCAPACITY_EMPTY_ELEMENTDATA(创建时调用的是无参构造器)
    if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        int newCapacity = ArraysSupport.newLength(oldCapacity,
                									minCapacity - oldCapacity, /* 最小增长量 */
                									oldCapacity >> 1           /* 倾向增长量 */);
        return elementData = Arrays.copyOf(elementData, newCapacity);
    // 如果 数组是DEFAULTCAPACITY_EMPTY_ELEMENTDATA(容量等于0的话,只剩这一种情况了)
    } else {
        return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
    }
}
  • 容量增加 1 的情况:
    • 原来的容量为 0,而且是有参构造器创建的 ArrayList(传入 0 或者是空 Collection ,不能是无参构造器创建)
  • 容量变为原来1.5倍的情况:
    • 原来的容量大于 0
  • 容量变为 max(DEFAULT_CAPACITY, minCapacity) 的情况:
    • 原来的容量为 0,而且是无参构造器创建的 ArrayList(其中 DEFAULT_CAPACITY = 10
    • 用默认无参构造方法创建的数组在添加元素前,ArrayList 的容量为0,添加一个元素后,ArrayList 的容量就变为 10

四 Collection 的子接口 Set

1 HashSet、LinkedHashSet 和 TreeSet

  • HashSet
    • 底层数据结构是 HashMap
  • LinkedHashSet
    • 底层数据结构是 LinkedHashMap,元素的插入和取出顺序满足 FIFO 或 LRU 规则
    • HashSet 的子类,在添加数据的同时,每个 Entry 还维护了两个引用,记录此数据前一个数据和后一个数据
    • 对于频繁的遍历操作,LinkedHashSet 效率高于 HashSet
  • TreeSet
    • 底层数据结构是 TreeMap,元素是有序的,排序的方式有自然排序和定制排序
    • 比较两个对象是否相同的标准为:compare() 返回 0,而不再是 equals()
    • 不能放入无法排序的对象
  • 使用场景
    • 都不是线程安全的
    • HashSet 用于不需要保证元素插入和取出顺序的场景
    • LinkedHashSet 用于保证元素的插入和取出顺序满足 FIFO 或 LRU 的场景
    • TreeSet 用于支持对元素自定义排序规则的场景
	// TreeSet的自定排序(TreeMap同理)
    public void test2(){
        Comparator com = new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
            		// 定制比较方法...
                } else {
                    // 类型不匹配...
                }
            }
        };
        TreeSet set = new TreeSet(com);
    }

2 HashSet / HashMap 加入实例时,查重的方式**

  • 当对象加入 HashSet 时,HashSet 会先计算对象的 hashcode值来判断对象加入的位置
    • 如果该位置无对象,则 HashSet 中无重复元素,加入成功
    • 如果该位置有对象(下标相同的两个对象, hashcode不一定相同),与相同位置的其他的对象的 hashcode 值作比较
      • 如果没有相符的 hashcode,则无重复元素,加入成功
      • 有相同 hashcode 值的对象,这时会调用 equals() 方法来检查 hashcode 相等的对象是否真的相同,如果两者相同,HashSet 就不会让加入操作成功
    public void test3(){
        HashSet set = new HashSet();
        Person p1 = new Person(1001,"AA");
        Person p2 = new Person(1002,"BB");

        set.add(p1);
        set.add(p2);
        System.out.println(set);

        p1.name = "CC";
        set.remove(p1);
        System.out.println(set);  // BB, CC
        set.add(new Person(1001,"CC"));
        System.out.println(set);  // BB, CC, CC
        set.add(new Person(1001,"AA"));
        System.out.println(set);  // BB, CC, CC, AA
    }

五 Collection 的子接口 Queue

1 Queue 与 Deque

  • Queue 是单端队列,只能从一端插入元素,另一端删除元素,实现上一般遵循 FIFO 规则

  • Queue 的两套方法
    在这里插入图片描述

  • Deque 是双端队列,在队列的两端均可以插入或删除元素

  • Deque 的两套方法
    在这里插入图片描述

2 PriorityQueue【Java堆实现】

  • PriorityQueue 利用了二叉堆的数据结构来实现的,底层使用可变长的数组来存储数据
  • PriorityQueue 通过堆元素的上浮和下沉,实现了在 O(logn) 的时间复杂度内插入元素和删除堆顶元素。
  • PriorityQueue 是非线程安全的,且不支持存储无法比较的对象
  • PriorityQueue 同样要求元素实现 Comparable 接口,或传入 Comparator 对象
  • 遍历输出时,只有首个元素是有序的

3 ArrayDeque 与 LinkedList【Java栈和队列实现】

  • ArrayDequeLinkedList 都实现了 Deque 接口,两者都具有双端队列的功能,可以实现栈和队列
  • ArrayDeque 是基于可变长的数组和双指针来实现,而 LinkedList 则通过链表来实现
  • ArrayDeque 插入时可能存在扩容过程, 不过均摊后的插入操作依然为 O(1)
    • 虽然 LinkedList 不需要扩容,但是每次插入数据时需要申请空间,均摊性能相比更慢
    • 从性能的角度上,选用 ArrayDeque 来实现队列要比 LinkedList 更好

六 Collection 的子接口 Map

1 HashMap 和 Hashtable

  • Hashtable
    • 线程安全,内部的方法基本都经过 synchronized 修饰
    • 不允许有 null 键和 null 值,否则会抛出 NullPointerException
    • 创建时如果给定了容量初始值,会直接使用给定的大小
    • 基本被淘汰,不要在代码中使用它
  • HashMap
    • 非线程安全
    • 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个
    • 总是使用 2 的整数次幂作为数组长度,创建时如果给定了容量初始值,会将其扩充为 2 的幂次方大小

2 HashMap 的长度为什么是 2 的幂次方

  1. Hash 值的范围值-2147483648 到 2147483647,前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的,但一个 40 亿长度的数组,内存是放不下的,用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。位运算比求模更高效,hash%length == hash&(length-1) 的前提是 length 是 2 的整数次幂
  2. 便于调整原数据在新数组中的索引,根据原来的 hash 值新增的 bit 是0还是1,0=索引不变,1=原来的索引 + 原来哈希表的长度

3 ConcurrentHashMap 和 Hashtable

  • 均线程安全
  • ConcurrentHashMap 不允许 key/value 为 null,但是 HashMap 允许
  • 如果允许 value = null 当调用 map.get(key) 返回 null 的时候,代表两种含义
    1. 不存在这个key
    2. 这个key是存在的,只是 value 设置为 null
  • 此时如果有A、B两个线程,A线程调用 get(key) 方法返回 null,但是不知道属于上述哪种情况
  • 假设是第一种情况,调用 containsKey(key) 方法去做一个判断,期望的返回结果是 false
  • 但是恰好在A线程 get(key) 之后,调用 constainsKey(key) 方法之前B线程执行了 put(key, null),那么当A线程执行完 containsKey(key) 方法之后我们得到的结果是 true,与预期的结果不符
  • 数据结构
类型数据结构使用的锁
ConcurrentHashMap JDK1.7Segment 数组 + HashEntry 数组 + 链表Segment(本质是 ReentrantLock),每次锁若干 HashEntry
ConcurrentHashMap JDK1.8Node 数组 + 链表/红黑树synchronized,每次锁一个 Node
Hashtable数组+链表synchronized,每次锁全表
  1. 在 JDK1.7 的时候,ConcurrentHashMap 采用分段锁机制,对整个桶数组进行了分割分段(Segment,每个 Segment 都是一个可重入锁),每一个 Segment 只锁容器其中一部分数据,多线程访问容器里不同数据段的数据不会存在锁竞争,提高并发访问率
static class Segment<K,V> extends ReentrantLock implements Serializable {...}

在这里插入图片描述

  1. JDK1.8 的时候已经摒弃了 Segment 的概念,synchronized 只锁定当前链表或红黑二叉树的首节点,并发控制使用 synchronized 和 CAS 来操作
    • 在 JDK1.8 中还能看到 Segment 的数据结构,只是为了兼容旧版本
    • CAS 的操作体现在:写入 (key, value) 时,如果数组位置为空,则使用 CAS 写入当前键值对;如果数组位置不为空,则通过 synchronized 加锁写入链表/红黑树

在这里插入图片描述

  1. Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下
    • 当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低

在这里插入图片描述

4 为什么JDK1.8的 ConcurrentHashMap 使用 CAS+Synchronized 代替 Segment*

  • Segment 数组本质上是 ReentrantLock 的数组,其中的每一个 ReentrantLock 锁的是 HashEntry 数组的若干个位置

  • 如果把每个 ReentrantLock 锁的范围细化为一个位置,是否能与 synchronized 锁一个位置的效果相同?答案是否定的,因为Synchronized 在优化后有偏向锁、轻量级锁和重量级锁三个等级,在不同场景下均优于 ReentrantLock

    • 锁被细化到一个哈希桶,出现并发争抢的可能性就很低了。对于一个哈希桶,在没有多线程竞争时,使用 Synchronized 的偏向锁机制的效率是最高的
    • 出现争抢时,Synchronized 轻量级锁具有自旋机制,避免线程状态切换引起的开销;而 ReentrantLock 倾向于将获取不到锁的线程挂起

5 CAS (Compare And Swap)

V:要更新的变量(var)
E:预期值(expected)
N:新值(new)

  • 比较并交换的过程:判断V是否等于E,如果等于,将V的值设置为N;如果不等,说明已经有其它线程更新了V,则当前线程放弃更新,什么都不做
  • 当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作

6 JDK8 相较于 JDK7 在底层实现方面的不同

  • JDK 8 new HashMap()时,底层没有创建数组,首次调用 put() 方法时,底层创建长度为16的数组(和 ArrayList 的实现一样,延迟申请空间,由饿汉式变为懒汉式)
  • JDK7 底层结构是 HashEntry 数组+链表;JDK8 中底层结构:Node 数组 + 链表 / 红黑树(数组元素类型的改变是因为支持了链表转红黑树)
    • 当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前键值对个数 > 64时,此时此索引位置上的所数据改为使用红黑树存储

7 HashMap 底层实现的参数

  • DEFAULT_INITIAL_CAPACITY : HashMap的默认容量(数组长度),16
  • DEFAULT_LOAD_FACTORHashMap的默认装载因子:0.75,是对空间和时间效率的一个平衡选择
  • threshold:扩容的临界值,数值上等于 容量*填充因子:16 * 0.75 => 12
  • TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树,默认8
  • MIN_TREEIFY_CAPACITY:桶中的Node被树化时最小的hash表容量,默认64

9 JDK 8 HashMap 的扩容

  • HashMap 的数组长度一定是 2 的幂次,目的是方便计算哈希值对数组长度取模
  • HashMap 只有在插入元素时才会初始化(创建长为16的 Node 数组),或者扩容
    • 具体地,扩容还要满足两个条件之一
      1. 存入当前数据导致大于扩容阈值
      2. 存入数据到某一条链表时,该链表数据个数大于 8,且键值对个数小于 64
  • 扩容的具体操作:
    1. 将 Node 数组长度变为原来的 2 倍
    2. 调整原数据在新数组中的索引,调用 resize 方法,根据原来的 hash 值新增的 bit 是0还是1,0=索引不变,1=原来的索引 + 原来哈希表的长度

七 Iterator

1 类与接口

  • Iterable 接口有方法 iterator() 返回 Iterator 对象,使用该对象的方法进行遍历
  • 对象实现了 Iterable,就可以使用 for each 语法
  • 不实现 Iterable 的类也可以创建 Iterator 对象
public interface Iterable<T> {
	/**
	* 获取迭代器
	**/
	Iterator<T> iterator();
}

public interface Iterator() {
	/**
	* 是否有下个元素
	**/
	boolean hasNext();
	
	/**
	* 迭代器游标下移,并返回指向的元素
	**/
	E next();

	/**
	* 删除最后返回的元素
	**/
	void remove();
}

2 只读使用方法

        Iterator iterator = coll.iterator();
        while(iterator.hasNext()){
            // next():①指针下移 ②将下移以后集合位置上的元素返回
            System.out.println(iterator.next());
        }

3 迭代的问题:在循环中增加或删除元素

  • 只能使用 Iterator,因为迭代器内部会维护一些索引位置相关的数据,迭代过程中容器不能发生结构性变化,否则这些数据会失效
  • 如果还未调用 next() 或在上一次调用 next() 方法之后已经调用了 remove() 方法,再调用 iterator.remove() 都会抛出 IllegalStateException
  • iterator 调用的 remove() ,和集合对象的 remove() 不同
        // 删除集合中"Tom"
        Iterator iterator = coll.iterator();
        while (iterator.hasNext()){
            Object obj = iterator.next();
            if("Tom".equals(obj)){
                iterator.remove();
            }
        }

八 Collections 工具类

  • 核心思想是面向接口编程:Collections 提供了很多针对容器接口的通用算法和功能,只要实现了指定接口,就能调用其中的功能
  • 提供的功能大概分为如下两类

1 对传入的容器接口对象进行操作:查找/替换/排序/添加/修改

// 二分查找
public static <T> int binarySearch(List<? extends Comparable<? super T> list, T key>);
public static <T> int binarySearch(List<? extends T> list, T key, Comparator<? super T> c);

// 查找元素出现的次数
public static int frequency(Collection<?> c, Object o);

// 最大值/最小值
...

2 返回一个容器接口对象

  • 两种情况
  1. 适配器:将其它类型的数据转换成容器接口对象(输入其它类型,输出容器)
// 1.空容器方法:返回静态不可变的空容器接口对象,只读,用于节省创建新对象的开销
public static final <T> List<T> emptyList();
public static final <T> Set<T> emptySet();
public static final <K, V> Map<K, V> emptyMap();
public static <T> Iterator<T> emptyIterator();

// 2.单一对象方法:将一个单独的对象转换为不可变的容器接口对象,同样只读
public static <T> Set<T> singleton(T o);
public static <T> List<T> singletonList(T o);
public static <K, V> Map<K, V> singletonMap(K key, V value);
  1. 装饰器:修饰给定的容器接口对象,对其增强(输入容器,输出容器)
// 1.写安全:将修改容器的方法重写为抛出异常,返回只读容器
public static <T> Collection<T> unmodifiableCollection(Collection<? extends T> c);
public static <T> List<T> unmodifiableList(List<? extends T> list));
public static <T> Set<T> unmodifiableSet(Set<? extends T> set));
public static <K, V> Map<K, V> unmodifiableMap(Map<? extends K, ? extends V> m);

// 2.类型安全:在泛型失效的情况下(和老版本JDK代码交互时),确保容器元素类型正确
public static <E> List<E> checkedList(List<E> list, Class<E> type);
...

// 3.线程安全:向方法加锁,使容器变成线程安全的,不推荐使用
public static <T> List<T> synchronizedList(List<T> list));
...
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值