集合总结(完善中...)

#集合总结(完善中…)

(1)ArrayList:底层数据结构是数组,查询快,增删慢,线程不安全,效率高,可以存储重复元素
(2)LinkedList 底层数据结构是链表,查询慢,增删快,线程不安全,效率高,可以存储重复元素
(3)Vector:底层数据结构是数组,查询快,增删慢,线程安全,效率低,可以存储重复元素
大部分方法都被synchronized 关键字修饰

一、arraylist源码解析(理解&重要)

扩容详解:使用空参构造会得到一个长度为 0 的数组,但是没有分配容量
在添加第一个元素时进行第一次扩容,新建一个长度为10 的数组,把元素添加进去
在添加元素后数组长度大于原本数组长度的,进行int newCapacity = oldCapacity + (oldCapacity >> 1); 1.5倍扩容
size永远指向下一个元素添加的位置,同时也代表了当前 list 的大小

1、继承和实现关系

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable

ArrayList 是一个数组队列,相当于 动态数组。与Java中的数组相比,它的容量能动态增长。它继承于AbstractList,实现了List, RandomAccess, Cloneable, java.io.Serializable这些接口。
ArrayList 继承了AbstractList,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。
ArrayList 实现了RandmoAccess接口,即提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能的。在ArrayList中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访问。稍后,我们会比较List的“快速随机访问”和“通过Iterator迭代器访问”的效率。
ArrayList 实现了Cloneable接口,即覆盖了函数clone(),能被克隆。
ArrayList 实现java.io.Serializable接口,这意味着ArrayList支持序列化,能通过序列化去传输。

和Vector不同,ArrayList中的操作不是线程安全的!所以,建议在单线程中才使用ArrayList,而在多线程中可以选择Vector或者CopyOnWriteArrayList。

2、arraylist属性

ArrayList属性主要就是当前数组长度size,以及存放数组的对象elementData数组,除此之外还有一个经常用到的属性就是从AbstractList继承过来的modCount属性,代表ArrayList集合的修改次数。

// 序列化id
private static final long serialVersionUID = 8683452581122892189L;
// 默认初始的容量
private static final int DEFAULT_CAPACITY = 10;
// 一个空对象
private static final Object[] EMPTY_ELEMENTDATA = new Object[0];
// 一个空对象,如果使用默认构造函数创建,则默认对象内容默认是该值
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0];
// 当前数据对象存放地方,当前对象不参与序列化
transient Object[] elementData;
// 当前数组长度
private int size;
// 数组最大长度
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

3、构造器

注意:此时我们创建的ArrayList对象中的elementData中的长度是0,size是0,当进行第一次add的时候,elementData将会变成默认的长度:10.

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

如果传入参数,则代表指定ArrayList的初始数组长度,传入参数如果是大于等于0,则使用用户的参数初始化,如果用户传入的参数小于0,则抛出异常,构造方法如下

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

4、常用方法

1、add方法 :

其实add方法整体逻辑还是比较简单。主要注意扩容条件:只要插入数据size比原来大就会进行扩容。因此如果在循环中使用ArrayList时需要特别小心,避免频繁扩容造成OOM异常

添加元素方法入口:

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

确保添加的元素有地方存储,当第一次添加元素的时候this.size+1 的值是1,所以第一次添加的时候会将当前elementData数组的长度变为10 (待确定)

private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);
}

将修改次数(modCount)自增1,判断是否需要扩充数组长度,判断条件就是用当前所需的数组最小长度与数组的长度对比,如果大于0,则增长数组长度。

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

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

如果当前的数组已使用空间(size)加1之后 大于数组长度,则增大数组容量,扩大为原来的1.5倍

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.5倍。这种操作的代价是很高的,因此在实际使用时,我们应该尽量避免数组容量的扩张。当我们可预知要保存的元素的多少时,要在构造ArrayList实例时,就指定其容量,以避免数组扩容的发生。或者根据实际需求,通过调用ensureCapacity方法来手动增加ArrayList实例的容量。

2、remove

步骤:1-越界检查 2-修改自增次数 3-将index处的元素存到oldValue 4-将index后面的元素前移一位,调用的是 System.arraycopy 方法 5-将最后一个元素置空并返回oldValue

注意点:remove 方法移除了元素,但是没有缩减数组的长度 (clear方法也类似,置空后等待gc回收)

public E remove(int index) {
    rangeCheck(index);

    modCount++;
    E oldValue = elementData(index);

    int numMoved = size - index - 1;
    if (numMoved > 0)
        System.arraycopy(elementData, index+1, elementData, index,
                         numMoved);
    elementData[--size] = null; // clear to let GC do its work

    return oldValue;
}

3、iterator

interator方法返回的是一个内部类,由于内部类的创建默认含有外部的this指针,所以这个内部类可以调用到外部类的属性

public Iterator<E> iterator() {
    return new Itr();
}

一般的话,调用完iterator之后,我们会使用iterator做遍历,这里使用next做遍历的时候有个需要注意的地方,就是调用next的时候,可能会引发ConcurrentModificationException,当修改次数,与期望的修改次数(调用iterator方法时候的修改次数)不一致的时候,会发生该异常,详细我们看一下代码实现:

@SuppressWarnings("unchecked")
public E next() {
    checkForComodification();
    int i = cursor;
    if (i >= size)
        throw new NoSuchElementException();
    Object[] elementData = ArrayList.this.elementData;
    if (i >= elementData.length)
        throw new ConcurrentModificationException();
    cursor = i + 1;
    return (E) elementData[lastRet = i];
}

expectedModCount这个值是在用户调用ArrayList的iterator方法时候确定的,但是在这之后用户add,或者remove了ArrayList的元素,那么modCount就会改变,那么这个值就会不相等,将会引发ConcurrentModificationException异常,这个是在多线程使用情况下,比较常见的一个异常。

final void checkForComodification() {
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
}

5、小结

ArrayList自己实现了序列化和反序列化的方法,因为它自己实现了 private void writeObject(java.io.ObjectOutputStream s)和 private void readObject(java.io.ObjectInputStream s) 方法
ArrayList基于数组方式实现,无容量的限制(会扩容)
添加元素时可能要扩容(所以最好预判一下),删除元素时不会减少容量(若希望减少容量,trimToSize()),删除元素时,将删除掉的位置元素置为null,下次gc就会回收这些元素所占的内存空间。
线程不安全
add(int index, E element):添加元素到数组中指定位置的时候,需要将该位置及其后边所有的元素都整块向后复制一位
get(int index):获取指定位置上的元素时,可以通过索引直接获取(O(1))
remove(Object o)需要遍历数组
remove(int index)不需要遍历数组,只需判断index是否符合条件即可,效率比remove(Object o)高
remove(object o) : remove元素的时候分为null和非null,并且是快速remove,并未做越界检查
contains(E)需要遍历数组
使用iterator遍历可能会引发多线程异常

6、总结

  • 注意其三个不同的构造方法。无参构造方法构造的ArrayList的容量默认为10,带有Collection参数的构造方法,将Collection转化为数组赋给ArrayList的实现数组elementData。
  • 注意扩充容量的方法ensureCapacity。ArrayList在每次增加元素(可能是1个,也可能是一组)时,都要调用该方法来确保足够的容量。当容量不足以容纳当前的元素个数时,就设置新的容量为旧的容量的1.5倍加1,如果设置后的新容量还不够,则直接新容量设置为传入的参数(也就是所需的容量),而后用Arrays.copyof()方法将元素拷贝到新的数组(详见下面的第3点)。从中可以看出,当容量不够时,每次增加元素,都要将原来的元素拷贝到一个新的数组中,非常之耗时,也因此建议在事先能确定元素数量的情况下,才使用ArrayList,否则建议使用LinkedList。
  • ArrayList的实现中大量地调用了Arrays.copyof()和System.arraycopy()方法。我们有必要对这两个方法的实现做下深入的了解。 --copyOf 方法内部调用了System.arraycopy() 方法
  • ArrayList基于数组实现,可以通过下标索引直接查找到指定位置的元素,因此查找效率高,但每次插入或删除元素,就要大量地移动元素,插入删除元素的效率低。
  • 在查找给定元素索引值等的方法中,源码都将该元素的值分为null和不为null两种情况处理,ArrayList中允许元素为null。

二、linkedlist源码解析(了解)

1、继承和实现关系

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable

1–继承了AbstractSequentialList抽象类:在遍历LinkedList的时候,官方更推荐使用顺序访问,也就是使用我们的迭代器。(因为LinkedList底层是通过一个链表来实现的)(虽然LinkedList也提供了get(int index)方法,但是底层的实现是:每次调用get(int index)方法的时候,都需要从链表的头部或者尾部进行遍历,每一的遍历时间复杂度是O(index),而相对比ArrayList的底层实现,每次遍历的时间复杂度都是O(1)。所以不推荐通过get(int index)遍历LinkedList。至于上面的说从链表的头部后尾部进行遍历:官方源码对遍历进行了优化:通过判断索引index更靠近链表的头部还是尾部来选择遍历的方向)(所以这里遍历LinkedList推荐使用迭代器)。
2–实现了List接口。(提供List接口中所有方法的实现)
3–实现了Cloneable接口,它支持克隆(浅克隆),底层实现:LinkedList节点并没有被克隆,只是通过Object的clone()方法得到的Object对象强制转化为了LinkedList,然后把它内部的实例域都置空,然后把被拷贝的LinkedList节点中的每一个值都拷贝到clone中。(后面有源码解析)
4–实现了Deque接口。实现了Deque所有的可选的操作。
5–实现了Serializable接口。表明它支持序列化。(和ArrayList一样,底层都提供了两个方法:readObject(ObjectInputStream o)、writeObject(ObjectOutputStream o),用于实现序列化,底层只序列化节点的个数和节点的值)。

解释在list中for iterator 遍历

为什么ArrayList的遍历中for比Iterator快,而LinkedList中却是Iterator远快于for?这得从ArrayList和LinkedList两者的数据结构说起了:
ArrayList是基于索引(index)的数组,索引在数组中搜索和读取数据的时间复杂度是O(1),但是要增加和删除数据却是开销很大的,因为这需要重排数组中的所有数据。
LinkedList的底层实现则是一个双向循环带头节点的链表,因此LinkedList中插入或删除的时间复杂度仅为O(1),但是获取数据的时间复杂度却是O(n)。
明白了两种List的区别之后,就知道,ArrayList用for循环随机读取的速度是很快的,因为ArrayList的下标是明确的,读取一个数据的时间复杂度仅为O(1)。但LinkedList若是用for来遍历效率很低,读取一个数据的时间复杂度就达到了为O(n)。而用Iterator的next()则是顺着链表节点顺序读取数据的效率就很高了。

2、源码分析之属性

//元素个数
transient int size = 0;

//首结点
transient Node<E> first;

//尾节点
transient Node<E> last;

3、主要内部类(static 关键字的作用之一)

//定义了链表的结点结构
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;
    }
}

4、构造器

//空构造器
public LinkedList() {
}
//将集合c里面的元素都添加到linkedlist里面去
public LinkedList(Collection<? extends E> c) {
    this();
    addAll(c);
}

5、添加元素/删除元素

添加元素位置:队列首部 — O(1),队列尾部 — O(1) ,队列中间部份— O(n)

删除元素位置:队列首部 — O(1),队列尾部 — O(1) ,队列中间部份— O(n)

6、总结

(1)LinkedList是一个以双链表实现的List;
(2)LinkedList还是一个双端队列,具有队列、双端队列、栈的特性;
(3)LinkedList在队列首尾添加、删除元素非常高效,时间复杂度为O(1);
(4)LinkedList在中间添加、删除元素比较低效,时间复杂度为O(n);
(5)LinkedList不支持随机访问,所以访问非队列首尾的元素比较低效;
(6)LinkedList在功能上等于ArrayList + ArrayDeque;

三、hashSet源码解析(理解)

1、源码分析之属性

//内部元素存储在hashMap中
private transient HashMap<E,Object> map;

//是一个虚拟元素,用来存放value放到map中,没有实际意义
private static final Object PRESENT = new Object();

2、构造方法

//空构造
public HashSet() {
    map = new HashMap<>();
}

//把另外一个集合的元素全部添加到当前的set中
//注意:这里初始化map 的时候是计算了它的初始容量的
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);
}
//这个比较特殊,没有public 修饰,意味着只能在本包中访问
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
    map = new LinkedHashMap<>(initialCapacity, loadFactor);
}

最后一个构造方法有点特殊,它不是public的,意味着它只能被同一个包或者子类调用,这是LinkedHashSet专属的方法。

3、常用方法

//直接调用hashMap的put() 方法,(e,PRESENT)键值对
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

//直接调用hashMap的remove() 方法
//但是hashMap 返回的是删除元素的value,而set返回的是Boolean类型的值
 public boolean remove(Object o) {
        return map.remove(o)==PRESENT;
    }

4、总结

(1)HashSet内部使用HashMap的key存储元素,以此来保证元素不重复;
(2)HashSet是无序的,因为HashMap的key是无序的;
(3)HashSet中允许有一个null元素,因为HashMap允许key为null;
(4)HashSet是非线程安全的;
(5)HashSet是没有get()方法的;

5、惊喜

1、hashMap初始化容量

(1)阿里手册上有说,使用java中的集合时要自己指定集合的大小,通过这篇源码的分析,你知道初始化HashMap的时候初始容量怎么传吗?
我们发现有下面这个构造方法,很清楚明白地告诉了我们怎么指定容量。
假如,我们预估HashMap要存储n个元素,那么,它的容量就应该指定为((n/0.75f) + 1),如果这个值小于16,那就直接使用16得了。
初始化时指定容量是为了减少扩容的次数,提高效率。

//在这里可以看到hashMap初始化容量的源码
public HashSet(Collection<? extends E> c) {
    map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
    addAll(c);
}

2、什么是 fail-fast (快速失败)?

fail-fast机制是java集合中的一种错误机制。
当使用迭代器迭代时,如果发现集合有修改,则快速失败做出响应,抛出ConcurrentModificationException异常。
这种修改有可能是其它线程的修改,也有可能是当前线程自己的修改导致的,比如迭代的过程中直接调用remove()删除元素等。
另外,并不是java中所有的集合都有fail-fast的机制。比如,像最终一致性的ConcurrentHashMap、CopyOnWriterArrayList等都是没有fast-fail的。
那么,fail-fast是怎么实现的呢?
细心的同学可能会发现,像ArrayList、HashMap中都有一个属性叫modCount,每次对集合的修改这个值都会加1,在遍历前记录这个值到expectedModCount中,遍历中检查两者是否一致,如果出现不一致就说明有修改,则抛出ConcurrentModificationException异常。

四、treeSet源码解析(了解)

1、源码分析之属性

//元素就存储在NavigableMap中
//它不一定就是TreeMap!!
private transient NavigableMap<E,Object> m;

//虚拟元素,用作value存储在Map中
private static final Object PRESENT = new Object();

2、构造器

//直接使用传进来的 NavigableMap 存储元素
//这里不是深拷贝,如果外面的 map 有增删改元素也会反映到这里
//这个方法不是public 的,只能在同包中使用
TreeSet(NavigableMap<E,Object> m) {
    this.m = m;
}

//使用TreeMap 初始化
public TreeSet() {
    this(new TreeMap<E,Object>());
}

//使用带有comparator的 TreeMap 初始化
public TreeSet(Comparator<? super E> comparator) {
    this(new TreeMap<>(comparator));
}

//将集合c 中所有的元素添加到TreeSet中
public TreeSet(Collection<? extends E> c) {
    this();
    addAll(c);
}

//将 SortedSet 中所有的元素添加到TreeSet中
public TreeSet(SortedSet<E> s) {
    this(s.comparator());
    addAll(s);
}

3、总结

(1)TreeSet底层使用NavigableMap存储元素;
(2)TreeSet是有序的;
(3)TreeSet是非线程安全的;
(4)TreeSet实现了NavigableSet接口,而NavigableSet继承自SortedSet接口;
(5)TreeSet实现了SortedSet接口;

4、惊喜

1、TreeSet和LinkedHashSet 都有序,但是有什么不同呢?

(1)通过之前的学习,我们知道TreeSet和LinkedHashSet都是有序的,那它们有何不同?
LinkedHashSet并没有实现SortedSet接口,它的有序性主要依赖于LinkedHashMap的有序性,所以它的有序性是指按照插入顺序保证的有序性;
而TreeSet实现了SortedSet接口,它的有序性主要依赖于NavigableMap的有序性,而NavigableMap又继承自SortedMap,这个接口的有序性是指按照key的自然排序保证的有序性,而key的自然排序又有两种实现方式,一种是key实现Comparable接口,一种是构造方法传入Comparator比较器。

2、TreeSet里面真的是使用TreeMap来存储元素的吗?

通过源码分析我们知道TreeSet里面实际上是使用的NavigableMap来存储元素,虽然大部分时候这个map确实是TreeMap,但不是所有时候都是TreeMap。
因为有一个构造方法是TreeSet(NavigableMap<E,Object> m),而且这是一个非public方法,通过调用关系我们可以发现这个构造方法都是在自己类中使用的,比如下面这个:

//最终发现走到了Map接口
public NavigableSet<E> tailSet(E fromElement, boolean inclusive) {
    return new TreeSet<>(m.tailMap(fromElement, inclusive));
}

    NavigableMap<K,V> tailMap(K fromKey, boolean inclusive);

public interface NavigableMap<K,V> extends SortedMap<K,V>

public interface SortedMap<K,V> extends Map<K,V> {
//可以看到,这个类并没有继承TreeMap,不过通过源码分析也可以看出来这个类是组合了TreeMap,也算和TreeMap有点关系,只是不是继承关系。

//所以,TreeSet的底层不完全是使用TreeMap来实现的,更准确地说,应该是NavigableMap。

五、treeMap源码解析(了解)

1、继承体系

//SortedMap规定了元素可以按key的大小来遍历
public class TreeMap<K,V>
    extends AbstractMap<K,V>
    implements NavigableMap<K,V>, Cloneable, java.io.Serializable

2、属性

//比较器,如果没有传key 要实现Comparable接口
//按key的大小排序有两种方式,一种是key实现Comparable接口,一种方式通过构造方法传入比较器。
private final Comparator<? super K> comparator;
//根节点
//根节点,TreeMap没有桶的概念,所有的元素都存储在一颗树中。
private transient Entry<K,V> root;
//元素个数
private transient int size = 0;
//修改次数
private transient int modCount = 0;

3、主要内部类

//设置黑色为true
private static final boolean RED   = false;
private static final boolean BLACK = true;

//红黑树节点的定义
static final class Entry<K,V> implements Map.Entry<K,V> {
    K key;
    V value;
    Entry<K,V> left;
    Entry<K,V> right;
    Entry<K,V> parent;
    boolean color = BLACK;

    Entry(K key, V value, Entry<K,V> parent) {
        this.key = key;
        this.value = value;
        this.parent = parent;
    }

   
    public K getKey() {
        return key;
    }


    public V getValue() {
        return value;
    }


    public V setValue(V value) {
        V oldValue = this.value;
        this.value = value;
        return oldValue;
    }

    public boolean equals(Object o) {
        if (!(o instanceof Map.Entry))
            return false;
        Map.Entry<?,?> e = (Map.Entry<?,?>)o;

        return valEquals(key,e.getKey()) && valEquals(value,e.getValue());
    }

    public int hashCode() {
        int keyHash = (key==null ? 0 : key.hashCode());
        int valueHash = (value==null ? 0 : value.hashCode());
        return keyHash ^ valueHash;
    }

    public String toString() {
        return key + "=" + value;
    }
}

4、构造器

//默认构造方法,key必须实现Comparable接口
public TreeMap() {
    comparator = null;
}
//使用传入的comparator比较
public TreeMap(Comparator<? super K> comparator) {
    this.comparator = comparator;
}

//key必须实现Comparable接口,把传入map中的所有元素保存到新的TreeMap中 
public TreeMap(Map<? extends K, ? extends V> m) {
    comparator = null;
    putAll(m);
}
//使用传入map的比较器,并把传入map中的所有元素保存到新的TreeMap中
public TreeMap(SortedMap<K, ? extends V> m) {
    comparator = m.comparator();
    try {
        buildFromSorted(m.size(), m.entrySet().iterator(), null, null);
    } catch (java.io.IOException cannotHappen) {
    } catch (ClassNotFoundException cannotHappen) {
    }
}

5、总结

(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点(NIL)是黑色。(注意:这里叶子节点,是指为空(NIL或NULL)的叶子节点!)
(4)如果一个节点是红色的,则它的子节点必须是黑色的。
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

除了上述这些标准的红黑树的特性,你还能讲出来哪些TreeMap的特性呢?
(1)TreeMap的存储结构只有一颗红黑树;
(2)TreeMap中的元素是有序的,按key的顺序排列;
(3)TreeMap比HashMap要慢一些,因为HashMap前面还做了一层桶,寻找元素要快很多;
(4)TreeMap没有扩容的概念;
(5)TreeMap的遍历不是采用传统的递归式遍历;
(6)TreeMap可以按范围查找元素,查找最近的元素;

更多见:https://www.cnblogs.com/tong-yuan/p/10651637.html

六、hashMap源码解析(理解&重要)

1、继承体系

//继承自AbstractMap,实现了Map接口,具有Map同用方法
//实现了Cloneable接口,可以克隆----
//实现了Serializable接口,可被序列化  ----这两个接口都是声明式接口,无需实现任何方法 
public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {

2、主要属性

//默认初始化容量--16
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

//最大容量,位运算
static final int MAXIMUM_CAPACITY = 1 << 30;

//默认的加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;

//树化门槛(精准翻译哈哈)默认8
static final int TREEIFY_THRESHOLD = 8;

//反树化(树 -> 链表)的门槛 ,默认 6
static final int UNTREEIFY_THRESHOLD = 6;

//最小树化容量(数组的长度到达64才会对满足条件的链表进行树化) 默认64
static final int MIN_TREEIFY_CAPACITY = 64;

//数组,又称为 哈希桶
transient Node<K,V>[] table;

//作为entryset的缓存
transient Set<Map.Entry<K,V>> entrySet;

//元素个数
transient int size;

//修改次数,在使用迭代器时执行快速失败策略(也就是非法增删改时会导致并发修改异常) 
transient int modCount;

//下一个达到扩容条件的门槛
int threshold;

//非默认的加载因子(我们手动指定加载因子的时候才会用到)  
final float loadFactor;   

3、重要内部类(还是一样,static —关键字的一种用法)

好处:
1、简化代码结构,不用另写一个类,同时静态内部类可以访问静态成员变量(private 修饰的也可以访问)
2、提高了初始化优先级,随着类加载而加载

//单链表节点,hash用来存储key计算得来的hash值
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    V value;
    Node<K,V> next;
}

//树节点
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
}

4、构造方法

//空参默认构造方法
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

//指定自定义初始容量
public HashMap(int initialCapacity) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR);
}


//使用自定义初始容量,自定义初始加载因子
//1、判断自定义初始容量是否小于0,是就抛异常
//2、判断自定义初始容量是否大于最大容量,是就将最大值赋值给它(自定义就能无限大了吗?想屁吃)
//3、判断自定义加载因子是否小于等于0或者是一个NaN(not a number,isNaN :判断传入值是 NaN, 那么 isNaN 函数返回 true ,否则返回 false)
public HashMap(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal initial capacity: " +
                                           initialCapacity);
    if (initialCapacity > MAXIMUM_CAPACITY)
        initialCapacity = MAXIMUM_CAPACITY;
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal load factor: " +
                                           loadFactor);
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

//将map集合元素添加
public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

5、常用方法

1、Put

//首先根据key计算出hash值
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
//判断key是否为空,是就返回0,否则(h.hashCode()) 跟(h 无符号右移16位)进行 异或
//据说是:这样做是为了使计算出的hash更分散
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
//判断哈希桶的数量是否为0,是的话初始化---第一次resize
    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;
//若是待插入元素key==桶中第一个元素key,将带插入元素保存起来,用于后续修改value值
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
//若第一个元素是树节点,就调用树节点的putTreeVal插入元素
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
//遍历这个桶对应的链表,binCount用于存储链表中元素的个数
            for (int binCount = 0; ; ++binCount) {
//如果链表遍历完了都没有找到相同key的元素,说明该key对应的元素不存在,则在链表最后插入一个新节点
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
//如果插入新节点后链表长度大于8,则判断是否需要树化,因为第一个元素没有加到binCount中,所以这里-1
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st time
                        treeifyBin(tab, hash);
                    break;
                }
//如果待插入的key在链表中找到了,则退出循环
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
// 如果找到了对应key的元素
        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;
}

(1)计算key的hash值;
(2)如果桶(数组)数量为0,则初始化桶;
(3)如果key所在的桶没有元素,则直接插入;
(4)如果key所在的桶中的第一个元素的key与待插入的key相同,说明找到了元素,转后续流程(9)处理;
(5)如果第一个元素是树节点,则调用树节点的putTreeVal()寻找元素或插入树节点;
(6)如果不是以上三种情况,则遍历桶对应的链表查找key是否存在于链表中;
(7)如果找到了对应key的元素,则转后续流程(9)处理;
(8)如果没找到对应key的元素,则在链表最后插入一个新节点并判断是否需要树化;
(9)如果找到了对应key的元素,则判断是否需要替换旧值,并直接返回旧值;
(10)如果插入了元素,则数量加1并判断是否需要扩容;

2、resize

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    if (oldCap > 0) {
//若旧容量大于等于最大值,不再扩容
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
//若旧容量的2倍小于最大容量,且旧容量大于等于16
//旧容量扩大为原来2倍,旧的扩容门槛增大为原来2倍
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    else if (oldThr > 0) // initial capacity was placed in threshold
//使用非默认构造方法,第一次插入元素会到这里
//如果旧容量为0且旧扩容门槛大于0,则把新容量赋值为旧门槛说明还未初始化过,则初始化容量为默认容量,扩容门槛为默认容量*默认装载因子
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
//使用默认空参构造方法,第一次插入元素会到这里
//如果旧容量为0且旧扩容门槛大于0,说明还未初始化过,则初始化容量为默认容量,扩容门槛为默认容量*默认装载因子
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { // preserve order
                    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);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

(1)如果使用是默认构造方法,则第一次插入元素时初始化为默认值,容量为16,扩容门槛为12
(2)如果使用的是非默认构造方法,则第一次插入元素时初始化容量等于扩容门槛,扩容门槛在构造方法里等于传入容量向上最近的2的n次方;
(3)如果旧容量大于0,则新容量等于旧容量的2倍,但不超过最大容量2的30次方,新扩容门槛为旧扩容门槛的2倍;
(4)创建一个新容量的桶;
(5)搬移元素,原链表分化成两个链表,低位链表存储在原来桶的位置,高位链表搬移到原来桶的位置加旧容量的位置;

3、treeify —树化方法

----判断是否需要进行树化

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
// 如果桶数量小于64,直接扩容而不用树化
// 因为扩容之后,链表会分化成两个链表,达到减少元素的作用
// 当然也不一定,比如容量为4,里面存的全是除以4余数等于3的元素这样即使扩容也无法减少链表的长度
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        TreeNode<K,V> hd = null, tl = null;
        do {
            TreeNode<K,V> p = replacementTreeNode(e, null);
            if (tl == null)
                hd = p;
            else {
                p.prev = tl;
                tl.next = p;
            }
            tl = p;
        } while ((e = e.next) != null);
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

真正的树化方法

final void treeify(Node<K,V>[] tab) {
    TreeNode<K,V> root = null;
    for (TreeNode<K,V> x = this, next; x != null; x = next) {
        next = (TreeNode<K,V>)x.next;
        x.left = x.right = null;
        if (root == null) {
            x.parent = null;
            x.red = false;
            root = x;
        }
        else {
            K k = x.key;
            int h = x.hash;
            Class<?> kc = null;
            for (TreeNode<K,V> p = root;;) {
                int dir, ph;
                K pk = p.key;
                if ((ph = p.hash) > h)
                    dir = -1;
                else if (ph < h)
                    dir = 1;
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0)
                    dir = tieBreakOrder(k, pk);

                TreeNode<K,V> xp = p;
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    x.parent = xp;
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    root = balanceInsertion(root, x);
                    break;
                }
            }
        }
    }
    moveRootToFront(tab, root);
}

(1)从链表的第一个元素开始遍历;
(2)将第一个元素作为根节点;
(3)其它元素依次插入到红黑树中,再做平衡;
(4)将根节点移到链表第一元素的位置(因为平衡的时候根节点会改变);

6、总结

(1)HashMap是一种散列表,采用(数组 + 链表 + 红黑树)的存储结构;
(2)HashMap的默认初始容量为16(1<<4),默认装载因子为0.75f,容量总是2的n次方;
(3)HashMap扩容时每次容量变为原来的两倍;
(4)当桶的数量小于64时不会进行树化,只会扩容;
(5)当桶的数量大于64且单个桶中元素的数量大于8时,进行树化;
(6)当单个桶中元素数量小于6时,进行反树化;
(7)HashMap是非线程安全的容器;
(8)HashMap查找添加元素的时间复杂度都为O(1);

其他

1、list.foreach

其实就是增强for循环,和for循环的区别在于,它对索引的边界值只会计算一次。所以在foreach中对集合进行添加或删掉会导致错误,抛出异常java.util.ConcurrentModificationException
因为他的指针只能前进不能后退!!!

@Override
public void forEach(Consumer<? super E> action) {
    Objects.requireNonNull(action);
    final int expectedModCount = modCount;
    @SuppressWarnings("unchecked")
    final E[] elementData = (E[]) this.elementData;
    final int size = this.size;
    for (int i=0; modCount == expectedModCount && i < size; i++) {
        action.accept(elementData[i]);
    }
    if (modCount != expectedModCount) {
        throw new ConcurrentModificationException();
    }
}
//函数式接口,有参数传入没有参数返回,典型的  消费型函数式接口
@FunctionalInterface
public interface Consumer<T> {

    void accept(T t);
  • 如果只是遍历集合或者数组,用foreach好些,快些。
  • 如果对集合中的值进行修改,确定循环次数就要用for循环了。

2、arrays.copyof

arrays.copyof(被复制数组,要复制的元素个数) 当要复制的元素个数大于被复制数组长度时,会自动添加数组的默认数据类型。

String[] str2 = {"1","2","3","4","5","6","7","8","9"};
String[] arr1 = Arrays.copyOf(str2, 3);
String[] arr2 = Arrays.copyOf(str2, 10);
System.out.println(Arrays.toString(arr1));
System.out.println(Arrays.toString(arr2));

[1, 2, 3]
[1, 2, 3, 4, 5, 6, 7, 8, 9, null]

参考

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值