Java入门-集合

今日语录:山有峰顶,海有彼岸。漫漫长途,终有回转。余味苦涩,终有回甘。

一、集合的基本概念和理解

  • 分析数组的不足

    1、长度必须一开始指定,而且一旦指定,不能更改

    2、保存的必须为同一类型的元素

    3、使用数组进行增加/删除元素的示意代码,比较麻烦

  • 集合的好处

    1、可以动态的保存任意多个元素,使用比较方便

    2、提供了一系列方便的操作对象的方法:add  remove    set   get  等

    3、使用集合添加,删除新元素的适宜代码

二、集合框架体系

  • 单列集合
Java入门-集合
image-20211205201555189
  • 双类集合(存放的键值对K-V)
Java入门-集合
image-20211205201753837

三、Collection

  • Collection接口实现类的特点

    public interface Collection<E> extends Iterable<E>

    1、collection实现子类可以存放多个元素,每个元素可以是Object

    2、有些Collection的实现类,可以存放重复的元素,有些不可以

    3、有些collection的实现类,有些是有序的(List),有些是无序的(Set)

    4、Collection接口没有直接的实现子类,是通过他的子接口Set和List来实现的

  • Collection常用方法

    1、add:添加元素

    2、remove:删除指定元素

    3、contains:查找元素是否存在

    4、size:获取元素个数

    5、isEmpty:判断是否是空

    6、clear:清空

    7、addAll:添加多个元素

    8、containsAll:查找多个元素是否都存在

    9、removeAll:删除多个元素


  • Collection接口遍历对象

    方式一:Iterator(迭代器)

  • 简介

    Collection接口遍历元素方式

    1、Iterator对象称为迭代器,主要用来遍历Collection集合中的元素

    2、所有实现了Collection接口的集合类都有一个Iterator()方法,用以返回一个实现了Iterator接口的对象,即可以返回一个迭代器

    3、Iterator的结构

    4、Iterator仅用于遍历集合,Iterator本身并不存放对象

    5、当退出while循环后,这时iterator迭代器,指向最后的元素

    iterator.next();

    6、如果希望再次遍历,需要重置我们的迭代器。

  • 迭代器的执行原理

    Iterator iterator = coll.iterator();

    hasNext();//判断是否还有下一个元素

    while(iterator.hasNext()){

    // next(); 1 指针下移 2 将下移以后集合位置上的元素返回

    System.out.println(iterator.next());

    }

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

  • 方式二-for循环增强

    增强for循环,可以代替iterator迭代器,特点:增强for循环就是简化版的iterator,本质是一样的。只能用于遍历集合或数组

    • 基本语法

      for(元素类型 元素名 :集合名或数组名){
      访问元素
      }

1、List

  • List简介

    List接口是Collection接口的子接口

    1、List集合类中元素有序(即添加顺序和取出顺序一致)、且可重复

    2、List集合中的每个元素都有其对应的顺序索引,即支持索引

    3、List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素

    4、JDK API中List接口的实现类有:

    ArrayList  LinkedList   Stack     Vector

  • List常用方法

    1、void add(int index,Object ele);在index位置插入ele元素

    2、bolean addAll(int index,Collection eles):从index位置开始将eles中的所有元素添加进来

    3、Object get(int index):获取指定index位置的元素

    4、int indexOf(Object obj); 返回obj在集合中首次出现的位置

    5、int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置

    6、Object remove(int index):移除指定index位置的元素,并返回此元素

    7、Object set(int index,Object ele):设置指定index位置的元素为ele,相当于是替换

    8、List subList(int fromIndex,int toIndex):返回从fromIndex到toIndex位置的子集合

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

    方式一:使用iterator
    Iterator iterator = col.iterator();
    while(iterator.hasNext()){
    Object obj = iterator.next();
    }


    方式二:使用增强for
    for(Object o : col){

    }

    方式三:使用普通for循环
    for(int i=0,i<col.size();i++){
    Object obj = list.get(i);
    System.out.println(obj);
    }

ArrayList

  • ArrayList注意事项

    1、permits all elements,include null.ArrayList可以加入null,并且可以有多个

    2、ArrayList是由数组来实现数据存储的

    3、ArrayList基本等同于Vector,除了ArrayList是线程不安全的(执行效率高),看源码,方法没加synchronized关键字

    /**
         * Appends the specified element to the end of this list.
         *
         * @param e element to be appended to this list
         * @return <tt>true</tt> (as specified by {@link Collection#add})
         */

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

    在多线程模式下,不建议使用ArrayList

  • ArrayList底层操作机制源码分析

    1、ArrayList中维护了一个Object类型的数组elementData

    /**
         * The array buffer into which the elements of the ArrayList are stored.
         * The capacity of the ArrayList is the length of this array buffer. Any
         * empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
         * will be expanded to DEFAULT_CAPACITY when the first element is added.
         * transient:表示瞬间,短暂的,表示该属性不会被序列化
         */

        transient:Object[] elementData; // non-private to simplify nested class access

    2、当创建ArrayList对象时,如果使用的是无参构造器,则初始化elementData容量为0,第一次添加,则扩容elementData为10,如果需要再次扩容,则扩容elementData为1.5倍

    /**
         * Constructs an empty list with an initial capacity of ten.
         */

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

    3、如果使用的是指定大小的构造器,则初始化elementData容量为指定大小,如果需要扩容,则直接扩容elementData为1.5倍

        /**
         * Constructs an empty list with the specified initial capacity.
         *
         * @param  initialCapacity  the initial capacity of the list
         * @throws IllegalArgumentException if the specified initial capacity
         *         is negative
         */

        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、扩容机制算法

    Java入门-集合
    image-20211207222026386
  • 无参构造方法-动态扩容机制演示

     // 添加元素,动态扩容机制演示
        ArrayList list = new ArrayList();
        for(int i = 0;i<10;i++){
      list.add(i);
     }
     


       /**
         * 执行list.add()
         * (1)先确定是否要扩容
         * (2)然后在执行赋值
         * Appends the specified element to the end of this list.
         *
         * @param e element to be appended to this list
         * @return <tt>true</tt> (as specified by {@link Collection#add})
         */

        public boolean add(E e) {
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            elementData[size++] = e;
            return true;
        }
        
        /**
         * 该方法确定minCapacity
         * (1)第一次扩容为 10
         */

        private static int calculateCapacity(Object[] elementData, int minCapacity) {
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                return Math.max(DEFAULT_CAPACITY, minCapacity);
            }
            return minCapacity;
        }
        
        /**
         * (1) modCount++ :记录这个集合被修改的次数(防止多线程重复操作)
         * (2) 如果elementData的大小不够,就调用grow(minCapacity)进行扩容
         */

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

            // overflow-conscious code
            if (minCapacity - elementData.length > 0)
                grow(minCapacity);
        }
        
        /**
         * Increases the capacity to ensure that it can hold at least the
         * number of elements specified by the minimum capacity argument.
         * 扩容机制
         * @param minCapacity the desired minimum capacity
         */

        private void grow(int minCapacity) {
            // overflow-conscious code
            int oldCapacity = elementData.length;
            // 进行1.5倍扩容
            int newCapacity = oldCapacity + (oldCapacity >> 1);
            // 如果新的容量 < 最小的容量(10),那么就把最小容量赋值给新容量
            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:
            // 使用Arrays.copyOf(),可以保留集合中原来的数据
            elementData = Arrays.copyOf(elementData, newCapacity);
        }
        

LinkedList

  • 说明

    1、LinkedList底层实现了双向链表和双端队列特点

    2、可以添加任意元素(元素可以重复),包括null

    3、线程不安全,没有实现同步

  • 类图Java入门-集合

  • LinkedList的底层操作机制

    1、LinkedList底层维护了一个双向链表

    transient Node<E> first;
    transient Node<E> last;

    2、LinkedList中维护了两个属性first和last分别指向首节点和尾节点

    3、每个节点(Node对象),里面维护了prev、next、item三个属性,其中通过prev指向前一个,通过next指向后一个节点。最终实现双向链表

    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、所以LinkedList的元素的添加和删除,不是通过数组完成的,相对来说效率最高

  • LinkedList源码分析

    add(E e)

    1、首先,创建LinkedList对象

    LinkedList linkedList = new LinkedList();
    // 此时底层源码如下

    /**
         * Constructs an empty list.
         */

        public LinkedList() {
        }

    2、此时linkedList的属性 first=null  last=null

    3、向linkedList添加元素,执行以下代码

    /**
         * 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++;
        }

    remove()

    1、无参,默认删除的是第一个节点,并返回第一个节点的元素

    /**
         * Retrieves and removes the head (first element) of this list.
         *
         * @return the head of this list
         * @throws NoSuchElementException if this list is empty
         * @since 1.5
         */

        public E remove() {
            return removeFirst();
        }
        
        
        /**
         * Removes and returns the first element from this list.
         *
         * @return the first element from this list
         * @throws NoSuchElementException if this list is empty
         */

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

    2、执行删除操作

    /**
         * 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;
            // 把当前要删除节点的item置空
            f.item = null;
            // 把第一个节点指向的下一个节点置空
            f.next = null// help GC
            // 把LinkedList的第一个节点重设为->下一个节点
            first = next;
            if (next == null)
                last = null;
            else
               //把下一个节点的指向之前节点的连接置空
                next.prev = null;
            // 将集合长度减1
            size--;
            // 记录修改次数:+1
            modCount++;
            // 返回被删除节点的元素
            return element;
        }
  • ArrayList和LinkedList的比较


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

    1、如果我们改查的操作多,选择ArrayList

    2、如果我们增删的操作多,选择LinkedList

    3、一般来说,在程序中,80%-90%都是查询,因此大部分情况下会选择ArrayList

    4、在一个项目中,根据业务灵活选择,也可能这样,一个模块使用的是ArrayList,另一个模块使用LinkedList

Vactor

  • 类定义

    public class Vector<E>
    extends AbstractList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable
  • 类图

    Java入门-集合
  • 简介

    1、Vector底层也是一个对象数组,protected Object[] elementData;

    2、Vector是线程同步的,即线程安全。Vector类的操作方法有synchronized

    /**
         * Returns the element at the specified position in this Vector.
         *
         * @param index index of the element to return
         * @return object at the specified index
         * @throws ArrayIndexOutOfBoundsException if the index is out of range
         *            ({@code index < 0 || index >= size()})
         * @since 1.2
         */

        public synchronized E get(int index) {
            if (index >= elementCount)
                throw new ArrayIndexOutOfBoundsException(index);

            return elementData(index);
        }

    3、在开发中,需要线程同步安全时,考虑使用Vector

  • Vector源码解读

    1、创建Vector类时,无参默认容量是10

    /**
    * Constructs an empty vector so that its internal data array
    * has size {@code 10} and its standard capacity increment is
    * zero.
    */
    public Vector() {
    this(10);
    }

    2、有参执行扩容机制

    /**
         * Appends the specified element to the end of this Vector.
         *
         * @param e element to be appended to this Vector
         * @return {@code true} (as specified by {@link Collection#add})
         * @since 1.2
         */

        public synchronized boolean add(E e) {
            modCount++;   //记录修改次数
            ensureCapacityHelper(elementCount + 1);  //确认集合容量是否够用
            elementData[elementCount++] = e;
            return true;
        }


        /**
         * 确认集合容量是否够用
         * This implements the unsynchronized semantics of ensureCapacity.
         * Synchronized methods in this class can internally call this
         * method for ensuring capacity without incurring the cost of an
         * extra synchronization.
         *
         * @see #ensureCapacity(int)
         */

        private void ensureCapacityHelper(int minCapacity) {
            // overflow-conscious code
            if (minCapacity - elementData.length > 0)
                grow(minCapacity);
        }

        /**
         * 集合容量不够用执行扩容(2倍扩容)
         */

        private void grow(int minCapacity) {
            // overflow-conscious code
            int oldCapacity = elementData.length;
            // 如果capacityIncrement不大于0,那么执行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);
        }

  • Vector和ArrayList比较


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

2、Set

  • 简介

    1、无序( 添加和取出的顺序不一致 ),没有索引

    2、不允许重复元素,所以最多包含一个null

    3、JDK API 中Set接口的实现类有

    Java入门-集合
    image-20230216232806582
  • Set接口的常用方法

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

  • Set接口的遍历方式

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

    1、可以使用迭代器

    2、增强for循环

    3、不能使用索引的方式来获取

HashSet

  • 说明

    1、HashSet实现了Set接口

    2、HashSet实际上是HashMap,如下源码

    /**
    * 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<>();
    }

    3、可以存放null值,但是只能有一个null

    4、HashSet不保证元素是有序的,取决于hash后,再确定索引的结果(即,不保证存放顺序和取出顺序一致)

    5、不能有重复元素/对象

  • HashSet底层机制说明

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

    2、示例代码

    package com.hspedu.set_;

    /**
     * @author 韩顺平
     * @version 1.0
     */

    @SuppressWarnings({"all"})
    public class HashSetStructure {
        public static void main(String[] args) {
            //模拟一个HashSet的底层 (HashMap 的底层结构)

            //1. 创建一个数组,数组的类型是 Node[]
            //2. 有些人,直接把 Node[] 数组称为 表
            Node[] table = new Node[16];

            //3. 创建结点
            Node john = new Node("john"null);

            table[2] = john;
            Node jack = new Node("jack"null);
            john.next = jack;// 将jack 结点挂载到john
            Node rose = new Node("Rose"null);
            jack.next = rose;// 将rose 结点挂载到jack

            Node lucy = new Node("lucy"null);
            table[3] = lucy; // 把lucy 放到 table表的索引为3的位置.
            System.out.println("table=" + table);


        }
    }
    class Node //结点, 存储数据, 可以指向下一个结点,从而形成链表
        Object item; //存放数据
        Node next; // 指向下一个结点

        public Node(Object item, Node next) {
            this.item = item;
            this.next = next;
        }
    }


HashSet扩容机制

1、HashSet底层就是HashMap

2、添加一个元素时,先得到hash值–会转成–>索引值

3、找到存储数据表table,看到这个索引位置是否已经存放元素

4、如果没有,直接放入元素

5、如果有,调用equals进行比较,如果相同,就放弃添加(因为相同的已存在),如果不相同,则添加到最后

6、在Java8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认64),就会进行树化(转为红黑树)


HashSet树化机制(转成红黑树)

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

(loadRactor)是0.75 = 12

2、如果table数组使用到了临界值12,就会扩容到16 * 2 =32,新的临界值就是32 * 0.75 = 24,依此类推

8 --> 16(临界值16*0.75=12) --> 32(临界值32*0.75=24) --> 64(临界值64*0.75=48) --> 128(临界值128*0.75=96)

3、在java8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认64),就会进行树化(红黑树),否则仍然采用数组扩容机制


HashSet源码分析

add()

1、执行HashSet(),底层其实是一个HashMap

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

2、执行add()

public boolean add(E e) {
// 例如:e = "java"
// PRESENT =static final Object PRESENT = new Object
return map.put(e, PRESENT)==null;
}

3、执行put(),会执行hash(key),得到key对应的hash值

算法为:( h = key.hashCode() ) ^ (h >>> 16)
public V put(K key, V value) {
// key = "java"
// value = PRESENT 共享
return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

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;
      // table就是transient 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 是否为null
      //(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;
      //(3)如果p不是null,并且当前索引位置对应的链表的第一个元素和准备添加的元素的hash值一样,
      //(3.1)并且满足 准备加入的key 和 p 指向的Node 节点的key是同一个对象
      //(3.2) p 指向的Node 节点的key 的equals()和准备加入的key比较后相同,就不能加入
          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);
          else {
          // 如果table对应索引位置已经是一个链表,则使用for循环进行比较
          //(1)依次和该链表的每一个元素比较后,都不相同,则加入到链表的最后
          // (1.1)注意在把元素添加到链表后,立即判断 该链表是否已经达到8个节点,就调用treeifyBin()对当前这个链表进行树化(转成红黑树),注意,在转为红黑树时,要进行判断,判断条件->
          if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY(64))
          resize();
          如果上面条件成立,先进行table扩容。
          只有上面条件不成立时,才进行树化(转成红黑树)
          //(2)依次和该链表的每一个元素比较过程中,如果有相同情况,就直接break
              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)
                  e.value = value;
              afterNodeAccess(e);
              return oldValue;
          }
      }
      ++modCount;
      if (++size > threshold)
          resize();
      afterNodeInsertion(evict);
      return null;
  }

LinkedHashSet

  • 说明

    1、LinkedHashSet是HashSet的子类

    2、LinkedHashSet底层是一个LinkedHashMap,底层维护了一个 数组 + 双向链表

    3、LinkedHashSet根据元素的hashCode 值来决定元素的存储位置,同时使用链表维护元素的次序,这使得元素看起来是以插入顺序保存的

    4、LinkedHashSet不允许添加重复元素

  • 类图

    Java入门-集合
    image-20211212182046044
  • LinkedHashSet源码分析

    1、LinkedHashSet加入顺序和取出元素/数据的顺序一致

    2、LinkedHashSet底层维护的是一个LinkedHashMap(是HashMap的子类)

    3、LinkedHashSet 底层结构(数组table+双向链表)

    4、添加第一次时,直接将数组table扩容到 16 ,存放的节点类型是 LinkedHashMap$Entry

    5、数组是 HashMapEntry

四、Map

  • 类图(双类集合)

    Java入门-集合
    image-20211215230528206
  • Map类特点(JDK1.8)

    1、Map与Collection并列存在,用于保存具有映射关系的数据:Key-Value

    2、Map中的 key 和 value 可以是任何引用类型的数据,会封装到HashMap$Node对象中

    3、Map中的 key 不允许重复

    4、Map中 value 可以重复

    5、Map中的 key 可以为Null,value也可以为null,注意key为null,只能有一个,value为null,可以有多个

    6、常用String类作为Map的key

    7、key 和 value 之间存在单向一对一关系,即通过指定的key 总能找到对应的value

    8、Map存放数据的key-value示意图,一对 k-v 是放在一个 Node中的,有因为Node实现了Entry接口,有些书上也说,一对k-v就是一个Entry

    1、k-v 最后是 HashMap$Node node = newNode(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,但是实际上存放的还是HashMapNode 实现了Map.Entry<K,V>

    static class Node<K,V> implements Map.Entry<K,V>{...}

    4、当把HashMap$Node 对象 存放到 entrySet 就方便我们的遍历,因为Map.Entry 提供了重要方法:K getKey(); V getValue();

    Java入门-集合
    image-20211215224354205
  • Map接口和常用方法

    1、Map接口常用方法

    1、containsKey:查找键是否存在

    2、keySet:获取所有的键

    3、entrySet:获取所有关系

    4、values:获取所有的值

    2、Map接口遍历方式

    Map map = new HashMap();
    map.put("1",1);
    map.put("2",2);
    map.put("3",3);

    第一组:先取出所有的 Key ,通过取出 key 取出对应的 value

    1、增强for循环

    Set keySet = map.keySet();
    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 + "-" + ,ap.get(key));
    }

    第二组:先取出所有的values

    Collecion values = map.values();

    1、增强for循环

    for(Object value : values){
    System.out.println(value);
    }

    2、迭代器

    Iterator iterator = values.iterator();
    while(iterator.hasNext()){
    Object value = iterator.next;
    System.out.println(value);
    }

    第三组:使用EntrySet 来获取 k – v

    Set entrySet = map.entrySet();

    1、增强for循环

    for(Object entry : entrySet){
    Map.Entry m = (Map.Entry)entry;
    System.out.println(m.getKey() + "-" + m.getValue());
    }

    2、使用Iterator

    Iterator iterator = entrySet.iterator();
    while(iterator.hasNext()){
     Object next = iterator.next();
     // System.out.println(next.getClass());//HashMap$Node ->实现Map.Entry(getKey,getValue)
     // 向下转型 Map.Entry
     Map.Entry m = (Map.Entry)entry;
     System.out.println(m.getKey() + "-" + m.getValue());
    }

HashMap

  • 小结

    1、Map接口的常用实现类:HashMap、Hashtable 和 Properties

    2、HashMap 是 Map 接口使用频率最高的实现类

    3、HashMap 是以 key-value 对的方式来存储数据

    4、key 不能重复,但是是值可以重复,允许使用null键和null值

    5、如果添加相同的key,则会覆盖原来的key-value,等同于修改(key不会替换,value会替换)

    6、与HashSet一样,不保证映射顺序,因为底层是以hash表的方式来存储的(jdk1.8的hashMap底层是 ->数组+链表+红黑树)

    7、HashMap没有实现同步,因此是线程不安全的,方法没有做同步互斥的操作,没有synchronized

  • HashMap底层机制及源码剖析

    Java入门-集合
    image-20220104213332538

    1>(k,v)是一个Node实现了Map.Entry<K,V>,查看HashMap的源码可以看到

    2> jdk7.0的hashmap底层实现[数组+链表],jdk8.0底层[数组+链表+红黑树]

  • HashMap扩容机制

    1、HashMap底层维护了Node类型的数组table,默认为null

    2、当创建对象时,将加载因子(loadfactor)初始化为0.75

    3、当添加key-value时,通过key 的哈希值得到在table的索引,然后判断该索引处是否有元素,如果没有元素则直接添加;如果该索引处有元素,继续判断该元素的key是否和准备加入的key相同,如果相等,则直接替换val;如果不相等需要判断是树结构还是链表结构,做出相应处理。如果添加时发现容量不够,则需要扩容

    4、第1次添加,则需要扩容table容量为16,临界值(threshold)为16*0.75=12

    5、以后再扩容,则需要扩容table容量为原来的2倍,临界值为原来的两倍,即32*0.75=24,依此类推

    6、在java8中,如果一条链表的元素个数超过TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN_TREEIFY_CAPACITY(默认64),就会进行树化(转为红黑树)

  • HashMap源码解读

    1、执行构造器new HashMap(),初始化加载因子loadfactor = 0.75,HashMap$Node[] table = null

    Java入门-集合
    image-20220104215208840

    2、执行put

    public V put(K key, V value) // key = "1"  value=1
            return putVal(hash(key), key, value, falsetrue);
    }

    3、计算key的hash值

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

    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;
            // 如果底层的table数组为null,或者length=0,就扩容到16(resize()方法)
            if ((tab = table) == null || (n = tab.length) == 0)
                n = (tab = resize()).length;
            // 取得hash值对应的table索引位置的Node,如果为null,就直接把加入的K-V,创建成一个Node,直接加入到该位置
            if ((p = tab[i = (n - 1) & hash]) == null)
                tab[i] = newNode(hash, key, value, null);
            // 如果key的hash值存在,则具体判断值
            else {
                // 辅助变量
                Node<K,V> e; K k;
                // 如果table的索引位置的key的hash相同和新的key的hash值相同,并满足(存在的节点的key和准备加入的key是同一个对象,或者 equals相等)
                if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                    e = p;
                // 如果当前的table的已有的Node是红黑树,就按照红黑树处理
                else if (p instanceof TreeNode)
                    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
               // 如果找到的node,后面是链表,则循环比较
                else {
                    for (int binCount = 0; ; ++binCount) {
                        // 如果整个链表没有一个对象和想要加入的对象相同,就加入到链表的最后
                        if ((e = p.next) == null) {
                            p.next = newNode(hash, key, value, null);
                            // 加入后,判断当前链表的个数,是否已经达到8个,到8个,就进行树化
                            if (binCount >= TREEIFY_THRESHOLD - 1// -1 for 1st
                                // 树化(转成红黑树)
                                treeifyBin(tab, hash);
                            break;
                        }
                        // 如果在循环过程中,发现有key相同(equals或hash相等),则break,然后只替换value
                        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
                        e.value = value;
                    afterNodeAccess(e);
                    return oldValue;
                }
            }
            ++modCount;
            // 如果size>临界值(threshold)12-24-48....,则执行扩容
            if (++size > threshold)
                resize();
            afterNodeInsertion(evict);
            return null;
        }

    5、树化(转成红黑树)

    /**
         * Replaces all linked nodes in bin at index for given hash unless
         * table is too small, in which case resizes instead.
         */

         // 如果table为null,或者大小还没达到64,暂时不树化,而是进行扩容,否则才会真正的树化-->剪枝
        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)
                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);
            }
        }

    6、扩容算法

    /* 初始化或加倍表大小。如果为空,则分配
     * 与初始容量目标一致,保持在现场阈值。
     * 否则,因为我们用的是2的幂展开 
     * 每个bin中的元素必须保持相同的索引,或者移动
     **/

    在新表中,偏移量的幂为2
    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
                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;
        }

Hashtable

  • 类图
Java入门-集合
image-20220104224740181
  • Hashtable基本介绍

    1、存放的元素是键值对:即K-V

    2、hashTable的键和值都不能为null

    3、hashTable使用方法基本上和HashMap一样

    4、hashTable是线程安全的(synchronized),hashMap是线程不安全的

  • Hashtable底层介绍

    1、底层有数组 Hashtable$Entry[],初始化大小为11

    2、临界值/阈值 threshold 8 = 11*0.75

    3、扩容机制

    4、执行方法 addEntry(hash, key, value, index);添加K-V封装到Entry

    5、当if(count >= threshold)满足时,执行扩容

    6、按照 int newCapacity = (oldCapacity << 1) + 1;的大小进行扩容(2倍+1)

  • HashMap和Hashtable对比


    版本 线程安全(同步) 效率 允许Null键Null值
    HashMap 1.2 不安全 允许
    Hashtable 1.0 安全 较低 不允许

LinkedHashMap

LinkedHashMap通过在HashMap的基础上增加一条双向链表

TreeMap

底层源码同HashMap

Properties

  • 基本介绍

    1、Properties类继承Hashtable类并且实现了Map接口,也是使用一种键值对的形式来保存数据

    2、他的使用特点和Hashtable类似

    3、Properties还可以用于从XXX.properties文件中,加载数据到Properties类对象,并进行读取和修改

    4、说明:工作后,xxx.properties文件通常作为配置文件,这个知识点在IO流举例

五、Collections

  • Collections工具类介绍

    1、Collections是一个操作Set、List和Map等集合的工具类

    2、Collections中提供了一系列静态的方法对集合元素进行排序、查询和修改等操作

    排序操作(均为static方法)

    1、reverse(List):反转List中元素的顺序

    2、shuffle(List):对List集合元素进行随机排序

    3、sort(List):根据元素的自然顺序对指定List集合元素按升序排序

    4、sort(List,Compartor):根据指定的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(Collection,Comparator):根据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对象的所有旧值

六、小结

  • 在实际开发中,选择什么集合实现类,主要取决于业务操作特点,然后根据集合实现类特性进行选择,分析如下:

    1、先判断存储的类型(一组对象[单列]或一组键值对[双列])

    2、一组对象:Collection接口

    允许重复:List

    增删多:LinkedList [底层维护了一个双向链表]

    改查多:ArrayList [底层维护了Object类型的可变数组]

    不允许重复:Set

    无序:HashSet [底层是HashMap,维护了一个哈希表 即(数组+链表+红黑树)]

    排序:TreeSet

    插入和取出顺序一致:LinkedHashSet [维护数组+双向链表]

    3、一组键值对:Map

    键无序:HashMap [底层是:哈希表 jdk7:数组+链表,jdk8:数组+链表+红黑树]

    键排序:TreeMap

    键插入和取出顺序一致:LinkedHashMap

    读取文件:Properties


原文始发于微信公众号(Whisper Coding):Java入门-集合

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/255487.html

(0)
李, 若俞的头像李, 若俞

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!