Java集合

本文深入探讨了Java集合框架中的ArrayList、LinkedList、HashMap、HashSet、TreeSet和Queue等核心类。分析了它们的实现原理、特点和区别,包括容器的扩容机制、线程安全、数据结构转换、比较器和访问器等关键概念。此外,还讨论了HashMap在JDK1.8中的优化和多线程问题,以及HashSet、LinkedHashSet和TreeSet的异同。
摘要由CSDN通过智能技术生成

1、Java中的容器有哪些?

答:容器主要包括Collection 和 Map 两种,其中Collection 中存储着对象的集合,Map 中存储两个对象的映射表(键值对)。

图后续补充

2、详细介绍一下集合有哪些类,和各自的特点

  • List 
    • ArrayList :是基于动态数组实现的,支持随机访问
    • LinkedLits :基于双向链表实现的,只能顺序访问,但是支持快速的在链表中插入和删除元素,还可以用作栈,队列 和双向队列
    • Vector :和ArrayList 类似,但是他是线程安全的。
  • Set 
    • TreeSet :基于红黑树实现的,支持有序性操作,如根据一个范围查找元素。但是查找选了不如HashSet, 查找的时间复杂度为O(logN)
    • HashSet :基于HashMap 实现的,支持快速的查找,但是不支持有序性操作。查找的时间复杂度为O(1)。失去了元素的插入顺序信息,就是说,用Iterator 去遍历的时候,得到的结果顺序可能是不确定的。
    • LinkedHashSet 是 HashSet 的子类,不能过去内部是通过LinkedHashMap 实现的,内部使用双向链表维护插入顺序,因此是有序的。
  • Queue
    • LinkedList 可以用它来实现双向队列。
    • PriorityQueue 基于堆结构实现,可以用它来实现优先队列。
    • ArrayQueue基于数组实现,可以用它实现双端队列,也可以作为栈。

3、说一下Map 有哪些实现类和他们的各自特点

  • TreeMap : 基于红黑树实现的,是有序的
  • HashMap :  jdk7 基于数组加链表实现,jdk8以后基于数组加链表加红黑树实现。
  • HashTbale : 与 HashMap类似,但是是线程安全的
  • LinkedHashMap : 继承于 HashMap,使用双向链表维护元素的顺序。

4、说一下ArrayList和LinkedList的区别?

ArrayList和LinkedList是List接口两种不同的实现方式,他们最本质的区别就是ArrayList 内部是用动态数组来存储元素,而LinkedList内部使用双线链表来存储元素。

根据存储元素的方式不同,导致他们的相对应的方法具有不同的时间复杂度。

首先对于ArrayList来说,随机查询get(int index)的时间复杂度是O(1),因为是直接从底层数组根据下标获取的,和数组的长度没有关系。这也是ArrayList最大的有点。

插入元素时add(E e)方法回自动将元素插到数组的末尾,如果不需要考虑扩容,时间复杂度是O(1),如果需要扩容,内部执行的Arrays.copyOf()是耗时的关键,因为需要把原有数组中的元素复制到扩容之后的新数组中。

如果指定位置插入元素 add(int index, E element) ,就会涉及到元素的赋值,因此时间复杂度为O(N);

对于删除元素,remove(int index) 方法回将指定位置的元素删除,这个步骤也会涉及到底层元素的复制,因此时间复杂度为O(n);

其次对于LinkedList来说,get(int index)的时间复杂度是O(N),因为每次查询都要遍历链表,由于是双向链表结结构,因此在遍历时,下标小于长度一半从前往后遍历,否则从后往前遍历,这样从理论上来说,时间可以节省一半。getFirst() 和 getLast()两个方法的时间复杂度是O(1)。因为first 和 last 在链表中是直接存储的。

插入元素时add(E e)方法默认将元素添加到链表的末尾,因此时间复杂度是O(1)。

如果指定位置插入元素 add(int index, E element),需要先遍历这个元素,然后在插入,时间复杂度为O(N)

对于删除元素,remove(int index),因为要考虑到查到元素,因此时间复杂度也是O(N)。

以上是通过时间复杂度对ArrayList和LinkedList的比较,需要注意的是,如果列表很大的时候,两个在内存上使用也是不同的,LinkedList 的每个元素都有更多开销,因为要存储上一个和下一个元素的地址,而ArrayList 没有这样的开销。但是ArrayList占用的内存是连续的,不论是否有那么多元素存储,内存都是已经占用了的。

5、说一下Vector 和 ArrayList 的区别?

Vector也是List接口的实现类之一,底层数据结构也是数组,同样具有查找快,增删满的特点。

Vector是线程安全的,源码中大量的方法都使用了synchronized关键字,这导致Vector的效率是比ArrayList要低的。

两者都采用了线性连续空间来存储,当需要扩容时,ArrayList默认扩容为原来的50%,而Vector默认扩容为原来的一倍。

//ArrayList
int newCapacity = oldCapacity + (oldCapacity >> 1);

// Vertor
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);

Vector可以设置capacityIncrement,从字面理解就是capacity容量,Increment增加,容量增长的参数,而ArrayList不可以。

6、ArrayList实现 RandomAccess接口有何作用?

RandomAccess接口其实是空的,什么都没有定义,这个接口其实只是一个标记接口,只要List 实现了这个接口,就能支持快速随机访问。 Collections中的 binarySearch() 方法中,会判断是否实现RandomAccess接口来实行查找方式:实现了此接口mid 是直接通过数据下标获得,而未实现则会采用迭代器来遍历。LinkedList没有实现这个接口,是因为他自身链表的属性不支持快速随机访问。

推荐做法:实现了RandomAccess接口的列表就可以用for循环遍历,否则建议用iterator或者foreach遍历。

    public static <T>
    int binarySearch(List<? extends Comparable<? super T>> list, T key) {
        if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
            return Collections.indexedBinarySearch(list, key);
        else
            return Collections.iteratorBinarySearch(list, key);
    }

    
    // indexedBinarySearch 中直接get值
    Comparable<? super T> midVal = list.get(mid);

    // iteratorBinarySearch中通过迭代器找中间值
    ListIterator<? extends Comparable<? super T>> i = list.listIterator();
    Comparable<? super T> midVal = get(i, mid);

7、ArrayList的扩容机制

先看一下成员属性:默认容量10,两个默认空数组,用于存储元素的elementData,以及一个int类型的size用于记录list所存的容量。

    private static final int DEFAULT_CAPACITY = 10;

    private static final Object[] EMPTY_ELEMENTDATA = {};

    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};

    transient Object[] elementData; // non-private to simplify nested class access

    private int size;

看一下ArraytList 的三个构造器,这里只关注无参构造器,可以看到,调用无参构造器的时候,返回的是一个空数组。因此,第一次添加数据的话,就会触发扩容。

    public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }

    public ArrayList(Collection<? extends E> c) {
        Object[] a = c.toArray();
        if ((size = a.length) != 0) {
            if (c.getClass() == ArrayList.class) {
                elementData = a;
            } else {
                elementData = Arrays.copyOf(a, size, Object[].class);
            }
        } else {
            // replace with empty array.
            elementData = EMPTY_ELEMENTDATA;
        }
    }

再来看看添加方法:

添加元素的时候,首先调用ensureCapacityInternal方法,并传值size+1,这个方法的作用是,首先判断当前数组是否为空,如果为空就将数组长度扩容为10,否则size+1 是否大于当前数组长度,如果大于,就进行扩容,否则就执行添加元素的操作。

    public boolean add(E e) {
        // 添加元素前首先调用ensureCapacityInternal方法,并传值size+1
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

    private void ensureCapacityInternal(int minCapacity) {
        //调用了两个方法
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }

    // 计算容量,把当前数组 ,和size+1 传进去
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        // 如果当前数组为空,就返回max(DEFAULT_CAPACITY, minCapacity),其实就是数组首次扩容,容量为10
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        // 不为空就返回 size+1
        return minCapacity;
    }

    // 
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;

        // overflow-conscious code
        // 如果size+1 大于当前数组长度,就触发grow 方法。
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

最后看一下ArrayList扩容的核心方法grow():新容量采用位运算的方式,计算后值为旧容量的1.5倍。

复制元素的方法调用了本地方法System.arraycopy()

    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        // 首次扩容时,newCapacity计算后仍为0,因此赋值为10
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        // 如果newCapacity 大于 MAX_ARRAY_SIZE ,就调用 hugeCapacity方法
        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);
    }

   
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
            MAX_ARRAY_SIZE;
    }

总结下来,ArrayList的扩容发生时机有两种:一个是初始化后第一次插入元素,第一次扩容长度为10 ,另外一个是当前数组已经满了,在往里插元素的时候触发扩容。 其中调用的各种方法都是用于判断是否需要扩容。

8、comparable和comparator的区别?

Comparable是java.lang包下的一个接口,是一个内部比较器,实现了Comparable接口的类可以和自己比较,内部有一个compareTo(Object o)方法,返回值是int类型的 :

  • 正整数:比较者大于被比较者
  • 零 :两者相等
  • 负整数:比较者小于被比较者

实现了Comparable接口的并重写了compareTo方法类的对象的列表或数组可以通过Collections.sort或Arrays.sort进行自动排序。

Comparator都是java.util包下的两个接口,可以称作是一个外部比较器;如果我们需要对某个类进行排序但是又不好修改这个类,那么就可以 定义一个实现了Comparator接口的类(类B),来作为类A的“比较器”,然后通过该比较器对类进行排序。

总结:实现两个接口都可以用来进行比较和排序,两者各有优缺点:Comparable 简单,但是需要修改源代码,Comparator虽然要另外实现一个比较器,但是不需要修改源码,并且在Comparator里面用户可以实现自己复杂统一的逻辑。

Comparable和Comparator区别(超详细对比分析)_只要酸菜不要鱼的博客-CSDN博客_comparable和comparator

9、介绍一下PriorityQueue?

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

优先队列的使用:

public class TestPriorityQueue {

    public static void main(String[] args) {
        PriorityQueue priorityQueue = new PriorityQueue();

        priorityQueue.offer(new Customer(1, "张三"));
        priorityQueue.offer(new Customer(2, "李四"));
        priorityQueue.offer(new Customer(1, "王五"));
        priorityQueue.offer(new Customer(3, "张无"));
        priorityQueue.offer(new Customer(1, "张四"));

        while (!priorityQueue.isEmpty()) {
            System.out.println(priorityQueue.poll());
        }
    }
}

class Customer implements Comparable<Customer> {
    public int level;
    public String name;
    public Customer(int level, String name) {
        this.level = level;
        this.name = name;
    }
    @Override
    public String toString() {
        return "Customer{" + "level=" + level + ", name='" + name + '\'' + '}';
    }
    @Override
    public int compareTo(Customer o) {
        return o.level - level;
    }
}

Print:
Customer{level=3, name='张无'}
Customer{level=2, name='李四'}
Customer{level=1, name='张三'}
Customer{level=1, name='王五'}
Customer{level=1, name='张四'}

PriorityQueue内部存储数据使用的是数组,初始化容量为11,对于扩容,当长度比较小时,容量乘以2+2,长度较大的时候,每次增加50%,内部通过堆排序实现有序,插入和删除堆顶元素的时间复杂度为O(log n);

【JDK源码】PriorityQueue源码分析_ΘLLΘ的博客-CSDN博客

 10、说一下HashSet的实现原理

底层使用HashMap来存储数据,初始化的时候会创建一个HashMap对象,HashSet的值不允许重复,因此HashSet 的值是作为HashMap的key来存储在HashMap中的。从源码中可以看出,HashSet 的方法都是调用HashMap的方法;

HashSet 有两个特点:无序性和唯一性(允许一个null值);

public class HashSet<E>  
          extends AbstractSet<E>  
          implements Set<E>, Cloneable, java.io.Serializable  
{  
static final long serialVersionUID = -5024744406713321676L;  

// 底层使用HashMap来保存HashSet中所有元素。  
private transient HashMap<E,Object> map;  
  
// 定义一个虚拟的Object对象作为HashMap的value,将此对象定义为static final。  
private static final Object PRESENT = new Object();  

//  默认的无参构造器,实际底层会初始化一个空的HashMap,并使用默认初始容量为16和加载因子0.75。 
public HashSet() {  
map = new HashMap<E,Object>();  
}  
// 返回此set中的元素的数量(set的容量)。 
public int size() {  
return map.size();  
}  
//  如果此set不包含任何元素,则返回true。 
public boolean isEmpty() {  
return map.isEmpty();  
}  
//  如果此set包含指定元素,则返回true。 
public boolean contains(Object o) {  
return map.containsKey(o);  
}  
public boolean add(E e) {  
       return map.put(e, PRESENT)==null;  
}  
public boolean remove(Object o) {  
return map.remove(o)==PRESENT;  
}  
//  从此set中移除所有元素。此调用返回后,该set将为空。 
//  
//  底层实际调用HashMap的clear方法清空Entry中所有元素。 
 
public void clear() {  
map.clear();  
}  
 

11、HashMap的实现原理和底层数据结构是什么?

(hashmap相关的知识参考如下链接:面试阿里,HashMap 这一篇就够了_程序员囧辉的博客-CSDN博客

jdk1.7 是数组加链表

jdk1.8 以后是数组加链表加红黑树

改成数组加链表加红黑树 主要是为了提升 在hash冲突严重时的查找性能,因为使用链表查询的性能是 O(n),而使用红黑树是 O(logn)。

为什么是红黑树而不是其他树,比如二叉查找树等?因为二叉查找树在特殊情况也会变成线性结构,存在深度遍历的问题,红黑树是平衡二叉树中的一种,插入新数据后会通过左旋 和 右旋 变色等保持黑节点平衡,解决查询深度问题。

对于插入情况:

        默认情况下使用的是链表,当同一个数组索引位置的节点在新增后超过8个,就会触发链表转换为红黑树。(当前,转换的前提是此时数组的长度大于等于64,如果数组长度小于64,因为此时数据量相对较少,会首先选择扩容)

对于移除情况:

        当同一个索引位置的节点在移除之后小于等于6个,并且该节点时红黑树节点时,会触发红黑树转化为链表节点。

链表转化成红黑树的阈值设置为8 是为什么呢?这个主要是和hashcode碰撞次数的泊松分布有关,主要是为了寻找一种时间和空间的平衡,负载因子为默认值0.75的情况下,单个槽内元素个数为8的概率小于百万分之一。

为什么不直接使用红黑树呢?因为红黑树的节点大小占用的内存大概是链表节点的两倍,节点比较少的时候,而红黑树的查找优势并不明显。

为什么红黑树转回链表的节点设置成6而不是8呢,因为如果节点在8个左右徘徊的时候,就会频繁的进行红黑树和链表的转换,这个是十分耗费性能的。

12、HashMap中有哪些重要的属性,他们都是用作什么?

存储元素的数组table,容量size,负载因子loadFactor,扩容阈值threshold

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    static final int TREEIFY_THRESHOLD = 8;
    static final int UNTREEIFY_THRESHOLD = 6;
    static final int MIN_TREEIFY_CAPACITY = 64;

    transient Node<K,V>[] table;    //存储元素的 table
    transient int size;  // 容量
    int threshold; // 扩容阈值 = 容量*负载因子
    final float loadFactor; //负载因子

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

13、HashMap 的默认初始容量是多少?HashMap 的容量有什么限制吗?

当我们新建一个HashMap对象时,是没有初始化table容量的,只有当插入第一个节点的时候,才会对table进行初始化,避免不必要的空间浪费,table初始化的长度默认是16,当然,也可以在新建HashMap的时候传入一个默认的初始容量,根据实际使用情况设置初始容量其实才是最合理的方案。

而负载因子的默认初始值是0.75,这个也是在时间和空间上权衡的结果,如果负载因子过高,就可增加hash冲突的概率,如果值比较低,虽然hash冲突会降低,但是浪费的空间也增大了。

HashMap的容量需要是2 的N次方,HashMap会根据我们传入的容量计算出一个大于等于该容量的最小的2 的N次方,例如传 7,容量为8,传 9,容量为16

我们看如下的计算代码:

首先解释一下:

  • >>>(无符号右移):例如 a >>> b 指的是将 a 向右移动 b 指定的位数,右移后左边空出的位用零来填充,移出右边的位被丢弃: 101 >>>1 = 010   010>>>1 = 001
  • a |= b ,可以转成:a = a | b;-----> 101 | 010 = 111

通过五次移位和或操作,我们能够通过n的最高位的1,拿到2个1 、4个1……,最终可以得到一个低位全是1的值,这个值的最高位1 取决于n的值的大小。然后返回的时候在加1,得到的就是一个会比n大的 2 的N次方。

int n = cap - 1 这个是什么意思呢?其实这个是考虑了传入的cap值本身就是2 的N次方的情况,如果cap 本事就是 2 的N次方,一通计算之后,返回的值还是他本身。

    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

14、HashMap的容量为什么必须是2 的N次方?

HashMap中计算元素所以的公式为 : int index = (n - 1) & hash;  当n 为 2 的N次方的时候,n-1的低位就全是1,那么此时任何值 和n-1进行&运算,得到的结果就是该值的低N位,这样就达到了取模的效果。

比如hash值为15,n为4,即2的2次方 。15&(4-1)=(1111&0011)=0011=3 ,而15%4=3;其实可以理解发现,对于二进制数,能够整除table的,都在高位(位数大于等于3的都算高位),剩下的低位值和4-1进行与运算,结果就是hash与table取模得到的值。

这么设计的原因就是因为位运算的效率要比取模更高。

15、HashMap的插入流程?

首先计算插入key的hash值,判断数组是否为空,如果为空,进行数组初始化,然后计算索引的位置:上一题说了,索引计算为(n-1)&hash ,判断索引的位置是否已经存在节点,不存在就直接放入,如果存在,就比较头节点的key 和插入的key是否一样,不一样就往后插入,涉及到链表或者红黑树,如果一样,就替换key的value。插入时,可能会涉及到数组table 的扩容、链表向红黑树的转化,以及红黑树的平衡调整。

流程图待补充

16、上述所说的计算key 的hash值,这个值是如何计算的?

计算代码如下所示,key.hashCode()是本地native方法,计算得到的值和自己的高16位进行或运算,得到最终的hash值。这么做的目的是为了在table长度比较小的时候,让高位也参与运算,并且不会有太大的开销。

比如:table=8,(8-1)=0111 ,在计算(n-1)&hash的时候,始终只有后三位参与&运算,这样就是说其实hash值也只有后三位参与了运算,hash值的高位无论这么变,都是无效的,而(h = key.hashCode()) ^ (h >>> 16)就会在后续计算索引的时候将高位也参与运算。

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

17、简述HashMap的扩容流程

只描述最简单的扩容,完整流程图后续贴。

触发扩容的时候,创建一个新的数组,容量和扩容阈值都变为原来的两倍,table指向新的数组。然后遍历老的数组,在遍历到的索引位置,看是否有节点,是链表还是红黑树,遍历此索引位置的链表或者红黑树,插入到新数组的索引位置。

18、HashMap扩容时,如何定位新表的索引位置?

新表的索引位置是通过 e.hash & oldCap == 0 来进行判断的:

1、等于0的时候,将节点加到loHead链表的尾部,loHead链表放到“原索引位置”;

2、不等于0的时候,将节点加到hiHead链表的尾部,hiHead链表放到“原索引位置+oldCap”位置。

如:扩容前的table容量为16,扩容后为32,新表的n-1相比于旧表变化在高位多个一个1,因为计算索引 是用 (n-1)&hash 计算的,两个节点在旧表中在同一个索引位置,计算这两个节点在新表中的索引时,其实就只取决于高位多出来的这一位(标红位置),而这一位的值刚好是oldCap,因此,只存在e.hash & oldCap == 0 (原索引位置) 和 e.hash & oldCap == 1 (原索引位置+oldCap)两种情况。

其实,上述几题中有关HashMap索引位置的计算其实都是取模运算,只不过为了优化HashMap 的性能,采用了位运算的方式,而 (n-1)&hash 、e.hash & oldCap 这些计算都是优化后的位运算写法。

 

 

19、HashMap扩容时的死循环问题

导致多线程下HashMap扩容时死循环的根本原因就是 jdk1.7 的头插法扩容,而jdk1.8之后的尾插法就不会存在死循环的问题。

jdk1.7的扩容代码如下:(自己对着代码手动画一下)

void transfer(Entry[] newTable) {
    Entry[] src = table;
    int newCapacity = newTable.length;
    for (int j = 0; j < src.length; j++) {
        Entry<K,V> e = src[j];
        if (e != null) {
            src[j] = null;
            do {
                Entry<K,V> next = e.next;
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } while (e != null);
        }
    }
}

20、在jdk1.8 对HashMap做了哪些优化?在jdk1.8 会存在多线程并发问题吗?

  • 底层数据结构变成了“数组+链表+红黑树”,主要是在哈希冲突严重的时候,优化了查找性能O(n) -> O(logn)
  • 计算table初始容量的时候,通过“五个移位+或等于运算”
  • hash值的计算方式,简单的让高16位参与了运算
  • 扩容采用尾插法代替头插法,避免死循环

jdk1.8虽然改用头插法解决了死循环的问题,但是仍然存在线程安全问题,比如在put操作过程中需要扩容,扩容时,线程1拿到了头结点和hash桶,此时CPU时间片让出,线程2获得时间片对这个hash桶进行更改,线程1重新获得时间片后,手上的是一个过期的桶和头节点,在线程1 对桶进行修改的时候,会覆盖之前线程的更改,造成数据丢失。(或者说,在判断当前桶链表个数的时候,有一个++size的操作,这个操作本身就不是线程安全的。)

21、说说LinkedHashMap :

LinkedHashMap继承自HashMap,因此LinkedHashMap的多种操作都是建立在HashMap的基础上,不同的是,LinkedHashMap自身维护了一个Entry的双向链表,保证了Entry 插入的顺序。

额外定义了一个 head 和 tail ,以及一个accessOrder(访问顺序为true,插入顺序为false)

如果不定义accessOrder这个属性,则默认赋值为false,是按照插入顺序来的,如果定义accessOrder属性为true,则按照访问顺序。(后面详细补充)

22、说一下HashMap 和 Hashtable 的区别?

  • 线程安全性:Hashtable是线程安全的,而HashMap不是
  • 效率问题:因为Hashtable使用synchronize 锁保证多线程安全,因此效率是不如HashMap
  • 对null key 和null value 的支持:HashMap可以存储 null 值的 key 和value ,而Hashtable不允许null key 和 value,否则抛出NullPointerException
  • 初始化容量大小和扩容容量不同:HashMap初始化为16,总是使用2的N次方,Hashtable初始化默认大小为11,每次扩容变成原来的2n+1。
  • 底层数据结构:JDK1.8以后HashMap使用数组加链表加红黑树,而 Hashtable没有红黑树的机制。

23、说一下HashMap 和 TreeMap 区别?

HashMap 和 TreeMap都继承自AbstractMap,但是TreeMap 还实现了NavigableMap(这个实现了SortedMap),这使得TreeMap具有了对集合内元素的搜索能力和根据key进行排序的能力,默认是按照key升序排序,不过也可以初始化的时候指定排序比较器。

24、为什么HashMap中String、Integer这样的包装类适合作为Key?

首先,这些包装类都是由final关键字修饰的,保证了key的不可变性,避免出现key的hash值发生改变的现象。其次,他们内部都已经重写过hashcode() equal()方法;

如果要使用Object对象来作为key,需要重写

  • hashcode() --> 计算hash值来确定存储位置
  •  equal()-->保证key的唯一性
  • 因此非要使用对象来当做key也是可以的,但是要注意重写上述两个方法。

25、Queue 与 Deque 有什么区别?

Queue 为单端队列,一端插入元素,一端删除元素,遵循先入先出规则(FIFO),Queue拓展了Collection的接口,根据容量问题而导致操作失败后处理不同分成两类方法,一种是失败后抛出异常,一种是返回特殊值。

Queue 接口抛出异常返回特殊值
插入队尾add(E e)offer(E e)
删除队首remove()poll()
查询队首元素element()peek()

Deque 为双端队列,在队列的首尾两端都可以插入或者删除元素,Deque拓展了Queue的接口,实现了在两端都可以进行插入和删除的方法。

Deque抛出异常返回特殊值
插入队首  addFirst(E e) offerFirst(E e)
插入队尾addLast(E e)  offerLast(E e)
删除队首removeFirst()   pollFirst()
删除队尾removeLast() pollLast()
查询队首元素getFirst() peekFirst()
查询队尾元素getLast()  peekLast()

26.说一下 HashSet、LinkedHashSet 和 TreeSet 三者的异同?


HashSet、LinkedHashSet 和 TreeSet 都是 Set 接口的实现类,都能保证元素唯一,并且都不是线程安全的。他们的不同点:

HashSet、LinkedHashSet 和 TreeSet 的主要区别在于底层数据结构不同:

  • HashSet 的底层数据结构是哈希表(基于 HashMap 实现)
  • LinkedHashSet 的底层数据结构是链表和哈希表,元素的插入和取出顺序满足 FIFO
  • TreeSet 底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序

底层数据结构不同又导致这三者的应用场景不同:

  • HashSet 用于不需要保证元素插入和取出顺序的场景
  • LinkedHashSet 用于保证元素的插入和取出顺序满足 FIFO 的场景
  • TreeSet 用于支持对元素自定义排序规则的场景
     

27、数组和list之间的转换方式:

  • 数组转list:使用Arrays.asList(array)
  • list转数组:使用List自带的toArray()方法
// list to array
List<String> list = new ArrayList<String>();
list.add("123");
list.add("456");
list.toArray();
 
// array to list
String[] array = new String[]{"123","456"};
Arrays.asList(array);

28、fail-fast(快速失败)机制是什么?

是Java集合中的一种错误检测机制,迭代集合的过程中如果该集合在结构上发生改变时,就有可能发生fail-fast,抛出ConcrurentModificationException异常。

java中的fail-fast(快速失败)机制_Howard_14的博客-CSDN博客_fail-fast机制

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值