重温java基础知识

前言

自从准备考试以后很久都没有整过我自己的老本行了,很多东西都忘得差不多了,我决定在这个学期把丢下的这些东西全都捡起来
还有硬件的一些专业知识,包括最新的ai都学习起来

快速整理:ctrl+alt+L
快速运行:ctrl+shift+F10
多行注释:ctrl+alt+/
单行:ctrl+/
构造方法等:alt+insert
try-catch: ctrl+alt+t
Math.sqrt() 用于计算平方根
ctrl + Alt+U 查看类图

  • 斐波那契数列公式:
    F(0) = 0

F(1) = 1

F(n) = F(n-1) + F(n-2) (当 n ≥ 2)

  • 海伦公式计算三角形面积:
    计算三边的长度:a, b, c
    计算半周长:s = (a + b + c) / 2
    计算面积:area = sqrt(s * (s - a) * (s - b) * (s - c))

  • 求2的n次方可以用 int result = 1 << n; // 使用位运算:1左移n位

第一章数据类型转换

格式化输出

1
System.out.printf("a/b=%.2f\n", (a / b));//保留小数点后两位
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package diyizhang;
//强制类型转换
public class App3_2 {
public static void main(String[] args) {
int a = 155 , b=6;
float g,h;
System.out.println("a="+a+",b="+b);
g=a/b;
System.out.println("g=a/b="+g);
System.out.println("a="+a+",b="+b);
h=(float) a/b;
System.out.println("h=a/b="+h);
System.out.println("(int) h="+(int)h);
/******************下面是文字转数据的**********************/
String name= "3.14";
float parseFloat = Float.parseFloat(name);
System.out.println(parseFloat);

}

}

精度保留

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 示例数据:计算 10/3 的结果
double result = 10.0 / 3;

// 1. 使用 System.out.printf() 直接输出
System.out.println("使用 printf() 保留小数:");
System.out.printf("保留2位小数:%.2f%n", result); // 输出 3.33
System.out.printf("保留5位小数:%.5f%n", result); // 输出 3.33333
System.out.printf("保留9位小数:%.9f%n", result); // 输出 3.333333333

// 2. 使用 String.format() 转换为字符串后再处理
System.out.println("\n使用 String.format() 保留小数:");
String str2 = String.format("%.2f", result);
String str5 = String.format("%.5f", result);
String str9 = String.format("%.9f", result);
System.out.println("保留2位小数:" + str2); // 输出 3.33
System.out.println("保留5位小数:" + str5); // 输出 3.33333
System.out.println("保留9位小数:" + str9); // 输出 3.333333333

// 处理用户输入的示例
Scanner scanner = new Scanner(System.in);
System.out.println("\n请输入两个整数(a b):");
int a = scanner.nextInt();
int b = scanner.nextInt();
double userResult = (double) a / b; // 注意先转换为double再计算
System.out.printf("结果保留6位小数:%.6f%n", userResult);

scanner.close();

局部变量的类型转换

重要部分:只有初始化变量时var才能用来声明变量,初始化变量就是赋一个值

1
2
var b;//这种写法是错的

数组,字符串,正则表达式

一维数组的定义

定义需要经过三个步骤:声明数组,数组分配内存空间,创建数组空间并且赋值

声明

数据类型[] 数组名
这里与c不同的是c是将 数组名和[] 调换了方向的。本人一开始学习的c,所以更倾向于c的写法

数组分配内存空间

分配内存给数组

1
2
数组名 = new 数据类型[个数]

数组创建后就不能修改其大小

一维数组的初始化

在定义数组的同时就为数组元素分配空间并赋值。

1
数据类型 [] 数组名 = {初值1,初值2,初值3,.....}

二维数组

二维数组初始化

1
2
3
4
5
6
7
8
9
10
11
// 方式1 分开来
int [][] a;
a = new [1][2];
// 第二种方式
int [][] a = new [1][2];

//正确的示范
int [][] a = new [1][];
int [][] a = new [1][2];
//错误示范
int [][] a = new [][2];

上述的错误示范其实很好理解
这是我问ai的出来的最通俗的理解
就是如果这个是一块地皮需要建楼
行代表我要建几层楼,至于每层楼建几米没有确定这个是能理解的
但是如果先说每层楼建几米,但是没说整栋楼要建基层是不对的这样就延申下来了以下的问题

杨辉三角形的问题里面

1
2
3
4
5
6
7
8
9
int i, j; //定义行列
int level = 7;
int[][] yh = new int[level][];
System.out.println("杨辉三角形");
for (i = 0; i< yh.length ; i++) {
yh[i] = new int[i+1];
}
yh[i] = new int[i+1]; //这段代码我就有点不理解

解释:
用搭积木的思维来理解
想象一下你要用积木搭建一个金字塔:

1
2
3
4
5
6
7
8
9
10
java
// 你说:我要建一个7层的金字塔
int level = 7;
int[][] pyramid = new int[level][]; // 先规划出7层的空间,但每层还是空的

// 现在开始为每一层放置合适长度的积木
for (int i = 0; i < pyramid.length; i++) {
pyramid[i] = new int[i + 1]; // 第0层放1块积木,第1层放2块,以此类推
}

这个过程就像:

你先申请了一块地,说这里要盖7层楼 (new int[7][])

然后你从第一层开始,每层搭建不同长度的墙体

第一层:搭建1米长的墙 (new int[1])

第二层:搭建2米长的墙 (new int[2])

第七层:搭建7米长的墙 (new int[7])

字符串

String型字符串

字符常量 使用单引号 ‘’ 括起来,并且里面必须且只能有一个字符。

1
2
3
4
5
6
7
char letter = 'A';      // 正确:一个大写字母A
char digit = '9'; // 正确:一个数字字符
char symbol = '#'; // 正确:一个符号
char ch = ' '; // 正确:一个空格字符
// char error = 'AB'; // 错误:单引号内只能有一个字符
// char error = ''; // 错误:单引号内不能为空
字符串常量 使用双引号 "" 括起来,里面的内容可以非常灵活。
1
2
3
4
String greeting = "Hello, World!"; // 正确:多个字符组成的序列
String single = "A"; // 正确:哪怕只有一个字符,也是字符串
String empty = ""; // 正确:空字符串,长度为0
String number = "123"; // 正确:这是字符串"123",不是数字123

类与对象

参数的传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package diyizhang;


class Cylinder {
double radius, pi;
int height;

void setCylinder(double radius, int h, double p) {
this.radius=radius;
pi = p;
radius = r;
height = h;
}

double area() {
return pi * radius * radius;
}

double volume() {
return area() * height;
}
}

public class App6_4 {
public static void main(String[] args) {
Cylinder volu = new Cylinder();
volu.setCylinder(2.5, 5, 3.14);
System.out.println("底半径:" + volu.radius);
System.out.println("圆柱体的高:" + volu.height);
System.out.println("圆周率:" + volu.pi);
System.out.println("圆柱体");
System.out.println("底面积:" + volu.area());
System.out.println("圆柱体体积:" + volu.volume());
}

}

在这里有个问题是如果我少传一个参数该怎么办

1
2
volu.setCylinder(2.5, 5, 3.14);

很显然,这个是不能的。但是我们可以使用方法重载

1
2
3
4
5
6
7
8
9
10
11
void setCylinder(double radius, int h, double p) {
this.radius=radius;
pi = p;
radius = r;
height = h;
}
void setCylinder(double radius, int h, ) { //重载后的
this.radius=radius;
radius = r;
height = h;
}

this的使用

1
2
3
4
5
6
7
8
9
10
11
class Cylinder {
double radius, pi;
int height;

void setCylinder(double radius, int h, double p) {
this.radius=radius;
pi = p;
radius = r;
height = h;
}

这里的radius成员变量和局部变量是相同的变量,如果我成员变量用作算其他的式子,局部变量算另一个式子。这样就会出现混乱
当特指成员变量时,要用this关键字

以数组作为参数进行传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class App6_5{
public static void main(String[] args){
int[] a={8,3,7,88,9,23};
LeastNumb minNumber=new LeastNumb();
minNumber.least(a);
}
}
class LeastNumb{
public void least(int[] array){
int temp=array[0];
for(int i=1;i<array.length;i++)
if(temp>array[i])
temp=array[i];
System.out.println("最小的数为:"+temp);
}
}

从例子中看出,如果想要将参数传递到方法中,只需要在方法名后面传入数组名就可以了。
如果传入的时

1
2
minNumber.least(a[1]);
这个就不满足条件了,这里需要的是一个数组类型的,然后提供的是int 类型的,即不满足对应条件

返回值为数组类型的方法

若方法需要返回一个数组,则必须在该方法名之前加上数组类型的修饰符,例如返回一维数组
int [] ,返回二维数组 int [][]

1
2
3
4
5
6
7
8
9
 int [] test(int [] a){

return a;
}

int [][] test2(int [][] b){
return b;
}

可变参数

参数的接收可以是固定的,或者不固定的。方法接收不固定的情况则为可变参数
方法接收可变参数的方法为

1
2
3
返回值类型 方法名 (固定参数列表,数据类型 ...可变参数名){
方法体
}

当有多个参数的情况下,可变参数只能位于最后一个
调用可变参数的时候,编译器为可变参数创建一个隐含的数组,通过调用数组的形式来访问可变参数

匿名对象

当一个对象被创建时可以不用那么麻烦的创建一个对象的引用变量,直接调用对象的方法

1
2
3
//这个是原始的写法
Cylinder cy = new Cylinder()
cy.setCylinder(11111)
1
2
//这个是匿名对象的写法
new Cylinder().setCylinder(111111)

使用场景:

  • 这个对象只需要一次调用的话就可以这样使用
  • 将匿名对象作为实参传递给一个方法调用。

java语言类的特性

类的私有成员与公共成员

无访问控制符

若在类成员的前面不加任何访问控制符,则该成员具有默认访问控制符
表示这个成员变量只能在相同的包内进行访问和调用

方法重载

方法的含义相同,但带有不同的参数,这些方法使用相同的名字,这就叫方法的重载

  • 注意:
    仅仅参数的变量名不同是不行的
    参数个数不同,参数的类型不同,参数的顺序不同
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Cylinder{
private double radius;
private int height;
private double pi=3.14;
private String color;
public double setCylinder(double r,int h){
radius=r;
height=h;
return r+h;
}
public void setCylinder(String str){
color=str;
}
public void show(){
System.out.println(color);
}
double area(){
return pi*radius*radius;
}
double volume(){
return area()*height;
}
}

构造方法详解

我理解你对构造方法这个概念感到困惑。让我通过你的代码示例来详细解释构造方法的作用和特点。

构造方法的基本概念

构造方法是一种特殊的方法,它在创建对象时自动调用,主要用于初始化对象的属性。

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Cylinder {
private double radius;
private int height;
private double pi = 3.14;

// 这是构造方法
public Cylinder(double r, int h) {
radius = r;
height = h;
}

double area() {
return pi * radius * radius;
}

double volume() {
return area() * height;
}
}

构造方法重载

在类中同时存在无参构造,有参构造这种就算构造方法重载

从一个构造方法调用另一个构造方法

在类中有多个方法,这些方法中可以相互调用,通过使用this()语句来调用的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
//FileName: App7_5.java
class Cylinder {
private double radius;
private int height;
private double pi = 3.14;
String color;

public Cylinder() {
this(2.5, 5, "蓝色");
System.out.println("无参构造方法被调用");
}

public Cylinder(double r, int h, String str) {
System.out.println("有参构造方法被调用");
radius = r;
height = h;
color = str;
}

public void show() {
System.out.println("圆柱体底半径为:" + radius);
System.out.println("圆柱体的高为:" + height);
System.out.println("圆柱体的颜色为:" + color);
}

double area() {
return pi * radius * radius;
}

double volume() {
return area() * height;
}
}

public class App7_5 {
public static void main(String[] args) {
Cylinder volu = new Cylinder();
System.out.println("圆柱体底面积=" + volu.area());
System.out.println("圆柱体体积=" + volu.volume());
volu.show();
}
}

staticfinal 的组合作用

staticfinal 虽然经常一起使用,但它们的作用是完全不同的。

两者的区别

关键字 作用 解决的问题
static 属于类而不是对象 内存分配问题
final 不可被修改 数值不变性问题

具体分析

只有 final(没有 static)的情况

1
public final double PI = 3.1415926535;

这种情况下:

  • 每个 MathUtils 对象都有自己的 PI 副本
  • 创建100个对象就有100个 PI 变量在内存中
  • 浪费内存空间

只有 static(没有 final)的情况

1
public static double PI = 3.1415926535;

这种情况下:

  • 只有一个 PI 变量在内存中(节省内存)
  • 但是这个值可以被修改:MathUtils.PI = 4.0; (危险!)

static + final 的完美组合

1
public static final double PI = 3.1415926535;

这种情况下:

  • ✅ 只有一个变量在内存中(节省内存)
  • ✅ 数值不可被修改(保证数学常量的正确性)
  • ✅ 通过类名直接访问:MathUtils.PI

实际内存对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test {
// 实例常量 - 每个对象都有一份
public final int INSTANCE_CONST = 100;

// 静态常量 - 只有一份
public static final int STATIC_CONST = 200;
}

// 使用
Test obj1 = new Test();
Test obj2 = new Test();
Test obj3 = new Test();

// 内存中有:
// - 3个 INSTANCE_CONST(每个对象一个)
// - 1个 STATIC_CONST(整个类共享)

为什么数学常量需要这样设计?

1. 数学真实性

π的值是固定不变的,不应该允许被修改

1
2
3
// 如果只有static,可能发生:
MathUtils.PI = 4.0; // 数学灾难!
double area = MathUtils.PI * radius * radius; // 错误的结果

2. 内存效率

π的值对所有对象都是一样的,不需要每个对象都存储一份

1
2
3
4
5
// 如果没有static:
MathUtils util1 = new MathUtils();
MathUtils util2 = new MathUtils();
MathUtils util3 = new MathUtils();
// 内存中有3个相同的PI值,浪费空间

3. 访问便利性

不需要创建对象就能使用

1
2
3
4
5
6
// 有static:直接使用
double area = MathUtils.PI * radius * radius;

// 没有static:需要先创建对象
MathUtils utils = new MathUtils();
double area = utils.PI * radius * radius; // 麻烦且浪费

其他常见的使用场景

颜色常量

1
2
3
4
5
6
public class Colors {
public static final String RED = "#FF0000";
public static final String GREEN = "#00FF00";
public static final String BLUE = "#0000FF";
}
// 使用:Colors.RED

错误代码

1
2
3
4
5
6
public class ErrorCodes {
public static final int SUCCESS = 0;
public static final int FILE_NOT_FOUND = 404;
public static final int PERMISSION_DENIED = 403;
}
// 使用:ErrorCodes.SUCCESS

配置参数

1
2
3
4
5
6
public class Config {
public static final int MAX_CONNECTIONS = 100;
public static final int TIMEOUT = 30000;
public static final String DATABASE_URL = "jdbc:mysql://localhost/db";
}
// 使用:Config.MAX_CONNECTIONS

总结

static final 组合的意义:

  • **static**:确保只有一个副本,节省内存,方便访问
  • **final**:确保数值不会被意外修改,保证数据完整性

对于真正的常量(数学常数、配置参数、状态码等),总是应该使用 public static final 的组合。

通俗来说,static就是整个类在内存中占领一个空间,static在类中就是一个公共的谁都可以使用的,只需要占一个内存就可以了

Java 自动装箱与拆箱机制

类型与包装类类型之间的自动转换

讲真的这个知识点之前一直在用,但是如此专业的名字我还真的不知道,之前用的时候也不知道为什么要这样用
于是我问了ai得出了下面的结果

概述

在官方文档中称为装箱(Boxing)拆箱(Unboxing),合称为自动装箱(Autoboxing)

这个特性是 JDK 5 引入的,极大地方便了我们的编码。

1. 核心概念

操作 转换方向 描述 示例
装箱 (Boxing) 基本类型包装类 将基本数据类型的值自动封装(转换)成其对应的包装类对象。 Integer i = 10;
拆箱 (Unboxing) 包装类基本类型 将包装类对象自动解封(转换)成其对应的基本数据类型值。 int num = i;

在没有自动装箱之前,这些操作都需要手动完成。

JDK 5 之前(手动):

1
2
Integer i = Integer.valueOf(10); // 手动装箱
int num = i.intValue(); // 手动拆箱

JDK 5 之后(自动):

1
2
Integer i = 10;  // 自动装箱:编译器背后执行的仍是 Integer.valueOf(10)
int num = i; // 自动拆箱:编译器背后执行的仍是 i.intValue()

2. 对应的类型关系表

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

3. 发生的常见场景

a) 赋值时(最常见)

这是最直接的自动装箱和拆箱。

1
2
3
// 赋值时的装箱和拆箱
Double d = 3.14; // 自动装箱:double -> Double
double pi = d; // 自动拆箱:Double -> double

b) 方法调用时(非常重要)

当传入方法的参数类型与实际值类型不匹配时,会自动发生。

1
2
3
4
5
6
7
8
public static void printInteger(Integer i) {
System.out.println(i);
}

public static void main(String[] args) {
int num = 5;
printInteger(num); // 自动装箱:int -> Integer,然后传入方法
}
1
2
3
4
5
6
7
8
9
public static int add(int a, int b) {
return a + b;
}

public static void main(String[] args) {
Integer a = 10;
Integer b = 20;
int sum = add(a, b); // 自动拆箱:Integer -> int,然后传入方法
}

c) 集合操作时(使用频率极高)

Java 的集合框架(如 ArrayList, HashMap)只能存储对象,不能存储基本类型。自动装箱让我们可以像直接存基本类型一样方便。

1
2
3
4
5
ArrayList<Integer> list = new ArrayList<>();
list.add(1); // 自动装箱:int -> Integer
list.add(2); // 自动装箱

int first = list.get(0); // 自动拆箱:Integer -> int

d) 运算和比较时

在数学运算中,包装类会自动拆箱为基本类型进行计算。在三元运算符、==>< 等混合运算中,也会发生自动拆箱和装箱。

1
2
3
4
5
Integer a = 10;
Integer b = 20;
int result = a + b; // 先自动拆箱 a->int, b->int,然后计算,最后将int结果赋值给result

Integer c = (a > b) ? a : b; // 比较时拆箱,赋值时装箱

4. 注意事项和陷阱(面试常考!)

自动装箱虽然方便,但也带来了一些容易忽略的问题。

陷阱一:== 比较的陷阱

== 在比较对象时,比较的是内存地址(引用是否指向同一个对象),而不是值。

情况1:值的范围在 [-128, 127] 之间

Java 对这部分常用的 Integer 对象进行了缓存,Integer.valueOf() 会返回缓存中的对象。

1
2
3
4
5
6
7
Integer a = 127;
Integer b = 127;
System.out.println(a == b); // true (因为指向缓存中的同一个对象)

Integer c = 128;
Integer d = 128;
System.out.println(c == d); // false (因为超出了缓存范围,是新new的对象)

情况2:任何范围,== 比较包装类和基本类型

这时包装类会自动拆箱,然后比较值。

1
2
3
Integer a = 128;
int b = 128;
System.out.println(a == b); // true (a被拆箱为int,然后进行值比较)

最佳实践:比较包装类的值,永远使用 .equals() 方法!

1
2
3
Integer a = 128;
Integer b = 128;
System.out.println(a.equals(b)); // true (比较的是值)

陷阱二:空指针异常(NullPointerException)

包装类可以是 null,而基本类型不能。如果在拆箱一个 null 的包装类,就会抛出异常。

1
2
Integer nullInteger = null;
int num = nullInteger; // 运行时抛出 NullPointerException

这种情况常发生在从方法返回 null 或从集合中取出 null 值时,需要格外小心。

陷阱三:性能开销

虽然微小,但装箱和拆箱确实会创建对象和调用方法,存在额外的性能开销。在大量的循环(如千万次、亿次)中,这种开销会累积变得显著。

  • 高性能场景: 应优先使用基本类型(如 int),避免无意识的自动装箱。
  • 一般业务场景: 为了方便,使用自动装箱完全没有问题。

Java 包装类与自动装箱的应用场景

概述

简单来说,使用包装类是为了弥补基本类型的局限性,让它们能在”面向对象”的世界里更好地工作。而自动装箱/拆箱是为了消除使用包装类时的繁琐代码,让我们写起来更方便。

核心应用场景

场景一:让基本类型能存入集合(最核心的原因)

这是最常用、最重要的原因。Java的集合框架(如 ArrayList, HashMap, HashSet)在设计上有一个基本原则:它们只能存储对象(Object),不能存储基本类型(int, double 等)。

为什么集合不能存基本类型?

因为集合的设计需要通用性,要能存放任何类型的数据。而”任何类型”在Java里最顶层的父类就是 Object。基本类型不是对象,不从 Object 继承,所以被排除在外。

没有包装类和自动装箱时(JDK5以前):

1
2
3
4
5
6
7
8
9
10
// 1. 想存一个int到ArrayList里,必须先手动"装箱"
ArrayList list = new ArrayList();
int age = 25;
Integer ageInteger = Integer.valueOf(age); // 手动打包成对象
list.add(ageInteger); // 才能存进去

// 2. 想取出来用,必须先手动"拆箱"
Integer result = (Integer) list.get(0); // 取出来是Object,要强转
int myAge = result.intValue(); // 再手动解包成基本类型才能计算
System.out.println(myAge + 1); // 输出26

太麻烦了! 每次存取的代码都变得很长。

有了包装类和自动装箱后:

1
2
3
4
5
// 现在我们可以这样写,直观又简洁
ArrayList<Integer> list = new ArrayList<>(); // 指定泛型为Integer
list.add(25); // 自动装箱:编译器帮你变成 list.add(Integer.valueOf(25));
int myAge = list.get(0); // 自动拆箱:编译器帮你变成 list.get(0).intValue();
System.out.println(myAge + 1); // 输出26

结论: 为了让基本数据类型也能方便地存入集合这种强大的容器中,我们必须使用包装类,而自动装箱/拆箱让这个过程毫不费力。


场景二:表示”缺失”或”未知”的值(空值)

基本类型都有默认值(如 int 默认是 0)。但在很多业务场景下,0 是一个有效的数值,而不是”未知”或”未设置”。

例子:统计学生年龄

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 使用基本类型 int - 有歧义
int studentAge = 0; // 他到底是0岁,还是年龄信息还没录?

// 使用包装类 Integer - 含义清晰
Integer studentAge = null; // 明确表示:年龄未知,尚未录入

// 后续处理逻辑可以这样写
if (studentAge == null) {
System.out.println("请录入该学生年龄");
} else if (studentAge > 18) {
System.out.println("成年人");
} else {
System.out.println("未成年人");
}

结论: 包装类可以用 null 来表示值的缺失,这在数据库查询、JSON解析等场景中极其常见(因为数据库中的字段很多就是可空的)。


场景三:使用泛型时

泛型(Generics)是JDK5另一个重大特性,它也和集合一样,类型参数必须是类类型,不能是基本类型

你不能这样写:

1
List<int> list = new ArrayList<>(); // 编译错误!

你只能这样写:

1
List<Integer> list = new ArrayList<>(); // 正确!

所以,只要你使用泛型(无论是集合还是自己定义的泛型类/方法),涉及到基本类型时,就必须使用其包装类。


场景四:使用方法时,需要对象而非值

有些API或方法设计就是要求传入一个对象,以便进行一些内部操作(比如同步锁、或需要修改传入的参数值)。

例如:使用同步锁

1
2
3
4
5
6
7
8
// 你可以用一个Integer对象作为锁(虽然不常见,但可行)
private final Integer lockId = 100;

public void someMethod() {
synchronized(lockId) { // synchronized需要传入一个对象
// 线程安全的代码
}
}

虽然基本类型 int 不能这么用,但它的包装类 Integer 可以。


总结:如何选择用哪个?

特性 基本类型 (e.g., int) 包装类 (e.g., Integer)
本质 纯数据值 对象
存储 在栈内存,效率极高 在堆内存,有开销
默认值 有(如 int0 null
用途 性能优先的场景 功能优先的场景

日常编码指南:

  • 定义类的成员属性:优先考虑包装类
  • 定义方法的局部变量:优先使用基本类型
  • 使用集合和泛型:必须使用包装类

JAVA语言垃圾回收机制

在整个java运行机制中,java运行环境提供了一套java回收机制。
其实核心就是计算调用的次数,每个对象都是有一个计数器的,当对象被调用一次的时候,该对象的计数器加一,反之则减一,当到最后的时候,该对象计数器
为0则可以判定为该对象没怎么被使用,则被回收避免内存的浪费。

1
2
3
4
String str1 = "This is a String"
String str2 = str1;
String str1 =null;
str2 = new String("This is a String");

当执行到第三行的时候对象仍然被str2使用,所以此时不能被回收,最后一句没有调用str1/str2使用了则可以回收了

类的继承

一个父类有可以有多个子类,一个子类只能有一个直接父类
在java语言中所有类都是直接或间接继承objec类得到的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package diyizhang;

class Person {
private String name;
private int age;

public Person() {
System.out.println("调用了个人类的构造方法Person()");
}

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

public void show() {
System.out.println("姓名" + name + "年龄" + age);
}
}

class Student extends Person {
private String department;

public Student() {
System.out.println("调用了学生类的构造方法Student()");
}

public void setDepartment(String dep) {
department = dep;
System.out.println("我是" + department + "的学生");
}

}

public class App8_1 {
public static void main(String[] args) {
Student stu = new Student();
stu.setNameAge("张小三", 21);
stu.show();
stu.setDepartment("计算机系");
}
}
输出结果:
调用了个人类的构造方法Person()
调用了学生类的构造方法Student()
姓名张小三年龄21
我是计算机系的学生

由此可见,本来我们创建的是Student的对象,构造方法来说,应该调用的是Student()构造方法,但是这里调用了父类的构造方法
java语言继承中,执行子类的构造方法之前,会先调用父类中没有参数的构造方法,其目的是要帮助继承自父类的成员做初始化操作

调用父类中特定的构造方法(super)

这里我们知道,在创建Student对象时,默认调用父类的无参构造,那么现在我需要调用有参构造呢?
super关键字是指向该super类的父类

调用父类有参构造,需要在子类的构造函数中进行,然后用super搞定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package diyizhang;

class Person {
private String name;
private int age;


public Person() {
System.out.println("调用了个人类的构造方法Person()");
}

public Person(String name, int age) {
System.out.println("调用了父类的有参构造");
this.name = name;
this.age = age;
}

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

public void show() {
System.out.println("姓名" + name + "年龄" + age);
}
}

class Student extends Person {
private String department;

public Student() {
System.out.println("调用了学生类的构造方法Student()");
}

public Student(String name, int age, String dep) {
super(name, age);
department = dep;
System.out.println("我是" + department + "的学生");
}

}

子类中访问父类的成员

在子类中不仅能访问父类的构造方法,还能访问父类非private的成员变量和成员方法,
但super不能访问在子类中添加的成员
super.变量名
super.方法名()

覆盖

与前面的重载不同的是,重载是指在一个类里面定义多个名称相同但参数个数或类型不同的方法
覆盖则是指在子类中定义名称,参数个数与类型均与父类中完全相同的方法

  • java中提供了一个注解@Override,该注解只用于方法,用来限定必须覆盖父类中的方法
    注意 :子类中不能覆盖父类中声明为final和static的方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 父类:动物
class Animal {
// 父类的方法
public void makeSound() {
System.out.println("动物发出声音");
}

public void move() {
System.out.println("动物在移动");
}
}

// 子类:狗(继承自动物)
class Dog extends Animal {
// 覆盖父类的makeSound方法
// 方法名、参数个数和类型与父类完全相同
@Override
public void makeSound() {
System.out.println("狗在汪汪叫");
}

// 这里没有覆盖move方法,将使用父类的实现
}

// 子类:猫(继承自动物)
class Cat extends Animal {
// 覆盖父类的makeSound方法
@Override
public void makeSound() {
System.out.println("猫在喵喵叫");
}

// 同时覆盖父类的move方法
@Override
public void move() {
System.out.println("猫轻轻悄悄地走");
}
}

public class OverrideExample {
public static void main(String[] args) {
Animal animal = new Animal();
Animal dog = new Dog(); // 多态:父类引用指向子类对象
Animal cat = new Cat(); // 多态:父类引用指向子类对象

animal.makeSound(); // 调用父类的方法:动物发出声音
dog.makeSound(); // 调用子类Dog覆盖后的方法:狗在汪汪叫
cat.makeSound(); // 调用子类Cat覆盖后的方法:猫在喵喵叫

animal.move(); // 调用父类的方法:动物在移动
dog.move(); // 调用父类的方法(未被覆盖):动物在移动
cat.move(); // 调用子类Cat覆盖后的方法:猫轻轻悄悄地走
}
}

用父类的对象访问子类的成员

如果希望通过父类类型的引用访问子类成员,需要满足两个条件:

该成员在父类中已定义(即子类覆盖了父类的方法,或继承了父类的属性)
通过多态(父类引用指向子类对象)的方式访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 父类
class Parent {
// 父类的属性
public String parentAttr = "父类属性";

// 父类的方法
public void parentMethod() {
System.out.println("父类方法");
}
}

// 子类
class Child extends Parent {
// 子类独有的属性(父类无法直接访问)
public String childAttr = "子类独有属性";

// 子类覆盖父类的方法
@Override
public void parentMethod() {
System.out.println("子类覆盖的父类方法");
}

// 子类独有的方法(父类无法直接访问)
public void childMethod() {
System.out.println("子类独有方法");
}
}

public class Main {
public static void main(String[] args) {
// 父类对象(只能访问父类自己的成员)
Parent parent = new Parent();
System.out.println(parent.parentAttr); // 正确:访问父类属性
parent.parentMethod(); // 正确:访问父类方法
// System.out.println(parent.childAttr); // 错误:父类对象不能访问子类独有的属性
// parent.childMethod(); // 错误:父类对象不能访问子类独有的方法

// 多态:父类引用指向子类对象
Parent poly = new Child();
System.out.println(poly.parentAttr); // 正确:访问父类继承的属性
poly.parentMethod(); // 正确:访问子类覆盖后的方法(多态特性)
// System.out.println(poly.childAttr); // 错误:父类引用仍不能直接访问子类独有属性
// poly.childMethod(); // 错误:父类引用仍不能直接访问子类独有方法

// 如果必须访问子类独有成员,需要强制类型转换(向下转型)
if (poly instanceof Child) {
Child child = (Child) poly; // 强制转换为子类类型
System.out.println(child.childAttr); // 正确:访问子类独有属性
child.childMethod(); // 正确:访问子类独有方法
}
}
}

final成员与final类

如果一个类或成员被声明为final,则该成员不会被覆盖,则为最终变量

Object

暂存

抽象类

1. 什么是抽象类?

抽象类是用 abstract 关键字修饰的类,它不能被实例化(即不能创建对象)。抽象类通常作为其他类的基类(父类),用于定义公共接口和部分实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
public abstract class Animal {
// 类内容
}
2. 为什么需要抽象类?
代码复用:将多个子类的共同代码提取到抽象类中

定义规范:强制子类实现特定的方法

实现多态:通过抽象类引用指向具体子类对象

3. 抽象类的特点
3.1 包含抽象方法
抽象类可以包含抽象方法(没有方法体的方法):

java
public abstract class Animal {
// 抽象方法(没有方法体)
public abstract void makeSound();

// 普通方法(有方法体)
public void sleep() {
System.out.println("动物正在睡觉");
}
}
3.2 不能实例化
java
// 错误:无法实例化抽象类
Animal animal = new Animal(); // 编译错误
3.3 可以包含成员变量
java
public abstract class Animal {
protected String name; // 成员变量
protected int age;

// 构造方法
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
}
3.4 可以包含构造方法
虽然抽象类不能实例化,但可以有构造方法(供子类调用):

java
public abstract class Animal {
private String name;

public Animal(String name) {
this.name = name;
}
}
4. 抽象类的使用
4.1 继承抽象类
子类必须实现父类的所有抽象方法,否则子类也必须声明为抽象类:

java
// 具体子类
public class Dog extends Animal {
public Dog(String name, int age) {
super(name, age);
}

// 实现抽象方法
@Override
public void makeSound() {
System.out.println("汪汪汪!");
}
}

// 使用
Animal myDog = new Dog("Buddy", 3);
myDog.makeSound(); // 输出:汪汪汪!
myDog.sleep(); // 输出:动物正在睡觉
4.2 部分实现抽象方法
如果子类没有实现所有抽象方法,必须声明为抽象类:

java
public abstract class Bird extends Animal {
// 只实现部分抽象方法
@Override
public void makeSound() {
System.out.println("叽叽喳喳");
}

// fly() 方法没有实现,所以Bird仍然是抽象类
public abstract void fly();
}
5. 抽象类 vs 接口
特性 抽象类 接口
方法实现 可以有具体方法和抽象方法 Java 8前只能有抽象方法,之后可以有默认方法
成员变量 可以有各种访问修饰符的变量 默认都是 public static final
构造方法 可以有 不能有
多重继承 单继承 多实现
设计理念 "是什么"(is-a关系) "能做什么"(has-a关系)
6. 实际应用示例
``` java
// 抽象类
public abstract class Employee {
private String name;
private int id;

public Employee(String name, int id) {
this.name = name;
this.id = id;
}

// 抽象方法
public abstract double calculateSalary();

// 具体方法
public String getDetails() {
return "ID: " + id + ", Name: " + name;
}
}

// 具体子类
public class FullTimeEmployee extends Employee {
private double monthlySalary;

public FullTimeEmployee(String name, int id, double monthlySalary) {
super(name, id);
this.monthlySalary = monthlySalary;
}

@Override
public double calculateSalary() {
return monthlySalary;
}
}

// 使用
Employee emp = new FullTimeEmployee(“张三”, 1001, 8000);
System.out.println(emp.getDetails());
System.out.println(“月薪: “ + emp.calculateSalary());
7. 总结
抽象类用 abstract 关键字修饰

不能被实例化,只能被继承

可以包含抽象方法和具体方法

子类必须实现所有抽象方法,否则也必须声明为抽象类

适合用于有共同特征和行为的类的模板设计

抽象类是Java面向对象编程中实现代码重用和多态性的重要工具,常用于框架设计和大型项目的架构中。

接口

  1. 接口是什么?
    定义:接口是一个完全抽象的类型。它是一组行为规范的集合,只声明“应该做什么”,而不关心“如何做”。

核心思想:定义标准,实现解耦。它主要体现的是一种 “像…一样” (can-do) 的关系,而不是“是” (is-a) 的关系。

关键字:interface

  1. 接口的特点 (Java 7及以前)
    在 Java 8 之前,接口是一个非常纯粹的“契约”。

成员变量 (字段):

默认都是 public static final 的常量。

必须显式初始化。

通常用于定义一些全局常量。

1
2
3
4
interface USB {
// 等同于 public static final double VERSION = 3.0;
double VERSION = 3.0;
}

方法:

所有普通方法默认都是 public abstract 的抽象方法(没有方法体)。

没有构造方法,不能被实例化。

1
2
3
4
5
interface Animal {
// 等同于 public abstract void eat();
void eat();
void sleep();
}
  1. 接口的实现
    关键字:implements

规则:

一个类使用 implements 关键字来实现一个或多个接口。

实现类必须重写(Override) 接口中所有的抽象方法,并提供具体的实现。

如果实现类没有重写所有抽象方法,那么它自己必须被声明为 abstract 抽象类。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 定义一个接口
interface Swimmable {
void swim();
}

// 类实现接口
class Duck implements Swimmable {
// 必须重写接口中的抽象方法
@Override
public void swim() {
System.out.println("鸭子在水上游泳");
}
}
  1. 接口的新特性 (Java 8+)
    Java 8 对接口进行了重大增强,引入了默认方法和静态方法。

4.1 默认方法 (Default Methods)
关键字:default

目的:允许在接口中添加新方法,而不会破坏已有的实现类。

特点:

有方法体,提供了默认实现。

实现类可以直接继承使用这个默认实现,也可以选择重写它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface Vehicle {
// 抽象方法
void start();

// 默认方法
default void honk() {
System.out.println("嘀嘀嘀!");
}
}

class Car implements Vehicle {
@Override
public void start() {
System.out.println("汽车启动");
}
// 没有重写 honk(),将使用接口中的默认实现
}

// 使用
Car myCar = new Car();
myCar.start(); // 输出: 汽车启动
myCar.honk(); // 输出: 嘀嘀嘀!

4.2 静态方法 (Static Methods)
关键字:static

目的:将工具方法与接口相关联,避免为工具方法创建额外的类。

特点:

有方法体。

属于接口本身,只能通过接口名直接调用,不能被实现类的对象调用。

1
2
3
4
5
6
7
8
9
10
11
12
interface MathOperation {
static int add(int a, int b) {
return a + b;
}
}

// 使用
int sum = MathOperation.add(5, 3); // 正确:通过接口名调用
System.out.println(sum); // 输出: 8

// MathOperation op = new ...();
// op.add(5, 3); // 错误!不能通过实现类对象调用静态方法

4.3 私有方法 (Private Methods) (Java 9+)
关键字:private

目的:作为默认方法或静态方法的辅助方法,用于抽取公共代码,减少冗余,但又不暴露给外部。

分类:

private:仅供接口内的默认方法使用。

private static:供接口内的默认方法和静态方法使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Logger {
default void logInfo(String message) {
log("INFO", message);
}

default void logError(String message) {
log("ERROR", message);
}

// 私有方法,减少重复代码
private void log(String level, String message) {
System.out.println("[" + level + "] " + message);
}
}
  1. 接口的继承
    接口可以使用 extends 关键字继承其他接口。

接口支持多重继承(一个接口可以继承多个父接口),这是与类(单继承)最大的不同之一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface Animal {
void eat();
}

interface Winged {
void fly();
}

// Bat 接口同时继承了 Animal 和 Winged 接口
interface Bat extends Animal, Winged {
void useEcholocation();
}

// 实现类必须实现所有接口链中的抽象方法
class FruitBat implements Bat {
@Override
public void eat() { /* ... */ }
@Override
public void fly() { /* ... */ }
@Override
public void useEcholocation() { /* ... */ }
}
  1. 接口 vs. 抽象类

特性 接口 (Interface) 抽象类 (Abstract Class)
方法 Java 8前:全是抽象方法
Java 8+:抽象、默认、静态、私有 抽象方法、具体方法都有
变量 只能是 public static final 常量 各类成员变量都可以
构造方法 没有 有(虽然不能直接实例化)
继承方式 多继承(一个类可实现多个接口) 单继承(一个类只能继承一个父类)
设计理念 “has-a” / “can-do”
定义行为契约、标准 “is-a”
表示的是从属关系,代码复用
默认方法 有(Java 8+) 有(一直都是)
访问修饰符 方法默认 public 方法可以是任意访问权限
如何选择?
如果你主要关心对象的 capabilities (能力),而不是其身份 (identity),使用接口。(例如:Comparable, Serializable)
如果你需要定义一种模板,包含一些公共代码,并要求子类是一种更具体的类型,使用抽象类。

下面是接口的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// 1. 定义一个「可充电」接口
interface Chargeable {
// 接口常量 (默认 public static final)
String STANDARD = "USB-C";

// 抽象方法 (默认 public abstract)
void charge();

// 默认方法 (Java 8+)
default void showChargingStandard() {
System.out.println("充电标准: " + STANDARD);
}

// 静态方法 (Java 8+)
static void printWelcome() {
System.out.println("欢迎使用充电系统");
}
}

// 2. 手机类实现接口
class Phone implements Chargeable {
private String brand;//品牌
private int batteryLevel;//手机电量

public Phone(String brand) {
this.brand = brand;
this.batteryLevel = 20; // 初始电量20%
}

// 必须实现抽象方法
@Override
public void charge() {
System.out.println(brand + "手机正在充电中...");
batteryLevel += 50;
if (batteryLevel > 100) batteryLevel = 100;
System.out.println("当前电量: " + batteryLevel + "%");
}

// 可以选择重写默认方法
@Override
public void showChargingStandard() {
System.out.println(brand + "手机使用" + STANDARD + "接口充电");
}
}

// 3. 笔记本电脑类实现接口
class Laptop implements Chargeable {
private String model;

public Laptop(String model) {
this.model = model;
}

@Override
public void charge() {
System.out.println(model + "笔记本电脑正在快速充电");
}
// 没有重写showChargingStandard(),将使用接口的默认实现
}

// 4. 主类进行调用
public class InterfaceExample {
public static void main(String[] args) {
// 调用接口的静态方法
Chargeable.printWelcome();
System.out.println("------------------------");

// 创建对象
Phone myPhone = new Phone("华为");
Laptop myLaptop = new Laptop("ThinkPad");

// 使用接口类型引用对象(多态的体现)
Chargeable[] devices = {myPhone, myLaptop};

// 遍历所有设备进行充电
for (Chargeable device : devices) {
System.out.println("--- 开始处理设备 ---");
device.showChargingStandard(); // 调用默认方法
device.charge(); // 调用实现的抽象方法
System.out.println();
}

// 访问接口常量
System.out.println("所有设备都使用" + Chargeable.STANDARD + "标准");
}
}

在这个例子中我发现了一个我不知道的东西

1
2
// 使用接口类型引用对象(多态的体现)
Chargeable[] devices = {myPhone, myLaptop};

深入理解:Chargeable[] devices = {myPhone, myLaptop};

  1. 提高代码的灵活性和可扩展性
    1
    2
    3
    4
    5
    6
    7
    // 假设我们新增一个平板类
    class Tablet implements Chargeable {
    @Override
    public void charge() {
    System.out.println("平板电脑充电中");
    }
    }
    // 原有的代码完全不需要修改!
    Tablet myTablet = new Tablet();
    Chargeable[] devices = {myPhone, myLaptop, myTablet}; // 直接添加新设备
  2. 统一处理不同类型的对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 如果没有接口,你需要这样写:
    Phone[] phones = {...};
    Laptop[] laptops = {...};
    Tablet[] tablets = {...};

    // 有了接口,只需要一个循环处理所有设备
    for (Chargeable device : devices) {
    device.charge(); // 统一调用充电方法
    }
  3. 降低代码耦合度
    1
    2
    3
    4
    5
    6
    // 方法参数使用接口类型,可以接受任何实现类
    public void chargeAllDevices(Chargeable[] devices) {
    for (Chargeable device : devices) {
    device.charge();
    }
    }
    // 可以传入任何实现了Chargeable接口的对象数组
    chargeAllDevices(devices);
    底层原理:多态(Polymorphism)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    interface Animal {
    void makeSound();
    }

    class Dog implements Animal {
    @Override
    public void makeSound() {
    System.out.println("汪汪!");
    }

    public void fetch() { // Dog特有的方法
    System.out.println("捡球");
    }
    }

    class Cat implements Animal {
    @Override
    public void makeSound() {
    System.out.println("喵喵!");
    }
    }

    public class PolymorphismExample {
    public static void main(String[] args) {
    // 多态的体现:父类/接口引用指向子类对象
    Animal myAnimal1 = new Dog(); // ✅ 正确
    Animal myAnimal2 = new Cat(); // ✅ 正确

    Animal[] animals = {new Dog(), new Cat()};

    for (Animal animal : animals) {
    animal.makeSound(); // 运行时根据实际对象类型调用相应方法
    }

    // ❌ 编译错误:Animal接口没有fetch()方法
    // myAnimal1.fetch();

    // ✅ 需要先向下转型
    if (myAnimal1 instanceof Dog) {
    Dog myDog = (Dog) myAnimal1;
    myDog.fetch();
    }
    }
    }
    实际应用场景
    场景1:集合框架(最经典的例子)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // List是接口,ArrayList和LinkedList是实现类
    List<String> list1 = new ArrayList<>();
    List<String> list2 = new LinkedList<>();

    // 可以统一处理
    List<String>[] lists = {list1, list2};
    for (List<String> list : lists) {
    list.add("Hello");
    System.out.println(list.size());
    }
    场景2:策略模式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    interface PaymentStrategy {
    void pay(double amount);
    }

    class CreditCardPayment implements PaymentStrategy {
    @Override
    public void pay(double amount) {
    System.out.println("使用信用卡支付: $" + amount);
    }
    }

    class PayPalPayment implements PaymentStrategy {
    @Override
    public void pay(double amount) {
    System.out.println("使用PayPal支付: $" + amount);
    }
    }

    class ShoppingCart {
    public void checkout(PaymentStrategy payment, double amount) {
    payment.pay(amount); // 不关心具体实现,只关心行为
    }
    }

    // 使用
    PaymentStrategy[] payments = {
    new CreditCardPayment(),
    new PayPalPayment()
    };

    ShoppingCart cart = new ShoppingCart();
    for (PaymentStrategy payment : payments) {
    cart.checkout(payment, 100.0);
    }
    重要规则总结
    向上转型(Upcasting):接口/父类 引用 = new 实现类/子类() ✅ 总是安全的

向下转型(Downcasting):需要显式转换,且必须用 instanceof 检查 ✅ 谨慎使用

方法调用:编译时看左边(引用类型),运行时看右边(实际对象类型)

字段访问:没有多态,编译和运行时都看左边(引用类型)

接口回调详解:从生活例子到代码实现

什么是接口回调?

这个知识点在之前的学习中用过,但是没有了解过这个叫什么名字重活一次,我一定要搞懂他(重生之我是java高手)
下面是ai给我的讲解

接口回调是一种编程模式,它允许一个类在完成特定任务后通知另一个类。这种模式实现了调用方和被调用方之间的解耦,是事件驱动编程和异步处理的基础。

生活化例子:送衣服干洗

场景分析

想象一下送衣服干洗的完整流程:

  • 你(调用方):需要干洗服务
  • 干洗店(被调用方):提供服务
  • 取衣通知(回调方法):完成后的通知机制

两种处理方式对比

方式一:轮询(低效)

1
2
3
4
5
你:打电话问"衣服好了吗?"
店员:还没
你:(一小时后)"衣服好了吗?"
店员:还没
...(重复直到完成)

缺点:效率低下,浪费资源

方式二:回调(高效)

1
2
3
你:留下电话号码,"洗好后通知我"
干洗店:完成工作后主动打电话通知你
你:在此期间可以做其他事情

优点:高效、解耦、异步处理

代码实现

1. 定义回调接口(协议)

1
2
3
4
5
6
7
8
9
10
/**
* 干洗回调接口 - 定义"通知协议"
*/
public interface DryCleanCallback {
/**
* 衣服准备好时的回调方法
* @param message 通知消息
*/
void onClothesReady(String message);
}

2. 调用方实现回调接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* 客户类 - 调用方,需要被通知
*/
public class Customer implements DryCleanCallback {

private String name;

public Customer(String name) {
this.name = name;
}

/**
* 实现回调方法:定义收到通知后的具体行为
*/
@Override
public void onClothesReady(String message) {
System.out.println("【客户通知】" + name + " 收到:" + message);
System.out.println(">>> " + name + "的后续动作:准备去取衣服");
// 这里可以添加更复杂的业务逻辑
}

/**
* 送衣服干洗的业务方法
*/
public void sendToDryClean() {
DryCleanShop shop = new DryCleanShop();
System.out.println("🚗 " + name + " 送衣服到干洗店");

// 关键步骤:传递this(即回调接口的实现)
shop.washClothes("冬季大衣", this);

System.out.println("💻 " + name + " 继续做其他事情...");
}
}

3. 被调用方使用回调接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/**
* 干洗店类 - 被调用方,服务提供者
*/
public class DryCleanShop {

/**
* 核心服务方法
* @param clothes 要洗的衣服
* @param callback 回调接口引用(客户的"电话号码")
*/
public void washClothes(String clothes, DryCleanCallback callback) {
System.out.println("\n🏪 干洗店开始工作 =====");
System.out.println("接收衣物:" + clothes);

// 模拟耗时操作
simulateWashingProcess(clothes);

System.out.println("🏪 干洗店工作完成 =====");

// 关键步骤:完成后回调客户
String completionMessage = "您的【" + clothes + "】已清洗消毒完毕,欢迎来取!";
callback.onClothesReady(completionMessage);
}

/**
* 模拟清洗过程
*/
private void simulateWashingProcess(String clothes) {
try {
System.out.println("⏳ 正在清洗 " + clothes + "...");
Thread.sleep(3000); // 模拟3秒清洗时间
System.out.println("✅ " + clothes + " 清洗完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

4. 运行演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 演示类 - 展示完整的回调流程
*/
public class CallbackDemo {
public static void main(String[] args) {
System.out.println("========== 接口回调演示开始 ==========\n");

// 创建客户
Customer customer = new Customer("张三");

// 发送衣服干洗(注册回调)
customer.sendToDryClean();

// 主线程继续执行,演示异步效果
System.out.println("\n📱 主程序继续运行...");
System.out.println("========== 演示结束 ==========");
}
}

运行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
========== 接口回调演示开始 ==========

🚗 张三 送衣服到干洗店

🏪 干洗店开始工作 =====
接收衣物:冬季大衣
⏳ 正在清洗 冬季大衣...
💻 张三 继续做其他事情...

📱 主程序继续运行...
========== 演示结束 ==========
✅ 冬季大衣 清洗完成
🏪 干洗店工作完成 =====
【客户通知】张三 收到:您的【冬季大衣】已清洗消毒完毕,欢迎来取!
>>> 张三的后续动作:准备去取衣服

核心概念解析

1. 解耦 (Decoupling)

  • 干洗店不需要知道具体的客户是谁
  • 客户不需要知道干洗店的具体工作流程
  • 双方只通过接口进行通信

2. 控制反转 (Inversion of Control)

  • 传统:调用方控制整个流程
  • 回调:被调用方在适当时机”回调”调用方
  • 遵循”好莱坞原则”:不要打电话给我们,我们会打电话给你

3. 异步处理 (Asynchronous Processing)

  • 调用方不必阻塞等待
  • 被调用方完成工作后自动通知
  • 提高资源利用率和响应性

实际应用场景

Android开发中的点击事件

1
2
3
4
5
6
7
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 这是回调方法
handleButtonClick();
}
});

JavaScript中的事件处理

1
2
3
4
document.getElementById('myButton').addEventListener('click', function() {
// 回调函数
alert('按钮被点击了!');
});

网络请求回调

1
2
3
4
5
6
7
8
9
10
11
12
13
httpClient.get(url, new HttpResponseCallback() {
@Override
public void onSuccess(String response) {
// 请求成功时的回调
processResponse(response);
}

@Override
public void onError(Exception e) {
// 请求失败时的回调
handleError(e);
}
});

总结

接口回调的三要素

  1. 回调接口:定义通信协议
  2. 调用方:实现接口,提供回调方法的具体逻辑
  3. 被调用方:持有接口引用,在适当时机调用回调方法

核心价值

  • 灵活性:易于扩展和修改
  • 可维护性:代码结构清晰,职责分离
  • 复用性:回调接口可以被多个类实现
  • 异步支持:适合处理耗时操作

记忆技巧

“留个电话,完事儿叫我”

  • 留电话 = 注册回调接口
  • 完事儿 = 被调用方完成工作
  • 叫我 = 执行回调方法

接口回调是现代编程中非常重要的概念,掌握它对于理解事件驱动编程、异步处理和各种设计模式都有很大帮助。

接口的继承

接口也具备继承性,接口继承与类的继承不同的是,接口可以有多个父类,中间用逗号隔开,新接口将继承父接口中的所有常量,抽象方法和默认方法,但不能继承父接口中的静态方法和私有方法
也不能被实现类所继承如果类实现的接口继承自另外一个接口,那么该类必须实现在接口继承链中定义的所有抽象方法

枚举

java语言中引入了包的概念来管理类名空间。就像文件夹把各种文件进行管理一样。一种区别类名空间的机制

异常处理

异常是指在程序运行中由代码产生的一种错误。

异常处理机制

  1. 抛出异常
    程序的运行中,发生了异常事件,则产生代表该异常的一个异常对象,交给运行系统,由运行系统去找寻对应的代码来处理相关异常
  2. 捕获异常
    异常抛出后,运行系统从生成的异常对象代码开始,沿着方法的调用栈逐层回溯查找,直到找到包含相应处理异常的方法,并把异常对象提交给该方法为止

异常处理类

异常类的最顶层有一个单独的类为:Throwable,该类派生出了Error和Exception两个子类其中Error由系统保留,我们一般使用Excption

Exception异常分类

运行时异常:RuntimeException及其子类,编译阶段不会出现错误提醒,运行时出现的异常(如:数组索引越界异常)
编译时异常:编译阶段就会出现错误提醒的。(如:日期解析异常)
运行时异常和编译时异常其实很好区分,当你在编码阶段没有报错,而点击运行代码后抛出了错误后这个其实就是运行错误
而编译错误就是在编码阶段就会提醒你的错误,所以编码错误也称检查错误

异常的处理

异常处理是通过try,catch,finally,thow,thows五个关键字实现的

  1. try-catch-finally语法格式:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    try {
    // 可能会抛出异常的代码
    // 业务逻辑代码
    } catch (ExceptionType1 e1) {
    // 处理 ExceptionType1 类型的异常
    System.out.println("捕获到异常: " + e1.getMessage());
    } catch (ExceptionType2 e2) {
    // 处理 ExceptionType2 类型的异常
    System.out.println("捕获到异常: " + e2.getMessage());
    } finally {
    // 无论是否发生异常都会执行的代码
    // 通常用于资源清理,如关闭文件、数据库连接等
    }
  2. 多异常处理机制:
  • 多异常处理机制:一个try块后可跟多个catch块,分别处理不同类型异常;catch块能捕获父类异常时,也能捕获其所有子类异常。

  • 异常匹配流程try抛异常后,程序先到第一个catch块匹配,匹配即进入该块执行;不匹配则依次往后找,直到找到能接收的catch块。若都不匹配,回退到上层方法找,最终无匹配则由Java运行系统处理(通常终止程序并输出信息)。

  • 无异常情况try内语句无异常时,所有catch块都不执行。

1
2
3
4
5
6
7
8
9
10
try {
test1();
} catch (FileNotFoundException e) {
e.printStackTrace();
System.out.println("文件没有找到");
} catch (ParseException e) {
e.printStackTrace();
System.out.println("时间格式有问题");
}

Java异常中 throw 和 throws 的区别

🎯 核心概念一句话总结

  • **throw**:制造问题(主动扔出一个炸弹)
  • **throws**:提前警告(挂个牌子说”这里有炸弹危险”)

🍎 生活化比喻

场景:你点了一份外卖

throw 就像厨师发现菜有问题时…

1
2
3
4
5
6
7
public void 做菜() {
if(发现食材变质了) {
// 厨师主动抛出问题
throw new 食材变质异常("鸡肉发臭了!");
}
// 正常做菜...
}

厨师说:”这菜我做不了!问题就在这!”

throws 就像菜单上的警告…

1
2
3
public void 做辣子鸡() throws 可能太辣异常, 可能上火异常 {
// 做菜过程...
}

菜单说:”这道菜可能会很辣,吃了可能上火,请您知悉!”


💻 代码示例(一步步来)

第一步:只有 throw(制造问题)

1
2
3
4
5
6
7
8
9
10
11
12
13
public class 测试 {
public static void 检查年龄(int age) {
if(age < 0) {
// 主动制造一个异常
throw new IllegalArgumentException("年龄不能为负数!");
}
System.out.println("年龄合法: " + age);
}

public static void main(String[] args) {
检查年龄(-5); // 这里会崩溃!程序直接报错停止
}
}

运行结果:程序崩溃,控制台显示 IllegalArgumentException: 年龄不能为负数!

第二步:加上 throws(提前警告)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class 测试 {
// 声明这个方法可能会抛出异常
public static void 检查年龄(int age) throws IllegalArgumentException {
if(age < 0) {
throw new IllegalArgumentException("年龄不能为负数!");
}
System.out.println("年龄合法: " + age);
}

public static void main(String[] args) {
// 现在调用者知道可能有异常,需要处理
try {
检查年龄(-5);
} catch (IllegalArgumentException e) {
System.out.println("捕获到异常: " + e.getMessage());
}
}
}

运行结果捕获到异常: 年龄不能为负数!(程序不会崩溃)


📋 最简对比表

特性 throw throws
是什么 动作 - 扔出异常 声明 - 警告可能有问题
在哪里 方法内部 方法开头(签名里)
后面跟什么 new Exception()
(异常对象)
Exception.class
(异常类型)
例子 throw new 炸弹(); throws 可能爆炸警告;

🎯 实际应用场景

场景1:用户注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class 用户服务 {
// 声明:这个方法可能会抛出这些异常
public void 注册用户(String 用户名, String 密码)
throws 用户名已存在异常, 密码太简单异常 {

if(用户已存在(用户名)) {
// 制造:用户名已存在的异常
throw new 用户名已存在异常(用户名 + " 已被注册");
}

if(密码.length() < 6) {
// 制造:密码太简单的异常
throw new 密码太简单异常("密码至少6位");
}

// 正常注册逻辑...
}
}

场景2:调用这个方法时

1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args) {
用户服务 service = new 用户服务();

try {
service.注册用户("张三", "123");
System.out.println("注册成功!");
} catch (用户名已存在异常 e) {
System.out.println("注册失败: " + e.getMessage());
} catch (密码太简单异常 e) {
System.out.println("注册失败: " + e.getMessage());
}
}

❓ 常见疑问解答

问:为什么要用 throws?直接 throw 不行吗?

答:throws 是给调用者的温馨提示

  • 没有 throws:调用者不知道可能出什么错,突然就崩溃了
  • throws:调用者知道可能出哪些错,可以提前准备处理方案

问:什么时候必须用 throws

答:当你抛出检查型异常时(比如 IOException, SQLException),Java强制要求你必须声明 throws。这是Java的安全机制。


💡 总结

可以把 throw 想象成 扔手榴弹throws 想象成 贴警告标志

  • throw:负责制造危险
  • throws:负责提前告知危险

java语言的输入输出与文件处理

文件的创建方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import java.io.File;
import java.io.IOException;

public class creatFile {
public void fileCreat1() {
String filePath = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\fileTest1.txt";
File file = new File(filePath);
try {
file.createNewFile();
System.out.println("文件创建成功");
} catch (IOException e) {
e.printStackTrace();
}
}

//方式2 new File(File parent,String child)//根据父目录文件+子路径构建//e:llnews2.txt
public void create02() {
File parentFile = new File("D:\\JAVA\\newJava\\new1\\src\\IO\\File");
String fileName = "news2.txt";
File file = new File(parentFile, fileName);
try {
file.createNewFile();
System.out.println("文件创建成功");
} catch (IOException e) {
throw new RuntimeException(e);
}

}

////方式3 new File(string parent,string child)//根据父目录+子路径构建
public void create03() {
String parentFile = "D:\\JAVA\\newJava\\new1\\src\\IO\\File";
String child = "news3.txt";
File file = new File(parentFile, child);
try {
file.createNewFile();
System.out.println("文件创建成功");
} catch (IOException e) {
throw new RuntimeException(e);
}
}

public static void main(String[] args) {
creatFile creatFile = new creatFile();
creatFile.fileCreat1();
creatFile.create02();
creatFile.create03();
}

}

获取文件信息

  • 获取文件的信息
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    import java.io.File;

    public class FileInformation {
    public static void main(String[] args) {
    info();
    }

    public static void info() {
    File file = new File("D:\\JAVA\\newJava\\new1\\src\\IO\\File\\fileTest1.txt");
    System.out.println("文件的名字是:" + file.getName());
    /*
    getAbsolutePath() - 获取文件的绝对路径
    getParent() - 获取父目录路径
    length() - 获取文件大小(字节数)
    exists() - 判断文件/目录是否存在
    isFile() - 判断是否为文件
    isDirectory() - 判断是否为目录*/
    System.out.println(file.getAbsolutePath());
    System.out.println(file.getParent());
    System.out.println(file.length());
    System.out.println(file.exists());
    System.out.println(file.isFile());
    System.out.println(file.isDirectory());
    }
    }

目录的操作

在java编程中,目录也被当做文件
创建一级目录使用mkdir(),创建多级目录使用mkdirs()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import java.io.File;

public class Directory_s {
public static void main(String[] args) {
Directory_s directoryS = new Directory_s();
directoryS.m1();
directoryS.m2();
}

public void m1() {
String filePath = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\news2.txt";
File file = new File(filePath);
if (file.exists()) {
System.out.println("文件存在");
if (file.delete()) {
System.out.println("文件删除成功");
} else {
System.out.println("文件删除失败");
}
} else {
System.out.println("该文件不存在");
}
}

//这里我们需要体会到,在java编程中,目录也被当做文件(使用的时候就当文件那样删除和新增就行了)
public void m2() {
String directoryPath = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\Demo02";
File file = new File(directoryPath);
if (file.exists()) {
System.out.println("文件存在");

} else {
System.out.println("文件不存在");
if (file.mkdir()) {
System.out.println("文件创建成功");
} else {
System.out.println("文件创建失败");
}

}
}

}

I0流原理及流的分类

I0流原理

  1. I/O是Input/Output的缩写,I/0技术是非常实用的技术,用于处理数据传输。
    如读/写文件,网络通讯等。
  2. Java程序中,对于数据的输入/输出操作以”流(stream)”的方式进行。
  3. java.io包下提供了各种“流”类和接口,用以获取不同种类的数据,并通过方法输入或输出数据

流的分类

流的分类
按操作数据单位不同分为:字节流(8 bit),字符流(按字符) 效率高一些
按数据流的流向不同分为:输入流,输出流
按流的角色的不同分为:节点流,处理流/包装流

IO流体系图-常用的类

InputStream:字节输入流
InputStream 抽象类是所有字节输入流的超类。

InputStream 常用的子类:

FileInputStream:文件输入流

BufferedInputStream:缓冲字节输入流

ObjectInputStream:对象字节输入流

FileInputStream


这里讲了两种写法,第一种是循环读取一个一个的读取字节数并且转成char类型
这里我觉得IO流这边很多异常处理的地方,所以我统一向上抛了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.io.FileInputStream;
import java.io.IOException;

//单个读取效率不高
//字节输入流,文件到程序
public class FileInputstream extends Exception {
public static void main(String[] args) throws IOException {
FileInputstream fileInputstream = new FileInputstream();
fileInputstream.fileRead01();
}

//这里注意是大写Stream
public void fileRead01() throws IOException {
String filePath = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\InputStream_\\test.txt";
int read = 0;

//这里注意是大写Stream
FileInputStream fileInputstream = new FileInputStream(filePath);
//从该输入流读取一个字节的数据。如果没有输入可用,此方法将阻止。
//如何返回-1,表示读取完毕

while ((read = fileInputstream.read()) != -1) {
System.out.println((char) read);//读取的时候是int 类型的,展示的时候要转为char类型的
}
fileInputstream.close();//当文件完成读取以后需要将其关闭
}
}

这里是用数组的形式读取,其实就是8个8个的读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.io.FileInputStream;
import java.io.IOException;

//优化版本的用数组的形式读取文件
//字节输入流,文件到程序
public class FileInputstream_1 extends Exception {
public static void main(String[] args) throws IOException {
FileInputstream_1 fileInputstream = new FileInputstream_1();
fileInputstream.fileRead01();
}

//这里注意是大写Stream
public void fileRead01() throws IOException {
String filePath = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\InputStream_\\test.txt";
// int read = 0;
byte[] buf = new byte[8];
int readLenth = 0;//读取的字节长度
//这里注意是大写Stream
FileInputStream fileInputstream = new FileInputStream(filePath);
//从该输入流读取一个字节的数据。如果没有输入可用,此方法将阻止。
//如何返回-1,表示读取完毕
//如果读取正常,返回实际读取的字节数
while ((readLenth = fileInputstream.read(buf)) != -1) {//改这里
System.out.println(new String(buf, 0, readLenth));//读取的时候是int 类型的,展示的时候要转为char类型的
System.out.println(readLenth);
}
fileInputstream.close();//当文件完成读取以后需要将其关闭
}
}

FileOutputStream


其实跟输入流大差不差的,就是将数据写入到文件中

  1. 同样的是先测试一个字节的写入,同时注意,这里是直接覆盖了文件原有的内容
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import java.io.FileOutputStream;
    import java.io.IOException;

    public class FileOutputStream01 {
    public static void main(String[] args) throws IOException {
    writeFile();
    }

    public static void writeFile() throws IOException {
    String FilePath = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\InputStream_\\test.txt";
    FileOutputStream fileOutputStream = new FileOutputStream(FilePath);
    //写一个字节,会覆盖之前文件中的内容
    fileOutputStream.write('a');
    fileOutputStream.close();//关闭
    }
    }

  2. 写入多个内容的时候,这里用字符串输入,然后用字符串自带的转byte的功能转换就行了(同样也是覆盖原有的内容)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.util.Scanner;

    public class FileOutputStream02 {
    public static void main(String[] args) throws IOException {
    writeFile();
    }

    public static void writeFile() throws IOException {
    String FilePath = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\InputStream_\\test.txt";
    FileOutputStream fileOutputStream = new FileOutputStream(FilePath);
    //写多个字节,String 自带转为bytes的功能,我们就懒得转换了
    String str = "hello word";
    fileOutputStream.write(str.getBytes());
    fileOutputStream.close();//关闭
    }
    }

  3. 要想不覆盖原有的内容也很简单
    1
    FileOutputStream fileOutputStream = new FileOutputStream(FilePath,true);//也就是追加的方式写入

文件拷贝

完成 文件拷贝 将
思路分析
先从文件的输入流,到java中去,然后再从java中输出文件文件很大循环
完成程序时,应该是读取部份数据,就写入到指定位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class FileCopy {
public static void main(String[] args) throws IOException {
String filePath = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\BufferedInputStream.png";
String filePathDest = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\Demo02\\BufferedInputStream.png";
int readLen = 0;
FileInputStream fileInputStream = new FileInputStream(filePath);
FileOutputStream fileOutputStream = new FileOutputStream(filePathDest);
byte[] buf = new byte[8];//这个写的8个效率太低,用1024,
while ((readLen = fileInputStream.read(buf)) != -1) {
//读取到后,就写入到文件
//即,边读边写
fileOutputStream.write(buf, 0, readLen);
}
System.out.println("拷贝成功");
fileInputStream.close();
fileOutputStream.close();
}

}
  • 注意:fileInputStream.read() 不带参数时,返回的是单个字节(0-255),不是读取的字节数
    当 readLen 大于缓冲区大小时(8),就会抛出 IndexOutOfBoundsException

常用的类

  • FileReader 和 FileWriter 介绍
    FileReader 和 FileWriter 是 Java 中用于字符流操作的类,专门用于读写文本文件。

  • 基本概念
    FileReader
    用于从文件读取字符数据

继承自 InputStreamReader

使用系统默认字符编码

  • FileWriter
    用于向文件写入字符数据

继承自 OutputStreamWriter

使用系统默认字符编码

FileWriter 常用方法

📝 构造函数

方法 说明
new FileWriter(File/String) 覆盖模式,流指针在文件开头,会覆盖原有内容
new FileWriter(File/String, true) 追加模式,流指针在文件末尾,在原有内容后追加

✍️ 写入方法

方法 说明 示例
write(int c) 写入单个字符 writer.write('A');
write(char[] cbuf) 写入整个字符数组 writer.write(charArray);
write(char[] cbuf, int off, int len) 写入字符数组的指定部分 writer.write(chars, 0, 5);
write(String str) 写入整个字符串 writer.write("Hello");
write(String str, int off, int len) 写入字符串的指定部分 writer.write("Hello", 1, 3);

🔗 相关 API

String 类方法:

  • toCharArray() - 将 String 转换成 char[]

💡 重要注意事项

FileWriter 使用后,必须要关闭 (close) 或刷新 (flush),否则写入不到指定的文件!

📋 完整示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import java.io.FileWriter;
import java.io.IOException;

public class FileWriterDemo {
public static void main(String[] args) {
// 1. 覆盖模式示例
try (FileWriter writer1 = new FileWriter("output.txt")) {
writer1.write("这是第一行 - 覆盖模式\n");
writer1.write("原有内容会被覆盖");
} catch (IOException e) {
e.printStackTrace();
}

// 2. 追加模式示例
try (FileWriter writer2 = new FileWriter("output.txt", true)) {
writer2.write("\n这是追加的内容 - 追加模式");
} catch (IOException e) {
e.printStackTrace();
}

// 3. 各种写入方法示例
try (FileWriter writer3 = new FileWriter("demo.txt")) {
// 写入单个字符
writer3.write(65); // 写入 'A'
writer3.write('\n');

// 写入字符数组
char[] chars = {'H', 'e', 'l', 'l', 'o'};
writer3.write(chars);
writer3.write('\n');

// 写入字符数组的指定部分
writer3.write(chars, 1, 3); // 写入 "ell"
writer3.write('\n');

// 写入整个字符串
String text = "Hello World";
writer3.write(text);
writer3.write('\n');

// 写入字符串的指定部分
writer3.write(text, 6, 5); // 写入 "World"
writer3.write('\n');

// 使用 String.toCharArray()
char[] textArray = text.toCharArray();
writer3.write(textArray);

} catch (IOException e) {
e.printStackTrace();
}
}
}

🎯 使用要点

1. 资源管理

1
2
3
4
5
6
7
8
9
10
11
12
// 推荐:使用 try-with-resources 自动关闭
try (FileWriter writer = new FileWriter("file.txt")) {
writer.write("内容");
} // 自动调用 close()

// 或者手动关闭
FileWriter writer = new FileWriter("file.txt");
try {
writer.write("内容");
} finally {
writer.close(); // 必须关闭!
}

2. 刷新缓冲区

1
2
3
4
5
FileWriter writer = new FileWriter("file.txt");
writer.write("内容");
writer.flush(); // 立即将缓冲区内容写入文件
// ... 其他操作
writer.close();

3. 模式选择

1
2
3
4
5
// 覆盖模式 - 文件存在则清空重写
FileWriter writer1 = new FileWriter("data.txt");

// 追加模式 - 在文件末尾添加内容
FileWriter writer2 = new FileWriter("data.txt", true);

⚠️ 常见错误

  1. 忘记关闭或刷新流

    1
    2
    3
    4
    // 错误:内容可能不会写入文件
    FileWriter writer = new FileWriter("file.txt");
    writer.write("内容");
    // 缺少 close() 或 flush()
  2. 使用错误的模式

    1
    2
    // 如果不想覆盖原有内容,记得使用追加模式
    FileWriter writer = new FileWriter("log.txt", true);

✅ 最佳实践

  1. 始终使用 try-with-resources
  2. 根据需求选择合适的模式(覆盖/追加)
  3. 及时关闭或刷新流
  4. 处理 IOException 异常
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.io.FileWriter;
import java.io.IOException;

public class FileWriter_ {
public static void main(String[] args) throws IOException {
String filePath = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\Reader\\story.txt";
//这个是覆盖
FileWriter fileWriter = new FileWriter(filePath);
//写入单个字符 write(int)
fileWriter.write('H');
//写入指定数组write(char[])
char[] chars = {'a', 'b', 'c'};
fileWriter.write(chars);
//write(char[],off,len):写入指定数组的指定部分
fileWriter.write("成都东软学院".toCharArray(), 0, 3);
//写入整个字符串write(String)
fileWriter.write("你好北京");
fileWriter.close();//一定要关闭,才能写入。如果不关闭文件会创建但是不会写入代码
//在数据量大的情况下使用循环操作
}

}

节点流和处理流

  1. 节点流可以从一个特定的数据源读写数据,如FileReader、FileWriter
  2. 处理流(也叫包装流)是“连接”在已存在的流(节点流或处理流)之上,为程序提供更为强大的读写功能,如BufferedReader、BufferedWriter

📚 基本概念

节点流 (Node Stream)

  • 定义:直接从特定数据源读写数据的流
  • 特点:数据源的直接连接点
  • 比喻:直接的水龙头

处理流 (Processing Stream) / 包装流 (Wrapper Stream)

  • 定义:连接在已有流之上,提供增强功能的流
  • 特点:不直接连接数据源,而是包装其他流
  • 比喻:水龙头上的过滤器或增压器

🆚 对比表格

特性 节点流 处理流
数据源连接 直接连接 间接连接(通过包装其他流)
功能 基础读写操作 增强功能(缓冲、转换等)
独立性 可独立使用 必须依赖其他流
性能 相对较低 通常更高(通过缓冲等机制)
例子 FileReader, FileWriter BufferedReader, BufferedWriter

💡 核心理解

设计模式:装饰器模式

处理流基于装饰器模式设计,允许动态地为对象添加功能。

流的关系

1
数据源 ← 节点流 ← 处理流 ← 处理流 ← 程序

📝 代码示例

1. 纯节点流使用(低效方式)

1
2
3
4
5
6
7
// 类比:直接对着水龙头喝水
FileReader fileReader = new FileReader("test.txt");
int data;
while ((data = fileReader.read()) != -1) {
System.out.print((char) data); // 一次读一个字符,效率低
}
fileReader.close();

2. 节点流 + 处理流(推荐方式)

1
2
3
4
5
6
7
8
9
10
// 类比:水龙头 + 水桶,一次接很多水
FileReader fileReader = new FileReader("test.txt");
BufferedReader bufferedReader = new BufferedReader(fileReader);

String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line); // 一次读一行,效率高!
}

bufferedReader.close(); // 关闭处理流会自动关闭节点流

3. 多层处理流包装

1
2
3
4
5
6
7
8
9
10
11
12
// 文件 → 文件流 → 缓冲流 → 数据流
DataInputStream dis = new DataInputStream(
new BufferedInputStream(
new FileInputStream("data.dat")
)
);

// 使用增强功能
int number = dis.readInt();
double value = dis.readDouble();

dis.close();

🛠️ 常见流分类

节点流示例

数据源 输入流 输出流
文件 FileInputStream
FileReader
FileOutputStream
FileWriter
内存数组 ByteArrayInputStream
CharArrayReader
ByteArrayOutputStream
CharArrayWriter
管道 PipedInputStream
PipedReader
PipedOutputStream
PipedWriter

处理流示例

功能 输入流 输出流
缓冲 BufferedInputStream
BufferedReader
BufferedOutputStream
BufferedWriter
数据类型 DataInputStream DataOutputStream
对象序列化 ObjectInputStream ObjectOutputStream
转换 InputStreamReader OutputStreamWriter

✅ 最佳实践

1. 正确的关闭方式

1
2
3
4
5
6
7
// 推荐:使用try-with-resources
try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
// 处理数据
}
} // 自动关闭所有流

2. 流组合原则

1
2
3
4
5
6
// 良好的流组合
BufferedReader br = new BufferedReader(
new InputStreamReader( // 字节转字符
new FileInputStream("file.txt") // 节点流
)
);

3. 资源管理

  • 只需要关闭最外层的处理流
  • 内层流会自动关闭
  • 推荐使用try-with-resources确保资源释放

🎯 核心要点总结

  1. 节点流是基础:建立与数据源的直接连接
  2. 处理流是增强:提供缓冲、转换、数据类型处理等高级功能
  3. 灵活组合:可以根据需要多层包装处理流
  4. 性能优化:处理流通常通过缓冲机制提升I/O性能
  5. 资源管理:正确处理流的关闭顺序和异常情况

BufferedReader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class BufferedReader_01 {
public static void main(String[] args) throws IOException {
String filePath = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\Reader\\story.txt";
//创建BufferedReader对象
BufferedReader bufferedReader = new BufferedReader(new FileReader(filePath));
String line;//读取一行的信息
//当字符串返回null的时候就表述读取完毕
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
//关闭流
bufferedReader.close();
}
}

BufferedWrite

1
2
3
4
5
6
7
8
9
10
11
12
import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;

public class BufferedWrite_ {
public static void main(String[] args) throws IOException {
String filePath = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\Reader\\story.txt";
BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(filePath));
bufferedWriter.write("晚上好,我在上课,看到信息后给你回复(自动回复 留言模式休眠30秒)");
bufferedWriter.close();
}
}

用BufferedWrite和BufferedRead进行文本文件的拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.io.*;

public class BufferedCopy_ {
public static void main(String[] args) throws IOException {
String scrFilePath = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\Reader\\story.txt";
String mudi = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\writer\\story.txt";
String line;
//1.BufferedReader和BufferedWriter 是安装字符操作
//2.不要去操作 二进制文件,文件可能会损坏
BufferedReader bufferedReader = new BufferedReader(new FileReader(scrFilePath));
BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(mudi));
while ((line = bufferedReader.readLine()) != null) {
bufferedWriter.write(line);
bufferedWriter.newLine();
}
bufferedWriter.close();
bufferedReader.close();
}
}

用BufferedInputStream和BufferedOutputStream进行音频视频图片的拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.io.*;

public class BufferedCopy_2 {
public static void main(String[] args) throws IOException {
String srcFilePath = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\Demo02\\BufferedInputStream.png";
String mudi = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\writer\\copy.png";
byte[] buf = new byte[1024];
int readLen;
BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(srcFilePath));
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(mudi));
while ((readLen = bufferedInputStream.read(buf)) != -1) {
bufferedOutputStream.write(buf, 0, readLen);
}
bufferedInputStream.close();
bufferedOutputStream.close();


}
}
方法规律
read(byte[]):字节流读取,返回读取的字节数

readLine():字符流读取一行,返回String

write(byte[], 0, len):字节流写入,要指定长度

write(String):字符流直接写字符串

节点流和处理流 - 对象流

需求场景

  1. int num = 100 这个 int 数据保存到文件中,注意不是保存数字”100”,而是保存 int 类型的 100,并且能够从文件中直接恢复 int 100
  2. Dog dog = new Dog("小黄", 3) 这个 dog 对象保存到文件中,并且能够从文件恢复

序列化和反序列化

序列化 (Serialization)

  • 在保存数据时,保存数据的数据类型
  • 将对象转换为字节序列的过程

反序列化 (Deserialization)

  • 在恢复数据时,恢复数据的数据类型
  • 将字节序列恢复为对象的过程

对象流类

ObjectOutputStream

  • 序列化功能,将对象转换为字节流写入文件

ObjectInputStream

  • 反序列化功能,从文件中读取字节流并恢复为对象

实现序列化的条件

要让某个对象支持序列化机制,其类必须是可序列化的。必须实现以下接口之一:

Serializable 接口

  • 这是一个标记接口,没有需要实现的方法
  • 推荐使用,比较简单

Externalizable 接口

  • 该接口有方法需要实现
  • 一般使用上面的 Serializable 接口

示例代码结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 需要序列化的类必须实现 Serializable
class Dog implements Serializable {
private String name;
private int age;

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

// getters and setters
}

// 序列化操作
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("data.dat"));
oos.writeInt(100); // 序列化基本数据类型
oos.writeObject(dog); // 序列化对象

// 反序列化操作
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("data.dat"));
int num = ois.readInt(); // 反序列化基本数据类型
Dog restoredDog = (Dog) ois.readObject(); // 反序列化对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.io.*;

public class ObjectOutStream_ {
public static void main(String[] args) throws IOException {
String sFilePath = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\FileOutputStream\\data.dat";
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(sFilePath));
//序列化数据到文件
objectOutputStream.write(100);//int ->Integer(实现了Serializable)
objectOutputStream.writeBoolean(true);//boolean -> Boolean(实现了Serializable)
objectOutputStream.write('a');//char ->Character(实现了Serializable)
objectOutputStream.writeDouble(9.5);//double ->Double(实现了Serializable)
objectOutputStream.writeUTF("字符串形式");//String

//狗狗对象序列化
objectOutputStream.writeObject(new Dog("旺财"));
//这里注意,对象里面的序列化了但是对象本身没有序列化对象实现一个Serializable就行了

}
}

class Dog implements Serializable {
String name;

public Dog(String name) {
this.name = name;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class ObjectInputStream_ {
public static void main(String[] args) throws IOException, ClassNotFoundException {
//读取文件的路径
String sFilePath = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\FileOutputStream\\data.dat";
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(sFilePath));
//1.读取(反序列化)的顺序需要和你保存数据(序列化)的顺序一致
System.out.println(objectInputStream.readInt());
System.out.println(objectInputStream.readBoolean());
System.out.println(objectInputStream.read());
System.out.println(objectInputStream.readDouble());
System.out.println(objectInputStream.readUTF());
Object dog = objectInputStream.readObject();
System.out.println("运行的类型:" + dog.getClass());
System.out.println("dog信息:" + dog);//dog信息:IO.File.FileOutputStream.Dog@1a93a7ca的原因是没有在dog里面重写一下
//注意重写了对象里面的内容,需要重新执行序列化一次才能正确的反序列化,否则反序列化报错
objectInputStream.close();
//1。如果我们希望调用Dog的方法,需要向下转型
// 2。需要我们将Dog类的定义,拷贝到可以引用的位置
Dog dog1 = (Dog) dog;
dog1.getName();
}
}

class Dog implements Serializable {
String name;

public Dog(String name) {
this.name = name;
}

public String getName() {
return name;
}

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

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

节点流与处理流核心区别及序列化注意事项

本文将清晰梳理节点流与处理流的核心差异,并系统整理序列化与反序列化操作的关键注意事项。

一、节点流与处理流核心区别

节点流和处理流的本质区别在于是否直接连接数据源/目的地,以及功能定位的不同。

对比维度 节点流(Node Stream) 处理流(Processing Stream)
数据源连接 直接连接数据源或目的地 不直接连接,而是包裹节点流或其他处理流
功能定位 负责基础的读写数据操作,是IO操作的基础 对已有流的数据进行加工处理,如缓冲、转换、过滤等
依赖关系 可独立使用,无需依赖其他流 必须依赖节点流或其他处理流才能工作
典型示例 FileInputStream、FileOutputStream、FileReader BufferedInputStream、BufferedReader、ObjectOutputStream

二、序列化与反序列化注意事项

在使用处理流(如ObjectOutputStream/ObjectInputStream)进行对象序列化或反序列化时,需严格遵循以下规则。

  1. 读写顺序必须一致
    序列化时写入对象的顺序,与反序列化时读取对象的顺序必须完全相同,否则会抛出EOFExceptionClassCastException

  2. 类必须实现Serializable接口
    只有实现了java.io.Serializable接口的类,其对象才能被序列化。该接口为标记接口,不含任何抽象方法,仅用于标识类具备序列化能力。

  3. 建议显式添加SerialVersionUID
    显式声明private static final long serialVersionUID,可固定类的版本标识。若不添加,JVM会根据类结构自动生成,类结构(如属性、方法)修改后会生成新值,导致旧版本序列化文件无法反序列化,影响版本兼容性。

  4. static与transient修饰的成员不序列化
    序列化时,默认对对象所有非静态、非瞬态属性进行序列化;static修饰的静态属性属于类,transient修饰的瞬态属性为临时数据,两者均不会被序列化。

  5. 属性类型需同样实现Serializable
    若对象的某个属性为自定义类型,该自定义类型也必须实现Serializable接口,否则序列化时会抛出NotSerializableException

  6. 序列化具备可继承性
    若父类已实现Serializable接口,其所有子类会默认继承该能力,无需再显式实现Serializable接口,子类对象可直接参与序列化。


标准输入流输出流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.io.BufferedInputStream;

public class InputAndoutput {
public static void main(String[] args) {
//System 类public final static InputStream in = null;
//System.in 编译类型InputStream
//System.in 运行类型BufferedInputStream
//System.in 标准输入 键盘
System.out.println(System.in.getClass());
//public static final PrintStream out = null;
//System.out表示标准输出 显示器
System.out.println(System.out.getClass());
}
}

乱码引出转换流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//读取文件

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;

//演示使用 InputStreamReader 转换流解决中文乱码问题
//将字节流 FileInputstream 转成字符流,指定编码 gbk/utf-8
public class InputStreamReader_ {
public static void main(String[] args) throws IOException {
//地址
String FilePath = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\Reader\\story.txt";
//创建
InputStreamReader inputStreamReader = new InputStreamReader(new FileInputStream(FilePath), "gbk");
//把inputStreamReader传入buff
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String s = bufferedReader.readLine();
System.out.println("输出的信息:" + s);
//关闭流,关闭外层的
bufferedReader.close();


}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
保存文件
import java.io.BufferedWriter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;

public class QutputStreamWriter_ {
public static void main(String[] args) throws IOException {
//路径
String FilePath = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\Reader\\story.txt";
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream(FilePath), "gbk"));
bufferedWriter.write("hello hi");
bufferedWriter.close();
}
}