集合

114 阅读10分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 1 天,点击查看活动详情

框架体系图

Collection:

image.png

Map:

image.png

Collection

常用方法:

  • 添加

    • add(Object obj)
    • addAll(Collection coll)
  • 获取有效元素的个数

    • int size()
  • 清空集合

    • void clear()
  • 是否为空集合

    • boolean isEmpty()
  • 是否包含某个元素

    • boolean contains(Object obj):是通过元素的equals方法来判断是否是同一个对象
    • boolean containsAll(Collection c):也是调用元素的equals方法来比较的。拿两个集合的元素挨个比较
  • 删除

    • boolean remove(Object obj) :通过元素的equals方法判断是否是要删除的那个元素。只会删除找到的第一个元素
    • boolean removeAll(Collection coll):取当前集合的差集,移除coll的所有元素
  • 取两个集合的交集

    • boolean retainAll(Collection c):把交集的结果存在当前集合中,不影响c
  • 集合是否相等

    • boolean equals(Object obj)
  • 转成对象数组

    • Object[] toArray()
  • 数组转为List集合

    • Arrays.asList(T... a) : 形参如果是基本数据类型数组,会被当成一个整体元素

      • 小坑:返回的ArrayList是Arrays类自己定义的一个静态内部类而不是java.util包下的ArrayList,这个内部类没有实现add()、remove()方法,而是直接使用它的父类AbstractList的相应方法,而AbstractList中的add()和remove()是直接抛出java.lang.UnsupportedOperationException异常的。
  • 遍历

    • iterator():返回迭代器对象,用于集合遍历

       while (iterator.hasNext()){
           System.out.println(iterator.next());
       }
      
      • hasNext():根据指针判断是否还有下一个元素
      • next():①指针下移 ②将下移以后集合位置上的元素返回
      • remove():根据指针删除,少用慎用
    • 增强for循环(forEach循环)

    • 普通for循环

List

有序的,可重复的,又称之为动态数组

  • ArrayList:作为List接口的主要实现类;线程不安全的,效率高;底层使用Object[] elementData存储
  • LinkedList:对于频繁的插入和删除操作,使用此类效率比ArrayList高;底层使用双向链表存储
  • Vector:作为List接口的古老实现类;线程安全的,效率低;底层使用Object[] elementData存储

常用方法:

  • void add(int index, Object ele):在index位置插入ele元素
  • boolean addAll(int index, Collection eles):从index位置开始将eles的所有元素添加进来
  • Object get(int index):获取指定index位置的元素
  • int indexOf(Object obj):返回obj在集合中首次出现的位置
  • int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置
  • Object remove(int index):移除指定index位置的元素,并返回此元素
  • Object set(int index, Object ele):设置指定index位置的元素为ele
  • List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex位置的集合,左闭右开,不会影响原本的集合

ArrayList

  • JDK1.7:使用空参构造器创建一个初始容量为10的数组;默认情况扩容为原本的1.5倍
  • JDK1.8:使用空参构造器创建一个初始容量为0的数组,当添加第一个元素时再创建一个容量为10的数组;后续添加和扩容操作与JDK1.7一致

LinkedList

双向链表,内部没有声明数组,而是定义了Node类型的first和last,用于记录首末元素。同时,定义内部类Node,作为LinkedList中保存数据的基本结构。Node除了保存数据,还定义了两个变量:

  • prev变量记录前一个元素的位置
  • next变量记录后一个元素的位置
 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;
     }
 }

image.png

Vector

无参构造器创建时底层是一个长度为10的数组;默认扩容为原来的2倍

Set

无序性:指的是添加的时候不是从0开始存储的。不等于随机性。

不可重复性不只equals()判断。

Set接口没有额外定义新的方法,使用的都是Collection中声明的方法。

  • HashSet:作为Set接口的主要实现类;线程不安全的;可以存储null值
  • LinkedHashSet:作为HashSet的子类,遍历其内部数据时,可以按照添加的顺序遍历
  • TreeSet:可以按照添加对象的指定属性进行排序

HashSet

添加元素过程:

  1. 向HashSet添加元素a,首先调用a所在类的hashCode()方法,计算a的哈希值
  2. 此哈希值通过某种算法计算出在HashSet底层数组的存放位置(即索引位置),判断数组此位置是是否已经有元素
  • 此位置没有其他元素,则a添加成功——情况1

  • 此位置有其他元素b(或以链表形式存在多个元素),则比较a与b的hash值

    • hash值不相同,则a添加成功——情况2

    • hash值相同,调用a所在类的equals()方法

      • equals()返回false,则a添加成功——情况3
      • equals()返回true,a添加失败

对于添加成功的情况2和情况3而言:a与指定索引的元素以链表的方式存储。

  • JDK1.7:a放到数组中,指向原来的元素
  • JDK1.8:原来的元素放在数组中,指向a

HashSet底层:使用HashMap来作为存储结构,value指向同一个Object。

image.png

LinkedHashSet

  • LinkedHashSet插入性能略低于HashSet,但在遍历Set时有很好的性能。
  • 在HashSet的基础上使用双向链表维护元素的顺序,可以根据插入顺序进行遍历。
 Set set = new LinkedHashSet();
 set.add(new String("AA"));
 set.add(456);
 set.add(new Customer("刘德华", 1001));

image.png

TreeSet

  • 向TreeSet添加的元素,必须是相同类的对象。

  • TreeSet是根据compareTo(obj)compare(Object o1,Object o2)是否为0判断元素是否相同

  • 两种排序方式:

    • 自然排序:compareTo(obj),在元素类
    • 定制排序:compare(Object o1,Object o2),在构造器中,匿名内部类(可用Lambda表达式代替)

Map

存储key-value对的数据。key-value构成了一个Entry对象。

  • HashMap:作为Map的主要实现类;线程不安全的,效率高;可以存储null的key和value

    • LinkedHashMap:保证在遍历map元素中,可以按照添加的顺序实现遍历
  • TreeMap:可以按照key进行自然排序或定制排序

  • Hashtable:作为古老的实现类;线程安全的,效率低;不能存储null的key和value

    • Properties:常用来处理配置文件;key和value都是String类型

常用方法:

  • Object put(Object key,Object value):如果key之前有对应的value,则返回对应value,否则返回null

  • void putAll(Map m):将m中的所有key-value对存放到当前map中

  • Object remove(Object key):移除指定key的key-value对,并返回value

  • void clear():清空当前map中的所有数据。不是map = null

  • Object get(Object key):获取指定key对应的value

  • boolean containsKey(Object key):是否包含指定的key。先找哈希值,再equals

  • boolean containsValue(Object value):是否包含指定的value

  • int size():返回map中key-value对的个数

  • boolean isEmpty():判断当前map是否为空

  • boolean equals(Object obj):判断当前map和参数对象obj是否相等

  • 遍历

    • Set keySet():返回所有key构成的Set集合
    • Collection values():返回所有value构成的Collection集合
    • Set entrySet():返回所有key-value对构成的Set集合

HashMap

JDK7

 HashMap map = new HashMap();
 //....执行多次put
 map.put(key1, value1);
  • 实例化之后,底层创建了长度为16的一维数组Entry[] table

  • 调用key1所在类的hashCode()计算key1的哈希值,此哈希值经过某种算法计算之后,得到在Entry数组中的存放位置

    • 如果此位置上的数据为空,则Entry添加成功

    • 如果此位置上的数据不为空,比较key1和已经存在的一个或多个数据(以链表形式存在)的哈希值

      • 如果key1的哈希值和已经存在的数据哈希值都不相同,则Entry添加成功 ----情况1

      • 如果和某一个数据(key2-value2)的哈希值相同,调用key1所在类的equals(key2)方法进行比较

        • 返回false,则Entry添加成功 ----情况2
        • 返回true,使用value1替换value2

关于情况1和情况2,此时的Entry和原来的数据以链表的方式存储

扩容:在到达扩容的临界值且此位置上的数据不为空,扩容为原来容量的2倍。

源码中的重要常量:

  • DEFAULT_INITIAL_CAPACITY: HashMap的默认容量,16
  • DEFAULT_LOAD_FACTOR:HashMap的默认加载因子,0.75
  • threshold:扩容的临界值,= 容量 * 加载因子,16 * 0.75 =>12

image.png

JDK8

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

  • 实例化时底层没有创建数组,在首次调用put()方法时创建长度为16的数组
  • 底层数组是Node[],而非Entry[]。只是单纯改个名,本身属性没有变
  • JDK7底层结构:数组+链表。JDK8:数组+链表+红黑树
  • 形成链表结构时,新添加的key-value对在链表的尾部
  • 当数组在某一个索引位置上的元素(以链表形式存在)> 8数组长度 > 64时,此索引位置上的所有数据改为使用红黑树存储

源码中新增的常量:

  • TREEIFY_THRESHOLD:Bucket中链表长度大于该默认值,转化为红黑树
  • MIN_TREEIFY_CAPACITY:Node被树化时最小的Node数组长度,64

image.png

面试八股

负载因子(加载因子,填充比)值的大小,对HashMap有什么影响?

  • 负载因子的大小决定了HashMap的数据密度。
  • 负载因子越大密度越大,发生碰撞的几率越高,数组中的链表越容易长,造成查询或插入时的比较次数增多,性能会下降。
  • 负载因子越小,就越容易触发扩容,数据密度也越小,意味着发生碰撞的几率越小,数组中的链表也就越短,查询和插入时比较的次数也越小,性能会更高。但是会浪费一定的内容空间。而且经常扩容也会影响性能,建议初始化预设大一点的空间。
  • 按照其他语言的参考及研究经验,会考虑将负载因子设置为0.7~0.75,此时平均检索长度接近于常数。

LinkedHashMap

HashMap的子类,能够记录添加元素的先后顺序

HashMap中的内部类:Node

 static class Node<K, V> implements Map.Entry<K, V> {
     final int hash;
     final K key;
     V value;
     Node<K, V> next;
 }

LinkedHashMap中的内部类:Entry

 static class Entry<K, V> extends HashMap.Node<K, V> {
     Entry<K, V> before, after; //能够记录添加元素的先后顺序
     Entry(int hash, K key, V value, Node<K, V> next) {
         super(hash, key, value, next);
     }
 }

TreeMap

  • 添加的key必须是相同类的对象
  • 按照key进行自然排序或定制排序

Collections工具类

Collections 是一个操作 Set、List 和 Map 等集合的工具类

常用方法

排序操作

  • reverse(List):反转 List 中元素的顺序
  • shuffle(List):对 List 集合元素进行随机排序
  • sort(List):根据元素的自然排序对指定List排序
  • sort(List,Comparator):根据指定的 Comparator 产生的定制排序对 List 集合元素进行排序
  • swap(List,int, int):将指定 list 集合中的 i 处元素和 j 处元素进行交换

查找、替换

  • Object max(Collection):根据元素的自然排序,返回给定集合中的最大元素

  • Object max(Collection,Comparator):根据Comparator指定的定制排序,返回给定集合中的最大元素

  • Object min(Collection)

  • Object min(Collection,Comparator)

  • int frequency(Collection,Object):返回指定集合中指定元素的出现次数

  • boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换List 对象的所有旧值

  • void copy(List dest,List src):将src中的内容复制到dest中

    • 需满足dest.size() >= src.size() ;size():集合元素个数

      解决方案:借助Arrays工具类

       List src = new ArrayList();
       src.add("Aa");
       src.add("Aa");
       List dest = Arrays.asList(new Object[src.size()]);//相当于创建src.size()个null的集合
       System.out.println(dest);
       Collections.copy(dest, src);
      

同步控制

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

所以实际开发中根本用不到TreeSet,Vector,Hashtable等,常用的就ArrayList和HashMap,因为他们都可以通过Collections工具类完成各种操作。