一、一维数组的核心定义与初始化

1.1 数组的本质

数组是Java中用于存储一批同类型数据的线性容器,核心特性如下:

  • 容器内所有元素的数据类型必须完全一致;
  • 数组在内存中占用连续的存储空间;
  • 元素通过索引(下标)访问,索引从0开始计数;
  • 数组长度一旦初始化,不可动态修改。

使用数组存储批量数据,相比独立变量具备显著优势:

  1. 代码结构简洁,避免定义大量冗余变量;
  2. 支持批量遍历与统一处理,大幅提升数据处理效率;
  3. 内存连续分配,数据访问性能稳定可控。

1.2 数组的两种初始化方式

Java数组提供静态初始化与动态初始化两种方式,分别适配不同的业务场景。

1.2.1 静态初始化

静态初始化适用于数组元素在定义时已完全明确的场景,定义时直接指定具体元素值,数组长度由元素个数自动确定。

标准语法:

// 简化格式(开发中主流写法)
数据类型[] 数组名 = {元素1, 元素2, 元素3, ...};

// 完整格式
数据类型[] 数组名 = new 数据类型[]{元素1, 元素2, 元素3, ...};

代码示例:

// 简化格式:存储3个int类型整数
int[] arr1 = {12, 24, 36};

// 完整格式:存储学生姓名列表
String[] names = new String[]{"张三", "李四", "王五", "赵六"};

注意事项:

  • 标准写法为数据类型[] 数组名,虽支持数据类型 数组名[]的写法,但易造成语义混淆;
  • 静态初始化时禁止在[]中指定数组长度,例如int[] arr = new int[3]{12,24,36};为非法语法,会编译报错。

1.2.2 动态初始化

动态初始化适用于数组元素在定义时未明确,需先确定容器长度,后续再赋值的场景,定义时仅指定数据类型与数组长度,元素采用Java默认值初始化。

标准语法:

数据类型[] 数组名 = new 数据类型[数组长度];

代码示例:

// 动态初始化长度为3的int数组,元素默认值为0
int[] arr = new int[3];

// 动态初始化长度为8的double数组,用于存储学生成绩,元素默认值为0.0
double[] scores = new double[8];

动态初始化的元素默认值规则,由数组的数据类型决定,具体如下表:

数据类型分类具体数据类型默认初始值
基本数据类型byte、short、char、int、long0
基本数据类型float、double0.0
基本数据类型booleanfalse
引用数据类型类、接口、数组、Stringnull

1.3 数组的元素访问与核心属性

数组元素通过数组名[索引] 的格式访问,支持元素的读取与修改;数组提供length属性,用于获取数组的元素个数(长度)。

核心语法:

// 1. 读取元素
元素类型 变量名 = 数组名[索引];

// 2. 修改元素
数组名[索引] = 新值;

// 3. 获取数组长度
int 长度变量 = 数组名.length;

代码示例:

public class ArrayAccessDemo {
    public static void main(String[] args) {
        int[] arr = {12, 24, 36};
        
        // 读取索引0的元素
        System.out.println(arr[0]); // 输出:12
        
        // 修改索引1的元素值
        arr[1] = 100;
        System.out.println(arr[1]); // 输出:100
        
        // 获取数组长度
        System.out.println(arr.length); // 输出:3
    }
}

注意事项:数组的合法索引范围为0 ~ 数组长度-1,若访问超出该范围的索引,会抛出ArrayIndexOutOfBoundsException(数组索引越界异常),是数组操作中最常见的运行时异常。


二、数组的核心遍历操作与经典场景实现

2.1 数组的遍历

数组遍历指依次访问数组中的每一个元素,是数组所有批量操作的基础,广泛应用于数据求和、最值查找、元素筛选、批量赋值等场景。

Java中最基础的遍历方式为for循环遍历,语法如下:

int[] arr = {20, 30, 40, 50};
// 遍历数组:i为索引,从0开始,到数组长度-1结束
for (int i = 0; i < arr.length; i++) {
    System.out.println(arr[i]);
}

2.2 经典场景1:数组元素求最值

业务需求:获取数组中元素的最大值、最小值,是业务开发中数据统计的基础能力。

实现思路:

  1. 定义变量存储当前最值,初始值赋值为数组第一个元素;
  2. 遍历数组,将每个元素与当前最值比较,若符合条件则更新最值变量;
  3. 遍历结束后,最值变量即为数组的最终结果。

代码实现:

public class ArrayMaxMinDemo {
    public static void main(String[] args) {
        int[] faceScores = {15, 9000, 10000, 20000, 9500, -5};
        
        // 初始化最值为数组第一个元素
        int max = faceScores[0];
        int min = faceScores[0];
        
        // 遍历数组更新最值
        for (int i = 1; i < faceScores.length; i++) {
            if (faceScores[i] > max) {
                max = faceScores[i];
            }
            if (faceScores[i] < min) {
                min = faceScores[i];
            }
        }
        
        // 输出结果
        System.out.println("数组最大值:" + max); // 输出:20000
        System.out.println("数组最小值:" + min); // 输出:-5
    }
}

2.3 经典场景2:学生成绩统计

业务需求:录入班级8名学生的Java课程成绩,计算并输出班级成绩的平均分、最高分、最低分。

代码实现:

import java.util.Scanner;

public class ScoreStatisticsDemo {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        // 动态初始化数组,存储8名学生的成绩
        double[] scores = new double[8];
        
        // 循环录入学生成绩
        for (int i = 0; i < scores.length; i++) {
            System.out.println("请输入第" + (i+1) + "名学生的Java成绩:");
            scores[i] = scanner.nextDouble();
        }
        scanner.close();
        
        // 初始化统计变量
        double sum = 0.0;
        double max = scores[0];
        double min = scores[0];
        
        // 遍历数组完成统计
        for (double score : scores) {
            sum += score;
            if (score > max) max = score;
            if (score < min) min = score;
        }
        
        // 计算平均分
        double average = sum / scores.length;
        
        // 输出统计结果
        System.out.println("班级成绩最高分:" + max);
        System.out.println("班级成绩最低分:" + min);
        System.out.println("班级成绩总分:" + sum);
        System.out.println("班级成绩平均分:" + average);
    }
}

2.4 数组元素交换

数组元素交换是排序算法、数组乱序(洗牌)的基础操作,核心实现思路是通过临时变量中转两个元素的值,完成交换。

代码实现:

public class ArraySwapDemo {
    public static void main(String[] args) {
        int[] arr = {10, 20, 30, 40, 50};
        // 交换索引0和索引4的元素
        int temp = arr[0]; // 临时变量存储第一个元素的值
        arr[0] = arr[4];   // 将最后一个元素赋值给第一个位置
        arr[4] = temp;     // 将临时变量的值赋值给最后一个位置
        
        // 遍历输出交换后的数组:50 20 30 40 10
        for (int num : arr) {
            System.out.print(num + " ");
        }
    }
}

三、数组的工程化实践案例

3.1 案例1:随机点名系统

业务需求:开发班级随机点名程序,从学生名单中随机抽取一名学生,实现课堂随机点名场景。

代码实现:

import java.util.Random;

public class RandomCallNameDemo {
    public static void main(String[] args) {
        // 数组存储班级学生姓名
        String[] names = {
            "张誉", "刘疏桐", "田启峰", "伊成元", "赵志刚",
            "解天野", "高续升", "王世举", "周字伦", "刘帅",
            "陈小伟", "陈伟北", "高明", "毕振宇", "王乾麟"
        };
        
        // 生成随机索引:范围 0 ~ names.length-1
        Random random = new Random();
        int randomIndex = random.nextInt(names.length);
        // 获取随机抽取的学生姓名
        String selectedName = names[randomIndex];
        
        // 输出点名结果
        System.out.println(selectedName + "同学,出来回答问题!");
    }
}

3.2 案例2:斗地主游戏-卡牌初始化与洗牌

业务需求:开发简易版斗地主游戏的核心基础能力,完成54张卡牌的初始化(做牌)与洗牌功能。

实现思路:

  1. 定义两个数组,分别存储卡牌的花色与点数;
  2. 动态初始化长度为54的String数组,存储所有卡牌;
  3. 嵌套循环组合花色与点数,完成52张普通卡牌初始化,单独添加大小王;
  4. 洗牌功能:遍历数组,随机生成索引,交换当前元素与随机索引的元素,完成数组乱序。

代码实现:

import java.util.Random;

public class LandlordCardDemo {
    public static void main(String[] args) {
        // 1. 定义卡牌的花色与点数
        String[] colors = {"♠", "♥", "♣", "♦"};
        String[] numbers = {"3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A", "2"};
        
        // 2. 动态初始化数组,存储54张卡牌
        String[] cards = new String[54];
        int index = 0; // 卡牌数组的索引
        
        // 3. 组合花色与点数,初始化普通卡牌
        for (String number : numbers) {
            for (String color : colors) {
                cards[index] = color + number;
                index++;
            }
        }
        
        // 4. 添加大小王
        cards[index++] = "小王";
        cards[index] = "大王";
        
        // 输出初始化完成的卡牌
        System.out.println("初始化完成的卡牌:");
        for (String card : cards) {
            System.out.print(card + " ");
        }
        System.out.println("\n");
        
        // 5. 洗牌:数组乱序
        Random random = new Random();
        for (int i = 0; i < cards.length; i++) {
            int randomIndex = random.nextInt(cards.length);
            // 元素交换
            String temp = cards[i];
            cards[i] = cards[randomIndex];
            cards[randomIndex] = temp;
        }
        
        // 输出洗牌后的卡牌
        System.out.println("洗牌后的卡牌:");
        for (String card : cards) {
            System.out.print(card + " ");
        }
    }
}

四、二维数组的核心定义与实践

4.1 二维数组的本质

二维数组是数组的数组,即数组中的每一个元素,都是一个独立的一维数组。二维数组适用于存储具有行、列二维结构的业务数据,如班级座位信息、表格数据、矩阵运算、2D游戏地图等场景。

4.2 二维数组的初始化方式

4.2.1 静态初始化

静态初始化适用于二维数组的行、列数据在定义时已明确的场景,直接指定每一行的一维数组元素。

标准语法:

// 简化格式
数据类型[][] 数组名 = {{元素1,元素2...}, {元素1,元素2...}, ...};

// 完整格式
数据类型[][] 数组名 = new 数据类型[][]{{元素1,元素2...}, {元素1,元素2...}, ...};

代码示例:存储班级座位信息

// 二维数组存储班级座位,每一行对应一排座位
String[][] seats = {
    {"张无忌", "赵敏", "周芷若"},
    {"张三丰", "宋远桥", "殷梨亭"},
    {"灭绝", "陈昆", "玄冥二老", "金毛狮王"},
    {"杨逍", "纪晓芙"}
};

4.2.2 动态初始化

动态初始化分为两种模式,适配不同的业务场景:

  1. 固定列数初始化:每一行的列数完全一致,适用于规则的表格、矩阵场景

    // 3行5列的二维数组
    数据类型[][] 数组名 = new 数据类型[行数][列数];
  2. 不规则列数初始化:先指定行数,后续为每一行单独定义列数,适用于行列数不统一的不规则场景

    // 先指定4行,列数后续定义
    数据类型[][] 数组名 = new 数据类型[行数][];
    // 为每一行初始化一维数组
    数组名[0] = new 数据类型[3];
    数组名[1] = new 数据类型[4];

代码示例:

// 固定列数:3行5列的int二维数组,初始默认值为0
int[][] arr1 = new int[3][5];

// 不规则列数:4行的二维数组,每行列数不同
String[][] seats = new String[4][];
seats[0] = new String[3]; // 第一排3个座位
seats[1] = new String[3]; // 第二排3个座位
seats[2] = new String[4]; // 第三排4个座位
seats[3] = new String[2]; // 第四排2个座位

4.3 二维数组的元素访问

二维数组的元素通过行索引列索引进行访问,语法如下:

// 访问某一行的一维数组:数组名[行索引]
// 访问具体元素:数组名[行索引][列索引]
// 修改元素值:数组名[行索引][列索引] = 新值;

代码示例:

public class TwoDArrayAccessDemo {
    public static void main(String[] args) {
        int[][] arr = new int[][]{{12, 24, 36}, {666, 888}, {10, 20, 30}, {999}};
        
        // 访问第3行(索引2)的一维数组
        int[] row = arr[2];
        // 遍历输出该行元素:10 20 30
        for (int num : row) {
            System.out.print(num + " ");
        }
        System.out.println();
        
        // 访问第3行第2列(索引2、1)的元素
        System.out.println(arr[2][1]); // 输出:20
        
        // 修改第4行第1列(索引3、0)的元素
        arr[3][0] = 111;
        System.out.println(arr[3][0]); // 输出:111
    }
}

4.4 二维数组的遍历

二维数组的遍历需要使用嵌套循环,外层循环遍历每一行,内层循环遍历当前行的每一列元素。

标准语法:

int[][] arr = {{12, 24, 36}, {666, 888}, {10, 20, 30}, {999}};
// 外层循环:遍历每一行
for (int i = 0; i < arr.length; i++) {
    // 内层循环:遍历当前行的每一列
    for (int j = 0; j < arr[i].length; j++) {
        System.out.print(arr[i][j] + "\t");
    }
    // 每行遍历完成后换行
    System.out.println();
}

4.5 实践案例:班级座位信息管理

业务需求:存储并输出班级的座位信息,要求输出时直观展示学生所在的排数与座位号。

代码实现:

public class SeatManagerDemo {
    public static void main(String[] args) {
        // 二维数组存储班级座位信息
        String[][] seats = {
            {"张无忌", "赵敏", "周芷若"},
            {"张三丰", "宋远桥", "殷梨亭"},
            {"灭绝", "陈昆", "玄冥二老", "金毛狮王"},
            {"杨逍", "纪晓芙"}
        };
        
        // 遍历输出座位信息
        System.out.println("===== 班级座位信息 =====");
        for (int i = 0; i < seats.length; i++) {
            System.out.print("第" + (i+1) + "排:");
            for (int j = 0; j < seats[i].length; j++) {
                System.out.print(seats[i][j] + "  ");
            }
            System.out.println();
        }
    }
}

执行结果:

===== 班级座位信息 =====
第1排:张无忌  赵敏  周芷若  
第2排:张三丰  宋远桥  殷梨亭  
第3排:灭绝  陈昆  玄冥二老  金毛狮王  
第4排:杨逍  纪晓芙  

4.6 拓展场景:2D游戏地图初始化

二维数组广泛应用于2D游戏的地图数据存储,例如石头迷阵、推箱子、贪吃蛇等游戏,均可通过二维数组存储地图格子数据,实现地图的初始化与渲染。

示例代码:石头迷阵地图初始化

public class StoneMazeDemo {
    public static void main(String[] args) {
        // 4行4列的二维数组,存储16格石头迷阵的数字
        int[][] maze = new int[4][4];
        int num = 1;
        // 初始化地图数字 1-15,最后一格为0(代表空位)
        for (int i = 0; i < maze.length; i++) {
            for (int j = 0; j < maze[i].length; j++) {
                maze[i][j] = num <= 15 ? num++ : 0;
            }
        }
        
        // 输出初始化的迷阵地图
        System.out.println("===== 石头迷阵初始地图 =====");
        for (int[] row : maze) {
            for (int data : row) {
                // 空位显示为空格,数字格式化输出
                System.out.print(data == 0 ? "  \t" : data + "\t");
            }
            System.out.println();
        }
    }
}


一、类与对象的核心定义

1.1 对象的本质

在Java中,对象是一种结构化的数据载体,本质是用于描述一个具体事物的专属数据结构,它可以完整存储一个事物的相关属性数据,并承载该事物对应的行为能力。

在实际开发中,无论是现实世界中的人物、商品,还是业务系统中的用户、订单、电影,都可以被抽象为对象,通过对象来统一管理其相关数据与行为。

1.2 类与对象的关系

类是对象的模板(也称为设计图),它定义了一类事物所具备的通用属性与行为;而对象是类的实例化产物,通过类可以创建出多个具备相同特征的独立对象。

简单来说,类是对一类事物的抽象描述,对象是该类事物的具体实例。例如:“明星”是一个类,而赵丽颖、杨幂就是该类的具体对象;“学生”是一个类,而播妞、播仔就是该类的具体对象。

1.3 类的基础语法

一个标准的类主要由两部分组成:

  • 成员变量(属性):用于描述事物的特征数据;
  • 成员方法(行为):用于描述事物的行为能力,即对数据的处理逻辑。

类的基础定义语法如下:

/**
 * 明星类(模板)
 * 定义了明星这类事物的通用属性
 */
public class Star {
    // 成员变量(属性)
    String name;    // 姓名
    int age;        // 年龄
    double height;  // 身高
    double weight;  // 体重
}

1.4 对象的创建与使用

在Java中,通过new关键字可以实例化类,得到一个具体的对象。每执行一次new操作,都会在内存中创建一个全新的独立对象。

对象创建完成后,可以通过对象名.属性名的方式为属性赋值,也可以通过对象名.方法名()的方式调用对象的行为方法。

public class StarTest {
    public static void main(String[] args) {
        // 实例化Star类,创建第一个明星对象
        Star s1 = new Star();
        // 为对象属性赋值
        s1.name = "赵丽颖";
        s1.age = 36;
        s1.height = 165.0;
        s1.weight = 44.6;

        // 实例化Star类,创建第二个明星对象
        Star s2 = new Star();
        s2.name = "杨幂";
        s2.age = 37;
        s2.height = 166.5;
        s2.weight = 45.0;
    }
}

1.5 带行为方法的类定义

类中不仅可以定义属性,还可以定义处理属性数据的行为方法。以学生类为例,我们可以在类中定义计算总成绩、平均成绩的方法,实现“谁的数据谁处理”的面向对象核心思想。

/**
 * 学生类
 * 包含学生属性与成绩处理的行为方法
 */
public class Student {
    // 成员变量(属性)
    String name;        // 学生姓名
    double chinese;     // 语文成绩
    double math;        // 数学成绩

    // 成员方法(行为):打印总成绩
    public void printTotalScore() {
        System.out.println(name + "同学的各科总分是:" + (chinese + math));
    }

    // 成员方法(行为):打印平均成绩
    public void printAverageScore() {
        System.out.println(name + "同学的各科平均分是:" + (chinese + math) / 2.0);
    }
}

方法的调用示例:

public class StudentTest {
    public static void main(String[] args) {
        // 创建学生对象并赋值
        Student s1 = new Student();
        s1.name = "播妞";
        s1.chinese = 100;
        s1.math = 100;
        // 调用对象的行为方法
        s1.printTotalScore();
        s1.printAverageScore();

        Student s2 = new Student();
        s2.name = "播仔";
        s2.chinese = 59;
        s2.math = 100;
        s2.printTotalScore();
        s2.printAverageScore();
    }
}

执行结果:

播妞同学的各科总分是:200.0
播妞同学的各科平均分是:100.0
播仔同学的各科总分是:159.0
播仔同学的各科平均分是:79.5

1.6 对象的内存模型

Java程序运行时,对象的内存分配主要涉及三块内存区域:

  1. 栈内存:存储对象的引用变量(即对象的内存地址),方法执行时的局部变量也存放在栈中;
  2. 堆内存:存储new关键字创建的对象本身,包括对象的所有成员变量;
  3. 方法区:存储类的字节码文件(.class),包括类的成员变量定义、方法定义等信息。

当执行Student s1 = new Student();时,会在堆内存中创建Student对象的实例,在栈内存中创建引用变量s1s1中存储的是堆内存中对象的内存地址,通过该地址实现对对象属性和方法的访问。


二、构造器

2.1 构造器的定义与作用

构造器(也称为构造方法)是类中一种特殊的方法,它在使用new关键字创建对象时会被自动调用,核心作用是完成对象的初始化操作,最常见的场景是在创建对象的同时为成员变量赋值。

2.2 构造器的语法规范

构造器的定义需要遵循以下语法规则:

  • 构造器的名称必须与类名完全一致;
  • 构造器没有返回值类型,连void都不能写;
  • 构造器可以定义参数,也可以无参数。

基础语法如下:

public class Student {
    String name;
    int age;

    // 无参构造器
    public Student() {
        // 初始化逻辑
    }

    // 有参构造器
    public Student(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

2.3 构造器的使用注意事项

  1. 默认无参构造器:任何一个类在定义时,即使不手动编写构造器,Java编译器都会自动为该类生成一个默认的无参构造器;
  2. 构造器的覆盖规则:如果手动为类定义了有参构造器,编译器将不再生成默认的无参构造器。此时如果需要使用无参构造器,必须手动显式定义;
  3. 构造器的重载:一个类中可以定义多个参数列表不同的构造器,即构造器的重载,以满足不同的初始化需求。

构造器的使用示例:

public class StudentTest {
    public static void main(String[] args) {
        // 调用无参构造器创建对象
        Student s1 = new Student();
        s1.name = "播妞";
        s1.age = 18;

        // 调用有参构造器创建对象,创建时直接完成赋值
        Student s2 = new Student("播仔", 19);
    }
}

三、this关键字

3.1 this的本质

this是Java中的一个关键字,本质是当前对象的引用。在成员方法中,this指向调用该方法的当前对象;在构造器中,this指向正在创建的对象。简单来说,哪个对象调用方法,this就指向哪个对象

3.2 this的核心应用场景

this最主要的作用是解决成员变量与方法内局部变量的命名冲突问题。当方法内的局部变量与类的成员变量名称相同时,Java会默认采用就近原则访问局部变量,此时需要通过this.变量名的方式明确指定访问类的成员变量。

示例代码:

public class Student {
    String name;
    double score;

    // 带参数的方法,参数名与成员变量名冲突
    public void checkPass(double score) {
        // this.score 访问成员变量,score 访问方法入参的局部变量
        if (this.score >= score) {
            System.out.println("成绩达标");
        } else {
            System.out.println("成绩未达标");
        }
    }

    // 有参构造器中使用this
    public Student(String name, double score) {
        this.name = name;
        this.score = score;
    }
}

四、封装特性

4.1 封装的核心定义

封装是面向对象编程的三大核心特性之一(封装、继承、多态)。其核心思想是:在设计类时,将事物的属性数据与处理数据的行为方法封装到同一个类中,对外部隐藏内部的实现细节,仅暴露必要的访问与操作入口。

4.2 封装的设计原则

封装的核心设计原则是合理隐藏、合理暴露

  • 合理隐藏:对类内部的属性数据进行隐藏,避免外部直接随意修改,保证数据的安全性;
  • 合理暴露:对外提供统一的、受控的访问与操作入口,通过方法实现对数据的校验与处理。

在Java代码层面,通过访问修饰符实现隐藏与暴露的控制:

  • private(私有):被修饰的成员只能在当前类内部访问,实现成员的隐藏;
  • public(公开):被修饰的成员可以在任意类中访问,实现成员的暴露。

4.3 封装的标准实现

规范的封装实现,通常会将类的成员变量用private修饰进行隐藏,然后为每个成员变量提供public修饰的getter(获取值)和setter(设置值)方法,实现对属性的受控访问。

示例代码:

public class Student {
    // 私有成员变量,外部无法直接访问
    private String name;
    private double chinese;
    private double math;

    // 公开的getter/setter方法
    public String getName() {
        return name;
    }

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

    public double getChinese() {
        return chinese;
    }

    public void setChinese(double chinese) {
        // 可以在setter方法中添加数据校验逻辑
        if (chinese >= 0 && chinese <= 100) {
            this.chinese = chinese;
        } else {
            System.out.println("语文成绩输入不合法,必须在0-100之间");
        }
    }

    public double getMath() {
        return math;
    }

    public void setMath(double math) {
        if (math >= 0 && math <= 100) {
            this.math = math;
        } else {
            System.out.println("数学成绩输入不合法,必须在0-100之间");
        }
    }

    // 成绩处理方法
    public void printTotalScore() {
        System.out.println(name + "同学的各科总分是:" + (chinese + math));
    }
}

通过封装,我们可以在setter方法中添加数据校验逻辑,避免非法数据的写入,保证了对象数据的安全性与完整性,这也是封装的核心价值之一。


五、JavaBean(实体类)规范

5.1 实体类的定义

JavaBean也称为实体类,是Java开发中一种符合特定规范的特殊类,其核心作用是对业务数据进行封装,仅负责数据的存取,是实现数据与业务逻辑分离的基础。

5.2 实体类的规范要求

一个标准的JavaBean实体类,必须满足以下两个核心要求:

  1. 类中的所有成员变量必须用private修饰私有化,并为每个成员变量提供对应的public修饰的gettersetter方法;
  2. 类中必须提供一个无参构造器,有参构造器为可选。

5.3 实体类的设计思想

实体类的设计遵循数据与业务处理相分离的原则:实体类的对象仅负责数据的存储与读取,而对数据的业务处理逻辑,交由专门的业务处理类来实现。这种设计方式可以让代码职责更加清晰,提升代码的可维护性与可扩展性。

标准实体类示例:

/**
 * 用户实体类,仅负责用户数据的存取
 */
public class User {
    // 私有成员变量
    private Long id;
    private String username;
    private Integer age;

    // 必须的无参构造器
    public User() {
    }

    // 可选的有参构造器
    public User(Long id, String username, Integer age) {
        this.id = id;
        this.username = username;
        this.age = age;
    }

    // 所有成员变量的getter/setter方法
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public Integer getAge() {
        return age;
    }

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

业务处理类示例:

/**
 * 用户业务处理类,负责用户相关的业务逻辑
 */
public class UserOperator {
    private User user;

    public UserOperator(User user) {
        this.user = user;
    }

    // 业务方法:判断用户是否成年
    public boolean isAdult() {
        return user.getAge() >= 18;
    }
}

六、static关键字

static关键字意为静态,可以用于修饰类的成员变量、成员方法,被修饰的成员称为静态成员,属于类本身,而非类的实例对象。

6.1 静态变量与实例变量

成员变量根据是否被static修饰,分为静态变量(类变量)和实例变量两类,二者的核心区别如下表:

特性静态变量(类变量)实例变量(对象变量)
修饰符static修饰static修饰
所属主体属于类本身属于每个实例对象
内存存储内存中仅存在一份,与类一起加载每个对象都有独立的一份,随对象创建
访问方式推荐:类名.变量名;也可通过对象访问仅能通过对象名.变量名访问
共享特性被该类的所有实例对象共享每个对象独有,互不影响

6.1.1 静态变量的使用示例

public class User {
    // 静态变量:统计创建的用户对象数量,属于类,所有对象共享
    public static int number;
    // 实例变量:每个用户的用户名,属于每个对象
    private String username;

    public User() {
        // 每创建一个对象,静态变量number自增1
        User.number++;
    }

    // getter/setter省略
}
public class UserTest {
    public static void main(String[] args) {
        // 类名直接访问静态变量,初始值为0
        System.out.println(User.number);

        // 创建3个用户对象
        new User();
        new User();
        new User();

        // 静态变量值变为3,被所有对象共享
        System.out.println(User.number);
    }
}

执行结果:

0
3

6.1.2 静态变量的应用场景

当某个数据只需要一份,且需要被该类的所有实例对象共享访问、修改时,该数据就适合定义为静态变量。例如:系统配置项、计数器、全局常量等。

6.2 静态方法与实例方法

成员方法根据是否被static修饰,分为静态方法(类方法)和实例方法两类,二者的核心区别如下:

特性静态方法(类方法)实例方法
修饰符static修饰static修饰
所属主体属于类本身属于每个实例对象
访问方式推荐:类名.方法名();也可通过对象调用仅能通过对象名.方法名()调用
访问限制只能直接访问静态成员,不能直接访问实例成员和this关键字可以直接访问静态成员和实例成员,可使用this

6.2.1 静态方法的典型应用:工具类设计

静态方法最核心的应用场景是设计工具类。工具类是一种仅提供通用功能方法的类,其中的方法都是静态方法,每个方法完成一个通用的功能,供开发者直接调用,无需创建对象。

使用静态方法设计工具类的优势:

  1. 调用方便,直接通过类名即可调用方法,无需实例化对象;
  2. 节省内存,避免了创建大量仅用于调用方法的对象,减少了堆内存的开销。

工具类的设计规范:

  1. 工具类中的所有方法都定义为静态方法;
  2. 将工具类的构造器私有化,避免外部创建该类的实例对象。

标准工具类示例:

/**
 * 字符串处理工具类
 */
public class StringUtils {
    // 私有化构造器,禁止外部实例化
    private StringUtils() {
    }

    /**
     * 判断字符串是否为空
     * @param str 待判断的字符串
     * @return 为空返回true,否则返回false
     */
    public static boolean isEmpty(String str) {
        return str == null || str.trim().length() == 0;
    }

    /**
     * 判断字符串是否为邮箱格式
     * @param email 待判断的邮箱字符串
     * @return 符合邮箱格式返回true,否则返回false
     */
    public static boolean isEmail(String email) {
        if (isEmpty(email)) {
            return false;
        }
        String emailRegex = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$";
        return email.matches(emailRegex);
    }
}

工具类的使用示例:

public class UtilsTest {
    public static void main(String[] args) {
        // 直接通过类名调用静态方法
        System.out.println(StringUtils.isEmpty(""));
        System.out.println(StringUtils.isEmail("test@163.com"));
    }
}

6.3 static关键字的使用注意事项

  1. 静态方法中可以直接访问类中的静态成员,不可以直接访问实例成员
  2. 实例方法中既可以直接访问静态成员,也可以直接访问实例成员;
  3. 实例方法中可以使用this关键字,静态方法中不可以使用this关键字
  4. 在本类中访问静态成员时,可以省略类名不写;访问其他类的静态成员时,必须携带类名访问。

七、综合实践:电影信息展示系统

7.1 需求说明

  1. 展示系统中的全部电影基础信息(电影名称、票价);
  2. 支持用户根据电影编号(ID)查询电影的详细信息。

7.2 实现思路

  1. 设计电影实体类Movie,封装电影的相关属性,符合JavaBean规范;
  2. 设计电影业务处理类MovieSystem,实现电影列表展示、根据ID查询电影详情的业务逻辑;
  3. 编写测试主类,完成功能的测试验证。

7.3 代码实现

7.3.1 电影实体类

/**
 * 电影实体类,封装电影数据
 */
public class Movie {
    // 电影属性
    private int id;             // 电影编号
    private String name;        // 电影名称
    private double price;       // 电影票价
    private double score;       // 电影评分
    private String director;    // 导演
    private String actor;       // 主演
    private String description; // 电影简介

    // 无参构造器
    public Movie() {
    }

    // 有参构造器
    public Movie(int id, String name, double price, double score, String director, String actor, String description) {
        this.id = id;
        this.name = name;
        this.price = price;
        this.score = score;
        this.director = director;
        this.actor = actor;
        this.description = description;
    }

    // getter/setter方法
    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    public double getScore() {
        return score;
    }

    public void setScore(double score) {
        this.score = score;
    }

    public String getDirector() {
        return director;
    }

    public void setDirector(String director) {
        this.director = director;
    }

    public String getActor() {
        return actor;
    }

    public void setActor(String actor) {
        this.actor = actor;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }
}

7.3.2 电影系统业务处理类

/**
 * 电影信息系统业务处理类
 */
public class MovieSystem {
    // 存储电影数据的数组
    private Movie[] movies;

    // 初始化电影数据
    public MovieSystem() {
        movies = new Movie[4];
        movies[0] = new Movie(1, "长津湖之水门桥", 45.0, 9.6, "徐克", "吴京、易烊千玺", "抗美援朝战争题材电影");
        movies[1] = new Movie(2, "一点就到家", 38.0, 8.7, "许宏宇", "刘昊然、彭昱畅", "农村电商创业题材电影");
        movies[2] = new Movie(3, "月球陨落", 52.0, 7.9, "罗兰·艾默里奇", "哈莉·贝瑞", "科幻灾难题材电影");
        movies[3] = new Movie(4, "出拳吧,妈妈", 35.0, 7.9, "唐晓白", "谭卓、田雨", "女性励志题材电影");
    }

    /**
     * 展示全部电影的基础信息
     */
    public void showAllMovies() {
        System.out.println("===== 系统全部电影信息 =====");
        for (Movie movie : movies) {
            System.out.println("电影编号:" + movie.getId() +
                    " | 电影名称:" + movie.getName() +
                    " | 电影票价:" + movie.getPrice() + "元");
        }
        System.out.println("============================");
    }

    /**
     * 根据电影编号查询电影详情
     * @param id 电影编号
     * @return 查到的电影对象,未查到返回null
     */
    public Movie queryMovieById(int id) {
        for (Movie movie : movies) {
            if (movie.getId() == id) {
                return movie;
            }
        }
        return null;
    }
}

7.3.3 测试主类

public class MovieTest {
    public static void main(String[] args) {
        // 初始化电影系统
        MovieSystem movieSystem = new MovieSystem();

        // 1. 展示全部电影基础信息
        movieSystem.showAllMovies();

        // 2. 根据ID查询电影详情
        int queryId = 2;
        Movie movie = movieSystem.queryMovieById(queryId);
        if (movie != null) {
            System.out.println("\n===== 电影详情信息 =====");
            System.out.println("电影编号:" + movie.getId());
            System.out.println("电影名称:" + movie.getName());
            System.out.println("电影评分:" + movie.getScore() + "分");
            System.out.println("电影票价:" + movie.getPrice() + "元");
            System.out.println("导演:" + movie.getDirector());
            System.out.println("主演:" + movie.getActor());
            System.out.println("电影简介:" + movie.getDescription());
            System.out.println("=========================");
        } else {
            System.out.println("未查询到编号为" + queryId + "的电影");
        }
    }
}

7.4 执行结果

===== 系统全部电影信息 =====
电影编号:1 | 电影名称:长津湖之水门桥 | 电影票价:45.0元
电影编号:2 | 电影名称:一点就到家 | 电影票价:38.0元
电影编号:3 | 电影名称:月球陨落 | 电影票价:52.0元
电影编号:4 | 电影名称:出拳吧,妈妈 | 电影票价:35.0元
============================

===== 电影详情信息 =====
电影编号:2
电影名称:一点就到家
电影评分:8.7分
电影票价:38.0元
导演:许宏宇
主演:刘昊然、彭昱畅
电影简介:农村电商创业题材电影
=========================

本文包含异常概念、体系分类、处理方式、自定义异常四大部分。

一、什么是异常

异常就是Java程序在编译或运行过程中出现的错误,会导致程序非正常终止。

开发中最常见的异常场景:

  1. 数组索引越界:访问了数组不存在的下标
  2. 算术异常:除数为0
  3. 空指针异常:调用null对象的方法
  4. 文件不存在:读取本地不存在的文件
// 常见运行时异常示例
public class ExceptionTest {
    public static void main(String[] args) {
        int[] arr = {10, 20, 30};
        System.out.println(arr[3]); // 数组索引越界异常
        System.out.println(10 / 0); // 算术异常
        String str = null;
        System.out.println(str.length()); // 空指针异常
    }
}

异常的核心作用

  1. 定位BUG:异常信息会精准打印错误位置和原因,是调试程序的关键
  2. 流程通知:作为方法的特殊返回值,告诉调用者方法执行失败的原因

二、Java异常完整体系

Java中所有异常和错误的根类是 java.lang.Throwable,整体分为两大分支:

1. Error(系统级错误)

属于JVM底层的严重问题(如内存溢出、系统崩溃),也无法通过代码处理。

2. Exception(程序异常,开发核心处理对象)

程序运行/编译中可预见的错误,我们所有异常处理都围绕这个类展开,又细分为运行时异常编译时异常

三、异常的两大分类

1. 运行时异常(RuntimeException)

  • 定义:RuntimeException 及其所有子类
  • 特点:编译阶段不报错,运行时才触发
  • 常见示例:数组越界、空指针、算术异常、类型转换异常

2. 编译时异常(受检异常)

  • 定义:非 RuntimeException 的Exception子类
  • 特点:编译阶段强制报错,必须处理才能运行
  • 常见示例:日期解析异常、文件读取异常、IO流异常
// 编译时异常示例(必须处理,否则编译失败)
import java.text.SimpleDateFormat;
import java.util.Date;

public class CompileException {
    public static void main(String[] args) throws Exception {
        String time = "2024-07-09";
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
        Date date = sdf.parse(time); // 日期解析异常(编译时异常)
    }
}

四、异常的两种核心处理方式

Java提供了抛出异常捕获异常两种处理方案,开发中可根据场景灵活使用。

1. 抛出异常(throws)

  • 作用:方法内部不处理异常,抛给调用者处理
  • 语法:在方法声明后添加 throws 异常类名
  • 简化写法:直接抛出 Exception 接收所有异常
// 抛出异常示例
public class ThrowException {
    public static void main(String[] args) throws Exception {
        // 调用者接收异常,也可继续抛出
        testException();
    }

    // 方法抛出异常,交给上层处理
    public static void testException() throws Exception {
        int num = 10 / 0;
    }
}

2. 捕获异常(try...catch)

  • 作用:直接拦截并处理异常,保证程序不终止
  • 语法:

    try {
        // 可能出现异常的代码
    } catch (异常类型 变量名) {
        // 异常处理逻辑
    }
  • 简化写法:catch (Exception e) 捕获所有异常
// 捕获异常示例
public class TryCatchException {
    public static void main(String[] args) {
        try {
            int num = 10 / 0;
        } catch (Exception e) {
            e.printStackTrace(); // 打印异常堆栈信息
            System.out.println("程序出现异常,已处理!");
        }
        // 异常处理后,程序继续执行
        System.out.println("程序正常结束");
    }
}

五、自定义异常

Java内置的异常类无法覆盖所有业务场景(如年龄非法、手机号格式错误),此时需要自定义异常来管理业务错误。

自定义异常的两种类型

1. 自定义编译时异常

  • 继承:Exception
  • 特点:编译强制处理,提醒严格
// 自定义编译时异常
public class AgeIllegalException extends Exception {
    // 无参构造
    public AgeIllegalException() {}
    // 带异常信息的构造
    public AgeIllegalException(String message) {
        super(message);
    }
}

2. 自定义运行时异常

  • 继承:RuntimeException
  • 特点:编译不报错,运行触发,开发更常用
// 自定义运行时异常
public class AgeIllegalRuntimeException extends RuntimeException {
    public AgeIllegalRuntimeException() {}
    public AgeIllegalRuntimeException(String message) {
        super(message);
    }
}

自定义异常使用示例

public class CustomExceptionTest {
    public static void main(String[] args) {
        try {
            saveAge(300); // 传入非法年龄
        } catch (AgeIllegalException e) {
            e.printStackTrace();
            System.out.println("年龄校验失败!");
        }
    }

    // 业务方法:校验年龄并抛出自定义异常
    public static void saveAge(int age) throws AgeIllegalException {
        if(age < 1 || age > 200){
            throw new AgeIllegalException("年龄非法,范围必须是1-200");
        }
        System.out.println("年龄保存成功:" + age);
    }
}

六、企业级异常处理实践

  1. 分层抛出,统一捕获
    底层方法不处理异常,全部向上抛出,最外层统一捕获,打印日志并返回友好提示
  2. 异常重试修复
    针对用户输入错误等场景,捕获异常后引导用户重新输入,保证程序不中断
// 异常重试修复示例
import java.util.Scanner;

public class ExceptionRetry {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        while (true) {
            try {
                System.out.print("请输入商品价格:");
                double price = sc.nextDouble();
                System.out.println("价格设置成功:" + price);
                break;
            } catch (Exception e) {
                System.out.println("输入格式错误,请重新输入数字!");
                sc.next(); // 清空错误输入
            }
        }
    }
}

七、总结

  1. 异常根类 Throwable,分 Error(无需处理)和 Exception(开发处理)
  2. 异常分类:运行时异常(编译不报错)、编译时异常(编译强制处理)
  3. 处理方式:throws 抛出(上层处理)、try...catch 捕获(自行处理)
  4. 自定义异常:继承 Exception(编译时)/RuntimeException(运行时)
  5. 开发规范:底层抛异常,上层统一捕获,避免程序崩溃

Java集合HashSet与TreeSet

聚焦Set集合两大核心实现类HashSetTreeSet,梳理核心特性、底层数据结构、实战用法及避坑要点。

一、Set集合基础特性

Set是Java单列集合Collection的子接口,全系列通用三大核心特性

  1. 不重复:集合中无法存储相同元素
  2. 无索引:不支持通过下标获取元素,无法普通for循环遍历
  3. 无独有方法:常用API全部继承自Collection接口

本文仅讲解最常用的两个实现类:HashSet(无序去重)、TreeSet(排序去重)。


二、HashSet 详解

2.1 核心特点

  • 无序:元素添加顺序 ≠ 遍历输出顺序
  • 不重复、无索引(Set通用特性)
  • 增删改查性能优异,是日常开发最常用的Set实现类

2.2 底层原理:哈希表

HashSet的高性能源于哈希表数据结构,JDK版本不同结构有差异:

  • JDK 8之前:数组 + 单向链表
  • JDK 8及以后:数组 + 链表 + 红黑树(优化查询性能)

关键概念:哈希值

  1. 所有Java对象均可调用Object.hashCode()获取int类型哈希码
  2. 特性:同一对象多次调用返回值相同;不同对象可能哈希值相同(哈希碰撞)

元素存储完整流程

  1. 初始化:创建默认长度16、加载因子0.75的数组table
  2. 计算位置:通过元素哈希值 % 数组长度算出存储下标
  3. 空位直接存:下标位置为null,直接存入元素
  4. 碰撞去重:位置非空时,调用equals()比较元素内容

    • 内容相同:判定为重复元素,不存储
    • 内容不同:形成链表,JDK8后新元素挂在链表尾部
  5. 树化优化:链表长度>8 且 数组长度≥64,链表自动转为红黑树
  6. 扩容机制:数组元素达到 16*0.75=12 时,自动扩容为原容量2倍

2.3 自定义对象去重(重点)

HashSet默认仅能对String、Integer等基础类型去重;自定义对象必须重写两个方法才能实现内容去重:

  1. hashCode():保证相同内容对象哈希值一致
  2. equals():判断对象内容是否相同

实战代码示例

  1. 实体类Student(重写核心方法)

    import java.util.Objects;
    
    public class Student {
     private String name;
     private int age;
     private String address;
     private String phone;
    
     // 无参、全参构造
     public Student() {}
     public Student(String name, int age, String address, String phone) {
         this.name = name;
         this.age = age;
         this.address = address;
         this.phone = phone;
     }
    
     // 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; }
     public String getAddress() { return address; }
     public void setAddress(String address) { this.address = address; }
     public String getPhone() { return phone; }
     public void setPhone(String phone) { this.phone = phone; }
    
     // 重写equals:比较对象内容
     @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)
                 && Objects.equals(address, student.address)
                 && Objects.equals(phone, student.phone);
     }
    
     // 重写hashCode:根据内容生成哈希值
     @Override
     public int hashCode() {
         return Objects.hash(name, age, address, phone);
     }
    
     // 重写toString:格式化输出
     @Override
     public String toString() {
         return "Student{name='" + name + "', age=" + age + "
                         , address='" + address + "', phone='" + phone + "'}";
     }
    }
  2. 测试类:HashSet去重效果

    import java.util.HashSet;
    import java.util.Set;
    
    public class HashSetTest {
     public static void main(String[] args) {
         Set<Student> studentSet = new HashSet<>();
         // 创建两个内容完全相同的对象
         Student s1 = new Student("张三", 18, "北京", "123456");
         Student s2 = new Student("张三", 18, "北京", "123456");
    
         studentSet.add(s1);
         studentSet.add(s2);
    
         // 输出仅1个元素,去重成功
         System.out.println(studentSet);
     }
    }

三、TreeSet 详解

3.1 核心特点

  • 可排序:默认对元素升序排列
  • 不重复、无索引(Set通用特性)
  • 排序基于红黑树实现,兼顾排序与性能

3.2 底层原理:红黑树

TreeSet底层完全基于红黑树(自平衡二叉排序树),特性:

  1. 自动排序,增删改查性能稳定
  2. 去重规则:排序比较结果为0 即判定为重复元素

3.3 默认排序规则

  • 数值类型(Integer/Double):按数值大小升序
  • 字符串类型:按字符ASCII码值升序
  • 自定义对象:无法直接排序,必须手动指定排序规则

3.4 自定义对象排序(两种方案)

TreeSet支持两种排序方式,比较器优先级 > 类自带排序

方案1:实体类实现Comparable接口

实体类重写compareTo()方法,定义默认排序规则

// 教师实体类:实现Comparable接口
public class Teacher implements Comparable<Teacher> {
    private String name;
    private int age;
    private double salary;

    // 无参、全参构造
    public Teacher() {}
    public Teacher(String name, int age, double salary) {
        this.name = name;
        this.age = age;
        this.salary = salary;
    }

    // getter/setter省略
    public String getName() { return name; }
    public int getAge() { return age; }
    public double getSalary() { return salary; }

    // 核心:定义排序规则(按年龄升序)
    @Override
    public int compareTo(Teacher o) {
        // 正数:当前对象大;负数:当前对象小;0:重复
        return this.age - o.age;
    }

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

方案2:创建TreeSet时传入Comparator比较器

匿名内部类/Lambda表达式,灵活定义临时排序规则

import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;

public class TreeSetTest {
    public static void main(String[] args) {
        // 方式1:使用类默认排序(年龄升序)
        Set<Teacher> teacherSet1 = new TreeSet<>();

        // 方式2:自定义比较器(薪资降序,Lambda简化)
        Set<Teacher> teacherSet2 = new TreeSet<>((o1, o2) -> Double.compare(o2.getSalary(), o1.getSalary()));

        // 添加元素
        teacherSet2.add(new Teacher("老陈", 20, 6232.4));
        teacherSet2.add(new Teacher("dlei", 18, 3999.5));
        teacherSet2.add(new Teacher("老王", 22, 9999.9));

        // 按薪资降序输出
        System.out.println(teacherSet2);
    }
}

四、HashSet vs TreeSet 选型指南

特性HashSetTreeSet
底层结构哈希表(数组+链表+红黑树)红黑树
顺序无序可排序(默认升序)
性能增删查最优(O(1))排序场景最优(O(logn))
去重依据hashCode()+equals()比较器返回值为0
适用场景通用去重、无排序需求元素排序、范围查询

五、学习总结

  1. HashSet:Java开发首选Set集合,核心是哈希表,自定义对象必须重写hashCode()equals()实现去重;
  2. TreeSet:专注排序场景,核心是红黑树,自定义对象需通过ComparableComparator指定排序规则;
  3. 通用原则:仅去重选HashSet,需要排序选TreeSet

Java基础:Collection与List集合

聚焦单列集合Collection及其子体系List,核心特性、常用API、遍历方式、底层原理与实战用法

一、集合基础概述

数组是Java基础容器,但长度固定,无法适配动态数据存储;而集合是Java提供的动态容器,长度可变,支持灵活的增删改查,是开发中最常用的数据存储工具。

Java集合分为两大核心体系:

  • 单列集合(Collection):每个元素仅存储一个值
  • 双列集合(Map):每个元素为键值对(key-value)

二、Collection 单列集合

2.1 集合体系分支

Collection 是所有单列集合的顶层接口,两大核心子接口:

  1. List 系列有序、可重复、有索引
    实现类:ArrayList、LinkedList
  2. Set 系列无序、不重复、无索引
    实现类:HashSet、TreeSet

2.2 基础特性演示

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class CollectionTest {
    public static void main(String[] args) {
        // List:有序、可重复、有索引
        List<String> list = new ArrayList<>();
        list.add("Java");
        list.add("Java");
        list.add("C++");
        System.out.println(list); // [Java, Java, C++]

        // Set:无序、不重复、无索引
        Set<String> set = new HashSet<>();
        set.add("Java");
        set.add("Java");
        set.add("C++");
        System.out.println(set); // [Java, C++]
    }
}

2.3 Collection 通用API

作为顶层接口,Collection 定义的方法所有单列集合通用,核心方法如下:

方法名功能说明
boolean add(E e)添加元素
void clear()清空集合
boolean remove(E e)删除指定元素
boolean contains(Object obj)判断是否包含指定元素
boolean isEmpty()判断集合是否为空
int size()获取元素个数
Object[] toArray()集合转数组

API 实战代码

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;

public class CollectionApiTest {
    public static void main(String[] args) {
        Collection<String> coll = new ArrayList<>();
        // 1. 添加元素
        coll.add("张三");
        coll.add("李四");
        coll.add("王五");
        System.out.println(coll); // [张三, 李四, 王五]

        // 2. 元素个数
        System.out.println(coll.size()); // 3

        // 3. 删除元素
        coll.remove("李四");
        System.out.println(coll); // [张三, 王五]

        // 4. 判断包含
        System.out.println(coll.contains("张三")); // true

        // 5. 集合转数组
        Object[] arr = coll.toArray();
        System.out.println(Arrays.toString(arr)); // [张三, 王五]
        
        // 6. 清空集合
        coll.clear();
        System.out.println(coll.isEmpty()); // true
    }
}

2.4 Collection 三种遍历方式

遍历是集合最常用操作,Java 提供3种标准遍历方案,仅遍历不修改时均可使用

方式1:迭代器遍历(集合专属)

迭代器 Iterator 是集合遍历的原生方式,核心方法:

  • hasNext():判断是否有下一个元素
  • next():获取当前元素并后移指针
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

public class IteratorTest {
    public static void main(String[] args) {
        Collection<String> coll = new ArrayList<>();
        coll.add("张无忌");
        coll.add("玄冥二老");
        coll.add("宋青书");

        // 获取迭代器
        Iterator<String> it = coll.iterator();
        // 循环遍历
        while (it.hasNext()) {
            String name = it.next();
            System.out.println(name);
        }
    }
}

⚠️ 注意:越界调用 next() 会抛出 NoSuchElementException 异常。

方式2:增强for循环(简化迭代器)

语法简洁,本质是迭代器的语法糖,支持集合+数组:

import java.util.ArrayList;
import java.util.Collection;

public class ForEachTest {
    public static void main(String[] args) {
        Collection<String> coll = new ArrayList<>();
        coll.add("张无忌");
        coll.add("玄冥二老");
        
        // 增强for遍历
        for (String name : coll) {
            System.out.println(name);
        }
    }
}

方式3:Lambda 表达式遍历(JDK8+)

极简写法,基于函数式编程:

import java.util.ArrayList;
import java.util.Collection;

public class LambdaTest {
    public static void main(String[] args) {
        Collection<String> coll = new ArrayList<>();
        coll.add("张无忌");
        coll.add("玄冥二老");
        
        // Lambda 遍历
        coll.forEach(System.out::println);
    }
}

2.5 并发修改异常解决方案

异常场景

遍历集合时,直接增删元素会触发 ConcurrentModificationException 并发修改异常。

核心解决方案(3种)

  1. 索引for循环:删除后 i-- 修正索引
  2. 倒序索引遍历:无需修正索引
  3. 迭代器自带 remove() 方法(推荐)

⚠️ 禁止:增强for、Lambda 遍历中增删元素

实战代码

import java.util.ArrayList;
import java.util.Iterator;

public class ModifyTest {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("Java入门");
        list.add("宁夏枸杞");
        list.add("黑枸杞");

        // 方案1:迭代器删除(无异常)
        Iterator<String> it = list.iterator();
        while (it.hasNext()) {
            String s = it.next();
            if (s.contains("枸杞")) {
                it.remove(); // 迭代器原生删除
            }
        }
        System.out.println(list); // [Java入门]
    }
}

三、List 集合(Collection 子接口)

List 继承 Collection,独有索引特性,是开发中使用频率最高的集合。

3.1 List 核心特性

  • 有序:元素存储顺序与取出顺序一致
  • 可重复:允许存储重复元素
  • 有索引:支持通过索引精准操作元素

3.2 List 独有索引API

List 新增索引相关方法,Collection 无此功能:

方法名功能说明
void add(int index, E e)指定索引插入元素
E remove(int index)删除指定索引元素,返回被删元素
E set(int index, E e)修改指定索引元素,返回原元素
E get(int index)获取指定索引元素

API 实战

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

public class ListApiTest {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("张三");
        list.add("李四");

        // 索引插入
        list.add(1, "赵敏");
        System.out.println(list); // [张三, 赵敏, 李四]

        // 索引删除
        list.remove(2);
        System.out.println(list); // [张三, 赵敏]

        // 索引修改+获取
        list.set(0, "金毛狮王");
        System.out.println(list.get(0)); // 金毛狮王
    }
}

3.3 List 四种遍历方式

在 Collection 三种遍历基础上,新增普通for索引遍历

  1. 普通for(索引)
  2. 迭代器
  3. 增强for
  4. Lambda
// 普通for索引遍历(List专属)
for (int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));
}

3.4 List 两大实现类对比

1. ArrayList(数组实现)

  • 底层:动态数组
  • 特性:查询快、增删慢(首尾增删除外)
  • 场景:高频查询、少量增删(开发首选)

2. LinkedList(双链表实现)

  • 底层:双向链表
  • 特性:查询慢、首尾增删极快
  • 场景:高频首尾操作、模拟队列/栈

3.5 LinkedList 实战:队列 & 栈

利用 LinkedList 首尾操作特性,快速实现经典数据结构:

  1. 队列:先进先出(FIFO)
  2. :后进先出(LIFO)
import java.util.LinkedList;

public class LinkedListTest {
    public static void main(String[] args) {
        // 1. 模拟队列
        LinkedList<String> queue = new LinkedList<>();
        queue.addLast("A"); // 入队
        queue.addLast("B");
        System.out.println(queue.removeFirst()); // 出队 A

        // 2. 模拟栈
        LinkedList<String> stack = new LinkedList<>();
        stack.push("1"); // 压栈
        stack.push("2");
        System.out.println(stack.pop()); // 弹栈 2
    }
}

四、总结

  1. Collection:单列集合顶层接口,通用增删查+3种遍历方式
  2. List:有序可重复有索引,独有索引API,4种遍历方式
  3. 实现类选型

    • 查多改少 → ArrayList
    • 首尾增删 → LinkedList
  4. 避坑指南:遍历增删用迭代器,禁止增强for/Lambda直接修改集合