Java集合章节笔记

集合体系图

在这里插入图片描述

在这里插入图片描述

Collection常用方法

1)add:添加单个元素
2)remove:删除指定元素
3)contains:查找元素是否存在
4)size:获取元素个数
5)isEmpty:判断是否为空
6)clear:清空
7)addAll::添加多个元素
8)containsAll::查找多个元素是否都存在
9)removeAll:删除多个元素

Interator使用

Iterator属于Collection接口的方法,只要实现了Collection接口就能用Iterator

快捷键 itit 直接生成带 while 的循环

Collection arrayList = new ArrayList();
arrayList.add("asd");
arrayList.add("as2d");
arrayList.add("a3sd");
Iterator iterator = arrayList.iterator();
while (iterator.hasNext()) {
    Object next = iterator.next();
    System.out.println("next = " + next);
}

Iterator迭代一遍后next的位置在最后一个元素上,如果想要再重新迭代需要再用arrayList.iterator();方法生成一个新的Iterator

增强for

增强for是简化版的迭代器,底层也使用了Iterator

快捷键 I (大 i)或 arrayList.iter 或 arrayList.for直接 iter

List

List有序,可重复,可用索引取元素

  • public void add(int index, E element) 在index位置插入元素
  • public boolean addAll(int index, Collection<? extends E> c)与上同理
  • public E get(int index)
  • public int indexOf(Object o)元素第一次出现的位置(即使有多个元素)
  • public int lastIndexOf(Object o)元素最后一次出现的位置(即使有多个元素)
  • public E remove(int index)
  • public E set(int index, E element)
  • public List subList(int fromIndex, int toIndex)截取从fromIndex到toIndex的元素,左闭右开

遍历方式有三种:iterator、增强for、普通for

ArrayList

ArrayList基本等同于Vector,除了ArrayList是线程不安全(执行效率高)看源码。在多线程情况下,不建议使用ArrayList

1)ArrayListE中维护了一个Object类型的数组elementData.[debug看源码]
transient Object[]elementData;//transient:表示瞬间,短暂的,表示该属性不会被序列化
2)当创建ArrayListi对象时,如果使用的是无参构造器,则初始elementData容量为0,第1次添加,则扩容elementData为10,如需要再次扩容,则扩容elementData为1.5倍。
3)如果使用的是指定大小的构造器,则初始elementData容量为指定大小,如果需要扩容,
则直接扩容elementData为1.5倍。
老师建议:自己去debug一把我们的ArrayList的创建和扩容的流程,

源码剖析

IDEA会简化数组显示,为方便debug要进行设置:取消掉 启动集合类的替代视图

在这里插入图片描述

Idea会自动跳过java包下的源代码,需要设置

在这里插入图片描述

源码流程图:

在这里插入图片描述

Vector

底层也是一个elementDate数组Object类型

源码剖析

Vector的源码也需要自己debug走一遍

初始化:

//1、空构造器,默认初始化大小为10
public Vector() {
    this(10);
}
//2、构造器
public Vector(int initialCapacity) {
    this(initialCapacity, 0);//这里的0是控制扩容幅度,默认是扩原来的2倍,如果这里赋值就扩这里的值的大小
}
//3、构造器
public Vector(int initialCapacity, int capacityIncrement) {
    super();
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    this.elementData = new Object[initialCapacity];//重点在这句,反应底层是维护数组
    this.capacityIncrement = capacityIncrement;
}

add添加函数:

//1、基本与ArrayList相同
public synchronized boolean add(E e) {
    modCount++;//记录修改次数,与多线程相关
    ensureCapacityHelper(elementCount + 1);//确定是否扩容
    elementData[elementCount++] = e;
    return true;
}
//2、确定是否扩容
private void ensureCapacityHelper(int minCapacity) {
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}
//3、确定扩容大小
private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    //这里如果初始化时给了默认扩容增量就用它,如果没有就扩容增加一倍
    int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                     capacityIncrement : oldCapacity);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    elementData = Arrays.copyOf(elementData, newCapacity);
}

ArrayList和Vector比较

在这里插入图片描述

LinkedList

1)LinkedList底层实现了双向链表和双端队列特点
2)可以添加任意元素(元素可以重复),包括null
3)线程不安全,没有实现同步

源码剖析

1)LinkedList底层维护了一个双向链表.
2)LinkedList中维护了两个属性first和last分别指向首节点和尾节点
3)每个节点(Node对象),里面又维护了prev、next、item三个属性,其中通过prev指向前一个,通过next指向后一个节点。最终实现双向链表.
4)所以LinkedListl的元素的添加和删除,不是通过数组完成的,相对来说效率较高。

add方法:

//主要添加过程就是:l=last; last=新结点; l.next=新节点
void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}

remove方法:

private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    final E element = f.item;
    final Node<E> next = f.next;
    f.item = null;
    f.next = null; // help GC (*这里这样处理后GC能更好的清空那个被删除的元素)
    first = next;
    if (next == null)
        last = null; //此时为空list了
    else
        next.prev = null; //如果next为空的话就无法访问prev
    size--;
    modCount++;
    return element;
}
ArrayListi和LinkedList比较

在这里插入图片描述

如何选择ArrayListi和LinkedList:
1)如果我们改查的操作多,选择ArrayList
2)如果我们增删的操作多,选择LinkedList
3)一般来说,在程序中,80%-90%都是查询,因此大部分情况下会选择ArrayList
4)在一个项目中,根据业务灵活选择,也可能这样,一个模块使用的是ArrayList,另外一个模块是LinkedList,也就是说,要根据业务来进行选择

Set

1)无序(添加和取出的顺序不一致,但每次取出的顺序是固定的,只是不按照添加的顺序来,底层是数组加链表)没有索引
2)不允许重复元素,所以最多包含一个null

遍历方法有两种:迭代器和增强for,无法使用普通for

HashSet

  • HashSet 基于 HashMap 来实现的,是一个不允许有重复元素的集合。
  • HashSet 允许有 null 值。
  • HashSet 是无序的,即不会记录插入的顺序。
  • HashSet 不是线程安全的, 如果多个线程尝试同时修改 HashSet,则最终结果是不确定的。 您必须在多线程访问时显式同步对 HashSet 的并发访问。

HashSet实际上是HashMap,看下面源码

public HashSet() {
    map = new HashMap<>();
}

HashSet不保证元素是有序的,取决于hash后,再确定索引的结果

源码剖析

分析HashSet底层是HashMap,HashMap底层是(数组+链表+红黑树),即数组每个元素存一个链表,后期随着元素增多链表会换成红黑树

先给一个经典面试题的坑

HashSet hashSet = new HashSet();
hashSet.add(new HashSetDemo());//加入成功
hashSet.add(new HashSetDemo());//加入成功
hashSet.add(new String("abc"));//加入成功
hashSet.add(new String("abc"));//加入失败,原因看add方法底层机制
System.out.println("hashSet = " + hashSet);

先说结论:

1.HashSet底层是HashMap
2.添加一个元素时,先得到hash值会转成->数组的索引值
3.找到存储数据表table,看这个索引位置是否已经存放的有元素
4.如果没有,直接存放
5.如果有,调用equals比较该条链表每一个元素(*因为equals来自Object类,所以不同类的equals可以自行编写),如果相同,就放弃添加,如果不相同,则添加到最后
6.在Java8中,如果一条链表的元素个数到达TREEIFY THRESHOLD(默认是8),并且tablel的大小>=MIN TREEIFY CAPACITY(默认64)( *table不够64的话会先扩容,一般是2倍),( *注意是两个条件),就会进行树化(红黑树)

初始化:

public HashSet() {
    map = new HashMap<>();
}

add方法:

//1、HashSet:add
public boolean add(E e) {
    //private static final Object PRESENT = new Object();
    //简而言之:PRESENT的作用是帮助判断e是否为不重复的新值,最终是否加入成功
    //PRESENT使得传入的对象e的所以对应的value值都是Object类型。
    //因为map是键值对形式的,而set不是。所以PRESENT无实际意义,是占位符,可以帮助判断是否为新加入的对象。
    //map也保证了作为key的e不能重复
    //HashMap#put() 方法返回的是上一次以同一 key 加入的 value(*此时加入失败,key不能重复),若从未以该 key 加入任何数据,则返回 null
    //因为上面所说,所以PRESENT不能使用null代替,因为这样就无法判断 null 究竟意味着这个 key 是第一次加入还是上一次使用了 null 作为 value 加入
    //传入的e是作为map的key,而value是固定的PRESENT的new Object()
    return map.put(e, PRESENT)==null;
}

//2、HashMap:put
public V put(K key, V value) {
    //为了进入hash方法,debug时要用红色的force setp into
    return putVal(hash(key), key, value, false, true);
}

//3、HashMap:hash
static final int hash(Object key) {
    int h;
    //>>>表示无符号右移,正数负数都是高位补零
    //^ 异或操作:相同为0,不相同为1,可以理解为不进位相加
    //右移16位是为了让高16位也参与运算,可以更好的均匀散列,减少碰撞,进一步降低hash冲突的几率
    //异或运算是为了更好保留两组32位二进制数中各自的特征
    //简而言之:hash方法是为了打散散列表减少冲突
    //面试官问:set和map用到的哈希值是Object原生hashcode吗,一定要回答不是
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

//4、HashMap:putVal
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //table为哈希表数组:transient Node<K,V>[] table;
    if ((tab = table) == null || (n = tab.length) == 0)
        //resize的作用是管理table哈希表的长度,初始化长度(为16)和决定扩容长度(为2倍)
        n = (tab = resize()).length;
    //一、i = (n - 1) & hash:根据key的hash值获取key在哈希表(其中tab=table)中的位置,然后赋给i
    //再将tab中在i位置的元素赋给p
    //注意:这里的(n-1)&hash = hash % n 是取余的意思。此公式的前提是n为2的幂次,如2、4、6、8...。
    //(n-1)&hash等价于hash&(n-1)。与运算规则:0&0=0;0&1=0;1&0=0;1&1=1
    //上面取余公式的目的是:将hash值对应到数组的下标范围内,同时与运算速度快于直接用%取余运算。
    //Hash 值的范围值-2147483648到2147483647,即为int的范围,前后加起来大概40亿的映射空间。
    //简而言之:取到key生成的hash值对应到哈希表数组的位置上的元素,判断该元素是否为空
    if ((p = tab[i = (n - 1) & hash]) == null)
        //如果为空就在哈希表数组中的该位置赋上新建Node结点
        //hash为key的hash,以后判断相同时会用;key就是要插入的对象;value为PRESENT占位对象;null该节点的next结点。
        tab[i] = newNode(hash, key, value, null);
    else {
        //局部使用的辅助变量就在局部定义
        Node<K,V> e; K k;
        //将发生冲突的两个对象(位于哈希表数组的该索引下标上的链表的第一个元素 和 准备插入的元素)进行比较
        //比较hash值和key。这里的key也用equals比较了,所以相同内容的new String()也认为相等
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        //判断p是否为红黑树,是的话就用红黑树方法去插入
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        //上面已经判断与该条链表的一个元素bu'x't在链表里逐个比较
        else {
            for (int binCount = 0; ; ++binCount) {
                //如果到最后都没有相同元素,就新建Node结点插在后面,此时e=null
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    //TREEIFY_THRESHOLD = 8;
                    //binCount从0开始,此时不算位于哈希表数组的链表的第一个元素,总共为8个元素
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        //链表元素大于等于8个,就把链表树化
                        //在treeifyBin方法中会先判断哈希表数组长度是否到达64,如果没有的话会先扩容哈希表,而不会去树化
                        treeifyBin(tab, hash);
                    break;
                }
                //如果找到相同元素直接退出循环,此时e在上面的if语言已经被赋值,值为相同元素Node结点
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        //如果发现相同元素,即插入失败
        if (e != null) { // existing mapping for key
            //value就是PRESENT
            V oldValue = e.value;
            //onlyIfAbsent为false
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            //空方法
            afterNodeAccess(e);	
            //返回的是PRESENT
            return oldValue;
        }
    }
    //记录修改次数
    ++modCount;
    //如果哈希表内所有元素个数(注意包括链表里的)超过阈值就扩容
    if (++size > threshold)
        resize();
    //该方法是留给HashMap的子类如LinkedHashMap去使用的。此处是空方法
    afterNodeInsertion(evict);
    //如果插入的是不重复的新元素,yi
    return null;
}

//5、HashMap: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;
        }
        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
        newCap = oldThr;
    else {               // zero initial threshold signifies using defaults
        //在此处初始化table长度,默认是16,默认临界值时12
        //static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
        //static final float DEFAULT_LOAD_FACTOR = 0.75f;
        newCap = DEFAULT_INITIAL_CAPACITY;//16
        //newThr得到的值时12,表示到12就要开始扩容,不能等到16才开始扩容,防止多线程访问造成溢出、卡死等,所以留了4个元素作为缓冲层
        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"})
    //此处创建了数组长度为16
    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;
}
重写equals和hashcode方法

当在HashSet插入对象时,即把对象作为HashMap的key,可以重写equals和hashcode方法使两个地址不同内容相同的对象相等,防止重复插入。
需要重写equals方法的类,都需要重写hashcode方法

重写方法一般就是把所有成员变量的hashcode累加

  • 基本数据类型,大家可以参考其对应的包装类型的hashcode方法

  • 引用类型则直接调用hashcode()

  • 数组类型则需要遍历数组,依次调用hashcode()

IDEA可以自动生成重写方法

注意:在HashSet插入时,先看hashcode是否相同(找索引),再比较equals是否相同。所以equals比hashcode更精确。
equals相同hashcode一定相同,反之hashcode相同equals不一定相同。精确度:equals > hashcode

IDEA自动生成的hashCode方法源码

//1、自定义对象:hashCode
public int hashCode() {
    return Objects.hash(name, age);
}

//2、Objects:hash
public static int hash(Object... values) {
    return Arrays.hashCode(values);
}

//3、Arrays:hashCode
public static int hashCode(Object a[]) {
    if (a == null)
        return 0;

    int result = 1;

    for (Object element : a)
        //这里的因子31是科学家推论出来的,防止碰撞。
        result = 31 * result + (element == null ? 0 : element.hashCode());

    return result;
}

LinkedHashSet

  1. LinkedHashSet是HashSet的子类
  2. LinkedHashSet底层是一个LinkedHashMap,底层维护了一个数组+双向链表
  3. LinkedHashSet有序,它根据元素的hashCode值来决定元素的存储位置,同时使用链表维护元素的次序(图),这使得元素看起来是以插入顺序保存的。
  4. LinkedHashSet不允许添重复元素
源码剖析

初始化

//1、LinkedHashSet:LinkedHashSet
public LinkedHashSet() {
    //初始化容积是16,与HashSet相同
    super(16, .75f, true);
}
//2、HashSet:HashSet
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
    //此处使底层为LinkedHashMap,而LinkedHashMap是HashMap的子类,debug时会进入HashMap类的源代码,但根据动态绑定机制会走LinkedHashMap的某些方法。
    map = new LinkedHashMap<>(initialCapacity, loadFactor);
}

在add过程中一些在LinkedHashMap中的方法。

//1、LinkedHashMap:newNode
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    LinkedHashMap.Entry<K,V> p =
        new LinkedHashMap.Entry<K,V>(hash, key, value, e);
    //插入之后变为最后的结点
    linkNodeLast(p);
    return p;
}
//2、LinkedHashMap:Entry
//通过HashMap.Node的引用方式可以看出,Node是HashMap的内部类且是静态的。只有静态内部类才能继承静态内部类。
static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after;//与Node相比就是多了两个前后指针
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}
//3、LinkedHashMap:linkNodeLast
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    LinkedHashMap.Entry<K,V> last = tail;
    tail = p;
    if (last == null)
        head = p;
    else {
        //更新了双向链表
        p.before = last;
        last.after = p;
    }
}
//4、LinkedHashMap:afterNodeInsertion
void afterNodeInsertion(boolean evict) { // possibly remove eldest
    LinkedHashMap.Entry<K,V> first;
    if (evict && (first = head) != null && removeEldestEntry(first)) {
        K key = first.key;
        removeNode(hash(key), key, null, false, true);
    }
}

总结:LinkedHashMap的区别就在于结点升级为双向链表,其他添加机制扩容机制都是相同的。

Map

  1. 如果key重复,put之后会替换原先的元素

  2. null可以为key,但Map中只能有一个key为null

  3. 常用string最为key

  4. 一对k-v是放在一个HashMap$Node中的,又因为Node实现了Entry接口,有些书上也说一对k-v就是一个Entry。
    为了方便程序员遍历,会创建EntrySet存放key-value。

    //HashMap中有一个内部类叫EntrySet ,它属于Set,存放Entey,Entey存放key和value
    final class EntrySet extends AbstractSet<Map.Entry<K,V>> {}
    //Node是Entey接口的实现子类,实际运行中其实是Node存放在EntrySet中
    static class Node<K,V> implements Map.Entry<K,V> {}
    //Entry是接口,是Map接口中的内部接口。它声明了getKey和getValue方法,所以能方便取值遍历
    interface Entry<K,V> {}
    

    除了EntrySet,还有KeySet(属于Set)和Values(属于Collection)内部类,分别专门存放key和value。返回这三者分别使用的方法为entrySet、keySet、values。

    上面三者存放的key或value与对应的Node存放的key或value指向的都是同一个对象,而不是拷贝。即真正的k-v对象都存放在Node中,哈希表中是存Node,EntrySet等三个存的是Node的父类Entry,但实际都是Node

Map的遍历

对于keySet
因为是set,所以有两种方式遍历:增强for和迭代器

对于values
也只有两种方式遍历:增强for和迭代器

对于entrySet
也只有两种方式遍历:增强for和迭代器。
此时要把遍历出来的对象转化为Map.Entry类型,才能调用getKey和getValue方法。注意不能转化为HashMap.Node,因为Node不是public的

HashMap

1)Map接口的常用实现类:HashMap、Hashtable和Properties。
2)HashMap是Map接口使用频率最高的实现类。
3)HashMap是以key-val对的方式来存储数据(HashMap$Node类型)[案例Entry]
4)key不能重复,但是值可以重复,允许使用null键和null值。
5)如果添加相同的key,则会覆盖原来的key-val,等同于修改.(key不会替换,val会替换)
6)与HashSet一样,不保证映射的顺序,因为底层是以hash表的方式来存储的.(Gjdk8的hashMap底层数组+链表+红黑树)
7)HashMap没有实现同步,因此是线程不安全的,方法没有做同步互斥的操作,没有synchronized

源码剖析

初始化

public HashMap() {
    //static final float DEFAULT_LOAD_FACTOR = 0.75f;
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}

put方法

public V put(K key, V value) {
    //hash方法内容同HashSet相同
    return putVal(hash(key), key, value, false, true);
}

putVal方法

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)
        //第一步先创建table表
        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;
        //注意hash是key的hash,不是key+value的hash。
        //只要key相同就不再添加新结点
        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)
                //这里是修改原先相同key结点的value值,key是原来的key,只修改value
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

扩容机制总结,和HashSet相同

1)HashMap底层维护了Node类型的数组table,默认为null
2)当创建对象时,将加载因子(loadfactor)初始化为0.75
3)当添加key-val时,通过key的哈希值得到在table的索引l。然后判断该索引处是否有元素,如果没有元素直接添加。如果该索引处有元素,继续判断该元素的key和准备加入的key相是否等,如果相等,则直接替换vl;如果不相等需要判断是树结构还是链表结构,做出相应处理。如果添加时发现容量不够,则需要扩容。
4)第1次添加,则需要扩容table容量为16,临界值(threshold)为12(16*0.75)
5)以后再扩容,则需要扩容table容量为原来的2倍(32),临界值为原来的2倍,即24,依次类推
6)在ava8中,如果一条链表的元素个数超过TREEIFY THRESHOLD(默认是8),并且tablel的大小>=MIN TREEIFY CAPACITY(默认64),就会进行树化(红黑树)

HashTable

1)存放的元素是键值对:即K-V
2)hashtablel的键和值都不能为null,否则会抛出NullPointerException
3)hashTable使用方法基本上和HashMap一样
4)hashTable是线程安全的(synchronized),hashMap是线程不安全的

源码剖析

HashTable的继承结构,除了实现了Map,还继承了字典Dictionary,该字典类已被废弃,而转用Map

public class Hashtable<K,V>
    extends Dictionary<K,V>
    implements Map<K,V>, Cloneable, java.io.Serializable {

初始化

底层HashTableEntry数组(实现了MapEntry数组(实现了MapEntry数组(实现了MapEntry)。初始化大小为11,临界因子也为0.75

public Hashtable() {
    //11为初始化数组容量,0.75为临界值因子,故临界值为8
    this(11, 0.75f);
}
//上面的this即这个构造器
public Hashtable(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal Load: "+loadFactor);

    if (initialCapacity==0)
        initialCapacity = 1;
    this.loadFactor = loadFactor;
    //初始化了Entry数组,大小为11
    table = new Entry<?,?>[initialCapacity];
    //MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    //临界值为8
    threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
}

put方法:主要处理key重复的情况,key不重复交给addEntry处理

public synchronized V put(K key, V value) {
    // Make sure the value is not null
    if (value == null) {
        //这里保证value值不能为null
        throw new NullPointerException();
    }

    // Makes sure the key is not already in the hashtable.
    Entry<?,?> tab[] = table;
    int hash = key.hashCode();
    //0x7FFFFFFF代表最大正整数(32位):0111 1111 1111 1111 1111 1111 1111 1111
    //取得数组下标
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    //找到头部链表节点:通过hash取出tab数组中位于该索引的头部entry
    Entry<K,V> entry = (Entry<K,V>)tab[index];
    //遍历该条链表
    for(; entry != null ; entry = entry.next) {
        //如果key完全相同则用新value替换旧value
        if ((entry.hash == hash) && entry.key.equals(key)) {
            V old = entry.value;
            entry.value = value;
            return old;
        }
    }
    //上面操作处理完了key相同的情况,下面进行真正的添加新元素
    addEntry(hash, key, value, index);
    return null;
}

addEntry方法:处理key不重复时,添加一个全新的元素

private void addEntry(int hash, K key, V value, int index) {
    modCount++;

    Entry<?,?> tab[] = table;
    //如果hashtable内的元素个数大于临界值就扩容
    if (count >= threshold) {
        // Rehash the table if the threshold is exceeded
        rehash();

        tab = table;
        hash = key.hashCode();
        index = (hash & 0x7FFFFFFF) % tab.length;
    }

    // Creates the new entry.
    @SuppressWarnings("unchecked")
    Entry<K,V> e = (Entry<K,V>) tab[index];
    //这里是头插法,新节点插在链表头部,即直接在数组上
    tab[index] = new Entry<>(hash, key, value, e);
    count++;
}

rehash方法:数组超过临界值扩容,扩容大小为原先容量乘以二再加一

protected void rehash() {
    int oldCapacity = table.length;
    Entry<?,?>[] oldMap = table;

    // overflow-conscious code
    //扩容方法为:乘以二再加一
    int newCapacity = (oldCapacity << 1) + 1;
    //超过最大容量的情况
    if (newCapacity - MAX_ARRAY_SIZE > 0) {
        if (oldCapacity == MAX_ARRAY_SIZE)
            // Keep running with MAX_ARRAY_SIZE buckets
            return;
        newCapacity = MAX_ARRAY_SIZE;
    }
    //扩容了
    Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];

    modCount++;
    threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    table = newMap;
    //将旧表数据导入新表
    for (int i = oldCapacity ; i-- > 0 ;) {
        for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
            Entry<K,V> e = old;
            old = old.next;

            int index = (e.hash & 0x7FFFFFFF) % newCapacity;
            e.next = (Entry<K,V>)newMap[index];
            newMap[index] = e;
        }
    }
}
Hashtable 和 HashMap 对比

在这里插入图片描述

Properties

  1. Properties类继承自Hashtable类并且实现了Map接口,也是使用一种键值对的形式来保存数据。
  2. 他的使用特点和Hashtable类似
    key和value不可以为null
    无序
  3. Properties还可以用于从xxx.properties文件中,加载数据到Properties类对象,并进行读取和修改
  4. 说明:工作后Xxx.properties文件通常作为配置文件,这个知识点在IO流举例,有兴趣可先看文章

TreeSet

treeset通过Comparator使列表有序

TreeSet判断相同的标准就是Comparator中重写compare方法的结果。compare返回int,只要compare判断结果为0,就判断为相同,就不会往里添加。返回负数就放在前面,返回正数就放在后面

底层是红黑树,红黑树属于平衡二叉查找树,但他比AVL平衡二叉树进行平衡的代价要小。红黑树的时间复杂度为: O(lgn)。

TreeSet treeSet = new TreeSet(new Comparator() {
    @Override
    public int compare(Object o1, Object o2) {
        return ((String)o1).compareTo((String) o2);
    }
});
源码剖析

初始化

//TreeSet
public TreeSet(TreeMapComparator<? super E> comparator) {
    //底层是TreeMap
    this(new TreeMap<>(comparator));
}
//上面this传到下面
TreeSet(NavigableMap<E,Object> m) {
    //public interface NavigableMap<K,V> extends SortedMap<K,V> {
    //public interface SortedMap<K,V> extends Map<K,V> {
    //总而言之NavigableMap继承了Map
    //private transient NavigableMap<E,Object> m;
    this.m = m;
}
//TreeMap
public TreeMap(Comparator<? super K> comparator) {
    this.comparator = comparator;
}

add方法

public boolean add(E e) {
    return m.put(e, PRESENT)==null;
}

put方法

public V put(K key, V value) {
    //可以发现TreeMap中的节点为Entry类型
    Entry<K,V> t = root;
    //root为空时,即为加入一个元素,要对root初始化
    if (t == null) {
        //检查一下key是否为空值,没有别的实际意义
        compare(key, key); // type (and possibly null) check

        root = new Entry<>(key, value, null);
        size = 1;
        modCount++;
        return null;
    }
    int cmp;
    Entry<K,V> parent;
    // split comparator and comparable paths
    //comparator为构造函数传入的匿名比较器
    Comparator<? super K> cpr = comparator;
    if (cpr != null) {
        do {
            parent = t;
            //!特别注意:这里调用的是构造器传入的比较器匿名类的compare方法	
            //重点在于:只要compare方法返回的int整数为0就被判断为相同,就不会把它添加到集合中
            cmp = cpr.compare(key, t.key);
            //比t小就和它的左孩子继续比较
            if (cmp < 0)
                t = t.left;
            //比t大就和它的右孩子继续比较
            else if (cmp > 0)
                t = t.right;
            else
                //key和t的key相同的话就把value赋给t的value
                //注意不动key,只动value
                return t.setValue(value);
        } while (t != null);
    }
    else {
        //这里保证key不能为null
        if (key == null)
            throw new NullPointerException();
        @SuppressWarnings("unchecked")
        //本身没有比较器,自己创建比较器
        //这里的key必须要实现了Comparable接口(该接口只包含一个compareTo方法,必须重写它),否则会报类型转换错误:key无法转换为Comparable
            Comparable<? super K> k = (Comparable<? super K>) key;
        do {
            parent = t;
            cmp = k.compareTo(t.key);
            if (cmp < 0)
                t = t.left;
            else if (cmp > 0)
                t = t.right;
            else
                return t.setValue(value);
        } while (t != null);
    }
    Entry<K,V> e = new Entry<>(key, value, parent);
    //cmp和parent已在上方得出,此时parent在cmp方向的孩子为空
    if (cmp < 0)
        parent.left = e;
    else
        parent.right = e;
    fixAfterInsertion(e);
    size++;
    modCount++;
    return null;
}

//compare方法
final int compare(Object k1, Object k2) {
    //如果创建treeset时构造器包含了比较器就用比较器,没有的话就把key转换成父类Comparable
    return comparator==null ? ((Comparable<? super K>)k1).compareTo((K)k2)
        : comparator.compare((K)k1, (K)k2);
}
试分析HashSet和TreeSet分别如何实现去重的

(1)HashSet的去重机制:hashCode0+equals(0,底层先通过存入对象,进行运算得到一个hash值,通过hash值得到对应的索引,如果发现table索引所在的位置,没有数据,就直接存放如果有数据,就进行equalsl比较[遍历比较],如果比较后,不相同,就加入,否则就不加入.
(2)TreeSet的去重机制:如果你传入了一个Comparator匿名对象,就使用实现的compare去重,如果方法返回0,就认为是相同的元素/数据,就不添加,如果你没有传入一个Comparator匿名对象,则以你添加的对象实现的Compareable接口的compareTo去重

开发中如何选择集合实现类

在这里插入图片描述

Collections工具类

1)reverse(List):反转List中元素的顺序
2)shuffle(List):对List集合元素进行随机排序
3)sort(List):根据元素的自然顺序对指定List集合元素按升序排序
4)sort(List,Comparator):根据指定的Comparator产生的顺序对List集合元素进行排序
5)swap(List,int,int):将指定list集合中的i处元素和j处元素进行交换

1)Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
2)Object max(Collection,Comparator):根据Comparator指定的顺序,返回给定集合中的最大元素
3)Object min(Collection)
4)Object min(Collection,Comparator)
5)int frequency(Collection,Object):返回指定集合中指定元素的出现次数
6)void copy(List dest,.List src):将src中的内容复制到dest中
7)boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换List对象的所有旧值

习题

已知:News类按照title和content重写了hashCode和equals方法,问下面代码输出什么?

HashSet hashSet = new HashSet();
News news1 = new News("t1", "AA");
News news2 = new News("t2", "BB");
hashSet.add(news1);
hashSet.add(news2);
news1.setContent("CC");
//因为重写了hashCode和equals方法,所以news1的hash值发生了改变,
//而在hashset中news1指向的对象还保存在根据原来hash值找到的位置,所以remove找不到对象无法删除
hashSet.remove(news1);
System.out.println("hashSet = " + hashSet);
//相当于hashSet.add(news1)。此时会根据修改后的hash值存在新的位置
hashSet.add(new News("t1","CC"));
System.out.println("hashSet = " + hashSet);
//存在t1修改前的位置,但因为equals不同所以会正常添加不会覆盖
hashSet.add(new News("t1","AA"));
System.out.println("hashSet = " + hashSet);

看源码感悟

学习新API的方法

(为自我总结)

先看类的结构图,分析不同类和接口之间的关系

再看公开的方法,怎么使用类和方法

最后看源码,分析底层具体如何实现

在一个方法内,使用的变量尽量保证是在方法内部定义的局部变量,比如要是有类成员变量,就先赋给方法内新建的一个同类型变量并取个简单易懂的名字。总的来说就是让整个方法内部更有逻辑性更清晰,方法内部局部变量的起名要易于联想到该变量在该方法内部的作用。

一个方法内部包含了一个小方法,这个小方法一般写在该大方法的上面

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值