ArrayList HashMap

ArrayList

ArrayList 是 java 集合框架中常用的数据结构,实现了List接口,同时还实现了 RandomAccess、Cloneable、Serializable 接口!

System.arraycopy() 方法

源码:

    // 我们发现 arraycopy 是一个 native 方法,接下来我们解释一下各个参数的具体意义
    /**
    *   复制数组
    * @param src 源数组
    * @param srcPos 源数组中的起始位置
    * @param dest 目标数组
    * @param destPos 目标数组中的起始位置
    * @param length 要复制的数组元素的数量
    */
    public static native void arraycopy(Object src,  int  srcPos,
                                        Object dest, int destPos,
                                        int length);

Arrays.copyOf()方法

源码:

    public static int[] copyOf(int[] original, int newLength) {
    	// 申请一个新的数组
        int[] copy = new int[newLength];
	// 调用System.arraycopy,将源数组中的数据进行拷贝,并返回新的数组
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }

ArrayList是如何扩容的?

jdk 1.7之前默认容量是10

//默认初始化容量
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
 public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

//集合的长度
private int size;

//集合存元素的数组
Object[] elementData; 

//默认的容量
private static final int DEFAULT_CAPACITY = 10;

private static int calculateCapacity(Object[] elementData, int minCapacity) {
     	//判断集合存储元素的数组是否为空
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            //如果当前最小容量小于默认容量10 就将默认容量10返回
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }

 private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
     	//判断最小容量是否大于当前数组实际容量(就是判断是否要扩容)
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }

//扩容的核心方法
private void grow(int minCapacity) {
    	//原来容量
        int oldCapacity = elementData.length;
    	//扩容1.5倍
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            //判断扩容之后还是小了,就直接把最小容量当做新的容量
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
    	//拷贝数组
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

对于ArrayList ,new无参对象时,底层是一个空数组,当添加第一个元素时,会进行扩容,将底层数组长度扩为10,
其中扩容触发的条件是:存元素时,即先让size+1的值(也就是最小容量的值)判断是否大于底层elementData.length的长度,如果大于,则先扩容再添加,扩容的倍数为1.5倍。

当我们初始化一个arraylist数组时,jdk1.8 默认是为空的,当我调用add方法时,会进行第一次的扩容,将数组长度扩容为10;当我们数组的元素大于数组的长度10时,会触发自动扩容机制,首先创建一个新的数组,这个数组的长度是原数组的1.5倍,然后使用Arrays.copyOf方法把原数组的数据拷贝到新数组里面

ArrayList频繁扩容导致添加性能急剧下降,如何处理?

当需要加入的数据量特别多,如果是无参的话,数组的容量是逐渐增加的,那么就会触发很多次的扩容,因为扩容的时候会使用到数组拷贝,这个过程很耗费性能,会导致ArrayList效率下降

可以在创建集合时指定初始容量大小,减少扩容次数

Arraylist 与 LinkedList 区别?

  1. 是否保证线程安全: ArrayListLinkedList 都是不同步的,也就是不保证线程安全;
  2. 底层数据结构: Arraylist 底层使用的是 Object 数组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 更多的空间(因为要存放直接后继和直接前驱以及数据)。

Array和ArrayList的区别?

  • array是数组,声明好后,长度是固定的;ArrayList是集合,底层是动态数组,长度可以改变
  • array可以存储基本类型和对象类型;ArrayList只能存储对象类型

ArrayList插入或删除元素一定比LinkedList慢吗?

不一定,得看操作元素的位置

ArrayList在插入或删除元素时都会拷贝数组,增删越靠前的元素,拷贝的元素越多,效率越低;

public void add(int index, E element) {
    rangeCheckForAdd(index);

    ensureCapacityInternal(size + 1);  // Increments modCount!!
    System.arraycopy(elementData, index, elementData, index + 1,
                     size - index;//把索引位置后的元素后移一格
    elementData[index] = element;
    size++;
}

LinkedList在插入或删除元素时,调用Node方法,折半查找要操作的元素,如果数据量大,并且操作的元素在中间,这时候效率也会很慢

//找元素的方法 
Node<E> node(int index) {
        // assert isElementIndex(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;
        }
    }

ArrayList线程不安全

因为ArrayList是动态数组,size(数组的长度)是成员变量,是共享的;当两个线程同时操作add方法时,线程a添加一个元素,数组下标增加,此时线程b在操作时发现没有这个下标,就会抛出数组越界异常;或者在赋值操作时候,多个线程操作同一个下标,会出现值被覆盖,值为null的现象;

解决:

  • 使用Vector类,因为该类的方法加了同步锁(synchronized)

  • 使用Collections.synchronizedCollection(Collection c):

    Object mutex = new Object()。对此对象使用synchronized

  • 使用CopyOnWriteArrayList,COW采用自旋锁(jdk1.6升级为synchronize)对写加锁,读不加锁,数组变量有使用volatile保证可见性,增删创建一新数组,读取的是原数据,适合读多写少场景。

Write的时候总是要Copy(将原来array复制到新的array,修改后,将引用指向新数组)。任何可变的操作(add、set、remove等)都通过ReentrantLock 控制并发。

复制ArrayList的5种方法

  1. 使用ArrayList的构造方法,底层实际上调用了Arrays.copyOf方法来对数组进行拷贝。这个拷贝调用了系统的native arraycopy方法,注意这里的拷贝是引用拷贝,而不是值的拷贝。
    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;
        }
    }
  1. 使用addAll方法
 public boolean addAll(Collection<? extends E> c) {
        Object[] a = c.toArray();
        int numNew = a.length;
        ensureCapacityInternal(size + numNew);  // Increments modCount
        System.arraycopy(a, 0, elementData, size, numNew);
        size += numNew;
        return numNew != 0;
    }
  1. 使用Collections.copy(dest,src):首先要指定要复制数组的大小(要大于或等于被复制数组的大小),因为该方法首先会判断两个数组的长度大小;
  2. 使用stream流的方式
  3. 直接使用clone方法
ArrayList<Integer> list = new ArrayList<Integer>();
        list.add(1);
        list.add(2);
        list.add(3);

        //构造方法
        ArrayList<Integer> listCopy1 = new ArrayList<Integer>(list);
        System.out.println(listCopy1);

        //addAll方法
        ArrayList<Integer> listCopy2 = new ArrayList<Integer>();
        listCopy2.addAll(list);
        System.out.println(listCopy2);

        //Collections.copy方法
        ArrayList<Integer> listCopy3 = new ArrayList<Integer>(
                Arrays.asList(new Integer[list.size()]));
        Collections.copy(listCopy3, list);
        System.out.println(listCopy3);

        //stream流方法
        List<Integer> listCopy4 = list.stream().collect(Collectors.toList());
        System.out.println(listCopy4);

 		//使用clone方法
        ArrayList<Integer> listCopy5 = (ArrayList<Integer>) list.clone();
        System.out.println(listCopy5);

HashMap

HashMap主要用来存放键值对,可以存放null的key和value,但是为null的key只能有一个,key是唯一的,存储的元素是无序的,线程不安全

HashMap底层实现

JDK1.8之前,是由数组加链表组成

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

HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过(n-1) & hash公式(n为数组长度)得到key在数组中存放的下标,如果当前下标位置存在元素(哈希冲突),就要判断当前元素与要存入元素的hash值和key是否相同,如果相同直接覆盖;如果不同,就通过拉链法解决冲突。

所谓 “拉链法” 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。

我们知道,链表查找数据必须从第一个元素开始查找,直到找到为止,时间复杂度为O(n),所以当链表越来越长是,hashmap的效率越来越低;

那么怎么解决这个问题?

  • JDK1.8开始采用:数组+链表+红黑树 结构来实现HashMap,当链表的长度(元素)大于阈值8(在链表转为红黑树之前会判断,如果当前数组的长度小于64,会先进行数组扩容,而不是转为红黑树)时,就会将链表转为红黑树,以提高查找效率;

类的属性:

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable {
    // 序列号
    private static final long serialVersionUID = 362498820763181265L;
    // 默认的初始容量是16
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
    // 最大容量
    static final int MAXIMUM_CAPACITY = 1 << 30;
    // 默认的填充因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    // 当桶(bucket)上的结点数大于这个值时会转成红黑树
    static final int TREEIFY_THRESHOLD = 8;
    // 当桶(bucket)上的结点数小于这个值时树转链表
    static final int UNTREEIFY_THRESHOLD = 6;
    // 桶中结构转化为红黑树对应的table的最小容量
    static final int MIN_TREEIFY_CAPACITY = 64;
    // 存储元素的数组,总是2的幂次倍
    transient Node<k,v>[] table;
    // 存放具体元素的集
    transient Set<map.entry<k,v>> entrySet;
    // 存放元素的个数,注意这个不等于数组的长度。
    transient int size;
    // 每次扩容和更改map结构的计数器
    transient int modCount;
    // 临界值(容量*填充因子) 当实际大小超过临界值时,会进行扩容
    int threshold;
    // 加载因子
    final float loadFactor;
}
  • loadFactor 加载因子

    loadFactor 加载因子是控制数组存放数据的疏密程度,loadFactor 越趋近于 1,那么 数组中存放的数据(entry)也就越多,也就越密,也就是会让链表的长度增加,loadFactor 越小,也就是趋近于 0,数组中存放的数据(entry)也就越少,也就越稀疏。

    loadFactor 太大导致查找元素效率低,太小导致数组的利用率低,存放的数据会很分散。loadFactor 的默认值为 0.75f 是官方给出的一个比较好的临界值

    给定的默认容量为 16,负载因子为 0.75。Map 在使用过程中不断的往里面存放数据,当数量达到了 16 * 0.75 = 12 就需要将当前 16 的容量进行扩容,而扩容这个过程涉及到 rehash、复制数据等操作,所以非常消耗性能。

为什么HashMap要用数组加链表来实现?

HashMap的key经过扰动函数之后得到一个hash值,然后通过公式(数组的长度-1)& hash值得到key在数组中的下标,如果当前下标位置存在元素,说明存在哈希冲突,就要判断当前元素与要存入元素的hash值和key值是否相同,如果相同就直接覆盖,如果不同,就要通过链表来解决哈希冲突;这是jdk1.7的做法,如果链表的长度过长,会导致查找和插入效率变低。所有jdk1.8新增加了红黑树

HashMapd的put方法的大致流程

  1. 首先根据key的值计算出hash值,然后通过(n-1)&hash操作找到该元素在数组中存储的下标
  2. 如果当前数组为空,则会调用resize方法进行初始化,得到一个默认容量16,阈值为12的数组
  3. 如果没有哈希冲突,就直接放入该数组的下标
  4. 如果有哈希冲突,并且key相同,就直接覆盖value的值
  5. 如果冲突后,发现该节点是红黑树(TreeNode),就把该节点挂在树上
  6. 如果冲突后是链表,判断链表的长度是否大于阈值8,如果大于8并且数组的容量小于64,就会进行扩容;

如果链表长度大于8并且数组容量大于64,就会把链表转为红黑树;否则,就会插入到链表中,如果有相同的key,就会直接覆盖。

jdk1.8对HashMap做了哪些优化?

  1. 引入了红黑树,当链表长度大于8并且数组长度大于64时,链表转为红黑树,解决了链表过长,导致查询效率降低的问题,链表查询时间复杂度O(n),红黑树增删查时间复杂度都为O(logn)
  2. 哈希冲突时,头插改尾插,可以避免扩容后相对位置的倒序,避免并发环境下,扩容产生循环链表,导致死循环
  3. 2倍扩容后,在计算数组下标时,直接判断hash高位值(前16位),如果为0,则原位置保持不变,如果为1,原位置加上原数组长度,这样省去了重新计算hash的时间,这样算出的数组下标具有随机性,减少哈希冲突

HashMap的扩容方式

jdk1.7中,扩容会重新计算hash值,并且会遍历hash表所有元素,是非常耗时的;

HashMap在容量大于阈值(加载因子*当前数组容量 =0.75*16=12 )时,就会调用resize方法进行扩容,将hashmap的大小扩容为原来的2倍,并将原来的对象放入新数组当中;

当我们初始化一个Hashmap集合时,默认集合容量大小为0,当我们调用put方法时,hashmap会进行扩容,将大小扩容为默认的容量16,阈值为12;当向集合添加元素时,如果元素的个数大于阈值12时,就会进行扩容,将大小扩容为原来的2倍(左移一位),然后遍历数组,判断要转移的元素是单个元素、链表或者红黑树;

  • 如果是单个元素,就直接放入到新数组

    if (e.next == null)
        newTab[e.hash & (newCap - 1)] = e;
    
  • 如果是链表,会有一个高低位的选择,计算的下标位置不变或者计算的(下标位置+原数组的长度)

Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
    next = e.next;
    if ((e.hash & oldCap) == 0) {
        if (loTail == null)
            loHead = e;
        else
            loTail.next = e;
        loTail = e;
    }
    else {
        if (hiTail == null)
            hiHead = e;
        else
            hiTail.next = e;
        hiTail = e;
    }
} while ((e = next) != null);
  • 如果是红黑树,遍历双向链表统计哪些元素在扩容完是原位置还是新位置,这样遍历完双向链表后,就会得到两个子链表,一个放在原下标位置,一个放在新下标位置,如果原下标位置或新下标位置没有元素,则红黑树不用拆分,否则判断这两个子链表的长度,如果超过八,则转成红黑树放到对应的
    位置,否则把单向链表放到对应的位置。
  • 元素转移完了之后,在把新数组对象赋值给HashMap的table属性,老数组会被回收。

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

为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。

为什么HashMap是不安全的?

jdk1.7,头插法——>当并发执行扩容操作时会造成环形链和数据丢失的情况。

HashMap在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他的元素也在进行put操作,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,**导致在get时会出现死循环,**所以HashMap是线程不安全的。

jdk1.8,尾插法——>在并发执行put操作时会发生数据覆盖的情况。

向HashMap集合中添加元素会存在覆盖的现象,导致了线程不安全。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

揽星河吖

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值