Java集合八股(高质量,无废话)----持续更新

Java中的集合类有哪些?如何分类的?

  • Java中的集合类主要由两大接口派生而来:一个是 Collection接口,主要用于存放单一元素;另一个是 Map 接口,主要用于存放键值对。
  • Collection接口下又有三个主要的子接口:ListSetQueue
  • List接口下的实现类:ArrayListLinkedListVector
  • Set接口下的实现类:HashSetTreeSet
  • Queue接口下的实现类:PriorityQueueArrayDequeLinkedList
  • Map结构下的实现类:HashMapTreeMapHashtable
    在这里插入图片描述

为什么索引数组从0开始?从1开始可以吗?

  • 根据数组索引获取元素的时候,会用索引和寻址公式来计算所对应的数据,寻址公式为数组的首地址+索引*存储数据的类型大小
  • 索引如果从1开始的话,寻址公式中,就增加了一次减法操作,对于CPU来说增加了一条指令,性能不高

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

  • 数组转ArrayList:使用JDK中Java.util.Arrays工具的asList方法
  • ArrayList转数组:使用List的toArray方法。无参toArray返回Object数组,传入初始化长度的数组对象,返回该对象数组

Arrays.asList转换list之后,如果修改了数组的内容,list会受影响,因为它的底层使用的Arrays
类中的一个内部类ArrayList来构造的集合,在这个集合的构造器中,把我们传入的这个集合进行了
包装而已,最终指向的都是同一个内存地址
list用了toArray转数组后,如果修改了list内容,数组不会影响,当调用了toArray以后,在底层
是它是进行了数组的拷贝,跟原来的元素就没啥关系了
,所以即使list修改了以后,数组也不受影

ArrayList和LinkedList的区别是什么?

  • 底层数据结构:ArrayList是动态数组数据结构实现,LinkedList是双向链表的数据结构实现的
  • 操作效率
    • 查找:ArrayList按照下标查询的时间复杂度O(1),LinkedList不支持下标查询;如果查找未知索引, ArrayList需要遍历,链表也需要遍历链表,时间复杂度都是O(n)
    • 插入和删除:ArrayList尾部插入和删除,时间复杂度是O(1);其他部分增删需要挪动数组,时间复杂度是O(n);LinkedList头尾节点增删时间复杂度是O(1),其他都需要遍历链表,时间复杂度是O(n)。(这里链表插入和删除为O(n)的原因是需要先遍历链表找到具体位置,然后再执行操作,若单纯说在指定位置插入删除则为O(1))
  • 线程安全:ArrayList和LinkedList都不是线程安全的,
  • 占用内存大小:ArrayList底层是数组,内存连续,节省内存,LinkedList 是双向链表需要存储
    据,和两个指针,更占用内存

如何构建线程安全的List?

  • Vector:它和ArrayList在常用方法的实现上很相似,不同的只是它采用了同步关键词synchronized修饰方法
public void add(int index, E element) {
    insertElementAt(element, index);
}
...
// 使用了synchronized关键词修饰
public synchronized void insertElementAt(E obj, int index) {
        modCount++;
        if (index > elementCount) {
            throw new ArrayIndexOutOfBoundsException(index
                                                     + " > " + elementCount);
        }
        ensureCapacityHelper(elementCount + 1);
        System.arraycopy(elementData, index, elementData, index + 1, elementCount - index);
        elementData[index] = obj;
        elementCount++;
    }

  • Collections.synchronizedList:同样是使用了同步代码块,无论是读操作还是写操作,它都会进行加锁,当线程的并发级别非常高时就会浪费掉大量的资源,因此有了下面的第三种线程安全的List的实现。
final Object mutex;
public void add(int index, E element) {
    synchronized (mutex) {list.add(index, element);}
}

public E get(int index) {
    synchronized (mutex) {return list.get(index);}
}
  • CopyOnWriteArrayList:写操作的时候复制数组,在该类的使用过程中,读读操作和读写操作都不互斥。读操作和写操作位于不同的数组上,因此它们不会发生安全问题。
    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            // 复制数组
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            // 赋值
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

HashMap的实现原理

  • 数据结构:数组链表或者红黑树
  • put元素时
    • 根据key的hashcode计算对象元素在数组中的下标
    • key相同,覆盖原始值;key不相同,则将当前的key-value放入链表或者红黑树中
  • get元素时:根据Hash值找到对应的下标,再进一步判断key是否相同,从而找到对应值

HashMap的get方法的具体流程?

  • 计算key的Hash值,并通过Hash值计算出在数组中的索引位置
  • 如果该位置为null,说明没有找到对应的键值对,返回null
  • 如果该位置不为null,遍历该位置上的元素,如果找到了与当前key相等的键值对,则返回,否则返回null
    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            // 定位到桶的位置元素就是想要获取的key对应的,则直接返回
            if (first.hash == hash && // always check first node
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            // 定位到的位置不是想要的
            if ((e = first.next) != null) {
                if (first instanceof TreeNode)
                	// 处理树化的查找
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                do {
                	// 链表的查找
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

HashMap的put方法的具体流程?

  • 判断断键值对数组table是否为空或者为null,否则执行resize()进行扩容(初始化)
  • 根据键值key计算hash值得到数组索引
  • 判断table[i]==null,条件成立,直接新建节点添加
  • 如果table[i]==null ,不成立
    • 如果key存在,则直接覆盖
    • 不存在的话, 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对
    • 遍历table[i],链表的尾部插入数据,然后判断链表是否需要转换成红黑树,如果转换成红黑树,在红黑树中执行插入操作
  • 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold(数组长度*0.75),
    如果超过,进行扩容

HashMap的扩容机制是什么?

  • 添加元素或初始化的时候需要调用resize方法进行扩容,第一次添加数据初始化数组长度为16,以后每次每次扩容都是达到了扩容阈值(数组长度 *0.75)
  • 每次扩容的时候,都是扩容之前容量的2倍
  • 扩容之后,会新创建一个数组,需要把老数组中的数据挪动到新的数组中
    • 没有hash冲突的节点,则直接使用 e.hash & (newCap - 1) 计算新数组的索引位置
    • 如果是红黑树,走红黑树的添加
    • 如果是链表,则需要遍历链表,可能需要拆分链表,判断(e.hash &oldCap)是否为0,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上(拆分链表)

为什么HashMap的数组长度一定是2的次幂

  • 计算索引时效率更高:如果是 2 的 n 次幂可以使用位与运算代替取模(a % b = a & (b - 1))
  • 扩容时重新计算索引效率更高: hash & oldCap == 0 的元素留在原来位置 ,否则新位置 = 旧位置

hashMap的寻址算法

  • 首先获取key的hashCode值,然后右移16位 异或运算原来的hashCode值,主要作用就是使原来的hash值更加均匀,减少hash冲突
  • 通过与运算实现hash值对数组大小取模得到下标

HashMap、Hashtable和ConcurrentHashMap的区别?

  • 线程安全
    • HashMap是非线程安全的
    • Hashtable中的方法是同步的,所以他是线程安全的
    • ConcurrentHashMap在JDK 1.8之前使用分段锁保证线程安全,ConcurrentHashMap默认情况下将hash表分为16个桶(分片),在加锁的时候,针对每个单独的分片进行加锁,其他分片不受影响。锁的粒度更细,所以他的性能更好。ConcurrentHashMap在JDK 1.8中,采用了一种新的方式来实现线程安全,即使用了CAS+synchronized,这个实现被称为“分段锁”的变种,也被称为“锁分离",它将锁定粒度更细,把锁的粒度从整个Map降低到了单个桶。
  • 继承关系
    • HashMap继承的抽象类AbstractMap实现了Map接口
    • ConcurrentHashMap同样继承了抽象类AbstractMap,并且实现了ConcurrentMap接口
    • Hashtable是基于陈旧的Dictionary类继承来的
  • 扩容机制
    • HashMap和ConcurrentHashMap的默认初始容量为16,默认的加载因子为0.75,即当HashMap中元素个数超过容量的75%时,会进行扩容操作。扩容时,容量会扩大为原来的两倍,并将原来的元素重新分配到新的桶中。
    • Hashtable,默认初始容量为11,默认的加载因子为0.75,即当Hashtable中元素个数超过容量的75%时,会进行扩容操作。扩容时,容量会扩大为原来的两倍加1,并将原来的元素重新分配到新的桶中。
  • 允不允许null值
    • HashMap中,null可以作为键或者值都可以。
    • ConcurrentHashMap和Hashtable中,key和value都不允许为nul

HashMap在多线程下会出现什么问题?

  • 元素丢失:当多个线程同时执行 put 操作时,如果它们计算出的索引位置相同,就会造成前一个 key被后一个 key 覆盖的情况,从而导致元素的丢失
  • get为null:当一个线程执行 put 操作导致扩容时,而另一个线程同时执行 get 操作,有可能会导致 get 返回 null 的情况。是因为在扩容过程中,HashMap 的结构发生了变化,get 操作可能会在旧的结构中查找元素而导致找不到
  • 扩容死循环:在 JDK1.7 中,HashMap 使用头插法插入元素,当多个线程同时进行扩容操作时,可能会导致环形链表的形成从而陷入死循环。为了解决这个问题,在」DK1.8 中采用了尾插法插入元素,保持了链表元素的顺序,避免了死循环的问题

Set如何保证元素不可重复的?

  • 在HashSet中,基本的操作都是有HashMap底层实现的,因为HashSet底层是用HashMap存储数据的。当向HashSet中添加元素的时候,首先计算元素的hashCode值然后通过扰动计算和按位与的方式计算出这个元素的存储位置,如果这个位置为空,就将元素添加进去;如果不为空,则用equals方法比较元素是否相等,相等就不添加,否则找个空位添加。
  • TreeSet的底层是TreeMap,元素在插入TreeSet时compareTo()方法要被调用,所以TreeSet中的元素要实现Comparable接口。TreeSet作为一种Set,它不允许出现重复元素。TreeSet是用compareTo()来判断重复元素的。

Set.contains()的时间复杂度?

  • HashSet底层是HashMap实现的,HashMap的containskey()方法的底层实现原理是依据hash()和equals()方法,首先根据key计算出对应的哈希值(O(1))并与hash表中的值比较,不同就继续比较,相同则再比较元素是否相同,二者都相同则得到的是true,否则说明Map中没有这个key,所以,HashSet的contains()方法的时间复杂度为O(1)~O(n),主要是看相同的hash值的元素有多少个
  • TreeSet 基于红黑树实现,因此 contains 方法的时间复杂度是 O(log n)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值