Java集合知识点回顾整理

请添加图片描述

List, Set, Queue, Map 四者的区别

  • List(对付顺序的好帮手): 存储的元素是有序的、可重复的。

  • Set(注重独一无二的性质): 存储的元素是无序的、不可重复的。

  • Queue(实现排队功能的叫号机): 按特定的排队规则来确定先后顺序,存储的元素是有序的、可重复的。

  • Map(用 key 来搜索的专家): 使用键值对(key-value)存储,类似于数学上的函数 y=f(x),“x” 代表 key,“y” 代表 value,key 是无序的、不可重复的,value 是无序的、可重复的,每个键最多映射到一个值。

集合底层数据结构

List

  • ArraylistObject[] 数组
  • VectorObject[] 数组
  • LinkedList: 双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环,现在是双向链表)

Set

  • HashSet(无序,唯一): 基于 HashMap 实现的,底层采用 HashMap 来保存元素

  • LinkedHashSet: LinkedHashSetHashSet 的子类,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的 LinkedHashMap 其内部是基于 HashMap 实现一样,不过还是有一点点区别的

  • TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树)

Queue

  • PriorityQueue: Object[] 数组来实现二叉堆
  • ArrayQueue: Object[] 数组 + 双指针

Map

  • HashMapJDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间
  • LinkedHashMapLinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。详细可以查看:
  • Hashtable: 数组+链表组成的,数组是 Hashtable 的主体,链表则是主要为了解决哈希冲突而存在的
  • TreeMap: 红黑树(自平衡的排序二叉树)

选用集合

  • 根据键值获取元素值使用Map接口下的集合

    • 排序:TreeSet
    • 不排序:HashMap
    • 线程安全:ConcurrentHashMap
  • 存取单个元素使用Collection接口下的集合

    • 保证元素唯一:set下的集合
    • 不需要保证元素唯一:List下的集合

为什么要使用集合来存储对象

  • 使用数组的话,数组的长度一旦声明后,就固定了。同时,数组中保存的元素类型也就固定了。
  • 使用集合的话,可以根据需要自动扩容或手动扩容;并且,集合中可以存储的对象类型是多种多样的。另外,有各种各样不同类型的集合满足生产需求。

Collection 子接口List

ArraylistVector 的区别?

  • Arraylist使用Object[ ]数组存储元素,适用于频繁的查找工作。非线程安全
  • Vector使用Object[ ]数组存储元素,线程安全

ArraylistLinkedList 的区别?

  • ArraylistLinkedList都是不同步的,线程不安全的。

  • Arraylist底层使用Object[ ]数组存储元素;LinkedList 底层在1.7之前使用循环链表,1.7开始取消循环链表使用双向链表

  • 随机插入和删除元素是否受元素位置的影响:

    • Arraylist采用数组实现,随机插入和删除元素会导致这个位置之后的每个元素都移动。所以随机插入和删除元素是受元素位置影响的,在首尾添加时间复杂度都为O(1)。
    • LinkedList采用链表实现,随机插入和删除元素是受元素位置影响的,需要先将指针移动到指定位置才可以插入删除元素。在首尾添加时间复杂度都为O(1)。
  • 快速随机访问元素:

    • Arraylist采用数组实现,可以根据索引快速定位到元素。
    • LinkedList采用链表实现,无法实现快速随机访问元素。
  • 内存占用

    • Arraylist的内存占用主要是数组需要预留一定的空间来存储新的元素;
    • LinkedList的内存占用是每一个元素占用的空间都比Arraylist更多,因为每一个元素需要存储直接前驱、直接后继和数据。
  • 补充:RandomAccess 接口

    RandomAccess接口中并没有任何语句,根据注释可以发现,这个借口是一个标志借口。实现了RandomAccess这个接口的集合,具有随即快速访问的功能。

/**Marker interface used by List implementations to indicate that they support fast (generally constant time) random access. 
列表实现所使用的标记接口,用于指示它们支持快速(通常为常数时间)随机访问。
public interface RandomAccess {
}

Arraylist实现了RandomAccess接口,所以是支持快速随机访问的。当然主要是因为它的数据结构决定了它适用于快速访问。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{

​ 而LinkedList并没有实现RandomAccess接口,底层的数据结构也不支持LinkedList快速访问。

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{

ArrayList 的扩容机制

  • 默认初始化容量为10

    JDK1.7之前无参构造创建的ArrayList是直接创建一个容量为10的Object [ ]数组

    JDK1.7开始无参构造创建的ArrayList是创建一个空Object [ ]数组,当插入第一个元素时,会将数组容量扩容到10。

/**
 * Default initial capacity.
 */
private static final int DEFAULT_CAPACITY = 10;
  • 使用add()方法添加元素超出当前数组容量时,数组会进行自动扩容。在调用add()方法后,会先调用ensureCapacityInternal方法获取当前容量和插入元素需要的最小容量;

        /**
         * 将指定的元素追加到此列表的末尾。
         */
        public boolean add(E e) {
       //添加元素之前,先调用ensureCapacityInternal方法
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            //这里看到ArrayList添加元素的实质就相当于为数组赋值
            elementData[size++] = e;
            return true;
        }
    
       //得到最小扩容量
        private void ensureCapacityInternal(int minCapacity) {
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                  // 获取默认的容量和传入参数的较大值
                minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
            }
    
            ensureExplicitCapacity(minCapacity);
        }
    

    再调用ensureExplicitCapacity方法判断当前数组应该有的最小容量是否大于默认容量;

      //判断是否需要扩容
        private void ensureExplicitCapacity(int minCapacity) {
            modCount++;
    
            // overflow-conscious code
            if (minCapacity - elementData.length > 0)
                //调用grow方法进行扩容,调用此方法代表已经开始扩容了
                grow(minCapacity);
        }
    

    如果大于,则会调用==grow方法进行扩容。使用位运算将旧容量右移一位,也就是扩大了1.5倍==。

    /**
     * 要分配的最大数组大小
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

    /**
     * ArrayList扩容的核心方法。
     */
    private void grow(int minCapacity) {
        // oldCapacity为旧容量,newCapacity为新容量
        int oldCapacity = elementData.length;
        //将oldCapacity 右移一位,其效果相当于oldCapacity /2,
        //我们知道位运算的速度远远快于整除运算,整句运算式的结果就是将新容量更新为旧容量的1.5倍,
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //然后检查新容量是否大于最小需要容量,若还是小于最小需要容量,那么就把最小需要容量当作数组的新容量,
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
       // 如果新容量大于 MAX_ARRAY_SIZE,进入(执行) `hugeCapacity()` 方法来比较 minCapacity 和 MAX_ARRAY_SIZE,
       //如果minCapacity大于最大容量,则新容量则为`Integer.MAX_VALUE`,否则,新容量大小则为 MAX_ARRAY_SIZE 即为 `Integer.MAX_VALUE - 8`。
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

​ 其中调用了Arrays.copyOf()方法对原有数组进行扩容

  • 在 add 大量元素之前用 ensureCapacity 方法,以减少增量重新分配的次数

Collection 子接口Set

无序性和不可重复性

  • 无序性:是指存储的数据并非按照索引的顺序来添加,而是使用哈希值确定添加位置
  • 不可重复性:是指添加元素时,需要调用equals()方法进行判断集合中是否已经添加了和这个元素值相等的元素。必须要同时重写equals()方法和hashCode()方法。

比较 HashSet、LinkedHashSet 和 TreeSet

  • 都是Set接口下的实现类,都可以保证元素唯一,并且都是线程不安全的。

  • HashSet的底层是基于HashMap实现的哈希表LinkedHashSet的底层数据结构是链表和哈希表TreeSet底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。

  • 应用场景:

    HashSet 用于不需要保证元素插入和取出顺序的场景,

    LinkedHashSet 用于保证元素的插入和取出顺序满足 FIFO 的场景,

    TreeSet 用于支持对元素自定义排序规则的场景。

comparable 和 Comparator 的区别

Comparable接口:

public interface Comparable<T> {
    public int compareTo(T o);
}

Comparator 接口:

public interface Comparator<T> {

    int compare(T o1, T o2);
  • 如果需要对一个集合实现两种排序方式,就可以重写compareTo方法并且自定义Comparator重写排序规则。

Collection 子接口Queue

Queue 与 Deque 的区别

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

    Queue 因为自身容量问题导致操作失败有两种处理方式:抛出异常和返回特殊值

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

    Deque继承自Queue,在队首和队尾都增加了插入和删除的方法。也根据操作失败后处理方式的不同分为两类。

    Deque中还提供了pushpop方法,可以模拟栈。

ArrayDeque 与 LinkedList 的区别

  • ArrayDequeLinkedList都实现了Deque接口,都具有双端队列的功能。

  • ArrayDeque是基于Object[]数组和双指针实现的,而LinkedList是双向链表

  • ArrayDeque 不支持存储 NULL 数据,但 LinkedList 支持。

  • ArrayDeque 是在 JDK1.6 才被引入的,而LinkedList 早在 JDK1.2 时就已经存在。

  • ArrayDeque 数组的初始化容量是8,插入值可能会存在扩容。LinkedList 无需扩容,但每一次插入数据都需要新的堆内存,均摊性能较低。

从性能的角度上,选用 ArrayDeque 来实现队列要比 LinkedList 更好。此外,ArrayDeque 也可以用于实现栈。

PriorityQueue

PriorityQueue 是在 JDK1.5 中被引入的, 其与 Queue 的区别在于元素出队顺序是与优先级相关的,即总是优先级最高的元素先出队。

  • PriorityQueue 利用了二叉堆的数据结构来实现的,底层使用可变长的数组来存储数据
  • PriorityQueue 通过堆元素的上浮和下沉,实现了在 O(logn) 的时间复杂度内插入元素和删除堆顶元素。
  • PriorityQueue 是非线程安全的,且不支持存储 NULLnon-comparable 的对象。
  • PriorityQueue 默认是小顶堆,但可以接收一个 Comparator 作为构造参数,从而来自定义元素优先级的先后。

Map接口

HashMap 和 Hashtable 的区别

  • 是否线程安全:Hashtable中的方法都是有synchronized关键字修饰的,是线程安全的;HashMap中的方法没有synchronized关键字修饰,是线程不安全的;

    public synchronized boolean isEmpty() {
        return count == 0;
    }
    public synchronized Enumeration<K> keys() {
        return this.<K>getEnumeration(KEYS);
    }
    public synchronized Enumeration<V> elements() {
        return this.<V>getEnumeration(VALUES);
    }
    
  • **效率:**因为HashMap是非线程安全的,不存在上锁,所以效率会比Hashtable更高一些。

  • 对 Null key 和 Null value 的支持:HashMap是支持Null key 和 Null value 的,但作为key只允许存在一个键位null,value可以有多个。而Hashtable中是不允许存储null键和null值的。

请添加图片描述

  • 初始容量大小和每次扩充容量大小的不同 :

    • HashMap的初始容量是16,每次扩容到之前容量的2倍。创建时如果指定了初始化容量大小,HashMap会将其扩充为2的幂次方大小。
    • Hashtable的初始容量是11,每次扩容为2n+1倍。创建时如果指定了初始化容量大小,Hashtable就会使用指定的容量初始化集合。

    注:Hashtable 基本被淘汰,在实际的业务中,很少使用。

HashMap 和 HashSet 区别

  • HashSet底层就是使用HashMap实现的,除去重写了少量方法,大部分方法都是直接使用HashMap中的方法。
HashMapHashSet
实现了Map接口实现了Set接口
存储键值对存储的是对象
put方法添加元素add方法添加元素
HashMap是使用key来计算hashCodeHashSet使用对象来计算hashCode

HashMap 和 TreeMap 区别

请添加图片描述

NavigableMap接口:实现该接口的集合具有对集合内元素的搜索能力

SortedMap接口:实现该接口的集合具有根据key值排序的能力,默认按照升序排序,但也可以自定义排序规则。

所以,TreeMap相比于 HashMap 主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力。

HashSet 如何检查重复

  • 插入对象时,会先计算对象对应的hashCode来判断对象加入的位置,并且检查集合中是否有相同hashCode值的元素,如果有,会使用equals方法来判断两个对象是否真的相同。如果返回true,就无法插入该元素;如果返回false,就 会重新生成hashCode值确定插入元素。

  • 通过查看源码。无论HashSet中是否存在该元素,都会先插入,然后通过返回值来告诉我们插入前是否存在相同元素。

/**
 * Implements Map.put and related methods
 *
 * @param hash hash for key
 * @param key the key
 * @param value the value to put
 * @param onlyIfAbsent if true, don't change existing value
 * @param evict if false, the table is in creation mode.
 * @return previous value, or null if none
 如果插入位置没有元素返回null,否则返回上一个元素
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
  • hashCode()equals() 的相关规定:

    • hashCode()是对堆上的对象产生独特值。

    • hashCode()值相同,不一定是相同对象。存在哈希碰撞。

    • hashCode()值不相同,一定是不相同对象。

    • 相同对象的 equals() 方法返回 true。

      总结:必须同时重写hashCode()equals()

HashMap

JDK1.8 之前的底层实现

JDK1.8 之前 HashMap 底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。

扰动函数:指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。

“拉链法” 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。

JDK1.8 之后的底层实现

  • 解决哈希冲突的方法变化:当链表长度大于阈值(默认为8),并且当此时数组长度是大于64的(如果小于64,会先进行数组扩容),就会将单向链表转换为红黑树,减少搜索时间。

  • 使用红黑树的原因:解决二叉查找树存在的缺陷:如果因为算法问题,大量的元素都存放在了二叉树的一个分支上,这样就影响了效率。

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

为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。

在Hash 值的范围值下,大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。

这个数组下标的计算方法是“ (n - 1) & hash”。(n 代表数组长度)。

使用取余运算来实现:hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方,并且使用&运算相比于%提高了运算效率,这就解释了为什么HashMap 的长度为什么是 2 的幂次方。

多线程操作HashMap 导致死循环问题

  • 并发下的 Rehash 会造成元素之间会形成一个循环链表。
  • 不过,jdk 1.8 后解决了这个问题,但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在其他问题比如数据丢失。
  • 并发环境下推荐使用 ConcurrentHashMap

HashMap常见的遍历方式

  • 迭代器(Iterator)方式遍历;

    • 使用迭代器(Iterator)EntrySet 的方式进行遍历;

      long l = System.currentTimeMillis();
      Iterator<Map.Entry<Integer, String>> iterator = hashMap.entrySet().iterator();
      while (iterator.hasNext()) {
          Map.Entry<Integer, String> next = iterator.next();
          System.out.println("key: " +next.getKey() + " | value: " + next.getValue());
      }
      long l1 = System.currentTimeMillis() - l;
      System.out.println("遍历用时:" + l1);
      
    • 使用迭代器(Iterator)KeySet 的方式进行遍历;

      long l = System.currentTimeMillis();
      Iterator<Integer> iterator = hashMap.keySet().iterator();
      while (iterator.hasNext()) {
          Integer next = iterator.next();
          System.out.println("key: " + next + " | value: " + hashMap.get(next));
      }
      long l1 = System.currentTimeMillis() - l;
      System.out.println("遍历用时:" + l1);//遍历用时:0
      
  • For Each 方式遍历;

    • 使用 For Each EntrySet 的方式进行遍历;

      long l = System.currentTimeMillis();
      for (Map.Entry<Integer, String> entry: hashMap.entrySet()) {
          System.out.println("key: " + entry.getKey() + " | value: " + entry.getValue());
      }
      long l1 = System.currentTimeMillis() - l;
      System.out.println("遍历用时:" + l1);//遍历用时:0
      
    • 使用 For Each KeySet 的方式进行遍历;

      long l = System.currentTimeMillis();
      for (Integer key : hashMap.keySet()) {
          System.out.println("key: " + key + " | value: " + hashMap.get(key));
      }
      long l1 = System.currentTimeMillis() - l;
      System.out.println("遍历用时:" + l1);//遍历用时:0
      
  • Lambda 表达式遍历(JDK 1.8+);

    • 使用 Lambda 表达式的方式进行遍历;

      long l = System.currentTimeMillis();
      // 遍历
      hashMap.forEach((key, value) -> {
          System.out.println("key: " + key + " | value: " + value);
      });
      long l1 = System.currentTimeMillis() - l;
      System.out.println("遍历用时:" + l1);//遍历用时:38
      
  • Streams API 遍历(JDK 1.8+)。

    • 使用 Streams API 单线程的方式进行遍历;

      long l = System.currentTimeMillis();
      	hashMap.entrySet().stream().forEach((entry) -> {
      System.out.println("key: " + entry.getKey() + " | value: " + entry.getValue());
      });
      long l1 = System.currentTimeMillis() - l;
      System.out.println("遍历用时:" + l1);//遍历用时:37
      
    • 使用 Streams API 多线程的方式进行遍历。

      long l = System.currentTimeMillis();
      hashMap.entrySet().parallelStream().forEach((entry) -> {
          System.out.println("key: " + entry.getKey() + " | value: " + entry.getValue());
      });
      long l1 = System.currentTimeMillis() - l;
      System.out.println("遍历用时:" + l1);//遍历用时:38
      

性能对比:

EntrySetKeySet 的性能高

原因:

KeySet 在循环时使用了 map.get(key)在使用迭代器或者 for 循环时,其实已经遍历了一遍 Map 集合了,因此再使用 map.get(key) 查询时,相当于遍历了两遍

EntrySet 只遍历了一遍 Map 集合,之后通过代码“Entry<Integer, String> entry = iterator.next()”把对象的 keyvalue 值都放入到了 Entry 对象中,因此再获取 keyvalue 值时就无需再遍历 Map 集合,只需要从 Entry 对象中取值就可以了。

所以,EntrySet 的性能比 KeySet 的性能高出了一倍,因为 KeySet 相当于循环了两遍 Map 集合,而 EntrySet 只循环了一遍

ConcurrentHashMap

底层具体实现

  • JDK1.7

请添加图片描述

ConcurrentHashMap 是由 Segment 数组结构 + HashEntry 数组结构组成

将数据分段存储,然后为每一段数据分配一把锁,当操作某一段数据时候,不会影响对其它段的数据。

Segment 的个数一旦初始化就不能改变,默认Segment的个数是16个,也可以认为ConcurrentHashMap 最多支持16个线程并发。

Segment 实现了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数据。

据。

static class Segment<K,V> extends ReentrantLock implements Serializable {
}

一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和 HashMap 类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 的锁。

  • JDK1.8

请添加图片描述

ConcurrentHashMap 取消了 Segment 分段锁,采用 CAS 和 synchronized 来保证并发安全。数据结构跟 HashMap1.8 的结构类似,数组+链表/红黑二叉树。Java 8 在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N)))

synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率又提升 N 倍。

ConcurrentHashMap 和 Hashtable 的区别

区别主要体现在实现线程安全的方式上不同。

  • 底层数据结构:

    • JDK1.7 的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟 HashMap1.8 的结构一样,数组+链表/红黑二叉树。
    • Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
  • 实现线程安全的方式(重要):

    • 在 JDK1.7 的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 到了 JDK1.8 的时候已经摒弃了 Segment 的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6 以后 对 synchronized 锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在 JDK1.8 中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;
    • Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。

Collections 工具类

  1. 排序

    List<Integer> list = new ArrayList<>();
    list.add(16);
    list.add(25);
    list.add(14);
    list.add(43);
    Collections.reverse(list);//反转
    Collections.shuffle(list);//随机排序
    Collections.sort(list);//按照自然升序排
    Collections.swap(list,1,2);//交换对应索引的元素
    Collections.rotate(list,1);//旋转。当distance为正数时,将list后distance个元素整体移到前面。当distance为负数时,将 list的前distance个元素整体移到后面
    for (Integer integer : list) {
        System.out.println(integer);
    }
    
  2. 查找,替换操作

    Collections.binarySearch(list,3);//二分查找,前提是有序数组
    Collections.max(list);//按照自然顺序返回最大的元素
    Collections.frequency(list,25);//统计指定元素的出现次数
    Collections.indexOfSubList(list,list1);//统计list1在list中第一次出现的索引,找不到的话
    Collections.replaceAll(list,25,56);//用新元素替代在集合中出现的旧元素
    Collections.fill(list,25);//用指定元素替代在集合中所有的元素
    
  3. 同步控制(不推荐,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合)

    Collections 提供了多个synchronizedXxx()方法·,该方法可以将指定集合包装成线程同步的集合,从而解决多线程并发访问集合时的线程安全问题。

    HashSetTreeSetArrayList,LinkedList,HashMap,TreeMap 都是线程不安全的。Collections 提供了多个静态方法可以把他们包装成线程同步的集合。

    注:尽量不使用这些方法,效率非常低,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合。

Collections.synchronizedList()
Collections.synchronizedMap()
Collections.synchronizedSet()

使用集合的注意事项

集合判空

判断所有集合内部的元素是否为空,使用 isEmpty() 方法,而不是 size()==0 的方式。

  • 因为isEmpty() 方法的可读性更好,并且时间复杂度为 O(1)。
  • 绝大多数集合的 size()的时间复杂度也为 O(1),但java.util.concurrent 包下的某些集合(ConcurrentLinkedQueueConcurrentHashMap…)时间复杂度就不是O(1)。

集合转 Map

在使用 java.util.stream.Collectors 类的 toMap() 方法转为 Map 集合时,一定要注意当 value 为 null 时会抛 空指针异常。

toMap() 调用了Map接口下的merge()方法

Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
                            Function<? super T, ? extends U> valueMapper,
                            BinaryOperator<U> mergeFunction,
                            Supplier<M> mapSupplier) {
    BiConsumer<M, T> accumulator
            = (map, element) -> map.merge(keyMapper.apply(element),
                                          valueMapper.apply(element), mergeFunction);
    return new CollectorImpl<>(mapSupplier, accumulator, mapMerger(mergeFunction), CH_ID);
}

这个方法是Map接口的默认实现,会先判断值是否为NULL

default V merge(K key, V value,
        BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
    Objects.requireNonNull(remappingFunction);
    Objects.requireNonNull(value);

如果valuenull,会抛出NPE异常

public static <T> T requireNonNull(T obj) {
    if (obj == null)
        throw new NullPointerException();
    return obj;
}

集合遍历

不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁。

foreach 语法糖底层其实还是依赖 Iterator 。不过, remove/add 操作直接调用的是集合自己的方法,而不是 Iteratorremove/add方法

这就导致 Iterator 莫名其妙地发现自己有元素被 remove/add ,然后,它就会抛出一个 ConcurrentModificationException 来提示用户发生了并发修改异常。这就是单线程状态下产生的 fail-fast 机制

fail-fast 机制 :多个线程对 fail-fast 集合进行修改的时候,可能会抛出ConcurrentModificationException。 即使是单线程下也有可能会出现这种情况,上面已经提到过。

集合去重

可以利用 Set 元素唯一的特性,可以快速对一个集合进行去重操作,避免使用 Listcontains() 进行遍历去重或者判断包含操作。

HashSetArrayList 为例

HashSetcontains()方法依赖于HashMapcontainsKey()方法,时间复杂度接近于 O(1)

public boolean contains(Object o) {
    return map.containsKey(o);
}

ArrayListcontains()方法调用了indexOf()方法,相当于遍历集合,时间复杂度接近于 O(n)

public boolean contains(Object o) {
    return indexOf(o) >= 0;
}

/**
 * Returns the index of the first occurrence of the specified element
 * in this list, or -1 if this list does not contain the element.
 * More formally, returns the lowest index <tt>i</tt> such that
 * <tt>(o==null&nbsp;?&nbsp;get(i)==null&nbsp;:&nbsp;o.equals(get(i)))</tt>,
 * or -1 if there is no such index.
 */
public int indexOf(Object o) {
    if (o == null) {
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

集合转数组

使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全一致、长度为 0 的空数组。

toArray(T[] array) 方法的参数是一个泛型数组,如果 toArray 方法中没有传递任何参数的话返回的是 Object类 型数组。

String[] strings = new String[]{"hello","spring","redis","linux"};
for (String string : strings) {
    System.out.println(string);
}

List<String> list2 = Arrays.asList(strings);
String[] strings1 = list2.toArray(new String[0]);
for (String s : strings1) {
    System.out.println(s);
}

由于 JVM 优化,new String[0]作为Collection.toArray()方法的参数现在使用更好,new String[0]就是起一个模板的作用,指定了返回数组的类型,0 是为了节省空间,因为它只是为了说明返回的类型。

数组转集合

使用工具类 Arrays.asList() 把数组转换成集合时,不能使用其修改集合相关的方法, 它的 add/remove/clear 方法会抛出 UnsupportedOperationException 异常。

1、Arrays.asList()是泛型方法,传递的数组必须是对象数组,而不是基本类型。

​ 当传入一个原生数据类型数组时,Arrays.asList() 的真正得到的参数就不是数组中的元素,而是数组对象本身!此时 List 的唯一元素就是这个数组,这也就解释了上面的代码。

​ 使用包装类型数组就可以解决这个问题。

2、使用集合的修改方法: add()remove()clear()会抛出异常。

Arrays.asList() 方法返回的并不是 java.util.ArrayList ,而是 java.util.Arrays 的一个内部类,这个内部类并没有实现集合的修改方法或者说并没有重写这些方法。

如何正确的将数组转换为 ArrayList ?

​ (1)最简便的方法

List list = new ArrayList<>(Arrays.asList("a", "b", "c"))

​ (2)使用 Java8 的 Stream(推荐)

Integer [] myArray = { 1, 2, 3 };
List myList = Arrays.stream(myArray).collect(Collectors.toList());
//基本类型也可以实现转换(依赖boxed的装箱操作)
int [] myArray2 = { 1, 2, 3 };
List myList = Arrays.stream(myArray2).boxed().collect(Collectors.toList());

​ (3)使用 Apache Commons Collections

List<String> list = new ArrayList<String>();
CollectionUtils.addAll(list, str);

​ (4) 使用 Java9 的 List.of()方法

Integer[] array = {1, 2, 3};
List<Integer> list = List.of(array);
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值