Java 集合

你。 提交于 2020-03-17 01:16:34

 

1、说说List,Set,Map三者的区别?

  • List:有序、元素可重复
  • Set:元素不能重复
  • Map: 一个元素即一个键值对,key唯一标识一个键值对,key不能重复,元素可以重复,key、value均可以是任意类型。

 

 

 

2、Arraylist 与 LinkedList 区别?

1. 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是都不保证线程安全;

2. 底层数据结构: Arraylist 底层使用数组;LinkedList 底层使用双向链表(JDK1.6之前为循环链表,JDK1.7取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!) 

3. 插入和删除是否受元素位置的影响: ① ArrayList 采用数组存储,插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e)方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element))时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 ② LinkedList 采用链表存储,所以对于add(E e)方法的插入、删除时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置i插入和删除元素的话((add(int index, E element)) 时间复杂度近似为o(n))因为需要先移动到指定位置再插入。

 4. 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)方法)。

5. 内存空间占用: ArrayList的空 间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。

 

 

 

 

3、RandomAccess接口

public interface RandomAccess {
}

 RandomAccess 接口中什么都没有定义。 RandomAccess 接口只是标识实现这个接口的类具有元素随机访问功能。

ArrayList 实现了 RandomAccess 接口, 而 LinkedList 没有实现。ArrayList 底层是Object数组,而 LinkedList 底层是链表。数组本身就支持随机访问,时间复杂度为 O(1),链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n)。,ArrayList 实现了 RandomAccess 接口,就表明了他具有快速随机访问功能。 RandomAccess 接口只是标识,并不是说 ArrayList 实现 RandomAccess 接口才具有快速随机访问功能的!好比我去买火车票,学生证标识我是学生,但不管有没有学生证,我本身都是一个学生。

 

list 的遍历方式选择:

  • 实现了 RandomAccess 接口的list(ArrayList),优先选择普通 for 循环 ,其次 foreach。因为普通for循环有一个计数器可以作为数组下标(随机访问)
  • 未实现 RandomAccess接口的list(LinkedList),优先选择iterator遍历(foreach底层也是通过iterator实现的)

 

 

 

 

4、双向链表、双向循环链表

双向链表: 包含两个指针,一个prev指向前一个节点,一个next指向后一个节点。

在第一个元素,要操作最后一个元素,需要沿着next正向遍历整个链表

在最后一个元素,要操作第一个元素,需要沿着prev逆向遍历整个链表

 

 双向循环链表: 最后一个节点的 next 指向head,而 head 的prev指向最后一个节点,构成一个环。

 

 

 

 

4、ArrayList 与 Vector 区别?

Vector类的所有方法都是同步的,线程安全,可以保证多个线程安全地访问一个Vector对象,但一个线程访问Vector对象时也会在同步操作上花费大量时间。

Arraylist不是同步的,所以在不需要保证线程安全时建议使用Arraylist,效率更高。

 

 

 

5、说一说 ArrayList 的扩容机制

ArrayList部分源码如下:

private static final int DEFAULT_CAPACITY = 10;  //默认的容量
private static final Object[] EMPTY_ELEMENTDATA = {};  //空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};  //默认的空数组
transient Object[] elementData; //存储元素的数组
private int size;  //ArrayList的元素个数
/*空参构造器,指向空数组 */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);    }}
/*传入集合,指向Arrays.copyOf()返回的新数组 */public ArrayList(Collection<? extends E> c) {    elementData = c.toArray();    if ((size = elementData.length) != 0) {        // c.toArray might (incorrectly) not return Object[] (see 6260652)        if (elementData.getClass() != Object[].class)            elementData = Arrays.copyOf(elementData, size, Object[].class);    } else {        // replace with empty array.        this.elementData = EMPTY_ELEMENTDATA;    }}

 

看一下add()源码:

  public boolean add(E e) {
        ensureCapacityInternal(size + 1);  //确保数组容量够,容量+1
        elementData[size++] = e; //往数组中添加元素
        return true;
    }

 

看一下调用的 ensureCapacityInternal()的源码:

private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }


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

        // overflow-conscious code
        if (minCapacity - elementData.length > 0) //如果传入的所需的最小容量大于数组长度,说明数组装不下了,扩容
            grow(minCapacity);
    }

数组的length属性是数组长度(容量),并不是数组的实际元素个数。

int[ ] arr={1,2,3};数组容量是3,length=3

int[ ] arr=new int[3];  数组容量是3,length=3,未赋初值默认是此种类型的默认值。

 数组一旦创建,其容量就不能修改。arr[index]赋值、取值只能在容量区间上,否则报错:数组下标越界。

 

看一下grow()源码:

  private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1); //扩容后的容量
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        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); //复制到新数组,指向新数组
    }

>>1,右移1位,即/2,如果是奇数结果有小数,丢掉小数部分

比如原容量是10 =>5,扩容后是10+5=15;原来是15=>7,扩容后是15+7=22。扩容为1.5倍左右。

那2个if是看扩容后的容量与传入的所需的最小容量的大小,即扩容后够不够。

 

不是说传入所需的最小容量,就给你扩到所需的最小容量,不是的,因为这样每次都要扩容、都要复制元素到新数组,IO开销大。

添加元素时数组容量不够了才扩,每次扩都是扩到原来的1.5倍左右。

 

 

获取数组长度用length属性,获取字符串长度用length()方法,获取集合长度|元素个数用size()方法。

 

源码中很多地方都用到了Arrays.copyOf()、System.arraycopy()2个方法,这2个方法都是将源数组中的元素复制到新数组中,

区别是:System.arraycopy()需要传入目标数组,Arrays.copyOf()则不需要,

Arrays.copyOf()是自己新建一个数组,再调用Arrays.copyOf()拷贝数组元素,返回新数组。

 

Arrays.copyOf()源码如下:

 public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        @SuppressWarnings("unchecked")
        T[] copy = ((Object)newType == (Object)Object[].class)  //新建一个数组作为目标数组
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));  //调用System.arraycopy()传入目标数组,拷贝元素
        return copy;  //返回目标数组
    }

 

 

 

 

 

6、HashMap 和 Hashtable 的区别

  1. 线程是否安全: HashMap 是非线程安全的,Hashtable 是线程安全的;Hashtable 内部的方法基本都经过synchronized 修饰。(t就是小写的)
  2. 效率: 因为线程安全的问题,HashMap 要比 Hashtable 效率高一点。Hashtable 基本被淘汰,不要在代码中使用它,如果要保证线程安全,使用 ConcurrentHashMap来代替Hashtable;
  3. 初始容量大小和每次扩充容量大小的不同 : ①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小,也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。
  4. 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。

 

 

 

 

7、HashMap 和 HashSet区别

HashMapHashSet
实现了Map接口 实现Set接口
存储键值对 仅存储对象
调用 put()向map中添加元素 调用 add()方法向Set中添加元素
HashMap使用键(Key)计算Hashcode     HashSet使用元素计算hashcode值                     

 

 

 

 

HashSet 底层是基于 HashMap 实现的。HashSet 的源码非常非常少,因为除了 clone()writeObject()readObject()是 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。

 

HashSet源码如下:

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

    private transient HashMap<E,Object> map;
    
    private static final Object PRESENT = new Object();

    public HashSet() {
        map = new HashMap<>();
    }
    
    public HashSet(Collection<? extends E> c) {
        map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
        addAll(c);
    }

    public HashSet(int initialCapacity, float loadFactor) {
        map = new HashMap<>(initialCapacity, loadFactor);
    }
    
    public HashSet(int initialCapacity) {
        map = new HashMap<>(initialCapacity);
    }
    
    HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }

    public Iterator<E> iterator() {
        return map.keySet().iterator();
    }
    
    public int size() {
        return map.size();
    }

    public boolean isEmpty() {
        return map.isEmpty();
    }

    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;
    }

    public void clear() {
        map.clear();
    }

    @SuppressWarnings("unchecked")
    public Object clone() {
        try {
            HashSet<E> newSet = (HashSet<E>) super.clone();
            newSet.map = (HashMap<E, Object>) map.clone();
            return newSet;
        } catch (CloneNotSupportedException e) {
            throw new InternalError(e);
        }
    }
    
    private void writeObject(java.io.ObjectOutputStream s)
            throws java.io.IOException {
        //......
    }
    
    private void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
       //......
    }
   
}

 

HashSet内部使用一个HashMap来存储元素,元素存储为HashMap的key,value都是同一个对象(指向同一个对象):

private static final Object PRESENT = new Object();

 

 

 

 

8、HashSet如何检查重复

当你把对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,同时也会与其他加入的对象的hashcode值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同hashcode值的对象,这时会调用equals()方法来检查hashcode相等的对象是否真的相同。如果两者相同,HashSet就不会让加入操作成功。

 

hashCode()与equals()的相关规定:

  1. 如果两个对象相等,则hashcode一定也是相同的
  2. 两个对象相等,对两个equals方法返回true
  3. 两个对象有相同的hashcode值,它们也不一定是相等的
  4. 综上,equals方法被覆盖过,则hashCode方法也必须被覆盖
  5. hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象的内容相同)。

 

 

 

 

 

9、HashMap的底层实现

hashMap的部分源码如下:

public class HashMap<K,V> extends AbstractMap<K,V>
        implements Map<K,V>, Cloneable, Serializable {

    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;     //........
    }transient Node<K,V>[] table;
    
    transient Set<Map.Entry<K,V>> entrySet;
    
    transient int size;
    

定义了一个内部类Node,即链表的一个节点,用来存储一个键值对,只有next,是单向链表。

定义了一个数组 Node<K,V>[ ] table; 数组的每个元素都是一个链表(链表第一个节点的地址)。查找键值对(节点)时在数组中查找,根据hash来查找,速度极快。

定义了一个Set:Set<Map.Entry<K,V>> entrySet,来缓存所有的键值对。遍历键值对时(比如获取Entry集合、key集合、value集合),遍历set,遍历时元素顺序不确定,因为set无序。

 

再看下HashMap的get()、put()方法:

   public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

    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) {
            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;
    }

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

都是操作数组(链表)。

 

相比于之前的版本, JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。

TreeMap、TreeSet以及JDK1.8之后的HashMap底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。

 

数组容量不够时,需要复制到新的数组,这时需要rehash重新确定每个键值对的存储位置,

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

 

 

 

 

 

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

不管扩容多少次,HashMap中的数组的长度总是2的n次方。为什么?

为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。Hash 值的范围值-2147483648到2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ (n - 1) & hash”。(n代表数组长度)。

如何来实现这个计算方法?可以采用%取余来实现,但是如果取余(%)操作中除数是2的n次幂则等价于与其除数减一的与(&)操作,即 hash%length==hash&(length-1), 采用二进制位操作 &,相对于%能够提高运算效率,但这有个前提:除数(length)要是2的n次幂,所以数组的长度被设计为2的n次幂。

 

 

 

 

 

11、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,竞争会越来越激烈效率越低。

ConcurrentHashMap效率更高,尽量使用ConcurrentHashMap代替Hashtable。

 

 

 

 

 

12、集合框架底层数据结构总结

Collection

1. List

  • Arraylist: Object数组
  • Vector: Object数组
  • LinkedList: 双向链表

2. Set

  • HashSet(无序,元素不能重复): 基于 HashMap 实现的,底层采用 HashMap 来保存元素
  • LinkedHashSet: LinkedHashSet 继承于 HashSet,其内部通过 LinkedHashMap 来实现。
  • TreeSet(有序,元素不能重复): 红黑树(自平衡的排序二叉树)

 

Map

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

 

 

 

 

13、如何选用集合?

根据集合的特点来选用:

元素是键值对采用Map接口下的集合、元素是单个对象使用Collection接口下的集合

双列:需要排序时选择TreeMap,不需要排序时就选择HashMap,要保证线程安全就选用ConcurrentHashMap

单列:要保证元素唯一(不重复)时选择实现Set接口的集合比如TreeSet或HashSet,不需要就选择实现List接口的比如ArrayList或LinkedList

 

 

 

原文出处:https://snailclimb.gitee.io/javaguide/#/docs/java/collection/Java%E9%9B%86%E5%90%88%E6%A1%86%E6%9E%B6%E5%B8%B8%E8%A7%81%E9%9D%A2%E8%AF%95%E9%A2%98

在原文基础上修改了部分内容

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!