一,集合的体系结构

1.1 集合介绍

为什么需要集合?

  • 数组的局限性:长度固定,无法动态扩容;只能存储同一类型数据;缺少现成的操作方法(如排序、查找)
  • 集合的优势:动态扩容(自动调整大小);可存储对象(包括不同类型);提供丰富的操作方法(增删改查、排序等)

集合主要分为两类:

  • Collection单列集合

    单列集合:在添加数据的时候每次只能添加一个据的集合

  • Map双列集合

    双列集合:在添加数据的时候每次可以添加两个数据的集合

示意图如下:

2953321-20230321210401258-1535489775.png

1.2 Collection体系

Collection 集合体系如下图:

2953321-20230321210414781-565842401.png

  1. List 系列集合:添加的元素是有序的,可重复的,有索引的

    有序:存放和拿取的顺序一致

    可重复:集合中存储得元素可重复

    有索引:可以通过索引获取集合中的每一个元素

  2. Set 系列集合:添加的元素是无序,不重复,没有索引的

    无序:存放和拿取得顺序不一致

    不重复:集合中得元素是不可重复

    没有索引:不能通过索引获取集合中的每一个元素

二,Collection

Collection 是单列集合的最终父级接口,它的功能是全部单列集合都可以继承使用的, 即 Collection 接口存放的方法都是所有单列集合中共有的方法

Collection 常用方法

方法名说明
public boolean add(E e)把给定的对象添加到当前集合中
public void clear()清空集合中的所有元素
public boolean remove(E e)把给定的对象在当前集合中删除,删除失败返回false
public boolean contains(Object obj)判断当前集合中是否包含给定的对象
public boolean isEmpty()判断当前集合是否为空
public int size()返回集合中元素的个数/集合的长度

注意要点:

  1. Collection 是接口,不能创建这玩意的对象而是创建它的实现类的对象

  2. add 方法中

    如果我们往List系列添加元素那么返回值永远为 true

    如果我们往Set系列添加元素并且这个元素集合中已经存在,那么会返回 false

  3. contains方法在底层是依赖equals方法判断对象是否一致的

    如果我们是自己定义的对象并且我们没有在这个对象中重写 equals 方法,那么默认使用 Object 类中的 equals 方法进行判断, 而 Object 方法中的 equals 方法是判断地址值的。

    解决方法:在自定义的类中重写 equals 方法即可

    import java.util.ArrayList;
    import java.util.Collection;
    public class Main {
        public static void main(String[] args){
            Collection<User> arr=new ArrayList<>();
            User u1=new User(1,2);
            User u2=new User(3,4);
            arr.add(u1);
            //如果没有在User类中重写equals方法那么调用的就是Object类中的equals就返回false
            System.out.println(arr.contains(u2));
        }
    }
    
    

三,Collection系列集合的遍历方式

之所以用新的遍历方式是因为在 Collection 系列集合中存在 Set 系列集合,Set 系列集合的特点有无索引!故使用新的遍历方式。

3.1 迭代器遍历

迭代器最大的特点:不依赖索引

迭代器在 Java 中的类是Iterator迭代器是集合专用的遍历方式

  • Collection 集合获取迭代器

    ArrayList<String> list=new ArrayList<>();
    Iterator<String> it=list.iterator();
    
  • Iterator中常用的方法

    方法名说明
    boolean hasNext()判断当前位置是否有元素,有元素返回true,没有返回false
    E next()获取当前位置的元素,并且迭代器对象移动到下一个位置
    void remove()删除当前迭代器指向的元素
  • 举例

    ArrayList<String> list=new ArrayList<>();
    list.add("a");
    list.add("aa");
    list.add("aaa");
    list.add("aaaa");
    Iterator<String> it=list.iterator();//获取当前集合的Iterator对象,需要注意迭代器的E和集合的E要一致
    
    while(it.hasNext()){//hasNext是判断当前位置(初始为0)是否有元素!!
    	System.out.println(it.next());//next是获取当前位置的元素的内容!并将迭代器移动到下一个(0->1)
    }
    

    底层就链表,迭代器可以看做 c 语言的指针。

    注意事项:

    1. 如果迭代器已经获取完了最后一个元素,我们再次指向 next 方法,会报错NoSunchElementException

    2. 迭代器遍历完,指针不会复位!

      如果还想再次遍历只能再次获取迭代器对象(因为底层是单向链表

    3. 循环中只能使用一个 next 方法

    4. 迭代遍历的时候,不能用集合的方式进行增加或删除!

3.2 增强for遍历

介绍:

  • 增强for的底层就是迭代器,为了简化迭代器的书写!
  • 它是在JDK5之后出现的,其内部原理就是一个Iterator迭代器
  • 所有的单列集合数组才能使用增强for进行遍历

格式:

for(元素类型 变量名 : 数组或集合){
    
}
for(String s:list){
    sout(s);
}
//s其实就是一个第三方变量,在循环的过程中依次表示集合中的每一个数据
//idea可以使用 集合名.for来快捷键生成

3.3 Lambda表达式遍历

得益于 JDK 8 开始的新技术 Lambda 表达式,提供了一种更简单,更直接的遍历集合的方式。

方法名说明
default void forEach(Consumer<? super T> action):结合lambda遍历集合
ArrayList<String> list=new ArrayList<>();
list.add("a");
list.add("aa");
list.add("aaa");
list.add("aaaa");
list.forEach(new Consumer<String>(){//Consumer是一个接口,并且是函数式接口!
    @Override
    //这里的s就依次表示集合中的每个数据
    public void accept(String s){
        System.out.println(s);//这里的方法体就代表我们要对集合进行的操作
    }
});
//写Lambda表达式的时候如果不太熟可以先写匿名内部类,然后删减多余部分即可!
list.forEach(s->System.out.println(s));//简化写法

forEatch 方法的底层:遍历集合依次得到每一个元素,每得到一个集合中的元素就会把数据传递给我们自己写的 accept 方法

四,List系列的集合

介绍:

  • List系列的集合是Collection单列集合中的一种,故Collection系列的所有方法List系列都可以使用
  • List集合因为有索引,所有多了很多对索引操作的特有的方法!
方法名说明
void add(int index,E element)在此集合中的指定位置插入指定的元素
E remove(int index)删除指定索引的元素,返回被删除的元素
E set(int index,E element)修改指定索引的元素,返回被修改的元素
E get(int index)返回指定索引处的元素

细节:

  1. add 方法:添加后,原来索引上的元素会依次往后移!

  2. remove 方法:存在删除索引和删除指定元素的方法一个是重载 Collection 的,一个是 Collection 的方法

    list.remove(1)此时是删除 1 索引上的元素!而不是元素 1

    原因:在调用方法的时候,如果方法出现了重载现象,优先调用实参和形参一致的那个方法!

    解决方法:手动装箱

    public class Main {
        public static void main(String[] args){
            List<Integer> list=new ArrayList<>();
            list.add(1);
            list.add(2);
            list.add(3);
            list.add(4);
            list.remove(1);//删除的是1索引内容
            System.out.println(list);
            Integer i=Integer.valueOf(1);//手动装箱
            list.remove(i);//删除的是元素1
            System.out.println(list);
        }
    }
    

4.1 List集合的五种遍历方式

List 是继承于 Collection 接口,故 List 也存在 Collection 的三种遍历方式,又 List 存在索引,故 List 也有自己特有的两种遍历方式

  1. 迭代器遍历
  2. 列表迭代器遍历
  3. 增强for遍历
  4. Lambda表达式遍历
  5. 普通for循环(因为List存在索引)

五种遍历方式的举例:

public class Main {
    public static void main(String[] args){
        List<Integer> list=new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        //1.迭代器
        Iterator<String> it=list.iterator();
        while(it.hasNext()){
            String str=it.next();
            System.out.println(str);
        }
        //2.增强for
      	for(String s:list){
            System.out.println(s);
        }
        //3.Lambda表达式
        list.forEach(s->System.out.println(s));
        //4.普通for
        for(int i=0;i<list.size();i++){
            String s=list.get(i);
            System.out.println(s);
        }
    }
}

列表迭代器:是迭代器的子接口(迭代器的方法它都可以使用)

  • 获取列表迭代器

    List<String> arr=new ArrayList<>();
    ListIterator<Stirng> it=arr.listIterator();
    
  • 常用方法:

方法名说明
boolean hasNext()判断当前位置是否有元素,有元素返回true,没有返回false
E next()获取当前位置的元素,并且迭代器对象移动到下一个位置
void remove()删除当前迭代器指向的元素
void set(E e)修改当前迭代器指向的元素
void add(E e)给指针指向的元素添加元素在下一个位置上!
public class Main {
    public static void main(String[] args){
        List<String> list=new ArrayList<>();
        list.add("aaa");
        list.add("bbb");
        list.add("ccc");
        list.add("ddd");
        ListIterator<String> it = list.listIterator();
        while(it.hasNext()){
            String i=it.next();
            if("bbb".equals(i)){
                it.add("qqq");
            }
        }
        System.out.println(list);
    }
}

总结:

  • 迭代器遍历:在遍历的过程中需要删除索引,使用迭代器遍历
  • 列表迭代器:在遍历的过程中需要添加元素,使用列表迭代器
  • 增强for遍历,Lambda表达式:只是想遍历,就用这个
  • 普通for:如果遍历的时候需要操作索引,使用普通for遍历

4.2 ArrayList集合

4.2.1 介绍

定义ArrayListList 接口的动态数组实现,可以存储任意类型的对象(通过泛型指定)。

核心特性

  • 元素有序(按插入顺序排列)
  • 元素可重复
  • 动态扩容(自动调整数组大小)
  • 非线程安全(多线程环境下需同步处理)

创建方法

ArrayList<String> list=new ArrayListM<>();

常用方法:ArrayList 的常用方法就是 Collection 的常用方法 +List 的常用方法

ArrayList<String> list1=new ArrayListM<>();
list1.add("aaa");
list1.add("bbb");
list1.add("ccc");
ArrayList<String> list2=new ArrayListM<>();
list2.addAll(list1);//直接将list1集合的全部内容添加到list2中

4.2.2 底层实现原理

数据结构

  • 基于 Object 数组 实现(源码中的 transient Object[] elementData

  • 默认初始容量为 10DEFAULT_CAPACITY = 10

  • 当数组容量不足时,自动触发扩容机制

扩容机制

  • 扩容公式:新容量 = 旧容量 * 1.5(源码中的 int newCapacity = oldCapacity + (oldCapacity >> 1)

    • 如果还是不够,就取所需的最小容量
  • 扩容步骤

    1. 当添加元素时发现数组已满
    2. 创建一个新数组(大小为原数组的 1.5 倍)
    3. 将旧数组元素复制到新数组
    4. 新元素添加到新数组末尾
  • 样板代码

    ArrayList<Integer> list = new ArrayList<>(10);  // 初始容量10
    
    // 一次性添加1000个元素
    list.addAll(Collections.nCopies(1000, 1));
    
    // 扩容过程:
    // 1. 当前容量10,需要1010
    // 2. 计算 newCapacity = 10 + (10 >> 1) = 15
    // 3. 15 < 1010,所以 newCapacity = minCapacity = 1010
    // 直接扩容到1010,而不是15!
    

性能特点

  • 查询快:通过索引直接访问数组元素(时间复杂度 O(1))

  • 增删慢

    • 尾部插入快(O(1))
    • 中间插入/删除需移动元素(O(n))

4.2.3 使用场景

场景说明
频繁查询适合使用索引快速访问元素(如数据展示)
尾部增删尾部操作效率高(如日志记录)
元素有序存储需要保持插入顺序时使用
不涉及线程安全单线程环境下优先选择(相比 Vector 性能更好)

4.2.4 常见问题与注意事项

  1. 并发修改异常

    • 问题:在遍历集合时修改元素会抛出 ConcurrentModificationException

      ArrayList<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
      for (String s : list) {
          if (s.equals("B")) {
              list.remove(s); // 抛出异常!
          }
      }
      

      原因

      // 增强for循环的内部实现
      Iterator<String> it = list.iterator();  // 获取迭代器
      while(it.hasNext()) {
          String s = it.next();  // 这里会检查modCount!
          if(s.equals("B")) {
              list.remove(s);  // 直接操作原集合,修改了modCount
          }
      }
      // 下次调用it.next()时,发现modCount变了,抛出异常
      
    • 解决方案

      • 使用迭代器的 remove() 方法

        ArrayList<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
        Iterator<String> it = list.iterator();
        
        while(it.hasNext()) {
            String s = it.next();
            if(s.equals("B")) {
                it.remove();  // 关键:使用迭代器的remove方法
            }
        }
        System.out.println(list);  // [A, C]
        
      • 使用 CopyOnWriteArrayList(并发场景)

        import java.util.concurrent.CopyOnWriteArrayList;
        
        List<String> list = new CopyOnWriteArrayList<>(Arrays.asList("A", "B", "C"));
        for(String s : list) {  // 可以安全遍历和删除
            if(s.equals("B")) {
                list.remove(s);  // CopyOnWriteArrayList允许这样操作
            }
        }
        System.out.println(list);  // [A, C]
        
      • 使用 fori 循环倒序遍历

        ArrayList<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
        
        for(int i = list.size() - 1; i >= 0; i--) {
            if(list.get(i).equals("B")) {
                list.remove(i);  // 从后往前删,不会影响索引
            }
        }
        // 或者正序但要调整索引
        for(int i = 0; i < list.size(); i++) {
            if(list.get(i).equals("B")) {
                list.remove(i);
                i--;  // 索引回退
            }
        }
        
      • 使用 removeIf()

        ArrayList<String> list = new ArrayList<>(Arrays.asList("A", "B", "C"));
        list.removeIf(s -> s.equals("B"));  // 一行搞定!
        System.out.println(list);  // [A, C]
        
  2. 优化建议

    • 预分配容量:如果已知数据量大小,初始化时指定容量避免频繁扩容
    • 避免中间插入:大量中间插入操作应改用 LinkedList
  3. 与数组的转换

    • ArrayList → Array

      String[] arr = list.toArray(new String[0]);
      
    • Array → ArrayList

      ArrayList<String> list = new ArrayList<>(Arrays.asList(arr));
      
  4. 线程安全问题

    • 默认非线程安全:多线程环境下需同步处理

    • 解决方案

      List<String> syncList = Collections.synchronizedList(new ArrayList<>());
      

4.3 LinkedList集合

4.3.1 介绍

定义LinkedListList 接口的双向链表实现,同时实现了 Deque 接口,可用作队列或双端队列。

核心特性

  • 元素有序(按插入顺序排列)
  • 元素可重复
  • 无需扩容(动态添加节点)
  • 非线程安全(多线程环境下需同步处理)

LinkedList 创建方法

LinkedList<String> list=new LinkedList<>();

LinkedList 独有的方法

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

4.3.2 底层实现原理

数据结构

  • 基于 双向链表 实现(源码中的 Node<E> 内部类)
  • 每个节点包含:
    • E item:存储元素
    • Node<E> next:指向下一个节点
    • Node<E> prev:指向前一个节点
  • 维护 firstlast 指针,分别指向链表的头和尾

操作原理

  • 插入/删除:只需修改相邻节点的引用(时间复杂度 O(1))
  • 随机访问:需要从头或尾遍历链表(时间复杂度 O(n))

性能特点

  • 增删快:头尾操作效率极高(如实现栈、队列时)
  • 查询慢:无法直接通过索引定位元素

4.3.3 使用场景

场景说明
频繁头尾操作实现栈、队列、双端队列的理想选择
中间插入/删除相比 ArrayList 更高效(无需移动元素)
不确定数据量无需担心扩容问题

4.3.4 常见问题与注意事项

  1. 线程安全问题

    默认非线程安全:多线程操作需手动同步
    解决方案

    List<String> syncList = Collections.synchronizedList(new LinkedList<>());
    
  2. 内存开销

    每个节点需要存储两个引用(prev/next),内存占用比 ArrayList 高约 3 倍

  3. 遍历性能

    • 避免通过索引遍历(如 list.get(i)),时间复杂度为 O(n^2)
    • 推荐使用迭代器,时间复杂度为 O(n)

五,Set系列集合

介绍

  • Set 系列集合添加的元素是无序不重复无索引

    无序:存和取得顺序不一致,即:存入 1,2,3 可能取出 2,3,1

    不重复:集合内元素不重复

    无索引:没有带索引的方法,不能用普通 for 循环遍历,无法使用索引获取元素

  • Set 集合的实现类:

    HashSet:无序,不重复,无索引

    LinkedHashSet:有序,不重复,无索引

    TreeSet:可排序,不重复,无索引

  • Set 接口注意事项

    • Set 接口是基础 Collection 接口的,故基本方法和 Collection 一致

    • Set 集合可以用 Collection 的三种遍历方式进行遍历

    • Set 集合的 add 方法如果第二次添加相同的元素结果会返回 false

在 Java 中,Set 集合不提供直接的方法来修改元素的内容。Set 是一种不允许重复元素的集合,它的设计目的是用于存储和管理不重复的对象。因此,如果你想修改 Set 集合中的元素内容,你需要先删除旧的元素,然后再添加新的元素。

5.1 HashSet

HashSet 的特点:无序不可重复无索引,允许一个 null。

HashSet 底层采用哈希表存储数据,哈希表是一种对于增删改查数据性能都比较好的数据结构

HashSet 底层:

  • JDK8之前:数组+链表
  • JDK8开始:数组+链表+红黑树

数据存储是无序的:数据存入位置公式int index=(数组长度-1) & 哈希值

哈希值:

  • 根据hashCode方法计算出的int类型的整数
  • 该方法定义在Object类中,所有对象都可以调用,默认使用地址值进行计算
  • 一般情况下会重写hashCode方法,利用对象内部的属性值计算哈希值

哈希值特点

  • 如果没有重写hashCode方法,不同对象计算出的哈希值是不同的
  • 如果重写了hashCode方法,不同的对象只要属性值相同,计算出的哈希值就是一样的
  • 在小部分情况下,不同属性值或不同地址值计算出的哈希值也可能一样(哈希碰撞

HashSet 添加元素的情况

2953321-20230326191817268-1326249936.png

  • 扩容时机:当数组内元素个数为:当前数组长度*默认加载因子16*0.75=12的时候扩容为原数组的两倍

  • 当链表长度 >8 且数组长度 >64,链表就会转化为红黑树

2953321-20250410142746295-1315962881.png

  • 注意点:如果 HashSet 存储的自定义对象,必须重写hashCodeequals方法!

    重写 hashCode 目的是想根据属性值去计算哈希值

    重写 equals 目的是想比较对象内部属性

    Object 类中的 hashCode 和 equals 都是通过地址值进行计算和比较的一般不用

  • HashSet 存取顺序不一致:因为是根据哈希值存入的,通过数组 0 索引开始查,每个数据的哈希值不一致,不可能按照顺序存入

  • HashSet 为何不用索引:因为哈希表用上了链表和红黑树,无法定义同一个数组下哪一个链表或红黑树节点为 0 索引,故不用

  • HashSet 利用什么机制去重的:利用 hashCode 得到哈希值从而确定当前元素在数组中的位置,利用 equals 比较对象内部的属性值是否相同。这就是为何自定义对象的时候需要重写方法

  • 线程安全问题

    • 非线程安全:多线程操作可能导致数据不一致

    • 解决方案

      Set<String> syncSet = Collections.synchronizedSet(new HashSet<>());
      
      Set<String> safeSet = ConcurrentHashMap.newKeySet();
      

5.2 LinkedHashSet

介绍

  • LinkedHashSet是HashSet的子类,具有全部的HashSet的方法,不需要多记其API只需记住特性即可
  • 特性:有序,不重复,无索引,允许一个null
  • 性能平衡
    • 查找效率接近 HashSet(平均 O(1))
    • 插入/删除效率略低于 HashSet(需维护链表)
  • 允许 null 值:可存储一个 null 元素
  • 非线程安全:多线程环境下需同步处理
  • LinkedHashSet会保证数据的存入和取出的顺序一致!
    • 原理:底层数据结构依旧是哈希表,只是每个元素又额外多了一个双链表机制记录存储的顺序

2953321-20230326191736437-1795147044.png

8 索引添加的是第一个元素,0 索引是添加的最后一个元素,两个单独的指针代表头节点和尾节点

当要求数据去重且存取有序的时候才使用 LinkedHashSet。要不然默认使用 HashSet,因为 LinkedHashSet 效率较低

5.3 TreeSet

介绍

  • TreeSet 是 Set 接口的子类,具有全部的 Set 接口的方法,不需要多记其 API 只需记住特性即可

  • 特性:

    • 元素唯一性:不允许重复元素(依赖 compareTo()compare() 判断重复)
      • compareTo()compare() 返回 0 时,视为重复元素,拒绝插入
      • 不允许NULL因为(compareTo()compare() 无法处理 null
    • 有序性:元素自动排序(默认升序,可通过 Comparator 自定义)
    • 无索引:不能通过索引访问元素
    • 非线程安全:多线程环境下需手动同步
  • TreeSet 底层是基于红黑树的数据结构实现排序的,增删改查的性能都比较好

    因为 TreeSet 底层是红黑树,故不需要重写 equals 方法和 hashCode 方法在自定义类中,但是要指定排序规则

  • TreeSet 集合默认的规则

    1. 对于数值类型,Integer,Double,默认按照从小到大排序

    2. 对于字符,按照字符在 ASCII 码表中的数字升序排序

    3. 对于字符串:按照字典序

    4. 对于自定义的类型,需要手动添加排序规则!

    TreeSet 的两种比较方式

    1. 默认排序 / 自然排序:javabean 类实现Comparable接口指定比较规则

      public class Student implements Comparable<Student>{//实现接口
          int age;
          String name;
          @Override//重写接口中的方法
          public int compareTo(Student o) {
              //指定排序规则
              //只看年龄,按照年龄的升序排序
              return this.getAge()-o.getAge();
              //this表示当前要添加的元素,o表示在已经在红黑树中存在的元素
              //返回值:
              //1.负数:认为要添加的元素是小的,存左边
              //2.正数:认为要添加的元素是大的,存右边
              //3.0:认为要添加的元素已经存在,舍弃
          }
      }
      
    2. 比较器排序:创建 TreeSet 对象的时候,传递比较器Comparator指定规则

      使用原则:默认使用第一种,如果第一种无法满足需求才使用第二种

      例:存入四个字符串 "c","ab",”df“,”qwer",按照长度排序,长度一致按照首字母排序

      import java.util.*;
      public class Main {
          public static void main(String[] args) {
              TreeSet<String> ts=new TreeSet<>(new Comparator<String>() {
                  @Override
                  //o1当前要添加的元素,o2已经在红黑树存在的元素
                  //返回规则一样
                  public int compare(String o1, String o2) {
                      int i=o1.length()-o2.length();
                      i= i== 0 ? o1.compareTo(o2):i;//比较长度,如果长度一致使用默认排序规则compareTo,否则以长度为准
                      return i;
                  }
              });
              //这个Comparator接口是函数式接口,可以用Lambda表达式简化
              TreeSet<String> ts=new TreeSet<>((o1,o2)->{
                  int i=o1.length()-o2.length();
                  i= i== 0 ? o1.compareTo(o2):i;
                  return i;
              });
              ts.add("c");
              ts.add("ab");
              ts.add("df");
              ts.add("qwer");
              System.out.println(ts);
          }
      }
      

      如果两种方式都存在,方式二优先使用

六,Map

6.1 Map简述

双列集合特点:

  1. 双列集合一次需要存一对数据,分为键 (key) 和值(value)

  2. 键不能重复,值可以重复

  3. 键和值是一一对应的,每一个键只能找到自己对于的值

  4. 键 + 值这个整体,叫键值对或键值对对象,在 Java 中叫Entry 对象

  5. Map 集合体系如下图:

2953321-20230321210401258-1535489775.png

Map 系列集合常用 API

  1. 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()集合的长度,即集合中键值对的个数
  2. put 方法的细节:

    在添加数据的时候,如果键不存在,那么直接把键值对对象添加到 map 集合中,返回 null

    在添加数据的时候,如果键是存在的,那么会把原有的数据覆盖,并把被覆盖的值进行返回

6.2 Map系列集合遍历方式

6.2.1 键找值方式遍历

import java.util.*;
public class Main {
    public static void main(String[] args) {
        Map<String,String> map=new HashMap<>();
        map.put("a","A");
        map.put("b","B");
        map.put("c","C");
        map.put("d","D");
        //获取所有的键,把键放入单列集合中
        Set<String> keys = map.keySet();//keySet方法就是获取所有的key并返回Set集合
        
        for (String key : keys) {//Set集合可以使用的三种遍历方式,Lambda,增强for,迭代器iterator
            String value = map.get(key);//Map系列集合的get方法,通过key获取value
            System.out.println(key+"="+value);
        }
        
        //可以简化,不需要keys这个变量
        for (String key : map.keySet()) {
            String value = map.get(key);
            System.out.println(key+"="+value);
        }
        
    }
}

6.2.2 键值对方式遍历

import java.util.*;
public class Main {
    public static void main(String[] args) {
        Map<String,String> map=new HashMap<>();
        map.put("a","A");
        map.put("b","B");
        map.put("c","C");
        map.put("d","D");
        //通过entrySet方法获取键值对对象
        //Entry是Map接口中的内部接口
        //可以通过外部接口.内部接口方式使用内部接口,也可也通过import java.util.Map.Entry方式导入
        //这样就直接Entry<K,V>即可
        Set<Map.Entry<String, String>> entries = map.entrySet();
        //通过entries这个单列集合获取键值对对象内部的k和v
        //因为是set集合,可以使用set集合的三种遍历方式,这里暂时采用Lambda方式,因为最简单
        entries.forEach(s-> System.out.println(s.getKey()+"="+s.getValue()));
        //getKey是键值对对象的方法,获取key
        //getValue是键值对对象的方法,获取value
    }
}

6.2.3 Lambda表达式方式遍历

使用 Map 集合中下列方法遍历元素

方法名说明
default void forEach(BiConsumer<? suoer K,? super V> action)结合Lambda表达式遍历Map集合
import java.math.BigDecimal;
import java.util.*;
import java.util.function.BiConsumer;

public class Main {
    public static void main(String[] args) {
        Map<String,String> map=new HashMap<>();
        map.put("a","A");
        map.put("b","B");
        map.put("c","C");
        map.put("d","D");
        //这里写内部类形式方便理解
        map.forEach(new BiConsumer<String, String>() {
            @Override
            public void accept(String key, String value) {
                System.out.println(key+"="+value);
            }
        });
        //简化为Lambda表达式
         map.forEach((key,value)->System.out.println(key+"="+value));
    }
}

forEatch 方法的底层:就是用第二种方式进行遍历,依次得到每一个键和值,再调用 accept 方法

6.1 HashMap

  • HashMap 特点:

    1. HashMap 是 Map 里面的一个实现类

    2. 没有额外需要学习的特有方法,直接使用 Map 中的方法即可

    3. 特点都是由键决定的:无序,不重复,无索引(这里都是关于键的特性,值无要求)

    4. HashMap 和 HashSet 底层原理是一模一样的,都是采用哈希表结构

      存入的方式和 HashSet 也一样,只不过 HashMap 是根据进判断的

    5. 如果存储的是自定义对象,需要重写 hashCode 和 equals 方法

    6. 如果值存储的是自定义对象,不需要重写

    7. 依赖 hashCode 和 equals 方法保证键的唯一

  • HashSet vs HashMap

    • HashSet内部直接封装了一个HashMap元素作为HashMap的键(Key),而值(Value)统一为一个固定占位对象(如PRESENT = new Object())。
    • 底层均基于**哈希表(数组 + 链表/红黑树)**实现,哈希冲突处理逻辑完全一致。

6.2 LinkedHashMap

  • LinkedHashMap特点:
    1. 由键绝对:有序,不重复,无索引。
    2. 原理:和LinkedHashSet一致。。。
    3. 方法和Map方法一样,不需要额外记。。

6.3 TreeMap

  • TreeMap 和 TreeSet 一样都是红黑树结构

  • 由键决定:可排序,不重复,无索引

    可排序:对键进行排序

  • 这玩意和 TreeSet 唯一的区别是多了个 Value 其他用法和 TreeSet 一致,只需看 TreeSet 即可

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

    排序规则实现方式和 TreeSet 一样

    1. 在自定义类中实现 Comparable 接口,指定比较规则

    2. 创建集合的时候传递 Comparator 比较器对象,指定比较规则

  • TreeSet vs TreeMap

    • TreeSet内部封装了一个TreeMap,元素作为键,值同样为固定占位对象。
  • 底层均基于 ** 红黑树(自平衡二叉搜索树)** 实现,排序和查找逻辑一致。

七,可变参数

//计算n个数据的和
//jdk5之前写法
int[] arr={1,2,3,4,5,6,7,8,9,10};
int sum=getSum(arr);
int get Sum(int[] arr){
    int sum=0;
    for(int i:arr){
        sum=sum+i
    }
    return i;
}
//jdk5开始的写法:
//可变参数:方法的形参的个数是可变的
//格式:数据类型...形参名
//int...nums
//举例
int sum=getSum(1,2,3,4,5,6,7,8,9,10);
int get Sum(int...nums){
    int sum=0;
    for(int i:nums){
        sum=sum+i
    }
    return i;
}
//底层:可变参数就是一个数组,是Java帮助我们创建好的数组,只需把nums当作数组调用即可

可变参数的小细节:

  1. 在方法的形参中,最多只能定义一个可变参数

    int get Sum(int...nums,int...a){//错误写法,无法判断前面参数应该接收多少个参数
    }
    
  2. 在方法当中,如果除了可变参数以外的形参数,那么可变参数必须在最后!

    int get Sum(int...nums,int a){//错误写法,无法判断前面参数应该接收多少个参数
    }
    int get Sum(int nums,int...a){//正确写法
    }
    

八,Collections类

  • Collections 是java.util.Collections的类,是集合的工具类

  • 常用方法:

    方法名说明
    public static boolean addAll(Collection c,T...elements)批量添加元素
    public static void shuffle(List<?> list)打乱List集合元素的顺序
    public static void sort(List list)排序
    public static void sort(List,Comparator c)根据指定规则排序
    public static int binarySearch(List list,T key)以二分查找算法查找元素(元素必须有序
    public static int fill(List list,T obj)使用指定元素填充集合
    public static void max/min(Collection coll)根据默认的自然排序获取最大/最小值
    public static void swap(List<?> list,int i,int j)交换指定索引的元素
  • 小细节:

    假设 list 集合里面为空,啥也没有,用 fill 里面是啥也填充不到的,因为不知道要填充多少。

    即要填充的集合里面必须有值才能填充(即将里面值全部替换成填充值 )

    例题:

    //实现概率抽取集合元素
    //随机抽取集合中元素且,男占70%女占30%
    
    import java.lang.reflect.Array;
    import java.util.*;
    
    public class Main {
        public static void main(String[] args) {
            ArrayList<Integer> list = new ArrayList<>();
            Collections.addAll(list, 1, 1, 1, 1, 1, 1, 1);//1代表男生占比,6个即百分之60
            Collections.addAll(list, 0, 0, 0, 0);//0代表女生占比,4代表百分之40
            Collections.shuffle(list);//打乱
            Random r = new Random();
            int index = r.nextInt(list.size());//这样获取到男生占比60%女生占比40%
            int nums = list.get(index);
            System.out.println(nums);
    
            ArrayList<String> boy = new ArrayList<>();
            ArrayList<String> girl = new ArrayList<>();
            Collections.addAll(boy, "男1", "男2", "男3", "男4", "男5", "男6");
            Collections.addAll(girl, "女1", "女2", "女3", "女4");
            if (nums == 1) {
                int boyIndex = r.nextInt(boy.size());
                System.out.println(boy.get(boyIndex));
            } else if (nums == 0) {
                int girIndex = r.nextInt(girl.size());
                System.out.println(girl.get(girIndex));
            }
        }
    } 
    

九,不可变集合

使用场景:

  • 如果某个数据不能被修改,就放入到不可变集合中
  • 当集合对象被不信任的库调用的时候,不可变形式是安全的。

简单理解:不想让外界修改集合中的内容

格式

//1.创建不可变的List集合
List<T> list=List.of(Element...元素);
//2.创建不可变的Set集合
Set<T> set=Set.of(Element...元素);
//3.创建不可变的Map集合
Map<K,V> map=Map.of(k1,v1,k2,v2,k3,v3....);

细节:

  1. 不可变集合一旦创建就无法修改,只能查询

  2. Set 类型的不可变集合里面的元素不能重复!

  3. Map 类型的集合默认把第一个参数当作 K,第二个参数当作 V 以此类推

  4. Map 集合的键不能重复

  5. Map 里面的 of 方法有上限,最多传递 20 个参数。

    因为其他两个是用可变参数传递的

    Map 集合是通过方法重载的方式写了一堆 of 方法

    解决方法:传递 Entry 键值对对象即可

    //创建一个普通map集合
    HashMap<String,String> hm=new HashMap<>();
    hm.put("a","A");
    hm.put("b","B");
    hm.put("c","C");
    hm.put("d","D");
    //2.利用上面的数据获取一个不可变集合
    //获取entry对象
    Set<Map.Entry<String,String>> entries=hm.entrySet();
    //把entries变成数组
    //使用方法 toArray(指定类型)
    Map.Entry[] arr = entries.toArray(new Map.Entry[0]);
    //这里的new Map.Entry[0]就是创建一个Map.Entry数组,长度为0
    //toArray底层细节:toArray底层会比较集合长度和数组长度大小,
    //如果集合长度>数组长度,即数据在数组中放不下,此时会根据实际数据个数重新创建数组
    //如果集合长度<=数组长度,即数据在数组中放得下,直接使用这个数组
    Map map=Map.ofEntries(arr);//将键值对对象传入不可变Map集合
    
    //上述方法太难记了可使用Map集合的copyOf方法构建一个不可变map集合(jdk10才有这个方法)
    Map map=Map.copyOf(hm);//这个底层就是上述代码