Java集合

难点:1.理解底层机制 2.底层源码 3.什么情况下使用哪种集合

集合:

  1. 动态保存任意多个对象,使用方便
  2. 提供一系列方便操作对象的方法,add remove set get
  3. 使用集合进行添加删除元素更方便

集合框架体系

Collection

Map

1.集合主要是两种(单列集合,双列集合)

2.Collection接口有两个重要的子接口 LIst Set,他们的实现子类都是单列集合

3.Map接口的实现子类,是双列集合,存放k - v

Collection接口和常用方法

  • 有些Collection实现类可以存放重复元素
  • 有些Collection实现类,有序List,无序Set

Collection常用方法

add:添加方法

remove:remove(Object object) return boolean ;remove(int index) return Object

contains: 查找元素是否存在 contains(Object object) return boolean

size:

isEmpty:

clear:清空集合

addAll:添加多个元素 addAll(Collection collection)

contailsAll:查找多个元素是否存在

removeAll :清空集合中的子集

Collection接口遍历元素方式

1.使用Iterator迭代器

所有实现了Collection接口的集合类,都有一个iterator方法,用以返回一个实现了Iterator接口的对象

在调用iterator.next()方法之前必须要调用iterator.hasNext进行检测,若不调用,且下一条记录无效,直接调用iterator.next()会抛出NoSuchElementException异常

当退出while循环后,这时iterator迭代器,指向最后的元素,如果希望再次便利,需要重置迭代器

2.增强for循环

底层也是迭代器

简化版迭代器

List常用方法

  1. list集合是有序的,且可重复
  2. list集合中每个元素都有索引,支持索引
  • void add(int index,Object object):在index位置插入obj元素
  • boolean addAll(int index,Collection col):加一堆
  • get
  • int indexOf(Object obj):返回obj首次在集合中出现的位置
  • int lastIndexOf(Object obj)
  • Object remove(int index) : 删除指定位置元素,返回此元素
  • Object set(int index,Object obj): 指定位置元素为obj,相当于替换
  • List subList(int formIndex,int toIndex):返回从fromIndex到toIndex位置的集合(前闭后开)

ArrayList底层结构和源码分析

  • 可以加入null,并且多个
  • ArrayList是由数组来实现存储的
  • ArrayList基本等同于Vector,但是ArrayList是线程不安全的(效率高)方法没有synchronized修饰

ArrayList底层操作机制源码分析(重点难点)

  • ArrayList中维护了一个Object类型的数组,elementData,transient Object[] elementData是一个Object类型的数组 //transient:表示瞬间,短暂,表示该属性不会被序列化

  • 当创建一个ArrayList集合时,如果使用无参构造器,则初始elementData容量为0,第一次添加,则扩容为elementData为10,如果需要再次扩容,则扩容elementData为1.5倍

    向集合中添加元素:

    public ArrayList(){//创建了一个空的elementData数组
    	this.elementData = DEFAULTTCAPACITY_EMPTY_ELEMENTDATA;
    }
    
    public boolean add(E e) {//执行list.add 1.先确定是否要扩容2.然后再执行赋值
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    
    //该方法确定minCapactiy
    //1.第一次扩容为10
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);
        }
        return minCapacity;
    }
    
    //modCount++:记录集合被修改的次数
    //如果elementData大小不够,就调用grow()去扩容
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;//记录集合修改次数
    
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    

    扩容代码

    //1.真正扩容
    //2.使用扩容机制来确定要扩容到多少
    //3.第一次newCapacity = 10
    //4.第二次及以后,按照1.5倍扩容
    //5.扩容是使用的是Arrays.copyOf(),保证之前的数据还在
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);//>> : /2   0
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;//10          //第一次是10
        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);//底层是System.copyOf
    }
    
  • 如果使用指定大小的构造器,则初始化elementData容量为指定大小,如果需要扩容则直接扩容elementData为1.5倍

    使用有参构造器

    //创建了一个指定大小的elementData数组,this.elementData = new Object[initialCapacity];
    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);
        }
    }
    
  • 小结:如果有参构造器,扩容机制

    1. 第一次扩容就按照elementData的1.5倍扩容
    2. 整个执行流程还是和前面讲的一样

Vectory底层结构和ArrayList的比较

底层结构 版本 线程安全(同步)效率 扩容倍数
ArrayList 可变数组 1.2 不安全,效率高 有参构造:1.5倍
无参构造:
1.第一次是10
2.第二次开始1.5倍
Vector 可变数组Object[] 1.0 安全,效率不高 无参构造:默认10,满后,按照二倍扩容
如果指定大小:每次直接二倍扩容

1.new Vector()底层

public Vector(){
	this(10);
}

2.vector.add(i)

//这个方法就是添加数据到vector集合
public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}
//确定是否需要扩容 条件:minCapacity - elementData.length > 0
private void ensureCapacityHelper(int minCapacity) {
    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

如果 需要的数组大小不够用 ,就扩容,扩容的算法:int newCapacity = oldCapacity + ((capacityIncrement > 0) ?capacityIncrement : oldCapacity);

capacityIncrement:有参构造个数

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

LinkedList底层结构

LinkedList的全面说明

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

LinkedList的底层操作机制

  1. LinkedList底层维护了一个双向链表
  2. LInkedList中维护了两个属性,first和last,分别指向首节点和尾节点
  3. 每个节点(Node对象),里面又维护了prev next item三个属性
  4. linkedList元素的增加和删除,不是通过数组完成的,先对来说效率较高

linkedList底层结构

1.LinkedList linkedList = new LinkedList();

/**
     * Constructs an empty list.
     */
public LinkedList() {
}

2.这时linkedList的属性first = null last = null

3.执行

/**
     * Appends the specified element to the end of this list.
     *
     * <p>This method is equivalent to {@link #addLast}.
     *
     * @param e element to be appended to this list
     * @return {@code true} (as specified by {@link Collection#add})
     */
public boolean add(E e) {
    linkLast(e);
    return true;
}

4.将新的节点加入到双向链表的最后

/**
     * Links e as last element.
     */
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++;
}

jpg

  • remove():删除第一个元素

    1.执行removeFirst

    public E remove(){
    	return removeFirst();
    }
    

    2.执行

    public E removeFirst() {
        final Node<E> f = first;
        if (f == null)
            throw new NoSuchElementException();
        return unlinkFirst(f);
    }
    

    3.执行unlinkFirst,将f指向的双向链表的第一个结点拿掉

    /**
         * Unlinks non-null first node f.
         */
    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
        first = next;
        if (next == null)
            last = null;
        else
            next.prev = null;
        size--;
        modCount++;
        return element;
    }
    

    jpg

修改某个节点对象

得到某个节点对象

ArrayList和LinkedList比较

  • ArrayList和LinkedList的比较

  • 底层结构 增删的效率 改查的效率
    ArrayList 可变数组 较低,数组扩容 较高
    LinkedList 双向链表 较高,通过链表追加 较低
  • 如何选择ArrayList和LinkedList

    1. 如果改查操作较多,选择ArrayList
    2. 如果增删操作较多,选择LinkedList
    3. 一般来说,在程序中,80% - 90%都是查询,因此大部分情况下会选择ArrayList
    4. 使用哪个视情况而定

Set接口和常用方法

  • set接口基本介绍

    1. 无序(添加和取出的顺序不一致但是取出的顺序是固定的(根据hash排序)),没有索引
    2. 不允许重复元素,所以最多包含一个null
  • set接口的常用方法

    和list接口一样,Set接口也是Collection的子接口,因此,常用方法和collection接口一样

  • set接口的遍历方式

    同Collection的遍历方式一样,因为Set接口也是Collection接口的子接口

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

Set接口实现类 - HashSet

  1. HashSet实现了Set接口

  2. HashSet实际上是一个HashMap

    public HashSet(){
    	map = new HashMap<>();
    }
    
  3. 可以存放null,但只能存一个null

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

  • add():return boolean
//经典面试题
set.add(new String("张三"));
set.add(new String("张三"));//加入失败

HashSet底层机制说明

分析HashSet底层是HashMap,HashMap底层是(数组 + 链表 + 红黑树)

模拟hashSet底层

//模拟hashSet底层
//1.创建一个数组,类型是node[]
//2.有些人直接吧node[]成为表
Node[] table = new Node[16];
System.out.println(table);
//3.创建节点
Node john = new Node("john", null);
table[2] = john;
Node jack = new Node("jack", null);
john.next = jack;
Node rose = new Node("Rose", null);
jack.next = rose;

table[3] = new Node("lucy",null);

分析HashSet的添加元素底层是如何实现的(hash() + equals())

  1. HashSet底层是HashMap
  2. 添加元素时,会得到hash值,会转成 索引值
  3. 找到存储表table,看这个索引位置是否已经存放的有元素
  4. 如果没有直接加入
  5. 如果有,调用equals比较,如果相同,就放弃添加,如果不相同,添加到最后(挂载到当前节点的next节点)
  6. 在jdk8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table大小>=MIN_TREEIFY_CAPACITY(默认64)就会进行树化(红黑树)

HashSet源码解读

1.执行构造器

/**
     * Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
     * default initial capacity (16) and load factor (0.75).
     */
public HashSet() {
    map = new HashMap<>();
}

2.执行add()

/**
     * Adds the specified element to this set if it is not already present.
     * More formally, adds the specified element <tt>e</tt> to this set if
     * this set contains no element <tt>e2</tt> such that
     * <tt>(e==null&nbsp;?&nbsp;e2==null&nbsp;:&nbsp;e.equals(e2))</tt>.
     * If this set already contains the element, the call leaves the set
     * unchanged and returns <tt>false</tt>.
     *
     * @param e element to be added to this set
     * @return <tt>true</tt> if this set did not already contain the specified
     * element
     */
public boolean add(E e) {
    return map.put(e, PRESENT)==null;//PRESENT : new Object();static final
}

3.执行put(),该方法会执行hash(key) 得到key对应的hash值 h = key.hashCode()) ^ (h >>> 16)

/**
     * Associates the specified value with the specified key in this map.
     * If the map previously contained a mapping for the key, the old
     * value is replaced.
     *
     * @param key key with which the specified value is to be associated
     * @param value value to be associated with the specified key
     * @return the previous value associated with <tt>key</tt>, or
     *         <tt>null</tt> if there was no mapping for <tt>key</tt>.
     *         (A <tt>null</tt> return can also indicate that the map
     *         previously associated <tt>null</tt> with <tt>key</tt>.)
     */
public V put(K key, V value) {//key = "java"    value = PRESENT
    return putVal(hash(key), key, value, false, true);
}

4.执行

/**
     * Implements Map.put and related methods.
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
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是存放Node<k,v>
    //table 就是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是否为空
   	//(2.1)如果p为null,表示还没有存放过元素,就创建一个Node(key="JAVA",value=PRESENT) 
    //(2.2)就放在该位置,tab[i] = newNode(hash, key, value, null);
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        //开发技巧:在需要局部变量(辅助变量)时候,再创建
        Node<K,V> e; K k;//
        //如果当前索引位置对应的链表的第一个元素和准备添加的key的hash值一样
        //并且满足下面两个条件之一:
        //(1)准备加入的key和p指向的node节点的key是同一个对象
        //(2)p指向的node节点的key的equals()和准备加入的key比较后相同
        //就不能加入
        if (p.hash == hash &&
            
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        //在判断p是不是一颗红黑树
        //如果是一颗红黑树,就调用putTreeVal,来进行添加
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {//如果table对应的索引位置,已经是一个链表,就是用for循环比较
              //(1)依次和链表的每一个元素比较后,都不同,则加入到该链表的最后
            	//注意:在把元素添加到链表后,立即判断 该链表是否已经达到8个节点TREEIFY_THRESHOLD,
            	//如果,到八个,就调用treeifyBin ,对当前链表进行树化红黑
            	//注意:在转成红黑树时,要进行判断,如果该table数组的大小<64
            	//if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            	//		resize();
            	//如果上面条件成立,先table扩容
            	//只有上面条件不成立时,才转成红黑树
              //(2)依次和链表的每一个元素比较过程中,如果有相同的情况,就直接break
            for (int binCount = 0; ; ++binCount) {
                //p是索引位置的元素,e是p的next
                if ((e = p.next) == null) {  
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st 													//TREEIFY_THRESHOLD = 8
                        //简单来说:第九个开始树化
                        //等于7的时候已经有8个了 ,8走完的时候又new一个
                        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)
                e.value = value;
            afterNodeAccess(e);
            return oldValue; 
        }
    }
    ++modCount;
    //size
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);//空的,为了让HashMap子接口实现
    return null;
}

HashSet扩容机制

  1. HashSet的底层是HashMap,第一次添加时,table数组扩容到16,临界值(threshold)时16*加载因子(loadFactor)是0.75 = 12
  2. 如果table数组用到临界值时,就会扩容到16*2 = 32,新的临界值就是32 * 0.75 = 24,以此类推
  3. 在java8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认值是8),并且table的大小 >=MAXIMUM_CAPACITY(默认64),就会进行树化(红黑),否则仍采用数组扩容机制
posted @ 2022-03-15 22:32  张三张三  阅读(37)  评论(0编辑  收藏  举报