第十章 集合

文章目录

10.1 集合

10.1.1 定义

  • 定义:集合类主要负责保存、盛装其他数据,因此集合类也被称为容器类。它主要包括CollectionMap集合。

  • 集合只能存放对象,Java中每一种基本数据类型都有对应的引用类型。例如在集合中存储一个int型数据时,要先自动转换成Integer类后再存入。

  • 集合存放的是对对象的引用,对象本身还是存放在堆内存中

  • 集合可以存放不同类型不限数量的数据类型。

  • 在传统模式下,把一个对象“丢进”集合中后,集合会忘记这个对象的类型。也就是说,系统把所有的集合元素都当成 Object 类型。从 Java 5 以后,可以使用泛型来限制集合里元素的类型,并让集合记住所有集合元素的类型

10.1.2 框架体系

  1. Collection接口的基本结构:

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  2. Map接口的基本结构:

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • 在 图 1 和图 2 中,黄色块为集合的接口,蓝色块为集合的实现类。表 1 介绍了这些接口的作用。

    接口名称作 用
    Iterator 接口集合的输出接口,主要用于遍历输出(即迭代访问)Collection 集合中的元素,Iterator 对象被称之为迭代器。迭代器接口是集合接口的父接口,实现类实现 Collection 时就必须实现 Iterator 接口。
    Collection 接口是 List、Set 和 Queue 的父接口,是存放一组单值的最大接口。所谓的单值是指集合中的每个元素都是一个对象。一般很少直接使用此接口直接操作。
    Queue 接口Queue 是 Java 提供的队列实现,有点类似于 List。
    Dueue 接口是 Queue 的一个子接口,为双向队列
    List 接口是最常用的接口。是有序集合,允许有相同的元素。使用 List 能够精确地控制每个元素插入的位置,用户能够使用索引(元素在 List 中的位置,类似于数组下标)来访问 List 中的元素,与数组类似。
    Set 接口不能包含重复的元素
    Map 接口是存放一对值的最大接口,即接口中的每个元素都是一对,以 key➡value 的形式保存
  • 对于 Set、List、Queue 和 Map 这 4 种集合,Java 最常用的实现类分别是 HashSetTreeSetArrayListArrayDueueLinkedListHashMapTreeMap 等。表 2 介绍了集合中这些常用的实现类。

    类名称作用
    HashSet为优化査询速度而设计的 Set。它是基于 HashMap 实现的,HashSet 底层使用 HashMap 来保存所有元素,实现比较简单
    TreeSet实现了 Set 接口,是一个有序的 Set,这样就能从 Set 里面提取一个有序序列
    ArrayList一个用数组实现的 List,能进行快速的随机访问,效率高而且实现了可变大小的数组
    ArrayDueue是一个基于数组实现的双端队列,按“先进先出”的方式操作集合元素
    LinkedList对顺序访问进行了优化,但随机访问的速度相对较慢。此外它还有 addFirst()、addLast()、getFirst()、getLast()、removeFirst() 和 removeLast() 等方法,能把它当成栈(Stack)或队列(Queue)来用
    HsahMap按哈希算法来存取键对象
    TreeMap可以对键对象进行排序

10.2 Collection接口和常用方法

  • Collection接口实现类的特点

    1. Collection实现子类可以存放多个元素,每个元素可以是Object。

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

    3. 有些Collection的实现类,是有序的(List),有些不是有序的(Set)。

10.2.1 Collection常用方法

  • 常用方法:Collection接口没有直接的实现子类,通过它的子接口Set和List来实现。

    1. add:添加单个元素。

      List list1 = new ArrayList();
      list1.add(0);
      list1.add("666");
      System.out.println("list1 = " + list1);  // list1 = [10, true]
      
    2. remove:删除指定下标则返回该下标下的元素,若删除指定元素则返回是否删除成功。

      System.out.println(list1.remove(0));  // 0
      System.out.println("list1 = " + list1);  // list1 = [666]
      System.out.println(list1.remove("666"));  // true
      System.out.println(list1.remove("6668"));  // false
      System.out.println("list1 = " + list1);  // list1 = []
      
    3. contains:查找元素是否存在。

      System.out.println(list1.contains("hello")); // false
      System.out.println(list1.contains("666")); // true
      
    4. size:获取元素个数。

      System.out.println(list1.size());  // 2
      
    5. isEmpty:判断是否为空.

      System.out.println(list1.isEmpty());  // false
      
    6. clear:清空。

      list1.clear();
      System.out.println("list1 = " + list1);  // list1 = []
      
    7. addAll:添加多个元素。

      // 7.添加多个元素
      List list2 = new ArrayList();
      list2.add("你好");
      list2.add("irving");
      list1.addAll(list2);
      System.out.println("list1 = " + list1);  // list1 = [0, 666, 你好, irving]
      
    8. removeAll:删除多个元素

      list1.removeAll(list2);
      System.out.println("list1 = " + list1);  // list1 = [0, 666]
      

10.2.2 Collection接口遍历元素方式

10.2.2.1 使用迭代器(iterator)
  • public class CollectionIterator {
        public static void main(String[] args) {
            Collection col = new ArrayList();
            col.add(new Book("三国演义", "罗贯中", 10.1));
            col.add(new Book("小李飞刀", "古龙", 666));
            col.add(new Book("红楼梦", "曹雪芹", 262.2));
    
            Iterator iterator = col.iterator();  // 获得迭代器
            while (iterator.hasNext()) {
                Object obj =  iterator.next();
                System.out.println("obj = " + obj);
            }
          /*  obj = Book{name='三国演义', author='罗贯中', price=10.1}
            obj = Book{name='小李飞刀', author='古龙', price=666.0}
            obj = Book{name='红楼梦', author='曹雪芹', price=262.2}*/
            iterator = col.iterator(); // 重置迭代器
            System.out.println(iterator.next());  // Book{name='三国演义', author='罗贯中', price=10.1}
        }
    }
    
10.2.2.2 使用增强for循环
  • public class CollectionFor {
        @SuppressWarnings({"all"})
        public static void main(String[] args) {
            Collection col = new ArrayList();
            col.add(new Book("三国演义", "罗贯中", 10.1));
            col.add(new Book("小李飞刀", "古龙", 5.1));
            col.add(new Book("红楼梦", "曹雪芹", 34.6));
            for (Object object : col) {
                System.out.println("book=" + object);
            }
            /*
            book=Book{name='三国演义', author='罗贯中', price=10.1}
    		book=Book{name='小李飞刀', author='古龙', price=5.1}
    		book=Book{name='红楼梦', author='曹雪芹', price=34.6}
    		*/
        }
    }
    

10.3 List

10.3.1 基本定义与细节

  • 定义:List接口是Collection接口的子接口,List 是一个有序、可重复的集合,集合中每个元素都有其对应的顺序索引。

  • 使用细节:

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

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

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

    4. JDK API中List接口的实现类有:其中常用的有ArrayListLinkedListVector

10.3.2 List常用方法

  1. void add(int index, Object ele):在index位置插入ele元素。

    List list1 = new ArrayList();
    list1.add("irving");
    list1.add("james");
    list1.add("durant");
    list1.add(1,"暴龙战士");
    System.out.println("list1 = " + list1);  // list1 = [irving, 暴龙战士, james, durant]
    
  2. Object remove(int index):移除指定index位置的元素,并返回此元素。

    System.out.println( list1.remove(1));  // james
    System.out.println("list1 = " + list1);  // list1 = [irving, durant]
    
  3. Object set(int index, Object ele):设置指定index位置的元素为ele , 相当于是替换。

    list1.set(1, "玛丽");
    System.out.println("list1 = " + list1);  // list1 = [irving, 玛丽, durant]
    
  4. Object get(int index):获取指定index位置的元素。

    System.out.println(list1.get(0)); // irving
    System.out.println(list1.get(2)); // durant
    
  5. boolean addAll(int index, Collection eles):从index位置开始将eles中的所有元素添加进来。

    List list2 = new ArrayList();
    list2.add("jack");
    list2.add("tom");
    list1.addAll(1, list2);
    System.out.println("list1 = " + list1);  // list1 = [irving, jack, tom, james, durant]
    
  6. List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex位置的子集合

    List returnList = list1.subList(0, 2);
    System.out.println("returnList = " + returnList);  // returnList = [irving, james]
    

10.3.3 三种元素遍历方式

  • List list1 = new ArrayList();
    list1.add("irving");
    list1.add("james");
    list1.add("durant");
    
  1. 使用迭代器遍历。

    Iterator iterator = list1.iterator();
    while (iterator.hasNext()) {
        Object obj = iterator.next();
        System.out.println("obj = " + obj);
    }
    
  2. 使用增强for循环。

    for (Object obj : list1) {
        System.out.println("obj = " + obj);
    }
    
  3. 使用普通for循环。

    for (int i = 0; i < list1.size(); i++) {
        System.out.println("obj = " + list1.get(i));
    }
    

10.3.4 ArrayList

10.3.4.1 ArrayList底层结构与源码分析
  • 注意事项:

    1. ArrayList可以加入null,而且多个。
    2. ArrayList是用数组来实现数据存储的。
    3. ArrayList基本等同于Vector,除了ArrayList是线程不安全的(执行效率高),在多线程情况下,不建议使用ArrayList。
  • ArrayList的底层操作机制源码分析

    在这里插入图片描述

  • 在这里插入图片描述

  • 扩容总结:先确定最小需要的容量(ensureCapacityInternal),再确定当前数组长度是否满足最小需要的容量(ensureExplicitCapacity),最后在进行扩容(grow),其中扩容采用的:Arrays.copyOf。

10.3.4.2 ArrayList常用方法
  • 方法名称说明
    E get(int index)获取此集合中指定索引位置的元素,E 为集合中元素的数据类型
    int index(Object o)返回此集合中第一次出现指定元素的索引,如果此集合不包含该元 素,则返回 -1
    int lastIndexOf(Object o)返回此集合中最后一次出现指定元素的索引,如果此集合不包含该 元素,则返回 -1
    E set(int index, Eelement)将此集合中指定索引位置的元素修改为 element 参数指定的对象。 此方法返回此集合中指定索引位置的原元素
    List subList(int fromlndex, int tolndex)返回一个新的集合,新集合中包含 fromlndex 和 tolndex 索引之间 的所有元素。包含 fromlndex 处的元素,不包含 tolndex 索引处的 元素

10.3.5 Vector底层结构与源码分析

  • 注意事项:

    1. Vector底层也是一个对象数组(protected Object[] elementData)。
    2. Vector是线程同步的,即线程安全。
    3. 在开发中,需要线程同步安全时,考虑用Vector。
  • Vector的底层操作机制源码分析

    • 扩容机制与ArrayList机制类似,基本都是先确定最小需要的容量,再确定最小容量是否够用,最后在进行扩容,其中扩容采用的:ArrayCopyOf
10.3.5.1 Vector VS ArrayList

10.3.6 LinkedList

10.3.6.1 LinkedList底层结构与源码分析(难点)
  • 说明:

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

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

10.3.6.1.1 add(增)
  1. add:默认是从在链表的最后添加。

  • 源码解析:

    void linkLast(E e) {
        final Node<E> l = last;  // 声明一个引用,指向LinkedList维护的last属性,这个引用的作用是,给予创建一个新节点时的需要的前驱
        final Node<E> newNode = new Node<>(l, e, null);  // 创建一个节点需要前驱、元素属性、后继,因为是在最后添加,所以后继是null
        // 接下来需要维护LinkedLiist的first、last属性以及节点的拼接问题(增加操作对表尾最后一个节点的影响)
        last = newNode; // 因为是在链表的最后添加,所以last一定指向新创建的节点
        if (l == null) // l与last的指向相同,而last指向的是最后一个元素,此时代表要添加元素的原始LinkedList为空
            first = newNode;  // first指向新节点,且不用拼接,新节点直接当首节点
        else  // 原始LinkedList不为空时
            l.next = newNode;  // 无需维护first属性,直接在l指向的最后一个元素后面,拼接新节点
        size++;  // LinkedList元素个数++
        modCount++;  // Linked修改次数++
    }
    
  • 当从链表的最后端往前添加节点时,LinkedList的last属性一定指向新增节点,无论前方是否还有节点。

  • 从链表的尾部添加节点时,最重要的就是先获得当前链表的最后一个节点的引用,此引用不仅可以判断当前链表是否为空的状态,还可以用来修改当前链表最后一个节点与新增节点之间的联系。

10.3.6.1.2 remove(删)
  1. remove:默认从链表的最前开始删除。

在这里插入图片描述

在这里插入图片描述

  • 源码解析:

    private E unlinkFirst(Node<E> f) {
        // assert f == first && f != null;
        final E element = f.item;  // 记录下第一个节点的内容,用于成功删除后,返回删除的节点的内容
        final Node<E> next = f.next;  // 声明一个引用,指向第二个节点
        // 接下来进行节点的删除操作
        f.item = null;  // 内容置空
        f.next = null; // help GC  // 因为默认从第一节点开始删除,所以第一个节点的前驱一定为空,所以只需后继也置为空
        first = next; // 因为是从链表的表头开始删除,所以first一定指向第二个节点
        // 接下来需要维护LinkedLiist的last属性以及节点的拼接问题(删除操作对表头最后二个节点的影响)
        if (next == null)  // 删除的元素为链表中的唯一一个元素
            last = null;  // last置为空,且不用考虑对链表中第二个节点的影响
        else  // 删除的元素不是链表中的唯一元素
            next.prev = null;  // 无需维护last属性、只需把第二个前驱置为空
        size--;  // LinkedList元素个数--
        modCount++;  // Linked修改次数++
        return element;  // 返回被删除的节点的内容
    }
    
  • 当从链表的表头开始删除节点时,LinkedList的First属性一定指向当前链表的第二个节点,无论第二个节点是否为空。

  • 当从链表的表头开始删除节点时,最重要的是获得当前链表第二个节点的引用,此引用不仅可以判断当前链表是否只有一个节点,还可以用来修改第二个节点的状态。

  • 进入链表的删除操作之前有一个链表的非空验证,即不会对一个空链表进行删除操作。

10.3.6.1.3 set(改)
  1. set:修改指定索引的节点的item属性。
  • 在这里插入图片描述

  • 在这里插入图片描述

  • 源码解析:

    public E set(int index, E element) {
        checkElementIndex(index);
        Node<E> x = node(index);  // 声明一个引用,该引用指向要修改的节点
        E oldVal = x.item;  // 保存要修改的节点的内容用于返回
        x.item = element;  // 修改
        return oldVal;  // 返回被修改的节点的内容
    }	  
    
  • 真正进行链表的修改操作前,会先进行数组元素下标的正确性校验(checkElementIndex)。

10.3.6.1.4 get(查)
  1. get:获得指定索引的节点的item属性。
  • 在这里插入图片描述

  • 源码解析:

    public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }
    
  • 真正进行链表的查询操作前,会先进行数组元素下标的正确性校验(checkElementIndex)。

10.3.6.2 LinkedList常用方法
  • 方法名称说明
    void addFirst(E e)将指定元素添加到此集合的开头
    void addLast(E e)将指定元素添加到此集合的末尾
    E getFirst()返回此集合的第一个元素
    E getLast()返回此集合的最后一个元素
    E removeFirst()删除此集合中的第一个元素
    E removeLast()删除此集合中的最后一个元素
10.3.6.3 LinkedList VS ArrayList

10.4 Set

10.4.1 基本介绍

  • Set 集合类似于一个罐子,程序可以依次把多个对象“丢进”Set 集合,而 Set 集合通常不能记住元素的添加顺序。也就是说 Set 集合中的对象不按特定的方式排序,只是简单地把对象加入集合。Set 集合中不能包含重复的对象,并且最多只允许包含一个 null 元素。

  • Set 实现了 Collection 接口,它主要有两个常用的实现类:HashSet 类和 TreeSet类。

  • 取出的顺序虽然是无序的,但是每一次取出的顺序是一样的,即按照某种算法取出。

    Set set = new HashSet();
    set.add("irving");
    set.add("james");
    set.add("durant");
    set.add("curry");
    set.add(null);
    set.add(null);
    for (int i = 0; i < 2; i++) {
        System.out.println("set = " + set);
    }
    /*
    set = [null, james, durant, curry, irving]
    set = [null, james, durant, curry, irving]
    */
    

10.4.2 常用方法

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

10.4.3 遍历方式

  • 同Collection的遍历方式一样,因为Set接口是Collection接口的子接口。
  1. 可以使用迭代器、增强for。
  2. 但不能使用索引的方式来获取

10.4.4 HashSet

  • HashSet 是 Set 接口的典型实现,大多数时候使用 Set 集合时就是使用这个实现类。HashSet 是按照 Hash 算法来存储集合中的元素。因此具有很好的存取和查找性能。
  • 不能保证元素的排列顺序,顺序可能与添加顺序不同,顺序也有可能发生变化。
  • HashSet 不是同步的,如果多个线程同时访问或修改一个 HashSet,则必须通过代码来保证其同步
  • 集合元素值可以是 null,但只能有一个null。
  1. Hashset不能添加相同的元素/数据,如果向 Set 集合中添加两个相同的元素,则后添加的会覆盖前面添加的元素,即在 Set 集合中不会出现相同的元素。

    System.out.println(set.add("lucy"));  // true
    System.out.println(set.add("lucy"));  // flase
    System.out.println(set.add(new Dog("tom")));  // true
    System.out.println(set.add(new Dog("tom")));  // true
    System.out.println("set=" + set);  // set=[Dog{name='tom'}, lucy, Dog{name='tom'}]
    
    System.out.println(set.add(new String("irving")));  // true
    System.out.println(set.add(new String("irving")));  // false
    System.out.println("set=" + set);   // set=[irving, Dog{name='tom'}, lucy, Dog{name='tom'}]
    
10.4.4.1 数组链表介绍(模拟HashMap)
  • 定义:数组的每一个元素都是一个链表。

    public class HashSetStructure {
        public static void main(String[] args) {
            //模拟一个HashSet的底层 (HashMap 的底层结构)
            Node[] table = new Node[16];  // 1.创建一个数组,数组的类型是 Node[]
    
            // 2.创建结点
            Node james = new Node("james", null);
            table[2] = james;
            Node irving = new Node("irving", null);
            james.next = irving;// 将irving结点挂载到james
            Node love = new Node("love", null);
            irving.next = love;// 将love结点挂载到irving
    
            Node curry = new Node("curry", null);
            table[3] = curry; // 把curry 放到 table表的索引为3的位置.
            System.out.println("table=" + table);  // table=[Lset_.Node;@1540e19d
        }
    }
    
    class Node { // 结点, 存储数据, 可以指向下一个结点,从而形成链表
        Object item; // 存放数据
        Node next; // 指向下一个结点
    
        public Node(Object item, Node next) {
            this.item = item;
            this.next = next;
        }
    }
    
10.4.4.2 HashSet底层机制说明(难点)
  • 在这里插入图片描述

  • HashSet hashSet = new HashSet();
    hashSet.add("java");
    hashSet.add("php");
    hashSet.add("java");
    System.out.println("hashSet=" + hashSet);
    
  1. HashSet的底层是HashMap,下图是HashSet类中的无参构造器。

    在这里插入图片描述

  2. 当执行HashSet中的add方法时,就会进入HashMap类中的put方法。

    在这里插入图片描述

  3. 在真正进行HashMap的put方法之前,会先执行 hash(key) 得到key对应的hash值,算法(h = key.hashCode()) ^ (h >>> 16),执行完该算法后得到的数字用来作为索引值,即元素存放在哈希表中的位置号。

    在这里插入图片描述

在这里插入图片描述

  • 补充说明String类计算hash值的方法hashCode。

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  1. HashMap中的putVal方法解析

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;  // 定义了辅助变量,table就是HashMap的一个数组,类型是Node[]
        if ((tab = table) == null || (n = tab.length) == 0)  // 如果当前table是null,或者大小为0,则进行第一次扩容,到16个空间。
            n = (tab = resize()).length;
        // (1)根据key得到hash跟table的长度进行位与,计算该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;  // e用于后续插入是否成功的校验,k用来保存指定索引位置的key
            // 前提条件:当该索引位置的hash值与要添加的相同时(因为添加节点所在的索引值与hash值相关,此步骤再次确定即将添加的节点与该索引下的第一个节点处在同一个索引)。
           // 第一种情况:若新添加的节点和原有节点是同一个对象
    	   // 第二种情况:经过equals方法判断为真
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;  // // e指向该索引下的第一个节点,后续用作返回
            else if (p instanceof TreeNode)  // 若该索引下是树形节点,则用树形节点的方式添加节点
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {   
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 找到相同对象或者经过equals对象判断为真,直接退出
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;  // p指向下一个节点,更新操作
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;  // 有返回值,则找到同一节点或相似节点(equals为true),添加节点失败,这时直接返回,不进行后续操作
            }
        }
        // 以下操作为添加节点成功时,进行的后续操作
        ++modCount;  // 修改次数++
        if (++size > threshold)  // 判断是否需要扩容
            resize();
        afterNodeInsertion(evict);
        return null;  // 无返回值,则未找到同一节点或相似节点,添加节点成功
    }
    
    • 总结putVal步骤:

      • // 定义了辅助变量,tab为table操作的辅助变量,p用来当链表的指针使用,n表示tab的长度,i用来操作tab
        Node<K,V>[] tab; Node<K,V> p; int n, i;  
        
      1. 首次扩容:如果当前table是null,或者大小为0,则进行第一次扩容,到16个空间。后续当使用0.75的table空间时,就会进行扩容,扩容为原先长度的2倍。

        // 如果当前table是null,或者大小为0,则进行第一次扩容,到16个空间。
        if ((tab = table) == null || (n = tab.length) == 0)  
        n = (tab = resize()).length;  // n记录下tab的长度
        
      2. 获取索引:用要添加的元素的hash(与要添加的元素通过hashCode得到的值不一样)与table的长度n进行位与(&),获得元素在哈希表中的位置索引。

        if ((p = tab[i = (n - 1) & hash]) == null)
        
      3. 添加

        • 直接添加:如若该索引下无元素,则直接在该索引创建新节点。

          if ((p = tab[i = (n - 1) & hash]) == null){
              tab[i] = newNode(hash, key, value, null);  // 哈希表中该索引位置无元素,则直接添加
          }  
          
        • 间接添加:若该索引下已经有元素,则按照如下三种情况,按照顺序分类讨论。

          Node<K,V> e; K k;  // e用于后续插入是否成功的校验,k用来保存指定索引位置的key
          
          1. 与该索引下的第一个节点比较,若满足以下两种情况其中一种,则添加新节点失败。

            Node<K,V> e; K k;  // k用来保存指定索引位置的key
            // 前提条件:当该索引位置的hash值与要添加的相同时(因为添加节点所在的索引值与hash值相关,此步骤再次确定即将添加的节点与该索引下的第一个节点处在同一个索引)。
            // 第一种情况:若新添加的节点和原有节点是同一个对象
            // 第二种情况:经过equals方法判断为真
            if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))){
                e = p;  // e指向该索引下的第一个节点,后续用作返回
            }
            
            
          2. 不满足第一种情况时,进行第二种情况讨论:若该索引下第一个节点是树形节点,则用树形节点的方式添加节点。

            else if (p instanceof TreeNode){
            	e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);    
            }
            
          3. 不满足以上两种情况时,即要添加的新节点想要指定的索引下的第一个结点不是同一个对象,或者经过equals方法判断为假。进行第三种情况:从头到尾逐个检验,如果没有找到一个节点与新添加的节点为同一个对象,或者经过equals判断为true,则挂载到链表的最后一个节点后面。

            else {   
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {  // 若下一个节点为空,直接拼接节点
                        p.next = newNode(hash, key, value, null);  // 拼接节点
                        // 当单个索引位置的链表的节点个数大于等于8时,一定会进行扩容操作,可能会进行树化操作
                        if (binCount >= TREEIFY_THRESHOLD - 1) 
                            treeifyBin(tab, hash);  // 扩容及可能发生的树化操作
                        break;
                    }
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;  //  p指向下一个节点,更新操作
                }
            }
            
      4. 添加节点后的扩容:分为以下两种情况讨论。

        1. 只要在table表中添加节点,无论节点是否在同一个索引下:临界值(threshold)是 16加载因子(loadFactor)是0.75 = 12,如果table 数组使用到了临界值 12,再次添加一个节点就会扩容到 16 * 2 = 32,新的临界值就是 320.75 = 24, 依次类推。

          for(int i = 1; i <= 100; i++) {
              hashSet.add(i);//1,2,3,4,5...100
          }
          
          • 第1次添加节点,默认开辟16个空间。

          • 第13次添加节点,达到临界值,扩容为32个空间。

        2. 在同一个索引位置添加节点,单个索引位置的链表可能会触发树化操作:在Java8中, 如果一条链表的元素个数到达 TREEIFY_THRESHOLD(默认是 8 ),并且table的大小 >= MIN_TREEIFY_CAPACITY(默认64),每添加一个节点就会进行树化(红黑树),否则仍然采用数组扩容机制

          for(int i = 1; i <= 12; i++) {
              hashSet.add(new A(i));
          }
          class A {
              private int n;
              public A(int n) {
                  this.n = n;
              }
              @Override
              public int hashCode() {  // 重写hashCOde方法,确保在同一个索引位置添加节点
                  return 100;
              }
          }
          
          • 第1次添加节点,默认开辟16个空间。

          • 第9次添加节点,单个索引位置的链表长度到达8,一定会进行数组的扩容操作,可能触发树化操作。

          • 第11次添加节点,单个索引位置的链表长度到达8,且table数组到达64,触发树化操作。

            在这里插入图片描述

10.4.5 LinkedHashSet

10.4.5.1 LinkedHashSet底层机制说明
  1. LinkedHashSet是HashSet的子类,底层维护的是一个LinkedHashMap(HashMap的子类)。

    Set set = new LinkedHashSet();
    

    在这里插入图片描述

    在这里插入图片描述

  2. LinkedHashSet底层结构 (数组table+双向链表),加入顺序和取出元素/数据的顺序一致

    Set set = new LinkedHashSet();
    set.add("irving");
    set.add("james");
    set.add("durant");
    System.out.println("set=" + set);  // set=[irving, james, durant]
    
  3. 数组是 HashMapNode[] 存放的元素/数据是 LinkedHashMap$Entry类型,而Hasm继承关系在静态内部类内部类实现。

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

10.4.6 TreeSet

10.4.6.1 基本定义与说明
  • TreeSet 类同时实现了 Set 接口和 SortedSet 接口。SortedSet 接口是 Set 接口的子接口,可以实现对集合进行自然排序,因此使用 TreeSet 类实现的 Set 接口默认情况下是自然排序的,这里的自然排序指的是升序排序
  1. 可以按照自己设定的方式来明确插入和输出的顺序,key不能为空,value可以为空TreeSet的本质是TreeMap

  2. TreeMap底层维护的是一个root数组用来存储TreeMap$Entry,此外还维护两个属性letf和right分别指向左节点和右节点。

    在这里插入图片描述

  3. TreeSet 只能对实现了 Comparable 接口的类对象进行排序,有一部分类已实现了java.lang.Comparable接口,如基本类型的包装类、String类等,它们在compareTo()方法中定义好了比较对象的规则。像这样的对象可以直接插入TreeSet集合。

    比较方式
    包装类(BigDecimal、Biglnteger、 Byte、Double、 Float、Integer、Long 及 Short)按数字大小比较
    Character按字符的 Unicode 值的数字大小比较
    String按字符串中字符的 Unicode 值的数字大小比较
  4. 使用TreeSet存储自定义类对象时,对象所在类要实现Comparable接口,在compareTo()方法中定义对象比较的规则,或者使用带有Comparator参数的构造器。

    class Dog implements Comparable{
        private String name;
        public Dog(String name) {
            this.name = name;
        }
        @Override
        public int compareTo(Object o) {
            return 0;
        }
        @Override
        public String toString() {
            return "Dog{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }
    
    TreeSet treeSet = new TreeSet(new Comparator() {
        @Override
        public int compare(Object o1, Object o2) {
            return 0;
        }
    });
    
10.4.6.2 TreeSet底层结构与源码解析
  1. 当我们使用无参构造器,创建TreeSet时,任然是无序的,而使用自己定义的比较器作为参数传入的有参构造器,则可以按照特定的顺序排序。

    • TreeSet treeSet1 = new TreeSet();
      treeSet1.add("irving");
      treeSet1.add("kfc");
      treeSet1.add("kevin durant");
      System.out.println("treeSet1 = " + treeSet1);  // treeSet1 = [irving, kevin durant, kfc]
      
      TreeSet treeSet2 = new TreeSet(new Comparator() {
          @Override
          public int compare(Object o1, Object o2) {
              return ((String) o1).length() - ((String) o2).length();
          }
      });
      treeSet2.add("irving");
      treeSet2.add("kfc");
      treeSet2.add("kevin durant");
      System.out.println("treeSet2 = " + treeSet2);  // treeSet2 = [kfc, irving, kevin durant]
      
    1. 调用TreeSet的add方法时,会自动调用TreeMap的put方法。

    2. TreeMap中的put方法解析

      public V put(K key, V value) {
          Entry<K,V> t = root;  // 声明一个引用t指向root数组
          if (t == null) {  // 数组为空
              compare(key, key); // type (and possibly null) check
              root = new Entry<>(key, value, null);  // 创建数组
              size = 1;
              modCount++;  // 修改次数++
              return null;  // 返回空对象,即没有找到同一对象或相似对象(经过equals为true),插入第一个节点成功
          }
          int cmp;  // 用于记录节点间比较的返回值,来确定插入在指定节点的左边(负数)还是右边(正数)
          Entry<K,V> parent;  // parent用于记录t的初始位置,供插入使用
          // split comparator and comparable paths
          Comparator<? super K> cpr = comparator;
          if (cpr != null) {  // 手动创建comparator时
              do {
                  parent = t;
                  cmp = cpr.compare(key, t.key);
                  if (cmp < 0)  // comparator返回值为负数
                      t = t.left;  // t向左边移动
                  else if (cmp > 0)  // comparator返回值为正数
                      t = t.right;  // t向右边移动
                  else
                      return t.setValue(value);  // comparator返回值为0,更改value并直接返回新value,对TreeSet无影响,主要作用于TreeMap。
              } while (t != null);
          }
          else {  // 没有手动创建comparator时
              if (key == null)
                  throw new NullPointerException();
              @SuppressWarnings("unchecked")
              Comparable<? super K> k = (Comparable<? super K>) key;
              do {
                  parent = t;
                  cmp = k.compareTo(t.key); 
                  if (cmp < 0)  // comparator返回值为负数
                      t = t.left;  // t向左边移动
                  else if (cmp > 0)  // comparator返回值为正数
                      t = t.right;  // t向右边移动
                  else
                      return t.setValue(value);  // comparator返回值为0,更改value并直接返回新value,对TreeSet无影响,主要作用于TreeMap。
              } while (t != null);
          }
          Entry<K,V> e = new Entry<>(key, value, parent); // 创建节点
          if (cmp < 0)
              parent.left = e;
          else
              parent.right = e;
          fixAfterInsertion(e);
          size++;
          modCount++;
          return null;
      }
      

10.5 Map

10.5.1 Map接口实现类的特点

  1. Map和Collection并列存在,用于保存具有映射关系的数据:key-value。

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

  3. Map中的key不允许重复。可以为null,但只能有一个

  4. Map中的value允许重复,可以为null,且可以有多个。

  5. 常用String类作为Map的key。

  6. key和value之间存在单向一对一关系,即可通过指定的key找到对应的value。

  7. Map存放数据的key-value示意图,一对key-value,是放在一个HashMap$Node中的,又因为Node实现了Entry接口。

  • Map map = new HashMap();
    Set entrySet = map.entrySet();
    System.out.println(entrySet.getClass());  // class java.util.HashMap$EntrySet
    for (Object obj : entrySet) {
        Map.Entry entry = (Map.Entry) obj;
        System.out.println(entry.getClass());  // class java.util.HashMap$Node
    }
    
    Set keySet = map.keySet();
    System.out.println(keySet.getClass());  // class java.util.HashMap$KeySet
    Collection values = map.values();
    System.out.println(values.getClass());  // class java.util.HashMap$Values
    

10.5.2 常用方法

方法名称说明
void clear()删除该 Map 对象中的所有 key-value 对。
boolean containsKey(Object key)查询 Map 中是否包含指定的 key,如果包含则返回 true。
boolean containsValue(Object value)查询 Map 中是否包含一个或多个 value,如果包含则返回 true。
V get(Object key)返回 Map 集合中指定键对象所对应的值。V 表示值的数据类型
V put(K key, V value)向 Map 集合中添加键-值对,如果当前 Map 中已有一个与该 key 相等的 key-value 对,则新的 key-value 对会覆盖原来的 key-value 对。
void putAll(Map m)将指定 Map 中的 key-value 对复制到本 Map 中。
V remove(Object key)从 Map 集合中删除 key 对应的键-值对,返回 key 对应的 value,如果该 key 不存在,则返回 null
boolean remove(Object key, Object value)这是 Java 8 新增的方法,删除指定 key、value 所对应的 key-value 对。如果从该 Map 中成功地删除该 key-value 对,该方法返回 true,否则返回 false。
Set entrySet()返回 Map 集合中所有键-值对的 Set 集合,此 Set 集合中元素的数据类型为 Map.Entry
Set keySet()返回 Map 集合中所有键对象的 Set 集合
boolean isEmpty()查询该 Map 是否为空(即不包含任何 key-value 对),如果为空则返回 true。
int size()返回该 Map 里 key-value 对的个数
Collection values()返回该 Map 里所有 value 组成的 Collection
  1. put:添加元素,添加相同的键时,值的内容会被覆盖

    Map map = new HashMap();
    map.put("curry", 30);
    map.put("james", 23);
    map.put("durant", 35);
    map.put("james", 6);  // 添加相同的键时,值的内容会被覆盖
    map.put(null, "abc");
    System.out.println("map = " + map);  // map = {null=abc, james=6, durant=35, curry=30}
    System.out.println(map.get("james"));  // 6
    
  2. remove:根据键删除映射关系。

    map.remove(null);
    System.out.println("map = " + map);  // map = {james=6, durant=35, curry=30}
    
  3. get:根据键获取值。

    System.out.println(map.get("james"));  // 6
    System.out.println(map.put("curry", 30));  // 30
    
  4. size:获取元素个数。

    System.out.println("k-v=" + map.size());  // k-v=3
    
  5. clear:清空元素。

    map.clear();
    System.out.println("k-v=" + map.size());  // k-v=3
    
  6. containsKey:查找键是否存在。

    System.out.println(map.containsKey("james"));  // true
    
10.5.2.1 Java8中Map的新增方法
  • Java 8 除了为 Map 增加了 remove(Object key, Object value) 默认方法之外,还增加了如下方法。

    名称说明
    Object compute(Object key, BiFunction remappingFunction)该方法使用 remappingFunction 根据原 key-value 对计算一个新 value。只要新 value 不为 null,就使用新 value 覆盖原 value;如果原 value 不为 null,但新 value 为 null,则删除原 key-value 对;如果原 value、新 value 同时为 null,那么该方法不改变任何 key-value 对, 直接返回 null。
    Object computeIfAbsent(Object key, Function mappingFunction)如果传给该方法的 key 参数在 Map 中对应的 value 为 null,则使用 mappingFunction 根据 key 计算一个新的结果,如果计算结果不为 null,则用计算结果覆盖原有的 value。如果原 Map 原来不包括该 key,那么该方法可能会添加一组 key-value 对。
    Object computeIfPresent(Object key, BiFunction remappingFunction)如果传给该方法的 key 参数在 Map 中对应的 value 不为 null,该方法将使用 remappingFunction 根据原 key、 value 计算一个新的结果,如果计算结果不为 null,则使 用该结果覆盖原来的 value;如果计算结果为 null,则删除原 key-value 对。
    void forEach(BiConsumer action)该方法是 Java 8 为 Map 新增的一个遍历 key-value 对的方法,通过该方法可以更简洁地遍 历 Map 的 key-value 对。
    Object getOrDefault(Object key, V defaultValue)获取指定 key 对应的 value。如果该 key 不存在,则返回 defaultValue。
    Object merge(Object key, Object value, BiFunction remappingFunction)该方法会先根据 key 参数获取该 Map 中对应的 value。如果获取的 value 为 null,则直接用 传入的 value 覆盖原有的 value(在这种情况下,可能要添加一组 key-value 对);如果获取 的 value 不为 null,则使用 remappingFunction 函数根据原 value、新 value 计算一个新的 结果,并用得到的结果去覆盖原有的 value。
    Object putIfAbsent(Object key, Object value)该方法会自动检测指定 key 对应的 value 是否为 null,如果该 key 对应的 value 为 null,该 方法将会用新 value 代替原来的 null 值。
    Object replace(Object key, Object value)将 Map 中指定 key 对应的 value 替换成新 value。与传统 put() 方法不同的是,该方法不可 能添加新的 key-value 对。如果尝试替换的 key 在原 Map 中不存在,该方法不会添加 key value 对,而是返回 null。
    boolean replace(K key, V oldValue, V newValue)将 Map 中指定 key-value 对的原 value 替换成新 value。如果在 Map 中找到指定的 key value 对,则执行替换并返回 true,否则返回 false。
    replaceAll(BiFunction function)该方法使用 BiFunction 对原 key-value 对执行计算,并将计算结果作为该 key-value 对的 value 值。

10.5.3 遍历方式

  • Map map = new HashMap();
    map.put("curry",30);
    map.put("james",23);
    map.put("durant",35);
    
  1. 利用中的EntrySet中的keySet,取出所有的key,再通过key取出对应的value。

    Set keySet = map.keySet();
    for (Object key :keySet) {  // 使用增强for
        System.out.println(key + "-" + map.get(key));
    }
    
    Iterator iterator = keySet.iterator();  // 使用迭代器
    while (iterator.hasNext()) {
        Object key =  iterator.next();
        System.out.println(key + "-" + map.get(key));
    }
    /*输出结果
    james-23
    durant-35
    curry-30*/
    
  2. 利用EntrySet中的values,取出value。

    Collection values = map.values();
    for (Object value :values) {  // 使用增强for
        System.out.println(value);
    }
    
    Iterator iterator = values.iterator();  // 使用迭代器
    while (iterator.hasNext()) {
        Object value =  iterator.next();
        System.out.println(value);
    }
    /*输出结果
    23
    35
    30*/
    
  3. 通过EntrySet提供的getKey和getValue方法来获取key-value。

    Set entrySet = map.entrySet();
    for (Object obj : entrySet) {  // 使用增强for
        Map.Entry entry = (Map.Entry) obj;  // 因为需要使用到Map.Entry中的方法,所以需要向下转型
        System.out.println(entry.getKey() + "-" + entry.getValue());
    }
    
    Iterator iterator = entrySet.iterator();
    while (iterator.hasNext()) {
        Object obj =  iterator.next();
        Map.Entry entry = (Map.Entry) obj;  //  因为需要使用到Map.Entry中的方法,所以需要向下转型
        System.out.println(entry.getKey() + "-" + entry.getValue());
    }
    /*输出结果
    james-23
    durant-35
    curry-30*/
    
  • 练习题:使用HashMap添加三个员工对象,要求键使用员工id,键使用员工对象。遍历显示工资>18000的员工。

    HashMap hashMap = new HashMap();
    // 使用keySet + 增强for
    Set keySet = hashMap.keySet();
    for (Object key : keySet) {
        Emp emp = (Emp) hashMap.get(key);  // 因为Map.get方法返回的是泛型,但是需要使用到Emp中的方法来比较大小,所以需要向下转型
        if (emp.getSal() > 180000) {
            System.out.println(emp);
        }
    }
    
    // 使用EntrySet + 迭代器
    Set entrySet = hashMap.entrySet();
    Iterator iterator = entrySet.iterator();
    while (iterator.hasNext()) {
        Map.Entry entry = (Map.Entry) iterator.next();  // 因为迭代器返回的是Object类型,但是需要使用到Map.Entry中的方法,所以需要向下转型
        Emp emp = (Emp) entry.getValue();  // 因为Entry.getValue方法返回的是泛型,但是需要使用到Emp中的方法来比较大小,所以需要向下转型
        if (emp.getSal() > 180000) {
            System.out.println(emp);
        }
    }
    

10.5.4 HashMap

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

10.5.5 Hashtable

  1. 存放的元素是键值对:key-value。
  2. HashTable的键和值不能为null,否则会抛出NullPointerException
  3. HashTable使用方法和HashMap基本一致。
  4. HashTable是线程安全(synchronized)的,但它性能较低,不建议使用 ,HashMap是线程不安全的。
10.5.5.1 Hashtable底层机制
  • Hashtable hashtable = new Hashtable();
    hashtable.put("irving",11);
    hashtable.put("durant",35);
    hashtable.put("james",23);
    System.out.println("hashtable = " + hashtable);
    hashtable.put("james",null);  // 空指针异常
    hashtable.put(null,23);  // 空指针异常
    
  1. 底层有数组Hashtable$Entry[] 初始化大小为 11。

  2. 临界值 threshold 8 = 11 * 0.75,到达临界后再次添加节点就会触发扩容机制: int newCapacity = (oldCapacity << 1) + 1。

10.5.5.2 Hashtable VS HashMap
  1. 从功能特性来说:

    • HashTable 是线程安全的,而 HashMap 不是。
    • HashMap 的性能要比 HashTable 更好,因为,HashTable 采用了全局同步锁来保证安全性,对性能影响较大。
  2. 从内部实现角度来说:

    1. HashMap 可以使用 null 作为 key,HashMap 会把 null 转化为 0 进行存储,而 Hashtable 不允许

    2. HashTable 使用数组加链表、HashMap 采用了数组+链表+红黑树。

    3. HashMap 初始容量是 16、HashTable 初始容量是 11

    4. 最后,他们两个的 key 的散列算法不同,HashTable 直接是使用 key 的 hashcode 对数组长度做取模。而 HashMap 对 key 的 hashcode 做了二次散列,从而避免 key 的分布不均匀问题影响到查询性能

10.5.5.3 Properties
  1. Properties类继承自Hashtable类并且实现了Map接口,也使用了键值对来保存数据,因此它的使用特点与Hashtable类似。

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

  3. 实际工作中,xxx.properties文件通常作为配置文件。

  4. 基本使用:

    Properties properties = new Properties();
    // 增加
    properties.put("irving", 11);
    properties.put("durant", 35);
    properties.put("james", 23);
    System.out.println("properties=" + properties);  // properties={irving=11, james=23, durant=35}
    // 删除
    properties.remove("james");
    System.out.println("properties=" + properties);  // properties={irving=11, durant=35}
    // 修改
    properties.put("james", 6);  // 如果有相同的key,value被替换
    System.out.println("properties=" + properties);  // properties={irving=11, james=6, durant=35}
    // 查询
    System.out.println(properties.get("james"));  //6
    properties.put(null, "abc");  //抛出 空指针异常
    properties.put("abc", null);  //抛出 空指针异常
    

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

10.7 Collections工具类

  • 定义:Collections是一个操作Set、List和Map等集合的工具类。

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

  • 常见方法:大体分为排序方法、查找方法、替换方法。

  • ArrayList arrayList = new ArrayList();
    arrayList.add("irving");
    arrayList.add("durant35");
    arrayList.add("harden");
    
  1. reverse(List):反转List中元素的顺序。

    Collections.reverse(arrayList);
    System.out.println("arrayList = " + arrayList);  // arrayList = [harden, durant35, irving]
    
  2. 2.shuffle(List):对List集合元素进行随机排序。

    for (int i = 0; i < 3; i++) {
        Collections.shuffle(arrayList);
        System.out.println("arrayList = " + arrayList);  
    }
    
  3. sort(List):根据元素的自然顺序对指定List集合元素按升序排序。

    Collections.sort(arrayList);
    System.out.println("经过自然排序后的arrayList = " + arrayList);  // 经过自然排序后的arrayList = [durant35, harden, irving]
    Collections.sort(arrayList, new Comparator<Object>() {
        @Override
        public int compare(Object o1, Object o2) {
            return ((String) o1).length() - ((String) o2).length();
        }
    });
    System.out.println("按字符串大小长度排序的arrayList = " + arrayList);  // 按字符串大小长度排序的arrayList = [harden, irving, durant35]
    
  4. swap(List,i,j):交换i,j的元素。

    Collections.swap(arrayList,0,1);
    System.out.println("arrayList = " + arrayList );  // arrayList = [durant35, irving, harden]
    
  5. Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素。

    System.out.println("自然顺序的最大元素 = " + Collections.max(arrayList));  // 自然顺序的最大元素 = irving
    Object maxObject = Collections.max(arrayList, new Comparator() {
        @Override
        public int compare(Object o1, Object o2) {
            return ((String)o1).length() - ((String)o2).length();
        }
    });
    System.out.println("长度最大的元素 = " + maxObject);  // 长度最大的元素 = durant35
    
  6. int frequency(Collection,Object):返回指定集合中指定元素的出现次数。

    System.out.println("irving出现的次数 = " + Collections.frequency(arrayList,"irving"));  // irving出现的次数 = 1
    
  7. void copy(List dest,List src):将src中的内容复制到dest中。

    ArrayList dest = new ArrayList();
    for (int i = 0; i < arrayList.size(); i++) {
        dest.add("");
    }
    Collections.copy(dest,arrayList);
    System.out.println("dest = " + dest);  // dest = [irving, durant35, harden]
    
  8. boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换 List 对象的所有旧值。

    Collections.replaceAll(arrayList, "irving", "欧文");
    System.out.println("替换后的arrayList = " + arrayList);  // 替换后的arrayList = [欧文, durant35, harden]
    

10.8 补充知识

10.8.1 HashSet和TreeSet的去重机制

  1. HashSet的去重机制:hashCode()+equals,底层先通过存入对象,进行运算(h = key.hashCode()) ^ (h >>> 16)得到一个hash值,通过hash值与数值的长度减一进行运算p = tab[i = (n - 1) & hash]得到对应的索引,如果发现table索引所在的位置,没有数据,就直接存放。如果有数据,就进行equals(遍历比较),如果比较后,不相同,就加入,否则就不加入。

    案例:HashSet底层机制练习题:Person类按照id,name重写了hashCode和equals方法。

    HashSet set = new HashSet();  // ok
    Person p1 = new Person(1001,"AA");  // ok
    Person p2 = new Person(1002,"BB");  // ok
    set.add(p1);  // ok
    set.add(p2);  // ok
    p1.name = "CC";  // ok
    System.out.println(set.remove(p1));  
    // false,此时的p1在table中索引位置是根据1001和CC,与原先1001和AA的地址不同,则删除失败
    // [Person{name='BB', id=1002}, Person{name='CC', id=1001}]
    
    set.add(new Person(1001,"CC")); 
    // [Person{name='BB', id=1002}, Person{name='CC', id=1001}, Person{name='CC', id=1001}]
    
    set.add(new Person(1001,"AA"));
    // [Person{name='BB', id=1002}, Person{name='CC', id=1001}, Person{name='CC', id=1001}, Person{name='AA', id=1001}]
    
  2. TreeSet的去重机制:如果你传入一个comparator匿名对象,就使用实现的compare去重,如果返回0,就认为是相同元素/数据,就不添加。如果没有传入一个comparator匿名对象,则以你添加的对象实现的compareable接口的compareTo去重。

10.8.2 HashMap如何解决Hash冲突

  1. 什么是Hash冲突?

    • Hash 算法,就是把任意长度的输入,通过散列算法,变成固定长度的输出,这个输出结果是散列值。

    • Hash 表又叫做“散列表”,它是通过 key 直接访问在内存存储位置的数据结构,在具体实现上,我们通过 hash 函数把 key 映射到表中的某个位置,来获取这个位置的数据,从而加快查找速度。

    • 所谓 hash 冲突,是由于哈希算法被计算的数据是无限的,而计算后的结果范围有限,所以总会存在不同的数据经过计算后得到的值相同,这就是哈希冲突。

  2. 如何解决Hash冲突?

    1. 链式寻址法,这是一种非常常见的方法,简单理解就是把存在 hash 冲突的 key,以单向链表的方式来存储,比如 HashMap 就是采用链式寻址法来实现的。向这样一种情况(如图),存在冲突的 key 直接以单向链表的方式进行存储。

    2. 开放定址法,也称为线性探测法,就是从发生冲突的那个位置开始,按照一定的次序从 hash 表中找到一个空闲的位置,然后把发生冲突的元素存入到这个空闲位置中。ThreadLocal 就用到了线性探测法来解决 hash 冲突的

    3. 再 hash 法,就是当通过某个 hash 函数计算的 key 存在冲突时,再用另外一个 hash 函数对这个 key 做 hash,一直运算直到不再产生冲突。这种方式会增加计算时间,性能影响较大。

    4. 建立公共溢出区, 就是把 hash 表分为基本表和溢出表两个部分,凡事存在冲突的元素,一律放入到溢出表中。

      HashMap 在 JDK1.8 版本中,通过链式寻址法+红黑树的方式来解决 hash 冲突问题,其中红黑树是为了优化 Hash 表链表过长导致时间复杂度增加的问题。当链表长度大于 8 并且 hash 表的容量大于 64 的时候,再向链表中添加元素时就会触发树化。

10.8.3 为什么重写equals()就必须重写hashCode()?

  1. equals:是Object类中的方法,只能判断引用类型。默认判断的是地址是否相等,子类中往往重写该方法,用于判断内容是否相等。比如Integer,String。
  2. hashCode:Java 里面任何一个对象都有一个 native 的 hashCode()方法,这个方法在散列集合中会用到,比如 HashTable、HashMap 这些,当添加元素的时候,需要判断元素是否存在。如果用 equals 效率太低,所以一般是直接用对象的 hashCode 的值进行取模运算。
  3. 如果只重写 equals 方法,不重写 hashCode 方法。就有可能导致 a.equals(b)这个表达式成立,但是 hashCode 却不同。就会造成一个完全相同的对象会在存储在 hash 表的两个位置,造成大家约定俗成的规则,出现一些不可预料的错误。

10.8.4 SortedSet VS List

  1. 存放:SortedSet 是一个有序的集合,不允许元素的重复,而 List 是一个有序的列表,允许元素的重复。

  2. 排序:SortedSet 可以按照元素的自然顺序或者自定义比较器进行排序,而 List 只能按照元素的添加顺序排序。因此, SortedSet 可以方便地进行范围查询操作,例如获取某个区间内的元素,而 List 只能通过遍历实现范围查询。

  3. 增删:在 SortedSet 中,元素的添加和删除操作的时间复杂度为 O(logn),而在 List 中,元素的添加和删除操作的时间复杂度为 O(n),因为需要移动其他元素的位置。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值