【Java】——集合


在这里插入图片描述

集合的理解和好处

数组的不足:

  • 长度开始时必须制定,而且一旦制定,不能更改
  • 保存的必须为同一类型的元素
  • 使用数组进行增加 / 删除元素时 比较麻烦

集合的好处:

  • 可以动态保存任意多个对象,使用比较方便
  • 提供了一系列方便的操作对象的方法:add、remove、get等
  • 使用集合添加、删除元素 简洁了

Collection接口和常用方法

Collection接口实现类的特点:
public interface Collection<E> extends Iterable<E>

  • collection实现子类可以存放多个元素,每个元素可以是Object
  • 有些Collection的实现类,可以存放重复元素,有些不可以
  • 有些Collection的实现类,有些是有序的(List) , 有些不是有序的(Set)
  • Collection接口没有直接的实现子类,是通过它的子接口Set和List来实现的

Collection接口常用方法

  • add:添加单个元素
  • remove: 删除指定元素
  • contains : 查找元素是否存在
  • size:获取元素个数
  • isEmpty: 判断是否为空
  • clear :清空
  • addAll : 添加多个元素
  • containsAll : 查找多个元素是否存在
  • removeAll : 删除多个元素
Collection接口遍历元素方式

 1. 使用Iterator(迭代器)
Iterator iterator = coll.iterator(); //得到
 //hasNext(): 判断是否还有下一个元素
while(iterator.hasNext()){
//next()作用: 1.下移  2.将下移以后集合位置上的元素返回
 	System.out.println(iterator.next());
}
 //在调用iterator.next()方法之前必须要调用iterator.hasNext()进行检测。
 //若不调用,且下一条记录无效,直接调用it.next()会抛出NoSuchElementException异常

2. 用增强for循环,增强for循环的底层也是迭代器。可理解成简化版的迭代器
for(Object book: col){
	System.out.println("book="+book);
}

快捷键

  • 快速生成迭代器的while ==》 itit
  • 显示所有快捷键的快捷键 ctrl+j
  • 增强for的快捷键 ==》I (大写的 i ) 或者 集合.for

List

方法:

  • void add (int index, Object ele): 在index位置插入ele元素
  • boolean addAll (int index,Collection eles):从index位置开始将eles中的所有元素添加进来
  • Object get (int index) :获取指定Index位置的元素
  • int indexOf (Object obj) :返回obj在集合中首次出现的位置
  • int lastIndexOf (Object obj): 返回obj在当前集合中最后出现的位置
  • Object remove (int index): 移除指定index位置的元素,并返回此元素
  • Object set (int index ,Object ele): 设置指定Index位置的元素为ele ,相当于是替换
  • List subList (int fromIndex , int toIndex): 返回从fromIndex 到toIndex的子集合

List的三种遍历方式【ArrayList, LinkedList , Vector】

  1. 使用Iterator
    Iterator iter = col.iterator();
    while (iter.hasNext()){ Object o = iter.next();}
  2. 使用增强for
  3. 使用普通for
    for(int i=0; i<list.size();i++){
    Object obj = list.get(i);
    System.out.println(object);
    }

ArrayList

参考

ArrayList扩容机制

  • ArrayList中维护了一个Object类型的数组elementData
    transient Object[] elementData;// transient 表示瞬间,短暂的,表示该属性不会被序列化
  • 当创建ArrayList对象时,如果使用的是无参构造器,则初始elementData容量为0,第1次添加,则扩容elementData为10,如需要再次扩容,则扩容elementData为1.5倍(源码中是当前大小+当前大小右移一位)。
  • 如果使用的是指定大小的构造器,则初始elementData容量为指定大小,如果需要扩容,则直接扩容elementData为1.5倍。
  1. 当创建ArrayList对象时,如果使用的是无参构造器,则初始elementData容量为0,第1次添加,则扩容elementData为10,如需要再次扩容,则扩容elementData为1.5倍(源码中是当前大小+当前大小右移一位)。
ArrayList list = new ArrayList();
for(int i=1;i<=10;i++){
	list.add(i);
}

在这里插入图片描述
创建一个空的elementData数组={}

在这里插入图片描述

  • 先确定是否要扩容
  • 然后再执行 赋值

在这里插入图片描述

  • 该方法确定minCapacity
  • 第一次扩容为10

在这里插入图片描述

  1. modCount++ 记录集合被修改的次数
  2. 如果elementData的大小不够,就调用grow()去扩容

在这里插入图片描述

  • 真的扩容
  • 使用扩容机制来确定要扩大到多少
  • 第一次 newCapacity=10
  • 第二次及其以后,按照1.5倍扩容
  • 扩容使用的是 Arrays.copyof()
  1. 如果使用的是指定大小的构造器,则初始elementData容量为指定大小,如果需要扩容,则直接扩容elementData为1.5倍。

在这里插入图片描述

  • 创建了一个指定大小elementData数组
  • 如果是有参数的构造器,扩容机制
    (1)第一次扩容,就按照elementData的1.5倍扩容
    (2)整个指定的流程还是和前面讲的一样

Vector

参考

  • Vector底层也是一个对象数组,protected Object[] elementData
  • Vector是线程同步的,即线程安全,Vector类的操作方法带有synchronized
  • 在开发中,需要线程同步安全时,考虑使用Vector

ArrayList与Vector比较

  • ①不安全,效率高 ②安全,效率不高
  • 扩容倍数
    ① 如果有参构造1.5倍扩容;
    如果是无参构造:第一次10,从第二次开始按1.5倍扩容
    ② 如果是无参,默认10,满后,就按2倍扩容;
    如果指定大小,则每次直接按2倍扩
1. new Vector() 底层
    /**
     * 构造一个指定容量为10、自增容量为0的空vector。
     */
    public Vector() {
        this(10);
    }
2. vector.add(i)
	2.1 ----------------
	    /**
     * 添加指定元素到vector的末尾. 
     * @param e 被添加到vector的元素
     * @return true
     * @since 1.2
     */
    public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
    }
    2.2 确定是否需要扩容----------------
    /**
     * ensureCapacity()方法的unsynchronized实现。
     * ensureCapacity()是同步的,它可以调用本方法来扩容,而不用承受同步带来的消耗
     *
     * @see #ensureCapacity(int)
     */
    private void ensureCapacityHelper(int minCapacity) {
        // 如果至少需要的容量 > 数组缓冲区当前的长度,就进行扩容
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    2.3 如果需要的数组大小 不够用,就扩容----------------
    /**
    * 扩容,保证vector至少能存储minCapacity个元素。
    * 首次扩容时,newCapacity = oldCapacity + ((capacityIncrement > 0)     ?capacityIncrement : oldCapacity);即如果capacityIncrement>0,就加capacityIncrement,如果不是就增加一倍。
    * 如果第一次扩容后,容量还是小于minCapacity,就直接将容量增为minCapacity。
    * 
    * @param minCapacity 至少需要的容量
    */
    private void grow(int minCapacity) {
        // 获取当前数组的容量
        int oldCapacity = elementData.length;
        // 扩容。新的容量=当前容量+当前容量/2.即将当前容量增加一倍
        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);
    } 

LinkedList

参考

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

LinkedList底层结构

  • LinkedList底层维护了一个双向链表
  • LinkedLisr中维护了两个属性first和last分别指向 首节点和尾节点
  • 每个节点(Node对象),里面又维护了prev,next, item三个属性,其中通过prev指向前一个,通过next指向后一个节点,最终实现双向链表
  • 添加和删除元素效率高
  1. LinkedList的成员变量
public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
    /**
     * 链表节点的个数
     * 关键字transient,序列化对象的时候,这个属性就不会被序列化
     */
    transient int size = 0;

    /**
     * 链表首节点
     */
    transient Node<E> first;

    /**
     * 链表尾节点
     */
    transient Node<E> last;
}
  1. LinkedList的静态内部类
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;
        }
    }
  1. LinkedList 构造方法
/**
     * 无参构造器创建一个空链表
     */
    public LinkedList() {
    }

    /**
     * 先调用无参构造器,在通过addAll方法将元素加入到链表中
     */
    public LinkedList(Collection<? extends E> c) {
        this();
        addAll(c);
    }
  1. LinkedList添加元素的方法
  • 添加元素
public boolean add(E e) {
        linkLast(e);//将元素添加到链表尾部
        return true;
    }
void linkLast(E e) {
        //将最后一个元素赋值给l
        final Node<E> l = last;
        //创建一个新节点,新节点的前一个节点是尾节点,后一个节点为null
        final Node<E> newNode = new Node<>(l, e, null);
        //将新节点作为新的尾节点
        last = newNode;
        //判断链表是否为空链表
        if (l == null)
            //如果是空链表则将新节点作为新的首节点
            first = newNode;
        else
            //如果链表不为空则将原尾节点的next指向新节点
            l.next = newNode;
        //更新链表节点个数
        size++;
        //更新链表修改次数
        modCount++;
    }
  • 删除元素
//无参的删除方法默认删除头部节点,源码解析见:删除头节点
public E remove() {
        return removeFirst();
    }

public E removeFirst() {
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
        return unlinkFirst(f);
    }
private E unlinkFirst(Node<E> f) {
        //获取首节点的元素值
        final E element = f.item;
        //获取首节点的下一个节点
        final Node<E> next = f.next;
        //将首节点的元素置为空等待GC
        f.item = null;
        将首节点的next置为空等待GC
        f.next = null; // help GC
        //将原首节点的下一个节点作为新的头节点
        first = next;
        //如果原先链表中只有头部节点,那原先的头部节点即是头节点也是尾节点,头部节点删除后,头节点和尾         //节点都置为null
        if (next == null)
            last = null;
        else
            //新的头部节点的前一个节点为null
            next.prev = null;
        // 更新链表节点个数 
        size--;
         //更新链表修改次数
        modCount++;
        return element;
    }
  • LinkedList队列操作
/**
     *获取头节点的值
     */
    public E peek() {
        final Node<E> f = first;
        return (f == null) ? null : f.item;
    }

    /**
     * 获取头节点的值
     */
    public E element() {
        return getFirst();
    }
    public E getFirst() {
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
        return f.item;
    }

    /**
     * 删除头部节点
     */
    public E poll() {
        final Node<E> f = first;
        return (f == null) ? null : unlinkFirst(f);
    }
    /**
     * 在尾部添加元素
     */
 public boolean offer(E e) {
        return add(e);
    }

    // Deque operations
    /**
     * 在头部添加元素
     */
    public boolean offerFirst(E e) {
        addFirst(e);
        return true;
    }

    /**
     * 在尾部添加元素
     */
    public boolean offerLast(E e) {
        addLast(e);
        return true;
    }

    /**
     *获取头节点元素
     */
    public E peekFirst() {
        final Node<E> f = first;
        return (f == null) ? null : f.item;
     }

    /**
     * 获取尾节点元素
     */
    public E peekLast() {
        final Node<E> l = last;
        return (l == null) ? null : l.item;
    }

    /**
     *删除头部节点
     */
    public E pollFirst() {
        final Node<E> f = first;
        return (f == null) ? null : unlinkFirst(f);
    }

    /**
     *删除尾部节点
     */
    public E pollLast() {
        final Node<E> l = last;
        return (l == null) ? null : unlinkLast(l);
    }

    /**
     * 添加头部元素
     */
    public void push(E e) {
        addFirst(e);
    }

    /**
     * 删除头部元素
     */
    public E pop() {
        return removeFirst();
    }

Set

  • 无序(添加和去除的顺序不一致),没有索引
  • 不允许重复元素,所以最多包含一个null
  • JDK API中Set接口的常见实现类有:HashSet , TreeSet
  • 注意:取出的顺序虽然不是添加的顺序,但是固定的

Set接口和常用方法

  • Set接口的常用方法:
    和List接口一样,Set接口也是Collection的子接口。因此,常用方法和Collection接口一样
  • Set接口的遍历方式

同Collection的遍历方式一样

  1. 可以使用迭代器
  2. 增强for
  3. 不能使用索引的方式来获取

HashSet

参考

  • HashSet实现了Set接口
  • HashSet实际上是HashMap
    public HashSet() { map = new HashMap<>(); }
  • 可以存放null值,但是只有一个null
  • HashSet不保证元素是有序的,取决于hash后,再确定索引的结果
  • 不能有重复元素/对象
  • 在存储对象进HashSet时,对象重写equals方法一定要重写hashCode方法,不然就会出现两个对象里面的数据一模一样,但是都可以存储在HashSet集合中。因为如果没有重写HashCode方法,对象会使用Object父类的hashCode方法,而Object的HashCode方法是根据地址来计算的,一定不会一样

HashMap底层是(数组+链表+红黑树)

HashSet扩容机制

  • HashSet底层是HashMap
  • 添加一个元素时,先得到hash值-会转成->索引值
  • 找到存储数据表table,看这个索引位置是否已经存放有元素
  • 如果没有,直接加入
  • 如果有,调用equals比较,如果相同,就放弃添加,如果不相同,则添加到最后
  • 在Java8中,如果一条链表的元素个数达到TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树)。
向HashSet中添加元素的源码
1. 构造函数------------------
	/**
     * 构建一个空的set集合,其底层的HashMap实例使用默认的初始容量(16)和加载因子(0.75)。
     */
    public HashSet() {
        map = new HashMap<>();
    }
2. 执行 add()------------------
	/**
     * 将元素放到map中,key是要添加的元素e,value是final修饰的object对象。
     */
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
	/**
	 * HashMap的value值。在HashSet中就是个占位的,没有意义
	 */
	private static final Object PRESENT = new Object();
	
3. 执行 put()--------------------------
	public V put (K key,V value){
		return putVal(hash(key),ley,value,false,true);
	}
	//该方法会执行hash(key) 得到key对应的hash值
	static final int hash(Object key){
		int h;
		return (key==null) ? 0:(h=key.hashCode())^(h>>>16);	//右移16位是为了防止哈希冲突
	}

hash为什么要右移16位异或?

  • 为什么要右移16位?
    其实是为了减少碰撞,进一步降低hash冲突的几率。int类型的数值是4个字节的,右移16位异或可以同时保留高16位于低16位的特征
  • 为什么要异或运算?
    首先将高16位无符号右移16位与低十六位做异或运算。如果不这样做,而是直接做&运算那么高十六位所代表的部分特征就可能被丢失
    将高十六位无符号右移之后与低十六位做异或运算使得高十六位的特征与低十六位的特征进行了混合得到的新的数值中就高位与低位的信息都被保留了
    ,而在这里采用异或运算而不采用& ,| 运算的原因是
    异或运算能更好的保留各部分的特征,如果采用&运算计算出来的值会向0靠拢,采用|运算计算出来的值会向1靠拢
4. 执行 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;
    //tab就是HashMap的一个数组,类型是Node[]
    
    //if语句表示如果当前table 是Null,或者大小=0
    //就是第一次扩容,到16个空间
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //哈希值所对应的位置为空,代表不会产生冲突,直接赋值即可
    	//(1) 根据key,得到Hash 去计算该Key应该存放到table表的哪个索引位置,并把这个位置的对象,赋给p
    	//(2) 判断p是否为null
    		//(2.1)如果p为null , 表示还没有存放元素,就创建一个Node(key="java",value=PRESENT)
    		//(2.2) 就放在该位置 tab[i]
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
    	//一个开发技巧提示:在需要局部变量(辅助变量)时,再创建
        //产生哈希冲突
        Node<K,V> e; K k;
        //如果哈希值相等,并且key也相等,则直接覆盖值
        if (p.hash == hash //如果当前索引位置对应的链表的第一个元素和待添加key的hash值一样
        	//并且满足下面条件之一:
        		//① 准备加入的key 和 p指向的Node节点的key 是同一个对象
        		//② p指向的Node节点的key 的 equlas() 和准备加入的Key比较后相同
        	&& ((k = p.key) == key || (key != null && key.equals(k))))// 比较key的两种方式,引用相同或equals返回true,都表示一个Key,前提是hash相同。
            e = p;
        //p为红黑树 使用红黑树逻辑进行添加(可以查看TreeMap)
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        //p为链表
        else {
            for (int binCount = 0; ; ++binCount) {
                //查找到链表末尾未发现相等元素则直接添加到末尾
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    //链表长度大于8时,扩容哈希桶数组或将链表转换成红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //遍历链表过程中存在相等元素则直接覆盖value
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        //覆盖value
        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);//是个空方法,是为了让HashMap的子类(例如Linkedhashmap)重写此方法
    return null;
}


resize()-----------------------------

// static final int DEFAULT_INITIAL_CAPACITY = 1<<4;1*2*2*2*2=16
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    //哈希桶数组已经初始化 则直接向左位移1位 相当于扩容一倍
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        //向左位移1位 扩容一倍
        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;
    //哈希桶数组未初始化 并且未初始化容量 则使用默认容量DEFAULT_INITIAL_CAPACITY
    else {               // zero initial threshold signifies using defaults
        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;
                        //扩容后最高位为0,则不需要移动到新的位置
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        //扩容后最高位为1,则需要移动
                        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;
}

//treeifyBin--------------------
// 需先进行判断:如果数组小于64,先扩容数组;否则,树化
final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    //哈希桶数组小于64则扩容哈希桶数组
    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;
        //将所有Node<K,V>节点类型的链表转换成TreeNode<K,V>节点类型的链表
        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);
        //将TreeNode<K,V>链表转换成红黑树
        if ((tab[index] = hd) != null)
            hd.treeify(tab);
    }
}

HashSet扩容和转成红黑树机制

  • HashSet底层是HashMap,第一次添加时,table数组扩容到16,临界值(threshold)是16*加载因子(loadFactor)0.75 = 12

注意:不论是数组下标中链表的第一个元素,还是某个下标位置的链表中的元素增加一个,都算数,都要加1,与临界值12去比较。

  • 如果Table数组使用到了临界值12,就会扩容到16×2=32,新的临界值就是32*0.75=24,依次类推
  • 在Java8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN_treeify_capacity(默认64),就会进行树化(红黑树),否则仍然采用数组扩容机制

注意:
在寻找元素所处数组中的位置时

  • 先计算hash:
//该方法会执行hash(key) 得到key对应的hash值 
static final int hash(Object key)
{ 	int h; 	
	return (key==null) ? 0:(h=key.hashCode())^(h>>>16);	//右移16位是为了防止哈希冲突 
}
  • 再计算数组中的下标 i = (n - 1) & hash // n表示扩容后数组的长度

为什么要同时重写equals和hashcode方法?
参考
现在有两个Student对象:

Student s1=new Student("小明",18);

Student s2=new Student("小明",18);

此时s1.equals(s2)一定返回false

(注意,以下是关于hashcode的一些规定:

两个对象相等,hashcode一定相等
两个对象不等,hashcode不一定不等
hashcode相等,两个对象不一定相等
hashcode不等,两个对象一定不等)

  • 假如只重写equals而不重写hashcode,那么Student类的hashcode方法就是Object默认的hashcode方法,由于默认的hashcode方法是根据对象的内存地址经哈希算法得来的,显然此时s1!=s2,故两者的hashcode不一定相等。
  • 然而重写了equals,且s1.equals(s2)返回true,根据hashcode的规则,两个对象相等其哈希值一定相等,所以矛盾就产生了,因此重写equals一定要重写hashcode,而且从Student类重写后的hashcode方法中可以看出,重写后返回的新的哈希值与Student的两个属性有关。

想想这样会有什么后果:

  • 在用Set去重的时候,会执行hashcode和equals方法:
    当添加到Set的对象 HashCode码不相同时 不会调用equals方法,对象直接存到Set集合中
    hashset相同时 才会调用equals方法 查看是否是同一个对象(是否重复) 是—则无法存入
    所以会重复添加元素,无法去重。

自己总结

  • 针对两个内容一样的对象,地址值不同,若重写equals方法,即(name与age相同),则认为equals相同。hashcode是根据地址值Hash得到的,显然是不同的。 当Hashcode不同时,则判定两个对象不同,就将两个对象都加入了HashSet / hashMap中,没有起到去重的作用。

LinkedHashSet

  • LinkedHashSet是HashSet的子类
  • LinkedHashSet 底层是一个LinkedHashMap,底层维护了一个数组+链表+红黑树+双向链表
  • LinkedHashSet 根据元素的hashCode值来决定元素的存储位置,同时使用链表维护元素的次序,这使得元素看起来是以插入顺序保存的。
  • LinkedHashSet 不允许添加重复元素
  • 每一个节点有pre和next属性,这样可以形成双向链表
  • 在添加一个元素时,先求hash值,再求索引,确定该元素在hashtable的位置,然后将添加的元素加入到双向链表(如果已经存在,不添加,原则和hashset一样)
    tail.next = newElement
    newElement.pre = tail
    tail = newElement
  • 这样的话,遍历LinkedHashSet 也能确保插入顺序和遍历顺序一致

源码分析

  1. LinkedHashSet 的底层使用LinkedHashMap存储元素。
  2. LinkedHashSet 是有序的,它是按照插入的顺序排序的。
  3. 添加第一次时,直接将 数组table 扩容到 16,存放的节点类型是 LinkedHashMap$Entry
  4. 数组是 HashMap$Node[] 类型,但存放的元素/ 数据是 LinkedHashMap$Entry类型
//继承关系是在内部类完成的
 static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }
// HashSet的构造方法
    HashSet(int initialCapacity, float loadFactor, boolean dummy) {
        map = new LinkedHashMap<>(initialCapacity, loadFactor);
    }
//然后,通过调用LinkedHashMap的构造方法初始化map
	public LinkedHashMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
        accessOrder = false;
    }
//可以看到,这里把accessOrder写死为false了。
//所以,LinkedHashSet是不支持按访问顺序对元素排序的,只能按插入顺序排序
  • LinkedHashSet底层使用LinkedHashMap存储元素,而LinkedHashMap是支持按元素访问顺序遍历元素的,也就是可以用来实现LRU的
  • LinkedHashSet是不支持按访问顺序对元素排序的,只能按插入顺序排序

参考

关于accessOrder

  • 表示是否需要按访问顺序排序,如果为false则按插入顺序存储元素,

如果是true则按访问顺序存储元素(实现LRU缓存的关键)。
afterNodeAccess(Node e)方法在节点访问之后被调用,主要在put()已经存在的元素或get()时被调用,如果accessOrder为true,调用这个方法把访问到的节点移动到双向链表的末尾。
(1)如果accessOrder为true,并且访问的节点不是尾节点;
(2)从双向链表中移除访问的节点;
(3)把访问的节点加到双向链表的末尾;(末尾为最新访问的元素)

LinkedHashMap可以用来实现LRU缓存淘汰策略;

TreeSet

Map

  1. Map与Collection并列存在,用于保存具有映射关系的数据:Key-Value(双列元素)
  2. Map中的Key与value 可以是任何引用类型的数据,会封装到HashMap$Node对象中
  3. Map中Key不能重复,value可以重复

源码解读

  1. k-v 最后是 HashMap$Node node = new Node(hash,key,value,null)
  2. k-v 为了方便程序员的遍历,还会 创建 EntrySet集合,该集合存放的元素类型Entry ,而一个Entry 对象就有k,v EntrySet<Entry<K,V>> 即: transient Set<Map.Entry<K,V>> entrySet;
  3. entrySet 中, 定义的类型是Map.Entry , 但是实际上存放的还是HashMap$Node
    这是因为HashMap$Node implements Map.Entry static class Node<K,V> implements Map.Entry<K,V>
  4. 当把HashMap$Node 对象 存放到 entrySet 就方便我们的遍历,因为 Map.Entry 提供了重要方法 K getKey(); V geyValue();

Map接口常用方法

  • put : 添加
  • remove :根据键删除映射关系
  • get : 根据键获取值
  • size: 获取元素个数
  • isEmpty : 判断个数是否为0
  • clear
  • containsKey

Map遍历

//第一组:先取出所有key,通过Key 取出对应的Value
Set keyset = map.keySet();
//(1)增强for
for(Object key: keyset){
	System.out.println(key+"-"+map.get(key));
}
//(2)迭代器
Iterator iterator = keyset.iterator();
while (iterator.hasNext()){
	Object key = iterator.next();
	System.out.println(key+"-"+map.get(key));
}



//第二组:把所有values取出
Collection values = map.values();
//这里可以使用所有的Collections使用的遍历方法
//(1)增强for
for(Object value: values){
	System.out.println(value);
}
//(2)迭代器
Iterator iterator2 = values.iterator();
while (iterator2.hasNext()){
	Object value = iterator2.next();
	System.out.println(value);
}


//第三组:通过EntrySet 来获取 k-v
//此方法遍历,只需访问一次,而keySet方法需要访问map两次(阿里规范)
Set entrySet = map.entrySet();
//(1)增强for
for(Object entry: entrySet){
	//将entry 转成 Map.Entry
	Map.Entry m = (Map.Entry) entry;
	System.out.println(m.getKey() + "-" + m.getValue());
}
(2)迭代器
Iterator iterator3 = entrySet.iterator();
while(iterator3.hasNext()){
	Object entry = iterator3.next();
	//next.getClass(); 为HashMap$Node - 实现 -> Map.Entry(getKey,getValue)
	//向下转型 Map.Entry
	Map.Entry m = (Map.Entry) entry;
	System.out.println(m.getKey() + "-" + m.getValue());
}
  • Map接口的常用实现类:HashMap、Hashtable 和 Properties
  • HashMap是Map接口使用频率最高的实现类
  • HashMap 是以 key-value 对的方式来存储数据(HashMap$Node类型)
  • 与HashSet一样,HashMap不保证映射的顺序,因为底层是以hash表的方式来存储的
  • HashMap的key可以为Null,但只能有一个
  • HashMap的值可以为null
  • 如果添加相同的Key,则会覆盖原来的key-val,等同于修改
  • HashMap没有实现同步,因此是线程不安全的。方法没有做同步互斥的操作,没有synchronized

HashMap

  • (k,v)是一个Node实现了Map.Entry<K,V>,查看HashMap的源码可以看到
  • jdk7.0的hashmap底层实现(数组+链表),jdk8.0底层(数组+链表+红黑树)

扩容机制(与HashSet完全一致)

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

源码分析

  1. 执行构造器 new HashMap()
    初始化加载因子 laodfactor=0.75
    HashMap$Node[ ] table = null
  2. 执行put 会调用hash方法,计算key的hash值
public V put(K key, V value) {
    // 调用hash(key)计算出key的hash值
    return putVal(hash(key), key, value, false, true); 
} 

static final int hash(Object key) {
    int h;
    // 如果key为null,则hash值为0,否则调用key的hashCode()方法
    // 并让高16位与整个hash异或,这样做是为了使计算出的hash更分散
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); 
}
  1. 执行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;
    // 如果桶的数量为0,则初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        // 调用resize()初始化
        n = (tab = resize()).length;
    // (n - 1) & hash 计算元素在哪个桶中
    // 如果这个桶中还没有元素,则把这个元素放在桶中的第一个位置
    if ((p = tab[i = (n - 1) & hash]) == null)
        // 新建一个节点放在桶中
        tab[i] = newNode(hash, key, value, null);
    else {
        // 如果桶中已经有元素存在了
        Node<K, V> e;
        K k;
        // 如果桶中第一个元素的key与待插入元素的key相同,保存到e中用于后续修改value值
        if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        else if (p instanceof TreeNode)
            // 如果第一个元素是树节点,则调用树节点的putTreeVal插入元素
            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
                        treeifyBin(tab, hash); //注意此处并不是马上树化,有判断条件的,去看treefyBin源码
                    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;
            // 在节点被访问后做点什么事,在LinkedHashMap中用到
            afterNodeAccess(e);
            // 返回旧值
            return oldValue;
        }
    }
    // 到这里了说明没有找到元素
    // 修改次数加1
    ++modCount;
    // 元素数量加1,判断是否需要扩容
    if (++size > threshold)
        // 扩容
        resize();
    // 在节点插入后做点什么事,在LinkedHashMap中用到
    afterNodeInsertion(evict);
    // 没找到元素返回null
    return null;
}
final void treeifyBin(Node<K, V>[] tab, int hash) {
    int n, index;
    Node<K, V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        // 如果桶数量小于64,直接扩容而不用树化
        // 因为扩容之后,链表会分化成两个链表,达到减少元素的作用
        // 当然也不一定,比如容量为4,里面存的全是除以4余数等于3的元素
        // 这样即使扩容也无法减少链表的长度
        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);
    }
}

注:关于扩容后,链表重哈希的过程
参考

  • 在 HashMap 做扩容操作时,除了把数组容量扩大为原来的两倍外,还会对所有元素重新计算 hash 值,因为长度扩大以后,hash值也随之改变。
  • 正常我们是把所有元素都重新计算一下下标值,再决定放入哪个桶,JDK1.8 优化成直接把链表拆成高位和低位两条,通过位运算来决定放在原索引处或者原索引加原数组长度的偏移量处。

(也可能不拆分链表)

// 把原有链表拆成两个链表
// 链表1存放在低位(原索引位置)
Node loHead = null, loTail = null;
// 链表2存放在高位(原索引 + 旧数组长度)
Node hiHead = null, hiTail = null;
Node next;
do {    
	next = e.next;    // 链表1    
	if ((e.hash & oldCap) == 0) {        
		if (loTail == null)    loHead = e;        
		else   loTail.next = e;        
		loTail = e;    
		}    
	// 链表2    
	else {        
			if (hiTail == null)  hiHead = e;        
			else   hiTail.next = e;        
			hiTail = e;    
		}
	} while ((e = next) != null);
	// 链表1存放于原索引位置
	if (loTail != null) {    
		loTail.next = null;    
		newTab[j] = loHead;
	}
	// 链表2存放原索引加上旧数组长度的偏移量
	if (hiTail != null) {    
		hiTail.next = null;    
		newTab[j + oldCap] = hiHead;
	}

HashTable

参考

基本介绍:

  • 存放的元素是键值对:即K-V
  • hashtable的键和值都不能为null, 否则会抛出空指针异常
  • hashTable使用方法基本和HashMap一样
  • hashTable是线程安全的(synchronized),hashMap线程不安全

底层:

  • 底层有数组 Hashtable$Entry[ ] 初始化为11
  • 临界值 threshold 8 = 11*0.75
  • 执行方法 addEntry(int hash, K key, V value, int index), 添加k-v,封装到Entry
  • 当 if ( count>= threshold) 满足时,就进行扩容
    扩容 :新容量=旧容量 * 2 + 1
    int newCapacity = (oldCapacity << 1) + 1;

Properties

  • Properties类继承自Hashtable类并且实现了Map接口,也是使用一种键值对的形式来保存数据
  • 使用特点和Hashtable类似
  • 可通过k-v 存放数据,key和value不能为null
  • Properties还可以用于从 xxx.properties 文件中,加载数据到Properties类对象,并进行读取和修改
  • 说明:工作后 xxx.properties文件通常作为配置文件

TreeMap

Collections工具类

collections的sort方法:归并排序
arraylist为例 源码分析

  • reverse(List): 反转List中元素的顺序
  • shuffle(List): 对List集合元素进行随机排序
  • sort(List) : 根据元素的自然顺序对指定List集合元素按升序排序
  • sort(List,Comparator) :根据指定的Comparator产生的顺序对List集合排序
  • swap(List,int,int): 将指定list集合中的i和 j处元素进行交换
  • Object max(Collection) :根据元素的自然顺序,返回集合中的最大元素
  • Object max(Collection, Comparator)
  • Object min(Collection) :根据元素的自然顺序,返回集合中的最大元素
  • Object min(Collection, Comparator)
  • int frequency(Collection, Object) :返回指定集合中指定元素的出现次数
  • void copy(List dest,List src):将src中的内容复制到dest中
  • boolean replaceAll(List list, Object oldVal ,Object newVal): 使用新值替换List对象的所有旧值。

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

  • 先判断存储的类型(一组对象【单列】或一组键值对【双列】
  • 一组对象【单列】:Collection接口
  1. 允许重复:List
    增删多:LinkedList 【底层维护了一个双向链表】
    改查多:ArrayList【底层维护Object类型的可变数组】
  2. 不允许重复:Set
    无序:HashSet 【底层是HashMap,维护了一个哈希表 即(数组+链表+红黑树)
    排序:TreeSet
    插入和取出顺序一致:LinkedHashSet
  3. 一组键值对:map
    键无序:HashMap
    键排序:TreeMap
    键插入和取出顺序一致:LinkedHashMap
    读取文件:Properties

红黑树

  • 红黑树介绍:参考
  • 为什么hashmap不直接采用红黑树,为什么达到8个才转红黑树:参考
    链表的查询效率是O(k),红黑树的查询效率是O(log k),k为桶中的元素个数,所以当元素数量非常多的时候,转化为红黑树能极大地提高效率。当元素少时,相差不大,没必要转红黑树,因为红黑树需要左旋右旋变色保持平衡。
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值