章节5-集合

198 阅读24分钟

Collection

  • 特点: 单列集合

继承结构(JDK21)

  • 有序集合
    • HashSet , HashMap
  • 无序集合
    • 除了上面两个, 其他元素都或多或少的具有有序的特征, 统称为有序集合
  • List集合中存储的元素可以重复, Set集合中存储的元素不可重复

图片1.png

Collection接口

  • 常用方法

    • boolean add(E e); 向集合中添加元素

    • int size(); 获取集合中元素个数

    • boolean addAll(Collection c); 将参数集合中所有元素全部加入当前集合

    • boolean contains(Object o); 判断集合中是否包含对象o

    • boolean remove(Object o); 从集合中删除对象o

    • void clear(); 清空集合

    • boolean isEmpty(); 判断集合中元素个数是否为0

    • Object[] toArray(); 将集合转换成一维数组

    • @Test
      public void test01() {
          Collection c = new ArrayList();
          /**
           *for (int i = start; i < end; i++) {
           *    if (o.equals(es[i])) {
           *        return i;
           *    }
           *}
           *
           * contains()方法进行比较时,是调用equals方法进行比较, o代指"张三",
           * 因为String底层默认重写equals方法
           */
          String s = new String("张三");
          System.out.println(c.contains(s));  //true
      
          Object o1 = new Object();
          Object o2 = new Object();
          c.add(o1);
          System.out.println(c.contains(o2));  //false
      
            删除集合中的元素
          System.out.println(c);  //[100, hello, 张三, 李四, java.lang.Object@71a794e5]
          /**
           * for (; i < size; i++)
           *      if (o.equals(es[i]))
           *          break found;
           *}
           * remove同样是调用equals方法进行比较
           */
          c.remove(s);
          System.out.println(c);  //[100, hello, 李四, java.lang.Object@71a794e5]
      }
      
  • Collection中的迭代方法

    1. 迭代器遍历 --> Iterator iterator(); 获取迭代器, 该迭代器属于最高级(集合的通用遍历方式)

    • Collection<String> strings =   new ArrayList<>();
      
      strings.add("zhangsan");
      strings.add("lisi");
      strings.add("wangwu");
      
      Iterator<String> iterator = strings.iterator();
      
      while (iterator.hasNext()){
          System.out.println(iterator.next());
      }
      
    • 迭代器原理:

      • 指针先下移判断元素知否为null, 再进行获取元素

      • private class Itr implements Iterator<E> {
            int cursor;       // 指针,默认值为0
            int lastRet = -1; // (返回最后一个元素的索引;如果没有-1)
            int expectedModCount = modCount; //将之前记录的集合操作此数赋值给迭代器记录
            
            public boolean hasNext() {
                //size:实际元素的个数,指针指向的位置与元素个数判断是否相等
                /*
                例如:实际长度为3,获取到索引为3的元素后指针下移(此时cursor==4),
                	 与实际元素个数判断,不等,则终止遍历
                */
                    return cursor != size;	
                }
            
             public E next() {
                    checkForComodification();
                    int i = cursor;
                    if (i >= size)
                        throw new NoSuchElementException();
                    Object[] elementData = ArrayList.this.elementData;
                    if (i >= elementData.length)
                        throw new ConcurrentModificationException();
                    cursor = i + 1;	//指针先下移
                    return (E) elementData[lastRet = i];	//获取元素
                }
        
    1. 增强for循环

      • 底层还是迭代器遍历

      • @Test
        public void test2() {
            Collection<String> strings =   new ArrayList<>();
        
            strings.add("zhangsan");
            strings.add("lisi");
            strings.add("wangwu");
        
            for (String string : strings) {
                System.out.println(string);
            }
        }
        
    2. default void forEach(Consumer<? super T> action)方法遍历

      • Iterable接口中的默认方法

      • 源码 ==> 底层是增强for循环

        • default void forEach(Consumer<? super T> action) {
              Objects.requireNonNull(action);
              for (T t : this) {
                  action.accept(t);
              }
          }
          
      • 代码示例

        • @Test
          public void test3() {
              Collection<String> strings =   new ArrayList<>();
          
              strings.add("zhangsan");
              strings.add("lisi");
              strings.add("wangwu");
          
              strings.forEach(new Consumer<String>() {  //Consumer: 函数式接口
                  @Override
                  public void accept(String s) {
                      System.out.println(s);
                  }
              });
              
              strings.forEach(s->System.out.println(s));
          }
          
  • 集合迭代过程中的并发修改异常

    • fail-fast 快速失败机制:

      /**
       * 对集合进行增删改查时,会使用modCound进行记录, 每一次执行添加,删除,修改都会使modCount+1
       * 当new出迭代器时,会先将modConunt赋值给expectedModCount: expectedModCount=modCount
       *
       * 若在迭代的过程中,使用集合删除元素,则会执行fastRemove方法,进行删除元素,同时modCount++
       *
       * 执行完删除操作后,会将expectedModCount值与modCount进行比较,若不相等,抛出并发修改异常
       *
       * 当使用迭代器进行删除时,也是调用fastRemove方法,只是调用完后会将modCount值加重新赋值给expectedModCount
       *
       * 这种检查机制就是fail-fast快速失败机制,避免并发修改问题
       */
      
    • @Test
      public void test03() {
          Collection<String> c = new ArrayList<>();
          c.add("zhangsan");
          c.add("lisi");
          c.add("wangwu");
          c.add("zhaoliu");
      
          Iterator<String> iterator = c.iterator();
          while (iterator.hasNext()) {
              String name = iterator.next();
              if (name.equals("lisi")) {
                  //Collection接口自带的remove方法进行删除
                  c.remove(name);
                  iterator.remove();  //迭代器删除时,不报错
              }
          }
      }
      

List接口

  • List集合存储元素特点:

    • 元素可重复
    • 有索引: 可以通过索引处理元素
  • 常用方法(只适合List家族使用的方法,这些方法都和下标有关系)

    • void add(int index, E element); 在指定索引处插入元素

    • E set(int index, E element); 修改索引处的元素

    • E get(int index); 根据索引获取元素

    • E remove(int index); 删除索引处的元素

    • int indexOf(Object o); 获取对象o在当前集合中第一次出现时的索引。

    • int lastIndexOf(Object o); 获取对象o在当前集合中最后一次出现时的索引。

    • List subList(int fromIndex, int toIndex); 截取子List集合生成一个新集合(对原集合无影响)

    • static List of(E... elements); 静态方法, 返回包含任意数量元素的不可修改列表 (获取的集合是只读的, 不可修改的)

  • List接口特有迭代方法

    1. ListIterator迭代器遍历 --> ListIterator listIterator(): 获取List集合特有的迭代器(该迭代器功能更加强大,但只适合于List集合使用)

    • boolean hasNext(); 判断光标当前指向的位置是否存在元素。

    • E next(); 将当前光标指向的元素返回,然后将光标向下移动一位。

    • boolean hasPrevious(); 判断当前光标指向位置的上一个位置是否存在元素。

    • E previous(); 获取上一个元素(将光标向上移动一位,然后将光标指向的元素返回)

    ​ ==注:== 使用倒序遍历时 , 需要先进行正序遍历 , 使指针指向末尾

    • int nextIndex(); 获取光标指向的那个位置的下标

    • int previousIndex(); 获取光标指向的那个位置的上一个位置的下标

    ​ ==注:==

    • /**
       * ListIterator: 包含add(), set(), remove()方法, 也就是为了避免并发修改异常,
       * 一般对集合进行数据修改等操作都是使用迭代器进行处理
       * <p>
       * 使用迭代器中的set() remove()方法时,
       * 不是根据索引处理,而是处理next()方法取出的元素;所以使用set() remove()时,需要先执行next()方法;
       */
      
    • void add(E e); 添加元素(将元素添加到光标指向的位置,然后光标向下移动一位。)

    • void set(E e); 修改的是上一次next()方法返回的那个数据 (修改的是集合中的) . set()方法调用的前提是:先调用了next()方法。不然会报错。

    • void remove(); 删除上一次next()方法返回的那个数据 (删除的是集合中的) . remove()方法调用的前提是:先调用next()方法。不然会报错。

    • 代码示例

    • @Test
      public void test2() {
          List<String > list = new ArrayList<>();
      
          list.add("hello");
          list.add("world");
          list.add("java");
          list.add("java");
      
          ListIterator<String> listIterator = list.listIterator();
          while (listIterator.hasNext()) {
              System.out.println(listIterator.nextIndex());  //获取指针索引
              System.out.println(listIterator.next());
          }
      }
      
    1. List接口使用 get() 方法遍历

      • @Test
        public void test1() {
            List<String > list = new ArrayList<>();
        
            list.add("hello");
            list.add("world");
            list.add("java");
            list.add("java");
        
            for (int i = 0; i < list.size(); i++) {
                System.out.println(list.get(i));
            }
        }
        
  • ==List接口特有排序方法, Java8新增== --> void sort( Comparator<? super E> comparator) : Comparator--> 比较器

    • /**
       * 数组排序对象时实现了Comparable接口,重写了compareTo()方法 --> 返回int类型判断
       *
       * List排序
       * default void sort(Comparator<? super E> c)
       * <p>
       * Comparator 比较器
       * Comparable接口只有一个抽象方法,是一个函数式接口
       */
      
    • 代码示例:

    • @Test
      public void test03() {
          List<Student> list = new ArrayList<>();
          list.add(new Student(18, "张三"));
          list.add(new Student(19, "李四"));
          list.add(new Student(20, "王五"));
      
          /**
           * 按照年龄从大到小排序
           *
           * 之前想要对对象进行排序需要让对象实现Comparable接口,重写compareTo()方法
           *
           * 现在对对象排序只需要传入一个Comparator接口即可
           */
          list.sort(new Comparator<Student>() {
              @Override
              public int compare(Student o1, Student o2) {
                  return o2.getAge() - o1.getAge();
              }
          });
      
          ListIterator<Student> listIterator = list.listIterator();
          while (listIterator.hasNext()) {
              System.out.println(listIterator.next());
          }
      }
      
ArrayList实现类测试
  • 特点

    • ArrayList集合底层采用了数组这种数据结构。
    • 优点
      • 查询效率高: 由于数组中存储的使想同数据结构且连续的线性结构, 所以可以根据索引与偏移量直接计算出需要查询的地址 , 时间复杂度使O(1)
    • 缺点
      • 在数组中间增删效率低, 由于需要保持内存的连续性, 若对数组中间进行增删操作, 则会涉及数组整体移位(对末尾增删不受影响) , 时间复杂度O(n)
  • 构造方法分析

    • 初始化容量: 0 , 即只是创建ArrayList,不添加元素时 , 集合容量为0 . (1.8版本前创建ArrayList就直接给10个容量)

      • /**
         * public ArrayList() {
         *         // Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};  
         *         this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;  
         *}
         */
        ArrayList<String> list = new ArrayList<>();
        
  • 添加方法分析

    • 执行添加方法时 , 先判断集合长度是否与size(实际包含元素的个数)是否相等, 若相等, 则执行扩容操作

      • private void add(E e, Object[] elementData, int s) {  //s就是size
            if (s == elementData.length)
                elementData = grow();
            elementData[s] = e;
            size = s + 1;
        }
        
    • ==扩容原理解析== (每次扩容都是原容量的二分之一, 即每次扩容长度为原数组长度的1.5倍)

      • private Object[] grow(int minCapacity) {  //minCapacity(最小容量) = size +1 ==> 避免索引越界
            int oldCapacity = elementData.length;
            if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                int newCapacity = ArraysSupport.newLength(oldCapacity,
                        minCapacity - oldCapacity,   //值为1 
                        oldCapacity >> 1      //当前数组长度的二分之一
                return elementData = Arrays.copyOf(elementData, newCapacity);
            } else {//只执行一次,就是第一个添加方法执行时执行int DEFAULT_CAPACITY = 10;即立即给10个容量
                return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
            }
        }
        
                                                          
        public static int newLength(int oldLength, int minGrowth, int prefGrowth) {
            //minGrowth = 1, prefGrowth = 当前数组长度的二分之一. 即扩容长度为原数组长度的1.5倍
            int prefLength = oldLength + Math.max(minGrowth, prefGrowth); 
            if (0 < prefLength && prefLength <= SOFT_MAX_ARRAY_LENGTH) {
                return prefLength;
            } else {
                // put code cold in a separate method
                return hugeLength(oldLength, minGrowth);
            }
        }                                               
        
  • 修改方法分析

    • 没啥说的, 就获取索引赋值而已
  • 插入方法分析

    • 与删除类似,先获取对应索引,再获取后续元素,再覆盖,最后将元素赋值

    • public void add(int index, E element) {
          rangeCheckForAdd(index);
          modCount++;
          final int s;
          Object[] elementData;
          if ((s = size) == (elementData = this.elementData).length)
              elementData = grow();
          System.arraycopy(elementData, index,
                           elementData, index + 1,
                           s - index);
          elementData[index] = element;
          size = s + 1;
      }
      
  • 删除方法分析

    • /**
       * 删除集合元素(list特有的删除方法)
       * System.arraycopy(es, i + 1, es, i, newSize - i);
       *获取需要删除的索引,将该索引后续所有的元素全部获取
       * 将被获取到数组从原数组索引位置开始拷贝覆盖
       * 最后将最后的元素从新赋值为null
       */
       list.remove(0);
      
      /**
       * Collection的删除方法
       * 流程:
       *  先遍历数组,获取到对应索引,再调用 fastRemove(es, i)与上面删除方法类似
       */
      list.remove("bbb");
      
Vector实现类测试
  • 特点

    • Vector底层也是数组,和ArrayList相同
    • Vector几乎所有的方法都是线程同步的(被synchronized修饰:线程排队执行,不能并发),因此Vector是线程安全的,但由于效率较低,很少使用。因为控制线程安全有新方式。
  • 构造方法分析

    • Vector初始化就给10个容量: public Vector() { this(10); }

    • 自动扩容(与ArrayList类似) : 每次扩容都是原容量的大小, 即原容量的2倍

      • private Object[] grow(int minCapacity) {
            int oldCapacity = elementData.length;
            int newCapacity = ArraysSupport.newLength(oldCapacity,
                    minCapacity - oldCapacity, 
                    //oldCapacity原容量
                    capacityIncrement > 0 ? capacityIncrement : oldCapacity);
            return elementData = Arrays.copyOf(elementData, newCapacity);
        }
        
LinkedList实现类测试
  • 特点

    • 底层是双链表

      img转存失败,建议直接上传图片文件
    • 代码实现双链表结构:

      • 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;
            }
        }
        
    • 链表优点:

      • 因为链表节点在空间存储上,内存地址不是连续的。因此删除某个节点时不需要涉及到元素位移的问题。因此随机增删元素效率较高。时间复杂度O(1)
    • 链表缺点:

      • 链表中元素在查找时,只能从某个节点开始顺序查找,因为链表节点的内存地址在空间上不是连续的。链表查找元素效率较低,时间复杂度O(n)
  • 添加方法分析

    • LinkedList的add方法,默认添加到链表的最后

      • void linkLast(E e) {
            final Node<E> l = last;	//将末尾结点使用临时变量存储
            /**
            * 创建新节点:
            * 	l: 上一个结点的地址
            * 	e: 元素
            * 	null: 下一个结点的地址, 由于当前结点为末尾节点,所以Node.next值为null
            */
            final Node<E> newNode = new Node<>(l, e, null);	
            last = newNode;	//将该结点赋值给last, 重新刷新末尾结点地址
            if (l == null)
                first = newNode;
            else
                l.next = newNode; //将前节点的Node.next值存储为当前结点地址
            size++; //长度加一
            modCount++;
        }
        
  • 修改方法分析

  • ==插入方法分析==

    • public void add(int index, E element) {
          checkPositionIndex(index);
          if (index == size)
              linkLast(element);
          else
              linkBefore(element, node(index));  //若索引值不是末尾则执行该方法
      }
      
      Node<E> node(int index) {	//先获取对应索引的节点node(index) 
          if (index < (size >> 1)) {	//先判断对应索引是否大于中间值,以决定从头开始遍历还是从尾部
              Node<E> x = first;
              for (int i = 0; i < index; i++)
                  x = x.next;
              return x;
          } else {
              Node<E> x = last;	//获取尾部结点的地址 
              //遍历链表,从尾部结点开始遍历,每循环一次都会获取到前一个结点,直到循环结束,则获取到对应结点
              for (int i = size - 1; i > index; i--)	
                  x = x.prev;
              return x;
          }
      }
      
      void linkBefore(E e, Node<E> succ) {	//succ: 对应索引的节点
          final Node<E> pred = succ.prev;	//获取当前节点的前一个节点地址
          final Node<E> newNode = new Node<>(pred, e, succ);	//创建一个节点,同时存储前一个节点,下一个节点
          succ.prev = newNode;	//新节点的地址赋值给当前节点的前一个节点地址
          if (pred == null)
              first = newNode;
          else
              pred.next = newNode;	//同上
          size++;
          modCount++;
      }
      
  • 删除方法分析

Queue接口

  • 特点

    • 单向队列, 一般使用双端队列, 也就是其子类接口Deque
  • Queue接口基于Collection扩展的方法:

    • boolean offer(E e); 入队。

    • E poll(); 出队,如果队列为空,返回null。

    • E remove(); 出队,如果队列为空,抛异常。

    • E peek(); 查看队头元素,如果为空则返回null。

    • E element(); 查看对头元素,如果为空则抛异常。

Deque子接口
  • Deque接口基于Queen接口扩展的方法:

    • 以下2个方法可模拟队列:

      • boolean offerLast(E e); 从队尾入队
      • E pollFirst(); 从队头出队
    • 以下4个方法可模拟双端队列:

      • boolean offerLast(E e); 从队尾入队

      • E pollFirst(); 从队头出队

      • boolean offerFirst(E e); 从队头入队

      • E pollLast(); 从队尾出队

ArrayDeque实现类测试
  • 特点

    • 实现了Deque接口 , 所以是可以实现双端队列结构

    • 底层是环形数组 , 同时也实现了栈结构

Set接口

  • Set集合存储元素特点:

    • ==无索引 :== 可以通过索引处理元素, HashSet底层为数组加链表, 理论上有索引, 但无法通过索引操作元素, 所以被认为无索引

    • 元素不可重复 : 相对而言, 可以通过比较器定义是否需要存储重复元素

      • //根据年龄做主要排序(降序)
        int ageResult = o.age - this.age;
        //根据年龄相同的情况下,根据姓名做次排序(升序)
        int nameResult = ageResult == 0 ? this.name.compareTo(o.name) : ageResult;
        //同姓名,同年龄需要保留
        return nameResult == 0 ? 1 : nameResult; //如果相同,则强制性返回为1,就可以骗过比较器不重复
        
  • Set接口无自身特有方法 , 所有方法皆继承于 Collection接口

HashSet实现类测试
  • HashSet集合底层是new了一个HashMap。往HashSet集合中存储元素实际上是将元素存储到HashMap集合的key部分。

  • //静态常量,内存中只有一个
    private static final Object PRESENT = new Object();
    
    public boolean add(E e) {
        //使用PRESENT将value填充
            return map.put(e, PRESENT)==null;
        }
    
  • HashMap集合的key是无序不可重复的,因此HashSet集合就是无序不可重复的。

  • HashMap集合底层是哈希表/散列表数据结构,因此HashSet底层也是哈希表/散列表。

  • 根据源码可以看到向Set集合中add时,底层会向Map中put。value只是一个固定不变的常量,只是起到一个占位符的作用。主要是key。

  • @Test
    public void test01(){
        /**
         * HashSet构造器底层是调用的HashMap
         * public HashSet() {
         *    map = new HashMap<>();
         *}
         */
        HashSet<String> hashSet = new HashSet<>();
    
        /**
         * HashSet的add()方法也是调用的put方法,将数据存入HashMap的key中
         * public boolean add(E e) {
         *     return map.put(e, PRESENT)==null;
         * }
         */
        hashSet.add("1");
    }
    
LinkedHashSet实现类测试
  • 底层调用的LinkedHashMap

  • 同上, 底层调用的HashMap的put()方法

  • @Test
    public void test02(){
        /**
         * 
         * HashSet(int initialCapacity, float loadFactor, boolean dummy) {
         *    map = new LinkedHashMap<>(initialCapacity, loadFactor);
         *}
         */
        LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();
    
        //同上,底层调用的HashMap的put()方法
        linkedHashSet.add("001");
        linkedHashSet.add("002");
    
        System.out.println(linkedHashSet);  //[001, 002]:存储顺序与取出顺序一致
    }
    
SortedSet接口
TreeSet实现类测试
  • TreeSet集合底层是new了一个TreeMap。往TreeSet集合中存储元素实际上是将元素存储到TreeMap集合的key部分。

  • TreeMap集合的key是不可重复但可排序的,因此TreeSet集合就是不可重复但可排序的。

  • TreeMap集合底层是红黑树,因此TreeSet底层也是红黑树。它们的排序通过java.lang.Comparable和java.util.Comparator均可实现。

Map

  • 特点:
    • Map集合以key和value的键值对形式存储。key和value存储的都是引用
    • Map集合中key起主导作用。value是附属在key上的
    • Map集合的key都是不可重复的。key重复的话,value会覆盖

继承结构(JDK21)

图片2.png

Map接口

  • 常用方法

    • V put(K key, V value) 添加键值对

    • void putAll(Map<? extends K,? extends V> m) 添加Map集合

    • boolean containsKey(Object key) 是否包含某个key

    • boolean containsValue(Object value) 是否包含某个value

    • V remove(Object key) 通过key删除key-value

    • void clear() 清空Map

    • int size() 键值对个数

    • boolean isEmpty() 判断是否为空Map

    • V get(Object key) 通过key获取value

    • Set keySet() 获取所有的key

    • Collection values() 获取所有的value

    • Set<Map.Entry<K,V>> entrySet() 将Map集合转为Set集合(迭代方法)

    • //Map的迭代方式
      Set<Map.Entry<Integer, String>> entries = map.entrySet();
      for (Map.Entry<Integer, String> entry : entries) {
          System.out.println(entry);
      }
      
    • default void forEach(BiConsumer<? super K, ? super V> action) Map默认方法, 通用遍历集合方式

    • //使用默认方法forEach()遍历Map集合
      map.forEach(new BiConsumer<Integer, String>() {
          @Override
          public void accept(Integer key, String value) {
              System.out.println(key+"="+value);
          }
      });
      

HashMap实现类测试

  • 集合特性

    • key和value的键值对形式存储
    • 无序, key不可重复, key具有唯一性, key起主导作用。value是附属在key上的
    • 由于是散列表结构, 增删与查询效率都比较快
    • 缺点: 扩容成本大
  • 内存结构

    • 数组 + 单向链表 + 红黑树

    • transient Node<K,V>[] table;
      
      /*HashMap结构:
            底层是Node[]数组,Node就是结点*/
      static class Node<K,V> implements Map.Entry<K,V> {
          final int hash; //hash值
          final K key;
          V value;
          Node<K,V> next; //下一个结点的地址
      }
      

image-20241211200909988.png

  • 存储原理-->put() 方法 : 里面包含初始化数组长度, 是否需要树化操作, 是否需要扩容操作

    • 存入数据时, 先根据存入对象的key计算出hash值 , 然后使用这个hash值与数组长度取余计算. 计算出需要插入数组中的索引位置. 然后判断该位置是否存在是元素, 若没有元素, 则直接插入. 若有元素, 则开始遍历该索引位置的所有结点 , 将存入数据hash值依此与这些结点中hash值比对, 若hash值相同, 则覆盖value , 返回oldValue. 若未找到相同hash值的结点对象. 则执行尾插, 返回value. 插入完之后, 元素个数size++ , 然后判断是否需要扩容.
    • 源码解析
    •  final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
           Node<K, V>[] tempTable; //临时teble 变量
           Node<K, V> IndexNode;  //tempTable[index] 计算出的索引位置结点
           int tableLength;    //数组长度
           int index;  //通过hash值算出的索引//第一次调用put()方法时执行
           if ((tempTable = table) == null || (tableLength = tempTable.length) == 0) {  
               tableLength = (tempTable = resize()).length;  //将初始化后的数组长度赋值给tableLength
           }
       ​
           //判断tempTable[index]是否为null,即数组中第一个结点是否时null
           if ((IndexNode = tempTable[index = (tableLength - 1) & hash]) == null) { 
               tempTable[index] = new Node(hash, key, value, null);  //为null 直接插入
       ​
           } else {  //执行到这里,说明索引位置有结点存在,即遍历链表,使用equals()方法判断key是否相同
               Node<K, V> e;  //结点临时变量,用于获取当前链表的最后结点对象或相同key的Node结点对象
               K k; //该索引位置链表中第一个结点的key对象//与对应索引位置的第一个结点的key对象是否相同. 这样判断可以减少遍历
               if (IndexNode.hash == hash&&((k=IndexNode.key) == key||(key!= null && key.equals(k)))){
                   e = IndexNode;
               }else if (IndexNode instanceof TreeNode) {  //判断是否是红黑树结构
                   e = ((TreeNode<K, V>) IndexNode).putTreeVal(this, tempTable, hash, key, value);
       ​
                   //执行到这里说明传入的结点对象不是第一个,必须开始遍历链表
               } else {
                   for (int binCount = 0; ; ++binCount) {  //遍历链表中的所有结点
                       e = IndexNode.next;
                       if (e == null) {  //遍历到末尾,e==null
                           IndexNode.next = new Node(hash, key, value, null); //开始尾插if (binCount >= 8 - 1) {  //同时判断链表长度,是否需要>=8
                               treeifyBin(tempTable, hash);
                           }
                           break;
                       }
                       //判断结点对象中的key是否与传入的key相等,若相等,则停止遍历,由于e = IndexNode.next;                        停止遍历时,也就拿到了相同key结点的对象
                       if(e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))){ 
                           break;
                       }
       ​
                       IndexNode = e;
                   }
               }
               //执行到这里,说明有相同key的结点,则覆盖value值并返回oldValue值即可. e就是对应结点对象
               if (e != null) { 
                   V oldValue = e.value;
                   if (!onlyIfAbsent || oldValue == null) {
                       e.value = value;
                   }
                   return oldValue;
               }
           }
       ​
           ++modCount;
           //threshold = 当前数组长度与负载因子DEFAULT_LOAD_FACTOR计算出的数组扩容阈值
           if (++size > threshold) {  
               resize();
           }
       ​
           return null;
       }
      
    • 手写put()方法 :

    • /**
       * 添加元素
       *
       * @param key
       * @param value
       * @return oldValue
       */
      public V put(K key, V value) {
          /*
          【第一步】:处理key为null的情况
          如果添加键值对的key就是null,则将该键值对存储到table数组索引为0的位置。
           */
          if (key == null) {
              return putForNullKey(value);
          }
          //key不为0
          //判断是否是首位
          int hash = key.hashCode();
          int index = Math.abs(hash % table.length);  //hash值可能为复数
          Node<K, V> node = table[index];
          Node<K, V> newNode = new Node<>(hash, key, value, null);
          if (null == node) {
              table[index] = newNode;
              size++;
              return value;
          }
          //不是首位,判断hash值是否重复
          Node<K, V> lastNode = null;
          while (null != node) {
              if (hash == node.hash) {
                  V oldValue = node.value;
                  node.value = value;
                  return oldValue;
              }
              lastNode = node;
              node = node.next;
          }
          //无重复值,执行尾插
          lastNode.next = newNode;
          return value;
      }
      
      private V putForNullKey(V value) {
          //如果索引0没有元素,直接添加
          Node<K, V> node = table[0];
      
          Node<K, V> newNode = new Node<K, V>(0, null, value, null);
          if (node == null) {
              table[0] = newNode;
              size++;
              return value;
          }
          //执行到这里,说明table[0]有结点
          /**
           *遍历table[0]中所有的结点
           * while (node != null){ //node代指table[0]的第一个结点
           *    node = node.next;  //将当前结点的下一个结点的内存地址赋值. 当遍历到最后一个结点时,node.next == null
           *}
           */
          Node<K, V> lastNode = null;  //获取到最后的结点
          while (node != null) {
              if (node.key == null) {
                  V oldValue = node.value;
                  node.value = value;
                  return oldValue;
              }
              lastNode = node;
              node = node.next;
          }
          //执行到这里,则说明链表中没有key==null的结点,将新结点进行尾插
          lastNode.next = newNode;
          size++;
          return value;
      }
      
    • 手写get()方法 :

    • /**
       * 通过key获取value
       */
      public V get(K key) {
          //先判断key是否为null
          if (null == key) {
              Node<K, V> node = table[0];
              if (null == node) {
                  return null;
              }
              //不是null,遍历链表
              while (node != null) {
                  if (node.key == null) {
                      return node.value;
                  }
                  node = node.next;
              }
          }
          //key不是null
          int hash = key.hashCode();
          int index = Math.abs(hash % table.length);
          Node<K, V> node = table[index];
          //健壮性判断,防止乱传key
          if (null == node) {
              return null;
          }
          while (node != null) {
              if (node.key.equals(key)) {
                  return node.value;
              }
              node = node.next;
          }
          return null;
      }
      
  • HashMap的遍历原理

    • 集合的遍历顺序:

      • 从0索引开始遍历,取出0索引中所有的结点,再往下遍历,直到遍历完所有的结点
    • 手写toString()方法:

    • /**
           * 重写toString,直接输出map集合
           */
          @Override
          public String toString() {
              StringBuilder stu = new StringBuilder();
              for (int i = 0; i < table.length; i++) {
                  Node<K, V> node = table[i];
                  while (null != node) {
                      stu.append("{");
                      stu.append(node);
                      stu.append("}");
                      node = node.next;
                  }
              }
              return stu.toString();
          }
      
  • HashMap在Java8后的改进

    • 初始化时机:

      • Java8之前,构造方法执行时初始化table数组。
      • Java8之后,第一次调用put方法时初始化table数组。
    • 插入法:

      • Java8之前,头插法
      • Java8之后,尾插法
    • 数据结构:

      • Java8之前:数组 + 单向链表

      • Java8之后:数组 + 单向链表 或 + 红黑树。

      • 树化条件

        • if (binCount >= TREEIFY_THRESHOLD - 1){ //TREEIFY_THRESHOLD == 8
              treeifyBin(tab, hash);
          } // -1 for 1st
                                      
          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;
              }
          }
          
        • 当任意链表长度大于等于8时, 数组长度小于64

          • 执行扩容操作, 打乱全部结点重新插入
        • 当任意链表长度大于等于8时, 且数组长度大于等于64

          • 将链表转化为红黑树
      • 当删除红黑树上的结点时,结点数量 <= 6 时。红黑树转换为单向链表。

      • static final int TREEIFY_THRESHOLD = 8;  //红黑树化阈值
        
        static final int UNTREEIFY_THRESHOLD = 6;  //取消红黑树阈值
        
        static final int MIN_TREEIFY_CAPACITY = 64;  //进行树化的最小表容量
        
  • HashMap的容量问题

    • HashMap集合初始化容量16(第一次调用put方法时初始化)

    • HashMap集合的容量永远都是2的次幂(包括自动扩容),假如给定初始化容量为27,它底层也会变成32的容量。

    • //Returns a power of two size for the given target capacity. (返回给定目标容量的2次幂大小。)
      static final int tableSizeFor(int cap) {
          int n = -1 >>> Integer.numberOfLeadingZeros(cap - 1);
          return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
      }
      
      • 作用 :
        • 加快哈希计算

          • hash & (length-1) 的结果和 hash % length的结果相同
          • 注意:只有是2的次幂时,以上等式才会成立。因为了使用 & 运算符
        • 减少哈希冲突

          • 底层运算是:hash & length - 1
          • 如果length是偶数:length-1后一定是奇数,奇数二进制位最后一位一定是1,1和其他二进制位进行与运算,结果可能是1,也可能是0,这样可以减少哈希冲突,让散列分布更加均匀。
          • 如果length是奇数:length-1后一定是偶数,偶数二进制位最后一位一定是0,0和任何数进行与运算,结果一定是0,这样就会导致发生大量的哈希冲突,白白浪费了一半的空间。
    • HashMap初始化容量设置值

      • 自动扩容原理(判断的是元素个数)

      • //在put()方法底部判断是否需要扩容
        ++modCount;
        if (++size > threshold)  //threshold : 数组长度与负载因子算出来的值
            resize();
        
        • 当执行put()操作的时候,如果HashMap中存储==元素的个数==超过“数组长度* loadFactor”的结果(loadFactor指的是负载因子,loadFactor的默认值一般为0.75),那么就需要执行数组扩容操作.(loadFactor可以自定义)

        • public HashMap(int initialCapacity, float loadFactor) {
              this.loadFactor = loadFactor;  //负载因子
              this.threshold = tableSizeFor(initialCapacity);
          }
          
      • HashMap扩容就是数组长度变化. 而每一个结点的存入的索引位置是根据hash值与数组长度取余计算的, 若长度变化, 那将会使HashMap中所有的结点根据新长度重新计算索引, 重新插入. 这是非常消耗性能的, 所以创建集合的时候尽量给出一个比较合理的长度值. 如此假如需要存储1000个元素, 若给1024的长度实际可以存储的容量 ==> 1024 * 0.75 = 768.

  • ==多学一招:==

    • 当对象根据内容计算出hash值后, 存入HashMap中后. 此时再修改对象属性值, 不会重新计算hash值重新插入. 而是使用之前的hash值存入的位置.
LinkedHashMap
  • 特点:

    • LinkedHashMap集合和HashMap集合的用法完全相同

    • 不过LinkedHashMap可以==保证插入顺序与取出顺序一致==

    • 底层采用了双向链表来记录顺序

image-20241210163722422.png

  • LinkedHashMap集合底层采用的数据结构是:哈希表 + 双向链表

  • static class Entry<K,V> extends HashMap.Node<K,V> {
        Entry<K,V> before, after;  //使用Entry对象记录插入顺序
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }
    
  • 构造方法, put()等方法底层也都是调用HashMap的

SortedMap接口

TreeMap实现类测试
  • 元素在集合中是有序排列的。(key为主导, 所以可排序是key可以排序)

  • 底层为红黑树结构(自平衡二叉树)

    • 相较与平衡二叉树, 增删效率更高一点. 因为平衡二叉树为了保证每个节点的子节点高度差不超过1, 则每一次增删操作都会使结构进行左右旋进行调整. 而红黑树结构的条件没有这么苛刻, 对元素修改后整体结构不一定会旋转修改.
  • 红黑树的特点:

    1. 节点由红色和黑色组成
    2. 根节点必须使黑色
    3. 没有子节点,则指向Nil节点. Nil节点必须使黑色
    4. 任意节点的到Nil节点的相对路径中的黑色节点数量相同
    5. 两个相邻的节点不能同为红色.
  • 这些约束强化了红黑树的关键性质:从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。这样就让树大致上是平衡的。

  • ==注意:==

    • 当同时存在自然排序与比较器排序的排序规则时,会优先执行比较器排序规则
  • TreeMap 排序 (自然排序)

    • 类实现Comparable接口(自然排序),重写comparTo()方法,在comparTo()方法中定义排序规则

    • /*
      根据方法的返回值, 来组织排序规则
      - 负数 : 左边走
      - 正数 : 右边走
      - 0  : 不存 
      */
      @Override
      public int compareTo(Student o) {
          //根据年龄做主要排序(降序)
          int ageResult = o.age - this.age;
          //根据年龄相同的情况下,根据姓名做次排序(升序)
          int nameResult = ageResult == 0 ? this.name.compareTo(o.name) : ageResult;
          //同姓名,同年龄需要保留
          return nameResult == 0 ? 1 : nameResult;
      }
      
  • TreeMap 排序 (比较器排序)

    • 使用Compartor比较器排序: 在创建TreeSet集合的构造器中传入Comparator比较器对象

    • TreeMap <String> strings = new TreeMap <>(new Comparator<String>() {
          @Override
          public int compare(String o1, String o2) {
              //按照长度排序
              return o1.length() - o2.length() == 0 ? 1 : o1.length() - o2.length();
          }
      });
      

Hashtable实现类

  • Hashtable和HashMap一样,底层也是哈希表。
  • Hashtable是线程安全的,方法上都有synchronized关键字。使用较少,因为保证线程安全有其他方式。
  • Hashtable的初始化容量:11。默认加载因子:0.75
  • Hashtable的扩容策略:2倍。
Properties实现类测试
  • 特点:
    • Properties被称为属性类。通常和xxx.properties属性文件一起使用。
    • Properties的父类是Hashtable。因此Properties也是线程安全的。
    • Properties不支持泛型,key和value只能是String类型。
  • Properties相关方法:
    • Object setProperty(String key, String value); 和put方法一样。

    • String getProperty(String key); 通过key获取value

    • Set propertyNames(); 获取所有的key

Collections

  • Collections是 Java 中的一个工具类,位于java.util包中。它提供了一系列对集合(如ListSetMap等)进行操作的静态方法,这些方法可以用于排序、搜索、修改等多种操作,极大地丰富了集合的功能。

  • 常用方法

    • public static boolean addAll(Collection<? super T> c, T... elements) 给集合对象批量添加元素

    • public static void shuffle(List<?> list) 打乱List集合元素的顺序

    • public static int binarySearch (List list, T key) 以二分查找法查找元素

    • public static void max/min(Collection coll) 根据默认的自然排序获取最大/小值

    • public static void swap(List<?> list, int i, int j) 交换集合中指定位置的元素

泛型

定义

  • 在类上定义泛型

    • /**
       * 泛型测试(在类上定义泛型)
       *
       * <T> T(Type)只是一个代称, 可以写任意字符. 当MyClass<String> myClass = new MyClass<>("liny")执行后
       * 后续会就获取到<String>类型,所以后续代码都改为String类型了
       */
      public class MyClass<T> {
          private T name;
      }
      
  • 在接口上定义泛型

    • /**
       * 在接口中定义泛型(与类一样)
       */
      public interface MyInterface<T> {
          void show(T product);
      }
      
  • 在静态方法上定义泛型

    • /**
       * 在静态方法上定义泛型
       */
      public class MyStatic  {
          public static <T> void show(T product){
              System.out.println("展示: "+product);
          }
      }
      

使用

  • 无限定通配符 : <?>, 此处 ? 可以是任意引用数据类型
  • 上限通配符 : <? extends Comparable>, 此处 ? 只能是Comparable 或 Comparable 的子类
  • 下限通配符 : <? super Comparable>, 此处 ? 只能是Comparable 或 Comparable 的父类