(五)Java基础知识复习(集合、迭代器、泛型与数据结构)

148 阅读48分钟

一、Collection 集合

(一)Collection 集合概述

集合是存储数据的容器,可以存储多个引用类型的数据,Collection 是单列集合的顶层接口。 集合与数组的区别

  • 数组:长度固定,可以存储任意数据类型。
  • 集合:长度可变,只能存储引用数据类型,要存储基本数据类型可使用包装类。

(二)Collection 集合的继承体系

Collection 是单例集合的顶层接口,定义了单例集合的共有特性。

image.png

不同集合的特点

  • List:元素有索引,元素可重复
    • ArrayList:存取有序,查询快,增删慢
    • LinkedList:存取有序,查询慢,增删快
  • Set:元素无索引,元素不可重复
    • HashSet:存取无序
    • LinkedHashSet:存取有序
    • TreeSet:存取无序,对元素进行排序

(三)Collection 集合的常用功能

image.png

    public static void main(String[] args) {
        //使用多态的方式创建 Collection 对象
        Collection<String> col = new ArrayList<>();

        //添加元素
        col.add("张三");
        col.add("李四");
        col.add("王五");
        System.out.println(col); //[张三, 李四, 王五]

        //移除指定的元素
        col.remove("王五");
        System.out.println(col); //[张三, 李四]

        //判断集合中是否包含指定的元素
        System.out.println(col.contains("张三")); //true
        System.out.println(col.contains("王五")); //false

        //判断集合是否为空
        System.out.println(col.isEmpty()); //false

        //获取集合中元素的数量
        System.out.println(col.size()); //2

        //集合转数组
        //new String[0] = [张三, 李四]
        //new String[3] = [张三, 李四, null]
        //如果传递数组的长度小于集合元素的数量,toArray 会创建一个长度是集合数量的新数组用于存放元素,并返回
        //如果传递数组的长度大于或等于集合元素的数量,toArray 方法就不会创建新数组了,而是直接使用你给的数组来存储元素
        String[] arr = col.toArray(new String[0]);
        System.out.println(Arrays.toString(arr));

        //清空集合中所有的元素
        col.clear();
        System.out.println(col); //[]
    }

二、迭代器

(一)迭代器概述

迭代,是获取集合中元素的一种方式,在有索引的的集合中我们可以使用 for 循环进行元素遍历,而对于没有索引的集合,则可以使用迭代器来进行遍历。

Iterator 是迭代器的顶层接口,实现了 Iterator 接口的类就具备了进行迭代的能力。

Collection 接口中定义了一个获取迭代器的方法

Iterator<E> iterator();

Iterator 迭代器有两个重要的方法,分别是

  • hasNext() :判断是否还有下一个元素。
  • next() :获取下一个元素。
    public static void main(String[] args) {
        //创建集合
        Collection<String> col = new ArrayList<>();

        //添加元素
        col.add("张三");
        col.add("李四");
        col.add("王五");

        //取出集合中所有的元素,需要先获得一个迭代器
        Iterator<String> it = col.iterator();

        //迭代集合中所有的元素
        while (it.hasNext()) {
            String name = it.next();
            System.out.println(name);
        }

    }

(二)使用迭代器的注意事项

  • 迭代器是一次性的,如果要重新迭代集合中的元素,必须再获取一个新的迭代器。
  • 如果已经迭代到集合的末尾,就不能使用 next 方法获取元素,否则会抛出 NoSuchElementException 异常。
  • 在迭代集合的过程中,不能增加或删除集合中的元素,否则会抛出 ConcurrentModificationException 异常。
    public static void main(String[] args) {
        Collection<String> col = new ArrayList<>();

        col.add("张三");
        col.add("李四");
        col.add("王五");

        Iterator<String> it = col.iterator();

        while (it.hasNext()) {
            String name = it.next(); //ConcurrentModificationException 并发修改异常
            System.out.println(name);

            //如果当前元素是“李四”,那么就将“李四”从集合中删除
            if (name.equals("张三")) {
                col.remove("张三");
            }
        }

        System.out.println(col);

        //在这里不能再通过迭代器去获取集合中的元素,NoSuchElementException
//        String name = it.next();
//        System.out.println(name);
    }

三、增强 for 循环

增强 for 又称为 for each 循环,是一种基于迭代器的增强版 for 循环,主要用于遍历数组和 Collection 集合。

由于增强 for 循环基于迭代器,所以在使用增强 for 循环遍历的过程中,不能对集合的元素进行增加和删除。

语法格式

for (数据类型 变量名 : 集合 or 数组) {
//循环体
}
public class Demo01 {

    public static void main(String[] args) {
        //创建数组
        int[] arr = {11, 22, 33, 44, 55};

        //使用普通 for 循环遍历数组中的元素
        for (int i = 0; i < arr.length; i++) {
            System.out.println(arr[i]);
        }

        //使用增强 for 循环遍历数组中的元素
        for (int i : arr) {
            System.out.println(i);
        }
    }
}
public class Demo02 {

    public static void main(String[] args) {
        //创建集合
        Collection<Integer> col = new ArrayList<>();

        col.add(100);
        col.add(200);
        col.add(300);
        col.add(400);
        col.add(500);

        //使用迭代器遍历集合中所有的元素
        Iterator<Integer> it = col.iterator();
        while (it.hasNext()) {
            Integer number = it.next();
            System.out.println(number);
        }

        System.out.println("=======");

        //使用增强 for 循环遍历集合中所有的元素
        for (Integer number : col) {
            System.out.println(number);
        }
    }
}

四、泛型

(一)泛型概述

泛型,即“参数化类型”,是 JDK5 引入的新特性。泛型提供了编译期的类型安全检查机制,避免出现错误的类型。

1. 集合不使用泛型

public static void main(String[] args) {    
    Collection col = new ArrayList();    
    col.add("北京");    
    col.add("上海");    
    col.add(100);    
    Iterator it = col.iterator();    
    while (it.hasNext()) {        
        String ele = (String) it.next(); //ClassCastException        
        System.out.println(ele);    
    }
}

集合不使用泛型时,任意类型的元素都能添加到集合中,这对取出元素非常不友好,因为返回的是 Object 类型,在使用元素时容易出现问题。

2. 集合使用泛型

public static void main(String[] args) {    
    Collection<String> col = new ArrayList<>();    
    col.add("北京");    
    col.add("上海");   
    Iterator<String> it = col.iterator();    
    while (it.hasNext()) {        
        String ele = it.next();        
        System.out.println(ele);    
    }
}

集合使用泛型时,由于泛型限制了集合中元素的类型,所以在取出元素时无需强转,避免了类型转换的问题。

(二)常用的泛型类型参数

  1. T(Type)

    • 通常用于表示任意类型。是最常用的泛型类型参数。
    • 常用于类、接口和方法中表示单个类型的占位符。
    • 示例:public class Box<T> { private T t; }
  2. E(Element)

    • 通常用于集合框架中,表示集合的元素类型。
    • 示例:public interface List<E> { void add(E element); }
  3. K(Key)和 V(Value)

    • 常用于表示键值对中的键和值,主要用于映射(如Map)。
    • 示例:public interface Map<K, V> { V get(K key); }
  4. N(Number)

    • 通常用于表示数值类型。
    • 可以用于限制类型参数为数值类型或其子类。
    • 示例:public class Calculator<N extends Number> { }
  5. R(Result)

    • 通常用于表示返回类型。
    • 示例:public interface Callable<R> { R call(); }
  6. ?(Wildcard)

    • 用于表示未知类型。
    • 常用于方法参数和集合中,以提供灵活性。
    • 示例:List<?> list = new ArrayList<Integer>();

泛型类型参数的使用场景

  • 类和接口:定义可以处理多种类型的类或接口。

    public class Pair<K, V> {
        private K key;
        private V value;
    }
    
  • 方法:创建能够操作不同类型的通用方法。

    public static <T> void print(T item) {
        System.out.println(item);
    }
    
  • 边界约束:限制类型参数的范围。

    public <T extends Comparable<T>> T findMax(T[] array) {
        // Implementation
    }
    

使用这些泛型类型参数可以使代码更具通用性和可重用性,同时保持类型安全性。

(二)泛型类

声明类时定义泛型,该泛型可以在整个类中使用(静态除外),泛型类中的泛型在类的对象被创建时确定,如果创建对象时不指定泛型,则类中的泛型默认是 Object。

1. 泛型类的定义格式

public class 类名<泛型> { 
    //泛型可以是任意字母,通常是大写,例如 T、E 等
}

2. 泛型类的定义

public class GenericClass<E> {    

    private E e;   
    
    public E getE() {        
        return e;    
    }    
    
    public void setE(E e) {        
        this.e = e;    
    }
}

3. 泛型类的使用

public static void main(String[] args) {    
    GenericClass<String> gc = new GenericClass<>();    
    gc.setE("Hello");    
    String e = gc.getE();    
    System.out.println(e); //Hello
}

(三)泛型方法

我们也可以在声明方法时在方法中使用泛型,方法中泛型的具体类型将在方法被调用时确定。

1. 泛型方法的定义格式

泛型方法的定义在返回类型之前声明一个类型参数,类型参数放在方法的修饰符和返回类型之间

public <泛型> 返回值类型 方法名(数据类型 参数名) {    
    //...
}

2. 泛型方法的定义和使用

public static void main(String[] args) {    
    String retValue1 = test("Hello");    
    System.out.println(retValue1);    
    Integer retValue2 = test(100);    
    System.out.println(retValue2);
}
public static <T> T test(T arg) {
    System.out.println(arg);    
    return arg;
}

(四)泛型接口

定义接口时,也可以定义泛型,接口中的泛型在实现类实现接口时确定。

1. 泛型接口的定义格式

public interface 接口名<泛型> {    
    //...
}

2. 泛型接口的定义和使用

实现类在实现接口时,如果能够确定接口的泛型类型,则可以直接指定

public interface GenericInterface<E> {    
    E show(E arg);
}

public class GenericInterfaceImpl implements GenericInterface<String> {   
    @Override   
    public String show(String arg) {        
        System.out.println(arg);        
        return arg;    
    }
}

实现类在实现接口时,如果不能确定接口的泛型类型,也可以将泛型的确定延迟到实现类创建对象时。

public interface GenericInterface<E> {    
    E show(E arg);
}

public class GenericInterfaceImpl<E> implements GenericInterface<E> {    
    @Override    
    public E show(E arg) {        
        System.out.println(arg);       
        return arg;    
    }
}

public static void main(String[] args) {    
    GenericInterfaceImpl<String> gci = new GenericInterfaceImpl<>();    
    String retValue = gci.show("Hello");    
    System.out.println(retValue); //Hello
}

(五)泛型通配符

在指定泛型的具体类型时如果不确定泛型的类型,可以使用泛型通配符。泛型通配符和不指定泛型是有区别的,泛型通配符可以指定上限和下限。

不指定泛型:

public static void main(String[] args) {    
    Collection<Object> col1 = new ArrayList<>();   
    Collection<String> col2 = new ArrayList<>();   
    Collection<Number> col3 = new ArrayList<>();    
    Collection<Integer> col4 = new ArrayList<>();  
    col1.add(new Object());   
    col2.add("Hello");  
    col3.add(100);  
    col4.add(200);    
    //调用未指定泛型的方法  
    each(col1);
    each(col2);
    each(col3);  
    each(col4);
}

public static void each(Collection col) {   
    //遍历集合中的元素    
    for (Object ele : col) {       
        System.out.println(ele);    
    }    
    //没有指定泛型时,泛型的类型默认是 Object     
    //可以添加数据
    col.add(100);
}

使用泛型统配符:

public static void main(String[] args) {    
    Collection<Object> col1 = new ArrayList<>();    
    Collection<String> col2 = new ArrayList<>();   
    Collection<Number> col3 = new ArrayList<>();  
    Collection<Integer> col4 = new ArrayList<>();    
    col1.add(new Object());   
    col2.add("Hello");   
    col3.add(100);    
    col4.add(200);    
    //调用指定了泛型通配符的方法   
    each(col1);   
    each(col2);    
    each(col3);   
    each(col4);
}

public static void each(Collection<?> col) {    
    //遍历集合中的元素  
    for (Object ele : col) {        
        System.out.println(ele);    
    }    
    //由于无法确定泛型的类型,所以不能添加数据    
    //col.add(100);
}

//使用泛型通配符 `<?>` 时,表示集合可以包含任何类型的元素,但在方法内部不能确定具体的类型。
//因此,不能向集合中添加任何元素(除了 `null`),因为这可能会破坏集合的类型安全性。
//使用无界通配符 `<?>` 的主要目的是为了读取集合中的元素,而不是修改集合。如果需要向集合中
//添加元素,可以使用下界通配符 `<? super Type>` 来保证类型安全性。

泛型通配符之受限泛型,在前面的代码中,我们没有对泛型统配的最终类型进行限定,这意味着指定泛型的人可以设置任意的类型,但有时候我们希望泛型的类型能够被限制在某个范围。

1. 无界通配符 (<?>)

  • 用法:无界通配符表示未知的类型。
  • 场景:用于在方法、类或接口中表示可以接受任何类型的集合或对象。
  • 示例
public void printList(List<?> list) {
    for (Object obj : list) {
        System.out.println(obj);
    }
}

这里,printList方法可以接受任何类型的List,如List<Integer>List<String>等。

2. 有限制的通配符

(1) 上界通配符 (<? extends Type>)
  • 用法:上界通配符表示一个类型参数是某个类的子类型(包括该类本身)。
  • 场景:用于安全读取集合中的元素。
  • 示例
public void processNumbers(List<? extends Number> list) {
    for (Number num : list) {
        System.out.println(num.doubleValue());
    }
}

这里,processNumbers可以接受List<Integer>List<Double>等,因为IntegerDouble都是Number的子类。

(2) 下界通配符 (<? super Type>)
  • 用法:下界通配符表示一个类型参数是某个类的超类型(包括该类本身)。
  • 场景:用于安全地向集合中添加元素。
  • 示例
public void addIntegers(List<? super Integer> list) {
    list.add(1);
    list.add(2);
}

这里,addIntegers可以接受List<Integer>List<Number>List<Object>等,因为Integer是这些类型的子类。

3. 泛型通配符的作用

  • 灵活性:允许在类型参数未知或不重要的情况下使用泛型。
  • 类型安全:通过上界和下界通配符,提供了一种在不损失类型安全性的前提下处理不同类型的集合的机制。
  • 通用性:使得泛型代码可以更广泛地适用于不同类型,增强代码的可重用性。

五、数据结构

1. 数据结构概述

数据结构是计算机科学中用于组织和存储数据的方式,以便于高效地访问和修改。它们是算法的基础,影响程序的性能和效率。

数据结构的内容非常多,细学起来也是相对费功夫,不可能一蹴而就。现在,我们将常见的数据结构如栈、队列、数组、链表、树这几种数据结构的特点先介绍一下,了解不同数据结构的特点对我们学习集合会有帮助。

数据结构可以分为两大类:线性结构非线性结构。线性结构中的数据元素按照线性顺序排列,而非线性结构中的数据元素则以层级或网状形式组织。

2. 线性结构

线性结构中的元素按照线性顺序排列,每个元素最多有一个前驱和一个后继。线性结构的特点是简单易用,适合存储顺序数据。以下是几种常见的线性结构:

(1) 数组

  • 描述
    • 数组是最基本的线性数据结构,存储相同类型的元素,以连续的内存空间实现。元素通过索引访问。
    • 数组(Array)是有序的元素序列,数组结构在内存中要求必须是连续的内存空间,数组中的元素都存放在这块连续的内存中,每个元素都有一个索引编号,可根据索引操作数组中的元素。
    • 数组结构有如下特点:
      • 查找元素快(根据索引直接定位内存地址,查找速度非常快)
      • 增删元素慢(动态数组需要保证扩容以及元素数据的规整,所以在增删元素时会涉及元素内容的移动,效率偏低)
  • 优点:访问速度快(O(1)),因为可以直接通过索引访问元素。
  • 缺点:插入和删除操作效率低(O(n)),因为需要移动其他元素。
  • 示例动画.gif
int[] numbers = {1, 2, 3, 4, 5};
System.out.println(numbers[0]); // 访问数组的第一个元素

(2) 链表

  • 描述
    • 链表是一种由节点组成的线性结构,其中每个节点包含数据和一个指向下一个节点的引用。
    • 链表(LinkedList)结构是由一系列节点(Node)组成,链表结构中的每一个元素被包装在一个一个的节点中,节点于节点之间存在前后关联,就像一根链条,所以称之为链表结构。
    • 链表的节点包含两个部分,一个是数据域,用于存储数据,另一个是指针域,用于指向下一个节点的地址。链表有单向链表和双向链表,这里我们先介绍单向链表。
    • 链表结构有如下特点:
      • 查找元素慢(查找一个元素需要从链表的头部一个一个的找,所以速度慢)
      • 增删元素快(增删元素只需要修改节点的连接即可,速度非常快)
  • 优点:插入和删除操作效率高(O(1)),特别是在操作链表的头部或中间位置时。
  • 缺点:访问速度慢(O(n)),因为需要从头部开始遍历链表。
  • 示例动画.gif
class Node {
    int data;
    Node next;
}

(3) 栈

  • 描述
    • 栈是一种后进先出(LIFO)的数据结构,只能在一端(称为栈顶)进行插入和删除操作。
    • 栈结构(Stack),它是运算受限的线性表,其限制是仅允许在栈结构的一端对元素进行插入和删除操作,不允许在其他任何位置对元素进行操作。
    • 栈结构有如下特点:
      • 先进后出(先入栈的元素最后出栈,反之,最后入栈的元素最先出栈)
      • 入口和出口都是栈顶(栈结构只允许从栈顶对元素进行入栈和出栈操作)
    • 栈结构中有两个名词需要注意
      • 压栈:即入栈,也就是将元素存入栈结构中。
      • 弹栈:即出栈,也就是将元素从栈结构中取出。
  • 应用:函数调用、表达式求值、括号匹配等。
  • 示例动画.gif
Stack<Integer> stack = new Stack<>();
stack.push(1);
stack.pop();

(4) 队列

  • 描述
  • 队列是一种先进先出(FIFO)的数据结构,在队列的尾部插入元素,在头部删除元素。
  • 队列(Queue)也是一种运算受限的线性表,其限制是只允许在队列的一端插入元素,在队列的另一端删除元素。
  • 队列结构有如下特点:
    • 先进先出(最先入队的元素最先出队,反之,最后入队的元素最后出队)
    • 队列的入口和出口在队列结构的两端。
  • 应用:任务调度、缓冲区管理、广度优先搜索等。
  • 示例动画.gif
Queue<Integer> queue = new LinkedList<>();
queue.add(1);
queue.remove();

3. 树结构

(1) 树结构概述

树结构是一种非线性的数据结构,适用于表示具有层次关系的数据。

  • 树由节点组成,每个节点有一个父节点(除了根节点)和零个或多个子节点。
  • 没有父节点的节点称之为根节点,一棵树只能有一个根节点
  • 每个非根节点有且仅有一个父节点 image.png

树结构中的一些名词及解释

  • 节点:树结构中的每一个元素
  • 节点的度:节点拥有子节点的数量
  • 叶子节点:度为0的节点
  • 高度:叶子节点的高度为1,叶子节点的父节点高度为2,以此类推
  • 层:根节点在第1层,以此类推
  • 父节点:若一个节点有子节点,则该节点就是其子节点的父节点
  • 子节点:子节点是父节点的下一层节点
  • 兄弟节点:拥有共同父节点的节点互为兄弟节点

以下是常见的树结构:

(1) 二叉树

  • 描述:二叉树是每个节点最多有两个子节点(称为左子节点和右子节点)的树。
  • 应用:表达式树、决策树、二叉查找树(BST)等。
  • 示例

image.png

class TreeNode {
    int value;
    TreeNode left;
    TreeNode right;
}

(2) 二叉查找树(BST)

  • 描述
    • 二叉查找树是二叉树的一种,具有以下性质:
      • 左子树上所有节点的值均小于或等于当前节点的值
      • 右子树上所有节点的值均大于或等于当前节点的值
      • 每一个节点最多只有两个子节点
  • 优点:支持高效的查找、插入和删除操作(平均O(log n))。
  • 缺点:如果树不平衡,操作效率可能退化到O(n)。
  • 应用:动态集合的查找、插入和删除。
  • 二叉查找树可使用前序、中序、后续的方式进行元素遍历。(二叉查找树可能会出现 “瘸子现象”,严重影响查询效率) image.png

image.png

(3) 平衡二叉树

为了避免树出现“瘸子”现象,减少树的高度,提升查询效率,又出现了一种新的树,即平衡二叉树。
规则:每个节点的左右子树的高度差的绝对值不超过1,并且左右子树都是一棵平衡二叉树。

  • 描述:平衡树是一种保持平衡的二叉查找树,以确保在最坏情况下仍能提供高效操作。
  • 种类:AVL树、红黑树、B树等。
  • 应用:数据库索引、内存管理等。

image.png

平衡二叉树(旋转)

在构建一颗平衡二叉树的过程中,每当有新的节点插入或被删除时,需要检查插入或删除操作是否破坏了树的平衡,如果是,则需要做相应的旋转来维护平衡。

左旋:左旋就是将节点的右支往左拉,右子节点变成父节点,并把晋升之后多余的左子节点出让给降级节点的右子节点。

image.png

右旋:将节点的左支往右拉,左子节点变成了父节点,并把晋升之后多余的右子节点出让给降级节点的左子节点。

image.png

举个例子,下图中,左图在没插入前"19"节点前,该树还是平衡二叉树,但是 在插入"19"后,导致了"15"的左右子树失去了"平衡", 所以此时可以将"15"节点进行左旋,让"15"自身把节点出让给"17"作为"17"的左树,使得"17"节点左右 子树平衡,而"15"节点没有子树,左右也平衡了。

image.png

由于在构建平衡二叉树的时候,当有新节点插入时,都会判断插入后时是否平衡,这说明了插入新节点前,都是平衡的,也即高度差绝对值不会超过1。当新节点插入后, 有可能会有导致树不平衡,这时候就需要进行调整,而可能出现的情况就有4种,分别称作左左,左右,右左,右右。

  • 左左:只需要做一次右旋就变成了平衡二叉树。
  • 右右:只需要做一次左旋就变成了平衡二叉树。
  • 左右:先做一次分支的左旋,再做一次树的右旋,才能变成平衡二叉树。
  • 右左:先做一次分支的右旋,再做一次数的左旋,才能变成平衡二叉树。
a.左左:只需要做一次右旋就变成了平衡二叉树。

左左即为在原来平衡的二叉树上,在节点的左子树的左子树下,有新节点插入,导致节点的左右子树的高度差为2,如下即为"10"节点的左子树"7",的左子树"4",插入了节点"5"或"3"导致失衡。

image.png

左左调整其实比较简单,只需要对节点进行右旋即可,如下图,对节点"10"进行右旋

image.png

b.右右:只需要做一次左旋就变成了平衡二叉树。

右右即为在原来平衡的二叉树上,在节点的右子树的右子树下,有新节点插入,导致节点的左右子树的 高度差为2,如下即为"11"节点的右子树"13",的左子树"15",插入了节点 "14"或"19"导致失衡。

image.png

右右只需对节点进行一次左旋即可调整平衡,如下图,对"11"节点进行左旋。

image.png

c.左右:先做一次分支的左旋,再做一次树的右旋,才能变成平衡二叉树。 左右即为在原来平衡的二叉树上,在节点的左子树的右子树下,有新节点插入,导致节点的左右子树的 高度差为2,如上即为"11"节点的左子树"7",的右子树"9", 插入了节点"10"或"8"导致失衡。

image.png

左右这种情况,进行一次旋转是不能满足我们的条件的,正确的调整方式是,将左右进行第一次旋转, 将左右先调整成左左,然后再对左左进行调整,从而使得二叉树平衡。 即先对上图的节点"7"进行左旋,使得二叉树变成了左左,之后再对"11"节点进行右旋,此时二叉树就 调整完成,如下图:

image.png

d.右左:先做一次分支的右旋,再做一次数的左旋,才能变成平衡二叉树。 右左即为在原来平衡的二叉树上,在节点的右子树的左子树下,有新节点插入,导致节点的左右子树的 高度差为2,如上即为"11"节点的右子树"15",的左子树"13", 插入了节点"12"或"14"导致失衡。

image.png

右左这种情况,要先对节点"15"进行右旋,使得二叉树 变成右右,之后再对"11"节点进行左旋,此时二叉树就调整完成,如下图:

image.png

(4) 红黑树

红黑树是一种自平衡的二叉查找树,是计算机科学中用到的一种数据结构,它是在1972年由 Rudolf Bayer 发明的,当时被称之为平衡二叉B树,后来,在1978年被 Leoj Guibas 和 Robert Sedgewick 修改为如今的"红黑树"。它是一种特殊的二叉查找树,红黑树的每一个节点上都有存储位表示节点的颜色,可以是红或者黑;

红黑树不是高度平衡的,它的平衡是通过"红黑树的特性"进行实现的;

红黑树的特性:

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

下图就是一棵红黑树

image.png 在进行元素插入的时候,和之前一样; 每一次插入完毕以后,使用黑色规则进行校验,如果不满足红黑 规则,就需要通过变色,左旋和右旋来调整树,使其满足红黑规则。

树结构在表示层次化数据(如文件系统、组织结构)时非常有用,同时也是实现高效查找、插入和删除操作的基础。

六、List 集合

(一)List 集合概述

List 接口继承自 Collection 接口,是单列集合的一个重要分支。

List 集合的特点

  • 元素存取有序
  • 元素有索引
  • 元素可以重复

(二)List 接口常用方法

List 接口继承自 Collection 接口,因此具备 Collection 接口的所有功能。此外,List 接口还增加了一些基于索引操作集合元素的方法。以下是 List 接口的常用方法:

方法描述
add(E e)在列表的末尾添加指定的元素 e
add(int index, E element)在指定位置 index 插入指定的元素 element
set(int index, E element)替换指定位置 index 处的元素为 element
get(int index)获取指定位置 index 处的元素。
remove(int index)删除指定位置 index 处的元素,并返回该元素。
clear()删除列表中的所有元素。
size()返回列表中的元素个数。
isEmpty()判断列表是否为空。

(三)ArrayList 集合

ArrayListList 接口的一个实现类,也是最常用的集合类之一。ArrayList 的特点包括:

  • 增删慢、查询快:由于底层基于数组实现,插入和删除操作可能会较慢(需要移动数组中的元素),而查找操作非常快速。
  • 可变长度ArrayList 的长度是可变的,可以根据需要动态增加或减少。
  • 基于数组存储数据ArrayList 使用一个数组来存储元素,因此支持快速的随机访问。

示例代码

以下是 ArrayList 的使用案例:

package com.example.poker;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * @description 扑克牌发牌模拟案例
 */
public class PokerDemo {

    /**
     * 主方法,用于演示发牌过程
     */
    public static void main(String[] args) {
        // 创建扑克牌
        List<String> deck = createDeck();

        // 洗牌
        Collections.shuffle(deck);

        // 发牌
        List<String> playerA = new ArrayList<>();
        List<String> playerB = new ArrayList<>();
        List<String> playerC = new ArrayList<>();
        List<String> remainingCards = new ArrayList<>();
        dealCards(deck, playerA, playerB, playerC, remainingCards);

        // 打印结果
        printResults(playerA, playerB, playerC, remainingCards);
    }

    /**
     * 创建扑克牌集合
     * @return 扑克牌集合
     */
    private static List<String> createDeck() {
        List<String> deck = new ArrayList<>();
        
        // 花色
        List<String> suits = new ArrayList<>();
        Collections.addAll(suits, "♠", "♥", "♣", "♦");

        // 牌面值
        List<String> values = new ArrayList<>();
        Collections.addAll(values, "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A");

        // 组装牌
        for (String suit : suits) {
            for (String value : values) {
                deck.add(suit + value);
            }
        }

        // 添加大小王
        deck.add("Big Joker");
        deck.add("Small Joker");

        return deck;
    }

    /**
     * 发牌
     * @param deck 扑克牌集合
     * @param playerA 玩家A的牌
     * @param playerB 玩家B的牌
     * @param playerC 玩家C的牌
     * @param remainingCards 剩余的牌
     */
    private static void dealCards(List<String> deck, List<String> playerA, List<String> playerB, List<String> playerC, List<String> remainingCards) {
        for (int i = 0; i < deck.size(); i++) {
            if (i >= 48) { // 剩余牌
                remainingCards.add(deck.get(i));
            } else if (i % 3 == 0) {
                playerA.add(deck.get(i));
            } else if (i % 3 == 1) {
                playerB.add(deck.get(i));
            } else if (i % 3 == 2) {
                playerC.add(deck.get(i));
            }
        }
    }

    /**
     * 打印结果
     * @param playerA 玩家A的牌
     * @param playerB 玩家B的牌
     * @param playerC 玩家C的牌
     * @param remainingCards 剩余的牌
     */
    private static void printResults(List<String> playerA, List<String> playerB, List<String> playerC, List<String> remainingCards) {
        System.out.println("玩家A " + playerA);
        System.out.println("玩家B " + playerB);
        System.out.println("玩家C " + playerC);
        System.out.println("剩余牌 " + remainingCards);
    }
}

使用场景

ArrayList 适用于以下场景:

  • 当需要频繁进行读操作,而对元素的插入和删除要求不高时。
  • 当需要一个动态增长的数组,能够灵活地添加和删除元素时。
  • 需要进行高效的随机访问操作时。

(四)LinkedList 集合

LinkedListList 接口的另一种实现,它的特点和使用场景与 ArrayList 略有不同。LinkedList 采用链表数据结构来存储元素。以下是 LinkedList 的特点及其常用方法的介绍:

LinkedList 的特点

  • 增删快、查询慢:由于底层是链表实现,LinkedList 在插入和删除元素时比 ArrayList 更快,因为不需要移动数组中的元素。然而,查询操作较慢,因为需要从头节点遍历链表。
  • 双向链表LinkedList 实际上是一个双向链表,每个节点包含对前一个节点和下一个节点的引用。这使得它在两端都能进行快速的插入和删除操作。
  • 可以作为队列或栈使用LinkedList 实现了 Deque 接口,因此可以用作双端队列(队列和栈)。

常用方法

LinkedList 除了继承自 List 接口的方法,还提供了一些额外的操作方法。以下是 LinkedList 的常用方法:

方法描述
addFirst(E e)在链表的开头插入指定的元素 e
addLast(E e)在链表的末尾插入指定的元素 e
removeFirst()删除并返回链表的第一个元素。
removeLast()删除并返回链表的最后一个元素。
getFirst()获取链表的第一个元素。
getLast()获取链表的最后一个元素。
peekFirst()查看链表的第一个元素,但不删除。
peekLast()查看链表的最后一个元素,但不删除。
offerFirst(E e)将指定的元素 e 插入到链表的开头。
offerLast(E e)将指定的元素 e 插入到链表的末尾。
pollFirst()删除并返回链表的第一个元素,如果链表为空,则返回 null
pollLast()删除并返回链表的最后一个元素,如果链表为空,则返回 null

示例代码

以下是 LinkedList 的基本操作示例:

import java.util.LinkedList;
import java.util.List;

public class LinkedListDemo {
    public static void main(String[] args) {
        LinkedList<Integer> list = new LinkedList<>();

        // 添加元素
        list.addFirst(100);  // 在链表的开头添加 100
        list.addLast(200);   // 在链表的末尾添加 200
        list.addLast(300);   // 在链表的末尾添加 300
        System.out.println(list); // 输出: [100, 200, 300]

        // 修改元素
        list.set(1, 400);    // 修改索引 1 处的元素为 400
        System.out.println(list); // 输出: [100, 400, 300]

        // 查询元素
        System.out.println(list.getFirst()); // 输出: 100
        System.out.println(list.getLast());  // 输出: 300

        // 删除元素
        Integer removedFirst = list.removeFirst(); // 删除并返回链表的第一个元素
        System.out.println(removedFirst); // 输出: 100
        System.out.println(list); // 输出: [400, 300]

        Integer removedLast = list.removeLast(); // 删除并返回链表的最后一个元素
        System.out.println(removedLast); // 输出: 300
        System.out.println(list); // 输出: [400]
    }
}

使用场景

LinkedList 适用于以下场景:

  • 当需要频繁进行插入和删除操作,特别是在链表的开头或末尾时。
  • 当需要支持双端队列(即同时在队列两端进行操作)时。
  • 对于大量的插入和删除操作,性能需求高时,LinkedListArrayList 更合适。

模拟案例

package com.example.taskqueue;

import java.util.LinkedList;

/**
 * @description 使用 LinkedList 实现任务队列的示例
 */
public class TaskQueueDemo {

    public static void main(String[] args) {
        // 创建任务队列
        LinkedList<String> taskQueue = new LinkedList<>();

        // 添加任务
        addTask(taskQueue, "任务1: 完成报告");
        addTask(taskQueue, "任务2: 参加会议");
        addTask(taskQueue, "任务3: 发送邮件");

        // 查看任务
        viewTasks(taskQueue);

        // 执行并移除任务
        executeTask(taskQueue);
        executeTask(taskQueue);

        // 查看剩余任务
        viewTasks(taskQueue);
    }

    /**
     * 添加任务到队列
     * @param taskQueue 任务队列
     * @param task 任务内容
     */
    private static void addTask(LinkedList<String> taskQueue, String task) {
        taskQueue.addLast(task);
        System.out.println("添加任务: " + task);
    }

    /**
     * 查看所有任务
     * @param taskQueue 任务队列
     */
    private static void viewTasks(LinkedList<String> taskQueue) {
        System.out.println("当前任务队列:");
        for (String task : taskQueue) {
            System.out.println(task);
        }
        System.out.println();
    }

    /**
     * 执行并移除队列中的第一个任务
     * @param taskQueue 任务队列
     */
    private static void executeTask(LinkedList<String> taskQueue) {
        if (!taskQueue.isEmpty()) {
            String task = taskQueue.removeFirst();
            System.out.println("执行任务: " + task);
        } else {
            System.out.println("任务队列为空,没有任务可执行。");
        }
    }
}

七、Collections 工具类

(一)Collections 常用方法

1. shuffle(List<?> list)

shuffle 方法用于打乱集合中元素的顺序。示例代码如下:

public class Demo {

    public static void main(String[] args) {
        // 创建集合
        List<Integer> list = new ArrayList<>();
        // 添加元素
        Collections.addAll(list, 5, 3, 8, 9, 1, 6, 7, 2, 4, 0);

        System.out.println("打乱前:" + list);
        // 打乱顺序
        Collections.shuffle(list);
        System.out.println("打乱后:" + list);
    }
}

2. sort(List<T> list)

sort 方法用于对集合中的元素进行排序。排序规则如下:

  • 数值类型元素:按升序排列。
  • 字符串类型元素:按字典顺序(字符编码值)排序。
  • 自定义类型元素:根据 Comparable 接口的 compareTo 方法进行排序。

示例代码如下:

public class Demo {

    public static void main(String[] args) {
        // 数值类型集合排序
        List<Integer> numberList = new ArrayList<>();
        Collections.addAll(numberList, 5, 3, 8, 9, 1, 6, 7, 2, 4, 0);

        System.out.println("排序前:" + numberList);
        // 排序
        Collections.sort(numberList);
        System.out.println("排序后:" + numberList);

        // 字符串类型集合排序
        List<String> stringList = new ArrayList<>();
        Collections.addAll(stringList, "李四", "张三", "王五");

        System.out.println("排序前:" + stringList);
        // 排序
        Collections.sort(stringList);
        System.out.println("排序后:" + stringList);
    }
}

3. 自定义类型排序

自定义类型需要实现 Comparable 接口,重写 compareTo 方法来定义排序规则。示例如下:

public class Demo2 {

    public static void main(String[] args) {
        // 创建集合
        List<Student> studentList = new ArrayList<>();

        // 创建学生对象
        Student stu1 = new Student("张三", 18);
        Student stu2 = new Student("李四", 20);
        Student stu3 = new Student("王五", 19);
        Student stu4 = new Student("赵六", 18);

        // 添加元素到集合中
        studentList.add(stu1);
        studentList.add(stu2);
        studentList.add(stu3);
        studentList.add(stu4);

        System.out.println("排序前:" + studentList);
        // 排序
        Collections.sort(studentList);
        System.out.println("排序后:" + studentList);
    }
}

class Student implements Comparable<Student> {

    private String name;
    private int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + ''' +
                ", age=" + age +
                '}';
    }

    @Override
    public int compareTo(Student o) {
        // 先比较年龄
        int result = Integer.compare(this.age, o.age);
        // 如果年龄相同,则按姓名排序
        if (result == 0) {
            result = this.name.compareTo(o.name);
        }
        return result;
    }
}

(二)Comparator 比较器

Comparator 接口允许你定义自定义的排序规则,用于对集合中元素进行排序。sort 方法的重载版本可以接受一个 Comparator 对象来实现自定义排序。示例如下:

public class Demo {

    public static void main(String[] args) {
        // 创建集合
        List<Student> studentList = new ArrayList<>();

        // 创建学生对象
        Student stu1 = new Student("张三", 18);
        Student stu2 = new Student("李四", 20);
        Student stu3 = new Student("王五", 19);
        Student stu4 = new Student("赵六", 18);

        // 添加元素到集合中
        studentList.add(stu1);
        studentList.add(stu2);
        studentList.add(stu3);
        studentList.add(stu4);

        System.out.println("排序前:" + studentList);
        // 使用 Comparator 对象进行排序
        Collections.sort(studentList, new Comparator<Student>() {
            @Override
            public int compare(Student o1, Student o2) {
                return Integer.compare(o1.getAge(), o2.getAge());
            }
        });
        System.out.println("排序后:" + studentList);
    }
}

class Student {

    private String name;
    private int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + ''' +
                ", age=" + age +
                '}';
    }
}

八、可变参数

(一)可变参数

JDK1.5 之后,Java 提供了一个新的特性,允许我们在方法接收多个参数且参数的类型一致时对其进行简化,我们将这种特性称为可变参数。

语法格式如下:

权限修饰符 返回值类型 方法名(数据类型... 参数名) {    
    //
}
public class Demo01 {

    /**
     * 可变参数
     */
    public static void main(String[] args) {
        //传递多个参数
//        int sum = getSum(10, 20, 30, 40, 50);

        //传递一个数组
        int[] arr = {11, 22, 33, 44, 55};
        int sum = getSum(arr);

        System.out.println(sum);
    }

    public static int getSum(int... arr) { //这个参数本质是一个数组,在这个方法中就可以将参数当作数组来使用
        int sum = 0;
        for (int i = 0; i < arr.length; i++) {
            sum += arr[i];
        }

        return sum;
    }

}

可变参数的好处是它既能接受数组参数,接受多个相同数据类型的参数,更加的灵活。

(二)注意事项

  • 一个方法只能有一个可变参数
  • 如果方法有多个参数,可变参数必须定义在参数列表的最后
public class Demo02 {

    /**
     * 可变参数注意事项
     *      1、一个方法只能有一个可变参数
     *      2、如果方法有多个参数,可变参数必须在形参的末尾
     */
    public static void main(String[] args) {
        test(10, 20, 30, 40, 50);
    }

    public static void test(int n1, int n2, int... arr) {
        System.out.println(n1);
        System.out.println(n2);

        System.out.println(Arrays.toString(arr));
    }

    //错误的,可变参数必须写在末尾
//    public static void test(int... arr, double n1) {
//
//    }

    //错误的,一个方法只能有一个可变参数
//    public static void test(int... arr1, double... arr2) {
//
//    }
}

(三)可变参数在 Collections 中的应用

Collections 类提供了一个便利的方法 addAll,用于将多个元素添加到集合中。这个方法使用了可变参数,使得向集合中添加多个元素变得简单直观。

1. 方法签名

public static <T> boolean addAll(Collection<? super T> c, T... elements)
  • 参数说明:

    • c:目标集合,元素类型是 T 的超类。
    • elements:要添加的元素,类型为 T
  • 返回值: 返回 true,如果集合 c 在添加元素后发生了改变。

2. 示例

下面的示例展示了如何使用 addAll 方法将多个元素添加到集合中:

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

public class AddAllDemo {

    public static void main(String[] args) {
        // 创建一个空的集合
        List<String> list = new ArrayList<>();
        
        // 使用 addAll 方法添加多个元素
        Collections.addAll(list, "Alice", "Bob", "Charlie", "Diana");

        System.out.println("集合内容:" + list);
    }
}

这样,你可以利用 Collections.addAll 方法简化向集合中添加多个元素的操作,提高代码的可读性和维护性。

九、Set 集合

(一)Set 集合概述

Set 是一个接口,继承自 Collection 接口,是单列集合的一个分支。

Set 集合的特点

  • 元素无索引
  • 元素不可重复

Set 集合的实现类

  • HashSet (底层结构是哈希表,存取无序)
  • LinkedHashSet (底层结构是链表 + 哈希表,存取有序)
  • TreeSet(底层结构是红黑树,存取无序,支持对元素进行排序)

(二)HashSet

HashSet 是 Set 接口的实现类,它的底层由哈希表实现,特点是无索引、元素不可重复、存取无序。

public static void main(String[] args) {    
    //创建 HashSet 集合    
    HashSet<String> set = new HashSet<>();    
    set.add("cba");    
    set.add("bac");    
    set.add("cab");    
    set.add("bca");    
    set.add("abc");    
    //打印集合中的元素    
    System.out.println(set); //[bca, cba, abc, bac, cab]
}

HashSet 集合是如何保证元素唯一的呢?

使用 equals 判断是一个不错的方法,但每添加一个元素到集合中,都要调用 equals 和集合中所有的元素进行比较,效率太低。

Java 的解决方案是先判断待添加元素的 hashCode 在集合中是否已经存在,如果不存在,则说明当前添加的不是重复元素。但 hashCode 并非完全可靠,两个不同的对象,hashCode 却可能是相同的,例如字符串 Aa 和 BB 的 hashCode 就是相同的。所以,在 hashCode 相同的情况下,还需要再调用 equals 方法进一步确认,避免出现误判。

public class Demo {

    /**
     * hashCode()   //获取对象的哈希码,哈希码是一个int类型的值
     */
    public static void main(String[] args) {

        Student stu = new Student("张三", 18);
        System.out.println(stu.hashCode());

        System.out.println("aaa".hashCode());
        System.out.println("bbb".hashCode());
        System.out.println("ccc".hashCode());

        System.out.println("a".hashCode());

        //如果只靠hashcode来判断集合元素是否重复,那么就会出现问题
        //"Aa"
        //"BB"
        System.out.println("Aa".hashCode()); //2112
        System.out.println("BB".hashCode()); //2112
    }
}


class Student {

    //成员变量
    private String name;
    private int age;

    //构造方法
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    //getter & setter
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

注意:添加到 HashSet 集合中的元素类型,必须要重写 hashCode 和 equals 方法!

public class Demo {

    public static void main(String[] args) {
        //创建集合
        HashSet<Student> set = new HashSet<>();
        //创建学生对象
        Student stu1 = new Student("张三", 18);
        Student stu2 = new Student("李四", 19);
        Student stu3 = new Student("李四", 19);

        System.out.println(stu1.hashCode());
        System.out.println(stu2.hashCode());
        System.out.println(stu3.hashCode());

        //添加元素到集合中
        set.add(stu1);
        set.add(stu2);

        //HashSet 会先判断集合中是否有 hashCode 和 stu3 相同的元素
        //如果 hashcode 重复,会进一步调用 equals 来进行判断,确保元素绝对唯一
        set.add(stu3); //可以添加成功吗?

        System.out.println(set);
    }
}

class Student {

    private String name;
    private int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    /**
     * 重写之后的 equals 比的不再是地址值,而是对象的姓名和年龄
     * @param o
     * @return
     */
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return age == student.age && Objects.equals(name, student.name);
    }

    /**
     * 重写之后的 hashcode 是有姓名和年龄决定的
     */
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }


}

(三)LinkedHashSet

HashSet 集合是存取无序的,如果我们希望存取有序,可以使用 HashSet 的子类 LinkedHashSet 集合,它的底层结构是链表+哈希表。

public static void main(String[] args) {    
    //创建 LinkedHashSet 集合    
    LinkedHashSet<String> set = new LinkedHashSet<>();    
    set.add("cba");    
    set.add("bac");    
    set.add("cab");    
    set.add("bca");    
    set.add("abc");    
    //打印集合中的元素    
    System.out.println(set); //[cba, bac, cab, bca, abc]
}

(四)TreeSet

TreeSet 集合是 Set 接口的一个实现类,底层是基于红黑树的实现。它的特点是没有索引、元素不可重复、存取无序,支持对集合中的元素进行排序。

构造方法

public TreeSet()
public TreeSet(Comparator<? super E> comparator)

注意:由于 TreeSet 需要对集合中的元素进行排序,所以元素类型必须实现 Comparable 接口并重写 compareTo 方法,或者指定自定义的比较器。

public class Demo {

    public static void main(String[] args) {
        //数值类型,默认按数字大小升序排列
        TreeSet<Integer> set1 = new TreeSet<>();
        Collections.addAll(set1, 5, 2, 1, 4, 3);
        System.out.println(set1);

        //字符串类型,默认按字典排序
        TreeSet<String> set2 = new TreeSet<>();
        Collections.addAll(set2, "ccc", "aaa", "bbb");
        System.out.println(set2);

        //自定义类型(实现了 Comparable 接口)
        TreeSet<Student> set3 = new TreeSet<>();
        //创建学生对象
        Student stu1 = new Student("张三", 19);
        Student stu2 = new Student("李四", 18);
        Student stu3 = new Student("王五", 20);
        set3.add(stu1);
        set3.add(stu2);
        set3.add(stu3);
        System.out.println(set3);

        //自定义类型(不实现 Comparable 接口,使用比较器)
        TreeSet<Teacher> set4 = new TreeSet<>(new Comparator<Teacher>() {

            /**
             * 前:o1
             * 后:o2
             *
             * 前减后:升序
             * 后减前:降序
             */
            @Override
            public int compare(Teacher o1, Teacher o2) {
                return o2.getAge() - o1.getAge();
            }
        });
        //创建学生对象
        Teacher tea1 = new Teacher("张三", 19);
        Teacher tea2 = new Teacher("李四", 18);
        Teacher tea3 = new Teacher("王五", 20);
        set4.add(tea1);
        set4.add(tea2);
        set4.add(tea3);
        System.out.println(set4);
    }
}

class Teacher {

    private String name;
    private int age;

    public Teacher(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}


class Student implements Comparable<Student> {

    private String name;
    private int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    /**
     * 前:this
     * 后:o
     *
     * 前减后:升序
     * 后减前:降序
     */
    @Override
    public int compareTo(Student o) {
        return this.age - o.age;
    }
}

(五)单列集合总结

Collection

  • List:有索引,存取有序,元素可以重复
    • ArrayList:基于数组,特点是查询快、增删慢
    • LinkedList:基于链表,特点是查询慢、增删快
  • Set:无索引,元素不可重复
    • HashSet:基于哈希表,存取无序
    • LinkedHashSet:基于哈希表+链表,存取有序
    • TreeSet:基于红黑树,可以对元素进行排序
  • 存储多个元素,有重复元素,查询较多:ArrayList
  • 存储多个元素,有重复元素,增删较多:LinkedList
  • 存储多个元素,元素要求唯一(去重):HashSet
  • 存储多个元素,要求元素进行排序:TreeSet

十、Map 集合

(一)Map 集合概述

Map 接口中定义了双列集合的共有方法,Map 集合是一种双列集合,元素以键值对的形式存储在集合中。

单列集合与双列集合的区别

  • 单列集合:集合中的每一个元素只有一个值,或者是一个对象。
  • 双列集合:集合中的每一个元素是以 键值对 的形式存储的,即元素有键和值两个部分。

Map 集合的特点

  • Map 集合存储的元素是键值对
  • Map 集合的键是唯一的,不可重复,但值可以重复
  • 根据键取值

Map 接口的常用实现类

  • HashMap (底层采用数组+链表+红黑树结构,存取无序)
  • LinkedHashMap (在 HashMap 的基础上增加了链表结构来维护存取顺序,存取有序)
  • TreeMap (底层采用红黑树结构,存取无序,支持对集合元素进行排序)

Map 接口是 Java 集合框架中的一个核心接口,提供了一些用于操作键值对的方法。下面是 Map 接口常用方法的表格:

方法描述
void clear()清空 Map 中的所有键值对。
boolean containsKey(Object key)检查 Map 中是否包含指定的键。
boolean containsValue(Object value)检查 Map 中是否包含指定的值。
Set<Map.Entry<K,V>> entrySet()返回 Map 中键值对的 Set 视图。
V get(Object key)根据指定的键获取对应的值,如果键不存在,则返回 null
boolean isEmpty()检查 Map 是否为空,即是否没有任何键值对。
Set<K> keySet()返回 Map 中键的 Set 视图。
V put(K key, V value)将指定的键值对插入 Map 中。如果键已经存在,则更新值并返回旧值。
void putAll(Map<? extends K, ? extends V> m)将指定 Map 中的所有键值对复制到当前 Map 中。
V remove(Object key)根据指定的键移除对应的键值对,并返回被移除的值。如果键不存在,则返回 null
int size()返回 Map 中键值对的数量。
Collection<V> values()返回 Map 中值的 Collection 视图。

下面是一个 Map 接口方法的综合案例,以及两种常见的 Map 集合遍历方式的示例。

代码案例

import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.Collection;

public class MapExample {
    public static void main(String[] args) {
        // 创建一个 HashMap 实例
        Map<String, Integer> map = new HashMap<>();
        
        // 添加键值对
        map.put("Alice", 30);
        map.put("Bob", 25);
        map.put("Charlie", 35);
        
        // 打印 Map 的大小
        System.out.println("Map size: " + map.size());
        
        // 检查 Map 是否为空
        System.out.println("Is map empty? " + map.isEmpty());
        
        // 检查是否包含某个键
        System.out.println("Contains key 'Alice'? " + map.containsKey("Alice"));
        
        // 检查是否包含某个值
        System.out.println("Contains value 25? " + map.containsValue(25));
        
        // 获取某个键对应的值
        System.out.println("Value for 'Bob': " + map.get("Bob"));
        
        // 遍历 Map 的键值对
        System.out.println("Entries in map:");
        for (Map.Entry<String, Integer> entry : map.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
        
        // 遍历 Map 的键
        System.out.println("Keys in map:");
        for (String key : map.keySet()) {
            System.out.println(key);
        }
        
        // 遍历 Map 的值
        System.out.println("Values in map:");
        for (Integer value : map.values()) {
            System.out.println(value);
        }
        
        // 移除某个键值对
        map.remove("Charlie");
        System.out.println("Map after removing 'Charlie': " + map);
        
        // 清空 Map
        map.clear();
        System.out.println("Map after clearing: " + map);
    }
}

两种遍历方式

  1. 使用 entrySet() 遍历键值对

    // 遍历 Map 的键值对
    System.out.println("Entries in map:");
    for (Map.Entry<String, Integer> entry : map.entrySet()) {
        System.out.println(entry.getKey() + ": " + entry.getValue());
    }
    

    说明entrySet() 返回一个包含 Map.Entry 对象的 Set 视图,每个 Map.Entry 对象表示一个键值对。通过遍历这个 Set,可以访问每个键和值。

  2. 使用 keySet() 遍历键,再通过键获取值

    // 遍历 Map 的键
    System.out.println("Keys in map:");
    for (String key : map.keySet()) {
        System.out.println(key + ": " + map.get(key));
    }
    

    说明keySet() 返回一个包含 Map 中所有键的 Set 视图。遍历这个 Set 可以访问每个键,然后通过 get() 方法获取对应的值。

这两种遍历方式都有其适用场景:entrySet() 适合在遍历过程中同时访问键和值,而 keySet() 更适合只关心键的场景。

(二)HashMap

1. HashMap 集合存储自定义类型

Map 集合的 key 不允许重复,依据的是 hashCode 和 equals 方法的判断结果。所以当 Map 的 key 是自定义类型时,必须重写 hashCode 和 equals 方法。

public class Demo03 {

    public static void main(String[] args) {
        //创建Map集合
        Map<Student, String> map = new HashMap<>();
        //创建学生对象
        Student stu1 = new Student("张三", 18);
        Student stu2 = new Student("李四", 20);
        Student stu3 = new Student("王五", 19);
        Student stu4 = new Student("张三", 18);

        //添加元素
        map.put(stu1, "北京");
        map.put(stu2, "上海");
        map.put(stu3, "广州");

        map.put(stu4, "深圳");
        System.out.println(map);
    }
}

class Student {

    private String name;
    private int age;

    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return age == student.age && Objects.equals(name, student.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

2. HashMap 的源码分析

在 JDK 1.8 之前,HashMap 的底层结构采用的是数组+链表,元素没有冲突时,直接存储在数组中,当出现哈希冲突时,使用链表存储。

当出现哈希冲突的元素较多时,链表的长度会比较长,这会降低元素查找的速度。所以,在 JDK1.8 时,底层结构改成了数组+链表+红黑树的方式进行存储。元素不冲突时直接存储在数组,出现冲突时存储在链表,当链表的长度大于8时直接转换成红黑树进行存储。 image.png

(三)LinkedHashMap

HashMap 无法保证元素的存取顺序,如果需要存取有序,则可以使用 LinkedHashMap 集合。

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

public class MapOrderExample {
    public static void main(String[] args) {
        // 创建 HashMap 和 LinkedHashMap 实例
        Map<String, Integer> hashMap = new HashMap<>();
        Map<String, Integer> linkedHashMap = new LinkedHashMap<>();
        
        // 向 HashMap 中添加元素
        hashMap.put("Alice", 30);
        hashMap.put("Bob", 25);
        hashMap.put("Charlie", 35);
        
        // 向 LinkedHashMap 中添加元素
        linkedHashMap.put("Alice", 30);
        linkedHashMap.put("Bob", 25);
        linkedHashMap.put("Charlie", 35);
        
        // 打印 HashMap 中的元素
        System.out.println("HashMap 中的元素:");
        for (Map.Entry<String, Integer> entry : hashMap.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
        
        // 打印 LinkedHashMap 中的元素
        System.out.println("\nLinkedHashMap 中的元素:");
        for (Map.Entry<String, Integer> entry : linkedHashMap.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
        
        // 修改 LinkedHashMap 中的元素顺序
        linkedHashMap.put("David", 40); // 插入新的元素
        
        System.out.println("\n添加 'David' 后的 LinkedHashMap 元素:");
        for (Map.Entry<String, Integer> entry : linkedHashMap.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
        
        // 修改 HashMap 中的元素顺序
        hashMap.put("David", 40); // 插入新的元素
        
        System.out.println("\n添加 'David' 后的 HashMap 元素:");
        for (Map.Entry<String, Integer> entry : hashMap.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }
}

image.png

(四)TreeMap

TreeMap 集合的底层是基于红黑树结构,可以对元素的键进行排序。排序的方式有两种,自然排序和比较器排序。使用何种排序方式,取决于我们的业务需求。

构造方法

构造方法描述
TreeMap()创建一个新的、空的 TreeMap,按照键的自然顺序进行排序。
TreeMap(Comparator<? super K> comparator)创建一个新的、空的 TreeMap,按照指定的比较器进行排序。
TreeMap(Map<? extends K, ? extends V> m)创建一个新的 TreeMap,并将指定 Map 中的所有映射关系存储在此 TreeMap 中。
TreeMap(SortedMap<K, ? extends V> m)创建一个新的 TreeMap,并将指定的有序映射关系存储在此 TreeMap 中,排序方式与给定 SortedMap 相同。

特有方法

方法名描述
Map.Entry<K,V> ceilingEntry(K key)返回与大于等于给定键的最小键关联的键值对。如果不存在这样的键,则返回 null
K ceilingKey(K key)返回大于等于给定键的最小键。如果不存在这样的键,则返回 null
Map.Entry<K,V> floorEntry(K key)返回与小于等于给定键的最大键关联的键值对。如果不存在这样的键,则返回 null
K floorKey(K key)返回小于等于给定键的最大键。如果不存在这样的键,则返回 null
Map.Entry<K,V> higherEntry(K key)返回与严格大于给定键的最小键关联的键值对。如果不存在这样的键,则返回 null
K higherKey(K key)返回严格大于给定键的最小键。如果不存在这样的键,则返回 null
Map.Entry<K,V> lowerEntry(K key)返回与严格小于给定键的最大键关联的键值对。如果不存在这样的键,则返回 null
K lowerKey(K key)返回严格小于给定键的最大键。如果不存在这样的键,则返回 null
Map.Entry<K,V> firstEntry()返回与 TreeMap 中最小键关联的键值对。如果 TreeMap 为空,则返回 null
Map.Entry<K,V> lastEntry()返回与 TreeMap 中最大键关联的键值对。如果 TreeMap 为空,则返回 null
Map.Entry<K,V> pollFirstEntry()移除并返回与 TreeMap 中最小键关联的键值对。如果 TreeMap 为空,则返回 null
Map.Entry<K,V> pollLastEntry()移除并返回与 TreeMap 中最大键关联的键值对。如果 TreeMap 为空,则返回 null
NavigableSet<K> navigableKeySet()返回 TreeMap 中的键的集合,按升序排序。
NavigableMap<K,V> descendingMap()返回此映射中包含的键值对的逆序视图。
NavigableSet<K> descendingKeySet()返回键的降序 NavigableSet 视图。
SortedMap<K,V> headMap(K toKey, boolean inclusive)返回此映射的部分视图,其键严格小于(或等于,如果 inclusivetruetoKey
SortedMap<K,V> tailMap(K fromKey, boolean inclusive)返回此映射的部分视图,其键严格大于(或等于,如果 inclusivetruefromKey
SortedMap<K,V> subMap(K fromKey, boolean fromInclusive, K toKey, boolean toInclusive)返回此映射的部分视图,其键范围在 fromKeytoKey 之间。

下面是一个 TreeMap 的综合案例,展示了如何使用 TreeMap 的构造方法和特有方法。

import java.util.Comparator;
import java.util.Map;
import java.util.TreeMap;

public class TreeMapExample {
    public static void main(String[] args) {
        // 创建一个 TreeMap,使用自然顺序排序
        TreeMap<String, Integer> naturalOrderMap = new TreeMap<>();
        naturalOrderMap.put("Alice", 30);
        naturalOrderMap.put("Bob", 25);
        naturalOrderMap.put("Charlie", 35);
        
        System.out.println("自然顺序的 TreeMap:");
        for (Map.Entry<String, Integer> entry : naturalOrderMap.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }

        // 创建一个 TreeMap,使用自定义比较器进行排序(按键长度)
        TreeMap<String, Integer> lengthOrderMap = new TreeMap<>(Comparator.comparingInt(String::length));
        lengthOrderMap.put("Alice", 30);
        lengthOrderMap.put("Bob", 25);
        lengthOrderMap.put("Charlie", 35);

        System.out.println("\n按键长度排序的 TreeMap:");
        for (Map.Entry<String, Integer> entry : lengthOrderMap.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }

        // 使用特有方法
        System.out.println("\n特有方法示例:");
        
        // 获取最小键的键值对
        Map.Entry<String, Integer> firstEntry = naturalOrderMap.firstEntry();
        System.out.println("最小键的键值对: " + firstEntry);

        // 获取最大键的键值对
        Map.Entry<String, Integer> lastEntry = naturalOrderMap.lastEntry();
        System.out.println("最大键的键值对: " + lastEntry);

        // 获取小于或等于指定键的最大键
        String floorKey = naturalOrderMap.floorKey("Bob");
        System.out.println("小于或等于 'Bob' 的最大键: " + floorKey);

        // 获取大于指定键的最小键
        String higherKey = naturalOrderMap.higherKey("Alice");
        System.out.println("大于 'Alice' 的最小键: " + higherKey);

        // 获取键的降序集合
        System.out.println("\n降序键集合:");
        for (String key : naturalOrderMap.descendingKeySet()) {
            System.out.println(key);
        }
    }
}

(五)Map 集合案例

需求:统计一个字符串中每个字符出现的次数。

分析:

  • 获取一个字符串(键盘录入)。

  • 创建一个 Map 集合,键存储字符,值存储出现的次数。

  • 遍历字符串中的每一个字符。

  • 判断字符是否存在于 Map 集合的键中:

    • 如果没有,说明该字符第一次出现,次数为 1;
    • 如果有,说明该字符之前已经出现过,次数 +1。
  • 打印结果。

import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;

public class CharacterCount {
    public static void main(String[] args) {
        // 获取用户输入的字符串
        Scanner scanner = new Scanner(System.in);
        System.out.print("请输入一个字符串: ");
        String input = scanner.nextLine();
        
        // 创建一个 Map 集合用于存储字符和出现次数
        Map<Character, Integer> charCountMap = new HashMap<>();
        
        // 遍历字符串中的每一个字符
        for (char ch : input.toCharArray()) {
            // 判断字符是否已经在 Map 中
            if (charCountMap.containsKey(ch)) {
                // 如果存在,次数+1
                charCountMap.put(ch, charCountMap.get(ch) + 1);
            } else {
                // 如果不存在,说明第一次出现,次数为1
                charCountMap.put(ch, 1);
            }
        }
        
        // 打印结果
        System.out.println("字符出现的次数:");
        for (Map.Entry<Character, Integer> entry : charCountMap.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }
}

十一、集合嵌套

嵌套集合是 Java 集合框架的一种灵活应用,它允许在集合中包含其他集合,从而实现复杂的数据结构。本文将对嵌套集合进行概述,介绍其使用场景,并提供几个常见的嵌套集合示例代码,包括 List 嵌套 ListList 嵌套 MapMap 嵌套 MapMap 嵌套 List

(一)概述

在 Java 中,集合是用于存储多个元素的对象,主要分为 ListSetMap 三大类。嵌套集合是指在一个集合中包含另一个集合的情况,形成多层次的数据结构。它可以用于表示复杂的关系,比如二维数组、键值对中的列表等。

(二)使用场景

嵌套集合在以下场景中尤为有用:

  1. 多维数据表示:例如,一个 List 嵌套 List 可以表示二维矩阵。
  2. 复杂数据映射Map 嵌套 Map 可以用于描述一个多级分类。
  3. 结构化数据存储:如将用户信息与其订单历史关联,可以使用 Map 嵌套 List
  4. 动态关系管理:如社交网络中的用户及其朋友列表可以使用 List 嵌套 Map

(三)场景代码案例

1. List 嵌套 List

List 嵌套 List 常用于表示二维数据,例如棋盘、表格等。

import java.util.ArrayList;
import java.util.List;

public class ListInListExample {
    public static void main(String[] args) {
        // 创建一个嵌套的 List,用于表示二维矩阵
        List<List<Integer>> matrix = new ArrayList<>();

        // 初始化矩阵行
        for (int i = 0; i < 3; i++) {
            List<Integer> row = new ArrayList<>();
            for (int j = 0; j < 3; j++) {
                row.add(i * j);
            }
            matrix.add(row);
        }

        // 打印矩阵
        for (List<Integer> row : matrix) {
            System.out.println(row);
        }
    }
}

2. List 嵌套 Map

List 嵌套 Map 常用于将多个映射关系组织在一起,例如学生及其成绩列表。

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class ListInMapExample {
    public static void main(String[] args) {
        // 创建一个 List 存储多个学生的成绩记录
        List<Map<String, Integer>> studentScores = new ArrayList<>();

        // 添加第一个学生的成绩
        Map<String, Integer> aliceScores = new HashMap<>();
        aliceScores.put("Math", 85);
        aliceScores.put("English", 90);
        studentScores.add(aliceScores);

        // 添加第二个学生的成绩
        Map<String, Integer> bobScores = new HashMap<>();
        bobScores.put("Math", 78);
        bobScores.put("English", 88);
        studentScores.add(bobScores);

        // 打印每个学生的成绩
        for (Map<String, Integer> scores : studentScores) {
            System.out.println(scores);
        }
    }
}

3. Map 嵌套 Map

Map 嵌套 Map 常用于实现多级键值映射,如城市的区域分类。

import java.util.HashMap;
import java.util.Map;

public class MapInMapExample {
    public static void main(String[] args) {
        // 创建一个 Map 存储国家及其城市信息
        Map<String, Map<String, String>> countries = new HashMap<>();

        // 添加美国的城市
        Map<String, String> usCities = new HashMap<>();
        usCities.put("New York", "NY");
        usCities.put("Los Angeles", "CA");
        countries.put("USA", usCities);

        // 添加中国的城市
        Map<String, String> cnCities = new HashMap<>();
        cnCities.put("Beijing", "BJ");
        cnCities.put("Shanghai", "SH");
        countries.put("China", cnCities);

        // 打印每个国家的城市信息
        for (Map.Entry<String, Map<String, String>> country : countries.entrySet()) {
            System.out.println("Country: " + country.getKey());
            for (Map.Entry<String, String> city : country.getValue().entrySet()) {
                System.out.println("  City: " + city.getKey() + ", Code: " + city.getValue());
            }
        }
    }
}

4. Map 嵌套 List

Map 嵌套 List 常用于关联单个键与多个值,如部门和员工列表。

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class MapInListExample {
    public static void main(String[] args) {
        // 创建一个 Map 存储部门及其员工列表
        Map<String, List<String>> departments = new HashMap<>();

        // 添加开发部的员工
        List<String> devTeam = new ArrayList<>();
        devTeam.add("Alice");
        devTeam.add("Bob");
        departments.put("Development", devTeam);

        // 添加销售部的员工
        List<String> salesTeam = new ArrayList<>();
        salesTeam.add("Charlie");
        salesTeam.add("David");
        departments.put("Sales", salesTeam);

        // 打印每个部门的员工列表
        for (Map.Entry<String, List<String>> department : departments.entrySet()) {
            System.out.println("Department: " + department.getKey());
            for (String employee : department.getValue()) {
                System.out.println("  Employee: " + employee);
            }
        }
    }
}

(四)总结

嵌套集合提供了一种灵活的方式来表示复杂的数据结构,在处理多维数据、复杂映射关系和动态关系时尤其有用。通过结合使用 ListMap 等集合类型,开发者可以构建适合具体应用场景的高效数据存储和访问方案。