一、Java 基础

1. 注释

平时我们编写代码,在代码量少的时候,还可以看得懂自己写的代码;但是当项目结构一旦复杂起来,我们就需要用到注释。

注释并不会被执行,是给我们写代码的人看的。

书写注释是一个非常好的习惯。

平时写代码一定要注意规范。

1.1 单行注释

// 单行注释

1.2 多行注释

/* 
多行注释:
可以注释一段文字
*/

1.3 文档注释

JavaDoc 文档注释:

/**
* @author zhangzhicai
* @version 1.0
* @description: TODO
* @date 2026/10/16 21:45
*/

1.4 有趣的代码注释

/***
* _ooOoo_
* o8888888o
* 88 " . " 88
* (| -_- |)
* O\ = /O
* ____/`---'\____
* . ' \\| |// `.
* / \\||| : |||// \
* / _||||| -:- |||||- \
* | | \\\ - /// | |
* | \_| ''\---/'' | |
* \ .-\__ `-` ___/-. /
* ___`. .' /--.--\ `. . __
* ."" '< `.___\_<|>_/___.' >'"".
* | | : `- \`.; `\ _ /`;.`/ - ` : | |
* \ \ `-. \_ __\ /__ _/ .-` / /
* ==== == `-.____`-.___\__ ___/___.-`____.-'======
* `=---='
*
* .............................................
* 佛祖保佑 永无 BUG
*/
/***
* 佛曰:
* 写字楼里写字间,写字间里程序员;
* 程序人员写程序,又拿程序换酒钱。
* 酒醒只在网上坐,酒醉还来网下眠;
* 酒醉酒醒日复日,网上网下年复年。
* 但愿老死电脑间,不愿鞠躬老板前;
* 奔驰宝马贵者趣,公交自行程序员。
* 别人笑我忒疯癫,我笑自己命太贱;
* 不见满街漂亮妹,哪个归得程序员?
*/
/***
* ___====-_ _-====___
* _--^^^#####// \\#####^^^--_
* _-^##########// ( ) \\##########^-_
* -############// |\^^/| \\############-
* _/############// (@::@) \\############\_
* /#############(( \\// ))#############\
* -###############\\ (oo) //###############-
* -#################\\ / VV \ //#################-
* -###################\\/ \//###################-
* _#/|##########/\######( /\ )######/\##########|\#_
* |/ |#/\#/\#/\/ \#/\##\ | | /##/\#/ \/\#/\#/\#| \|
* ` |/ V V ` V \#\| | | |/#/ V ' V V \| '
* ` ` ` ` / | | | | \ ' ' ' '
* ( | | | | )
* __\ | | | | /__
* (vvv(VVV)(VVV)vvv)
* 神兽保佑
* 代码无 BUG!
*/
/***
* http://www.flvcd.com/
* .--, .--,
* ( ( \.---./ ) )
* '.__/o o\__.'
* {= ^ =}
* > - <
* / \
* // \\
* //| . |\\
* "'\ /'"_.-~^`'-.
* \ _ /--' `
* ___)( )(___
* (((__) (__))) 高山仰止, 景行行止.虽不能至, 心向往之。
*/
/***
* 頂頂頂頂頂頂頂頂頂 頂頂頂頂頂頂頂頂頂
* 頂頂頂頂頂頂頂     頂頂     
*    頂頂   頂頂頂頂頂頂頂頂頂頂頂
*    頂頂   頂頂頂頂頂頂頂頂頂頂頂
*    頂頂   頂頂       頂頂
*    頂頂   頂頂  頂頂頂  頂頂
*    頂頂   頂頂  頂頂頂  頂頂
*    頂頂   頂頂  頂頂頂  頂頂
*    頂頂   頂頂  頂頂頂  頂頂
*    頂頂       頂頂頂 
*    頂頂      頂頂 頂頂 頂頂
*  頂頂頂頂   頂頂頂頂頂 頂頂頂頂頂
*  頂頂頂頂   頂頂頂頂   頂頂頂頂
*/
/***
* ┌───┐ ┌───┬───┬───┬───┐ ┌───┬───┬───┬───┐ ┌───┬───┬───┬───┐ ┌───┬───┬───┐
* │Esc│ │ F1│ F2│ F3│ F4│ │ F5│ F6│ F7│ F8│ │ F9│F10│F11│F12│ │P/S│S L│P/B│ ┌┐ ┌┐ ┌┐
* └───┘ └───┴───┴───┴───┘ └───┴───┴───┴───┘ └───┴───┴───┴───┘ └───┴───┴───┘ └┘ └┘ └┘
* ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───────┐ ┌───┬───┬───┐ ┌───┬───┬───┬───┐
* │~ `│! 1│@ 2│# 3│$ 4│% 5│^ 6│& 7│* 8│( 9│) 0│_ -│+ = │ BacSp │ │Ins│Hom│PUp│ │N L│ / │ * │ - │
* ├───┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─────┤ ├───┼───┼───┤ ├───┼───┼───┼───┤
* │ Tab │ Q │ W │ E │ R │ T │ Y │ U │ I │ O │ P │{ [│} ] │ | \ │ │Del│End│PDn│ │ 7 │ 8 │ 9 │ │
* ├─────┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴┬──┴─────┤ └───┴───┴───┘ ├───┼───┼───┤ + │
* │ Caps │ A │ S │ D │ F │ G │ H │ J │ K │ L │: ; │ " '│ Enter │ │ 4 │ 5 │ 6 │ │
* ├──────┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴─┬─┴────────┤ ┌───┐ ├───┼───┼───┼───┤
* │ Shift │ Z │ X │ C │ V │ B │ N │ M │ < ,│> .│? /│ Shift │ │ ↑ │ │ 1 │ 2 │ 3 │ │
* ├─────┬──┴─┬─┴──┬┴───┴───┴───┴───┴───┴──┬┴───┼───┴┬────┬────┤ ┌───┼───┼───┐ ├───┴───┼───┤ E││
* │ Ctrl│ │Alt │ Space │ Alt│ │ │Ctrl│ │ ← │ ↓ │ → │ │ 0 │ . │←─┘│
* └─────┴────┴────┴───────────────────────┴────┴────┴────┴────┘ └───┴───┴───┘ └───────┴───┴───┘
*/

2. 关键字

Java关键字 是 Java 中预先定义的、具有特殊含义的 单词。这些单词不能用作变量名、方法名、类名等标识符,因为它们已经被编译器用于控制程序的结构、数据类型、访问权限、异常处理等功能。

常见的 Java 关键字包括:

  • 基本数据类型:int, double, boolean, char 等
  • 控制流程:if, else, for, while, switch, case, break, continue
  • 面向对象:class, interface, extends, implements, abstract, final, static, new, this, super
  • 访问修饰符:public, protected, private
  • 异常处理:try, catch, finally, throw, throws
  • 其他:return, void, import, package, enum, synchronized, volatile, transient, native

true、false、null 虽然在语法上看似关键字,但严格来说是字面量(literal /ˈlɪtərəl/),不过同样不能用作标识符

3. 保留字

定义:Java 语言中预留的、目前尚未使用的单词,留作未来扩展用。

特点:也不能 用作标识符,但当前没有实际功能。

例子:goto、const(这两个在 C/C++中是关键字,Java 保留了但未使用)。

4. 标识符

标识符(Identifier) 是程序中用来给变量、方法、类、接口、包等元素起名字的符号。简单说,就是你自定义的名字。

标识符的命名规则(必须遵守)

  1. 组成字符:只能包含字母(A-Z, a-z)、数字(0-9)、下划线(_)和美元符号($)。
  2. 首字符限制:不能以数字开头。
  3. 大小写敏感:myVar 和 MyVar 是两个不同的标识符。
  4. 无长度限制,但建议简洁有意义。
  5. 不能是关键字或保留字(如 class, if, int 等)。

可以使用中文命名,但是一般不建议这样使用,也不建议使用拼音,很 low

public static void main(String[] args) {
String 游戏 = "王者荣耀";
System.out.println(游戏); // low
}

合法与非法示例

合法 非法
myVariable 123abc(数字开头)
_count my-var(连字符不允许)
$value class(关键字)

命名惯例(非强制,但推荐)

  • 类名:大驼峰(PascalCase),如 StudentInfo
  • 方法名、变量名:小驼峰(camelCase),如 getAge()、studentName
  • 常量名:全大写加下划线,如 MAX_SIZE
  • 包名:全小写,如 com.example.utils

阿里规范

image-20261017122120439

image-20261017122535614

Snipaste_2026-10-17_12-25-42

image-20261017122341233

image-20261017122431035

image-20261017122448115

5. 数据类型

5.1 基本数据类型(primitive type)

整型: byte, short, int, long 1 字节,2 字节,4 字节,8 字节

浮点: float, double 4 字节,8 字节

字符: char 2 字节

布尔: boolean 1 字节

image-20261017130719978


什么是字节?

位(bit): 是计算机内部 数据存储 的最小单位,11001100 是一个 8 位二进制数

字节: 是计算机中 数据处理 的最小单位,习惯上用 B 来表示

1B(byte, 字节)= 8bit(位)

字符: 是计算机中使用的字母、数字、字、符号

image-20261017125329395

电脑中的 32 位和 64 位是什么?

32 位和 64 位指的是 CPU(中央处理器)一次能处理的数据最大长度,就像一条马路一次能并排通过多少辆车。

  1. 核心区别:一次能处理多少数据
  • 32 位:CPU 一次能处理 32 位(4 字节) 数据。就像一条只有 4 车道的路,一次最多并行通过 4 辆车。
  • 64 位:CPU 一次能处理 64 位(8 字节) 数据。相当于一条 8 车道的路,一次能并行通过 8 辆车,通行能力翻倍。
  1. 最重要的实际影响:内存支持上限

这是对普通用户最直观的区别:

  • 32 位操作系统:最大只能使用 约 4GB 内存(实际可用通常只有 3.25GB 左右)。因为 32 位地址总线最多能寻址 $2^{32}$ 个内存地址,换算过来就是 4GB。
  • 64 位操作系统:理论上最大支持 16EB(Exabyte,艾字节) 内存,目前主流消费级主板和 Windows 系统支持 128GB~2TB 不等。所以如果你的电脑内存大于 4GB,必须装 64 位系统才能全部用上。
  1. 性能和软件兼容性
  • 性能:64 位 CPU 在处理大量数据(如视频渲染、大型游戏、科学计算)时明显更快,因为一次能搬运更多数据。但对于日常办公、看网页,差距不大。
  • 软件兼容:
    • 64 位系统 可以同时运行 64 位软件 和 32 位软件(通过模拟兼容模式)。
    • 32 位系统 只能运行 32 位软件,无法运行 64 位软件。

5.2 引用数据类型(reference type)

类,接口,数组

5.3 强类型语言

强类型语言 是指一种编程语言,它对变量的数据类型有严格的约束:每个变量在使用前必须明确声明其类型(或由编译器推断出确定的类型)

要求变量的使用要严格符合规定,所有变量必须先定义后才能使用。常见的强类型语言有:Java

5.4 弱类型语言

弱类型语言 是指对数据类型约束较为宽松的语言,它允许不同类型之间进行大量的隐式转换,甚至在不相关的类型之间也能自动转换,而不会报错或抛出异常。

如:JavaScript

5.5 进制

int i = 10; // 十进制
int i2 = 010; // 八进制
int i3 = 0x10; // 十六进制 0-9 A-F

5.6 类型转换

强制类型转换:(类型)变量名 高-> 低

自动类型转换:低-> 高

byte, short-> int

char-> int

  • 布尔类型 boolean 不能转换

  • 转换的时候可能存在内存溢出,或者精度问题

5.7 字符编码

  • ASCII 码:最早的字符编码,只包含英文字母、数字、标点和控制字符,共 128($2^7)$ 个字符,用 7 位(实际存为 1 字节)表示。

  • Unicode:一个庞大的 字符集,为世界上几乎所有的字符分配了一个唯一的数字编号(码点),范围从 U+0000 到 U+10FFFF,包含中文、日文、emoji 等。但它只规定了编号,没规定如何存储。

  • UTF-8:是 Unicode 的一种 具体存储/编码方式,用 1 到 4 个字节变长表示一个字符。它向后兼容 ASCII(前 128 个字符用 1 字节表示,与 ASCII 完全相同),同时能高效表示其他字符。

UTF-8 编码下的字节数

英文字母:占 1 字节

中文汉字:占 3 字节

  • GBK 是中国国家标准的中文编码扩展,全称《汉字内码扩展规范》,主要用于简体中文环境。

GBK 编码下的字节数

  • 英文字母(ASCII):占 1 字节(与 ASCII 完全兼容)。
  • 中文字符(含繁体):占 2 字节。

6. 变量

变量是什么:就是可以变化的量。

Java 是一种强类型语言,每个变量都必须声明其类型。

Java 变量是程序中最基本的存储单元,其要素包括:变量名,变量类型,作用域。

type varName [=value][{,varName[=value]}];
// 数据类型 变量名 = 值; 可以使用逗号来隔开来声明多个通类型变量
int a=1,b=2,c=3; // √
int a,b,c;// √
int a=1,b,c;// √
int a,int b; // x

注意事项:

  1. 每个变量都有类型,可以是基本类型,也可以是引用类型。
  2. 变量名必须是合法的标识符。
  3. 变量声明是一条完整的语句,因此每一个声明都必须一分号结束。

变量作用域

类变量,实例变量,局部变量

public class Variable {
static int allClicks = 0; // 类变量
String str = "hello world"; // 实例变量
public void method() {
int i = 0; // 局部变量:必须声明和初始化值
}
}

实例变量:从属于对象,如果不自行初始化,默认 0 或 0.0

布尔类型默认 false

除了基本类型,其余的默认值都是 null

局部变量:必须声明和初始化值

常量

常量:初始化之后不能再改变值!不会变动的值。在程序运行过程中不允许被改变。

final 常量名=值;
final double PI = 3.14;

常量名一般使用大写字符

阿里规范:

image-20261017135530512

变量的命名规范

  • 所有变量、方法、类名:见名知意
  • 类成员变量:首字母小写和驼峰原则:monthSalary
  • 局部变量:首字母小写和驼峰原则
  • 常量:大写字母和下划线:MAX_VALUE
  • 类名:首字母大写和驼峰原则:Man, GoodMan
  • 方法名:首字母小写和驼峰原则:run(), sendMsg()

7.运算符

算术运算符:+,-,*,/,%,++,–

赋值运算符:=

关系运算符:>, <,> =,<=,==,!=, instanceof

逻辑运算符:&&,||,!

三元运算符:? :

扩展赋值运算符:+=,-=,*=,/=

位运算符:&,|,^,~,>>, <<,> >>

// 自增自减运算符
int a = 3;
int b = a++; // b = 3; a++, 先赋值,再自增
int c = ++a; // c = 4; ++a, 先自增,再赋值

幂运算:Math.pow(a, b) => $a^b$

求根:Math.sqrt(2) => $\sqrt{2}$

使用 LaTeX 数学公式(推荐,需要渲染器支持)

许多 Markdown 编辑器(如 Typora、Obsidian、GitHub、VSCode 等)支持通过 $...$ 或 $$...$$ 嵌入 LaTeX 公式。

  • 平方根:$\sqrt{x}$ → $\sqrt{x}$
  • n 次方根:$\sqrt[n]{x}$ → $\sqrt[n]{x}$

位运算

<< *2

>> /2

研究位运算:《计算机组成原理》

8. 包机制

为了更好的组织类,Java 提高了包机制,用于区分类名的命名空间。

包语句语法格式:

package pkg1[.pkg2[.pkg3...]];
  • 一般利用公司域名倒置作为包名;

  • 为了能够使用某一个包的成员,我们需要在 Java 程序中明确导入该包。使用 “import” 语句

    import packge1[.packge2...].(classname|*);

9. JavaDoc

javadoc 是用来生成自己的 API 文档的

参数信息:

  • @author 作者名
  • @version 版本号
  • @since 指明需要最早使用的 jdk 版本
  • @param 参数名
  • @return 返回值情况
  • @throws 异常抛出情况

命令行形式

javadoc -encoding UTF-8 -charset UTF-8 -docencoding UTF-8 -d 输出目录 DocTest.java

image-20261017144155323

IDEA 中生成 Javadoc

image-20261017143646254

image-20261017142553393

Command line arguments:

-J-Dstdout.encoding=UTF-8 -J-Dstderr.encoding=UTF-8 -encoding UTF-8 -charset UTF-8 -docencoding UTF-8
  1. -encoding UTF-8
  • 作用:告诉 javadoc 工具,源代码文件(.java) 是用什么编码保存的。
  • 为什么重要:如果你的 .java 文件中有中文注释,而文件实际是用 UTF-8 保存的,但 javadoc 默认按系统编码(Windows 下是 GBK)去读,就会读到乱码,进而报错或生成乱码文档。
  • 示例:-encoding UTF-8 表示“请用 UTF-8 解读我的源码”。
  1. -charset UTF-8
  • 作用:指定 生成的 HTML 文档 所使用的字符集(写在 <meta charset="UTF-8"> 中)。
  • 为什么重要:浏览器打开文档时,会根据这个声明的字符集来解码页面。如果不指定,可能默认使用 ISO-8859-1 等,导致中文显示乱码。
  • 示例:生成的 index.html 头部会包含 <meta charset="UTF-8">。
  1. -docencoding UTF-8
  • 作用:指定生成的 HTML 文件 实际保存时的编码。
  • 与 -charset 的关系:通常两者设为相同值(UTF-8),-docencoding 负责文件存储,-charset 负责声明。如果只设 -charset 不设 -docencoding,文件可能以其他编码保存,导致声明与实际不符。
  • 示例:生成的 .html 文件以 UTF-8 字节写入磁盘。
  1. -J-Dstdout.encoding=UTF-8 和 -J-Dstderr.encoding=UTF-8
  • -J 前缀:表示后面的参数要传递给 javadoc 底层运行的 JVM,而不是 javadoc 工具本身。
  • -Dstdout.encoding=UTF-8:强制 JVM 的标准输出流(stdout)使用 UTF-8 编码输出字符。
  • -Dstderr.encoding=UTF-8:强制标准错误流(stderr)使用 UTF-8 编码。
  • 为什么需要:JDK 18+ 之后,控制台输出的编码不再随 file.encoding 变化,而是由 stdout.encoding 和 stderr.encoding 独立控制。如果不加,Windows 下默认用 GBK 输出日志,而 IDEA 控制台按 UTF-8 解码,导致乱码。加上这两个参数后,javadoc 打印的进度、错误信息都以 UTF-8 输出,与 IDEA 控制台一致,乱码消失。

二、locale(区域设置)参数

在 Generate JavaDoc 对话框中,有一个 Locale 输入框(你截图里填的是 zh_CN)。

作用

  • 控制生成的 HTML 文档的导航栏、标题、月份名称等界面文字的语言。
  • 例如,文档顶部的“Overview”、“Package”、“Class”等文字,以及“Since”、“Version”等标签的显示语言。

常用值

Locale 值 效果
zh_CN 导航栏显示中文(如“概述”、“包”、“类”)
en_US 导航栏显示英文(默认)
ja_JP 日语
留空 使用系统默认语言(通常是英文)

生成结果:

image-20261017144302756


二、Java 流程控制

1. 用户交互 Scanner

Scanner 对象:用了获取用户的输入,java.util.Scanner 是 java5 新特征。

基本语法:

next()方式

package com.it.day01;

import java.util.Scanner;

/**
* @author zhangzhicai
* @version 1.0
* @description: TODO
* @date 2026/10/17 15:51
*/
public class ScannerDemo {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.println("使用next方式接收:");
if (scanner.hasNext()) {
String s = scanner.next();
System.out.println("接收的内容:" + s);
}
// 凡是 IO 流的类,不关闭就会一直占用资源,要养成好习惯,用完就关掉
scanner.close();
}
}

运行结果:

Snipaste_2026-10-17_15-57-42

nextLine()方式

package com.it.day01;

import java.util.Scanner;

/**
* @author zhangzhicai
* @version 1.0
* @description: TODO
* @date 2026/10/17 15:51
*/
public class ScannerDemo {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("使用nextLine()方式接收:");
if (sc.hasNextLine()) {
String s = sc.nextLine();
System.out.println("接收的内容:" + s);
}
sc.close();
}
}

运行结果:

image-20261017160108067

next()和 nextLine()区别

image-20261017160215533

其他

Scanner sc = new Scanner(System.in);
int i = sc.nextInt();
double d = sc.nextDouble();
long l = sc.nextLong();
float f = sc.nextFloat();
BigDecimal bigDecimal = sc.nextBigDecimal();

hasNextxxx(), nextxxx()相互配合

实战案例:

package com.it.day01;

import java.util.Scanner;

/**
* @author zhangzhicai
* @version 1.0
* @description: TODO
* @date 2026/10/17 15:51
*/
public class ScannerDemo {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
// 输入多个数字,并求其总和与平均数,每输入一个数字用回车确认,通过输入非数字来结束输入并输出执行结果
System.out.println("输入多个数字,以非数字结束:");
double sum = 0;
int count = 0;
while (scanner.hasNextDouble()) {
double d = scanner.nextDouble();
sum += d;
count++;
}
System.out.println("总和:" + sum);
double avg = sum / count;
System.out.println("平均数:" + avg);
scanner.close();
}
}

运行结果:

image-20261017163154096

2. 顺序结构

Java 的基本结构就是顺序结构,除非特别指明,否则就按照顺序一句一句执行。

顺序结构是最简单的算法结构。

image-20261017163458868

语句与语句之间,框与框之间是按从上到下的顺序进行的,它是由若干个依次执行的处理步骤组成的,它是任何一个算法都离不开的一种基本算法结构。

3. 选择结构

if 单选择

if (布尔表达式) {
// 如果布尔表达式为 true 将执行的语句
}

if 双选择

if (布尔表达式) {
// 如果布尔表达式为 true 将执行的语句
} else {
// 如果布尔表达式为 false 将执行的语句
}

if 多选择

if (布尔表达式1) {
// 如果布尔表达式 1 为 true 将执行的语句
} else if (布尔表达式2){
// 如果布尔表达式 2 为 true 将执行的语句
} else if (布尔表达式3){
// 如果布尔表达式 3 为 true 将执行的语句
} else {
// 如果以上布尔表达式都不为 true 则的语句
}

嵌套 if

if (布尔表达式1) {
// 如果布尔表达式 1 为 true 将执行的语句
if (布尔表达式2) {
// 如果布尔表达式 2 为 true 将执行的语句
}
}

switch 多选择结构

多选择结构还有一个实现方式就是 switch case 语句

switch case 语句判断一个变量与一系列值中的某个值是否相等,每个值称为一个分支。

switch(expression) {
case value1:
// 语句
break; // 可选
case value2:
// 语句
break; // 可选
default: // 可选
// 语句
}

switch 语句中的变量类型可以是:

  • byte, short, int, char 及其包装类
  • enum 枚举类型
  • 从 JavaSE7 开始, 支持 String 类型了

case 标签的要求

  • 必须是编译时常量(compile-time constant),包括:

    • 字面量:1、'A'、"hello"
  • final 常量:final int MONDAY = 1;

    • 枚举常量:Day.MONDAY
  • 字符串常量(final 且初始化值为字面量或常量表达式)

枚举

package com.it.day01;

/**
* @author zhangzhicai
* @version 1.0
* @description: TODO
* @date 2026/10/17 16:57
*/
public class SwitchEnumDemo {
public static void main(String[] args) {
Day day = Day.MONDAY;
switch (day) {
case MONDAY:
System.out.println("周一");
break;
case TUESDAY:
System.out.println("周二");
break;
// ...
}
}
}

enum Day { MONDAY, TUESDAY, WEDNESDAY }

字符串

package com.it.day01;

import java.util.Scanner;

/**
* @author zhangzhicai
* @version 1.0
* @description: TODO
* @date 2026/10/17 16:48
*/
public class SwitchDemo {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String day = "1";
if(sc.hasNext()) {
day = sc.next();
}
switch (day) {
case "1":
System.out.println("周一");
break;
case "2":
System.out.println("周二");
break;
case "3":
System.out.println("周三");
break;
case "4":
System.out.println("周四");
break;
case "5":
System.out.println("周五");
break;
case "6":
System.out.println("周六");
break;
case "7":
System.out.println("周日");
break;
default:
System.out.println("输入有误!");
break;
}
}
}

新特性

  • JDK 14 引入 switch (obj) { case ... -> ... } 这种箭头写法
  • JDK 21 正式使用 类型模式 case String s / case Integer i/ case null
  • JDK 21 正式使用 when 守卫条件 case Integer i when i > 0 ->
Object obj = getSomeObject();
String description = switch (obj) {
case null -> "空值";
case String s -> ...
case Integer i when i > 0 -> ...
case Integer i -> ...
case Point(int x, int y) -> ...
default -> ...
};

用了箭头 -> 之后:

  • 不需要写 break:箭头分支执行完后自动结束,不会继续执行后续分支。
  • 不会穿透(fall-through):传统 case: 不加 break 会继续执行下一个 case 的代码,而箭头语法天然避免了这种行为。

示例对比

// 传统冒号语法(需 break,否则穿透)
switch (x) {
case 1:
System.out.println("one");
// 忘记 break → 会继续输出 "two"
case 2:
System.out.println("two");
break;
}

// 箭头语法(无需 break,无穿透)
switch (x) {
case 1 -> System.out.println("one"); // 执行完立即退出
case 2 -> System.out.println("two");
}

4. 循环结构

while 循环

while (布尔表达式) {
// 循环内容
}
  • 只要布尔表达式为 true,循环就会一直执行下去
  • 我们大多数情况是会让循环停止下来的,我们需要一个让表达式失效的方式来结束循环
  • 少部分情况需要循环一直执行,比如服务器的请求响应监听等
  • 循环条件一直为 true 就会造成无限循环(死循环),我们正常的业务编程中一个尽量避免死循环。会影响程序性能或者造成程序卡死崩溃!

示例:计算 1+2+3+…+100 =?

public class WhileDemo {
public static void main(String[] args) {
int i = 1;
int sum = 0;
while (i <= 100) {
sum += i;
i++;
}
System.out.println("sum="+sum);
}
}

运行结果:

sum = 5050

do…while 循环

对于 while 语句而言,如果不满足条件,则不能进入循环。但有时候我们需要及时不满足条件,也至少执行一次。

do…while 至少执行一次

do {
// 代码语句
} while(布尔表达式)

while 和 do…while 区别:

  1. while 先判断后执行,do…while 先执行后判断

  2. do…while 总是保证循环体至少被执行一次!

for 循环

for 循环语句是支持迭代的一种通用结构,是最有效、最灵活的循环结构

for 循环执行的次数是在执行前就确定的。

语法格式:

for(初始化;布尔表达式;更新) {
// 代码语句
}

示例:

  1. 计算 0 到 100 之间的奇数(odd)和偶数(even)的和
public static void main(String[] args) {
// 1. 计算 0 到 100 之间的奇数和偶数的和
int oddSum = 0; // 奇数和
int evenSum = 0; // 偶数和
for (int i = 1; i <= 100; i++) {
// 能被 2 整除的数是偶数
if (i % 2 == 0) {
evenSum += i;
} else { // 反之是偶数
oddSum += i;
}
}
System.out.println("oddSum="+ oddSum);
System.out.println("evenSum="+ evenSum);
}

运行结果:

oddSum = 2500
evenSum = 2550

  1. 用 while 或 for 循环输出 1-1000 之间能被 5 整除的数,并且每行输出 3 个。
// 方法一
public static void main(String[] args) {
// 2. 用 while 或 for 循环输出 1-1000 之间能被 5 整除的数,并且每行输出 3 个。
for (int i = 1; i <= 1000; i++) {
// 判断 i 是否能被 5 整除的数
if (i % 5 == 0) {
System.out.print(i + " ");
}
//每行输出 3 个
if (i % (5 * 3) == 0) System.out.println();
}
}

// 方法二
public static void main(String[] args) {
// 2. 用 while 或 for 循环输出 1-1000 之间能被 5 整除的数,并且每行输出 3 个。
int count = 0;
for (int i = 1; i <= 1000; i++) {
// 判断 i 是否能被 5 整除的数
if (i % 5 == 0) {
System.out.print(i + " ");
count++;
//每行输出 3 个
if (count % 3 == 0) System.out.println();
}
}
System.out.println();
System.out.println("1-1000 之间能被 5 整除的数总共有:" + count + "个");
}
  1. 打印九九乘法表

image-20261017174853640

public class ForDemo4 {
public static void main(String[] args) {
// 打印九九乘法表
for (int i = 1; i <= 9; i++) { // 外层循环控制行数
for (int j = 1; j <= i; j++) { // 内层循环控制列数
System.out.print(j + "x" + i + "=" + i * j+"\t");
}
System.out.println();
}
// 第一行 i = 1, j 从 1 迭代到 1
// 第二行 i = 2, j 从 1 迭代 2
// 第三行 i = 3, j 从 1 迭代 3
// ...
// 第九行 i = 9, j 从 1 迭代 9
}
}

运行结果:

image-20261017175801044

增强 for 循环

在 java5 引入了主要用于数组或集合的增强 for 循环

语法格式:

for(声明语句 : 表达式) {
// 代码语句
}

声明语句:声明新的局部变量,该变量的类型必须和数组元素(或集合元素)的类型匹配。其作用域限定在循环代码块,其值与此时数组或集合元素的值相等。

表达式:表达式是要访问的数组名或集合名,或者是返回值为数组或集合的方法。

public class ForDemo5 {
public static void main(String[] args) {
int[] numbers = {10, 20, 30, 40, 50};
for (int n : numbers) {
System.out.println(n);
}

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
for (Integer i : list) {
System.out.println(i);
}
}
}

IDEA 快捷代码:

  • 10.fori -> for(int i = 0; i < 10; i++){}

  • itit -> 增强 for

5. break, continue, goto

break 在任何循环语句的主体部分,均可以用 break 控制循环的流程。break 用于强行退出循环,不执行循环中剩余的语句(break 也在 swtich 语句中使用)

continue 语句用在循环语句体中,用于终止某次循环过程,即跳过循环体中尚未执行的语句,接着进行下一次是否执行循环的判断。

goto:

  • goto 在 Java 中是一个保留字

标签使用【了解】

image-20261017184333039

示例:要打印 101 到 150 之间的所有质数(质数是大于 1 的自然数,只能被 1 和自身整除。)

求质数的数学原理:

image-20261017184049257

public static void main(String[] args) {
for (int num = 101; num <= 150; num++) {
boolean isPrime = true;
for (int i = 2; i <= Math.sqrt(num); i++) { // 从 2 开始到 √n
if (num % i == 0) {
isPrime = false; // 如果能被任意一个 i 整除, num 就不是质数
break; // 退出内层循环
}
}
if (isPrime) { // 是质数就输出
System.out.print(num +" ");
}
}
}

运行结果:

101 103 107 109 113 127 131 137 139 149

6. 练习

打印三角形

public static void main(String[] args) {
//打印三角形
for (int i = 1; i <= 5; i++) {
for (int j = 5; j >= i; j--) {
System.out.print(" ");
}
for (int j = 1; j <= i; j++) {
System.out.print("*");
}
for (int j = 1; j < i; j++) {
System.out.print("*");
}
System.out.println();
}
}

运行结果:

image-20261017185518814


三、Java 方法

1. 方法

Java 方法时语句的集合,他们在一起执行一个功能。

  • 方法是解决一类问题的步骤的有序组合
  • 方法包含与类或对象中
  • 方法在程序中被创建,在其他地方被引用。

设计方法的原则:方法的本意是功能块,就是实现某个功能的语句块的集合。我们设计方法的时候,最好保持方法的原子性,就是一个方法只完成一个功能,这样利于我们后期的扩展

// 加法
public static int add(int a,int b) {
return a+b;
}

2. 方法的定义

Java 的方法类似于其他语言的函数,是一段 用来完成特点功能的代码片段,一般情况下,定义一个方法包含以下语法:

方法包含一个方法头和方法体。 下面是一个方法的所有部分:

  1. 修饰符:修饰符,这是可选的,告诉编译器如何调用该方法。定义类该方法的访问类型。

  2. 返回值类型:方法可能 UI 返回值。returnValueType 是方法返回值的数据类型。有些方法执行所需的操作,但没有返回值。在这种情况下,returnValueType 是关键字 void。

  3. 方法名:是方法的实际名称。方法名和参数列表共同构成方法签名。

  4. 参数类型:参数像是一个占位符。当方法被调用时,传递值给参数。这个值被称为实参或变量。参数列表是指方法的参数类型、顺序和参数的个数。参数是可选的,方法可以不包含任何参数。

    • 形式参数:在方法被调用时用于接收外界输入的数据。如 public int add(int a,int b);
    • 实参:调用方法时实际传给方法的数据。如 int sum = add(1,2);
  5. 方法体:方法体包含具体的语句,定义该方法的功能。

语法格式:

修饰符 返回值类型 方法名(参数类型 参数名) {
...
方法体
...
return 返回值;
}

3. 方法调用

调用方法:对象名.方法名(实参列表)

Java 支持两种调用方法的方式,根据方法是否返回值来选择。

  1. 当方法返回一个值的时候,方法调用通常被当做一个值。例如:
int larger = max(30, 40);
  1. 如果方法返回值是 void,方法调用一定是一条语句.
System.out.println("Hello world");

拓展:值传递和引用传递

4. 方法的重载

方法重载就是在一个类中,有相同的方法名称,但形式不同的方法。

方法重载的规则:

  1. 方法名必须相同。
  2. 参数列表必须不同(个数不同,或类型不同,参数顺序不同)
  3. 方法的返回类型可以相同也可以不同。仅仅返回类型不同不足以成为方法的重载。

实现理论:

方法名称相同时,编译器会根据调用方法的参数个数、参数类型等去逐个匹配,以选择对应的方法,如果匹配失败,则编译器报错。

5. 命令行传递参数

有时候你希望运行一个程序时候再传递给它消息。这要靠传递命令行参数给 main()函数实现。

public class CommandLine {
public static void main(String[] args) {
for (int i = 0; i < args.length; i++) {
System.out.println("args[" + i+"]: "+ args[i]);
}
}
}

IDEA 中配置命令行参数:

image-20261017192418355

cmd 命令行方式:

java com.it.day01.CommandLine 1 2 3 4 5

image-20261017192914379

6. 可变参数

JDK1.5 开始,Java 支持传递同类型的可变参数给一个方法。

在方法声明中,在指定参数类型后加一个省略号(...)。

一个方法中只能指定一个可变参数,它必须是方法的最后一个参数。 任何普通的参数必须在它之前声明。

package com.it.day01;

/**
* @author zhangzhicai
* @version 1.0
* @description: TODO
* @date 2026/10/17 19:32
*/
public class VariableParametersDemo {
public static void main(String[] args) {
test(3.14,1,2,3,4,5);
}

/**
* 测试可变参数
* @param d
* @param numbers
*/
public static void test(double d, int... numbers) {
System.out.println("d="+d);
if (numbers.length == 0) {
System.out.println("没有输入可变参数...");
return;
}
int max = numbers[0];
for (int i = 1; i < numbers.length; i++) {
if (i > max) {
max = numbers[i];
}
}
System.out.println("可变参数最大值:"+max);
}
}

运行结果:

d = 3.14
可变参数最大值:5

7. 递归

recursion /rɪˈkɜːʃn/ n.(数学、计算机科学)递归,循环(函数、过程或定义直接或间接引用自身);(数学)递归式,递推式

A 方法调用 B 方法,我们很容易理解。

递归就是:A 方法调用 A 方法!即自己调用自己。

利用递归可以用简单的程序来解决一些复杂的问题。它通常把一个大型复杂的问题层层转化为一个与原向题相似的规模较小的向题来求解,递归策略只需少量的程序就可描述出解题过程所需要的多次重复计算,大大地减少了程序的代码量。递归的能力在于用有限的语句来定义对象的无限集合。

递归结构包括两个部分:

递归头:什么时候不调用自身方法。如果没有头,将陷入死循环。

递归体:什么时候需要调用自身方法。

实例:求 5 的阶乘 5!

public class RecursionDemo {
public static void main(String[] args) {
// 求 5 的阶乘 5!
System.out.println(f(5));
}

/**
* 求 5!
* 5!= 5 *4* 3 *2* 1
* = 5*4!
* = 5* 4*3!
* = 5* 4 *3* 2!
* = 5 *4* 3 *2* 1!
* = 5 *4* 3 *2* 1
* = n*(n-1)!
*
* @param n
* @return
*/
public static int f(int n) {
if (n == 1) return 1;
return n * f(n - 1);
}
}

运行结果:

120

8. 作业

写一个计算器,要求实现加减乘除功能,并且能够循环接收新的数据,通过用户交互实现

calculator /ˈkælkjuleɪtə(r)/ n.计算器

思路推荐:

  1. 写 4 个方法:加减乘除(add /æd/, subtract /səbˈtrækt/, multiply /ˈmʌltɪplaɪ/, divide /dɪˈvaɪd/)
  2. 利用循环和 switch 进行用户交互
  3. 传递需要操作的两个数
  4. 输出结果
package com.it.day01;

import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* @author zhangzhicai
* @version 1.0
* @description: TODO
* @date 2026/10/17 19:51
*/
public class CalculatorDemo2 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("计算器使用方法:\n如:1+1(q退出)");
String expr = sc.nextLine().trim();
if ("q".equalsIgnoreCase(expr)) {
System.out.println("退出计算器...");
sc.close();
break;
}
Pattern pattern = Pattern.compile("(\\d+(?:\\.\\d+)?)\\s*([+\\-*/])\\s*(\\d+(?:\\.\\d+)?)");
Matcher matcher = pattern.matcher(expr);
if (!matcher.matches()) {
System.out.println("输入格式错误!");
continue;
}
try {
String op = matcher.group(2);
double a = Double.parseDouble(matcher.group(1));
double b = Double.parseDouble(matcher.group(3));
expr = a + op + b;
double result;
switch (op) {
case "+" -> result = add(a, b);
case "-" -> result = subtract(a, b);
case "*" -> result = multiply(a, b);
case "/" -> result = divide(a, b);
//防御性编程(防患于未然)假如未来你修改了正则,比如增加了取模运算符 %
//忘记在 switch 中添加 case "%"
default -> throw new IllegalArgumentException("不支持的运算符: " + op);
}
System.out.printf("%s = %.2f%n", expr, result);
} catch (ArithmeticException e) {
System.out.println(e.getMessage());
} catch (IllegalArgumentException e) {
System.out.println(e.getMessage());
}
}
sc.close();
}

public static double add(double a, double b) {
return a + b;
}

public static double subtract(double a, double b) {
return a - b;
}

public static double multiply(double a, double b) {
return a * b;
}

public static double divide(double a, double b) {
if (b == 0) {
throw new ArithmeticException("除数不能为0");
}
return a / b;
}


}

使用了正则表达式

在这个计算器项目中,正则表达式经历了从 基础版 到 增强版 的演进,核心作用是 校验输入格式 并 提取计算要素。以下是该项目中正则表达式使用的全面总结:

1. 核心演进对比

维度 基础版正则 增强版正则
表达式 (\\d+)\\s*([+\\-*/])\\s*(\\d+) (\\d+(?:\\.\\d+)?)\\s*([+\\-*/])\\s*(\\d+(?:\\.\\d+)?)
数字匹配 仅支持整数(如 1) 支持整数和小数(如 1.5)
小数校验 不支持 强制小数点后至少有 1 位数字
适用场景 简单整数计算器 实用的浮点数计算器

2. 增强版正则结构拆解

最终使用的正则:(\\d+(?:\\.\\d+)?)\\s*([+\\-*/])\\s*(\\d+(?:\\.\\d+)?)

  • (\\d+(?:\\.\\d+)?)(捕获组 1 和 3):匹配操作数。\\d+ 匹配整数部分;(?:\\.\\d+)? 为非捕获组,匹配可选的小数部分。
  • \\s\*:匹配运算符与数字之间的任意数量空格,提升输入容错率。
  • ([+\\-\*/])(捕获组 2):匹配并提取运算符。\\- 必须转义以避免被识别为范围符号。

3. 在代码中的关键配合

  • 全串匹配校验:配合 matcher.matches() 使用,要求整个输入字符串严格符合“数字 运算符 数字”的结构,防止误匹配多余字符。

  • 数据提取与转换:通过 matcher.group(1/2/3) 精准提取两个操作数和运算符,并交由 Double.parseDouble() 转换为浮点数。

  • 前置预处理:结合 sc.nextLine().trim() 去除输入首尾空格,与正则内部的 \\s* 形成双重容错。

    方法 匹配范围 行为描述
    matches() 整个输入字符串 尝试将整个输入序列与模式完全匹配,只有从头到尾完全一致时才返回 true。
    find() 输入字符串的子串 扫描输入序列,查找下一个能与模式匹配的子序列,只要找到任意一处就返回 true。

4. 设计优势与避坑点

  • 严谨的小数匹配:使用 (?:\\.\\d+)? 而非 \\.?\\d*,有效拦截了 123. 这类不合法的数字格式。
  • 异常防御:正则前置校验拦截了大部分格式错误,使得后续的 Double.parseDouble() 极难触发 NumberFormatException。
  • Java 转义规范:在 Java 字符串中正确使用了双反斜杠(如 \\d、\\.、\\-)。
  • 减号转义:在字符类 [...] 中对减号进行了转义,避免了正则语法错误。

5. 项目收获

通过本项目,你掌握了如何利用正则表达式快速实现一个 文本解析器,理解了 捕获组 与 非捕获组 的实际区别,并体会到了“正则校验 + 异常分层处理”在提升程序健壮性上的重要作用。

扩展思考:当前正则仅支持两个操作数的二元运算。若未来要支持连续运算(如 1+2 * 3)或负数开头(如 -5+3),只需在正开头增加 -? 并对剩余部分进行调整即可。


四、Java 数组

1. 数组的定义

数组是 相同类型数据 的 有序集合

数组描述的是相同类型的若干个数据,按照一定的先后次序排列组合而成。

其中,每一个数据称作一个数组元素,每个数组元素可以通过一个下标来访问它们。

2. 数组的声明和创建

首先必须声明数组变量,才能在程序中使用数组。下面是声明数组变量的语法:

dataType[] arrayRefVar; // 首选方法,阿里推荐
或
dataType arrayRefVar[]; // 效果相同,但阿里不推荐

Java 语言使用 new 操作符来创建数组,语法如下:

dataType[] arrayRefVar = new dataType[arraySize];

数组的元素是通过索引访问的,数组索引从 0 开始

获取数组长度:arrays.length

3. 内存分析

Java 内存分析

image-20261018094104626

上图只是简单说明,Java 内存不止这三个。

数组内存结构:

image-20261018094322718

4. 数组的三种初始化

静态初始化

int[] a = {1,2,3};
Man[] mans = {new Man(1,1),new Man(2,2)};

动态初始化

int[] a = new int[2];
a[0]=1;
a[1]=2;

数组的默认初始化

数组是引用类型,它的元素相指当于类的实例变量,因此数组一经分配空间,其中的每个元素也被按照实例变量同样的方式被 隐式初始化

5. 数组的四个基本特点

  1. 其长度是确定的。数组一旦被创建,它的大小就是不可以改变的。

  2. 其完素必须是相同类型, 不充许出现混合类型。

  3. 数组中的元素可以是任向数据类型,包活基本类型和引用类型

  4. 数组变量属引用类型,数组也可以看成是对象,数组中的每个元素相当于该对象的成员变量。
    数组本身就是对象,Java 中对象是在堆中的,因此数组无论保存原始类型还是其他对象类型数组对象本身是在堆中的。

6. 数组边界

下标的合法区间:[0,length-1],如果越界就会报错

public static void main(String[] args) {
int[] a = new int[2];
System.out.println(a[2]); // ArrayIndexOutOfBoundsException
}

ArrayIndexOutOfBoundsExcepton: 数组下标越界异常

小结:
数组是相同数据类型(数据类型可以为任意类型)的有序集合

数组也是对象。数组元素相当于对象的成员变量

数组长度是确定的,不可变的。如果越界,则报:ArrayIndexOutOfBoundsException

7. 数组的使用

普通 for 循环

打印数组

// 使用 StringBuilder 拼接更高效
// 格式: {1, 2, 3, 4, 5}
public static void printArray(int[] arr) {
StringBuilder sb = new StringBuilder();
sb.append("{");
for (int i = 0; i < arr.length; i++) {
if (i > 0) {
sb.append(", ");
}
sb.append(arr[i]);
}
sb.append("}");
System.out.println(sb.toString());
}

求最大值,求和,反转

// 求数组最大值
public static int max(int[] arr) {
int max = arr[0];
for (int i = 0; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
return max;
}

// 数组求和
public static int sum(int[] arr) {
int sum = 0; // 局部变量必须先初始化值
for (int i = 0; i < arr.length; i++) {
sum += arr[i];
}
return sum;
}

// 数组反转
public static int[] reverse(int[] arr) {
int[] result = new int[arr.length]; // Shift + F6
for (int i = 0,j = arr.length- 1; i < arr.length; i++,j--) {
result[j] = arr[i];
}
return result;
}

增强 for

// forEach
public static void testForEach(int[] arr) {
for (int i : arr) {
System.out.println(arr[i]);
}
}

测试

public static void main(String[] args) {
int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
printArray(arr);
System.out.println(max(arr));
System.out.println(sum(arr));
printArray(reverse(arr));
}

8. 二维数组

多维数组可以看成是数组的数组,比如 二维数组 就是一个特殊的一维数组,其 每一个元素都是个一维数组。

image-20261018113108643

二维数组

int[][] a = new int[2][5];

使用

public static void main(String[] args) {
//int [][] arr = new int [2][5]; // √
int[][] arr = {{1, 2, 3}, {4, 5}, {6, 7, 8, 9, 10}};
//int [][] arr2 = new int [3][]; // √
//int [][] arr3 = new int [][3]; // x
for (int i = 0; i < arr.length; i++) {
for (int j = 0; j < arr[i].length; j++) {
System.out.print(arr[i][j]+" ");
}
System.out.println();
}
}

运行结果:

1 2 3
4 5
6 7 8 9 10

9. Arrays 类

数组的工具类 java.util.Arrays

由于数组对象本身并没有什么方法可以供我们调用,但 API 中提供了一个工具类 Arrays 供我们使用, 从而可以对数据对象进行一些基本的操作。

Arrays 类中的方法都是 static 修饰的静态方法,在使用的时候可以直接使用类名进行调用,而 “ 不用”使用对象来调用(注意: 是“不用而不是“不能”)

具有以下常用功能:

给数组赋值:通过 fill()方法。

int[] arr = new int[10];
Arrays.fill(arr,10);
System.out.println(Arrays.toString(arr)); // 以 [10, 10, 10, 10, 10, 10, 10, 10, 10, 10] 形式打印数组元素

[10, 10, 10, 10, 10, 10, 10, 10, 10, 10]

对数组排序:通过 sort()方法, 按升序。

// 排序
int[] arr = {10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
Arrays.sort(arr);
System.out.println(Arrays.toString(arr));

对于 Integer 数组,可以传入比较器自定义排序方式

Integer[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
Arrays.sort(arr, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return Integer.compare(o2,o1);
}
});
System.out.println(Arrays.toString(arr));

// Lambda 表达式
Arrays.sort(arr,(o1,o2) -> Integer.compare(o2,o1));

[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]

比较数组:通过 equals()方法查找数组中元素值是否相等

查找数组元素:通过 binarySearch()方法能对排序好的数组进行二分查找。(注意:二分查找的数组必须保证有序从小到大)

Integer[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
System.out.println(Arrays.toString(arr));
// 返回 key 值在数组中的索引 index
int index = Arrays.binarySearch(arr, 9);
System.out.println(index);

[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
8


Arrays.asList() 方法详解

Arrays.asList(T... a) 是 java.util.Arrays 类中的一个静态方法,用于将数组快速转换为一个固定大小的 List。


1. 基本用法

String[] arr = {"A", "B", "C"};
List<String> list = Arrays.asList(arr);

// 也可以直接传可变参数
List<Integer> list2 = Arrays.asList(1, 2, 3);

2. 返回的 List 的特点

特点 说明
固定大小 返回的 List 长度与原数组相同,不能增加或删除元素(调用 add、remove 会抛出 UnsupportedOperationException)
视图(View) 返回的 List 是对原数组的视图,修改 List 中的元素会同步影响原数组,反之亦然
序列化支持 返回的 List 实现了 Serializable 和 RandomAccess
泛型限制 如果传入的是基本类型数组(如 int[]),会被当作一个元素处理(见下文坑点)

3. 常见坑点与注意事项

坑点一:不能增删元素
List<String> list = Arrays.asList("A", "B");
list.add("C"); // ❌ UnsupportedOperationException
list.remove("A"); // ❌ UnsupportedOperationException

原因:返回的是 java.util.Arrays 内部类 ArrayList(不是 java.util.ArrayList),它底层直接使用传入的数组,不支持结构性修改。

坑点二:基本类型数组被当作单个元素
int[] arr = {1, 2, 3};
List<int[]> list = Arrays.asList(arr);
System.out.println(list.size()); // 输出 1,而不是 3

原因:泛型不支持基本类型,int[] 被当作一个 Object 元素。解决方案:使用包装类型 Integer[] 或 Java 8+ 的 Stream API。

坑点三:修改 List 会影响原数组
String[] arr = {"A", "B"};
List<String> list = Arrays.asList(arr);
list.set(0, "Z");
System.out.println(arr[0]); // 输出 Z

原因:List 是数组的视图,操作直接作用于底层数组。


4. 如何得到一个真正的可变 List?

如果需要可增删的 List,通常这样处理:

// 方式一:传给 ArrayList 构造器
List<String> mutableList = new ArrayList<>(Arrays.asList("A", "B"));

// 方式二:Java 9+ 使用 List.of() 返回不可变列表,再包装
List<String> mutableList2 = new ArrayList<>(List.of("A", "B"));

// 方式三:Stream 流(适用于基本类型数组)
List<Integer> list = Arrays.stream(new int[]{1,2,3})
.boxed()
.collect(Collectors.toList());

10. 冒泡排序

冒泡排序思路:

  1. 比较数组中两个相邻元素,如果第一个数比第二个数大,交换
  2. 每一次比较,都会产生出一个最大或者最小数
  3. 下一轮则可以少一次比较
  4. 依次循环,直到结束

image-20261018125647345

// n 个元素,内部比较 n-1 次,
// 每一次外部循环得到一个最大或者最小值,n 个元素,外循环只需 n-1 轮
public static int[] bubbleSort(int[] arr) {
boolean swapped; // 加一个标志位,如果某一轮没有发生任何交换,说明数组已经有序,提前结束排序
for (int i = 0; i < arr.length - 1; i++) {
swapped = false;
int temp = 0;
for (int j = 0; j < arr.length -1 - i; j++) {
if (arr[j] >arr[j+1]) {
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
swapped= true;
}
if (!swapped) break; // 没有交换,提前结束
}
return arr;
}

arr.length-1 : 数组有 n 个数,只需要比较 n-1 轮得到 n-1 个数的位置,最后一个自然就有序

内层 arr.length-1-i: -i 就是已经有 i 个元素有序了,不需要比较。只需要比较剩下的 arr.length-1-i 个元素。

11. 稀疏数组

需求:编写五子棋游戏中,有存盘退出和续上盘的功能

image-20261018130719379

分析问题:因为该二维数组的很多值是默认值 0,因此记录了很多没有意义的数据

解决:稀疏数组

稀疏数组

当一个数组中大部分元素为 0,或者为同一值的数组时,可以使用稀疏数组来保存该数组。

稀疏数组的处理方式:

  1. 记录数组一共有几行几列,有多少个不同值
  2. 把具有不同值的元素和行列及值记录在一个小规模的数组中,从而缩小程序的规模

image-20261018131023455

示例:将五子棋棋盘存储为稀疏数组,再还原回来

package com.it.day02;

/**
* @author zhangzhicai
* @version 1.0
* @description: TODO
* @date 2026/10/18 13:12
*/
public class SparseArrayDemo {
public static void main(String[] args) {
// 原始棋盘 11x11 0: 无棋子 1: 黑子 2: 白子
int[][] chessBoard = new int[11][11];
chessBoard[1][2] = 1;
chessBoard[2][3] = 2;
// 打印原始棋盘
printArray(chessBoard);

// 将原始棋盘转换为稀疏数组
int[][] sparseArray = getSparseArray(chessBoard);
// 打印稀疏数组
printArray(sparseArray);
// 将稀疏数组还原为棋盘
int[][] newChessBoard = getChessBoard(sparseArray);
// 打印还原后的棋盘
printArray(newChessBoard);
}

/**
* 从稀疏数组还原二维数组
* @param sparseArray
* @return
*/
private static int[][] getChessBoard(int[][] sparseArray) {
int[][] chessBoard2 = new int[sparseArray[0][0]][sparseArray[0][1]];
for (int i = 1; i < sparseArray.length; i++) {
chessBoard2[sparseArray[i][0]][sparseArray[i][1]] = sparseArray[i][2];
}
return chessBoard2;
}

/**
* 将二维数组转换成稀疏数组
* @param chessBoard
* @return
*/
private static int[][] getSparseArray(int[][] chessBoard) {
// 构建稀疏数组
// 获取棋盘长度
int length = chessBoard.length;
// 获取棋盘中有效值个数
int sum = 0;
for (int i = 0; i < chessBoard.length; i++) {
for (int j = 0; j < chessBoard[i].length; j++) {
if (chessBoard[i][j] != 0) {
sum++;
}
}
}
int[][] sparseArray = new int[sum + 1][3];
// 稀疏数组第一列记录:行数,列数,有效值个数
sparseArray[0][0] = length;
sparseArray[0][1] = length;
sparseArray[0][2] = sum;
// 遍历原始棋盘元素
int count = 1; // 从第二行开始存放值
int value;
for (int i = 0; i < chessBoard.length; i++) {
for (int j = 0; j < chessBoard[i].length; j++) {
value = chessBoard[i][j];
if (value != 0) {
sparseArray[count][0] = i;
sparseArray[count][1] = j;
sparseArray[count][2] = value;
count++;
}
}
}
return sparseArray;
}

/**
* 打印棋盘
*
* @param chessBoard
*/
private static void printArray(int[][] chessBoard) {
for (int i = 0; i < chessBoard.length; i++) {
for (int j = 0; j < chessBoard[i].length; j++) {
System.out.print(chessBoard[i][j] + " ");
}
System.out.println();
}
}
}

0 0 0 0 0 0 0 0 0 0 0
0 0 1 0 0 0 0 0 0 0 0
0 0 0 2 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
11 11 2
1 2 1
2 3 2
0 0 0 0 0 0 0 0 0 0 0
0 0 1 0 0 0 0 0 0 0 0
0 0 0 2 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0


五、面向对象

1. 什么是面向对象

面向过程思想

  • 步骤清晰简单,第一步做什么,第二步做什么..

  • 面向过程适合处理一些较为简单的问题

面向对象思想

  • 物以类聚,分类 的思维模式,思考问题首先会解决问题需要哪些分类,然后对这些分类进行单独思考。
    最后,才对某个分类下的细节进行面向过程的思索

  • 面向对象适合处理复杂的问题,适合处理需要多人协作的问题!

对于描述复杂的事物,为了从宏观上把握、从整体上合理分析,我们需要使用面向对象的思路来分析整个系统。但是,具体到微观操作,仍然需要面向过程的思路去处理。

面向对象编程(Object-Oriented Programming, OOP)

面向对象编程的本质就是:以类的方式组织代码,以对象的方式组织(封装)数据。

抽象:编程思想

三大特性:

  1. 封装
  2. 继承
  3. 多态

从认识论角度考虑是先有对象后有类。对象,是具体的事物。类,是抽象的,是对 对象 的抽象

从代码运行角度考虑是先有类后有对象。类是对象的 模板

2. 回顾方法及加深

方法的定义

  • 修饰符

  • 返回类型

  • break:跳出switch,结束循环和return的区别

  • 方法名:注意规范就 OK 见名知意

  • 参数列表:(参数类型,参数名)…

  • 异常抛出: throws

方法的调用

  1. 静态方法

  2. 非静态方法

  3. 形参和实参

  4. 值传递和引用传递

  5. this 关键字

3. 类与对象的关系

类是一种抽象的数据类型,它是对某一类事物整体描述/定义,但是并不能代表某一个具体的事物。

  • 动物,植物,手机,电脑…
  • Person 类,Pet 类,Car 类等,这些类都是用来描述/定义某一类具体的事物应该具备的特点和行为。

对象是抽象概念的具体实例

  • 张三就是人的一个具体实例,张三家里的旺财就是狗的一个具体实例

  • 能够体现出特点,展现出功能的是具体的实例,而不是一个抽象的概念

4. 创建与初始化对象

使用 new 关键字创建对象

使用 new 关键学创建的时候,除了分配内存空间之外,还会给创建好的对象进行默认的初始化以及对类中构造器的调用。

类中的构造器也称为构造方法,是在进行创建对象的时候必须要调用的。并且构造器有以下俩个特点:

  1. 必须和类的名字相同
  2. 必须没有返回类型,也不能写 void

5. 构造器

使用 new 关键字,本质是在调用构造器

默认会有一个无参构造

有参构造:一且定义了有参构造,无参构造就必须显示定义

public class Person {
private String name;

// 无参构造
public Person() {

}

// 有参构造
public Person(String name) {
this.name = name;
}
}

6. 创建对象内存分析

image-20261018141449453

7. 封装

该露的露,该藏的藏

我们程序设计要追求“高内聚,低耦合”。高内聚就是类的内部数据操作细节自已完成,不允许外部干涉;
低耦合:仅暴露少量的方法给外部使用。

封装(数据的隐藏)

通常,应禁止直接访问一个对象中数据的实际表示,而应通过操作接口来访问,这称为信息隐藏。

记住这句话就够了:属性私有,get/set

public class Person {
private String name;

// 无参构造
public Person() {

}

// 有参构造
public Person(String name) {
this.name = name;
}

/**
* 获取
* @return name
*/
public String getName() {
return name;
}

/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}

public String toString() {
return "Person{name = " + name + "}";
}
}

通过 get/set 方法操作对象的字段

8. 继承

继承的本质是对某一批类的抽象,从而实现对现实世界更好的建模

extands 的意思是“扩展”。子类是父类的扩展。

JAVA 中类只有单继承,没有多继承!

继承是类和类之间的一种关系。除此之外, 类和类之间的关系还有依赖、组合、聚合等。

继承关系的俩个类,一个为子类(派生类,一个为父类(基类)。子类继承交类,使用关键字 extends 来表示。

子类和父类之间, 从意义上讲应该具有 “is-a” 的关系。

所有类的父类是 Object 类

super

super 注意点:

  1. super 调用父类的构造方法,必须在构造方法的第一个
  2. super 必须只能出现在子类的方法或者构造方法中!
  3. super 和 this 不能同时调用构造方法!

Vs this:

代表的对象不同:
this:
super: 代表父类对象的应用

前提
this:没有继承也可以使用
super:只能在继承条件才可以使用

构造方法
this();本类的构造
super():父类的构造!

方法重写

重写:需要有继承关系,子类重写父类的方法

  1. 方法名必须相同
  2. 参数列表必须相同
  3. 修饰符:范围可以扩大,不能缩小。public > protected > default > private
  4. 抛出的异常:范围可以缩小,不能扩大。ClassNotFoundException –> Exception(扩大)

为什么需要重写:

  1. 父类的功能,子类不一定需要,或者不一定满足!

Alt + Insert: override

9. 多态

即同一方法可以根据发送对象的不同而采用多种不同的行为方式。

一个对象的实际类型是确定的,但可以指向对象的引用的类型有很多

多态存在的条件

  • 有继承关系
  • 子类重写父类方法
  • 父类引用指向子类对象

注意:多态是方法的多态,属性没有多态性。

static 方法,属于类,不属于实例

final 常量

private 方法

都不能重写

instanceof

image-20261018153335519

10. static 关键字

好的,我们来系统地总结 Java 中 static 关键字的全部用法。


1. 静态变量(类变量)

  • 定义:用 static 修饰的成员变量,属于类本身,不属于某个对象实例。

  • 特点:

    • 所有实例共享同一份静态变量(只有一个副本)。
    • 在类加载时初始化,生命周期与类相同。
    • 可以通过类名直接访问(推荐),也可以通过实例访问(不推荐,易混淆)。
  • 用途:常量(public static final)、计数器、全局配置等。

class Counter {
public static int count = 0; // 静态变量
public static final double PI = 3.14; // 静态常量
}
// 访问:Counter.count++,Counter.PI

2. 静态方法

  • 定义:用 static 修饰的方法,属于类而非实例。

  • 特点:

    • 可以直接通过类名调用,无需创建对象。
    • 不能直接访问非静态成员(变量和方法),因为没有隐式的 this 引用。
    • 可以访问其他静态成员。
    • 不能被重写(override),但可以被隐藏(子类定义同名静态方法)。
  • 用途:工具方法(如 Math.max()、Arrays.sort())、工厂方法、单例模式的 getInstance()。

class Utils {
public static int add(int a, int b) {
return a + b;
}
}
// 调用:Utils.add(1, 2)

3. 静态代码块

  • 定义:用 static {} 包裹的代码块,在类加载时执行一次。

  • 特点:

    • 用于初始化静态变量或执行一次性资源加载(如读取配置文件)。
    • 可以有多个静态代码块,按书写顺序执行。
    • 在类第一次被使用时触发(加载、实例化、访问静态成员等)。
  • 执行顺序:父类静态块 → 子类静态块 → main 方法 → 实例初始化块 → 构造器。

class Database {
static {
System.out.println("数据库驱动加载");
// 加载 JDBC 驱动等
}
}

4. 静态内部类(嵌套类)

  • 定义:用 static 修饰的内部类,不依赖于外部类的实例。

  • 特点:

    • 可以独立创建对象:new Outer.Inner(),无需外部类实例。
    • 不能直接访问外部类的非静态成员(因为没有隐式的外部类引用)。
    • 可以拥有静态成员(普通内部类不可以)。
  • 用途:辅助类、构建器模式(Builder)、单例(静态内部类方式)。

class Outer {
private static String msg = "Hello";

static class Inner {
public void show() {
System.out.println(msg); // 只能访问外部类的静态成员
}
}
}
// 使用:Outer.Inner inner = new Outer.Inner();

5. 静态导入(import static)

  • 语法:import static 包名.类名.静态成员; 或 import static 包名.类名.*;
  • 作用:直接使用静态成员名称,无需写类名前缀。
  • 适用场景:频繁使用的静态常量或工具方法(如 Math.PI、System.out)。
  • 注意:过度使用会降低代码可读性,建议适度。
import static java.lang.Math.PI;
import static java.lang.Math.sqrt;

public class Test {
public static void main(String[] args) {
System.out.println(PI); // 直接写 PI
System.out.println(sqrt(16)); // 直接写 sqrt
}
}

6. 静态与非静态的区别总结

特性 静态成员 非静态成员
归属 类本身 实例对象
内存分配 类加载时分配,方法区(或堆中元空间) 对象创建时分配,堆
访问方式 类名.成员(推荐) 对象引用.成员
this 引用 无 有
能否直接访问非静态 否(需通过对象引用) 能
生命周期 随类加载而生,随类卸载而亡 随对象创建而生,随 GC 回收而亡
重写 不可重写(可隐藏) 可重写

7. 注意事项

  1. 静态方法不能使用 super 和 this,因为它们依赖实例上下文。
  2. 静态变量在多线程环境下存在线程安全问题,需要自行同步。
  3. 静态方法不能被抽象(abstract),也不能被接口中的默认方法使用。
  4. 静态内部类中不能有静态变量? 错!静态内部类可以有静态成员,而普通内部类不能。
  5. main 方法必须是 public static void,因为 JVM 在启动时需要调用它而不创建对象。

8. 经典应用场景

场景 示例
常量池 public static final String VERSION = "1.0";
工具类 Collections.sort(list)
单例模式(饿汉/静态内部类) public static Singleton getInstance()
工厂方法 Calendar.getInstance()
缓存/全局状态 public static Map<String, Cache> cacheMap
静态代理 Thread.setDefaultUncaughtExceptionHandler(...)

11. 抽象类

abstract 修饰符可以用来修饰方法也可以修饰类,如果修饰方法,那么该方法就是抽象方法;如果修饰类,那么该类就是抽象类。

抽象类中可以没有抽象方法,但是有抽象方法的类一定要声明为抽象类。

抽象类, 不能使用 new 关键字来创建对象,它是用来让子类继承的。

抽象方法,只有方法的声明没有方法的实现,它是用来让子类实现的

子类继承抽象类那么就必须要实现抽象类没有实现的抽象方法,否则该子类也要声明为抽象类。

抽象类

  • 不能直接创建实例(不能 new 抽象类)。
  • 可以包含抽象方法(只有声明,没有方法体),也可以包含具体方法(有完整实现)。
  • 主要用于定义模板和规范,强制子类实现某些行为。
public abstract class Animal {
public abstract void sound(); // 抽象方法,子类必须实现
public void sleep() { // 具体方法
System.out.println("睡觉");
}
}

抽象类的成员

抽象类几乎可以拥有普通类的一切成员,甚至更多:

成员类型 是否可以拥有 说明
成员变量 ✅ 可以 可以是实例变量、静态变量、常量(final)
构造器 ✅ 可以 但不能直接 new,供子类通过 super() 调用
具体方法 ✅ 可以 普通方法、静态方法、final 方法
抽象方法 ✅ 零个或多个 只有声明,没有方法体,子类必须实现(除非子类也是抽象类)
代码块 ✅ 可以 实例初始化块、静态初始化块
内部类 ✅ 可以 普通内部类、静态内部类等

抽象类可以没有抽象方法

12. 接口

什么是接口(Interface)

接口是 Java 中的一种引用类型,它定义了一组规范(契约),规定实现类必须提供哪些行为(方法),但不关心这些行为如何实现。

你可以把接口理解为一个合同:合同上写了要实现哪些功能(方法签名),但具体怎么做由签合同的类自己决定。


接口的核心特征

  1. 方法都是抽象的(Java 8 之前)
    接口中的方法默认是 public abstract,只有方法签名,没有方法体。

  2. 不能实例化
    不能 new 一个接口,必须由类来实现它。

  3. 多实现
    一个类可以实现多个接口(弥补 Java 单继承的不足)。

  4. 成员变量只能是常量
    接口中的变量默认是 public static final,必须初始化。

  5. Java 8 之后新增

    • default 方法:可以有默认实现,实现类可以选择重写或不重写。
    • static 方法:属于接口本身,通过接口名调用。
  6. Java 9 之后新增

    • private 方法:供接口内部的 default/static 方法调用,不暴露给外部。

接口的语法

// 定义一个接口
public interface Flyable {
// 抽象方法(必须由实现类实现)
void fly();

// 默认方法(Java 8+)
default void glide() {
System.out.println("滑翔中...");
}

// 静态方法(Java 8+)
static void info() {
System.out.println("这是一个飞行接口");
}
}

// 实现接口
public class Bird implements Flyable {
@Override
public void fly() {
System.out.println("鸟拍翅膀飞行");
}
// glide() 可以不用重写,直接继承默认实现
}

接口的主要用途

用途 说明
制定规范 比如 USB 接口,规定了设备必须支持插拔和数据传输,但不同设备实现不同
实现多态 面向接口编程,提高灵活性和可扩展性
解耦 调用者只依赖接口,不依赖具体实现类,便于替换和维护
多重继承替代 一个类可以实现多个接口,获得多种行为能力
回调机制 比如事件监听器接口,注册后由框架回调

接口 vs 抽象类(快速对比)

对比项 接口 抽象类
实例化 不能 不能
构造器 无 可以有
成员变量 只能是 public static final 常量 任意类型变量
方法实现 抽象方法 + default/static/private 方法(Java 8+) 抽象方法 + 具体方法
继承/实现 类可以实现多个接口 类只能继承一个抽象类
设计思想 “能做什么”(能力契约) “是什么”(类层次结构)

经典例子

// 接口:可充电
interface Rechargeable {
void charge();
}

// 接口:可拍照
interface Photographable {
void takePhoto();
}

// 手机同时具备两种能力
class Phone implements Rechargeable, Photographable {
@Override
public void charge() {
System.out.println("手机正在充电");
}

@Override
public void takePhoto() {
System.out.println("手机拍照");
}
}

普通类:只有具体实现

抽象类:具体实现和规范(抽象方法)都有!

接口:只有规范!

内部类

内部类就是在一个类的内部在定义一个类,比如,A类中定义一个B类,那么B类相对A类来说就称为内部类,而A类相对B类来说就是外部类了。

1. 成员内部类

成员内部类是定义在另一个类(外部类)内部的、不带 static 修饰的类。它是外部类的一个成员,类似于外部类的属性或方法。

成员内部类依赖外部类

public class Out {
private String name;

public Out(){}
public Out(String name) {
this.name = name;
}
private void say() {
System.out.println("Out...");
}
public class Inner {
private String name;
public void say() {
Out.this.say(); // 调用外部类私有方法
System.out.println("In...");
}
public String getOutName() {
//return name;
return Out.this.name; // 获取外部类 Out的私有属性name
}
}
}
public static void main(String[] args) {
Out out = new Out("外部类");
Out.Inner inner = out.new Inner(); // 方式一 创建成员内部类对象
// 或
Outer.Inner inner2 = new Outer().new Inner(); // 方式二(匿名外部类对象)
System.out.println(inner.getOutName());
inner.say();
}

当成员内部类中的属性或者方法和外部类中的一样时,在成员内部类中要使用外部类.this.属性或外部类.this.方法访问外部类的属性,方法

外部类访问内部类:外部类必须通过内部类对象来访问其成员(即使是私有的)

成员内部类的访问修饰符

public class Outer {
// 合法的成员内部类修饰符组合
public abstract class InnerAbstract {}
protected final class InnerFinal {}
private strictfp class InnerStrictfp {}
class InnerDefault {}
}

2. 静态内部类

静态内部类是用 static 修饰的、定义在另一个类内部的类。它本质上是外部类的一个静态成员,不依赖于外部类的实例。


1. 基本定义

public class Outer {
private static String staticMsg = "静态变量";
private String instanceMsg = "实例变量";

// 静态内部类
static class StaticInner {
public void display() {
// 只能访问外部类的静态成员
System.out.println(staticMsg); // ✅
// System.out.println(instanceMsg); // ❌ 编译错误
}
}
}

2. 核心特点

特点 说明
不依赖外部类实例 可以直接 new Outer.StaticInner(),无需外部类对象
只能访问外部类的静态成员 不能直接访问外部类的实例变量和方法(因为没有隐式的 Outer.this 引用)
可以有静态成员 与普通类一样,可以定义 static 变量、方法、常量等
访问修饰符 同样可以使用 public、protected、default、private
编译产物 生成 Outer$StaticInner.class 文件

3. 创建对象

// 方式一:通过外部类名直接创建(推荐)
Outer.StaticInner inner = new Outer.StaticInner();

// 方式二:通过外部类对象创建(不推荐,看起来像成员内部类)
Outer outer = new Outer();
Outer.StaticInner inner2 = new Outer.StaticInner(); // 注意:这里 outer 并未被使用

4. 访问规则

  • 静态内部类内部:可以直接访问外部类的所有静态成员(包括 private 的)。
  • 访问外部类实例成员:必须通过外部类对象引用(例如 new Outer().instanceMethod())。
  • 外部类访问静态内部类:与访问其他静态成员类似,通过 Outer.StaticInner 使用。

5. 静态内部类 vs 成员内部类

对比项 静态内部类 成员内部类
是否需要外部类实例 不需要 需要
能否访问外部类实例成员 不能(需通过对象) 能(直接访问)
能否有静态成员 能 不能(除 static final 常量)
创建方式 new Outer.StaticInner() outer.new Inner()
隐式引用 无 持有 Outer.this
内存泄漏风险 低(无外部类引用) 高(持有外部类引用)

6. 典型应用场景

场景 说明
Builder 模式 经典实现:new Pizza.Builder().setSize(LARGE).build()
单例模式(静态内部类方式) 线程安全、延迟加载的单例实现
辅助工具类 与外部类逻辑紧密相关,但不需要访问外部类实例
数据结构节点 如链表节点 Node 定义为静态内部类,节省内存

7. 经典示例:单例模式

public class Singleton {
private Singleton() {}

// 静态内部类持有唯一实例
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}

public static Singleton getInstance() {
return Holder.INSTANCE; // 类加载时才初始化
}
}

8. 注意事项

  • 命名:虽然叫“静态内部类”,但官方术语是“静态嵌套类”(Static Nested Class),因为它更像一个顶层类被放在了另一个类里面。
  • 序列化:静态内部类实现 Serializable 时,不会自动包含外部类的引用,序列化更安全。
  • 与普通类的区别:除了名字空间被限定在外部类内,静态内部类本质上就是一个普通的顶层类(可以独立存在)。

3. 局部内部类

局部内部类是定义在方法、构造器或代码块中的类,它只在所属的局部范围内可见。


1. 基本定义

public class Outer {
public void method() {
// 局部内部类,定义在方法内部
class LocalInner {
public void show() {
System.out.println("我是局部内部类");
}
}

// 只能在方法内部使用该类
LocalInner inner = new LocalInner();
inner.show();
}
}

2. 核心特点

特点 说明
作用域受限 只在定义它的方法/代码块内有效,外部无法访问
不能有访问修饰符 不能用 public、protected、private、static
可以访问外部类的所有成员 包括 private 成员
可以访问所在方法的局部变量 但局部变量必须是 final 或事实上不可变(Java 8+ 要求 effectively final)
不能有静态成员 除了 static final 常量
编译产物 生成 Outer$1LocalInner.class(数字表示序号)

3. 为什么局部变量必须是 final 或 effectively final?

局部内部类对象可能在方法执行完毕后仍然存活(例如作为返回值或传递给事件监听器),而此时方法的局部变量已经销毁。为了确保内部类能访问到正确的值,Java 要求局部变量是 final 或 effectively final(赋值后不再改变),编译器会将变量的值复制到内部类中。

public class Outer {
public Runnable createRunnable(final int x) {
// 局部变量 y 是 effectively final(只赋值一次)
int y = 20;

class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println(x + y); // 可以访问
}
}
return new MyRunnable();
}
}

4. 典型应用场景

场景 说明
复杂算法辅助类 只在某个方法内需要的临时数据结构或逻辑
事件处理器 在 GUI 编程中,为某个组件创建一次性的事件监听器
测试辅助 在单元测试方法中定义临时的桩类

5. 完整示例

public class Calculator {
public int[] filterPositive(int[] numbers) {
// 局部内部类:临时辅助过滤逻辑
class PositiveChecker {
boolean isPositive(int num) {
return num > 0;
}
}

PositiveChecker checker = new PositiveChecker();
int count = 0;
for (int num : numbers) {
if (checker.isPositive(num)) count++;
}

int[] result = new int[count];
int index = 0;
for (int num : numbers) {
if (checker.isPositive(num)) {
result[index++] = num;
}
}
return result;
}
}

6. 局部内部类 vs 匿名内部类

对比项 局部内部类 匿名内部类
是否有名字 有(可以定义多个方法) 无(只能有一个实例)
可重用性 可以在方法内多次实例化 只能实例化一次(通常)
语法复杂度 相对简单 更简洁(适合单次使用)
典型用途 需要多个方法或多次实例化 一次性回调/事件监听

7. 注意事项

  • 不能声明为 static:因为局部内部类本身就是方法内的局部实体,没有静态的概念。
  • 不能有静态初始化块:同上。
  • 接口不能定义为局部内部类:局部内部类只能是类,不能是接口(接口总是顶层的或静态的)。
  • 枚举不能定义为局部内部类:枚举类型不能定义在方法内部。

4. 匿名内部类(Anonymous Inner Class)

匿名内部类是没有显式名字的内部类,它在创建对象的同时定义类的实现。通常用于需要快速创建某个接口或抽象类的实例,且只用一次的场景。


1. 基本语法

// 基于接口
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println("任务执行");
}
};

// 基于抽象类
Animal dog = new Animal() {
@Override
public void sound() {
System.out.println("汪汪");
}
};

// 基于具体类(可以重写方法)
ArrayList<String> list = new ArrayList<String>() {
// 匿名内部类继承了 ArrayList,可以添加额外方法或重写
};

2. 核心特点

特点 说明
没有名字 定义时直接 new 接口/类() { ... },不写类名
必须继承一个类或实现一个接口 不能凭空定义,必须基于已有的类型
只能创建一个实例 因为没名字,无法在其他地方再次 new 这个类
不能有构造器 匿名内部类没有构造器(但可以调用父类的构造器)
可以定义自己的成员 变量、方法、代码块都可以,但不能有静态成员
可以访问外部类成员 与局部内部类规则相同
访问局部变量需 final/effectively final 与方法内的局部内部类一致

3. 典型应用场景

场景 示例
事件监听器 button.addActionListener(new ActionListener() {...})
线程创建 new Thread(new Runnable() {...}).start()
比较器 Arrays.sort(arr, new Comparator<Integer>() {...})
回调函数 异步操作完成后执行的逻辑
临时适配器 快速实现一个接口,覆盖少量方法

4. 完整示例

import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class Main {
public static void main(String[] args) {
JButton button = new JButton("点击");

// 匿名内部类实现 ActionListener
button.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("按钮被点击");
}
});
}
}

5. 匿名内部类 vs 局部内部类

对比项 匿名内部类 局部内部类
是否有名字 无 有
可重用性 仅创建一次 可在方法内多次实例化
定义位置 在 new 表达式中 在方法/代码块中
适用场景 一次性回调、事件处理 需要多次使用或包含多个方法的临时类
可读性 简洁但较长时难读 较清晰,适合复杂逻辑

6. 注意事项

  • 不能有构造器:但可以调用父类带参数的构造器(通过 super(参数) 的方式在匿名内部类中实现?实际上匿名内部类不能显式写构造器,但可以在实例初始化块中完成初始化)。
  • 编译产物:生成 Outer$1.class、Outer$2.class 等(数字递增)。
  • 内存泄漏风险:匿名内部类持有外部类引用(如果是在非静态上下文中定义的),可能导致外部类对象无法被 GC 回收。
  • Lambda 表达式的替代:对于函数式接口(只有一个抽象方法),Java 8+ 推荐使用 Lambda 表达式代替匿名内部类,更简洁。
// Lambda 替代匿名内部类
button.addActionListener(e -> System.out.println("按钮被点击"));
new Thread(() -> System.out.println("任务")).start();

六、异常

1. 什么是异常(Exception)

异常是程序运行过程中出现的不正常情况,它打断了正常的指令流。Java 通过异常处理机制让开发者能够优雅地捕获和处理这些错误,而不是让程序直接崩溃。

2. 异常的分类(Throwable 体系)

Java 中所有异常类都继承自 Throwable,主要分为三大类

Throwable
├── Error(错误):严重问题,程序无法处理,如 OutOfMemoryError、StackOverflowError
└── Exception(异常)
├── RuntimeException(运行时异常):编译时不检查,如 NullPointerException、ArrayIndexOutOfBoundsException
└── 其他异常(Checked Exception):编译时必须处理,如 IOException、SQLException

image-20261018163512805

类别 特点 是否需要处理 示例
Error 系统级错误,程序无法恢复 不强制处理 OutOfMemoryError、StackOverflowError
RuntimeException 编程错误,如空指针、越界 不强制处理(可选) NullPointerException、ArithmeticException
Checked Exception 外部因素导致的异常,如文件不存在 必须处理(try-catch 或 throws) IOException、ClassNotFoundException

3. 异常处理机制

try-catch-finally 块

try {
// 可能抛出异常的代码
FileReader reader = new FileReader("test.txt");
} catch (FileNotFoundException e) {
// 处理特定类型的异常
System.out.println("文件未找到:" + e.getMessage());
} catch (IOException e) {
// 可以有多 catch,子类在前父类在后
e.printStackTrace();
} finally {
// 无论是否发生异常,都会执行(通常用于释放资源)
System.out.println("清理资源");
}

throws 声明

如果方法不打算处理异常,可以在方法签名上声明抛出,交给调用者处理:

public void readFile() throws IOException {
FileReader reader = new FileReader("test.txt");
}

throw 手动抛出异常

if (age < 0) {
throw new IllegalArgumentException("年龄不能为负数");
}

try-with-resources(Java 7+)

自动关闭实现了 AutoCloseable 的资源,省去 finally 块:

try (FileReader reader = new FileReader("test.txt")) {
// 使用 reader
} catch (IOException e) {
e.printStackTrace();
}
// reader 自动关闭

4. 常见异常举例

运行时异常

编译时不强制处理,通常由程序逻辑错误引起。

异常类型 常见原因
NullPointerException 调用 null 对象的方法或访问其属性
ArrayIndexOutOfBoundsException 数组下标越界(索引为负或 ≥ 数组长度)
ArithmeticException 整数除法中除数为零
NumberFormatException 尝试将字符串转换为数字时格式不正确(如 "abc" 转 int)
ClassCastException 强制类型转换时目标类型与实际类型不兼容
IllegalArgumentException 方法接收到不合法的参数(如负数作为年龄)
IndexOutOfBoundsException 索引越界(数组、列表等的通用父类)
ConcurrentModificationException 在使用迭代器遍历集合时,集合的结构被并发修改(增删元素),且未使用迭代器自身的 remove 方法
UnsupportedOperationException 调用了一个集合或对象不支持的操作,例如对 Arrays.asList() 返回的固定长度列表调用 add 或 remove

检查型异常

编译时必须处理(try-catch 或 throws),通常由外部环境或资源问题引起。

异常类型 常见原因
FileNotFoundException 试图打开的文件不存在
IOException 输入输出操作失败(如读写文件中断)
SQLException 数据库连接或查询错误
ClassNotFoundException 找不到指定的类(反射加载时)
InterruptedException 线程在等待/休眠时被中断
ParseException 解析字符串为日期等格式时失败

5. 自定义异常

可以继承 Exception(Checked)或 RuntimeException(Unchecked)来创建业务相关的异常:

public class InsufficientBalanceException extends RuntimeException {
public InsufficientBalanceException(String message) {
super(message);
}
}

6. 最佳实践

  • 不要捕获 Error:系统级错误无法恢复,应该让程序终止。
  • 尽量使用更具体的异常类型:不要笼统地 catch (Exception e)。
  • 不要忽略异常:空的 catch 块会让问题难以排查。
  • 尽早抛出,晚点捕获:在合适的层级处理异常,底层抛出让上层决策。
  • 使用日志记录异常:e.printStackTrace() 在生产环境中不好用,建议用日志框架(Log4j、SLF4J)。
  • 资源使用 try-with-resources:避免忘记关闭资源导致泄漏。

image-20261018165043363


七、常用类

Object类

image-20261018184910900

java.lang.Object 是 Java 中所有类的根类(超类)。每个类都直接或间接继承自 Object,因此 Object 类中的方法在所有 Java 对象中都可用。


常用方法一览

方法 作用 说明
toString() 返回对象的字符串表示 默认输出 类名@哈希码十六进制,通常建议重写
equals(Object obj) 判断两个对象是否相等 默认比较引用地址(==),通常需要重写以比较内容
hashCode() 返回对象的哈希码 与 equals 保持一致:相等的对象必须有相同的哈希码
getClass() 返回对象的运行时类(Class<?> 对象) 可用于反射,不能重写
clone() 创建并返回对象的一个副本 需实现 Cloneable 接口,否则抛出 CloneNotSupportedException
finalize() 垃圾回收前调用(已废弃) Java 9 起标记为 @Deprecated,不建议使用
notify() 唤醒一个在此对象监视器上等待的线程 用于线程间通信
notifyAll() 唤醒所有在此对象监视器上等待的线程 同上
wait() 让当前线程进入等待状态,直到被唤醒或超时 有三个重载版本:wait()、wait(long timeout)、wait(long timeout, int nanos)

各方法详解

1. toString()

public String toString()
  • 默认实现:getClass().getName() + "@" + Integer.toHexString(hashCode())
  • 用途:方便调试和日志输出,几乎所有 Java Bean 都会重写此方法以展示有意义的内容。

2. equals(Object obj)

public boolean equals(Object obj)
  • 默认实现:return (this == obj);(比较引用地址)
  • 重写原则:自反性、对称性、传递性、一致性、非空性(x.equals(null) 返回 false)
  • 与 hashCode 的约定:如果两个对象 equals 返回 true,它们的 hashCode 必须相同;反之不必然。

3. hashCode()

public int hashCode()
  • 默认实现:通常是对象的内存地址转换成的整数(但非绝对)
  • 用途:用于散列数据结构(如 HashMap、HashSet)的快速查找
  • 重写规则:与 equals 保持一致,尽量让不同对象的哈希码均匀分布以减少碰撞。

4. getClass()

public final native Class<?> getClass()
  • 返回当前对象的运行时类对象,可用于反射获取类的信息(方法、字段、构造器等)。
  • 不能重写,是 final 方法。

5. clone()

protected native Object clone() throws CloneNotSupportedException
  • 创建并返回对象的浅拷贝(shallow copy)。
  • 必须实现 Cloneable 接口(标记接口),否则调用时会抛出 CloneNotSupportedException。
  • 若需要深拷贝,需自行重写该方法。

使用 clone() 实现深拷贝的完整 Java 示例。我们将创建一个 Person 类,它包含一个引用类型的成员 Address。通过重写 clone() 方法,我们手动复制 Address 对象,从而实现深拷贝。

// Address 类,也需要实现 Cloneable 并重写 clone()
class Address implements Cloneable {
String city;
String street;

public Address(String city, String street) {
this.city = city;
this.street = street;
}

@Override
protected Object clone() throws CloneNotSupportedException {
// Address 只有基本类型字段(String 是不可变的,但这里视为值),浅拷贝即可
return super.clone();
}

@Override
public String toString() {
return city + ", " + street;
}
}

// Person 类,包含 Address 引用
class Person implements Cloneable {
String name;
int age;
Address address;

public Person(String name, int age, Address address) {
this.name = name;
this.age = age;
this.address = address;
}

@Override
protected Object clone() throws CloneNotSupportedException {
// 第一步:调用父类的 clone() 进行浅拷贝(复制基本类型和引用)
Person cloned = (Person) super.clone();
// 第二步:手动深拷贝引用类型成员(address)
cloned.address = (Address) address.clone(); // 调用 Address 的 clone()
return cloned;
}

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

// 测试深拷贝
public class DeepCloneExample {
public static void main(String[] args) throws CloneNotSupportedException {
Address addr = new Address("北京", "长安街");
Person p1 = new Person("张三", 30, addr);

// 深拷贝
Person p2 = (Person) p1.clone();

System.out.println("===== 拷贝前 =====");
System.out.println("p1: " + p1);
System.out.println("p2: " + p2);

// 修改 p2 的 address 城市
p2.address.city = "上海";
p2.address.street = "南京路";

System.out.println("\n===== 修改 p2 的 address 后 =====");
System.out.println("p1: " + p1); // p1 的 address 不变(深拷贝成功)
System.out.println("p2: " + p2);

// 验证 p1 和 p2 的 address 不是同一个对象
System.out.println("\np1.address == p2.address ? " + (p1.address == p2.address)); // false
}
}

运行结果:

image-20261018190300809

6. finalize()(已废弃)

@Deprecated(since="9")
protected void finalize() throws Throwable
  • 在垃圾回收器回收对象之前调用一次,用于释放非 Java 资源(如文件句柄)。
  • 不推荐使用:调用时机不确定、性能开销大、可能导致对象复活等问题。建议使用 try-with-resources 或 Cleaner(Java 9+)替代。

7. notify()、notifyAll()、wait()

这三个方法用于线程间的协作(生产者-消费者模式等),必须在同步块(synchronized)内调用,否则会抛出 IllegalMonitorStateException。

  • wait():让当前线程释放锁并进入等待状态,直到其他线程调用 notify() 或 notifyAll() 唤醒它。
  • notify():随机唤醒一个在此对象监视器上等待的线程。
  • notifyAll():唤醒所有在此对象监视器上等待的线程。

其他重要方法

方法 说明
registerNatives() 私有静态方法,用于注册本地方法,由 JVM 在类加载时调用
finalize() 如上所述,已废弃
wait(long timeout, int nanos) 更精细的超时控制,纳秒部分会被近似处理

Math类

image-20261018190614450

java.lang.Math 是 Java 提供的数学工具类,包含用于执行基本数学运算的静态方法,如指数、对数、平方根、三角函数等。该类是 final 的,构造器私有,因此不能实例化,所有方法均通过类名直接调用。


常用常量

常量 说明
Math.PI 圆周率 π(≈3.141592653589793)
Math.E 自然常数 e(≈2.718281828459045)

常用方法分类

1. 三角函数与角度转换

方法 说明
sin(double a) 正弦(弧度)
cos(double a) 余弦(弧度)
tan(double a) 正切(弧度)
asin(double a) 反正弦(返回弧度)
acos(double a) 反余弦(返回弧度)
atan(double a) 反正切(返回弧度)
toRadians(double angdeg) 角度转弧度
toDegrees(double angrad) 弧度转角度

2. 指数、对数与幂运算

方法 说明
exp(double a) e^a
log(double a) 自然对数 ln(a)
log10(double a) 以10为底的对数
pow(double a, double b) a^b
sqrt(double a) 平方根
cbrt(double a) 立方根
hypot(double x, double y) √(x²+y²),防止中间溢出

3. 取整运算

方法 说明
ceil(double a) 向上取整(返回 double)
floor(double a) 向下取整(返回 double)
rint(double a) 四舍五入取整,返回最接近的整数(double),遇.5时取偶数
round(float a) 四舍五入返回 int
round(double a) 四舍五入返回 long

4. 绝对值、最大值、最小值

方法 说明
abs(int/long/float/double a) 绝对值
max(int/long/float/double a, int/long/float/double b) 较大值
min(int/long/float/double a, int/long/float/double b) 较小值

5. 随机数与符号

方法 说明
random() 返回 [0.0, 1.0) 的随机 double
signum(double d) 返回符号函数:负数→-1.0,零→0.0,正数→1.0
copySign(double magnitude, double sign) 用 sign 的符号替换 magnitude 的符号

6. 其他实用方法

方法 说明
IEEEremainder(double f1, double f2) IEEE 754 标准的余数运算
nextAfter(double start, double direction) 返回 start 向 direction 方向的下一个浮点数
nextUp(double d) 返回 d 正方向的下一个浮点数
ulp(double d) 返回 d 的最后一个单位精度(unit in the last place)

使用示例

System.out.println(Math.PI);                    // 3.141592653589793
System.out.println(Math.abs(-5)); // 5
System.out.println(Math.max(10, 20)); // 20
System.out.println(Math.sqrt(25)); // 5.0
System.out.println(Math.pow(2, 3)); // 8.0
System.out.println(Math.round(3.6)); // 4
System.out.println(Math.random()); // 0.0 ~ 1.0 之间的随机数
System.out.println(Math.sin(Math.toRadians(30))); // 0.5(30度的正弦)

注意事项

  • 所有方法均为 public static,直接通过 Math.方法名() 调用。
  • 涉及浮点数运算时注意精度问题(如 0.1 + 0.2 的结果不是精确的 0.3)。
  • random() 返回的是伪随机数,如果需要更安全的随机数,可使用 java.security.SecureRandom。
  • 对于整数运算,Math.abs(Integer.MIN_VALUE) 会返回负数(溢出),需注意边界情况。

Random类

image-20261018192129424

java.util.Random 是 Java 提供的伪随机数生成器,用于生成各种类型的随机数(如 int、long、float、double、boolean 等)。与 Math.random() 相比,Random 类功能更丰富、更灵活。


核心特点

  • 伪随机:基于线性同余算法生成,可通过种子(seed)控制随机序列。
  • 线程不安全:多线程环境下建议使用 ThreadLocalRandom(Java 7+)或 SecureRandom。
  • 可重复性:给定相同种子,生成的随机数序列完全相同(便于测试复现)。

常用构造器

构造器 说明
Random() 以当前系统时间(纳秒)作为种子,每次运行结果不同
Random(long seed) 指定种子,每次运行生成相同的随机序列

常用方法

方法 返回类型 说明
nextInt() int 返回一个随机 int(范围:-2³¹ ~ 2³¹-1)
nextInt(int bound) int 返回 [0, bound) 之间的随机 int
nextLong() long 返回一个随机 long
nextFloat() float 返回 [0.0, 1.0) 之间的随机 float
nextDouble() double 返回 [0.0, 1.0) 之间的随机 double
nextBoolean() boolean 返回一个随机 boolean
nextBytes(byte[] bytes) void 用随机字节填充给定的 byte 数组
setSeed(long seed) void 重新设置种子
doubles() / ints() / longs() DoubleStream / IntStream / LongStream 生成无限随机数流(Java 8+)

使用示例

import java.util.Random;

public class RandomDemo {
public static void main(String[] args) {
Random rand = new Random(); // 默认种子

// 生成 0~99 的随机整数
int randomInt = rand.nextInt(100);

// 生成 50~100 的随机整数
int rangeRandom = rand.nextInt(51) + 50; // nextInt(51) 返回 0~50,加上 50 得 50~100

// 生成随机小数
double randomDouble = rand.nextDouble(); // [0.0, 1.0)

// 生成随机布尔值
boolean randomBool = rand.nextBoolean();

// 指定种子的可重复随机
Random fixedRand = new Random(42);
System.out.println(fixedRand.nextInt(10)); // 每次运行都输出相同结果
}
}

Random vs Math.random()

对比项 Math.random() Random 类
返回类型 double [0.0, 1.0) 多种类型(int、long、double、boolean 等)
灵活性 单一功能 丰富的方法,可指定范围、种子
性能 内部使用 Random 实例,稍慢 更高效(可复用对象)
种子控制 无法指定种子 可指定种子实现可重复性

线程安全替代方案

  • ThreadLocalRandom(Java 7+):每个线程独立的随机数生成器,性能更高,适合并发场景。
int r = ThreadLocalRandom.current().nextInt(100);
  • SecureRandom:密码学安全的强随机数生成器,用于加密场景(如生成令牌、密钥)。
SecureRandom secureRandom = new SecureRandom();
byte[] key = new byte[16];
secureRandom.nextBytes(key);

一句话总结

Random 是 Java 中生成伪随机数的工具类,支持多种数据类型和范围控制,可通过种子复现随机序列,适用于大多数非安全场景的随机需求。

File类

image-20261018192911773

java.io.File 是 Java 中用于表示文件和目录路径名 的抽象表示。它提供了对文件和目录的基本操作,如创建、删除、重命名、获取属性、遍历目录等,但不包含读写文件内容 的功能(读写内容需要使用 FileInputStream、FileOutputStream、FileReader、FileWriter 等流类)。


核心概念

  • File 对象 代表一个路径(可以是文件或目录),不一定对应磁盘上真实存在的文件。
  • 平台无关性:内部使用系统相关的路径分隔符(Windows 为 \,Linux/macOS 为 /),但可以通过 File.separator 获取当前系统的分隔符。
  • 不可变:一旦创建,File 对象表示的路径不会改变,但可以调用方法创建新的 File 对象表示其他路径。

常用构造器

构造器 说明
File(String pathname) 通过路径名字符串创建 File 对象
File(String parent, String child) 通过父路径和子路径创建
File(File parent, String child) 通过父 File 对象和子路径创建
File(URI uri) 通过 URI 创建(如 file:///C:/test.txt)

常用方法分类

1. 路径与名称操作

方法 说明
getName() 返回文件名或目录名
getPath() 返回构造时传入的路径字符串
getAbsolutePath() 返回绝对路径字符串
getCanonicalPath() 返回规范化的绝对路径(去除 .、.. 等,可能抛出 IOException)
toURI() 转换为 file: 协议的 URI
getParent() / getParentFile() 返回父路径字符串 / 父 File 对象

2. 文件/目录状态判断

方法 说明
exists() 文件或目录是否存在
isFile() 是否为文件(不是目录)
isDirectory() 是否为目录
isHidden() 是否隐藏
canRead() 是否可读
canWrite() 是否可写
canExecute() 是否可执行
length() 文件大小(字节),目录返回 0 或不确定
lastModified() 最后修改时间戳(毫秒)

3. 创建与删除

方法 说明
createNewFile() 创建新文件(不存在时创建,返回 boolean)
mkdir() 创建单级目录
mkdirs() 创建多级目录(包括缺失的父目录)
delete() 删除文件或空目录
deleteOnExit() 虚拟机退出时删除(用于临时文件清理)

4. 目录遍历

方法 说明
list() 返回目录下所有文件名/目录名的字符串数组
listFiles() 返回目录下所有 File 对象数组
list(FilenameFilter filter) 带过滤器的文件名列表
listFiles(FileFilter filter) 带过滤器的 File 对象列表
listRoots() 静态方法,返回系统根目录(如 Windows 的 C:\、D:\)

5. 重命名与临时文件

方法 说明
renameTo(File dest) 重命名/移动文件或目录(跨平台行为不一致)
createTempFile(String prefix, String suffix) 静态方法,在默认临时目录创建临时文件
createTempFile(String prefix, String suffix, File directory) 在指定目录创建临时文件

使用示例

import java.io.File;
import java.io.IOException;

public class FileDemo {
public static void main(String[] args) throws IOException {
// 创建 File 对象
File file = new File("example.txt");

// 检查是否存在,不存在则创建
if (!file.exists()) {
boolean created = file.createNewFile();
System.out.println("文件创建成功:" + created);
}

// 获取属性
System.out.println("文件名:" + file.getName());
System.out.println("绝对路径:" + file.getAbsolutePath());
System.out.println("文件大小:" + file.length() + " 字节");
System.out.println("是否可读:" + file.canRead());
System.out.println("最后修改时间:" + file.lastModified());

// 创建目录
File dir = new File("myDir/subDir");
if (dir.mkdirs()) {
System.out.println("目录创建成功");
}

// 遍历目录
File root = new File(".");
File[] files = root.listFiles();
for (File f : files) {
System.out.println(f.getName() + (f.isDirectory() ? " [目录]" : " [文件]"));
}

// 删除文件
file.delete();
}
}

递归遍历目录及子目录

package com.it.day02;

import java.io.File;

/**
* @author zhangzhicai
* @version 1.0
* @description: TODO
* @date 2026/10/18 19:31
*/
public class FileDemo {
public static void main(String[] args) {
File file = new File(".");
list(file);
}

private static void list(File file) {
File[] files = file.listFiles();
for (File f : files) {
if (f.isDirectory()) {
System.out.println(f.getName()+"[目录]");
list(f);
} else {
System.out.println(f.getName()+"[文件]");
}
}
}
}

注意事项

  • 不处理文件内容:File 类只管理路径和元数据,读写文件内容需使用流(FileInputStream / FileOutputStream / FileReader / FileWriter)或 Scanner、Files(Java 7+ NIO.2)。
  • 安全性:某些操作(如创建、删除、读取属性)可能受系统权限限制,会抛出 SecurityException。
  • 跨平台路径分隔符:推荐使用 File.separator 或 File.separatorChar 拼接路径,避免硬编码 / 或 \。
  • Java 7+ 推荐使用 java.nio.file.Path 和 Files 类:功能更强大、异常处理更好、支持符号链接等高级特性。File 类虽未废弃,但新项目建议优先考虑 NIO.2 API。

包装类

Java 为八种基本数据类型提供了对应的包装类,使基本类型能够以对象的形式参与面向对象的操作(如放入集合、泛型、反射等)。

基本类型 包装类 父类
byte Byte Number
short Short Number
int Integer Number
long Long Number
float Float Number
double Double Number
char Character Object
boolean Boolean Object

核心特性

  1. 自动装箱/拆箱(Java 5+):编译器自动在基本类型和包装类之间转换。
Integer a = 10;       // 自动装箱:Integer.valueOf(10)
int b = a; // 自动拆箱:a.intValue()
  1. 缓存机制:部分包装类对常用值进行缓存,节省内存。
  • Integer:默认缓存 -128 ~ 127(可通过 JVM 参数调整上限)。
  • Long:缓存 -128 ~ 127。
  • Short、Byte:缓存全部值(-128 ~ 127)。
  • Character:缓存 0 ~ 127(ASCII 字符)。
  • Boolean:缓存 TRUE 和 FALSE 两个常量。
  • Float、Double:无缓存。

常用方法(以 Integer 为例,其他类似)

1. 构造与转换

方法 说明
Integer(int value) 构造(已不推荐,优先用 valueOf)
Integer(String s) 从字符串构造(可能抛出 NumberFormatException)
valueOf(int i) 返回 Integer 实例,利用缓存(推荐)
valueOf(String s) 从字符串解析并返回 Integer 实例
parseInt(String s) 将字符串解析为 int 基本类型
parseInt(String s, int radix) 按指定进制解析
intValue() 返回 int 值(拆箱)
longValue()、floatValue()、doubleValue() 转换为其他数值类型

2. 字符串与进制转换

方法 说明
toString() 返回十进制字符串
toString(int i) 静态方法,将 int 转为字符串
toBinaryString(int i) 二进制字符串(无前导零)
toOctalString(int i) 八进制字符串
toHexString(int i) 十六进制字符串(小写)
parseInt(String s, int radix) 按 radix 进制解析字符串

3. 比较与判断

方法 说明
equals(Object obj) 比较值是否相等(注意:与 == 区别,== 比较引用)
compareTo(Integer another) 比较大小,返回 -1、0、1
compare(int x, int y) 静态方法,比较两个 int 值
MAX_VALUE / MIN_VALUE 常量,表示 int 的取值范围
SIZE 位数(32)
BYTES 字节数(4)

4. 其他实用方法

方法 说明
reverse(int i) 按位反转
rotateLeft(int i, int distance) 循环左移
rotateRight(int i, int distance) 循环右移
highestOneBit(int i) 返回最高位 1 的值(如 12→8)
lowestOneBit(int i) 返回最低位 1 的值
numberOfLeadingZeros(int i) 前导零个数
bitCount(int i) 二进制中 1 的个数

Character 特有方法

方法 说明
isLetter(char ch) 是否为字母
isDigit(char ch) 是否为数字
isWhitespace(char ch) 是否为空白字符
isUpperCase(char ch) 是否大写
isLowerCase(char ch) 是否小写
toUpperCase(char ch) 转大写
toLowerCase(char ch) 转小写
getNumericValue(char ch) 获取字符代表的数值(如 ‘9’→9)

Boolean 特有方法

方法 说明
booleanValue() 返回基本类型 boolean
parseBoolean(String s) 字符串解析为 boolean(”true”→true,其余→false)
logicalAnd(boolean a, boolean b) 逻辑与(Java 7+)
logicalOr(boolean a, boolean b) 逻辑或
logicalXor(boolean a, boolean b) 逻辑异或

注意事项

  1. == 与 equals 陷阱:自动装箱后,Integer a = 127; Integer b = 127; a == b 返回 true(因为缓存),但 Integer a = 128; Integer b = 128; a == b 返回 false(不同对象)。应始终使用 equals() 比较值。
  2. 空指针风险:包装类可为 null,自动拆箱时可能抛出 NullPointerException。
  3. 性能:频繁的装箱拆箱会产生额外对象,大量循环中建议使用基本类型。

Date类

java.util.Date 是 Java 最早提供的日期时间类(JDK 1.0),用于表示特定的瞬间(精确到毫秒)。但它存在许多设计缺陷,大部分构造和方法已在 Java 8 之后被标记为 @Deprecated(已弃用),官方推荐使用 java.time 包(Java 8+)替代。


核心特性

特性 说明
表示时刻 从 1970-01-01 00:00:00 GMT 开始的毫秒数(时间戳)
可变性 可变(setTime() 等方法可修改,线程不安全)
已弃用 大部分构造和方法(如 getYear()、getMonth()、setHours() 等)已弃用
遗留兼容 仍广泛存在于旧系统和 JDBC 中

构造方法

构造方法 说明
Date() 当前时间(精确到毫秒)
Date(long date) 根据时间戳(毫秒)创建
Date(int year, int month, int day) 已弃用,年份从1900开始,月份0~11
Date(String s) 已弃用,格式不易控

推荐用法:

Date now = new Date();                   // 当前时间
Date specific = new Date(1700000000000L); // 根据时间戳

常用方法(未弃用部分)

方法 说明
getTime() 返回时间戳(毫秒)
setTime(long time) 设置时间戳
before(Date when) 是否早于指定时间
after(Date when) 是否晚于指定时间
compareTo(Date another) 比较先后顺序
equals(Object obj) 比较时间是否相等
toString() 返回默认格式字符串(如 Sun Oct 18 14:35:22 CST 2026)
clone() 克隆对象

示例代码

import java.util.Date;

public class DateDemo {
public static void main(String[] args) {
// 当前时间
Date now = new Date();
System.out.println("当前时间:" + now.toString());
System.out.println("时间戳:" + now.getTime());

// 创建指定时间
Date future = new Date(now.getTime() + 3600_000); // 一小时后
System.out.println("一小时后的时间:" + future);

// 比较
System.out.println("now 是否早于 future?" + now.before(future));
System.out.println("compareTo:" + now.compareTo(future)); // 负数
}
}

Date 的缺陷与弃用原因

  1. 年份偏移:getYear() 返回 year - 1900,月份从 0 开始(0=一月),容易出错。
  2. 线程不安全:Date 是可变的,多线程下修改可能引发问题。
  3. 命名混乱:Date 既表示日期又包含时间,实际上表示的是时刻。
  4. 国际化差:toString() 输出英文/依赖系统区域,难以定制格式。
  5. 缺乏日期计算:加减天数、月份等需要配合 Calendar,复杂且易错。

与 Calendar 和 SimpleDateFormat 的配合(旧式写法)

虽然 Date 本身已弃用,但在 Java 8 之前,通常这样使用:

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;

// 格式化 Date
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String formatted = sdf.format(new Date());
System.out.println(formatted); // 2026-10-18 14:35:22

// 日期运算(使用 Calendar)
Calendar cal = Calendar.getInstance();
cal.setTime(new Date());
cal.add(Calendar.DAY_OF_MONTH, 7); // 加7天
Date weekLater = cal.getTime();

Java 8+ 现代替代方案(推荐)

java.time 包提供了不可变、线程安全、设计清晰的日期时间 API:

旧类 新类 说明
Date Instant 表示时间戳(时刻)
Date + SimpleDateFormat LocalDateTime + DateTimeFormatter 不带时区的日期时间
Calendar LocalDate / LocalTime / ZonedDateTime 日期、时间、带时区时间

现代写法示例:

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

// 当前时间
LocalDateTime now = LocalDateTime.now();
System.out.println(now); // 2026-10-18T14:35:22.123

// 格式化
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formatted = now.format(formatter);
System.out.println(formatted); // 2026-10-18 14:35:22

// 日期运算
LocalDateTime weekLater = now.plusDays(7);
System.out.println(weekLater);

什么时候仍然使用 Date?

  • 遗留系统维护:老项目中大量使用 Date 和 Calendar。
  • JDBC 交互:java.sql.Date、java.sql.Timestamp 继承自 java.util.Date,部分框架仍依赖。
  • 第三方库兼容:某些旧库方法参数要求 Date 类型。

在这种情况下,可以在新旧 API 之间转换:

// Date ↔ Instant
Date date = new Date();
Instant instant = date.toInstant();
Date back = Date.from(instant);

// Date ↔ LocalDateTime(需先转为 ZonedDateTime)
LocalDateTime ldt = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
Date fromLdt = Date.from(ldt.atZone(ZoneId.systemDefault()).toInstant());

一句话总结

Date 是 Java 最早的日期时间类,表示时刻(毫秒时间戳),但因设计缺陷(可变、月份偏移、国际化差)已被弃用,新项目应优先使用 java.time 包(LocalDateTime、Instant 等);仅在维护旧代码或与遗留 API 交互时保留使用。

Java 8 日期时间类(java.time 包)

Java 8 引入了全新的日期时间 API,位于 java.time 包中,解决了旧版 Date 和 Calendar 的诸多缺陷(可变、线程不安全、设计混乱等)。新 API 基于 ISO-8601 标准,所有核心类都是不可变且线程安全 的。


核心类总览

类 说明 示例值
LocalDate 日期(不含时间和时区) 2026-10-18
LocalTime 时间(不含日期和时区) 14:35:22.123
LocalDateTime 日期+时间(不含时区) 2026-10-18T14:35:22
Instant 时间戳(从1970-01-01T00:00:00Z开始的纳秒数) 2026-10-18T06:35:22Z
Duration 时间段(基于秒/纳秒,用于时间差) PT1H30M(1小时30分)
Period 日期间隔(基于年月日) P1Y2M3D(1年2月3天)
ZonedDateTime 带时区的日期时间 2026-10-18T14:35:22+08:00[Asia/Shanghai]
ZoneId 时区标识符 Asia/Shanghai、UTC
DateTimeFormatter 日期时间的格式化与解析 模式如 yyyy-MM-dd HH:mm:ss

1. LocalDate – 日期

// 创建
LocalDate today = LocalDate.now(); // 当前日期(系统时区)
LocalDate specific = LocalDate.of(2026, 10, 18); // 指定日期
LocalDate parsed = LocalDate.parse("2026-10-18"); // 从字符串解析(ISO格式)

// 常用操作
System.out.println(today.getYear()); // 2026
System.out.println(today.getMonthValue()); // 10
System.out.println(today.getDayOfMonth()); // 18
System.out.println(today.getDayOfWeek()); // SUNDAY
System.out.println(today.isLeapYear()); // false(2026不是闰年)

// 日期运算
LocalDate tomorrow = today.plusDays(1);
LocalDate lastMonth = today.minusMonths(1);
LocalDate firstDayOfMonth = today.withDayOfMonth(1);

// 比较
System.out.println(today.isBefore(tomorrow)); // true
System.out.println(today.isAfter(tomorrow)); // false

2. LocalTime – 时间

LocalTime now = LocalTime.now();                      // 当前时间(含毫秒)
LocalTime specific = LocalTime.of(14, 35, 22); // 14:35:22
LocalTime parsed = LocalTime.parse("14:35:22");

System.out.println(now.getHour()); // 14
System.out.println(now.getMinute()); // 35
System.out.println(now.getSecond()); // 22
System.out.println(now.getNano()); // 纳秒部分

// 运算
LocalTime later = now.plusHours(2);
LocalTime earlier = now.minusMinutes(30);

3. LocalDateTime – 日期+时间

LocalDateTime now = LocalDateTime.now();
LocalDateTime specific = LocalDateTime.of(2026, 10, 18, 14, 35, 22);
LocalDateTime parsed = LocalDateTime.parse("2026-10-18T14:35:22");

// 拆分
LocalDate datePart = now.toLocalDate();
LocalTime timePart = now.toLocalTime();

// 运算(同 LocalDate/LocalTime)
LocalDateTime nextWeek = now.plusWeeks(1);

4. Instant – 时间戳

Instant now = Instant.now();                          // 当前UTC时间戳
Instant epoch = Instant.ofEpochMilli(1700000000000L); // 从毫秒创建
Instant fromEpochSecond = Instant.ofEpochSecond(1700000000);

System.out.println(now.toEpochMilli()); // 转毫秒
System.out.println(now.getNano()); // 纳秒部分

常用于记录事件发生的时间点,与 Date 互转:

Date date = Date.from(Instant.now());
Instant instant = date.toInstant();

5. Duration – 时间间隔(秒/纳秒)

LocalTime start = LocalTime.of(10, 0);
LocalTime end = LocalTime.of(12, 30);
Duration duration = Duration.between(start, end);

System.out.println(duration.toHours()); // 2
System.out.println(duration.toMinutes()); // 150
System.out.println(duration.getSeconds()); // 9000

// 创建
Duration twoHours = Duration.ofHours(2);
Duration tenMinutes = Duration.ofMinutes(10);

6. Period – 日期间隔(年/月/日)

LocalDate birth = LocalDate.of(2000, 1, 1);
LocalDate today = LocalDate.now();
Period period = Period.between(birth, today);

System.out.println(period.getYears()); // 26
System.out.println(period.getMonths()); // 9
System.out.println(period.getDays()); // 17

// 创建
Period oneYearTwoMonths = Period.of(1, 2, 0);

7. DateTimeFormatter – 格式化与解析

// 预定义格式
DateTimeFormatter isoDate = DateTimeFormatter.ISO_LOCAL_DATE;
System.out.println(LocalDate.now().format(isoDate)); // 2026-10-18

// 自定义格式
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
String formatted = LocalDateTime.now().format(formatter);
System.out.println(formatted); // 2026/10/18 14:35:22

// 解析
LocalDateTime parsed = LocalDateTime.parse("2026/10/18 14:35:22", formatter);

常用模式字母:

字母 含义 示例
yyyy 四位年份 2026
MM 两位月份 10
dd 两位日期 18
HH 24小时制小时 14
mm 分钟 35
ss 秒 22
SSS 毫秒 123

8. ZonedDateTime – 带时区的日期时间

// 当前时区
ZonedDateTime now = ZonedDateTime.now();
System.out.println(now); // 2026-10-18T14:35:22.123+08:00[Asia/Shanghai]

// 指定时区
ZonedDateTime tokyo = ZonedDateTime.now(ZoneId.of("Asia/Tokyo"));
System.out.println(tokyo); // 2026-10-18T15:35:22.123+09:00[Asia/Tokyo]

// 时区转换
ZonedDateTime shanghai = ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
ZonedDateTime newYork = shanghai.withZoneSameInstant(ZoneId.of("America/New_York"));
System.out.println(newYork); // 2026-10-18T02:35:22.123-04:00[America/New_York]

新旧API互转

旧类 → 新类 新类 → 旧类
new Date().toInstant() Date.from(instant)
calendar.toInstant() GregorianCalendar.from(zdt)
java.sql.Date.valueOf(localDate) localDate.toEpochDay() 再构造
Timestamp.valueOf(localDateTime) Timestamp.from(instant)

使用建议

  • 纯日期(生日、纪念日)→ LocalDate
  • 纯时间(营业时间、闹钟)→ LocalTime
  • 完整时间点(订单时间、日志)→ LocalDateTime(若需时区则用 ZonedDateTime)
  • 时间戳(记录事件顺序)→ Instant
  • 时间差(耗时统计)→ Duration
  • 日期间隔(年龄、合同有效期)→ Period
  • 格式化/解析 → DateTimeFormatter

一句话总结

Java 8 的 java.time 包提供了不可变、线程安全、设计清晰的日期时间类,包括 LocalDate、LocalTime、LocalDateTime、Instant、Duration、Period 和 DateTimeFormatter,完全取代了旧的 Date 和 Calendar,是现代 Java 开发的标配。

String类

java.lang.String 是 Java 中最常用的类之一,用于表示不可变的字符序列。一旦创建,String 对象的内容就不能被更改(任何看似修改的操作都会返回一个新的 String 对象)。


核心特性

特性 说明
不可变性 字符串内容不可变,保证了线程安全和哈希缓存的高效
字符串常量池 字面量字符串(如 "hello")存储在堆中的字符串常量池,相同内容的字面量复用同一个对象
final 类 不能被继承
实现接口 Serializable、Comparable<String>、CharSequence

创建方式

// 方式一:字面量(推荐,会放入常量池)
String s1 = "hello";

// 方式二:new 关键字(强制创建新对象,不进入常量池)
String s2 = new String("hello");

// 方式三:字符数组
char[] chars = {'h', 'e', 'l', 'l', 'o'};
String s3 = new String(chars);

// 方式四:字节数组(可指定编码)
byte[] bytes = {104, 101, 108, 108, 111};
String s4 = new String(bytes, StandardCharsets.UTF_8);

注意:s1 == s2 返回 false,因为字面量和 new 创建的是不同对象。应使用 equals() 比较内容。


常用方法分类

1. 比较方法

方法 说明
equals(Object obj) 比较内容是否相等(区分大小写)
equalsIgnoreCase(String str) 忽略大小写比较
compareTo(String str) 按字典顺序比较,返回 int
compareToIgnoreCase(String str) 忽略大小写的字典比较
contentEquals(CharSequence cs) 与 CharSequence 比较内容

2. 查找与判断

方法 说明
length() 返回字符串长度
isEmpty() 是否为空字符串(长度为0)
isBlank() 是否为空或仅含空白字符(Java 11+)
charAt(int index) 返回指定索引处的 char
indexOf(String str) 返回子串首次出现的索引,未找到返回 -1
lastIndexOf(String str) 返回子串最后一次出现的索引
contains(CharSequence s) 是否包含指定子串
startsWith(String prefix) 是否以指定前缀开头
endsWith(String suffix) 是否以指定后缀结尾
matches(String regex) 是否匹配正则表达式

3. 截取与提取

方法 说明
substring(int beginIndex) 从 beginIndex 截取到末尾
substring(int begin, int end) 截取 [begin, end) 范围的子串
split(String regex) 按正则分割为字符串数组
toCharArray() 转换为 char 数组
getBytes() 转换为字节数组(默认编码)
chars() 返回 IntStream(每个字符的 Unicode 码点)

4. 转换与替换

方法 说明
toUpperCase() 转大写
toLowerCase() 转小写
trim() 去除首尾空白字符
strip() 去除首尾空白(Java 11+,支持 Unicode)
replace(char old, char new) 替换所有指定字符
replace(CharSequence target, CharSequence replacement) 替换所有匹配的子串
replaceAll(String regex, String replacement) 用正则替换所有匹配
replaceFirst(String regex, String replacement) 用正则替换第一个匹配
concat(String str) 拼接字符串(等价于 +)
join(CharSequence delimiter, CharSequence... elements) 静态方法,用分隔符拼接多个字符串
String s = String.join("-", "1", "2", "3", "4", "5");
System.out.println(s);

1-2-3-4-5

5. 格式化与拼接

方法 说明
format(String format, Object... args) 静态方法,格式化字符串(类似 printf)
valueOf(X x) 静态方法,将基本类型/对象转为字符串
repeat(int count) 重复字符串 count 次(Java 11+)
indent(int n) 缩进/取消缩进(Java 12+)

6. 检查与转换(Java 8+ Stream 相关)

方法 说明
lines() 返回按换行符分割的行流(Stream)
transform(Function<String, R> f) 对字符串应用函数并返回结果(Java 12+)

不可变性的好处与代价

好处 代价
线程安全,无需同步 频繁拼接时产生大量临时对象,性能差
可作为 HashMap 的键(哈希值稳定) 字符串拼接用 + 在循环中效率极低
字符串常量池节省内存 应使用 StringBuilder/StringBuffer 处理可变字符串
安全性(如文件路径、SQL 注入预防)

String、StringBuilder、StringBuffer 对比

类 可变性 线程安全 性能
String 不可变 安全(天然) 拼接慢
StringBuilder 可变 不安全 快
StringBuffer 可变 安全(方法同步) 较慢(有同步开销)

使用建议:

  • 单线程下字符串拼接:StringBuilder
  • 多线程下字符串拼接:StringBuffer
  • 少量拼接或不需修改:String 的 concat 或 +

intern() 方法

String s1 = new String("hello");
String s2 = s1.intern(); // 从常量池中获取 "hello"
String s3 = "hello";
System.out.println(s2 == s3); // true

intern() 将字符串对象加入常量池(如果池中已有则返回池中的引用),可用于减少重复字符串的内存占用。


示例代码

public class StringDemo {
public static void main(String[] args) {
String str = " Hello, World! ";

// 常用操作
System.out.println(str.length()); // 17
System.out.println(str.trim()); // "Hello, World!"
System.out.println(str.toLowerCase()); // " hello, world! "
System.out.println(str.replace("World", "Java")); // " Hello, Java! "
System.out.println(str.contains("Hello")); // true
System.out.println(str.indexOf("World")); // 8

// 分割
String csv = "apple,banana,orange";
String[] fruits = csv.split(",");
System.out.println(Arrays.toString(fruits)); // [apple, banana, orange]

// 拼接
String joined = String.join("-", "A", "B", "C");
System.out.println(joined); // A-B-C

// 格式化
String formatted = String.format("姓名:%s,年龄:%d", "张三", 25);
System.out.println(formatted); // 姓名:张三,年龄:25
}
}

一句话总结

String 是不可变的字符序列,提供了丰富的字符串操作方法,适合存储和查询固定的文本;需要频繁修改字符串时应使用 StringBuilder 或 StringBuffer 以提高性能。

StringBuffer类

image-20261018202746311

java.lang.StringBuffer 是 Java 中另一个可变字符序列 类,与 StringBuilder 功能几乎完全一致,但它的方法是线程安全 的(几乎所有公共方法都加了 synchronized 关键字)。


核心特性

特性 说明
可变性 内容可修改(追加、插入、删除、替换等)
线程安全 方法使用 synchronized 同步,适合多线程环境
初始容量 默认 16 个字符,自动扩容策略与 StringBuilder 相同
实现接口 Appendable、Serializable、CharSequence

与 StringBuilder 的对比

对比维度 StringBuffer StringBuilder
线程安全 ✅ 安全(同步) ❌ 不安全
性能 较慢(有同步开销) 更快
方法签名 几乎相同 几乎相同
引入版本 JDK 1.0 JDK 5.0
适用场景 多线程共享可变字符串 单线程或局部变量

常用方法(与 StringBuilder 完全一致)

方法 说明
append(X x) 追加各种类型的数据
insert(int offset, X x) 在指定位置插入
delete(int start, int end) 删除 [start, end) 范围的字符
deleteCharAt(int index) 删除指定位置的字符
replace(int start, int end, String str) 替换指定范围的字符
reverse() 反转字符序列
charAt(int index) 获取指定位置的字符
setCharAt(int index, char ch) 设置指定位置的字符
substring(int start) / substring(int start, int end) 截取子串(返回 String)
length() 返回当前字符数
capacity() 返回当前容量
ensureCapacity(int min) 确保容量至少为指定值
trimToSize() 将容量调整为当前长度
setLength(int newLength) 设置长度
toString() 转换为 String

使用示例

public class StringBufferDemo {
public static void main(String[] args) {
// 多线程环境下使用 StringBuffer
StringBuffer sb = new StringBuffer();

// 模拟多个线程同时追加
Runnable task = () -> {
for (int i = 0; i < 100; i++) {
sb.append(Thread.currentThread().getName()).append(" ");
}
};

Thread t1 = new Thread(task, "T1");
Thread t2 = new Thread(task, "T2");

t1.start();
t2.start();

try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println("最终长度:" + sb.length()); // 结果正确,不会出现数据竞争
}
}

何时使用 StringBuffer?

  • 多线程环境 中,多个线程需要同时访问和修改同一个字符串缓冲区。
  • 使用 StringBuilder 可能出现数据不一致或 ConcurrentModificationException 的场景。
  • 如果确定只在单线程中使用,应优先选择 StringBuilder 以获得更好的性能。

历史背景

StringBuffer 是 JDK 1.0 就存在的早期类,当时 Java 还没有 StringBuilder。JDK 5.0 引入了 StringBuilder,作为 StringBuffer 的单线程非同步版本,旨在提高性能。官方建议:单线程优先使用 StringBuilder,多线程才用 StringBuffer。


性能对比示例

// StringBuffer 较慢(同步开销)
long start = System.nanoTime();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < 100000; i++) {
sb.append(i);
}
long time1 = System.nanoTime() - start;

// StringBuilder 更快
start = System.nanoTime();
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 100000; i++) {
builder.append(i);
}
long time2 = System.nanoTime() - start;

System.out.println("StringBuffer: " + time1 / 1_000_000 + " ms");
System.out.println("StringBuilder: " + time2 / 1_000_000 + " ms");
// StringBuilder 通常快 2~3 倍

一句话总结

StringBuffer 是线程安全的可变字符序列,方法与 StringBuilder 相同但带有同步锁,适用于多线程共享字符串缓冲区的场景;单线程下应优先使用 StringBuilder 以获得更高性能。

StringBuilder类

image-20261018202440638

java.lang.StringBuilder 是 Java 中用于处理可变字符序列 的类,专门用来高效地拼接和修改字符串。它与 StringBuffer 功能几乎相同,但不是线程安全的,因此在单线程环境下性能更优。


为什么需要 StringBuilder?

  • String 不可变:每次拼接(如 str += "abc")都会创建新的 String 对象,频繁操作会导致大量临时对象,浪费内存和 CPU。
  • StringBuilder 可变:内部维护一个可扩容的字符数组,直接在原对象上修改,避免了创建大量中间对象。

核心特性

特性 说明
可变性 内容可修改(追加、插入、删除、替换等)
线程不安全 不同步,单线程下速度快
初始容量 默认 16 个字符,超出时自动扩容(通常翻倍 + 2)
实现接口 Appendable、Serializable、CharSequence

常用构造器

构造器 说明
StringBuilder() 初始容量 16
StringBuilder(int capacity) 指定初始容量
StringBuilder(String str) 初始内容为 str,容量为 str.length() + 16

常用方法

1. 追加(append)

最常用的方法,支持各种类型(String、int、double、char、boolean、Object 等)。

StringBuilder sb = new StringBuilder();
sb.append("Hello").append(' ').append(123).append(true);
System.out.println(sb); // Hello 123true

2. 插入(insert)

在指定位置插入内容,原位置后的字符向后移动。

StringBuilder sb = new StringBuilder("HelloWorld");
sb.insert(5, ",");
System.out.println(sb); // Hello,World

3. 删除(delete / deleteCharAt)

方法 说明
delete(int start, int end) 删除 [start, end) 范围内的字符
deleteCharAt(int index) 删除指定位置的单个字符
StringBuilder sb = new StringBuilder("abcdef");
sb.delete(1, 3); // 删除 bc → "adef"
sb.deleteCharAt(0); // 删除 a → "def"

4. 替换(replace)

替换指定范围内的字符为新字符串。

StringBuilder sb = new StringBuilder("Hello World");
sb.replace(6, 11, "Java"); // 替换 "World" → "Hello Java"

5. 反转(reverse)

将字符序列反转。

StringBuilder sb = new StringBuilder("abc");
sb.reverse(); // "cba"

6. 获取与修改

方法 说明
charAt(int index) 获取指定位置的字符
setCharAt(int index, char ch) 设置指定位置的字符
substring(int start) / substring(int start, int end) 截取子串(返回 String)
length() 返回当前字符数
capacity() 返回当前容量(可能大于 length)
ensureCapacity(int minimumCapacity) 确保容量至少为指定值
trimToSize() 将容量缩小为当前长度(节省空间)
setLength(int newLength) 设置长度(缩短会丢弃尾部字符,加长会用 ‘\0’ 填充)

7. 转换为 String

String result = sb.toString(); // 将 StringBuilder 内容转为不可变的 String

完整示例

public class StringBuilderDemo {
public static void main(String[] args) {
// 构建 SQL 语句示例
StringBuilder sql = new StringBuilder();
sql.append("SELECT * FROM users WHERE 1=1");

String name = "张三";
int age = 25;

if (name != null) {
sql.append(" AND name = '").append(name).append("'");
}
if (age > 0) {
sql.append(" AND age > ").append(age);
}

System.out.println(sql.toString());
// SELECT * FROM users WHERE 1=1 AND name = '张三' AND age > 25
}
}

StringBuilder vs StringBuffer vs String

对比维度 String StringBuilder StringBuffer
可变性 不可变 可变 可变
线程安全 安全(不可变天然安全) 不安全 安全(方法加 synchronized)
性能(单线程) 拼接极慢 最快 较慢(同步开销)
性能(多线程) 安全但慢 不安全 安全且适中
适用场景 固定字符串、少量操作 单线程大量拼接/修改 多线程共享字符串缓冲区

使用建议

  • 单线程 中频繁拼接字符串 → 首选 StringBuilder
  • 多线程 中需要共享可变字符串 → 使用 StringBuffer
  • 少量拼接或代码简洁性优先 → 可以使用 String 的 +(编译器会优化为 StringBuilder,但循环中仍建议手动使用 StringBuilder)

一句话总结

StringBuilder 是可变的字符序列,专为单线程下高效拼接和修改字符串而设计,比 String 的 ‘+’ 拼接性能提升数十倍,是 Java 字符串处理的核心工具之一。


八、集合

Java 集合框架

Java 集合框架是一套用于存储和操作对象组的统一架构,位于 java.util 包中。它提供了接口(定义行为)、实现类(具体数据结构)和算法(排序、搜索等),极大地提高了开发效率和代码复用性。


核心接口层次结构

Iterable (接口)
└── Collection (接口)
├── List (接口) → 有序、可重复、有索引
├── Set (接口) → 无序、不可重复
│ ├── HashSet
│ ├── LinkedHashSet
│ └── SortedSet (接口) → TreeSet
└── Queue (接口) → 队列(FIFO)
├── Deque (接口) → 双端队列
└── PriorityQueue

Map (接口) → 键值对(Key-Value)
├── HashMap
├── LinkedHashMap
├── Hashtable
├── SortedMap (接口) → TreeMap
└── ConcurrentHashMap

一、Collection 接口

Collection 是所有单列集合的根接口,定义了通用的操作方法。

常用方法

方法 说明
add(E e) 添加元素
remove(Object o) 移除元素
size() 返回元素个数
isEmpty() 是否为空
contains(Object o) 是否包含某元素
clear() 清空所有元素
iterator() 返回迭代器
toArray() 转为数组
addAll(Collection<? extends E> c) 添加另一集合的所有元素
removeAll(Collection<?> c) 移除另一集合中包含的元素
retainAll(Collection<?> c) 保留两集合的交集
stream() / parallelStream() 返回流(Java 8+)

二、List 接口

特点:有序(按插入顺序)、允许重复、每个元素有索引。

实现类对比

类 底层结构 特点 线程安全
ArrayList 动态数组 查询快 O(1),增删慢(尾部除外) 否
LinkedList 双向链表 增删快 O(1),查询慢 O(n) 否
Vector 动态数组 与 ArrayList 类似,但方法同步 是(已过时)
Stack 继承 Vector 栈(后进先出 LIFO) 是(已过时)

ArrayList 示例

List<String> list = new ArrayList<>();
list.add("Apple");
list.add("Banana");
list.add(0, "Orange"); // 在索引0插入
String fruit = list.get(1); // Banana
list.remove(0); // 移除索引0
list.contains("Apple"); // true
list.size(); // 2

LinkedList 特有方法

LinkedList<String> linked = new LinkedList<>();
linked.addFirst("First");
linked.addLast("Last");
linked.getFirst();
linked.getLast();
linked.removeFirst();
linked.removeLast();

三、Set 接口

特点:无序(除非使用有序实现)、不允许重复(基于 equals() 和 hashCode())。

实现类对比

类 底层结构 顺序 允许 null 线程安全
HashSet 哈希表(HashMap) 无序 最多一个 否
LinkedHashSet 哈希表 + 链表 按插入顺序 最多一个 否
TreeSet 红黑树 按自然顺序或 Comparator 排序 不允许(可定制) 否

HashSet 示例

Set<String> set = new HashSet<>();
set.add("A");
set.add("B");
set.add("A"); // 重复,不会被添加
System.out.println(set); // [A, B](无序)

TreeSet 示例(自然排序)

Set<Integer> numbers = new TreeSet<>();
numbers.add(5);
numbers.add(1);
numbers.add(3);
System.out.println(numbers); // [1, 3, 5](升序)

四、Queue 接口

特点:先进先出(FIFO),常用于任务调度、缓冲等。

类 说明
PriorityQueue 优先级队列,按自然顺序或 Comparator 出队
LinkedList 也可作为 Queue 使用
ArrayDeque 双端队列,性能优于 LinkedList

Queue 常用方法

操作 抛出异常 返回特殊值
插入 add(e) offer(e)
移除 remove() poll()
检查 element() peek()
Queue<String> queue = new LinkedList<>();
queue.offer("A");
queue.offer("B");
queue.offer("C");
System.out.println(queue.poll()); // A(取出并移除)
System.out.println(queue.peek()); // B(不移除)

PriorityQueue 示例

Queue<Integer> pq = new PriorityQueue<>(); // 最小堆
pq.offer(30);
pq.offer(10);
pq.offer(20);
System.out.println(pq.poll()); // 10(最小的先出)
System.out.println(pq.poll()); // 20

五、Map 接口

特点:键值对存储,键唯一(基于 equals() 和 hashCode()),值可重复。

实现类对比

类 底层结构 顺序 允许 null 线程安全
HashMap 哈希表(数组+链表/红黑树) 无序 键/值均可 否
LinkedHashMap 哈希表+双向链表 按插入顺序或访问顺序 键/值均可 否
TreeMap 红黑树 按键的自然顺序或 Comparator 排序 键不允许,值允许 否
Hashtable 哈希表 无序 键/值均不允许 是(已过时)
ConcurrentHashMap 分段锁/CAS 无序 键/值均不允许 是(推荐)

Map 常用方法

方法 说明
put(K key, V value) 添加键值对
get(Object key) 根据键获取值
remove(Object key) 移除键值对
containsKey(Object key) 是否包含键
containsValue(Object value) 是否包含值
keySet() 返回所有键的 Set
values() 返回所有值的 Collection
entrySet() 返回所有键值对的 Set
getOrDefault(Object key, V defaultValue) 获取值,不存在返回默认值
forEach(BiConsumer) 遍历(Java 8+)

HashMap 示例

Map<String, Integer> map = new HashMap<>();
map.put("Alice", 90);
map.put("Bob", 85);
map.put("Charlie", 95);

// 遍历方式1:增强 for + entrySet
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + " → " + entry.getValue());
}

// 遍历方式2:Lambda(Java 8+)
map.forEach((name, score) -> System.out.println(name + ": " + score));

// 常用操作
int bobScore = map.getOrDefault("Bob", 0); // 85
map.containsKey("David"); // false

TreeMap 示例(按键排序)

Map<Integer, String> treeMap = new TreeMap<>();
treeMap.put(3, "Three");
treeMap.put(1, "One");
treeMap.put(2, "Two");
System.out.println(treeMap); // {1=One, 2=Two, 3=Three}

六、迭代器(Iterator)

所有集合都可通过 iterator() 获取迭代器,用于遍历元素。

List<String> list = Arrays.asList("A", "B", "C");
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String item = it.next();
if (item.equals("B")) {
it.remove(); // 安全删除(不能用 for-each 的 remove)
}
}

增强 for 循环 本质上是语法糖,底层使用迭代器,但不能在遍历时修改集合(会抛 ConcurrentModificationException),此时应显式使用迭代器的 remove()。


七、Collections 工具类

java.util.Collections 提供了大量静态方法操作集合。

方法 说明
sort(List<T> list) 排序(元素需实现 Comparable)
sort(List<T> list, Comparator<? super T> c) 按比较器排序
binarySearch(List<? extends Comparable<? super T>> list, T key) 二分查找
reverse(List<?> list) 反转
shuffle(List<?> list) 随机打乱
max(Collection<? extends T> coll) 最大值
min(Collection<? extends T> coll) 最小值
unmodifiableXXX(collection) 返回不可修改的视图
synchronizedXXX(collection) 返回线程安全的包装集合
List<Integer> nums = new ArrayList<>(Arrays.asList(3, 1, 4, 1, 5));
Collections.sort(nums); // [1, 1, 3, 4, 5]
Collections.reverse(nums); // [5, 4, 3, 1, 1]
int idx = Collections.binarySearch(nums, 3); // 2
List<Integer> unmod = Collections.unmodifiableList(nums);
unmod.add(6); // ❌ UnsupportedOperationException

八、线程安全的集合

方式 说明
Collections.synchronizedXXX() 包装原有集合,所有方法加同步锁
Vector、Hashtable 遗留类,方法同步,性能较差
ConcurrentHashMap 高并发下推荐,分段锁或 CAS
CopyOnWriteArrayList 写时复制,适合读多写少
BlockingQueue 系列 阻塞队列,用于生产者-消费者模式

九、集合的选择指南

需求 推荐实现
快速随机访问(按索引) ArrayList
频繁在中间插入/删除 LinkedList
保证元素唯一性(无序) HashSet
保证元素唯一性(有序) TreeSet / LinkedHashSet
键值对存储(无序) HashMap
键值对存储(按键排序) TreeMap
保持插入顺序的键值对 LinkedHashMap
FIFO 队列 LinkedList / ArrayDeque
优先级队列 PriorityQueue
多线程共享 Map ConcurrentHashMap
多线程共享 List CopyOnWriteArrayList

十、Java 8+ 新增特性

  • Stream API:collection.stream().filter(...).collect(...)
  • Lambda 表达式:简化遍历和比较器
  • Map 新方法:computeIfAbsent、merge、forEach、replaceAll
  • Collectors:收集器(toList、toSet、toMap、groupingBy 等)
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
List<String> filtered = names.stream()
.filter(name -> name.startsWith("A"))
.collect(Collectors.toList());
System.out.println(filtered); // [Alice]

一句话总结

Java 集合框架提供了 List、Set、Queue、Map 四大接口及其丰富实现,覆盖了绝大多数数据结构的场景,配合泛型、迭代器和工具类(Collections、Stream),能高效、安全地管理对象组。


九、IO

Java I/O

一、什么是 Java I/O?

I/O 是 Input/Output(输入/输出)的缩写,指程序与外部设备(文件、键盘、显示器、网络等)之间的数据交换。在 Java 中,I/O 功能主要由 java.io 包提供(传统阻塞 I/O),此外还有 java.nio 包(非阻塞 I/O,Java 1.4+)作为补充。

核心思想:数据以流(Stream) 的形式流动——输入流将数据从外部读入程序,输出流将数据从程序写出到外部。


二、Java I/O 的核心组成

1. 两大体系

体系 抽象基类 处理的数据类型 典型场景
字节流 InputStream(输入)
OutputStream(输出)
二进制数据(图片、视频、音频、任意文件) 文件复制、网络传输、对象序列化
字符流 Reader(输入)
Writer(输出)
文本数据(字符、字符串) 读写文本文件、处理字符串

2. 四种抽象基类

基类 方向 主要方法
InputStream 输入 int read()、int read(byte[] b)、close()
OutputStream 输出 void write(int b)、void write(byte[] b)、flush()、close()
Reader 输入 int read()、int read(char[] cbuf)、close()
Writer 输出 void write(int c)、void write(String str)、flush()、close()

三、流的分类

分类标准 类别 说明 举例
数据单位 字节流 以字节为单位读写 FileInputStream, BufferedOutputStream
字符流 以字符为单位读写(自动处理编码) FileReader, BufferedWriter
方向 输入流 从数据源读到程序 InputStream, Reader 的子类
输出流 从程序写到目标 OutputStream, Writer 的子类
功能角色 节点流 直接连接数据源(文件、数组、管道等) FileInputStream, StringReader
处理流 包装节点流,增加功能(缓冲、转换、对象序列化等) BufferedInputStream, DataOutputStream, ObjectInputStream

四、常用具体流类一览

字节流常用类

类名 类型 作用
FileInputStream 节点流(输入) 从文件读取字节
FileOutputStream 节点流(输出) 向文件写入字节
ByteArrayInputStream 节点流(输入) 从内存中的字节数组读取
ByteArrayOutputStream 节点流(输出) 向内存中的字节数组写入
BufferedInputStream 处理流(输入) 为输入流提供缓冲,提高效率
BufferedOutputStream 处理流(输出) 为输出流提供缓冲,提高效率
DataInputStream 处理流(输入) 读取 Java 基本数据类型(int, double, boolean 等)
DataOutputStream 处理流(输出) 写入 Java 基本数据类型
ObjectInputStream 处理流(输入) 反序列化对象
ObjectOutputStream 处理流(输出) 序列化对象
PipedInputStream 节点流(输入) 与 PipedOutputStream 配合实现线程间通信
PipedOutputStream 节点流(输出) 同上
SequenceInputStream 处理流(输入) 将多个输入流合并成一个
PrintStream 处理流(输出) 格式化输出(System.out 的类型)

字符流常用类

类名 类型 作用
FileReader 节点流(输入) 从文件读取字符(默认平台编码)
FileWriter 节点流(输出) 向文件写入字符(默认平台编码)
BufferedReader 处理流(输入) 提供缓冲和 readLine() 方法
BufferedWriter 处理流(输出) 提供缓冲和 newLine() 方法
InputStreamReader 处理流(输入) 字节流 → 字符流的桥梁,可指定编码
OutputStreamWriter 处理流(输出) 字符流 → 字节流的桥梁,可指定编码
CharArrayReader 节点流(输入) 从字符数组读取
CharArrayWriter 节点流(输出) 向字符数组写入
StringReader 节点流(输入) 从字符串读取
StringWriter 节点流(输出) 向字符串写入
PrintWriter 处理流(输出) 格式化输出(不会抛 IOException)
LineNumberReader 处理流(输入) 继承 BufferedReader,可获取行号

五、详细示例

1. 字节流复制文件(带缓冲)

import java.io.*;

public class ByteStreamCopy {
public static void main(String[] args) {
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("source.jpg"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("dest.jpg"))) {

byte[] buffer = new byte[8192]; // 8KB 缓冲区
int len;
while ((len = bis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
bos.flush(); // 刷新缓冲区
System.out.println("文件复制成功");
} catch (IOException e) {
e.printStackTrace();
}
}
}

2. 字符流读取文本(逐行)

import java.io.*;

public class TextFileRead {
public static void main(String[] args) {
try (BufferedReader reader = new BufferedReader(new FileReader("notes.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

3. 指定编码读写(转换流)

import java.io.*;
import java.nio.charset.StandardCharsets;

public class EncodingDemo {
public static void main(String[] args) {
// 写入 UTF-8 文件
try (OutputStreamWriter osw = new OutputStreamWriter(
new FileOutputStream("utf8.txt"), StandardCharsets.UTF_8);
BufferedWriter bw = new BufferedWriter(osw)) {
bw.write("你好,世界!");
bw.newLine();
bw.write("Hello, World!");
} catch (IOException e) {
e.printStackTrace();
}

// 读取 GBK 文件
try (InputStreamReader isr = new InputStreamReader(
new FileInputStream("gbk.txt"), "GBK");
BufferedReader br = new BufferedReader(isr)) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

4. 数据流读写基本类型

import java.io.*;

public class DataStreamDemo {
public static void main(String[] args) throws IOException {
// 写入
try (DataOutputStream dos = new DataOutputStream(new FileOutputStream("data.bin"))) {
dos.writeInt(100);
dos.writeDouble(3.14);
dos.writeBoolean(true);
dos.writeUTF("Java I/O");
}

// 读取(顺序必须一致)
try (DataInputStream dis = new DataInputStream(new FileInputStream("data.bin"))) {
int i = dis.readInt();
double d = dis.readDouble();
boolean b = dis.readBoolean();
String s = dis.readUTF();
System.out.printf("%d, %.2f, %b, %s%n", i, d, b, s);
}
}
}

5. 对象序列化

import java.io.*;

class Student implements Serializable {
private static final long serialVersionUID = 1L;
String name;
transient int age; // transient 字段不序列化

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

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

public class ObjectStreamDemo {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Student stu = new Student("Alice", 20);

// 序列化
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.ser"))) {
oos.writeObject(stu);
}

// 反序列化
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.ser"))) {
Student deserialized = (Student) ois.readObject();
System.out.println(deserialized); // age 为 0(transient)
}
}
}

六、装饰者模式(流的组合)

Java I/O 的核心设计模式是装饰者模式——你可以将多个处理流层层包裹在节点流上,动态增加功能。

// 从文件读取压缩过的对象数据,并解压
InputStream fileIn = new FileInputStream("objects.gz");
InputStream bufferedIn = new BufferedInputStream(fileIn); // 缓冲
InputStream gzipIn = new GZIPInputStream(bufferedIn); // 解压
ObjectInputStream objectIn = new ObjectInputStream(gzipIn); // 反序列化
Object obj = objectIn.readObject();
objectIn.close();

七、标准 I/O 流

Java 提供了三个全局流对象:

对象 类型 用途
System.in InputStream 标准输入(通常关联键盘)
System.out PrintStream 标准输出(通常关联控制台)
System.err PrintStream 标准错误输出
// 从键盘读取一行
BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in));
System.out.print("请输入:");
String input = consoleReader.readLine();
System.out.println("你输入了:" + input);

八、补充:NIO(New I/O)与 Files 工具类

1. NIO 核心组件

  • Channel:双向通道(如 FileChannel, SocketChannel)
  • Buffer:缓冲区(如 ByteBuffer)
  • Selector:多路复用选择器(用于网络)

2. Java 7 NIO.2(Files 类)

简化文件操作,无需手动管理流:

// 读取所有行
List<String> lines = Files.readAllLines(Paths.get("test.txt"), StandardCharsets.UTF_8);

// 写入
Files.write(Paths.get("out.txt"), "Hello".getBytes());

// 复制
Files.copy(Paths.get("src.txt"), Paths.get("dst.txt"), StandardCopyOption.REPLACE_EXISTING);

// 遍历目录
Files.walk(Paths.get(".")).forEach(System.out::println);

九、总结

  • Java I/O 是程序与外部设备交换数据的桥梁,通过流 实现。
  • 核心包 java.io 提供字节流(InputStream/OutputStream)和字符流(Reader/Writer)两大体系。
  • 流分为节点流(直接连接数据源)和处理流(包装节点流增强功能),通过装饰者模式灵活组合。
  • 常用流包括文件流、缓冲流、数据流、对象流、转换流、打印流等。
  • 推荐使用 try-with-resources 自动管理资源,处理文本时优先用字符流并指定编码。
  • 新项目可考虑使用 NIO 的 Files 工具类简化文件操作,高性能场景使用 NIO 的 Channel/Buffer。

十、泛型

什么是泛型(Generics)

泛型 是 Java 5 引入的一种参数化类型机制,它允许你在定义类、接口和方法时使用类型参数(Type Parameters),从而让代码能够处理多种类型而不牺牲类型安全性。

简单说:泛型让你编写“通用”的代码,在编译时确定具体类型,而不是在运行时强制转换。


一、为什么需要泛型?

在没有泛型之前,Java 使用 Object 作为通用类型,然后进行强制类型转换,存在两个严重问题:

  1. 类型不安全:可以向集合中添加任意类型的对象,编译时不报错,运行时可能 ClassCastException。
  2. 需要手动强转:从集合取出元素时必须强制转换为目标类型,繁琐且容易出错。
// 没有泛型(Java 5 之前)
List list = new ArrayList();
list.add("hello");
list.add(123); // 编译通过,但这是错误的用法

String s = (String) list.get(0); // 必须强转
Integer i = (Integer) list.get(1); // 没问题
String s2 = (String) list.get(1); // 运行时报 ClassCastException!

有了泛型后,编译器可以在编译期检查类型一致性:

// 使用泛型
List<String> list = new ArrayList<>();
list.add("hello");
// list.add(123); // 编译错误!只能添加 String

String s = list.get(0); // 无需强转

二、泛型的基本语法

1. 泛型类

// T 是类型参数,可以是任意标识符(习惯用 T, E, K, V)
public class Box<T> {
private T content;

public void set(T content) {
this.content = content;
}

public T get() {
return content;
}
}

// 使用
Box<String> stringBox = new Box<>();
stringBox.set("Hello");
String value = stringBox.get(); // 自动推断为 String

Box<Integer> intBox = new Box<>();
intBox.set(123);
int num = intBox.get(); // Integer

2. 泛型方法

public class Utils {
// 泛型方法:<T> 声明类型参数,放在返回值前面
public static <T> T getMiddle(T... array) {
return array[array.length / 2];
}
}

// 调用
String middle = Utils.getMiddle("A", "B", "C"); // B
Integer midNum = Utils.getMiddle(1, 2, 3); // 2

3. 泛型接口

public interface Pair<K, V> {
K getKey();
V getValue();
}

public class OrderedPair<K, V> implements Pair<K, V> {
private K key;
private V value;

public OrderedPair(K key, V value) {
this.key = key;
this.value = value;
}

@Override
public K getKey() { return key; }

@Override
public V getValue() { return value; }
}

// 使用
Pair<String, Integer> pair = new OrderedPair<>("age", 30);

三、类型参数命名惯例

参数名 含义
E Element(集合元素)
K Key(键)
V Value(值)
T Type(任意类型)
N Number(数字)
S, U, V 第二、第三、第四个类型

四、通配符(Wildcards)

通配符 ? 用于表示未知类型,主要在方法参数中使用,增加灵活性。

1. 无界通配符 ?

表示任何类型,但不能写入(除了 null)。

public static void printList(List<?> list) {
for (Object obj : list) {
System.out.print(obj + " ");
}
}

2. 上界通配符 ? extends T

表示类型必须是 T 或 T 的子类,只能读取,不能写入(因为不知道具体子类型)。

public static double sumOfNumbers(List<? extends Number> list) {
double sum = 0;
for (Number n : list) {
sum += n.doubleValue();
}
return sum;
}
// 可以传入 List<Integer>, List<Double> 等

3. 下界通配符 ? super T

表示类型必须是 T 或 T 的父类,只能写入,读取时只能作为 Object。

public static void addNumbers(List<? super Integer> list) {
list.add(1);
list.add(2);
// Integer i = list.get(0); // 编译错误,只能作为 Object 读取
Object obj = list.get(0);
}
// 可以传入 List<Integer>, List<Number>, List<Object>

PECS 原则(Producer Extends, Consumer Super):

  • 如果你只从集合中读取数据(生产者),用 ? extends T。
  • 如果你只向集合中写入数据(消费者),用 ? super T。

五、类型擦除(Type Erasure)

Java 的泛型是编译期 实现的,运行时泛型信息会被擦除(替换为边界类型或 Object)。这意味着:

List<String> strings = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
System.out.println(strings.getClass() == integers.getClass()); // true
// 运行时都是 ArrayList.class,泛型信息丢失

擦除规则:

  • 若类型参数无边界(如 <T>),擦除为 Object。
  • 若有上界(如 <T extends Comparable>),擦除为 Comparable。

因此,你不能:

  • 创建泛型数组:new T[10] ❌
  • 使用 instanceof 检查泛型类型:if (obj instanceof List<String>) ❌
  • 在静态上下文中使用类型参数(静态变量、静态方法无法访问类的类型参数,除非方法本身是泛型方法)。

六、泛型与继承

泛型不支持协变(Covariance),即 List<String> 不是 List<Object> 的子类型:

List<String> strs = new ArrayList<>();
// List<Object> objs = strs; // 编译错误!

但是可以使用通配符实现协变:

List<? extends Object> objs = strs; // OK

数组是协变的(历史遗留问题):

String[] strs = new String[10];
Object[] objs = strs; // OK,但运行时可能抛出 ArrayStoreException
objs[0] = 123; // 运行时抛出 ArrayStoreException

七、泛型的限制

  1. 基本类型不能作为类型参数:List<int> 错误,必须用包装类 List<Integer>。
  2. 不能实例化类型参数:new T() 错误。
  3. 不能创建泛型数组:new T[10] 错误。
  4. 不能在静态变量或静态初始化块中使用类的类型参数。
  5. 异常类不能是泛型:class MyException<T> extends Exception 错误。

八、完整示例:泛型栈

public class GenericStack<E> {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_CAPACITY = 10;

@SuppressWarnings("unchecked")
public GenericStack() {
elements = new Object[DEFAULT_CAPACITY]; // 不能直接 new E[]
}

public void push(E item) {
ensureCapacity();
elements[size++] = item;
}

@SuppressWarnings("unchecked")
public E pop() {
if (isEmpty()) throw new EmptyStackException();
E result = (E) elements[--size];
elements[size] = null; // 避免内存泄漏
return result;
}

public boolean isEmpty() { return size == 0; }

private void ensureCapacity() {
if (size == elements.length) {
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
}

// 使用
GenericStack<String> stack = new GenericStack<>();
stack.push("A");
stack.push("B");
System.out.println(stack.pop()); // B

九、总结

  • 泛型 是一种编译时类型安全机制,通过类型参数使代码通用且安全。
  • 消除了强制类型转换,减少了 ClassCastException。
  • 支持泛型类、泛型方法、泛型接口。
  • 通配符(? extends T / ? super T)提供了灵活的协变和逆变支持。
  • 由于类型擦除,运行时泛型信息丢失,带来一些限制。
  • 泛型是现代 Java 编程的基础,广泛用于集合框架、函数式接口、自定义数据结构等

十一、反射

什么是反射(Reflection)

反射 是 Java 提供的一种运行时自省机制,允许程序在运行时 动态地获取类的完整信息(构造器、方法、字段、注解等),并能够操作这些成员(创建对象、调用方法、访问/修改字段),即使在编译时对这些类一无所知。

简单说:反射让代码在运行时“观察”和“操控”自身或其他类的内部结构。


一、反射的核心 API

反射功能集中在 java.lang.reflect 包中,核心类如下:

类 作用
Class<?> 代表一个类或接口,是反射的入口
Constructor<T> 代表类的构造方法
Method 代表类的方法(包括继承的)
Field 代表类的字段(成员变量)
Modifier 解析修饰符(public, static, final 等)
Annotation 代表注解
Array 动态创建和操作数组

二、获取 Class 对象的三种方式

// 方式1:通过类名.class(最安全、性能最好)
Class<String> clazz1 = String.class;

// 方式2:通过对象的 getClass()
String str = "hello";
Class<? extends String> clazz2 = str.getClass();

// 方式3:通过 Class.forName()(最灵活,可动态加载类)
Class<?> clazz3 = Class.forName("java.lang.String");

三、反射的常见操作

1. 获取类的完整信息

Class<?> clazz = User.class;

// 类名
System.out.println(clazz.getName()); // com.example.User
System.out.println(clazz.getSimpleName()); // User

// 修饰符
int mod = clazz.getModifiers();
System.out.println(Modifier.toString(mod)); // public

// 包名
System.out.println(clazz.getPackage().getName()); // com.example

// 父类
System.out.println(clazz.getSuperclass()); // class java.lang.Object

// 实现的接口
Class<?>[] interfaces = clazz.getInterfaces();

2. 创建实例

// 方式1:调用无参构造器(类必须有 public 无参构造)
User user1 = (User) clazz.newInstance(); // 已过时,建议用 Constructor

// 方式2:通过 Constructor 对象(推荐)
Constructor<User> constructor = clazz.getDeclaredConstructor(String.class, int.class);
User user2 = constructor.newInstance("Alice", 25);

// 调用私有构造器
Constructor<User> privateCons = clazz.getDeclaredConstructor(String.class);
privateCons.setAccessible(true); // 绕过访问权限检查
User user3 = privateCons.newInstance("Bob");

3. 调用方法

// 获取方法(参数:方法名,参数类型列表)
Method method = clazz.getMethod("sayHello", String.class);
// 调用(对象,参数值)
Object result = method.invoke(user2, "World");

// 调用私有方法
Method privateMethod = clazz.getDeclaredMethod("secretMethod");
privateMethod.setAccessible(true);
privateMethod.invoke(user2);

// 调用静态方法
Method staticMethod = clazz.getMethod("staticMethod");
staticMethod.invoke(null); // 静态方法不需要对象

4. 访问和修改字段

// 获取公有字段
Field nameField = clazz.getField("name"); // 必须是 public 字段
String name = (String) nameField.get(user2);

// 获取私有字段并修改
Field ageField = clazz.getDeclaredField("age");
ageField.setAccessible(true);
int age = ageField.getInt(user2);
ageField.set(user2, 30); // 修改私有字段的值

5. 操作数组

// 动态创建数组
int[] arr = (int[]) Array.newInstance(int.class, 5);
Array.set(arr, 0, 10);
int val = Array.getInt(arr, 0);

6. 获取注解

// 获取类上的注解
Deprecated dep = clazz.getAnnotation(Deprecated.class);
if (dep != null) { ... }

// 获取方法上的注解
Method m = clazz.getMethod("someMethod");
Annotation[] annotations = m.getAnnotations();

四、完整示例:通过反射执行任意类的任意方法

import java.lang.reflect.Method;

public class ReflectInvoker {
/**
* 动态调用目标对象的方法
* @param obj 目标对象
* @param methodName 方法名
* @param args 参数值
* @return 方法返回值
*/
public static Object invokeMethod(Object obj, String methodName, Object... args) throws Exception {
Class<?> clazz = obj.getClass();
// 获取参数类型列表
Class<?>[] paramTypes = new Class<?>[args.length];
for (int i = 0; i < args.length; i++) {
paramTypes[i] = args[i].getClass();
}
Method method = clazz.getDeclaredMethod(methodName, paramTypes);
method.setAccessible(true);
return method.invoke(obj, args);
}

public static void main(String[] args) throws Exception {
String text = "Hello Reflection";
// 动态调用 String 的 substring 方法
Object result = invokeMethod(text, "substring", 0, 5);
System.out.println(result); // Hello
}
}

五、反射的应用场景

  1. 框架开发(Spring、MyBatis、Hibernate、JUnit 等)
  • Spring IoC 容器通过反射创建 Bean 并注入依赖。
  • MyBatis 通过反射将 SQL 结果集映射为 Java 对象。
  • JUnit 通过反射找到并执行 @Test 标注的方法。
  1. 动态代理(JDK 动态代理基于反射实现 InvocationHandler)

  2. IDE 和开发工具

  • IntelliJ IDEA 的代码补全、重构功能依赖反射分析类结构。
  1. 序列化/反序列化框架(Jackson、Gson 使用反射读写对象字段)

  2. 数据库 ORM(将数据库表记录映射为对象)

  3. 插件系统(动态加载第三方类并调用其方法)


六、反射的优缺点

优点 缺点
运行时动态获取类信息,灵活性极高 性能较低:反射调用比直接调用慢(因为需要解析方法、安全检查等)
可以实现通用的框架代码,降低耦合 破坏封装性:可以访问私有成员,违反面向对象原则
支持动态代理、注解处理等高级特性 安全限制:在某些安全管理器下可能被禁止
是 Java 动态性的基石 代码可读性差:反射代码通常较复杂,不易调试

七、性能优化建议

  • 缓存 Method/Field/Constructor 对象,避免重复获取。
  • 对于高频调用,尽量使用直接调用而非反射。
  • 使用 setAccessible(true) 可以跳过 Java 访问权限检查,略微提升性能。
  • 现代 JVM 会对反射调用进行内联优化(如 invoke 方法的 -Dsun.reflect.inflationThreshold 参数)。

八、注意事项

  1. setAccessible(true) 可能会被 SecurityManager 阻止,尤其是在受限环境中(如 Applet)。
  2. 反射调用方法时参数类型匹配:基本类型与包装类需要精确对应,例如 int.class 与 Integer.class 不同。
  3. 数组的 Class 对象:int[].class 与 int.class 不同,可通过 Class.forName("[I") 获取。
  4. 泛型信息在运行时被擦除:反射无法直接获取泛型的具体类型参数(如 List<String> 中的 String),但可以通过 ParameterizedType 获取声明时的泛型信息。

九、总结

反射是 Java 语言动态特性的核心机制,允许程序在运行时探查和操纵类的内部结构。 虽然它带来了极大的灵活性,但也伴随着性能开销和封装破坏的风险。在实际开发中,反射主要用于框架、工具库和底层基础设施,普通业务代码应尽量避免过度使用。


十二、网络编程

什么是网络编程

网络编程 是指编写能够在不同计算机之间通过网络进行数据交换的程序。在 Java 中,网络编程主要涉及 java.net 包(传统阻塞 I/O)和 java.nio 包(非阻塞 I/O),用于实现客户端-服务器通信、网页请求、远程方法调用等。

简单说:网络编程让程序能够跨越机器边界发送和接收数据。


一、网络编程的核心概念

1. IP 地址与端口号

  • IP 地址:标识网络中的一台主机(如 192.168.1.1、IPv6 地址)。
  • 端口号:标识主机上的一个进程(065535,01023 为知名端口,如 HTTP 80、HTTPS 443)。
  • 套接字(Socket):IP 地址 + 端口号,唯一标识一个网络通信端点。

2. TCP 与 UDP 协议

特性 TCP(传输控制协议) UDP(用户数据报协议)
连接 面向连接(三次握手) 无连接
可靠性 可靠传输(确认重传) 不可靠(可能丢包)
顺序 保证数据有序到达 不保证顺序
速度 相对较慢 较快
适用场景 文件传输、网页浏览、邮件 视频直播、在线游戏、DNS 查询

3. OSI 七层模型与 TCP/IP 四层模型

OSI 七层 TCP/IP 四层 主要协议
应用层 应用层 HTTP, FTP, SMTP, DNS
表示层 ↑ SSL/TLS
会话层 ↑ RPC
传输层 传输层 TCP, UDP
网络层 网际层 IP, ICMP, ARP
数据链路层 网络接口层 Ethernet, PPP
物理层 ↓ 光纤、双绞线

二、Java 网络编程基础(java.net)

1. InetAddress —— IP 地址封装

import java.net.InetAddress;
import java.net.UnknownHostException;

public class InetAddressDemo {
public static void main(String[] args) throws UnknownHostException {
// 获取本机 IP
InetAddress local = InetAddress.getLocalHost();
System.out.println(local.getHostAddress()); // 192.168.x.x
System.out.println(local.getHostName()); // 主机名

// 根据域名获取 IP
InetAddress baidu = InetAddress.getByName("www.baidu.com");
System.out.println(baidu.getHostAddress());

// 获取所有 IP
InetAddress[] all = InetAddress.getAllByName("www.google.com");
for (InetAddress addr : all) {
System.out.println(addr);
}
}
}

运行结果:

image-20261018211612711

2. TCP Socket 编程(Socket / ServerSocket)

服务端流程:

  1. 创建 ServerSocket 绑定端口。
  2. 调用 accept() 阻塞等待客户端连接。
  3. 通过返回的 Socket 获取输入/输出流进行通信。
  4. 关闭资源。

客户端流程:

  1. 创建 Socket 连接指定 IP 和端口。
  2. 获取输入/输出流进行通信。
  3. 关闭资源。
示例:回声服务器(Echo Server)

服务端:

import java.io.*;
import java.net.*;

public class EchoServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8888);
System.out.println("服务器启动,等待客户端连接...");

try (Socket client = serverSocket.accept()) {
System.out.println("客户端已连接:" + client.getInetAddress());
BufferedReader in = new BufferedReader(
new InputStreamReader(client.getInputStream()));
PrintWriter out = new PrintWriter(
new OutputStreamWriter(client.getOutputStream()), true);

String line;
while ((line = in.readLine()) != null) {
System.out.println("收到:" + line);
out.println("回声:" + line); // 原样返回
}
}
}
}

客户端:

import java.io.*;
import java.net.*;

public class EchoClient {
public static void main(String[] args) throws IOException {
try (Socket socket = new Socket("127.0.0.1", 8888)) {
BufferedReader console = new BufferedReader(
new InputStreamReader(System.in));
BufferedReader in = new BufferedReader(
new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(
new OutputStreamWriter(socket.getOutputStream()), true);

String userInput;
while ((userInput = console.readLine()) != null) {
out.println(userInput);
System.out.println("服务器回应:" + in.readLine());
}
}
}
}

3. UDP Socket 编程(DatagramSocket / DatagramPacket)

UDP 无连接,直接发送数据报。

发送端:

import java.net.*;

public class UDPSender {
public static void main(String[] args) throws Exception {
DatagramSocket socket = new DatagramSocket();
String msg = "Hello UDP";
byte[] data = msg.getBytes();
InetAddress address = InetAddress.getByName("localhost");
DatagramPacket packet = new DatagramPacket(data, data.length, address, 9999);
socket.send(packet);
socket.close();
}
}

接收端:

import java.net.*;

public class UDPReceiver {
public static void main(String[] args) throws Exception {
DatagramSocket socket = new DatagramSocket(9999);
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
System.out.println("等待接收...");
socket.receive(packet);
String msg = new String(packet.getData(), 0, packet.getLength());
System.out.println("收到:" + msg);
socket.close();
}
}

4. URL 与 URLConnection —— HTTP 访问

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import java.net.URLConnection;

public class URLDemo {
public static void main(String[] args) throws Exception {
URL url = new URL("https://www.baidu.com");
URLConnection conn = url.openConnection();
BufferedReader reader = new BufferedReader(
new InputStreamReader(conn.getInputStream(), "UTF-8"));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
reader.close();
}
}

运行结果:

image-20261018212001735


三、Java NIO 网络编程(非阻塞 I/O)

NIO 使用 Channel(通道)、Buffer(缓冲区)和 Selector(选择器)实现高效的网络通信,特别适合高并发场景。

核心组件

组件 作用
SocketChannel 非阻塞 TCP 通道
ServerSocketChannel 非阻塞服务端通道
DatagramChannel 非阻塞 UDP 通道
ByteBuffer 字节缓冲区
Selector 多路复用器,监听多个 Channel 的事件

示例:NIO 回声服务器(单线程管理多连接)

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;

public class NIOServer {
public static void main(String[] args) throws IOException {
Selector selector = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(8888));
ssc.configureBlocking(false); // 非阻塞
ssc.register(selector, SelectionKey.OP_ACCEPT);

ByteBuffer buffer = ByteBuffer.allocate(1024);

while (true) {
selector.select(); // 阻塞直到有事件发生
Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
while (keys.hasNext()) {
SelectionKey key = keys.next();
keys.remove();

if (key.isAcceptable()) {
// 新连接
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel client = server.accept();
client.configureBlocking(false);
client.register(selector, SelectionKey.OP_READ);
System.out.println("新客户端:" + client.getRemoteAddress());
} else if (key.isReadable()) {
// 可读
SocketChannel client = (SocketChannel) key.channel();
buffer.clear();
int bytesRead = client.read(buffer);
if (bytesRead > 0) {
buffer.flip();
byte[] data = new byte[buffer.limit()];
buffer.get(data);
String msg = new String(data);
System.out.println("收到:" + msg);

// 原样返回
buffer.rewind();
client.write(buffer);
} else if (bytesRead == -1) {
client.close();
}
}
}
}
}
}

四、高级网络编程

1. Netty 框架

Netty 是一个基于 NIO 的高性能网络应用框架,封装了复杂的 NIO 细节,广泛应用于 RPC、消息推送、游戏服务器等。

2. HTTP 客户端库

  • HttpURLConnection(内置,但较繁琐)
  • Apache HttpClient
  • OkHttp
  • Spring WebClient(响应式)

3. WebSocket

全双工通信协议,适合实时应用(聊天、股票行情)。Java EE 提供 javax.websocket API。

4. RMI(远程方法调用)

Java 原生的分布式对象技术,允许调用远程 JVM 中的方法(已被更现代的框架取代)。


五、常见问题与注意事项

  1. TCP 粘包/拆包:TCP 是流式协议,多条消息可能合并或拆分。解决方法:固定长度、分隔符、消息头记录长度。
  2. 端口占用:BindException: Address already in use,更换端口或等待 TIME_WAIT 状态结束。
  3. 防火墙:确保端口未被防火墙拦截。
  4. 编码问题:网络传输统一使用 UTF-8 编码。
  5. 线程模型:传统 BIO 每连接一线程,高并发下资源耗尽;NIO 单线程可管理数千连接。
  6. 超时设置:socket.setSoTimeout(timeout) 防止无限阻塞。

六、总结

  • 网络编程 是让程序通过网络进行数据交换的技术,核心是 Socket 编程(TCP/UDP)。
  • Java 提供 java.net 包支持传统的阻塞 I/O,java.nio 包支持非阻塞 I/O。
  • TCP 可靠但开销大,UDP 快速但不可靠,根据场景选择。
  • 实际项目中常使用 Netty、OkHttp 等成熟框架简化开发。
  • 理解 IP、端口、协议、Socket 是网络编程的基础。

netty,nio?


十三、多线程

什么是多线程

多线程 是指在一个程序中同时运行多个独立的执行路径(线程),每个线程可以并行地执行不同的任务,从而提高程序的响应速度和资源利用率。

在 Java 中,线程是 java.lang.Thread 类的实例,或者实现了 Runnable 接口的任务。JVM 允许多个线程共享同一个进程的内存空间(堆和方法区),但每个线程拥有独立的程序计数器、虚拟机栈和本地方法栈。


一、为什么需要多线程?

场景 单线程的问题 多线程的优势
GUI 应用 耗时操作(下载、计算)会卡住界面 后台线程处理,前台保持响应
服务器 串行处理请求,吞吐量低 每个请求分配一个线程,并发处理
CPU 密集型任务 无法充分利用多核 CPU 多线程并行计算,缩短总耗时
I/O 密集型任务 等待 I/O 时 CPU 空闲 切换其他线程执行,提高 CPU 利用率

二、Java 中创建线程的四种方式

1. 继承 Thread 类

class MyThread extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 运行中");
}
}

// 启动
MyThread t = new MyThread();
t.start(); // 不要直接调用 run(),那是普通方法调用

2. 实现 Runnable 接口(推荐)

class MyTask implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 运行中");
}
}

// 启动
Thread t = new Thread(new MyTask());
t.start();

// Lambda 简化
new Thread(() -> System.out.println("Lambda 线程")).start();

3. 实现 Callable 接口(带返回值 + 异常)

import java.util.concurrent.*;

class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
Thread.sleep(1000);
return 42;
}
}

// 使用 FutureTask 包装
FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
new Thread(futureTask).start();
Integer result = futureTask.get(); // 阻塞等待结果

4. 使用线程池(ExecutorService)

ExecutorService executor = Executors.newFixedThreadPool(4);
executor.submit(() -> System.out.println("线程池任务"));
executor.shutdown();

三、线程的生命周期

Java 线程共有 6 种状态(Thread.State 枚举):

状态 说明 触发条件
NEW 新建,尚未 start() new Thread()
RUNNABLE 就绪或运行中 调用 start(),或从阻塞恢复后获得 CPU
BLOCKED 阻塞等待监视器锁 试图进入 synchronized 块但锁被占用
WAITING 无限期等待 调用 wait()、join()、park()
TIMED_WAITING 有限期等待 调用 sleep(millis)、wait(timeout)、join(timeout)
TERMINATED 终止 run() 正常结束或抛出未捕获异常

image-20261018212826188


四、线程安全与同步

1. 线程不安全的原因

  • 原子性:操作被中断(如 count++ 不是原子操作)。
  • 可见性:一个线程修改的变量对其他线程不可见(CPU 缓存导致)。
  • 有序性:指令重排序可能导致意外结果。

2. 解决方案

a. synchronized 关键字
  • 修饰方法:锁住当前实例(静态方法锁住 Class 对象)。
  • 修饰代码块:指定锁对象。
public class Counter {
private int count = 0;

public synchronized void increment() { // 实例方法锁
count++;
}

public void decrement() {
synchronized (this) { // 代码块锁
count--;
}
}
}
b. Lock 接口(java.util.concurrent.locks)

更灵活的锁,支持尝试获取、可中断、公平锁等。

import java.util.concurrent.locks.ReentrantLock;

public class LockCounter {
private final ReentrantLock lock = new ReentrantLock();
private int count = 0;

public void increment() {
lock.lock();
try {
count++;
} finally {
lock.unlock(); // 必须在 finally 中释放
}
}
}
c. volatile 关键字

保证变量的可见性(禁止指令重排),但不保证原子性。适用于单个变量的读写场景。

public class Flag {
private volatile boolean running = true;

public void stop() { running = false; }
public void run() {
while (running) { /* 循环 */ }
}
}
d. 原子类(AtomicInteger 等)

利用 CAS(Compare-And-Swap)实现无锁线程安全。

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicCounter {
private AtomicInteger count = new AtomicInteger(0);

public void increment() {
count.incrementAndGet();
}
}

五、线程间通信

1. wait() / notify() / notifyAll()

必须在 synchronized 块中调用,作用于同一个锁对象。

class Message {
private String content;
private boolean empty = true;

public synchronized String take() throws InterruptedException {
while (empty) {
wait(); // 等待生产者放入
}
empty = true;
notifyAll(); // 唤醒生产者
return content;
}

public synchronized void put(String content) throws InterruptedException {
while (!empty) {
wait(); // 等待消费者取走
}
empty = false;
this.content = content;
notifyAll(); // 唤醒消费者
}
}

2. Condition(配合 Lock)

ReentrantLock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition();
Condition notFull = lock.newCondition();
// 类似 wait/notify 的用法

3. CountDownLatch / CyclicBarrier / Semaphore

工具 作用
CountDownLatch 一个或多个线程等待其他线程完成操作
CyclicBarrier 多个线程互相等待,到达屏障后继续
Semaphore 控制同时访问资源的线程数量

六、线程池

1. 为什么用线程池?

  • 避免频繁创建/销毁线程的开销。
  • 控制最大并发数,防止资源耗尽。
  • 提供任务队列、定时执行等管理功能。

2. 常用线程池(Executors 工厂方法)

方法 特点 适用场景
newFixedThreadPool(n) 固定线程数,无界队列 负载稳定的服务器
newCachedThreadPool() 可伸缩,空闲60秒回收 短时间异步任务
newSingleThreadExecutor() 单线程顺序执行 需要保证任务顺序
newScheduledThreadPool(n) 定时/周期性执行 定时任务

3. 自定义线程池(推荐)

ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // corePoolSize
5, // maximumPoolSize
60, // keepAliveTime
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10), // 工作队列
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);

4. 提交任务

executor.execute(runnable);       // 无返回值
Future<?> future = executor.submit(callable); // 有返回值

七、常见并发问题

1. 死锁

多个线程相互等待对方持有的锁,导致永久阻塞。

预防:按固定顺序获取锁、使用 tryLock 超时、检测死锁。

// 死锁示例
Object lockA = new Object();
Object lockB = new Object();

// 线程1
synchronized (lockA) {
Thread.sleep(100);
synchronized (lockB) { ... }
}

// 线程2
synchronized (lockB) {
Thread.sleep(100);
synchronized (lockA) { ... }
}

2. 活锁

线程不断重试但始终无法进展(类似两人在走廊互相让路)。

3. 饥饿

低优先级线程长时间得不到 CPU 时间。

4. 伪共享(False Sharing)

多线程修改同一缓存行的不同变量导致缓存失效,性能下降。可通过填充字节对齐解决。


八、ThreadLocal

每个线程拥有独立的变量副本,互不干扰。

public class UserContext {
private static final ThreadLocal<String> currentUser = new ThreadLocal<>();

public static void setUser(String user) { currentUser.set(user); }
public static String getUser() { return currentUser.get(); }
public static void clear() { currentUser.remove(); } // 防止内存泄漏
}

// 使用
UserContext.setUser("Alice");
String user = UserContext.get();

九、完整示例:生产者-消费者

import java.util.LinkedList;
import java.util.Queue;

public class ProducerConsumer {
private static final int MAX_SIZE = 5;
private final Queue<Integer> queue = new LinkedList<>();

public synchronized void produce(int value) throws InterruptedException {
while (queue.size() == MAX_SIZE) {
wait();
}
queue.offer(value);
System.out.println("生产:" + value);
notifyAll();
}

public synchronized int consume() throws InterruptedException {
while (queue.isEmpty()) {
wait();
}
int value = queue.poll();
System.out.println("消费:" + value);
notifyAll();
return value;
}

public static void main(String[] args) {
ProducerConsumer pc = new ProducerConsumer();

new Thread(() -> {
for (int i = 0; i < 10; i++) {
try { pc.produce(i); Thread.sleep(200); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}).start();

new Thread(() -> {
for (int i = 0; i < 10; i++) {
try { pc.consume(); Thread.sleep(500); }
catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}
}).start();
}
}

十、总结

  • 多线程 让程序同时执行多个任务,提升性能和响应性。
  • Java 提供 Thread、Runnable、Callable、线程池等多种方式创建线程。
  • 线程安全需要关注原子性、可见性、有序性,通过 synchronized、Lock、volatile、原子类等解决。
  • 线程间通信使用 wait/notify、Condition 或并发工具类。
  • 线程池是管理线程的最佳实践,避免手动创建线程。
  • 注意死锁、活锁、饥饿等并发问题,合理设计锁的顺序和粒度。

多线程是 Java 开发的核心技能之一,理解其原理和最佳实践对于构建高性能、可靠的应用程序至关重要。