java集合(Collection)底层细节

在这里插入图片描述

所有的Java集合都继承了Collection接口,而Collection接口又集成了Iterable(迭代器)接口。也就是说所有的Java集合类(ArrayList、HashMap等)都可以使用iterator进行遍历,且都实现了Collection接口中规定的方法。

List

ArrayList

    先说结论ArrayList是动态数组,底层实现是一个Object数组。当使用无参构造器ArrayList()时,初始容量elementData为0。第一次扩容时容量+10。之后再次需要扩容时,扩为原容量的1.5倍。当使用有参构造器时,初始初始容量为传进去参数的容量大小。需要扩容时,扩为原容量的1.5倍。

ArrayList的底层实现

    ArrayList的底层实现是一个Object数组。由于是Object数组所以,int等基本类型不能往里面放得先转成Integer等类。transient表示在序列化时忽略

transient Object[] elementData;

ArrayList扩容

    ArrayList有三个构造器ArrayList(),ArrayList(Collection<? extends E> c),ArrayList(int initialCapacity)
    当使用无参构造器ArrayList()时,初始容量elementData为0。第一次扩容时容量+10。之后再次需要扩容时,扩为原容量的1.5倍。源码中扩为原容量的1.5倍是使用位运算来进行的。

  int oldCapacity = elementData.length;
  int newCapacity = oldCapacity + (oldCapacity >> 1);

oldCapacity >> 1表示oldCapacity 向右移动一位也就是oldCapacity 除以2。位运算详细内容可以看我之前写的位运算说明

    当使用有参构造器时,初始初始容量为传进去参数的容量大小。需要扩容时,扩为原容量的1.5倍。
    扩容时会new一个新的Object[],再把老的数组拷贝过去,因此扩容后ArrayList中的元素内存地址会发生改变。当然ArrayList的内存地址没变。

elementData和Size的区别

    elementData容量和size大小是不一样的,size表示ArrayList最后一个元素所在的位置,elementData数组是ArrayList实际存放元素的地方。size<=elementData的容量。

ArrayList的最大容量

    根据ArrayList源码,ArrayList的最大容量为Integer.MAX_VALUE - 8也就是2147483639。为什么不是Integer.MAX_VALUE呢?因为有些虚拟机在数组中保留一些header words,所以预留了8来防止越界

 /**
     * The maximum size of array to allocate (unless necessary).
     * Some VMs reserve some header words in an array.
     * Attempts to allocate larger arrays may result in
     * OutOfMemoryError: Requested array size exceeds VM limit
     */
    private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

Vector

    先说结论Vector也是动态数组,基本使用方式和ArrayList差不多,但是两者有些许区别。
| | 效率 |线程安全|
|–|–|–|–|–|–|
| Vector |低 |安全|
| ArrayList| 高 |不安全|
    Vector有三个构造器,不传参默认初始容量为10(调用了那个单参构造器)。其他的两个有参构造器默认容量为参数大小。当Vector扩容时,如果capacityIncrement(扩容增量不为0),那么就在原基础上扩容 capacityIncrement的大小。如果capacityIncrement为0则容量翻倍。

Vector类

    Vector类中重要的三个成员变量和一个常量分别是

protected Object[] elementData; 存放元素的数组
protected int elementCount; 实际元素个数(类似ArrayList的Size变量)
protected int capacityIncrement; 每次扩容的增长量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; 最大容量

    以及三个构造器

双参构造器,传入初始容量大小和容量增量大小。
public Vector(int initialCapacity, int capacityIncrement) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
        this.capacityIncrement = capacityIncrement;
    }
单参构造器传入初始容量
public Vector(int initialCapacity) {
        this(initialCapacity, 0);
    }
无参构造器
public Vector() {
        this(10);
    }

    可以看到,不传参默认初始容量为10(调用了那个单参构造器)。其他的两个有参构造器默认容量为参数大小。

Vector扩容机制

private Object[] grow(int minCapacity) {
        return elementData = Arrays.copyOf(elementData,newCapacity(minCapacity));
    }
private int newCapacity(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
        							capacityIncrement : oldCapacity);最重要
        if (newCapacity - minCapacity <= 0) {
            if (minCapacity < 0) // overflow
                throw new OutOfMemoryError();
            return minCapacity;
        }
        return (newCapacity - MAX_ARRAY_SIZE <= 0)
            ? newCapacity
            : hugeCapacity(minCapacity);
    }

    当Vector扩容时,如果capacityIncrement(扩容增量不为0),那么就在原基础上扩容 capacityIncrement的大小。如果capacityIncrement为0则容量翻倍。

Vector的线程安全

    Vector的getremoveadd方法都有synchronized关键字。因此Vector是线程安全的。

  public synchronized boolean add(E e) {
        modCount++; 定义在AbstractList中,记录被修改了几次
        add(e, elementData, elementCount);
        return true;
    }
  public synchronized boolean removeElement(Object obj) {
        modCount++;
        int i = indexOf(obj);
        if (i >= 0) {
            removeElementAt(i);
            return true;
        }
        return false;
    }
 public synchronized E get(int index) {
        if (index >= elementCount)
            throw new ArrayIndexOutOfBoundsException(index);

        return elementData(index);
    }

LinkedList

LinkedList是java实现的双向队列,他有三个成员变量。LinkedList因为是链表引起增加和删除操作速度快于ArrayList,修改和查询慢于ArrayList。

transient int size = 0; 表示链表长度
transient Node<E> first; 头节点
transient Node<E> last; 尾节点

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

Node(根据索引查找节点)

LinkList的Node()方法是根据索引查找节点。Node()是LinkedList里面最重要的方法,增删改查都需要用到这个方法。这个Node()并不是傻傻的从头到尾遍历一遍。他是先判断索引的大小,如果索引大于size/2 (用的还是位运算),那就从尾向前遍历。如果索引小于size/2,那就从头到尾遍历。

Node<E> node(int index) {
        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

indexOf(根据节点值查找索引)

这个就真的是傻傻的从头到尾遍历了

 public int indexOf(Object o) {
        int index = 0;
        if (o == null) {
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null)
                    return index;
                index++;
            }
        } else {
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item))
                    return index;
                index++;
            }
        }
        return -1;
    }

Set

HashSet

当我们new了一个HashSet然后点进他的构造器,我们就会惊讶的发现HashSet的底层实现是HashMap。而当我们传入一个集合时,HashMap的初始容量是max(集合容量/0.75+1,16)。
HashSet的实际数据存在Node里面的key。Node中的value使用了一个静态变量PERSENT占坑

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

add方法

由于HashSet的底层实现是HashMap,所以很容易就能猜到HashSet的add()方法必然会调用HashMap的put()方法。由于HashMap的put方法必须传入key-value。所以HashSet预留了一个PRESENT常量来占坑(占value的坑,无任何实际意义),实际的元素存放在key中。 下面代码中的E e就是实际存放的元素。

HashSet源码
private static final Object PRESENT = new Object();
public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }

List和Set的区别

  1. 在List中允许插入重复的元素,而在Set中不允许重复元素存在。
  2. List是有序集合,会保留元素插入时的顺序,Set是无序集合。
  3. List可以通过下标来访问,而Set不能。

Map

HashMap

HashMap的底层是一个Node数组。而Node有一个next属性,也就是说他是一个链表的结点。因此HashMap的存储结构是链表头的数组
在这里插入图片描述

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

HashMap扩容机制

先说结论,HashMap除了传入集合的构造器外,其他构造器初始化的初始容量都是0。使用传入集合的构造器时,初始容量为大于等于集合size的最小2的n次。

首先来看源码中的几个常量,可以粗暴的得出结论初始容量为16,最大容量为2^30,当容量达到当前容量的75%时,就会扩容,扩容就是直接乘以2。

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; 2的四次方
static final int MAXIMUM_CAPACITY = 1 << 30; 230次方
static final float DEFAULT_LOAD_FACTOR = 0.75f;  默认载入系数

put方法

首先使用hash()方法获得key的hash索引(hash索引!=hashcode)。可以看到如果key是空则返回0。不为空则返回key的hashCode^key的hashCode无符号右移16位(为了减少冲突)。

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

获得hash索引后,根据hash索引将数据插入Map中。

  1. 如果那个位置无元素那就直接放入
  2. 如果那个位置有元素,那就比较是否相同。(判断是否是同一个对象且equals(),(不同类的equals是不同的。具体得看是怎么重写的)为否)
    2.1 相同就直接放弃
    2.2 不同就继续对比这个位置的链表中的元素,直到链表末尾都不同就放在最后。
  3. 元素添加到链表后,如果长度大于等于8
    3.1 数组长度大于等于64树化该位置的链表
    3.2 否则就扩容数组。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值