Java基础学习(十)

Java基础学习(十):集合

本文为个人学习记录,内容学习自 黑马程序员


概念

  • 数组长度固定,不够灵活,因此出现了集合

  • 集合 vs 数组

    • 长度:数组的长度固定,集合长度可变(会在添加数据时自动扩容,删除数据时自动压缩)
    • 存储类型:数组可以存储所有数据类型,集合只能存储引用数据类型,如果要存储基本数据类型的话需要将其转换成对应包装类
  • 集合可以分成两大类:单列集合Collection 和 双列集合Map

  • 单列集合中的常用集合:

    • List 系列集合:添加的元素是有序、可重复、有索引的
    • Set 系列集合:添加的元素是无序、不重复、无索引的
    • 下图中 Collection,List,Set为接口,其余为实现类,箭头表示继承/实现关系
    图10-1
  • 双列集合特点:一次需要存储一对数据,分别为键和值;键不能重复,值可以重复;称之为“键值对”、“键值对对象”、“Entry对象”

  • 双列集合中的常用集合:

    图10-2

数据结构

  • 数据结构是计算机底层存储、组织数据的方式

  • 数据结构是为了更加方便地管理和使用数据,需要结合具体的应用场景进行选择,合适的数据结构能带来更高的运行或存储效率

  • 常见数据结构:栈,队列,数组,链表,二叉树,二叉查找树,平衡二叉树,红黑树

  • 栈:后进先出,先进后出

  • 队列:先进先出,后进后出

  • 数组:查询快,增删慢

    • 查询速度快:通过地址值找到数组,再通过索引找到元素,查询任意数据耗时相同,元素在内存中连续存储
    • 删除效率低:要将原始的数据删除,同时后面每个数据前移
    • 添加效率低:添加位置后地每个元素后移,再添加元素
  • 链表:查询慢,增删快

    • 链表中的每一个元素称为结点/节点(node),每个结点都是独立对象,在内存中不连续,结点中存储具体数据和下一个结点的地址

    • 查询速度慢:无论哪个数据都要从头开始查找

    • 删除和添加效率高:只要修改结点指向的地址就可以了

      图10-3
    • 单向链表 vs 双向链表:双向链表中不仅存储了具体值和下一个结点地址,还存储了上一结点地址,能提高查找效率

    • 基本概念:

      • 树中的每个 节点 都是一个独立对象

      • 节点 A 如果下方连接了节点 B 和 C,则称节点 A 为父节点,节点 B 和 C 从左到右排列时称 B 为左子节点,C 为右子节点

      • 节点中存储了当前节点的数据、父节点地址、左子节点地址和右子节点地址,如果不存在则记为 null

      • 将节点的子节点数量称为 ,例如二叉树中任意节点的度<= 2

      • 将树的总层数称为 树高

      • 将树的最顶层节点称为 根节点

      • 将根节点的左子节点及其以下的内容称为 根节点的左子树,同理还有 根节点的右子树,其它节点也有左子树和右子树的概念

      图10-4
  • 二叉树

    • 满足任意节点最多有两个子节点的树为二叉树
    • 普通二叉树的弊端:没有顺序要求,查找效率低下
    • 二叉树遍历方式(适用于所有二叉树):
      • 前序遍历:从根节点开始,按照当前节点-左子节点-右子节点的顺序遍历,例如上面的树遍历顺序为 20-18-16-19-23-22-24
      • 中序遍历:从最左边的子节点开始,按照左子节点-当前节点-右子节点的顺序遍历,例如上面的树遍历顺序为 16-18-19-20-22-23-24,对二叉查找树进行中序遍历可以得到从小到大排列的数据,因此这种方法最为常用
      • 后序遍历:从最左边的子节点开始,按照左子节点-右子节点-当前节点的顺序遍历,例如上面的树遍历顺序为 16-19-18-22-24-23-20
      • 层序遍历:从根节点开始一层层遍历,例如上面的树遍历顺序为 20-18-23-16-19-22-24
  • 二叉查找树:

    • 又称二叉排序树或者二叉搜索树
    • 满足三个条件:任意节点最多有两个子节点,任意节点左子树上的值都小于当前节点,任意节点右子树上的值都大于当前节点
    • 数据存储规则:小的存左边,大的存右边,一样的不存
    • 二叉查找树查找方式:从根节点开始查,如果目标值小于当前节点值,则向左子树查找,如果大于当前节点值,则向右子树查找
    • 二叉查找树的优势:相比于普通二叉树提高了查找效率
    • 二叉查找树的弊端:可能存在左子树和右子树高度相差过大的情况,会大大降低查找效率
  • 平衡二叉树

    • 平衡二叉树就是在二叉查找树的基础上,规定任意节点左右子树高度差不超过1

    • 平衡二叉树的实现机制 —— 旋转机制

      • 旋转:分成左旋(逆时针旋转)和右旋(顺时针旋转)
      • 触发时机:当添加一个节点后,该树不再是一颗平衡二叉树时触发旋转
    • 平衡二叉树旋转流程:

      1. 确定支点:从添加的节点开始,不断往父节点找不平衡的节点作为支点,将支点及其下的内容作为一个整体旋转,其余不变

      2. 确定旋转方向:将支点视为根节点,根据插入的节点位置分成四种情况:

        • 左左:当根节点左子树的左子树有节点插入导致不平衡时,只需要一次右旋
        • 左右:当根节点左子树的右子树有节点插入导致不平衡时,需要先将根节点的左子树单独进行一次左旋,旋转后就相当于”左左“的插入情况了,这时再进行一次右旋即可
        • 右右:当根节点右子树的右子树有节点插入导致不平衡时,只需要一次左旋
        • 右左:当根节点右子树的左子树有节点插入导致不平衡时,需要先将根节点的右子树单独进行一次右旋,旋转后就相当于”右右“的插入情况了,这时再进行一次左旋即可
      3. 进行旋转:如果是左旋,就将原来的右子节点升级到支点所在位置,原来的支点降级为新支点的左子节点;如果是右旋,就将原来的左子节点升级到支点所在位置,原来的支点降级为支点的右子节点

        图10-5

      4. 复杂情况:如果在左旋时,原来的右子节点已经有左子节点了,不会将该左子节点一块升级,而是作为降级后的新左子节点的右子节点;右旋同理

        图10-6

  • 红黑树

    • 红黑树是一种自平衡的二叉查找树,但它并不是平衡二叉树

    • 红黑树的每个节点上都有存储位表示节点的颜色,每一个节点可以是红或者黑

    • 红黑树不是高度平衡的,它的平衡是通过红黑规则实现的

    • 红黑树 vs 平衡二叉树:平衡二叉树是高度平衡的,查找效率很高,但添加数据时由于可能存在多次旋转,添加效率不高;红黑树不一定是高度平衡的,但增删改查效率都很高

    • 红黑树的红黑规则:

      • 每一个节点或者是红色的,或者是黑色的
      • 根节点必须是黑色
      • 如果一个节点没有子节点或者父节点,则该节点相应的指针属性值为 Nil,这些 Nil 视为叶节点,叶节点为黑色的
      • 如果某一个节点是红色的,那么它的子节点必须是黑色的(不能出现两个红色节点相连的情况)
      • 对每个节点,从该节点到其所有后代叶节点的简单路径上,均包含相同数目的黑色节点
      图10-7
    • 红黑树添加节点的规则:

      • 默认颜色:添加节点默认是红色的

      • 添加细则如下:

        图10-8

      • 注意:“把父/祖父设置为当前节点再进行判断”:指的是对添加的节点进行处理后,还要再将父/祖父视为添加的节点,再次进行上述判断和处理


泛型

  • JDK5 引入的特性,可以在编译阶段约束操作的数据类型,并进行检查

  • 格式:<数据类型>

  • 泛型的出现:在使用集合时,如果不存在泛型,那么集合中的元素可以是任何引用数据类型,在底层都是使用 Object obj = input;来实现的,而多态是无法使用子类特有的方法的,虽然可以通过强制类型转换转换回子类对象,但每种类型都要强转实在太麻烦了。因此泛型被用于约束数据类型,虽然在输入时仍然是使用 Object obj = input;的伪泛型,但在输出时会自动帮我们强转成约束类型

  • 优势:1. 统一数据类型 2. 把运行时期可能出现的问题提前到了编译时期

  • 注意事项:

    1. 泛型只能支持引用数据类型
    2. 指定泛型的具体类型后,传递数据时可以传入该类型或者其子类类型
    3. 创建集合对象时如果不指定泛型,默认是 Object 类型
    4. 泛型中可以指定多个变量,如 <E, T>
  • 泛型可以在很多地方定义:当泛型写在类后面时,是泛型类;写在方法上面时,是泛型方法;写在接口后面时,是泛型接口

    • 泛型类

      • 使用场景:当一个类中,某个变量的数据类型不确定时,就可以定义带有泛型的类

      • 格式:修饰符 class 类名<类型> {}

      • 说明:此处的 E 用于记录数据类型,一般写成 T(Type)、E(Element)、K(Key)、V(Value)等

      • 示例:

        public class MyArrayList<E> {
            Object[] obj = new Object[10];
            int size;
            public boolean add(E e) {
                obj[size] = e;
                return true;
            }
        }
        
    • 泛型方法

      • 使用场景:当一个方法中,某个变量的数据类型不确定时,可以定义带有泛型的方法

      • 格式:修饰符 <类型> 返回值类型 方法名(形参) {}

      • 示例:

        public class MyArrayList {
            public static <E> void add(E e) {
                // 方法体
            }
        }
        
    • 泛型接口

      • 使用场景:当一个接口中,某个变量的数据类型不确定时,可以定义带有泛型的接口

      • 格式:修饰符 interface 接口名<类型> {}

      • 两种使用方式:

        1. 实现类给出具体类型

          public class MyArrayList implements List<String> {
          	// 重写方法,方法中不存在泛型
          }
          
        2. 实现类延续泛型,创建实现类对象时再确定类型

          public class MyArrayList<E> implements List<E> {
              // 重写方法,方法中存在泛型
          }
          
  • 泛型的通配符

    • ? 表示不确定的类型

    • 在 ? 的基础上可以进一步限定类型

      • ?:表示可以传递任何类型

        注意:使用 ? 表示能传递任何类型时,不需要在修饰符和返回值类型之间写泛型

        public static void method(ArrayList<?> list) {
            // 方法体
        }
        
      • ? extends E:表示可以传递 E 或者 E 所有的子类类型

        public static void method(ArrayList<? extends GrandFather> list) {
        	// 方法体
        }
        
      • ? super E:表示可以传递 E 或者 E 所有的父类类型

        public static void method(ArrayList<? super Son> list) {
        	// 方法体
        }
        
  • 应用场景:如果在定义类、方法、接口时存在不确定的类型,就可以定义泛型类、泛型方法、泛型接口;如果类型不确定,但能传递的参数在某个继承结构中,就可以使用泛型的通配符


Collection

  • Collection 是单列集合的祖宗接口,它的功能是全部单列集合都可以继承使用的

  • 常用方法:

    方法名 说明
    public boolean add(E e) 把给定的对象添加到当前集合中
    public void clear() 清空集合中所有的元素
    public boolean remove(E e) 把给定的对象从当前集合中删除
    public boolean contains(Object obj) 判断当前集合中是否包含给定的对象
    public boolean isEmpty() 判断当前集合是否为空
    public int size() 返回集合中元素的个数
  • 注意事项:

    1. add() 方法的返回值表征添加成功还是失败,如果是 List 系列的集合返回值一定是 true,如果是 set 系列的集合需要看要添加的元素是否已经存在,若已经存在则会添加失败,返回值为 false
    2. remove() 方法的返回值表征删除成功还是失败,删除成功返回 true,若要删除的元素不存在则会删除失败返回 false
    3. contains() 方法底层是调用 equals() 方法来判断是否存在的,所以当集合中存储自定义对象时,需要重写自定义对象的 equals() 方法,否则判断是否存在时不会根据属性值判断,而是根据地址值判断
  • 遍历方式:迭代器遍历,增强型 for 循环遍历,Lambda 表达式遍历

    • 需要注意,由于 Set 系列集合是没有索引的,因此 Collection 集合无法使用普通 for 循环进行遍历

    • 迭代器遍历

      • 路径:java.util.Iterator

      • 迭代器是集合专用的遍历方式

      • Collection 集合获取迭代器对象

        方法名 说明
        Iterator<E> iterator() 返回迭代器对象,默认指向当前集合的 0 索引
      • Iterator 常用方法:

        方法名 说明
        boolean hasNext() 判断当前位置是否有元素,有元素返回 true,否则返回 false
        E next() 获取当前位置元素,并将迭代器对象移向下一个位置
        default void remove() 从集合中删除迭代器对象返回的最后一个元素(当前指向元素的前一个元素)
      • 注意事项:

        1. 若迭代器当前指向的位置没有元素,调用 next() 方法会报错:NoSuchElementException
        2. 迭代器遍历完指针不会复位
        3. 迭代器遍历时,不能用集合的方法进行增加或删除元素,但可以用迭代器自身的方法删除元素
      • 示例:

        ArrayList<String> list = new ArrayList<>();
        Iterator<String> it = list.iterator();
        while (it.hasNext()) {
            String str = it.next();
        }
        
    • 增强型 for 循环遍历

      • 增强型 for 循环的底层就是通过迭代器实现的,是为了简化迭代器的代码书写才出现的

      • 只有单列集合和数组能够使用增强型 for 循环遍历

      • 示例:

        ArrayList<String> list = new ArrayList<>();
        // 注意事项:str是一个第三方变量,就算修改它的值也不会对数组/集合的元素发生影响
        for (String str: list) {
            System.out.println(str);
        }
        
    • Lambda 表达式遍历

      • 得益于 JDK8 提出的 Lambda 表达式,提供了一种更简单的遍历集合方式

      • 使用方法:

        方法名 说明
        default void forEach(Consumer<? super T> action): 结合 Lambda 遍历集合
      • 不采用 Lambda 表达式时:

        ArrayList<String> list = new ArrayList<>();
        list.forEach(new Consumer<String>() {
            @Override
            public void accept(String s) {						// s依次表示集合中的每一个元素,同样也是第三方变量
                System.out.println(s);
            }
        });
        
      • 采用 Lambda 表达式的完整形式时(可以根据具体情况继续简化):

        ArrayList<String> list = new ArrayList<>();
        list.forEach((String s) -> {
                System.out.println(s);
            }
        );
        
    • 总结:通常使用增强 for 循环或者 Lambda 表达式实现遍历,如果需要在遍历时删除元素才使用迭代器遍历


List

  • 特点:有序 —— 存和取的元素顺序一致,有索引 —— 可以通过索引操作元素,可重复 —— 存储的元素可以重复

  • List 集合在继承了 Collection 的基础上,多了很多索引操作的方法

  • List 中特有的常见方法:

    方法名 说明
    void add(int index, E element) 在此集合中的指定位置插入指定的元素
    E remove(int index) 删除指定索引处的元素,返回被删除的元素
    E set(int index, E element) 修改指定索引处的元素,返回被修改的元素
    E get(int index) 返回指定索引处的元素
  • 注意事项:

    List 集合有两个 remove() 方法,一个的形参是 int 类型的索引,另一个是元素的类型,前者是根据索引删除元素,后者是根据元素值删除元素。当元素类型是 Integer 时,实参为 int 类型的数据时两种方法都可以调用,此时优先调用“实参类型和形参类型完全一致的方法”,也就是说优先根据索引删除元素

  • List 集合的遍历方式:迭代器遍历,列表迭代器遍历,增强型 for 循环遍历,Lambda 表达式遍历,普通 for 循环遍历

    • 迭代器遍历,增强型 for 循环遍历,Lambda 表达式遍历和 Collection 中的遍历一致

    • 普通 for 循环遍历

      ArrayList<String> list = new ArrayList<>();
      for (int i = 0; i < list.size(); i++) {
          String s = list.get(i);
      }
      
    • 列表迭代器遍历

      • 路径:java.util.ListIterator

      • List 集合获取迭代器对象

        方法名 说明
        ListIterator<E> listIterator() 返回列表迭代器对象,默认指向当前集合的 0 索引
      • ListIterator 常用方法:

        方法名 说明
        boolean hasNext() 判断当前位置是否有元素,有元素返回 true,否则返回 false
        E next() 获取当前位置元素,并将迭代器对象移向下一个位置
        void add(E e) 将指定元素插入到集合中,插入位置为当前指向的位置,其余元素后置
        void remove() 从集合中删除迭代器对象返回的最后一个元素
    • 总结:在遍历时需要删除元素时使用迭代器,在遍历时需要添加元素时使用列表迭代器,仅仅想遍历时使用增强 for 循环或者Lambda 表达式,如果遍历时需要操作索引则使用普通 for 循环


ArrayList

  • 路径:java.util.ArrayList

  • 底层数据结构是 数组

  • 定义格式:集合类型<数据类型> 集合名 = new 集合类型<数据类型>();

    // <E>:泛型,用于限定集合中的数据类型,E表示具体的数据类型;泛型可以省略,省略后该集合能添加任意类型的数据
    // 在JDK7之后,后面的String可以省略,但是尖括号不能省略
    // 调用无参构造时,默认生成一个初始容量为 10 的空列表
    ArrayList<String> list = new ArrayList<String>();
    
  • 常用方法:

    方法名 说明
    boolean add(E e) 添加元素,返回值表示是否添加成功
    boolean remove(E e) 删除指定元素,返回值表示是否删除成功
    E remove(int index) 删除指定索引的元素,返回被删除元素
    E set(int index, E e) 修改指定索引下的元素,返回原来的元素
    E get(int index) 获取指定索引的元素
    int size() 返回集合长度,也就是集合中元素的个数
    ArrayList<String> list = new ArrayList<>();
    list.add("A");										// 由于add方法返回值一定为true,因此不需要用到该返回值
    list.add("B");
    list.add("C");
    list.add("A");
    System.out.println(list);							// 输出为 [A, B, C, A]
    boolean result = list.remove("A");					// 当需要删除的元素存在时返回true,否则返回false
    System.out.println(list);							// 输出为 [B, C, A],表明存在多个要删除的元素时,只删除第一个
    String str = list.set(1, "D");
    System.out.println(list);							// 输出为 [B, D, A]
    
  • 示例:使用集合存储基本数据类型 —— 包装类的应用

    ArrayList<Integer> list = new ArrayList<>();
    list.add(1);										// 在JDK5之后int和Integer可以互相转化
    
  • 扩展 —— 底层原理:

    • 利用空参创建的集合,在底层创建一个默认长度为 0 的数组
    • 添加第一个元素时,底层会创建一个新的长度为 10 的数组
    • 存满时,扩容为原来的 1.5 倍
    • 如果一次添加多个元素,扩容 1.5 倍后还是放不下,则具体扩容大小以添加元素为准

LinkedList

  • 路径:java.util.LinkedList

  • 底层数据结构是 双向链表

  • 由于双向链表操作首尾元素时速度很快,因此 LinkedList 中提供了很多直接操作首尾元素的特有 API

  • 常用特有方法:

    特有方法 说明
    public void addFirst(E e) 在该列表开头插入指定的元素
    public void addLast(E e) 将指定的元素追加到此列表的末尾
    public E getFirst() 返回此列表中的第一个元素
    public E getLast() 返回此列表中的最后一个元素
    public E removeFirst() 从此列表中删除并返回第一个元素
    public E removeLast() 从此列表中删除并返回最后一个元素

Set

  • 特点:无序 —— 存取顺序不一致,无索引 —— 没有带索引的方法,不重复 —— 可以用来去重复
  • Set 是一个接口,其中的方法基本上与 Collection 中的 API 一致

HashSet

  • 路径:java.util.HashSet
  • 特点:无序,不重复,无索引
  • 底层数据结构是 哈希表
  • 哈希表是一种对于增删改查数据性能都较好的结构,在 JDK8 之前哈希表由数组+链表组成,从 JDK8 开始由数组+链表+红黑树组成
  • 哈希值:对象的整数表现形式
    • 哈希值是根据 hashCode() 方法计算出来的 int 类型的整数
    • 该方法定义在 Object 类中,所有对象都可以调用,默认使用地址值进行计算,因此不同对象的哈希值是不同的
    • 一般情况下,会重写 hashCode() 方法,利用对象内部的属性值计算哈希值,因此只要属性值相同哈希值就相同
    • 在小部分情况下,不同属性或者不同地址计算出来的哈希值也有可能一样(哈希碰撞)
  • HashSet 的底层原理
    • 创建集合对象并添加元素:
      1. 创建一个默认长度为 16 ,默认加载因子为 0.75 的数组(加载因子用于数组扩容)
      2. 根据元素的哈希值跟数组的长度计算出应存入的位置:int index = (数组长度 - 1) & 哈希值
      3. 判断当前位置是否为 null,如果是 null 则直接存入,否则调用 equals() 方法比较对象内部的属性值,如果属性值一样就直接舍弃,如果不一样会存入数组形成链表
      4. 形成链表的具体方式根据 JDK 版本有所不同,在 JDK8 之前是将新元素存入数组,然后将老元素挂在新元素下面;从 JDK8 开始变成了新元素直接挂在老元素下面
      5. 从 JDK8 开始,当链表长度超过8,且数组长度大于等于64时,链表会自动转换成红黑树
    • 注意事项:如果集合中存储的是自定义对象,那么必须重写 hashCode() 和 equals() 方法

LinkedHashSet

  • 路径:java.util.LinkedHashSet
  • 特点:有序,不重复,无索引
  • 底层数据结构是 哈希表,只是每个元素又额外多了一个双向链表的机制记录存储的顺序
  • LinkedHashSet 的有序:HashSet 在遍历时是根据数组索引对数组中的元素和链表进行挨个输出的,由于哈希值的存在这种遍历是无序的;而 LinkedHashSet 引入了双向链表记录了存储顺序(每个元素都记录了前一个、后一个元素的地址值),因此能够有序输出

TreeSet

  • 路径:java.util.TreeSet

  • 特点:可排序,不重复,无索引

  • 底层数据结构是 红黑树

  • TreeSet 可排序:按照元素的默认规则排序

    • 对于数值类型:默认按照从小到大的顺序进行排序
    • 对子字符、字符串类型:按照字符在 ASCII 码表中的数字升序进行排序,字符串对字符逐个比较,例如:"aaa" < "ab" < "aba"
    • 对于自定义类:必须指定比较规则,否则在添加元素时会报错
  • 自定义类指定比较规则:

    • 使用原则:默认使用第一种,只有当第一种方法不能满足比较规则时才使用第二种。比如需要使用自定义规则对字符串进行排序时,由于在 java 中已经对字符串进行了第一种方法的实现(默认按升序排列),此时就需要使用第二种方法

    • 两种方法的区别:第一种方法采用的是 TreeSet 类的空参构造,第二种方法采用的是有参构造且传入比较器

    • 方法一:默认排序/自然排序:Javabean 类实现 Comparable 接口指定比较规则

      // 实现Comparable接口,指定泛型为自身
      public class Student implements Comparable<Student> {
          private int age;
      
          // 构造方法
          public Student() {
          }
          public Student(int age) {
              this.age = age;
          }
          // get/set方法
          public int getAge() {
              return age;
          }
          public void setAge(int age) {
              this.age = age;
          }
      
          // 重写抽象方法
          @Override
          public int compareTo(Student o) {
              // this表示当前要添加的元素,o表示已经在红黑树中的元素,比较时从根节点开始比
              // 返回值为负表明当前要添加的元素是小的,存在左边;返回值为正表明当前要添加的元素是大的,存在右边;返回值为0舍弃
              // 此处按照年龄升序排列
              int result = this.getAge() - o.getAge();
              return result;
          }
      }
      
    • 方法二:比较器排序:创建 TreeSet 对象时,传递比较器 Comparator 指定规则

      示例:按照字符串长度进行升序排列

      // 由于Comparator是接口,使用匿名内部类的方式传递参数(也可以使用Lambda表达式简化)
      TreeSet<String> set = new TreeSet<>(new Comparator<String>() {
          // o1表示当前要添加的元素,o2表示已经在红黑树中的元素,比较时从根节点开始比
          // 返回值为负表明当前要添加的元素是小的,存在左边;返回值为正表明当前要添加的元素是大的,存在右边;返回值为0舍弃
          // 此处按照字符串长度升序排列
          @Override
          public int compare(String o1, String o2) {
              int result = o1.length() - o2.length();
              return result;
          }
      });
      
  • 总结:不同集合的使用场景:

    • ArrayList:集合的元素可以重复时
    • LinkedList:集合中元素可以重复,且增删操作明显多于查询
    • HashSet:如果想对集合中的元素去重
    • LinkedHashSet:如果想对集合中的元素去重,且保证存取顺序
    • TreeSet:如果想对集合中的元素去重并进行排序

Map

  • 路径:java.util.Map

  • Map 是双列集合的顶层接口,它的功能是全部双列集合都可以继承使用的

  • 常用方法:

    方法名 说明
    V put(K key, V value) 添加元素
    V remove(Object key) 根据键删除键值对元素
    void clear() 移除所有的键值对元素
    boolean containsKey(Object key) 判断集合是否包含指定的键
    boolean containsValue(Object value) 判断集合是否包含指定的值
    boolean isEmpty() 判断集合是否为空
    int size() 集合的长度,也就是集合中键值对的个数
  • 注意事项:

    使用 put() 方法时,如果键不存在,则会直接把键值对对象添加到 map 集合中,返回 null;如果键已经存在了,那么会把原有的键值对对象进行覆盖,并把被覆盖的值进行返回

    String a = map.put("1", "1");
    String b = map.put("1", "2");
    System.out.println(a);										// 输出为 null
    System.out.println(b);										// 输出为 "1"
    
  • Map 的遍历方式:键找值,键值对,Lambda表达式

    • 键找值:将键存入单列集合 Set 中,通过遍历单列集合获取键,再通过键找到对应的值

      Map<String, String> map = new HashMap<>();
      map.put("1", "1");
      map.put("2", "2");
      
      Set<String> set = map.keySet();							// 通过keySet()方法将键存入单列集合中
      for (String key: set) {									// 遍历单列集合,可以采用迭代器/增强for/Lambda表达式
          String value = map.get(key);						// 通过get()方法获取键对应的值
          System.out.println(key);
          System.out.println(value);
      }
      
    • 键值对:将键值对对象存入单列集合 Set 中,再通过遍历获取键值对对象

      Map<String, String> map = new HashMap<>();
      map.put("1", "1");
      map.put("2", "2");
      
      // Entry接口是Map接口的内部接口,使用时要么事先import java.util.Map.Entry,要么采用Map.Entry的形式
      Set<Map.Entry<String, String>> set = map.entrySet();	// 通过entrySet()方法将键值对对象存入单列集合中
      for (Map.Entry<String, String> entry : set) {			// 遍历单列集合,可以采用迭代器/增强for/Lambda表达式
          String key = entry.getKey();						// 通过getKey()方法获取键
          String value = entry.getValue();					// 通过getValue()方法获取值
      }
      
    • Lambda表达式

      Map<String, String> map = new HashMap<>();
      map.put("1", "1");
      map.put("2", "2");
      
      // 通过forEach()方法,参数为函数式接口BiConsumer的实现类
      // forEach()方法的底层就是采用entrySet()的方法进行遍历,再调用accept()方法
      map.forEach(new BiConsumer<String, String>() {
          @Override
          public void accept(String key, String value) {		// key为键,value为值
              System.out.println(key);
              System.out.println(value);
          }
      });
      

HashMap

  • 路径:java.util.HashMap
  • HashMap 和 HashSet 的底层原理一模一样,都是哈希表结构,特点是键无序、不重复、无索引
  • 哈希值的计算 hashCode()、对象比较 equals() 都是通过键实现的,与值无关
  • HashMap 和 HashSet 的区别:后者在添加元素时,若计算出来的哈希值相同,equals() 返回值也为 true 则会舍弃;而前者会覆盖

LinkedHashMap

  • 路径:java.util.LinkedHashSet
  • LinkedHashMap 和 LinkedHashSet 的底层原理一模一样,都是哈希表结构+双向链表,特点是键有序、不重复、无索引
  • 哈希值的计算 hashCode()、对象比较 equals() 都是通过键实现的,与值无关
  • LinkedHashMap 和 LinkedHashSet 的区别:相同对象时后者舍弃,前者覆盖

TreeMap

  • 路径:java.util.TreeMap

  • TreeMap 和 TreeSet 的底层原理一模一样,都是红黑树结构,特点是键可排序、不重复、无索引

  • 默认按照键的从小到大进行排序,也可以自己规定键的排序规则

  • 两种排序规则(和 TreeSet 一样):

    • 实现 Comparable 接口,指定比较规则
    • 创建集合时传递 Comparator 比较器对象,指定比较规则
  • 注意事项:

    • TreeMap 的键是不需要重写 hashCode() 和 equals() 方法的
    • TreeMap 的键是自定义类时需要实现 Comparable 接口或者传递比较器对象来实现红黑树的排序,但 HashMap 虽然在 JDK8 之后也采用了红黑树,却是不需要实现 Comparable 接口或者传递比较器对象的,因为 HashMap 是无序的,它的底层默认使用哈希值的大小来实现红黑树的排序
    • 一般情况下,HashMap 的效率要高于 TreeMap
  • 总结:不同集合的使用场景:

    • HashMap:效率最高,默认情况下首选
    • LinkedHashMap:如果需要保证存取有序时采用
    • TreeMap:如果要进行排序时采用

Collections

  • 路径:java.util.Collections

  • 作用:Collections 是集合的工具类

  • 常用方法:

    方法名 说明
    public static <T> boolean addAll(Collection<T> c, T... elements) 批量添加元素
    public static void shuffle(List<?> list) 打乱 List 集合元素的顺序
    public static <T> void sort(List<T> list) 排序
    public static <T> void sort(List<T> list, Comparator<T> c) 根据指定的规则进行排序
    public static <T> int binarySearch(List<T> list, T key) 以二分查找法查找元素
    public static <T> void copy(List<T> dest, List<T> src) 拷贝集合中的元素
    public static <T> int fill(List<T> list, T obj) 使用指定的元素填充集合
    public static <T> void max/min(Collection<T> coll) 根据默认的自然排序获取最大/最小值
    public static <T> void swap(List<?> list, int i, int j) 交换集合中指定位置的元素

不可变集合

  • JDK9 新增功能

  • 定义:长度和内容不能修改的集合

  • 作用:提高代码安全性

  • 创建方式:在 List,Set,Map 接口中,都存在静态的 of() 方法,可以获取一个不可变的集合

    方法名 说明
    static <E> List<E> of(E... elements) 创建具有指定元素的 List 集合对象
    static <E> Set<E> of(E... elements) 创建具有指定元素的 Set 集合对象
    static <K, V> Map<K, V> of(K k1, V v1, ...) 创建具有指定元素的 Map 集合对象
    static <K, V> Map<K, V> ofEntries(Entry<? extends K, ? extends V>... entries) 创建具有指定元素的 Map 集合对象
  • 示例:

    List<String> list = List.of("a", "b", "c");
    Set<String> set = Set.of("a", "b", "c");
    Map<String, Integer> map = Map.of("a", 1, "b", 2, "c", 3);
    
  • 注意事项:

    • 集合一旦创建,就无法添加、删除、修改,只能查询

    • 当获取一个不可变的 Set 集合时,里面的元素一定要保证唯一性,否则会报错

    • 当获取一个不可变的 Map 集合时,里面的键一定要保证唯一性,否则会报错

    • List,Set 的 of() 方法形参是可变参数,可以传入无限个参数;而 Map 里的 of() 方法形参是键值对,可传入参数是有上限的,最多只能传递 20 个参数,也即 10 个键值对

    • 在 JDK9 时,如果需要创建无限长的不可变 Map 集合,需要使用 ofEntries() 方法,先将键值对存入 Entry 数组中(可变参数的本质就是数组),再作为参数传入

      // 1.将键值对存入Map集合
      HashMap<String,String> hm = new HashMap<>();
      hm.put("a", "1");
      hm.put("b", "2");
      hm.put("c", "3");
      // 2.使用entrySet()方法将Map集合转化成由Entry对象组成的单列集合Set
      Set<Map.Entry<String, String>> entries = hm.entrySet();
      // 3.使用toArray()方法将Set集合转换成数组,方法的形参是用于存储的数组
      // 4.toArray()方法在底层会比较集合长度和数组长度,如果集合长度更大,会根据实际长度重新创建数组
      Map.Entry[] arr = entries.toArray(new Map.Entry[0]);
      // 5.使用ofEntries()方法获得不可变集合
      Map map = Map.ofEntries(arr);
      
    • 在 JDK10 时,新增了 copyOf() 方法,用于简化不可变的 Map 集合的创建

      HashMap<String,String> hm = new HashMap<>();
      hm.put("a", "1");
      hm.put("b", "2");
      hm.put("c", "3");
      // 使用copyOf()方法创建不可变集合
      Map<String, String> map = Map.copyOf(hm);
      
posted @   victoria6013  阅读(6)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异
· 三行代码完成国际化适配,妙~啊~
点击右上角即可分享
微信分享提示