【Java】2025版一天学会Java基础到高级
Java
创建项目
Java项目的代码结构:
- 工程 project
- 模块 module
- 包 package
- 类 class
- 包 package
- 模块 module
- 创建项目

- 创建模块

- 创建包

- 创建类(类名不需要带后缀)

基础语法
注释
- 单行注释:
// 注释内容 - 多行注释:
/* 注释内容 */ - 文档注释:
/** 注释内容 */
字面量
定义:就是程序中能直接书写的数据,学这个知识的重点是:搞清楚lava程序中数据的书写格式。
字面量分类:
| 字面量类型 | 说明 | 示例 |
|---|---|---|
| 整数字面量 | 表示整数值,可以是十进制、八进制、十六进制或二进制形式。默认是int类型,可加L或l后缀表示long类型。 |
123(十进制)0123(八进制)0x7B(十六进制)0b1111011(二进制)200L(long类型) |
| 浮点数字面量 | 表示带小数的数值,可以是普通小数形式或科学计数法形式。默认是double类型,可加F或f后缀表示float类型。 |
3.14(double类型)3.14F(float类型)1.23e2(科学计数法) |
| 字符字面量 | 用单引号括起来的单个字符,可以是字母、数字、符号或转义字符。 | 'A'、'1'、'中'、'\n'(换行符)、'\t'(制表符) |
| 布尔字面量 | 表示逻辑真或假的值,只有两个取值。 | true、false |
| 字符串字面量 | 用双引号括起来的字符序列,可以包含任意字符,包括转义字符。 | "Hello World"、"Java"、"你好"、"\n"(换行字符串) |
| 空字面量 | 表示对象引用不指向任何对象,仅用于引用类型。 | null |

变量
定义格式:

📖知识扩展:比特
计算机中表示数据的最小单元。数据在计算机中的最小储存单元:字节(byte,简称B)是,一个字节占8个比特位(bit,简称b),因此1B=8b
进制计算
- 十进制转二进制的算法:除二取余法

- 二进制转十进制:8421法

- 八进制:每3位二进制作为一个单元,最小的是0,最大的是7,共8个数字:

- 十六进制:每4位二进制作为一个单元,最小数是0,最大数是15,共16个数字:

JaVa程序中支持书写二进制、八进制、十六进制的数据,分别需要以0B或者0b、0、0X或者0x开头。
数据类型
基本数据类型:4大类8种

注意事项:
- long类型:

- float类型:

关键字和标识符
关键字:
- Java 语法已经“占用”的单词,如
public、class、int、if……不能拿来当名字。
标识符:
- 程序员自己起的名字,如类名
HelloWorld、变量名age、方法名getSum……不能跟关键字重复,且必须遵守“字母/下划线/$开头,后接字母、数字、下划线或$”的规则。
标识符使用规则:
- 只能由 字母(A–Z、a–z、汉字等 Unicode 字母)、数字(0–9)、下划线
_和 美元符号$组成; - 第一个字符 不能是数字。
因此,以下都是合法标识符示例:
| 类别 | 合法举例 |
|---|---|
| 纯英文 | age MAX_VALUE $root _temp |
| 驼峰命名 | studentName getTotalScore |
| 常量风格 | PI CACHE_SIZE |
| 含 Unicode 字母 | 价格 变量① π(不推荐,但合法) |
不能出现的字符:空格、连字符 -、运算符 +*/、标点 . , ; : 等,也 不能跟 67 个关键字同名(如 int、class、true、null 等)。
方法
方法是一种用于执行特定任务或操作的代码块,代表一个功能,它可以接收数据进行处理,并返回一个处理后的结果。
方法的完整格式:
修饰符 返回值类型 方法名(形参列表){
方法体代码(需要执行的功能代码)
return 返回值;
}
例:
public static int add(int a, int b) { // 修饰符 返回类型 方法名(参数)
return a + b; // 方法体
}
// 调用
int sum = add(3,5); // sum = 8
⚠注意:
- 返回类型用
void表示“什么都不返回”。 - 参数列表可空
( ),也可多个(int x, double y)。 - 方法名遵循小驼峰,见名知义。
- 想复用就“调方法”,想灵活就“传参数”,想拿到结果就
return。
方法的其他形式:
-
方法
不需要接受参数:修饰符 void 方法名(形参列表){ 方法体代码(需要执行的功能代码) }如果方法没有返回结果,返回值类型必须声明
void// 调用 方法名()
方法重载
一个类中,出现多个方法的名称相同,但是它们的形参列表是不同的,那么这些方法就称为方法重载了。
重点:方法重载只关心方法名称相同,形参列表不同(类型不同,个数不同,顺序不同)。
return
无返回值的方法中可以直接通过单独的return;立即结束当前方法的执行。
类型转换
类型范围小的变量,可以直接赋值给类型范围大的变量。

例:
byte b = 10;
int i = b; // 自动转,OK
long L = i; // 自动转,OK
double d = L; // 自动转,OK
// 反方向不行,会编译报错
int x = 3.14; // ❌ double → int,必须强转
强类型转换
类型范围大的变量,不可以直接赋值给类型范围小的变量,会报错,需要强制类型转换过去
格式:
类型 变量2 = (类型) 变量1

⚠注意:
- 强制类型转换可能造成数据(丢失)溢出;
- 浮点型强转成整型,直接丢掉小数部分,保留整数部分返回
自动类型提升
在表达式中,小范围类型的变了,会自动转换成表达式中较大范围的类型,再参与运算。

⚠注意:
- 表达式的最终结果类型由表达式中的最高类型决定。
- 在表达式中,byte\short\char 是直接转换成
int类型再参与运算。
分支结构
if分支
根据条件的真或假,来决定执行某段代码。
结构:
if(条件表达式){
// 代码;
}
if-else分支:
if (条件表达式) {
// 代码1;
} else {
// 代码2;
}
多条件分支:
if (条件表达式) {
// 代码1;
} else if (条件表达式) {
// 代码2;
} else if (条件表达式) {
// 代码3;
} else {
// 代码4;
}
switch分支
是通过比较值是否相等,来决定执行哪条语句。
结构:
switch (变量) {
case 值1 -> 语句1;
case 值2 -> 语句2;
...
default -> 默认语句;
}
⚠注意:如果不编写break会出现穿透现象。
int month = 2;
switch (month) {
case 2:
case 3:
case 4:
System.out.println("春天");
break;
}
// 输出 "春天"
📖知识扩展:
if和switch的区别:
- if 是“范围题”,switch 是“单选题”
对比点 if / else if switch 判断类型 任意布尔表达式(>、<、&&、 都行) 只能对比单个值(==) 适合场景 区间、范围、复杂条件 固定几个候选值(如 1~7、A/B/C) 支持类型 所有类型 基本型+enum+String(Java7+) 写法 灵活但冗长 简洁、可读高 性能 差别极小,别纠结 现代 JVM 会优化,同样别纠结
for循环
for循环会自动重复那段代码。
结构:
for (初始化; 继续条件; 步进) {
// 循环体
}
例:从 1 数到 10,每数一次打印一次。
for (int i = 1; i <= 10; i++) {
System.out.println(i);
}
while循环
先判断,再执行;适合“不知道次数”的场景。
结构:
while (继续条件) {
// 循环体
}
do-while循环
先执行一次,再判断;至少跑一遍
结构:
do {
// 循环体
} while (继续条件)
循环嵌套
循环中又包含循环:
for(...){
for(...){
// ...
}
}
外部循环每循环一次,内部循环会全部执行完一轮。
最经典例子:打印矩形星号
for (int i = 1; i <= 3; i++) { // 外层:控制行
for (int j = 1; j <= 5; j++) { // 内层:控制列
System.out.print("*"); // 同一行连续打印
}
System.out.println(); // 换行
}
输出:
*****
*****
*****
执行顺序(想象秒表):
- 外层 i=1 → 内层 j 从 1 跑到 5 → 换行
- 外层 i=2 → 内层 j 再从 1 跑到 5 → 换行
- 外层 i=3 → 重复一次,结束。
死循环
死循环 = 停不下来的循环
条件永远为 true,程序一直转圈,除非手动停止或break。
3钟常见死循环:
-
while:while (true) { ... } -
for:for (;;) { ... } -
do-while:do { ... } while (true);
什么时候用死循环?
- 服务器(7×24 监听)
- 游戏引擎(不断刷新画面)
- 菜单(重复等待用户输入)
Scanner sc = new Scanner(System.in);
while (true) {
System.out.print("请输入指令(q退出): ");
String cmd = sc.nextLine();
if ("q".equals(cmd)) {
break; // 用户敲 q 才结束
}
System.out.println("你输入了: " + cmd);
}
break 和 continue
break:跳出并结束当前所在循环的执行。
⚠注意:只能用于结束所在循环,或者结束所在switch分支的执行。
continue:用于跳出当前循环的当次执行,直接进入循环的下一次执行。
⚠注意:只能在循环中进行使用。
例:
while (条件) {
语句A;
if (xxx) break; // 直接跳出 while
语句B;
if (yyy) continue; // 回到条件判断,不再执行语句C
语句C;
}
一句话总结区别:
break直接掀桌子 —— 立刻退出整个循环,一去不回头。continue跳过当次 —— 只跳过本轮剩余语句,继续下一轮循环。
数组
数组是一个数据容器,可用来存储一批同类型的数据。
静态初始化数组
静态初始化数组就是在定义的时候就确定了数据。
完整版:
数据类型[] 数组名 = new 数据类型[] {元素1,元素2, ...}
例:
int[] arr = new int[] {10, 20, 30, 40, 50};
简化版(比较常用):
数据类型[] 数组名 = {元素1,元素2, ...}
数组访问:数组名[索引]
获取数组长度:数组名.length
动态初始化数组
只确定数组的类型和存储数据的容量,不事先存入具体的数据。
结构:
数据类型[] 数组名 = new 数据类型[长度]
添加数组元素:
数组名[长度] = 元素
二维数组
静态初始化:
数据类型[][] 数组名 = new 数据类型[][]{元素1,元素2,...}
动态初始化:
数据类型[][] 数据名 = new 数据类型[长度1][长度2]
访问二维数组:数据名[行索引][列索引]
添加数组元素:
面向对象
对象
对象是类的实例每个对象在堆内存中拥有独立的存储空间。
对象包含:
- 状态(State):由成员变量(字段)表示。
- 行为(Behavior):由方法(函数)表示。
- 标识(Identity):每个对象在 JVM 中有唯一地址(即使内容相同,也是不同对象)。
格式:
Student s1 = new Student("张三");
Student s2 = new Student("张三");
System.out.println(s1 == s2); // false,两个不同对象
成员变量
成员变量是在类中、方法外定义的变量,用于表示对象的状态(属性)。每个对象(实例)都有自己的一份成员变量副本(除非是 static 的)。
特点:
- 作用域:整个类都可见。
- 生命周期:随着对象的创建而存在,随着对象的销毁而消失。
- 可以有访问修饰符。
- 可以被
static修饰,变成类变量。
示例:
public class Student {
// 成员变量
private String name; // 实例变量
private int age;
public static String school = "清华大学"; // 静态成员变量(类变量)
}
成员方法
成员方法是在类中定义的、用于描述对象行为的函数。它通常用于操作成员变量或执行特定任务。
特点:
- 可以访问本类中的成员变量和其他成员方法。
- 可以有参数、返回值。
- 也可以被
static修饰,变成类方法(通过类名直接调用)。 - 同样可以有访问修饰符。
示例:
public class Student {
private String name;
private int age;
// 成员方法(实例方法)
public void setName(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public void introduce() {
System.out.println("我叫 " + name + ",今年 " + age + " 岁。");
}
// 静态成员方法(类方法)
public static void printSchool() {
System.out.println("学校:" + school);
}
}
对象生命周期
- 创建:
new关键字 → 在堆中分配内存 → 调用构造器初始化。 - 使用:通过引用变量调用方法或访问属性。
- 销毁:当对象不再被引用时,成为垃圾(Garbage),由 JVM 的**垃圾回收器(GC)**自动回收。
构造器
构造器用于创建对象时初始化对象的状态。它的名字必须和类名完全相同,且没有返回值类型(连 void 都不能写)
默认构造器
如果你没有写任何构造器,java 会自动提供一个无参的默认构造器。
public class Student {
// 编译器自动添加: Student(){}
}
自定义构造器
你可以定义带参数的构造器来初始化属性:
public class Student {
private String name;
private int age;
// 无参构造器
public Student() {}
// 有参构造器
public Student(String name, int age) {
this.name = name;
this.age = age;
}
}
// 使用
Student s1 = new Student(); // 调用无参构造器
Student s2 = new Student("张三", 18); // 调用有参构造器
✅ 构造器可以重载(多个构造器,参数不同)。
this关键字
this 代表当前对象的引用,常用于:
-
区分成员变量和局部变量(当参数名和成员变量名相同时):
public Student(String name) { this.name = name; // this.name 是成员变量,name 是参数(局部变量) } -
在构造器中调用其他构造器(必须是第一行):
public Student() { this("未知", 0); // 调用另一个构造器 } -
返回当前对象(较少用,用于链式调用):
public Student setName(String name) { this.name = name; return this; } // 使用:s.setName("李四").setAge(20);
⭕this调用兄弟构造器
在任意类的构造器中,是可以通过this()区调用该类的其他构造器。
public class Student {
private int id;
private String name;
private int age;
/* 1. 全参构造器:终极入口 */
public Student(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
/* 2. 两参构造器:调用兄弟构造器补默认值 */
public Student(int id, String name) {
this(id, name, 18); // 把 age 默认成 18
}
/* 3. 无参构造器:继续套娃 */
public Student() {
this(0, "匿名"); // 再往前套一层
}
}
⚠注意:super()和this()必须写在构造器的第一行,并且两者不能同时出现。
权限修饰符
Java 的权限修饰符(Access Modifiers)用来控制类、接口、变量、方法、构造器的可见性范围。
按“从宽到严”依次是:
-
public → 全局可见
// 文件:com/foo/Util.java package com.foo; public class Util { public static void hello() { System.out.println("hello"); } } // 文件:com/bar/Main.java package com.bar; import com.foo.Util; public class Main { public static void main(String[] args) { Util.hello(); // 任何地方都能调到 } } -
protected → 同包 + 子类可见
package com.foo; public class Father { protected void say() {} } // 同包非子类 package com.foo; class Neighbor { void test() { new Father().say(); // ✅ 同包可见 } } // 不同包子类 package com.bar; import com.foo.Father; class Son extends Father { void test() { say(); // ✅ 子类内部可见 new Father().say(); // ❌ 不同包非子类视角 } } -
(default) → 仅同包可见(不写任何修饰符)
package com.foo; class Hidden { // 不写修饰符,包私有 void foo() {} } package com.bar; class Outsider { void test() { // new Hidden(); // ❌ 不同包完全不可见 } } -
private → 仅本类内部可见
public class Counter { private int count = 0; public void inc() { count++; // 本类内部随便用 } private void reset() { // 连子类都看不到 count = 0; } }
速查表(✅ = 可见,❌ = 不可见)
范围 public protected (default) private 本类 ✅ ✅ ✅ ✅ 同包其他类 ✅ ✅ ✅ ❌ 不同包子类 ✅ ✅ ❌ ❌ 不同包非子类 ✅ ❌ ❌ ❌
封装
封装 是将对象的属性和行为包装起来,并通过访问控制(如 private)隐藏内部细节,只暴露必要的接口(如 public 方法)。目的就是为了提高安全性(防止非法访问)和提高可维护性(内部修改不影响外部使用)。
封装步骤:
- 将成员变量设为
private - 提供
public的getter和setter方法
public class Person {
private String name;
private int age;
// Getter
public String getName() {
return name;
}
// Setter
public void setName(String name) {
if (name != null && !name.isEmpty()) {
this.name = name;
}
}
public int getAge() {
return age;
}
public void setAge(int age) {
if (age >= 0 && age <= 150) {
this.age = age;
}
}
}
实体类
实体类是专门用来封装数据的类。
例如:
public class User {
// User就是实体类
private String username;
private String password;
public User() {} // 无参构造器
// getter 和 setter
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}
静态变量
用 static 修饰的成员变量,也叫 类变量。
🔍 什么是静态变量?
- 用
static关键字修饰的变量属于类本身,而不是类的某个对象。 - 所有对象共享同一个静态变量。
- 在类加载时就分配内存,程序结束才释放。
public class Counter {
public static int count = 0; // 静态变量
public Counter() {
count++; // 每创建一个对象,count 加 1
}
}
// 使用
new Counter();
new Counter();
System.out.println(Counter.count); // 输出 2
所有对象共享同一个值,在类加载的时候初始化,通过类名.变量名来进行访问。
静态方法
用 static 修饰的方法,也叫 类方法。
它的最大特点是:不需要创建对象,直接通过类名就能调用!
不能访问非静态成员(因为非静态属于对象,而静态方法不依赖对象),常用于工具方法。
public class MathUtils {
public static int add(int a, int b) {
return a + b;
}
}
// 使用
int sum = MathUtils.add(3, 5); // 不需要创建对象!
❗ 注意:static 方法中不能使用 this 和 super。
继承
继承允许一个类(子类)获得另一个类(父类)的属性和方法,实现代码复用。使用extends关键字:
class Animal {
protected String name;
public void eat() {
System.out.println(name + " 在吃东西");
}
}
class Dog extends Animal {
public void bark() {
System.out.println(name + " 在汪汪叫");
}
}
// 使用
Dog dog = new Dog();
dog.name = "旺财";
dog.eat(); // 继承自 Animal
dog.bark(); // 自己的方法
关键点:
-
Java只支持单继承(一个类只能有一个直接父类)
-
子类可以重写父类方法
-
构造子类对象时,会先调用父类构造器(默认调用
super())class Dog extends Animal { public Dog(String name) { super(); // 调用父类无参构造器(可省略) this.name = name; } // 或者 public Dog(String name) { super(); // 必须在第一行 this.name = name; } }
子类构造器
特点:子类的全部构造器,都会先调用父类的构造器,再调用自己。

子类构造器是如何实现调用父类构造器的:
- 默认情况下,子类全部构造器的第一行代码都是
super()(写不写都有),它会调用父类的无参数构造器。
多态
多态字面意思是“多种形态”。在 Java 中,它指的是:同一个方法调用,在不同对象上会产生不同的行为。
举个生活中的例子: 你按“开机键”,对电脑来说是开机,对电视来说是打开电视,对空调来说是启动制冷——同一个动作(开机),不同对象(电脑/电视/空调)做出不同的反应。这就是多态!
⭕多态的前提条件(必须同时满足):
- 继承(或实现接口)
- 方法重写(子类重写父类的方法)
- 父类引用指向子类对象(这是关键!)
举个例子:
// 父类
class Animal {
public void makeSound() {
System.out.println("动物发出声音");
}
}
// 子类1
class Dog extends Animal {
@Override
public void makeSound() {
System.out.println("汪汪!");
}
}
// 子类2
class Cat extends Animal {
@Override
public void makeSound() {
System.out.println("喵喵~");
}
}
// 测试类
public class Test {
public static void main(String[] args) {
// 父类引用指向子类对象(多态的核心写法!)
Animal a1 = new Dog(); // 实际是 Dog 对象
Animal a2 = new Cat(); // 实际是 Cat 对象
a1.makeSound(); // 输出:汪汪!
a2.makeSound(); // 输出:喵喵~
}
}
关键点解析:
Animal a1 = new Dog();- 编译时类型:
Animal(左边) - 运行时类型:
Dog(右边)
- 编译时类型:
- 调用
makeSound()时,实际执行的是子类重写后的方法(不是父类的!) - 这就是 “编译看左边,运行看右边” 的经典口诀!
✅多态的好处:
-
代码灵活、可扩展:比如以后加一个
Bird类,只要继承Animal并重写makeSound(),不用改主程序!class Bird extends Animal { @Override public void makeSound() { System.out.println("叽叽~"); } } -
便于维护和解耦:你只需要面向父类编程,不用关心具体是哪个子类。
⚠️ 注意事项:
- 成员变量没有多态!
如果父类和子类有同名变量,访问的是编译时类型(即左边的类型)的变量。 - 静态方法也没有多态!
静态方法属于类,不是对象,调用时看的是引用类型(左边)。 - 多态下不能调用子类独有方法!
多态类型转换
- 向上转型(子 → 父)
语法:Parent p = new Child();
特点:自动完成,一定成功,但只能调用父类声明的方法(除非子类重写了)。 - 向下转型(父 → 子)
语法:Child c = (Child) p;
特点:必须显式强转,可能失败(运行期抛ClassCastException),强转前用instanceof判断。
在强转前,建议使用instanceof关键字进行判断当前对象的真实类型,在进行强转。
变量名 instanceof 类型
class Animal {
void eat() { System.out.println("animal eat"); }
}
class Cat extends Animal {
@Override
void eat() { System.out.println("cat eat fish"); }
void climb() { System.out.println("cat climb tree"); }
}
public class Demo {
public static void main(String[] args) {
/* 1. 向上转型:自动、安全 */
Animal a = new Cat(); // a 的编译类型 = Animal,运行类型 = Cat
a.eat(); // 动态绑定 → cat eat fish
// a.climb(); // 编译错误:Animal 没有 climb()
/* 2. 向下转型:先判断再强转 */
if (a instanceof Cat) { // 运行期检查“真实对象”是不是 Cat
Cat c = (Cat) a; // 安全通过
c.climb(); // 现在能调用子类独有方法
}
/* 3. 错误示例:类型不符 */
Animal dog = new Animal();
// Cat wrong = (Cat) dog; // 运行期抛 ClassCastException
}
}
单例类
final关键字
作用是将声明数据为常量,可以是编译时常量,也可以是在运行时被初始化后不能被改变的常量。
-
修饰普通变量(局部变量或者成员变量):
public class Example { public static void main(String[] args) { final int x = 10; // x = 20; ❌ 编译错误!不能修改 final 变量 System.out.println(x); // 输出:10 } } -
修饰成员变量:必须在声明时赋值,或在构造器中赋值(只能选一种方式,且只能赋一次)。
public class Person { private final String name; // 声明但未赋值 public Person(String name) { this.name = name; // ✅ 可以在构造器中赋值 } // name 之后不能再改! } -
final修饰方法不能被子类重写:
class Animal { public final void sleep() { System.out.println("动物睡觉"); } } class Dog extends Animal { // @Override // public void sleep() { } ❌ 编译错误!不能重写 final 方法 } -
final修饰类不能被继承:
final class MathUtils { public static int add(int a, int b) { return a + b; } } // class MyMath extends MathUtils { } ❌ 编译错误!不能继承 final 类 -
常用与
static连用:定义全局常量的标准写法public class Constants { public static final double PI = 3.14159; public static final String APP_NAME = "MyApp"; } // 调用:System.out.println(Constants.PI); // 3.14159
常量
使用static final修是的成员变量就被称为常量。作用是用于记录系统的配置信息。
⚠注意:常量名称的命名规范是全大写英文单词,多个单词通过下划线连接。
单例模式
作用:确保某个类只能创建一个对象。
实现步骤:
-
私有化构造器:确保单例类对外不能创建太多对象。
private 类名(){} -
定义一个静态变量:用于记住本类的一个唯一对象
public static final 类名 对象名称 = new 类名() // 或者私有化 private static 类名 对象名称 = new 类名() -
定义一个类方法:用于返回这个类的唯一对象
public static 类名 静态方法名(){ return 对象名称 } // 通过 类名.静态方法() 来调用。
**饿汉式:**类一加载,就先把实例创建好了
- 优点:简单、天然线程安全
- 缺点:如果一直没用到这个对象,会浪费一点内存(但对初学者完全不是问题)
例子:
public class Singleton {
// 1. 在类内部创建唯一实例(static 表示属于类,只有一份)
private static Singleton instance = new Singleton();
// 2. 私有构造方法:防止外部用 new 创建对象
private Singleton() {
// 空着就行,什么都不用写
}
// 3. 提供一个公共方法,让别人能拿到这个唯一实例
public static Singleton getInstance() {
return instance;
}
// 示例功能:打印一句话
public void showMessage() {
System.out.println("我是唯一的 Singleton 实例!");
}
}
使用:
public class Main {
public static void main(String[] args) {
// 不能这样写:Singleton s = new Singleton(); // ❌ 编译错误!构造方法是 private
// 正确方式:通过 getInstance() 获取
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
s1.showMessage(); // 输出:我是唯一的 Singleton 实例!
// 验证是不是同一个对象
System.out.println(s1 == s2); // 输出:true(说明确实是同一个)
}
}
**懒汉式:**使用对象时,才会开始创建对象。
- 好处:节省内存(如果一直没用,就不创建)
- 注意:基础版懒汉式在多线程下不安全,但我们先不考虑多线程(你还没学到),只关注单线程下的逻辑
例子:
public class LazySingleton {
// 1. 先不创建实例,初始为 null
private static LazySingleton instance = null;
// 2. 私有构造方法,防止外部 new
private LazySingleton() {
// 空着就行
}
// 3. 提供获取实例的方法:用到时才创建
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton(); // 第一次调用时才创建
}
return instance;
}
// 示例方法
public void doSomething() {
System.out.println("懒汉单例正在工作...");
}
}
使用:
public class Main {
public static void main(String[] args) {
// 第一次调用:创建对象
LazySingleton s1 = LazySingleton.getInstance();
s1.doSomething(); // 输出:懒汉单例正在工作...
// 第二次调用:直接返回已有对象
LazySingleton s2 = LazySingleton.getInstance();
System.out.println(s1 == s2); // 输出:true(是同一个对象)
}
}
枚举类
枚举是一种特殊的类,用来表示一组固定的常量值。
比如:
- 一周的星期:
MONDAY,TUESDAY, …,SUNDAY- 季节:
SPRING,SUMMER,AUTUMN,WINTER- 订单状态:
PENDING,SHIPPED,DELIVERED,CANCELLED这些值是有限的、确定的、不会变的,就非常适合用枚举。
你可能会想:
“我直接用
"MONDAY"或数字1表示星期不行吗?”
但这样有风险:
- 容易拼错:
"Mondy"❌ - 语义不清:
status = 2是什么意思? - 编译器无法检查合法性
而枚举是类型安全的:只能用预定义的几个值,写错了编译都通不过!
语法:
修饰符 enum 枚举类名{
名称1,名称2,...;
其他成员...
}
使用:
public class Main {
public static void main(String[] args) {
// 声明一个 Day 类型的变量
Day today = Day.MONDAY;
// 可以比较(用 ==,安全!)
if (today == Day.MONDAY) {
System.out.println("今天是周一,加油!");
}
// 打印枚举值
System.out.println("今天是:" + today); // 输出:今天是:MONDAY
// 遍历所有枚举值
for (Day d : Day.values()) {
System.out.println(d);
}
}
}
例子:
// 定义一个表示星期的枚举
public enum Day {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
⚠ 注意:枚举值默认是
public static final的,而且全部大写是惯例。
特点:

- 枚举类都是最终类,不可以被继承,枚举类都是继承
java.lang.Enum类的。 - 枚举类的第一行只能罗列一些名称,这些名称都是变了,并且每个常量都会记住枚举类的一个对象。
- 枚举类的构造器都是私有的,因此枚举类对外不能创建对象。
抽象类
抽象类是用 abstract 关键字修饰的类,它不能被直接实例化(不能用 new 创建对象),通常用来作为其他类的“模板”或“基类”。
💡 类比: 抽象类就像“水果”这个概念——你可以有“苹果”“香蕉”,但不能说“给我一个水果”(因为“水果”太抽象了,不是一个具体的东西)。
❓为什么需要抽象类
现实世界中,有些类本身就是没有实际意义的,只是为了定义通用行为,让子类可以实现具体细节。
比如:
- 动物会“叫”,但“动物”本身怎么叫?不知道!
- 具体到“狗”是“汪汪”,“猫”是“喵喵”——这些由子类决定。
✅抽象类写法
-
定义抽象类和抽象方法:
// 抽象类 abstract class Animal { // 普通方法(可以有方法体) public void sleep() { System.out.println("动物在睡觉"); } // 抽象方法(没有方法体,用 abstract 修饰) public abstract void makeSound(); // 子类必须实现这个方法! } -
子类继承抽象类,并实现抽象方法:
class Dog extends Animal { @Override public void makeSound() { System.out.println("汪汪!"); } } class Cat extends Animal { @Override public void makeSound() { System.out.println("喵喵~"); } } -
使用:(不能直接创建对象new Animal,因为抽象类不能被实例化)
public class Main { public static void main(String[] args) { // Animal a = new Animal(); ❌ 编译错误!抽象类不能实例化 Animal dog = new Dog(); // ✅ 可以用父类引用指向子类对象(多态) dog.makeSound(); // 输出:汪汪! dog.sleep(); // 输出:动物在睡觉 } }
模板方法模式
提供一个方法作为完成某类功能的模板,模板方法封装了每个实现步骤,但允许子类提供特定步骤的实现。可以提高代码复用、并简化子类设计。
⭕举个例子:
做饮料的通用流程是:
- 烧水
- 冲泡(咖啡 or 茶) ← 这一步不同
- 倒进杯子
- 加调料(糖/牛奶) ← 这一步也可能不同
这个流程就是“模板”,而“冲泡”和“加调料”是可变的步骤。
代码实现:
-
定义抽象父类(模板):
// 抽象类:定义做饮料的模板 abstract class Beverage { // 模板方法:final 防止子类修改流程顺序 public final void prepare() { boilWater(); // 烧水 brew(); // 冲泡(子类实现) pourInCup(); // 倒进杯子 addCondiments(); // 加调料(子类实现) } // 共同步骤:父类直接实现 void boilWater() { System.out.println("烧开水..."); } void pourInCup() { System.out.println("倒入杯中..."); } // 不同步骤:交给子类实现(抽象方法) abstract void brew(); // 冲泡 abstract void addCondiments(); // 加调料 }✅
prepare()是模板方法,用final保证流程不能被改。 -
子类实现具体步骤:
// 做咖啡 class Coffee extends Beverage { @Override void brew() { System.out.println("用沸水冲泡咖啡粉..."); } @Override void addCondiments() { System.out.println("加糖和牛奶..."); } } // 做茶 class Tea extends Beverage { @Override void brew() { System.out.println("用沸水泡茶叶..."); } @Override void addCondiments() { System.out.println("加柠檬..."); } } -
使用:
public class Main { public static void main(String[] args) { System.out.println("=== 制作咖啡 ==="); Beverage coffee = new Coffee(); coffee.prepare(); // 调用模板方法 System.out.println("\n=== 制作茶 ==="); Beverage tea = new Tea(); tea.prepare(); } } -
输出:
=== 制作咖啡 === 烧开水... 用沸水冲泡咖啡粉... 倒入杯中... 加糖和牛奶... === 制作茶 === 烧开水... 用沸水泡茶叶... 倒入杯中... 加柠檬...
接口类
接口定义了"一个类能做什么",但不关心具体做了什么。
接口用来被类实现的,实现接口的类称为实现类,一个类可以同时实现多个接口。
✅接口的写法
-
定义接口:
// 接口用 interface 关键字定义 public interface Flyable { // 接口中的方法默认是 public abstract(可以省略) void fly(); // 所有实现类必须实现这个方法 } -
类实现接口:(用implements)
public class Bird implements Flyable { @Override public void fly() { System.out.println("小鸟在天空飞翔!"); } } public class Airplane implements Flyable { @Override public void fly() { System.out.println("飞机起飞了!"); } } -
使用接口:
public class Main { public static void main(String[] args) { Flyable f1 = new Bird(); Flyable f2 = new Airplane(); f1.fly(); // 小鸟在天空飞翔! f2.fly(); // 飞机起飞了! } }
特点:
- 接口不能创建对象
- 一个类可以实现多个接口
- 接口可以继承接口,且支持"多继承"
⚠注意事项:
-
一个接口继承多个接口,如果接口中方法签名冲突,此时不支持多继承,也不支持多实现:
interface A { void show(); } interface B { String show(); } interface C extends A,b{ // 报错... } -
一个类继承父类,又同时实现了接口,如果父类中和接口中有同名方法,实现类会先用父类
// 1. 接口 interface Greet { void say(); } // 2. 父类——已经给了具体实现 class Father { public void say() { System.out.println("Father 说:嗨"); } } // 3. 子类:继承 Father 同时实现 Greet // 因为 Father 已有具体实现,所以不强制重写 class Son extends Father implements Greet { } // 4. 测试 public class Demo { public static void main(String[] args) { Greet g = new Son(); // 向上转型为接口类型 g.say(); // 到底用谁?——用 Father 的! } } -
一个类实现多个接口,如果多个接口中存在同名的默认方法,可以不冲突,这个类重写方法即可
案例:“支付通道”
需求:系统要支持微信\支付宝\银行卡三种支付,未来还能扩展
-
先定义接口:
public interface Payment { /** * 支付 * @param cents 金额,单位分 * @return true 成功 */ boolean pay(long cents); } -
三种实现:
public class WechatPay implements Payment { public boolean pay(long cents) { System.out.println("微信扫码支付了 " + cents + " 分"); return true; } } public class AliPay implements Payment { public boolean pay(long cents) { System.out.println("支付宝扣款 " + cents + " 分"); return true; } } public class BankPay implements Payment { public boolean pay(long cents) { System.out.println("银行卡扣款 " + cents + " 分"); return true; } } -
未来拓展:
public class OrderService { public void checkout(Payment p, long amount) { if (p.pay(amount)) { // 更新订单状态 } } }
JDK8接口增强
JDk8开始,接口新增了三种形式的方法:
-
默认方法:普通实例方法
public interface A { // 必须加default修饰 default void go(){} } // 调用:通过实现类 class Aimp imppements A{ } Aimp a = new Aimp(); a.go() -
私有方法:私有的实例方法
private void run(){} // 调用:使用接口中的其他实例方法来调用 -
静态方法:使用static修饰,默认会加上public修饰
static void show(){} // 调用:只能使用当前接口名来调用 A.show()
接口与抽象类
接口 vs 抽象类
| 对比项 | 接口(Interface) | 抽象类(Abstract Class) |
|---|---|---|
| 关键字 | interface |
abstract class |
| 继承方式 | implements |
extends |
| 多继承 | ✅ 一个类可实现多个接口 | ❌ 只能单继承 |
| 方法 | Java 8+ 可有默认/静态方法,其余是抽象方法 | 可有普通方法 + 抽象方法 |
| 成员变量 | 只能是 public static final 常量 |
可以是任意类型变量 |
| 设计目的 | 定义“能力”(能做什么) | 定义“是什么” + 部分实现 |
| 构造器 | ❌ 没有 | ✅ 有(子类调用) |
相同点:
- 都是抽象形式,都可以抽象方法,都不成创建对象
- 都是派生子类形式(抽象类继承子类,接口需要实现类)
- 继承抽象类或者实现接口都必须重写完他们的抽象方法
- 都能支持多态,都能够实现解耦合
不同点:
- 抽象类中可以定义类的全部普通成员,接口只能定义常量,抽象方法
- 抽象类只能被类单继承,接口可以被多个类实现,
- 一个类继承抽象类就不能在继承其他类,一个类实现了接口(还可以继承其他类或者实现其他接口)
类中的成分
代码块
代码块是类中的五大成分之一
类的五大成分:成员变量\构造器\方法\代码块\内部类
代码块分类:
-
静态代码块:
-
格式:
static() -
特点:类加载时自动执行,由于类只会加载一次,所以静态代码块也只会执行一次
public class Test { static { System.out.println("--------静态代码块--------"); } public static void main(String[] args) { System.out.println("========main方法========="); } } // 输出: //--------静态代码块-------- //========main方法========= -
作用:完成类的初始化
-
-
实例代码块:
-
格式:
{} -
特点:每次创建对象时,执行实例代码块,并在构造器前执行
public class Test { { System.out.println("--------实例代码块--------"); } public static void main(String[] args) { System.out.println("========main方法========="); new Test(); new Test(); new Test(); } } -
作用:和构造器一样,都是用来完成对象的初始化
-
内部类
如果一个类定义在另一个类的内部,这个类就是内部类。
public class Car {
public class Engine {}
}
成员内部类
成员内部类:无static修饰,属于外部类的对象特有的
public class Outer {
public class Inner{
public void show {}
}
}
调用:
外部类名称.内部类名称 对象名 = new 外部类名称().new 内部类名称();
Outer.Inner oi = new Outer().new Inner();
oi.show()
特点:
-
成员内部类中可以直接访问外部类的静态成员和静态方法,也可以直接访问外部类的实例成员
public class Outer { private static String staticField = "静态字段"; private String instanceField = "实例字段"; private static void staticMethod() { System.out.println("静态方法"); } private void instanceMethod() { System.out.println("实例方法"); } class Inner { // 成员内部类 void visit() { System.out.println(staticField); // 静态字段 staticMethod(); // 静态方法 System.out.println(instanceField); // 实例字段 instanceMethod(); // 实例方法 } } public static void main(String[] args) { new Outer().new Inner().visit(); // 一行创建 + 调用 } } -
成员内部类的实例方法中,可以直接拿到当前寄生的外部类对象:
外部类名.thispublic class Outer { private String name = "外部类"; class Inner { private String name = "内部类"; void show() { String name = "局部变量"; System.out.println(name); // 局部变量 System.out.println(this.name); // 内部类字段 System.out.println(Outer.this.name); // 外部类字段 } } public static void main(String[] args) { new Outer().new Inner().show(); } }
静态内部类
有static修饰的内部类,属于外部类自己持有。
public class Outer{
// 静态内部类
public static class Inner{
public void show(){}
}
}
调用:外部类名.内部类名 对象名 = new 外部类.内部类()
Outer.Inner in = new Outer.Inner();
inner.show()
特点:
- 静态内部类中可以直接访问外部类的静态成员。
- 静态内部类中不可以直接访问外部类的实例成员。
局部内部类
局部内部类是定义在方法中\代码块中\构造器等执行体中。
public class Test {
public static void main(String[] args){}
public static void go(){
class A{}
abstract class B{}
interface C{}
}
}
匿名内部类
是一种特殊的局部内部类,所谓匿名就是不需要为这个类声明名字,默认有一个隐藏的名字。
语法:
new 类或接口(参数...){
类体(一般是方法重写);
}
例如:
Animal a = new Animal(){
@Override
public void cry(){}
};
a.cry()
特点:匿名内部类本质就是一个子类,并会立即创建出一个子类对象。
**实际名字:
外部**类名.$编号.class
作用:用于更方便的创建一个子类对象
⭕常见使用形式:
通常作为一个对象参数传输给方法。
public class Test {
public static void main(String[] args) {
Swim s1 = new Swim() {
@Override
public void swimming() {
System.out.println("学生开始游泳~");
}
};
start(s1);
System.out.println("============");
Swim s2 = new Swim() {
@Override
public void swimming() {
System.out.println("老师开始游泳~");
}
};
start(s2);
}
interface Swim{
void swimming();
}
// 实现类
public static void start(Swim s){
System.out.println("开始游");
s.swimming();
System.out.println("结束游");
}
}
函数式编程
使用lambda函数去替代某些匿名内部类对象,从而让程序更加简洁。
lambda表达式
lambda表达式是JDK8新增的一种语法,代表函数;可以用于替代并简化函数式接口的匿名内部类。
语法:(参数列表) -> { 语句块 }
- 无参写
(),一个参数可省括号,一条语句可省{}和return。
匿名内部类写法:
new Thread(new Runnable() { @Override public void run() { System.out.println("hello"); } }).start();转换成lambda写法:
new Thread(() -> System.out.println("hello")).start();
lambda表达式只能替代函数式接口的匿名内部类。函数式接口就是有且仅有一个抽象方法的接口。在接口上加上@FuncationalInterface注解即可。
// 1. 函数式接口 → Lambda 合法
@FunctionalInterface
interface Calculator {
int calc(int a, int b);
}
Calculator c = (x, y) -> x + y; // ✅ 编译通过
// 2. 接口里多一个抽象方法 → 不是函数式接口 → Lambda 非法
interface NotFunc {
void do1();
void do2(); // 多了一个
}
NotFunc f = () -> {}; // ❌ 编译错误:NotFunc 不是函数式接口
⭕省略写法
- 参数类型全部可以省略不写
- 如果只有一个参数,产生类型省略的同时
()也可以省略,但多个参数不能省略() - 如果lambda表达式中方法体只有一行代码,大括号可以不写,同时要省略封号
;,如果这行代码是return语句,也必须去掉return
方法引用
静态方法引用
语法:类名::静态方法
如果某个Lambda表达式里只是调用一个静态方法,并且“→”前后参数的形式一致,就可以使用静态方法引用。
实例方法引用
语法:对象名::实例方法
如果某个Lambda表达式里只是通过对象名称调用并且“→”前后参数的形式一致,就可以使用实例方法引用。
特定类型方法引用
语法:特定类名称::方法
如果某个Lambda表达式里只是调用一个特定类型的实例方法,并且前面参数列表中的第一个参数是作为方法的主调,后面的所有参数都是作为该实例方法的入参的,则此时就可以使用特定类型的方法引用。
构造器引用
语法:类名::new
如果某个Lambda表达式里只是在创建对象,并且“→”首前后参数情况一致,就可以使用构造器引用。
常用API
String
String代表字符串,它的对象可以封装字符串数据,并提供了很多方法完成对字符串的处理。
- 创建字符串对象,封装字符串数据
- 调用String提供的操作字符串数据的方法
封装String方法
创建字符串对象的方式:
-
Java程序中的所有字符串文字(例如“abc”)都为此类的对象。
String s1 = "hello" -
调用String类的构造器初始化字符串对象。

String创建对象的区别:
- 只要是以”.”方式写出的字符串对象,会存储到字符串常量池,且相同内容的字符串只存储一份;
- 通过new方式创建字符串对象,每new一次都会产生一个新的对象放在堆内存中。
调用String方法

下面只将两个比较常见的API。
public boolean equals(Object anObject)
作用:比较两个字符串的内容是否完全相同。
- 是区分大小写的。
- 如果传入的不是
String类型,会返回false。 - 与
==不同,==比较的是内存地址,而equals比较的是内容。
示例:
String a = "hello";
String b = new String("hello");
System.out.println(a.equals(b)); // 输出 true
System.out.println(a == b); // 输出 false
public String substring(int beginIndex, int endIndex)
作用:截取字符串的一部分,返回一个新的字符串。
beginIndex:起始位置(包含)。endIndex:结束位置(不包含)。- 字符串索引从 0 开始。
- 如果
beginIndex == endIndex,返回空字符串""。
示例:
String str = "hello world";
String sub = str.substring(0, 5);
System.out.println(sub); // 输出 "hello"
ArrayList
本质就是一个可变长度数组。
基本使用:
ArrayList<> list = new ArrayList<>()
list.add('111')
常用方法汇总:

异常
异常体系
Error:代表系统级别异常
Exception:程序出现的异常
- 运行异常:运行阶段出现的异常(代码写的错误)
- 空值异常
- 编译异常:编译阶段出现的异常(提醒你代码易错点)
异常处理
异常的作用
-
异常是用来定位程序bug的关键信息
-
可以作为方法内部的一种特殊返回值,以便通知上层调用者,方法的执行问题
throw new Exception('异常')
抛出异常
在方法中使用throws关键字,可以将方法内部出现的异常抛出去去给调用者处理
方法 throws 异常1,异常2...{
...
}
捕获异常
直接捕获程序出现的异常
try{
// 监视可能出现异常的代码
}catch(异常类型1 变量){
// 处理异常
}catch(异常类型2 变量){
// 处理异常
}
例子:
public class Test {
public static void main(String[] args) {
int numerator = 10; // 被除数
int denominator = 0; // 除数
try{
test(numerator, denominator);
}catch(Exception e){
System.out.println("错误:不能除以零!");
}finally{
// finally块:无论是否捕获异常,都会执行
System.out.println("程序执行完毕!");
}
}
public static void test(int numerator, int denominator) throws Exception {
int result = numerator / denominator;
System.out.println("结果是:" + result);
}
}
自定义异常
Java无法为这个世界上全部的问题都提供异常类来代表,如果企业自己的某种问题,
想通过异常来表示,以便用异常来管理该问题,那就需要自己来定义异常类了。
- 自定义运行时异常(定义一个异常类继承RuntimeException)
- 自定义编译时异常(定义一个异常类继承Exception)
实现步骤:
-
继承
Exception// 自定义异常类 class DivisionByZeroException extends Exception { } -
重写
Exception构造器// 自定义异常类 class DivisionByZeroException extends Exception { public DivisionByZeroException(String message) { super(message); } } -
使用throw抛出自定义异常
public static void test(int numerator, int denominator) throws DivisionByZeroException { if (denominator == 0) { // 如果除数为0,抛出自定义异常 throw new DivisionByZeroException("不能除以零!"); } int result = numerator / denominator; System.out.println("结果是:" + result); }
例子:
package throwDemo;
// 自定义异常类
class DivisionByZeroException extends Exception {
public DivisionByZeroException(String message) {
super(message);
}
}
public class Test {
public static void main(String[] args) {
int numerator = 10; // 被除数
int denominator = 0; // 除数
try {
test(numerator, denominator);
} catch (Exception e) {
// 捕获自定义异常
System.out.println("错误:" + e.getMessage());
} finally {
// finally块:无论是否捕获异常,都会执行
System.out.println("程序执行完毕!");
}
}
public static void test(int numerator, int denominator) throws DivisionByZeroException {
if (denominator == 0) {
// 如果除数为0,抛出自定义异常
throw new DivisionByZeroException("不能除以零!");
}
int result = numerator / denominator;
System.out.println("结果是:" + result);
}
}
泛型
定义类、接口、方法时,同时声明了一个或者多个类型变量称为泛型类、泛型接口,泛型方法、它们统称为泛型。
作用:泛型提供了在编译阶段约束所能操作的数据类型,并自动进行检查的能力!
本质就是把具体的数据类型作为参数传给类型变量。
泛型的语法通常使用尖括号<>来定义类型参数。例如,List<T>表示一个可以存储类型为T的元素的列表。
泛型类
定义了一个可以存储任意类型数据的容器。
语法:
修饰符 class 类名<类型变量1,类型变量2,...>{
//...
}
注意:类型变量建议用大写的英文字母,常用的有:E、T、K、V 等
例子:
public class Box<T> {
private T content;
public void setContent(T content) {
this.content = content;
}
public T getContent() {
return content;
}
}
public class Test {
public static void main(String[] args) {
// 创建一个存储Integer的Box
Box<Integer> intBox = new Box<>();
intBox.setContent(10);
System.out.println("整数内容:" + intBox.getContent());
// 创建一个存储String的Box
Box<String> stringBox = new Box<>();
stringBox.setContent("Hello, World!");
System.out.println("字符串内容:" + stringBox.getContent());
}
}
泛型接口
泛型接口允许你在接口级别上使用类型参数。下面是一个泛型接口的例子,它定义了一个可以存储任意类型数据的队列。
语法:
修饰符 interface 接口名<类型变量1,类型变量2...>{
}
例子:
public interface Queue<T> {
void enqueue(T item); // 入队
T dequeue(); // 出队
boolean isEmpty(); // 检查队列是否为空
}
public class ArrayQueue<T> implements Queue<T> {
private T[] data;
private int front;
private int rear;
public ArrayQueue(int capacity) {
data = (T[]) new Object[capacity];
front = 0;
rear = 0;
}
@Override
public void enqueue(T item) {
if ((rear + 1) % data.length == front) {
throw new IllegalStateException("队列已满");
}
data[rear] = item;
rear = (rear + 1) % data.length;
}
@Override
public T dequeue() {
if (front == rear) {
throw new IllegalStateException("队列为空");
}
T item = data[front];
front = (front + 1) % data.length;
return item;
}
@Override
public boolean isEmpty() {
return front == rear;
}
}
public class Test {
public static void main(String[] args) {
Queue<Integer> intQueue = new ArrayQueue<>(5);
intQueue.enqueue(1);
intQueue.enqueue(2);
System.out.println("出队:" + intQueue.dequeue());
System.out.println("出队:" + intQueue.dequeue());
}
}
泛型方法
泛型方法允许你在方法级别上使用类型参数。下面是一个泛型方法的例子,它交换两个变量的值。
语法:
修饰符<类型变量1,类型变量2...> 返回值类型 方法名(形参列表){
}
通配符:
- 就是
?,可以在“使用泛型”的时候代表一切类型;E T K V是在定义泛型的时候使用。
上下限:
- 泛型上限:
?extends Car能接受的必须是Car或者其子类 - 泛型下限:
?super Car能接受的必须是Car或者其父类
例子:
public class Test {
// 泛型方法
public static <T> void swap(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
public static void main(String[] args) {
Integer[] intArray = {1, 2, 3, 4, 5};
System.out.println("交换前:" + java.util.Arrays.toString(intArray));
swap(intArray, 1, 3);
System.out.println("交换后:" + java.util.Arrays.toString(intArray));
String[] stringArray = {"a", "b", "c", "d", "e"};
System.out.println("交换前:" + java.util.Arrays.toString(stringArray));
swap(stringArray, 1, 3);
System.out.println("交换后:" + java.util.Arrays.toString(stringArray));
}
}
泛型支持的类型
泛型只支持对象类型(引用数据类型),不支持基本数据类型。
Array<int> list = new ArrayList<>(); ❌
包装类
包装类就是把基本数据类型的数据包装成对象的类型。
| 基本数据类型 | 对应的包装类 |
|---|---|
| byte | Byte |
| short | Short |
| int | Integer |
| long | Long |
| char | Character |
| float | Float |
| double | Double |
| boolean | Boolean |
使用方法:
-
过时用法:
Integer i = new Integer(100); -
建议用法:
Integer i = Integer.valueOf(100);
自动装箱
基本数据类型的数据可以直接变成包装对象的数据,不需要额外做任何操作。
Integer i = 100;
// 等同于 Integer i = Integer.valueOf(100);
自动拆箱 (了解)
把包装类型的对象直接给基本类型的数据
int it = i;
ArrayList <Integer> list = new ArrayList<>();
list.add(110); // 自动拆箱
int res = list.get(1); // 自动拆箱
包装类其他功能
-
可以把基本类型的数据转换成字符串类型
-
可以把字符串类型的数值转换成数值本身对应的真实数字
String str = '100' int i = Integer.parseInt(str) int i = Integer.valueOf(str) //也可以直接使用 valueOf
集合框架
集合是一种容器,用来装数据的,类似于数组,但集合的大小可变,在开发中也是非常常用。
Collection单列集合
单列集合就是每个元素(数据)只包含一个值。
Collection集合的分类:

Collection常用方法
| 方法名 | 说明 |
|---|---|
| public boolean add(E e) | 把给定的对象添加到当前集合中 |
| public void clear() | 清空集合中所有的元素 |
| public boolean remove(E e) | 把给定的对象在当前集合中删除 |
| public boolean contains(Object ob) | 判断当前集合中是否包含给定的对象 |
| public boolean isEmpty() | 判断当前集合是否为空 |
| public init size() | 返回集合中元素的个数 |
| public Object[] toArray() | 把集合中的元素存储到数组中 |
Collection遍历方式
-
迭代器
iterater():是用来遍历集合的专用方式Iterator<String> i = array.iterator(); // 得到迭代器对象 // 取数据 i.next(); // 使用while循环遍历 while(i.hasNext()){ String name = i.next() sout(name) } -
增强for循环:
// 格式: for(元素的数据类型 变量名 : 数组或者集合){ // 方法体 }// 例子 for(String name : names){ sout(name) } -
lambda表达式:
names.forEach(n -> sout(n));
循环遍历的区别:只有迭代器遍历才能解决`并发修改异常问题。
知识补充:认识并发修改异常问题
遍历集合的同时又存在增删集合元素的行为时可能出现业务异常,这种现象被称为
并发修改异常问题。
List系列集合特点就是元素是有序、可重复、有索引,并且可以通过索引来访问元素。
List实现类:
Java中的List是一个接口,不能直接实例化,需要使用它的实现类来实现,常见的实现类有:
ArrayList:基于动态数组实现,查询快,增删慢。LinkedList:基于双向链表来实现,增删快,查询慢。
List常用方法:
add(E e) |
添加元素到末尾 |
|---|---|
add(int index, E element) |
在指定位置插入元素 |
get(int index) |
获取指定位置的元素 |
set(int index, E element) |
修改指定位置的元素 |
remove(int index) |
删除指定位置的元素 |
remove(Object o) |
删除第一个匹配的元素 |
size() |
返回元素个数 |
isEmpty() |
判断是否为空 |
contains(Object o) |
判断是否包含某个元素 |
indexOf(Object o) |
返回元素第一次出现的索引 |
clear() |
清空所有元素 |
List使用方法:
-
导包
import java.util.ArrayList; import java.util.List; -
创建List对象
// 推荐写法:接口指向实现类 List<String> list = new ArrayListM<>(); // 其他写法 ArrayList<String> list = new ArrayList<>(); -
使用List对象
list.add('添加内容')
案例:
ArrayList:
import java.util.ArrayList;
import java.util.List;
public class ListDemo {
public static void main(String[] args) {
// 创建一个存储字符串的List
List<String> fruits = new ArrayList<>();
// 添加元素
fruits.add("苹果");
fruits.add("香蕉");
fruits.add("橙子");
System.out.println(fruits); // [苹果, 香蕉, 橙子]
// 在索引1处插入
fruits.add(1, "葡萄");
System.out.println(fruits); // [苹果, 葡萄, 香蕉, 橙子]
// 获取元素
String first = fruits.get(0);
System.out.println("第一个元素:" + first); // 苹果
// 修改元素
fruits.set(2, "芒果");
System.out.println(fruits); // [苹果, 葡萄, 芒果, 橙子]
// 删除元素
fruits.remove(0); // 删除索引0的元素
System.out.println(fruits); // [葡萄, 芒果, 橙子]
// 获取大小
System.out.println("大小:" + fruits.size()); // 3
// 遍历List
System.out.println("遍历方式1:for循环");
for (int i = 0; i < fruits.size(); i++) {
System.out.println(fruits.get(i));
}
System.out.println("遍历方式2:增强for循环");
for (String fruit : fruits) {
System.out.println(fruit);
}
System.out.println("遍历方式3:forEach + Lambda(Java 8+)");
fruits.forEach(System.out::println);
}
}
- List集合
LinkedList
LinkedList 特有方法(ArrayList 没有)
| 方法 | 说明 |
|---|---|
addFirst(E e) |
在链表开头插入元素 |
addLast(E e) |
在链表末尾插入元素(等同于 add) |
getFirst() |
获取第一个元素 |
getLast() |
获取最后一个元素 |
removeFirst() |
删除并返回第一个元素 |
removeLast() |
删除并返回最后一个元素 |
这些方法让 LinkedList 非常适合做 栈(Stack) 或 队列(Queue) 的实现。
案例:
LinkedList:
import java.util.LinkedList;
public class SimpleLinkedListDemo {
public static void main(String[] args) {
// 创建一个 LinkedList
LinkedList<String> fruits = new LinkedList<>();
// 添加几个水果
fruits.add("苹果");
fruits.add("香蕉");
fruits.add("橙子");
// 打印列表
System.out.println("水果列表:" + fruits); // [苹果, 香蕉, 橙子]
// 在最前面加一个
fruits.addFirst("葡萄");
System.out.println("在开头加上葡萄:" + fruits); // [葡萄, 苹果, 香蕉, 橙子]
// 在最后面加一个(和 add 一样)
fruits.addLast("芒果");
System.out.println("在末尾加上芒果:" + fruits); // [葡萄, 苹果, 香蕉, 橙子, 芒果]
// 删除第一个
fruits.removeFirst();
System.out.println("删除第一个:" + fruits); // [苹果, 香蕉, 橙子, 芒果]
// 删除最后一个
fruits.removeLast();
System.out.println("删除最后一个:" + fruits); // [苹果, 香蕉, 橙子]
// 获取第一个和最后一个
System.out.println("现在的第一个:" + fruits.getFirst()); // 苹果
System.out.println("现在的最后一个:" + fruits.getLast()); // 橙子
// 打印总共有几个
System.out.println("一共 " + fruits.size() + " 种水果");
}
}
- Set集合
Set系列集合特点是元素是无序的、不可以重复、无索引。
你可以把
set想象成一个"去重的List"
Set实现类
HashSet:最重用,无序,基于哈希表实现的Set集合。查询速度快LinkedHashSet:按照插入顺序,查询速度比HashSet稍慢TreeSet:按照自然顺序或者自定义规则排序,适合需要排序的场景。
Set常用方法
| 方法 | 说明 |
|---|---|
add(E e) |
添加元素(如果已存在,添加失败,返回 false) |
remove(Object o) |
删除指定元素 |
contains(Object o) |
判断是否包含某个元素 |
size() |
获取元素个数 |
isEmpty() |
是否为空 |
clear() |
清空所有元素 |
案例
HashSet去重:
import java.util.HashSet;
import java.util.Set;
public class SetDemo {
public static void main(String[] args) {
// 创建一个 Set 存放字符串
Set<String> names = new HashSet<>();
// 添加元素(尝试添加重复的)
names.add("张三");
names.add("李四");
names.add("王五");
names.add("张三"); // 重复了!
System.out.println("Set 内容:" + names);
// 输出:Set 内容:[张三, 李四, 王五] → 自动去重!
System.out.println("大小:" + names.size()); // 3
// 判断是否包含
System.out.println("包含张三吗?" + names.contains("张三")); // true
// 删除
names.remove("李四");
System.out.println("删除李四后:" + names); // [张三, 王五]
// 遍历 Set(不能用下标!)
System.out.println("遍历方式1:增强for循环");
for (String name : names) {
System.out.println(name);
}
System.out.println("遍历方式2:forEach + Lambda");
names.forEach(System.out::println);
}
}
三种Set的对比演示:
import java.util.*;
public class SetCompare {
public static void main(String[] args) {
Set<String> hashSet = new HashSet<>();
Set<String> linkedHashSet = new LinkedHashSet<>();
Set<String> treeSet = new TreeSet<>();
// 同时添加这些元素
String[] data = {"Bob", "Alice", "Charlie", "Alice"}; // Alice 重复
for (String s : data) {
hashSet.add(s);
linkedHashSet.add(s);
treeSet.add(s);
}
System.out.println("HashSet(无序,去重):" + hashSet);
// 可能输出:[Bob, Charlie, Alice] → 顺序不确定
System.out.println("LinkedHashSet(插入顺序):" + linkedHashSet);
// 输出:[Bob, Alice, Charlie] → 按你添加的顺序
System.out.println("TreeSet(自动排序):" + treeSet);
// 输出:[Alice, Bob, Charlie] → 按字母升序排列
}
}
⚠注意事项:
- 添加重复元素虽然不会报错,但是
add()会返回false - HashSet中的有且只能存一个
null - TreeSet中不能存
null,否则会报错
Map双列集合
双列集合就是每个元素包含两个值(键值对)。
生活中的例子:
- 姓名 ➜ 手机号(通过名字查电话)
- 单词 ➜ 中文意思(通过英文查中文)
- 身份证号 ➜ 学生信息
Map常用实现类:
HashMap:最常用,无序,查询快LinkedHashMap:按插入顺序排序TreeMap:按Key的自然顺序或者自定义顺序排序
Map核心方法:
| 方法 | 说明 |
|---|---|
put(K key, V value) |
添加或更新一个键值对 |
get(Object key) |
根据 key 获取 value,如果 key 不存在返回 null |
remove(Object key) |
删除指定 key 的键值对 |
containsKey(Object key) |
判断是否包含某个 key |
containsValue(Object value) |
判断是否包含某个 value |
size() |
获取键值对的数量 |
isEmpty() |
是否为空 |
clear() |
清空所有数据 |
keySet() |
获取所有 key 的集合(Set) |
values() |
获取所有 value 的集合(Collection) |
entrySet() |
获取所有“键值对”的集合(Set<Map.Entry<K,V>>) |
案例:
HashMap基本使用:
import java.util.HashMap;
import java.util.Map;
public class MapDemo {
public static void main(String[] args) {
// 创建一个 Map:姓名 -> 年龄
Map<String, Integer> ages = new HashMap<>();
// 添加数据
ages.put("张三", 25);
ages.put("李四", 30);
ages.put("王五", 28);
ages.put("张三", 26); // Key 重复,会覆盖之前的值
System.out.println("所有人:" + ages);
// 输出:{张三=26, 李四=30, 王五=28}
// 查询
System.out.println("张三的年龄:" + ages.get("张三")); // 26
System.out.println("赵六的年龄:" + ages.get("赵六")); // null(不存在)
// 判断是否存在 key
if (ages.containsKey("李四")) {
System.out.println("找到了李四!");
}
// 删除
ages.remove("王五");
System.out.println("删除王五后:" + ages); // {张三=26, 李四=30}
// 获取总数
System.out.println("共有 " + ages.size() + " 个人");
}
}
Map遍历方式
-
通过
keySet遍历for(String name : ages.keySet()){ Integer age = ages.get(name); System.out.println(name + "的年龄:" + age); } -
遍历
Valuesfor(Integer age : ages.values()){ System.out.println("年龄" + age) } -
通过
entrySetfor(Map.Entry<String,Integer> entry : ages.entrySet()){ String name = entry.getKey(); Integer age = entry.getValue(); System.out.println(name + "-->" + age); }
Stream流
在Java中,Stream(流)是一种用于处理集合数据的抽象方式。通过流,我们可以执行复杂的查询操作,如过滤、排序、映射等,而无需编写冗长的循环和条件语句。流的操作可以链式调用,使得代码更加简洁易读。
生活类比:
想象一下你有一个装满不同水果的篮子,你想从中挑选出所有的苹果并计算他们的总重量。如果没有流,你可能需要手动遍历整个篮子,检查每个水果是否为苹果,并累加苹果的重量。如果使用了流,这就会像给这个篮子添加了一个只能助手,自动帮你完成筛选和计算任务。
语法结构:
要创建一个流,通常从一个集合或数组开始,然后通过调用.stream()方法来生成流对象。
stream = collection.stream(); // 创建流
流的操作分为两种类型:中间操作(如过滤、映射)和终端操作(如收集结果)。
- 中间操作返回一个新的流,允许链式调用,
- 终端操作则触发实际的数据处理过程。
Stream的常用中间方法:
-
filter():过滤流中的元素stream.filter(s -> s.startsWith("a")); -
map():对流中的每个元素应用一个函数stream.map(String::toUpperCase); -
sorted():对流中的元素进行排序stream.sorted(); -
collect():对流的结果收集到一个集合中stream.collect(Collectors.toList()); -
distinct():去除流中的重复元素
使用案例:
下面的例子演示了如何使用流从一个字符串列表中筛选出所有长度大于3的字符串,并将它们转换为大写形式:
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class LambdaExample {
public static void main(String[] args) {
// 创建一个包含一些水果名称的列表
List<String> items = Arrays.asList("apple", "banana", "avocado", "kiwi");
// 使用Stream API和Lambda表达式进行数据处理
List<String> result = items.stream() // 转换列表为流
.filter((String s) -> s.startsWith("a")) // 过滤:只保留以"a"开头的字符串
.map((String s) -> s.toUpperCase()) // 映射:将过滤后的字符串转换为大写
.collect(Collectors.toList()); // 收集:将处理结果收集到一个新的列表中
// 打印处理后的结果
System.out.println(result); // 输出 [APPLE, AVOCADO]
}
}
Stream的常用终结方法:
-
forEach():对流中的每个元素执行某个操作stream.forEach(System.out::println); -
collect():将流的结果收集到一个集合或其他数据结构中List<String> collectedResult = stream.collect(Collectors.toList()); -
reduce():通过累加器函数将流中的元素组合起来Optional<String> reduced = stream.reduce((s1, s2) -> s1 + "-" + s2); -
count():返回流中元素的数量long count = stream.count(); -
findFirst():返回当前流中的第一个元素Optional<String> first = stream.findFirst(); -
findAny():返回当前流中的任意一个元素(在并行流中使用时特别有用)Optional<String> any = stream.findAny();
数据存储
File类
File是java.io.包下的类,File类的对象,用于代表当前操作系统的文件(可以是文件、或文件夹)
⚠注意:File类只能对文件本身进行操作,不能读写文件里面存储的数据。
📖知识补充:相对路径和绝对路径
- 带有盘符的都是绝对路径
- 不带有盘符,默认是到你的工程下直接寻找文件的
创建File对象:
File 对象名 = new File("绝对路径")
File常用方法:
-
创建新文件或目录
createNewFile():当且仅当不存在具有此抽象路径名指定名称的文件时,不可分地创建一个新的空文件。mkdir():创建由此抽象路径名命名的目录。mkdirs():创建由这个抽象路径名命名的目录,包括任何必需但不存在的父目录。
-
删除文件或目录
delete():删除此抽象路径名表示的文件或目录。(⚠注意:默认只能删除文件和空文件夹,删除后文件不会进入回收站)deleteOnExit():在虚拟机终止时,请求删除此抽象路径名表示的文件或目录。
-
获取信息
exists():测试此抽象路径名表示的文件或目录是否存在。isFile()和isDirectory():分别测试该抽象路径名是否为文件或目录。length():返回由此抽象路径名表示的文件的长度。lastModified():返回文件最后修改时间。
-
遍历目录
-
list():获取当前目录下所有一级文件名称到一个字符串数组中返回File f = new File("文件路径"); String[] names = f.list(); // 遍历出所有文件名 for(String name : names){ sout(name) } -
listFiles():去当前目录下所以的一级文件名称到一个File文件对象数组中去返回File f = new File("文件路径"); File[] files = f.listFiles();
-
| 方法名 | 描述 | 是否可能抛出异常 |
|---|---|---|
createNewFile() |
当且仅当不存在具有此抽象路径名指定名称的文件时,不可分地创建一个新的空文件。 | IOException |
mkdir() |
创建由此抽象路径名命名的目录。 | SecurityException (如果安全管理器存在并且其 checkRead 方法不允许该操作) |
mkdirs() |
创建由这个抽象路径名命名的目录,包括任何必需但不存在的父目录。 | SecurityException |
delete() |
删除此抽象路径名表示的文件或目录。 | SecurityException |
deleteOnExit() |
在虚拟机终止时,请求删除此抽象路径名表示的文件或目录。 | 无 |
exists() |
测试此抽象路径名表示的文件或目录是否存在。 | 无 |
isFile() |
测试该抽象路径名是否为文件。 | 无 |
isDirectory() |
测试该抽象路径名是否为目录。 | 无 |
length() |
返回由此抽象路径名表示的文件的长度。 | 无 |
lastModified() |
返回文件最后修改时间。 | 无 |
listFiles() |
返回一个抽象路径名数组,这些路径名表示此抽象路径名表示的目录中的文件。 | SecurityException, NullPointerException (如果抽象路径名为空) |
listFiles方法注意事项:
- 当主调是文件时候,或者路径不存在时候,返回
null- 当主调是空文件夹时,返回一个长度为0的数组
- 当主调是一个有内容的文件夹时,将其里面所有以及文件和文件夹的路径都放在File数组中返回
- 当主调是一个文件夹时,且里面有隐藏文件时,将里面所有文件和文件夹的路径放在File数组中返回(包含隐藏文件)
- 当主调是一个文件夹时,但是没有访问权限,则返回
null
实例代码:
import java.io.File;
import java.io.IOException;
public class Main {
public static void main(String[] args) {
// 创建一个File对象指向目标文件
File file = new File("example.txt");
try {
// 尝试创建新文件
if (file.createNewFile()) {
System.out.println("文件已创建:" + file.getName());
} else {
System.out.println("文件已存在。");
}
} catch (IOException e) {
System.out.println("发生错误!");
e.printStackTrace();
}
// 检查文件是否存在
if (file.exists()) {
System.out.println("文件名为: " + file.getName());
System.out.println("绝对路径: " + file.getAbsolutePath());
System.out.println("是否为文件: " + file.isFile());
} else {
System.out.println("文件不存在。");
}
}
}
方法递归
认识递归:
方法调用自身的形式称为方法递归。
递归算法三要素
- 递归公式
- 递归终结点
- 递归方向走向终结点
递归的形式:
直接递归:方法自己调用自己
// 计算n的阶乘 public static int factorial(int n) { if (n == 1) { return 1; // 基本情况 } else { return n * factorial(n - 1); // 递归调用 } }间接递归:方法调用其他方法,其方法又回调自己
public class SimpleRecursion { // 判断是否为偶数 public static boolean isEven(int number) { if (number == 0) { return true; } else { return isOdd(number - 1); // 调用isOdd方法 } } // 判断是否为奇数 public static boolean isOdd(int number) { if (number == 0) { return false; } else { return isEven(number - 1); // 回调isEven方法 } } }
递归在文件搜索中的思路:
- 先拿到文件夹中的一级文件对象
- 遍历全部一级文件对象,判断是否符号条件
- 如果是文件夹,则进入文件夹继续遍历
File dir = new File("文件目录")
searchFile("需要搜索的文件名")
/**
* 搜索指定文件名的文件
* @param dir 要搜索的目录
* @param fileName 搜索的文件名称
*/
public static void searchFile(File 搜索目录, String 搜索的文件名){
// 1. 判断极端情况
if(dirr == null || !dir.exists() || dir.isFile()){
return;
}
// 2. 获取目录下的所有以及文件或者文件夹对象
File[] files = dir.listFiles();
// 3. 判断当前目录下是否存在一级文件对象,存在才可以遍历
if(files != null && files.length > 0){
// 4. 遍历一级文件对象
for(File file : files){
// 5. 判断当前一级文件对象是否是文件
if(file.isFile()){
// 6. 判断文件名是否和目标文件名称一致
if(file.getName().contains(fileName)){
sout("找到目标文件:" + file.getAbsolutePath());
}
} else {
// 7、如果当前一级文件对象是文件夹,则继续递归调用
searchFile(file, fileName);
}
}
}
}
IO流
前置知识:
字符集
- ASCI字符集:只有英文、数字、符号等,占1个字节。
- GBK字符集:汉字占2个字节,英文、数字占1个字节
- UTF-8字符集:汉字占3个字节,英文、数字占1个字节。
字符集的编码、解码操作
对字符编码:
getBytes():使用平台的默认字符集将该String编码为一系列字节,将结果存储到新的直接数组中String name = 'AAA' byte[] bytes = name.getBytes(); 不写默认就是编译器默认编码使用指定的字符集将该String编码为一系列字节,将结果存储到新的字节数组中
String name = 'AAA' byte[] bytes = name.getBytes("GBK"); // 指定编码对字符的解码:
通过使用平台的默认字符集解码指定的字节数组来构造新的String
String names2 = new String(bytes); sout(name2)通过指定的字符集解码指定的字节数组来构造新的String
String names2 = new String(bytes,"GBK"); sout(name2)
IO流是用于处理输入和输出操作的基础工具。
通过使用IO流,程序可以读取外部数据(如文件或网络连接)到内存中,或者将内存中的数据写入外部存储介质。
字节流 vs 字符流:
- 字节流主要用于处理二进制数据(例如图像、音频文件等),它们包括
InputStream和OutputStream及其子类。 - 字符流适用于处理文本数据,它们基于字符编码(如UTF-8),包括
Reader和Writer及其子类。

文件字节输入流
作用:可以把磁盘文件中的数据以字节的形式读入到内存在中。
构造器:
-
FileInputStream(File file):创建直接输入流管道与源文件接通InputStream is = new FileInputStream(File文件对象) -
FileInputStream(String pathname):创建字节输入流管道与源文件接通InputStream is = new FileInputStream("绝对路径")
常用方法:
-
read():每次读取一个字节返回,如果发现没有数据则返回-1int b; while(( b = is.read()) != -1){ sout((char) b); }⚠读取汉字会出现乱码
-
read(byte[] buffer):每次用一个字节数据去读取数据,返回字节数组读取了多少个字节,如果没有数据则返回-1// 定义一个字节数组 byte[] buffer = new byte[3]; // 定义一个变量记住每次读取了多少个字节 int len; while((len = is.read(buffer))!= -1){ // 把读取的字节数组转换成字符串输出 String str = new String(buffer,0,len); sout(str) }参数说明:
-
0,表示从第一个读 -
len,表示读几个
⚠依然无法解决汉字输出乱码问题
-
使用字节流读取中文,如何保证输出不乱码,怎么解决?
readAllBytes():定义一个与文件一样大的字节数组,一次性读取完文件的全部字节,如果文件过大,创建的字节数组也会过大,可能引起内存溢出。
文件字节输出流
作用:以内存为基准,把内存中的数据以字节的形式写出到文件中去。
构造器:
-
FileOutputStream(File file):使用File对象创建字节输出流,将数据写入该文件。如果文件不存在,则会自动创建;如果存在,则默认覆盖原有内容。// 使用 File 对象创建流(覆盖模式) File file = new File("output.txt"); FileOutputStream fos1 = new FileOutputStream(file); -
FileOutputStream(String filepath):使用文件路径字符串创建字节输出流。功能与第一个构造器类似,但直接传入路径字符串。 -
FileOutputStream(File file, boolean append):创建输出流,并指定是否以追加模式写入。如果append为true,则在文件末尾追加数据;否则覆盖原内容。// 使用路径字符串创建流(追加模式) FileOutputStream fos2 = new FileOutputStream("output.txt", true); fos2.write("\nAppend more text.".getBytes()); -
FileOutputStream(String filepath, boolean append):使用文件路径和追加标志创建输出流。若append为true,则在文件末尾追加数据;否则覆盖原内容。
常用方法:
write(int a):将一个字节(int 的低8位)写入输出流。【注意:虽然参数是 int,但只使用其最低的8位】write(byte[] buffer): 将整个字节数组写入输出流write(byte[] buffer, int pos, int len):将字节数组从指定位置开始,写入指定长度的数据。pos是起始索引,len是要写入的字节数。close() throws IOException:关闭此输出流并释放与之关联的系统资源。必须调用,否则可能导致资源泄漏。
示例代码:
import java.io.FileOutputStream;
import java.io.IOException;
public class WriteExample {
public static void main(String[] args) {
String data = "Hello, Java IO!";
byte[] bytes = data.getBytes(); // 将字符串转换为字节数组
try (FileOutputStream fos = new FileOutputStream("output.txt")) {
// 方法1:写单个字节
for (byte b : bytes) {
fos.write(b);
}
// 方法2:写整个字节数组
// fos.write(bytes);
// 方法3:写数组的一部分
// fos.write(bytes, 0, bytes.length);
} catch (IOException e) {
e.printStackTrace();
}
}
}
字节输出流如何实现写出去的数据可以换行?
os.write( "\r\n".getBytes());
代码案例:字节流文件复制
public class CopyDemo1 {
public static void main(String[] args) {
// 目标:使用字节流完成文件的复制操作。
// 源文件:E:\resource\jt.jpg
// 目标文件:D:\jt_new.jpg (复制过去的时候必须带文件名的,无法自动生成文件名。)
try {
copyFile(srcPath: "E:\\resource\\jt.jpg", destPath: "D:\\jt_new.jpg");
} catch (Exception e) {
e.printStackTrace();
}
}
// 复制文件
public static void copyFile(String srcPath, String destPath) throws Exception {
// 1、创建一个文件字节输入流管道与源文件接通
InputStream fis = new FileInputStream(srcPath);
OutputStream fos = new FileOutputStream(destPath);
// 2、读取一个字节数组,写入一个字节数组 1024 + 1024 + 3
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, off: 0, len); // 读取多少个字节,就写入多少个字节
}
System.out.println("复制成功!");
}
}
JDK7开始提供了更简单的资源释放方案:try-with-resource,该资源使用完毕后,会自动调用其close()方法,完成对资源的释放!
public class CopyDemo1 {
public static void main(String[] args) {
try {
copyFile(srcPath: "E:\\resource\\jt.jpg", destPath: "D:\\jt_new.jpg");
} catch (Exception e) {
e.printStackTrace();
}
}
public static void copyFile(String srcPath, String destPath) throws Exception {
// 使用 try-with-resources 语句自动管理资源,确保流在使用后自动关闭
try (InputStream fis = new FileInputStream(srcPath);
OutputStream fos = new FileOutputStream(destPath)) {
byte[] buffer = new byte[1024];
int len;
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, off: 0, len);
}
System.out.println("复制成功!");
}
// try-with-resources 会自动处理资源的关闭,无需显式调用 close()
}
}
文件字符输入流
作用:以内存为基准,可以把文件中的数据以字符的形式读入到内存中。
构造器:
FileReader(File file)/FileReader(String pathname):创建字符输入流管道与源文件接通
常用方法:
read()/read(char[] buffer):读取字符输出
文件字符输出流
作用:以内存为基准,把内存中的数据以字符的形式写出到文件中去。
构造器
| 构造器 | 说明 |
|---|---|
public FileWriter(File file) |
创建字节输出流管道与源文件对象接通 |
public FileWriter(String filepath) |
创建字节输出流管道与源文件路径接通 |
public FileWriter(File file, boolean append) |
创建字节输出流管道与源文件对象接通,可追加数据 |
public FileWriter(String filepath, boolean append) |
创建字节输出流管道与源文件路径接通,可追加数据 |
方法名称
| 方法名称 | 说明 |
|---|---|
void write(int c) |
写一个字符 |
void write(String str) |
写一个字符串 |
void write(String str, int off, int len) |
写一个字符串的一部分 |
void write(char[] cbuf) |
写入一个字符数组 |
void write(char[] cbuf, int off, int len) |
写入字符数组的一部分 |
示例代码:
public static void main(String[] args) {
try (
// 1. 创建一个字符输出流对象,指定写出的目的地。
FileWriter fw = new FileWriter("day03-file-io/src/dlei07-out.txt")
) {
// 2. 写一个字符出去:public void write(int c)
fw.write('a');
fw.write(98); // ASCII码对应字符'b'
fw.write('磊');
// 3. 写一个字符串出去:public void write(String str)
fw.write("java");
fw.write("我爱Java,虽然Java不是最好的编程之一,但是可以挣钱");
// 4. 写一个字符数组出去:public void write(char[] cbuf)
char[] chars = "java".toCharArray();
fw.write(chars);
// 5. 写字符数组的一部分出去:public void write(char[] cbuf, int off, int len)
fw.write(chars, 1, 2); // 从索引1开始,写入2个字符:"va"
} catch (Exception e) {
e.printStackTrace();
}
}
字符输出流注意事项:
字符输出流写出数据后,必须刷新流,或者关闭流,写出去的数据才能生效。
| 方法名称 | 说明 |
|---|---|
public void flush() throws IOException |
刷新流,就是将内存中缓存的数据立即写到文件中去生效! |
public void close() throws IOException |
关闭流的操作,包含了刷新! |
import java.io.FileWriter;
import java.io.IOException;
public class FlushAndCloseExample {
public static void main(String[] args) {
try (
// 创建 FileWriter 对象,指定输出文件路径
FileWriter fw = new FileWriter("example.txt")
) {
// 写入一些数据到缓冲区
fw.write("Hello, ");
fw.write("world!");
// 1. 使用 flush() - 立即将缓冲区的数据写入文件,但不关闭流
System.out.println("正在执行 flush()...");
fw.flush(); // 此时 "Hello, world!" 已经被写入 example.txt 文件中
System.out.println("flush() 执行完毕,数据已写入文件。");
// 可以继续写入更多数据(因为流还未关闭)
fw.write(" 这是追加的内容。");
// 2. 使用 close() - 关闭流并自动刷新剩余数据
// 注意:try-with-resources 会自动调用 close()
// 如果手动调用,可以这样写:
// fw.close();
} catch (IOException e) {
e.printStackTrace();
}
// 当程序执行到这里时,fw 已经被自动 close()
// close() 内部已经包含了 flush() 操作
System.out.println("程序结束,资源已释放。");
}
}
缓冲流

缓冲字节流
作用:可可以提高字节输入流读取数据的性能
原理:缓冲字节输入流自带了8KB缓冲池;缓冲字节输出流也自带了8KB缓冲池
| 构造器 | 说明 |
|---|---|
public BufferedInputStream(InputStream is) |
把低级的字节输入流包装成一个高级的缓冲字节输入流,从而提高读取数据的性能 |
public BufferedOutputStream(OutputStream os) |
把低级的字节输出流包装成一个高级的缓冲字节输出流,从而提高写数据的性能 |
代码示例:
import java.io.*;
public class BufferedStreamExample {
public static void main(String[] args) {
try (
// 1. 创建一个低级字节输入流(FileInputStream)
InputStream fis = new FileInputStream("input.txt");
// 2. 使用 BufferedInputStream 包装低级输入流,提升读取性能
BufferedInputStream bis = new BufferedInputStream(fis)
) {
int data;
while ((data = bis.read()) != -1) {
System.out.print((char) data);
}
} catch (IOException e) {
e.printStackTrace();
}
try (
// 3. 创建一个低级字节输出流(FileOutputStream)
OutputStream fos = new FileOutputStream("output.txt");
// 4. 使用 BufferedOutputStream 包装低级输出流,提升写入性能
BufferedOutputStream bos = new BufferedOutputStream(fos)
) {
String content = "Hello, Buffered Output Stream!";
byte[] bytes = content.getBytes();
bos.write(bytes);
bos.flush(); // 手动刷新缓冲区(可选,close() 会自动刷新)
} catch (IOException e) {
e.printStackTrace();
}
}
}
缓冲字符输入流
作用:自带8K(8192)的字符缓冲池,可以提高字符输入流读取字符数据的性能。
构造器
| 构造器 | 说明 |
|---|---|
public BufferedReader(Reader r) |
把低级的字符输入流包装成字符缓冲输入流管道,从而提高字符输入流读取字符数据的性能 |
新增功能
字符缓冲输入流新增的功能:按照行读取字符
| 方法 | 说明 |
|---|---|
| public String readLine() | 读取一行数据返回,如果没有数据可读就返回null |
代码示例:
import java.io.*;
public class BufferedReaderExample {
public static void main(String[] args) {
try (
// 1. 创建低级字符输入流( FileReader 是 Reader 的子类)
Reader reader = new FileReader("input.txt");
// 2. 使用 BufferedReader 包装低级字符流,提升读取性能
BufferedReader br = new BufferedReader(reader)
) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
缓存字符输入流
作用:自带8K的字符缓冲池,可以提高字符输出流写字符数据的性能。
构造器
| 构造器 | 说明 |
|---|---|
public BufferedWriter(Writer r) |
把低级的字符输出流包装成一个高级的缓冲字符输出流管道,从而提高字符输出流写数据的性能 |
新增方法:换行
| 方法 | 说明 |
|---|---|
public void newLine() |
换行(自动使用系统默认的换行符) |
代码示例:
import java.io.*;
public class BufferedWriterExample {
public static void main(String[] args) {
try (
// 1. 创建低级字符输出流(FileWriter 是 Writer 的子类)
Writer writer = new FileWriter("output.txt");
// 2. 使用 BufferedWriter 包装低级字符流,提升写入性能
BufferedWriter bw = new BufferedWriter(writer)
) {
// 写入多行内容
bw.write("第一行内容");
bw.newLine(); // 换行
bw.write("第二行内容");
bw.newLine();
bw.write("第三行内容");
bw.newLine();
// 或者直接写入带换行的内容
bw.write("第四行内容");
bw.newLine();
System.out.println("数据已写入文件!");
} catch (IOException e) {
e.printStackTrace();
}
}
}
其他流
字符输入转换流
解决不同编码时,字符流读取文本内容乱码的问题。
解决思路:先获取文件的原始字节流,再将其按真实的字符集编码转成字符输入流,这样字符输入流中的字符就不乱码了。
构造器
| 构造器 | 说明 |
|---|---|
public InputStreamReader(InputStream is) |
把原始的字节输入流,按照代码默认编码转成字符输入流(与直接用 FileReader 的效果一样) |
public InputStreamReader(InputStream is, String charset) |
把原始的字节输入流,按照指定字符集编码转成字符输入流(重点) |
示例代码:
import java.io.*;
public class InputStreamReaderExample {
public static void main(String[] args) {
try (
// 1. 创建字节输入流(读取文件)
InputStream fis = new FileInputStream("input.txt");
// 2. 使用 InputStreamReader 将字节流转换为字符流(指定编码)
InputStreamReader isr = new InputStreamReader(fis, "UTF-8") // 指定 UTF-8 编码
) {
int data;
while ((data = isr.read()) != -1) {
System.out.print((char) data);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
打印流
作用:打印流可以实现更方便、更高效的打印数据出去,能能实现打印啥出去就是啥出去。
构造器
| 构造器 | 说明 |
|---|---|
public PrintStream(OutputStream/File/String) |
打印流直接通向字节输出流/文件/文件路径 |
public PrintStream(String fileName, Charset charset) |
可以指定写出去的字符编码 |
public PrintStream(OutputStream out, boolean autoFlush) |
可以指定实现自动刷新 |
public PrintStream(OutputStream out, boolean autoFlush, String encoding) |
可以指定实现自动刷新,并可指定字符的编码 |
示例代码:
import java.io.*;
public class PrintStreamExample {
public static void main(String[] args) {
try (
// 1. 直接通过文件路径创建 PrintStream(默认 UTF-8 编码)
PrintStream ps1 = new PrintStream("output1.txt");
// 2. 指定编码(如 GBK)创建 PrintStream
PrintStream ps2 = new PrintStream("output2.txt", "GBK");
// 3. 通过 OutputStream 创建并开启自动刷新
FileOutputStream fos = new FileOutputStream("output3.txt");
PrintStream ps3 = new PrintStream(fos, true); // autoFlush = true
// 4. 通过 OutputStream + 编码 + 自动刷新
PrintStream ps4 = new PrintStream(fos, true, "UTF-8")
) {
// 使用 ps1:基本打印
ps1.println("Hello, World!");
ps1.println("这是第一行内容。");
// 使用 ps2:指定编码(如 GBK)
ps2.println("中文测试,使用 GBK 编码写入。");
// 使用 ps3:自动刷新(每写一行就刷新到磁盘)
ps3.println("这行会立即写入文件,因为开启了 autoFlush。");
ps3.println("无需手动 flush()。");
// 使用 ps4:同时设置编码和自动刷新
ps4.println("UTF-8 编码 + 自动刷新,高效写入。");
System.out.println("所有数据已写入文件!");
} catch (IOException e) {
e.printStackTrace();
}
}
}
特殊数据流
允许把数据和其类型一并写出去,持直接写入 int, double, boolean, String 等类型,无需手动转换为字节。
构造器
| 构造器 | 说明 |
|---|---|
public DataOutputStream(OutputStream out) |
创建新数据输出流,包装基础的字节输出流 |
方法
| 方法 | 说明 |
|---|---|
public final void writeByte(int v) throws IOException |
将 byte 类型的数据写入基础的字节输出流 |
public final void writeInt(int v) throws IOException |
将 int 类型的数据写入基础的字节输出流 |
public final void writeDouble(Double v) throws IOException |
将 double 类型的数据写入基础的字节输出流 |
public final void writeUTF(String str) throws IOException |
将字符串数据以 UTF-8 编码成字节写入基础的字节输出流 |
void write(int/byte[]/byte[]一部分) |
支持写字节数据出去 |
示例代码:
import java.io.*;
public class DataOutputStreamExample {
public static void main(String[] args) {
try (
// 1. 创建基础字节输出流(写入文件)
FileOutputStream fos = new FileOutputStream("data.out");
// 2. 使用 DataOutputStream 包装基础流,支持写基本数据类型
DataOutputStream dos = new DataOutputStream(fos)
) {
// 写入各种基本数据类型
dos.writeByte((byte) 100); // 写 byte
dos.writeInt(12345); // 写 int
dos.writeDouble(3.14159); // 写 double
dos.writeUTF("Hello, Java!"); // 写字符串(UTF-8 编码)
// 写入字节数组
byte[] bytes = {1, 2, 3, 4, 5};
dos.write(bytes); // 写整个数组
// 写入字节数组的一部分
dos.write(bytes, 1, 3); // 从索引1开始,写3个字节
System.out.println("数据已写入文件!");
} catch (IOException e) {
e.printStackTrace();
}
}
}
IO框架
封装了JaVa提供的对文件、数据进行操作的代码,对外提供了更简单的方式来对文件进行操作,对数据进行读写等。
导入commons-io.jar框架到项目方法:
- 在项目中创建一个文件夹
lib - 将
commons-io.jar文件复制到lib文件夹中 - 在jar文件上点击右键,选择
Add as Library - 在类中导入包使用
FileUtils 类提供的部分方法展示
| 方法 | 说明 |
|---|---|
public static void copyFile(File srcFile, File destFile) |
复制文件 |
public static void copyDirectory(File srcDir, File destDir) |
复制文件夹 |
public static void deleteDirectory(File directory) |
删除文件夹 |
public static String readFileToString(File file, String encoding) |
读取文件内容为字符串 |
public static void writeStringToFile(File file, String data, String charset, boolean append) |
将字符串写入文件 |
代码示例:
import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;
public class FileUtilsExample {
public static void main(String[] args) {
try {
// 定义源文件和目标路径
File srcFile = new File("src.txt");
File destFile = new File("dest.txt");
File srcDir = new File("srcFolder");
File destDir = new File("destFolder");
// 1. 复制文件
FileUtils.copyFile(srcFile, destFile);
System.out.println("文件复制成功!");
// 2. 复制文件夹(递归)
FileUtils.copyDirectory(srcDir, destDir);
System.out.println("文件夹复制成功!");
// 3. 删除文件夹(递归删除所有内容)
FileUtils.deleteDirectory(destDir);
System.out.println("文件夹删除成功!");
// 4. 读取文件内容为字符串(指定编码)
String content = FileUtils.readFileToString(new File("data.txt"), "UTF-8");
System.out.println("文件内容:" + content);
// 5. 写字符串到文件(指定编码和是否追加)
FileUtils.writeStringToFile(new File("output.txt"), "Hello, Commons IO!", "UTF-8", false);
System.out.println("数据写入成功!");
} catch (IOException e) {
e.printStackTrace();
}
}
}
多线程
线程就是一个程序内部的一条执行流程。
多线程是指从软硬件上实现的多条执行流程的技术(多条线程由CPU负责调度执行)。
创建线程
继承Thread类创建
-
定义一个子类继承
Thread类,作为线程类class MyThread extends Thread {} -
重写
Thread类的run方法class MyThread extends Thread { @Override public void run() {} } -
在
run方法中编写线程的任务代码class MyThread extends Thread { @Override public void run() { System.out.println("线程开始执行"); } } -
创建线程类的对象
Thread t = new MyThread(); -
调用
start方法,启动线程t.start();
完整代码:
public class Main {
public static void main(String[] args) {
// 4. 创建线程类的对象
Thread t = new MyThread();
// 5. 调用`start`方法,启动线程
t.start();
// 对比主线程
for (int i = 0; i < 5; i++){
System.out.println("主线程开始执行"+ i);
}
}
}
// 1. 定义一个子类继承`Thread`类,作为线程类
class MyThread extends Thread {
@Override
// 2. 重写`Thread`类的`run`方法
public void run() {
// 3. 在`run`方法中编写线程的任务代码
for (int i = 0; i < 5; i++){
System.out.println("子线程开始执行" + i);
}
}
}
⚠注意事项:
- 不要把主线程放在启动子线程之前,不然永远都是主线程先跑
实现Runnable接口创建
- 定义一个线程任务类实现
Runnable接口
class MyRunnable implements Runnable{}
-
重写
run方法,设置线程任务class MyRunnable implements Runnable{ @Override public void run() {} } } -
创建线程任务类的对象
Runnable r = new MyRunnable(); -
把线程任务对象交给线程对象来处理
Thread t = new Thread(r); -
启动线程
start方法t.start();
完整代码:
public class Main {
public static void main(String[] args) {
// 3. 创建线程任务类的对象
Runnable r = new MyRunnable();
// 4. 把线程任务对象交给线程对象来处理
Thread t = new Thread(r);
// 5. 启动线程
t.start();
// 对比主线程
for (int i = 0; i < 5; i++){
System.out.println("主线程开始执行"+ i);
}
}
}
// 1. 定义一个线程任务类实现`Runnable`接口
class MyRunnable implements Runnable{
@Override
// 2. 重写`run方法`,设置线程任务
public void run() {
for (int i = 0; i < 5; i++){
System.out.println("子线程开始执行"+ i);
}
}
}
匿名内部类写法:
public class Main {
public static void main(String[] args) {
// 匿名内部类简化写法
new Thread(() -> {
for (int i = 0; i < 5; i++){
System.out.println("子线程开始执行"+ i);
}
}).start();
// 对比主线程
for (int i = 0; i < 5; i++){
System.out.println("主线程开始执行"+ i);
}
}
}
实现Callable接口创建
前两种线程创建方式都存在的一个问题:他们都是重写
run方法均不能直接返回结果。
-
定义一个类实现
Callable接口// 1. 定义一个类实现`Callable`接口 class Task implements Callable<Integer> { } -
重写
call方法,定义线程执行体private int n; public MyCallable(int n) { this.n = n; } // 2. 实现call方法,定义线程执行体 public Integer call() throws Exception { int sum = 0; for (int i = 0; i < n; i++) { sum += i; } return sum; } -
创建一个Callable接口的实现类对象
Callable<Integer> c = new MyCallable(传参); -
把
Callable类型的对象封装成FutureTask(线程任务对象)FutureTask<Integer> ft = new FutureTask<Integer>(c); -
把线程任务对象交给
Thread对象Thread t = new Thread(ft); -
调用
start方法启动线程t.start(); -
线程执行完毕后,通过
FutureTask对象的get方法去获取线程任务执行的结果ft.get()
完整代码:
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class Main {
public static void main(String[] args) {
// 3. 创建一个Callable接口的实现类对象
Callable<Integer> c = new MyCallable(344);
// 4. 把Callable对象封装成一个真正的线程任务对象FutureTask对象
FutureTask<Integer> ft = new FutureTask<Integer>(c);
// 5. 把FutureTask对象作为参数传递给`Thread`对象
Thread t = new Thread(ft);
// 6. 启动线程
t.start();
// 7. 获取线程执行结果
try {
System.out.println("子线程计算结果为:" + ft.get());
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 1. 定义一个类实现`Callable`接口
class MyCallable implements Callable<Integer> {
private int n;
public MyCallable(int n) {
this.n = n;
}
// 2. 实现call方法,定义线程执行体
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i < n; i++) {
sum += i;
}
return sum;
}
}
三种创建线程方法对比:
方式 优点 缺点 继承 Thread 类 编程比较简单,可以直接使用 Thread 类中的方法 扩展性较差,不能再继承其他的类,不能返回线程执行的结果 实现 Runnable 接口 扩展性强,实现该接口的同时还可以继承其他的类 编程相对复杂,不能返回线程执行的结果 实现 Callable 接口 扩展性强,实现该接口的同时还可以继承其他的类。可以得到线程执行的结果 编程相对复杂
多线程常用方法
⭕总结:
run()是线程要执行的核心逻辑。start()是启动线程的关键方法。getName()/setName()用于管理线程名称。currentThread()用于获取当前运行的线程。sleep()实现线程暂停。join()实现线程之间的协调与同步。- 构造器支持通过
Runnable实现多线程,也支持命名线程。
Thread常用方法
| 方法 | 说明 |
|---|---|
public void run() |
线程的任务方法。当线程启动后,会自动执行此方法中的代码。 |
public void start() |
启动线程,调用此方法后,JVM 会创建一个新的线程并执行 run() 方法。 |
public String getName() |
获取当前线程的名称,默认名称为 Thread-索引(如 Thread-0, Thread-1)。 |
public void setName(String name) |
为线程设置自定义名称,便于调试和识别。 |
public static Thread currentThread() |
返回当前正在执行的线程对象,常用于获取当前线程的信息。 |
public static void sleep(long time) |
让当前线程休眠指定的毫秒数,之后继续执行。如果中断,会抛出 InterruptedException。 |
public final void join()... |
让调用该方法的线程等待当前线程执行完毕后再继续执行。可以传入超时时间。 |
Thread 的常见构造器
| 构造器 | 说明 |
|---|---|
public Thread(String name) |
创建一个线程,并为其指定名称。 |
public Thread(Runnable target) |
将一个实现了 Runnable 接口的对象封装为线程对象,由该线程执行 Runnable 的 run() 方法。 |
public Thread(Runnable target, String name) |
封装 Runnable 对象的同时,指定线程名称。 |
线程安全
多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题。
- 存在多个线程同时执行
- 同时访问一个共享资源
- 存在修改共享资源
线程同步
线程同步是线程安全问题的解决方案。
线程同步的核心思想:让多个线程先后依次访问共享资源,这样就可以避免出现线程安全问题。
线程同步的常见方案:
- 加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来。
同步代码块
作用:把访问共享资源的核心代码给上锁,以此保证线程安全。
synchronized(同步锁){
访问共享资源的核心代码
}
⚠注意实现:
- 建议使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象。
示例代码:
public class Counter {
private int count = 0;
private Object lock = new Object(); // 同步锁对象
public void increment() {
synchronized (lock) {
count++; // 访问共享资源
}
}
public int getCount() {
synchronized (lock) {
return count;
}
}
}
synchronized (lock):使用lock对象作为锁,保护共享资源。count++是共享资源操作,被同步代码块保护,避免多线程同时修改。- 每次只有一个线程能进入该代码块,确保线程安全。
同步方法
作用:把访问共享资源的核心方法给上锁,以此保证线程安全。
修饰符 synchronized 返回值类型 方法名称(形参列表){
操作共享资源代码
}
lock锁
Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大。
Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象。
构造器:
| 构造器 | 说明 |
|---|---|
public ReentrantLock() |
获得Lock锁的实现类对象 |
常用方法:
| 方法名 | 说明 |
|---|---|
void lock() |
获得锁 |
void unlock() |
释放锁 |
线程池
线程池就是一个可以复用线程的技术。
创建线程池
方法一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象。
构造器
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 非核心线程空闲存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂(可选)
RejectedExecutionHandler handler // 拒绝策略(可选)
)
各参数详解
| 参数 | 说明 |
|---|---|
corePoolSize |
线程池中常驻的核心线程数量。即使这些线程空闲,也不会被回收(除非设置了 allowCoreThreadTimeOut(true))。 |
maximumPoolSize |
线程池中允许存在的最大线程数。当任务过多、队列满时,会创建额外线程,最多到此值。 |
keepAliveTime |
超过 corePoolSize 的非核心线程在空闲时等待新任务的最长时间。超时则被销毁。 |
unit |
keepAliveTime 的时间单位,如 TimeUnit.SECONDS、MILLISECONDS 等。 |
workQueue |
存放待执行任务的阻塞队列。常见有: - LinkedBlockingQueue(无界) - ArrayBlockingQueue(有界) - SynchronousQueue(不存储任务,直接移交) |
threadFactory |
(可选)用于创建新线程的工厂。可自定义线程名、优先级等。常用 Executors.defaultThreadFactory()。 |
handler |
(可选)当线程池饱和(线程数达上限且队列满)时,对新提交任务的拒绝策略。 |
常用方法
| 方法名称 | 说明 |
|---|---|
void execute(Runnable command) |
执行一个 Runnable 任务(无返回值) |
Future<T> submit(Callable<T> task) |
提交一个 Callable 任务,返回 Future 对象,用于获取结果 |
void shutdown() |
等待所有任务执行完毕后,关闭线程池 |
List<Runnable> shutdownNow() |
立即关闭线程池,停止正在执行的任务,返回未执行的任务列表 |
示例代码
import java.util.concurrent.*;
// 创建一个自定义线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
4, // 最大线程数
10, // 空闲线程存活时间
TimeUnit.SECONDS, // 时间单位
new ArrayBlockingQueue<>(10), // 任务队列,最多10个任务等待
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
// 提交任务
executor.execute(() -> System.out.println("Task running in thread: " + Thread.currentThread().getName()));
// 关闭线程池
executor.shutdown();
方法二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象。
Executors类是一个线程池的工具类,提供了很多静态方法用于返回不同特点的线程池对象。
| 方法名称 | 说明 |
|---|---|
newFixedThreadPool(int nThreads) |
创建固定大小的线程池,线程数不变,任务多时进入队列等待。 |
newSingleThreadExecutor() |
创建只有一个线程的线程池,保证任务顺序执行。 |
newCachedThreadPool() |
创建缓存线程池,线程数量动态增长,空闲超过60秒会被回收。 |
newScheduledThreadPool(int corePoolSize) |
创建支持定时或周期性任务的线程池。 |
并发和并行
进程:正在运行的程序(软件)就是一个独立的进程。
线程是属于进程的,一个进程中可以同时运行很多个线程,进程中的多个线程其实是并发和并行执行的。
并发:进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限,为了保证全部线程都能往前执行,CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。
并行:在同一个时刻上,同时有多个线程在被CPU调度执行。
网络编程
可以让设备中的程序与网络上其他设备中的程序进行数据交互的技术(实现网络通信)
基本通信架构
基本的通信架构有2种形式:
- CS架构(Client客户端/Server服务端)
- BS架构(Browser浏览器/Server服务端)
⚠无论是CS架构,还是BS架构的软件都必须依赖网络编程!
网络编程三要素
IP:设备在网络中的地址,是设备在网络中的唯一标识
端口:应用程序在设备中的唯一标识
协议:连接和数据在网络中传输的规则
IP
IP:全称”互联网协议地址”,是分配给上网设备的唯一标识。目前,被广泛采用的IP地址形式有两种:IPv4、IPv6。
- IPv4,它使用32位地址,通常以点分十进制表示。
- IPv6,它使用128位地址,号称可以为地球上的每一粒沙子编号。
| 方法名称 | 说明 |
|---|---|
getLocalHost() |
获取本机的 IP 地址和主机名,返回一个 InetAddress 对象。 |
getHostName() |
获取当前 InetAddress 对象对应的主机名(如:localhost)。 |
getHostAddress() |
获取当前 InetAddress 对象对应的 IP 地址(如:127.0.0.1)。 |
getByName(String host) |
根据域名或 IP 地址字符串,解析出对应的 InetAddress 对象。 |
isReachable(int timeout) |
判断指定主机是否可达(可连通),超时时间单位为毫秒。 |
总结
通过这张图,你可以快速掌握 InetAddress 的核心功能:
- ✅ 获取本机信息 →
getLocalHost() - ✅ 获取主机名/IP →
getHostName()/getHostAddress() - ✅ 域名解析 →
getByName() - ✅ 检测连通性 →
isReachable()
端口
用来标记标记正在计算机设备上运行的应用程序,被规定为一个16位的二进制,范围是0~65535。
端口分类:
- 周知端口:0~1023,被预先定义的知名应用占用(如:HTTP占用80,FTP占用21)
- 注册端口:1024~49151,分配给用户进程或某些应用程序。
- 动态端口:49152到65535,之所以称为动态端口,是因为它一般不固定分配某种进程,而是动态分配。
⚠注意:我们自己开发的程序般选择使用程序的端口号一样,否则报错。
协议
网络上通信的设备,事先规定的连接规则,以及传输数据的规则被称为网络通信协议。
开放式网络互联标准:OSI网络参考模型
| OSI 层 | TCP/IP 层 | 各层对应协议 | 面向操作(程序员关注点) |
|---|---|---|---|
| 应用层 | 应用层 | HTTP、FTP、SMTP 等 | 开发浏览器、邮箱等应用 |
| 表示层 | —— | 加密、压缩、格式转换 | 数据编码/解码(如 JSON/XML) |
| 会话层 | —— | 建立、管理会话 | 连接状态维护(如登录态) |
| 传输层 | 传输层 | TCP、UDP | 选择可靠(TCP)或不可靠(UDP)传输 |
| 网络层 | 网络层 | IP、ICMP、ARP | 封装源 IP 和目标 IP 地址 |
| 数据链路层 | 数据链路层 + 物理层 | MAC 地址、以太网帧 | 二进制数据在物理设备中传输 |
| 物理层 | —— | 电缆、光纤、无线电波 | 信号传输(硬件层面) |
传输层的2个通信协议:
- UDP:用户数据报协议。
- TCP:传输控制协议。
UDP通信
(挖坑)
TCP通信
(挖坑)
Java高级技术
单元测试
就是针对最小的功能单元:方法,编写测试代码对其进行正确性测试。
Junit单元测试框架:
- 可以灵活的编写测试代码,可以针对某个方法执行测试,也支持一键完成对全部方法的自动化测试,且各自独立。
- 不需要程序员去分析测试的结果,会自动生成测试报告出来。
具体步骤:
- 将Junit框架的jar包导入到项目中(注意:IDEA集成了Junit框架,不需要我们自己手工导入了)
- 为需要测试的业务类,定义对应的测试类,并为每个业务方法,编写对应的测试方法(必须:公共、无参、无返回值)
- 测试方法上必须声明
@Test注解,然然后在测试方法中,编写代码调用被测试的业务方法进行测试 - 开始测试:选中测试方法,右键选择“JUnit运行””,如果测试通过则是绿色;如果测试失败,则是红色
断言:
assertEquals(返回信息,期望值,测试方法() )
案例:
public class Main {
public static void main(String[] args) {
System.out.println("1到100的和为:" + sumFunction(100));
}
/**
* 计算从1到n的整数和
* @param n 正整数
* @return 从1到n的和,如果n小于0则返回-1
*/
public static int sumFunction(int n){
int sum = 0;
if (n < 0) {
System.out.println("-----出 ❌ 错-----");
System.out.println("输入的数字必须大于0");
return -1;
}
for (int i = 1; i <= n; i++) {
sum += i;
}
System.out.println("1到" + n + "的和为:" + sum);
return sum;
}
}
import org.junit.Test;
import static org.junit.Assert.*;
public class TestClass {
@Test
public void testMainMethods() {
// 测试Main类中的各种数学计算方法
int sumResult = Main.sumFunction(100);
System.out.println("测试求和结果: " + sumResult);
assertEquals(-1, Main.sumFunction(-1));
}
}
反射
反射就是:加载类,并允许以编程的方式解剖类中的各种成分(成员变量、方法、构造器等)。
反射调用
-
加载类,获取类的字节码
Class对象(获取Class对象的三种方式)-
Class c1 = 类名.class -
调用Class提供的方法
Class forName(类的全类名); -
Object提供的方法:
对象.getClass();
-
-
获取类的信息
// 获取类的全类名 getName() // 获取类的简名 getSimpleName() -
获取类的构造器:
Constructor对象// 获取构造器 getDeclaredConstructers() // 获取构造器(只能拿public修饰的) getConstructers() -
获取类的成员变量:
Field对象// 获取成员变量 getDeclaredFields() // 获取单个成员变量 getDeclaredField("变量名") -
获取类的成员方法:
Method对象// 获取成员方法 getDeclaredMethods() // 获取单个成员方法 getDeclaredMethod("方法名") getDeclaredMethod("方法名",参数类型)
反射使用
暴力反射setAccessible(true)
构造器对象.setAccessible(true)
构造器反射调用
构造器对象.newInstance():调用此构造器对象
成员变量反射调用
成员变量对象.set(对象,内容)成员变量对象.get(对象)
方法反射调用
成员方法对象.invoke(对象,?参数)
反射作用
基本作用:
- 可以得到一个类的全部成分然后操作
- 可以破换封装性
- 可以绕过泛型的约束
- 最重要的用途是:适合做Java的框架,基本上,主流的框架都会基于反射设计出一些通用的功能。
注解
就是Java代码里的特殊标记,比如:@Override、@Test等,作用是:让其他程序根据注解信息来决定怎么执行该程序。
自定义注解
语法格式:
public @interface 注解名称{
属性类型 属性名() ?default 默认值;
}
特殊属性名:value(在使用时属性必须只有一个value属性)
public @interface 注解名称{
属性类型 value;
}
使用:
@注解名称(属性名=属性值)
@注解名称(属性值) // 特殊属性名的时候
如果注解中只有一个value属性,使用注解时,value名称可以不写!!
元注解
元注解是注解注解的注解。
@Target(ElementType.Type)
@Retention(RetentionPolicy.RUNTIME)
public @interface 注解名称{
}
@Target
作用:声明被修饰的注解只能在哪些位置使用
@Target(ElementType.Type)
Type:类或者接口FIELD:成员变量METHOD:成员方法PARAMETER:方法参数CONSTRUCTOR:构造器LOCAL_VARIABLE:局部变量
@Retention
作用:声明注解的保留周期。
@Retention(RetentionPolicy.RUNTIME)
SOURCE:只作用于源码阶段,字节码文件中不存在。CLASS(默认值):保留到字节码文件阶段,运行阶段不存在。RUNTIME(开发常用):一直保留到运行阶段。
注解的解析
就是判断类上、方法上成员变量上否存在注解并把注解里的内容给解析出来。
如何解析注解?
指导思想:要解析谁上面的注解,京就应该先拿到谁,比如要解析类上面的注解,则应该先获取该类的Class对象,再通过Class对象解析其上面的注解。比如要解析成员方法上的注解,则应该获取到该成员方法的Method对象,再通过Method对象解析其上面的注解。
Class、Method、Field、Constructor、都实现了AnnotatedElement接口,它们都拥有解析注解的能力。
| 方法 | 返回值 | 作用 |
|---|---|---|
getDeclaredAnnotations() |
Annotation[] |
获取当前元素(类、方法、字段等)自己声明的所有注解(不包括继承的) |
getDeclaredAnnotation(Class<T> annotationClass) |
T |
获取当前元素上指定类型的注解对象,如果不存在则返回 null |
isAnnotationPresent(Class<? extends Annotation> annotationClass) |
boolean |
判断当前元素是否标注了指定类型的注解 |
动态代理
java.lang.reflect.Proxy类:提供了为对象产生代理对象的方法:newProxyInstance()
public static Object newProxyInstance(
ClassLoader loader, // 类加载器
Class<?>[] interfaces, // 代理对象需要实现的接口数组
InvocationHandler h // 调用处理器
)
三个参数的详细说明:
ClassLoader loader:- 用于加载代理类的类加载器
- 通常使用被代理对象的类加载器:
target.getClass().getClassLoader()
Class<?>[] interfaces:- 代理对象需要实现的接口数组
- 必须是接口,不能是类
- 代理对象会实现这些接口,因此可以转换为这些接口类型
InvocationHandler h:- 调用处理器,实现
InvocationHandler接口 - 当代理对象的方法被调用时,会执行处理器中的
invoke方法
- 调用处理器,实现
返回值:
- 返回一个实现了指定接口的代理对象
- 需要强制转换为相应的接口类型
示例代码:
// 学生行为接口
public interface StudentBehavior {
void study(String name);
String play();
}
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student implements StudentBehavior {
private String name;
@Override
public void study(String knowledge) {
System.out.println(name + "要开始学习" + knowledge + "啦!!!.");
System.out.println("");
}
@Override
public String play() {
System.out.println(name + " 开始游戏啦!!!.");
System.out.println("");
return "游戏中...🤗";
}
}
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
public class ProxyUtils {
// 创建一个代理对象
public static StudentBehavior createProxy(Student s) {
/**
* 参数1:用于执行哪个类加载器生成的代理类
* 参数2:用于指定代理对象需要实现的接口
* 参数3:用于指定代理类需要如何去代理(代理要做哪些事情)
*/
StudentBehavior proxy = (StudentBehavior) Proxy.newProxyInstance(
ProxyUtils.class.getClassLoader(),
s.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
/**
* 用来声明代理对象要干那些事情
* 参数一:proxy接受代理对象本身(暂时用处不大)
* 参数二:method表示代理对象要调用的方法(正在被代理的方法)
* 参数三:args表示代理对象要调用方法时实际参数
*/
System.out.println("代理对象开始...");
String methodName = method.getName();
if ("study".equals(methodName)) {
System.out.println("代理对象将提醒你学习📖...");
}else if("play".equals(methodName)){
System.out.println("代理对象将提醒你游戏🔮...");
}
System.out.println("代理对象完毕...");
System.out.println("---------------");
Object result = method.invoke(s, args);
return result;
}
}
);
return proxy;
}
}
public class Main {
public static void main(String[] args) {
Student s = new Student("张三");
StudentBehavior proxy = ProxyUtils.createProxy(s);
proxy.study("语文");
System.out.println(proxy.play());
}
}
更多推荐




所有评论(0)