Java集合容器总结

集合与数组的区别

  • 数组长度固定
  • 集合有自动扩容机制,长度可变
  • 数组可以存储基本数据类型和引用数据类型,前者存值,后者存地址值
  • 集合只能存储引用数据类型
  • 使用场景:元素个数固定用数组,不固定用集合

常用的集合类

Map接口和Collection接口是所有集合类接口的父接口:

Collection:

List:有序(元素存入集合的顺序与取出集合的顺序一致)、可重复、可通过下标访问

  • ArrayList:底层是数组,查找快、增删慢
  • LinkedList:底层是链表,增删快、查找慢
  • Vector:底层是数组,线程安全

Set:无序、不可重复、不可通过下标访问

  • HashSet:底层是单列HashMap
  • TreeSet:由单列TreeMap实现,内部是红黑树结构,可对存入元素进行排序
  • LinkedHashSet:底层通过LinkedHashMap实现

Map:
Map是一个键值对集合,存储键值对之间的映射。key无序,唯一。值不要求有序,可以重复

  • HashMap:底层由数组和链表组成,数组是主体,链表是为了解决Hash冲突,jdk1.8后,当链表的长度大于阈值8时,会转换为红黑树
  • HashTable:底层由数组和链表组成,线程安全
  • LikedHashMap:继承自HashMap,在HashMap的基础上维护了一个双向链表来记录元素添加的顺序,使得遍历的结果遵从插入的顺序,所以LinkedHashMap是有序的
  • TreeMap:底层是红黑树,每个键值对是红黑树的一个节点,按键进行排序

集合类结构图:
Java集合容器总结_第1张图片

集合的容量与扩容

初始容量 扩容数 加载因子
ArrayList 10 0.5 1
Vector 10 1 1
HashMap 16 1 0.75
HashTable 16 1 0.75

实现原理

LinkedHashMap

继承自HashMap,在HashMap的基础上维护了一个双向链表来记录元素添加的顺序,使得遍历的结果遵从插入的顺序,所以LinkedHashMap是有序的。
由于是双向链表所以要维护prev和next指针,所以这里创建一个Node的子类LinkedNode,而且LinkedHashMap类中还要维护first和last

public class LinkedHashMap extends HashMap {

    private LinkedNode first;
    private LinkedNode last;
    
    private static class LinkedNode extends Node {
        LinkedNode prev;
        LinkedNode next;

        public LinkedNode(K key, V value, Node parent) {
            super(key, value, parent);
        }
    }
} 

LinkedHashMap是继承HashMap的,添加元素使用HashMap的添加逻辑即可,然后在节点创建时维护节点的next和prev即可。在HashMap中定义一个createNode()方法,LinkedHashMap中实现如下:

/**
 * 添加元素时维护链表中元素的next和prev属性
 */
@Override
protected Node createNode(K key, V value, Node parent) {
    LinkedNode node = new LinkedNode(key, value, parent);
    if (first == null)
        first = last = node;
    else {
        last.next = node;
        node.prev = last;
        last = node;
    }
    return node;
}

HashSet

HashSet由单列的HashMap实现,HashMap在使用自定义的对象的作为key时,必须重写对象的hashCode和equals方法

TreeMap、TreeSet

  • TreeMap实现了SortedMap接口,它是有序的。它的内部是一个红黑树结构,每个key-value是红黑树的一个节点。
  • 如果在调用TreeMap的构造函数时没有指定比较器,则根据key执行自然排序。如果指定了比较器则按照比较器来进行排序。
  • TreeMap使用自定义对象作为key时,类必须实现Comparable<>接口,并重写compareTo()方法(也可在创建集合对象时传入Comparator比较器,并重写compare()方法),使其按照指定规则排序。

HashMap

几种线程安全的Map解析

HashTable:
HashTable的get/put方法都被synchronized关键字修饰,说明它们是方法级别阻塞的,它们占用共享资源锁,所以导致同时只能一个线程操作get或者put,而且get/put操作不能同时执行,所以这种同步的集合效率非常低,一般不建议使用这个集合

SynchronizedMap:
private Map map = Collections.synchronizedMap(new HashMap());
这种是直接使用工具类里面的方法创建SynchronizedMap,把传入进行的HashMap对象进行了包装同步而已,实现方式是加了个对象锁,每次对HashMap的操作都要先获取这个对象锁才能进入,所以性能也不会比HashTable好到哪里去

ConcurrentHashMap:
最推荐使用的线程安全的Map,实现方式最复杂, jdk8之前是使用分段加锁的方式,分成16个桶,每次只加锁其中一个桶,而在jdk8又加入了红黑树和CAS算法来实现。

一些面试题

如何边遍历边移除 Collection 中的元素?

边遍历边修改 Collection 的唯一正确方式是使用 Iterator.remove() 方法,如下:

Iterator it = list.iterator();
while(it.hasNext()){
   *// do something*
   it.remove();
}

一种最常见的错误代码如下:

for(Integer i : list){
   list.remove(i)
}

Iterator 和 ListIterator 有什么区别?

  • Iterator 可以遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。
  • Iterator 只能单向遍历,而 ListIterator 可以双向遍历(向前/后遍历)。
  • ListIterator 实现 Iterator 接口,然后添加了一些额外的功能,比如添加一个元素、替换一个元素、获取前面或后面元素的索引位置。

遍历一个 List 有哪些不同的方式?每种方法的实现原理是什么?Java 中 List 遍历的最佳实践是什么?

遍历方式有以下几种:

  1. for 循环遍历,基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止。
  2. 迭代器遍历,Iterator。Iterator 是面向对象的一个设计模式,目的是屏蔽不同数据集合的特点,统一遍历集合的接口。Java 在 Collections 中支持了 Iterator 模式。
  3. foreach 循环遍历。foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明 Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换。

最佳实践:Java Collections 框架中提供了一个 RandomAccess 接口,用来标记 List 实现是否支持 Random Access。

  • 如果一个数据集合实现了该接口,就意味着它支持 Random Access,按位置读取元素的平均时间复杂度为 O(1),如ArrayList。
  • 如果没有实现该接口,表示不支持 Random Access,如LinkedList。

推荐的做法就是,支持 Random Access 的列表可用 for 循环遍历,否则建议用 Iterator 或 foreach 遍历。

说一下 ArrayList 的优缺点

ArrayList的优点如下:

  • ArrayList 底层以数组实现,支持以下标访问,查找效率高。
  • ArrayList 在做顺序添加时,直接在数组尾部添加元素,效率也高。

ArrayList 比较适合顺序添加、随机访问的场景。

ArrayList的插入分三种情况,从首位插入,中间位置插入,尾部插入。线性表的插入删除操作都是通过移动来实现的,由于数组长度固定不变,插入数据时,需要一个新的数组。

  1. 首位插入时,先将新的数据放入到新的数组内,然后将原始数组中的数据复制到新的数组。
  2. 中间位置插入,先将插入位置前面的数据先放到新的数组里,再放新的数据,再复制旧的数据完成添加
  3. 尾部插入,由于不会影响其他元素,因此会直接插入到后面。

如何实现数组和 List 之间的转换?

  • 数组转 List:使用 Arrays. asList(array) 进行转换。
  • List 转数组:使用 List 自带的 toArray() 方法。

ArrayList 和 LinkedList 的区别是什么?

  • ArrayList底层由数组实现,查找快,增删慢
  • LinkedList底层由链表实现,增删快,查找慢

查找较多,或只需顺序遍历时,推荐使用ArrayList。在插入和删除操作较多时,推荐使用 LinkedList。

ArrayList 和 Vector 的区别是什么?

  • ArrayList线程不安全,扩容时每次只扩0.5倍
  • Vector线程安全,扩容时每次扩1倍

你可能感兴趣的:(java)