后端java重温java基础知识
Silence前言
自从准备考试以后很久都没有整过我自己的老本行了,很多东西都忘得差不多了,我决定在这个学期把丢下的这些东西全都捡起来
还有硬件的一些专业知识,包括最新的ai都学习起来
快速整理:ctrl+alt+L
快速运行:ctrl+shift+F10
多行注释:ctrl+alt+/
单行:ctrl+/
构造方法等:alt+insert
try-catch: ctrl+alt+t
Math.sqrt() 用于计算平方根
ctrl + Alt+U 查看类图
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
| double result = 10.0 / 3; System.out.println("使用 printf() 保留小数:"); System.out.printf("保留2位小数:%.2f%n", result); System.out.printf("保留5位小数:%.5f%n", result); System.out.printf("保留9位小数:%.9f%n", result); 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); System.out.println("保留5位小数:" + str5); System.out.println("保留9位小数:" + str9); 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; System.out.printf("结果保留6位小数:%.6f%n", userResult); scanner.close();
|
局部变量的类型转换
重要部分:只有初始化变量时var才能用来声明变量,初始化变量就是赋一个值
数组,字符串,正则表达式
一维数组的定义
定义需要经过三个步骤:声明数组,数组分配内存空间,创建数组空间并且赋值
声明
数据类型[] 数组名
这里与c不同的是c是将 数组名和[] 调换了方向的。本人一开始学习的c,所以更倾向于c的写法
数组分配内存空间
分配内存给数组
数组创建后就不能修改其大小
一维数组的初始化
在定义数组的同时就为数组元素分配空间并赋值。
1
| 数据类型 [] 数组名 = {初值1,初值2,初值3,.....}
|
二维数组
二维数组初始化
1 2 3 4 5 6 7 8 9 10 11
| 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
int level = 7; int[][] pyramid = new int[level][];
for (int i = 0; i < pyramid.length; i++) { pyramid[i] = new int[i + 1]; }
|
这个过程就像:
你先申请了一块地,说这里要盖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'; char digit = '9'; char symbol = '#'; char ch = ' ';
字符串常量 使用双引号 "" 括起来,里面的内容可以非常灵活。
|
1 2 3 4
| String greeting = "Hello, World!"; String single = "A"; String empty = ""; String number = "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
| 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(); } }
|
static
和 final
的组合作用
static
和 final
虽然经常一起使用,但它们的作用是完全不同的。
两者的区别
关键字 |
作用 |
解决的问题 |
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();
|
为什么数学常量需要这样设计?
1. 数学真实性
π的值是固定不变的,不应该允许被修改
1 2 3
| MathUtils.PI = 4.0; double area = MathUtils.PI * radius * radius;
|
2. 内存效率
π的值对所有对象都是一样的,不需要每个对象都存储一份
1 2 3 4 5
| MathUtils util1 = new MathUtils(); MathUtils util2 = new MathUtils(); MathUtils util3 = new MathUtils();
|
3. 访问便利性
不需要创建对象就能使用
1 2 3 4 5 6
| double area = MathUtils.PI * radius * radius;
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"; }
|
错误代码
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; }
|
配置参数
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"; }
|
总结
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; int num = i;
|
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 pi = d;
|
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); }
|
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); }
|
c) 集合操作时(使用频率极高)
Java 的集合框架(如 ArrayList
, HashMap
)只能存储对象,不能存储基本类型。自动装箱让我们可以像直接存基本类型一样方便。
1 2 3 4 5
| ArrayList<Integer> list = new ArrayList<>(); list.add(1); list.add(2);
int first = list.get(0);
|
d) 运算和比较时
在数学运算中,包装类会自动拆箱为基本类型进行计算。在三元运算符、==
、>
、<
等混合运算中,也会发生自动拆箱和装箱。
1 2 3 4 5
| Integer a = 10; Integer b = 20; int result = a + b;
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);
Integer c = 128; Integer d = 128; System.out.println(c == d);
|
情况2:任何范围,==
比较包装类和基本类型
这时包装类会自动拆箱,然后比较值。
1 2 3
| Integer a = 128; int b = 128; System.out.println(a == b);
|
最佳实践:比较包装类的值,永远使用 .equals()
方法!
1 2 3
| Integer a = 128; Integer b = 128; System.out.println(a.equals(b));
|
陷阱二:空指针异常(NullPointerException)
包装类可以是 null
,而基本类型不能。如果在拆箱一个 null
的包装类,就会抛出异常。
1 2
| Integer nullInteger = null; int num = nullInteger;
|
这种情况常发生在从方法返回 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
| ArrayList list = new ArrayList(); int age = 25; Integer ageInteger = Integer.valueOf(age); list.add(ageInteger);
Integer result = (Integer) list.get(0); int myAge = result.intValue(); System.out.println(myAge + 1);
|
太麻烦了! 每次存取的代码都变得很长。
有了包装类和自动装箱后:
1 2 3 4 5
| ArrayList<Integer> list = new ArrayList<>(); list.add(25); int myAge = list.get(0); System.out.println(myAge + 1);
|
结论: 为了让基本数据类型也能方便地存入集合这种强大的容器中,我们必须使用包装类,而自动装箱/拆箱让这个过程毫不费力。
场景二:表示”缺失”或”未知”的值(空值)
基本类型都有默认值(如 int
默认是 0
)。但在很多业务场景下,0
是一个有效的数值,而不是”未知”或”未设置”。
例子:统计学生年龄
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| int studentAge = 0;
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
| private final Integer lockId = 100;
public void someMethod() { synchronized(lockId) { } }
|
虽然基本类型 int
不能这么用,但它的包装类 Integer
可以。
总结:如何选择用哪个?
特性 |
基本类型 (e.g., int ) |
包装类 (e.g., Integer ) |
本质 |
纯数据值 |
对象 |
存储 |
在栈内存,效率极高 |
在堆内存,有开销 |
默认值 |
有(如 int 是 0 ) |
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 { @Override public void makeSound() { System.out.println("狗在汪汪叫"); } }
class Cat extends Animal { @Override public void makeSound() { System.out.println("猫在喵喵叫"); } @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(); cat.makeSound(); animal.move(); dog.move(); cat.move(); } }
|
用父类的对象访问子类的成员
如果希望通过父类类型的引用访问子类成员,需要满足两个条件:
该成员在父类中已定义(即子类覆盖了父类的方法,或继承了父类的属性)
通过多态(父类引用指向子类对象)的方式访问
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(); Parent poly = new Child(); System.out.println(poly.parentAttr); poly.parentMethod(); 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("叽叽喳喳"); } 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面向对象编程中实现代码重用和多态性的重要工具,常用于框架设计和大型项目的架构中。
接口
- 接口是什么?
定义:接口是一个完全抽象的类型。它是一组行为规范的集合,只声明“应该做什么”,而不关心“如何做”。
核心思想:定义标准,实现解耦。它主要体现的是一种 “像…一样” (can-do) 的关系,而不是“是” (is-a) 的关系。
关键字:interface
- 接口的特点 (Java 7及以前)
在 Java 8 之前,接口是一个非常纯粹的“契约”。
成员变量 (字段):
默认都是 public static final 的常量。
必须显式初始化。
通常用于定义一些全局常量。
1 2 3 4
| interface USB { double VERSION = 3.0; }
|
方法:
所有普通方法默认都是 public abstract 的抽象方法(没有方法体)。
没有构造方法,不能被实例化。
1 2 3 4 5
| interface Animal { void eat(); void sleep(); }
|
- 接口的实现
关键字: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("鸭子在水上游泳"); } }
|
- 接口的新特性 (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("汽车启动"); } }
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);
|
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); } }
|
- 接口的继承
接口可以使用 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(); }
interface Bat extends Animal, Winged { void useEcholocation(); }
class FruitBat implements Bat { @Override public void eat() { } @Override public void fly() { } @Override public void useEcholocation() { } }
|
- 接口 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
| interface Chargeable { String STANDARD = "USB-C";
void charge();
default void showChargingStandard() { System.out.println("充电标准: " + STANDARD); }
static void printWelcome() { System.out.println("欢迎使用充电系统"); } }
class Phone implements Chargeable { private String brand; private int batteryLevel;
public Phone(String brand) { this.brand = brand; this.batteryLevel = 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 + "接口充电"); } }
class Laptop implements Chargeable { private String model;
public Laptop(String model) { this.model = model; }
@Override public void charge() { System.out.println(model + "笔记本电脑正在快速充电"); } }
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 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}; // 直接添加新设备
- 统一处理不同类型的对象
1 2 3 4 5 6 7 8 9
| Phone[] phones = {...}; Laptop[] laptops = {...}; Tablet[] tablets = {...};
for (Chargeable device : devices) { device.charge(); }
|
- 降低代码耦合度
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() { 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(); } if (myAnimal1 instanceof Dog) { Dog myDog = (Dog) myAnimal1; myDog.fetch(); } } }
|
实际应用场景
场景1:集合框架(最经典的例子)1 2 3 4 5 6 7 8 9 10
| 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 {
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 + " 送衣服到干洗店"); 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 {
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); 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); } });
|
总结
接口回调的三要素
- 回调接口:定义通信协议
- 调用方:实现接口,提供回调方法的具体逻辑
- 被调用方:持有接口引用,在适当时机调用回调方法
核心价值
- 灵活性:易于扩展和修改
- 可维护性:代码结构清晰,职责分离
- 复用性:回调接口可以被多个类实现
- 异步支持:适合处理耗时操作
记忆技巧
“留个电话,完事儿叫我”
- 留电话 = 注册回调接口
- 完事儿 = 被调用方完成工作
- 叫我 = 执行回调方法
接口回调是现代编程中非常重要的概念,掌握它对于理解事件驱动编程、异步处理和各种设计模式都有很大帮助。
接口的继承
接口也具备继承性,接口继承与类的继承不同的是,接口可以有多个父类,中间用逗号隔开,新接口将继承父接口中的所有常量,抽象方法和默认方法,但不能继承父接口中的静态方法和私有方法
也不能被实现类所继承如果类实现的接口继承自另外一个接口,那么该类必须实现在接口继承链中定义的所有抽象方法
枚举
包
java语言中引入了包的概念来管理类名空间。就像文件夹把各种文件进行管理一样。一种区别类名空间的机制
异常处理
异常是指在程序运行中由代码产生的一种错误。
异常处理机制
- 抛出异常
程序的运行中,发生了异常事件,则产生代表该异常的一个异常对象,交给运行系统,由运行系统去找寻对应的代码来处理相关异常
- 捕获异常
异常抛出后,运行系统从生成的异常对象代码开始,沿着方法的调用栈逐层回溯查找,直到找到包含相应处理异常的方法,并把异常对象提交给该方法为止
异常处理类
异常类的最顶层有一个单独的类为:Throwable,该类派生出了Error和Exception两个子类其中Error由系统保留,我们一般使用Excption
Exception异常分类
运行时异常:RuntimeException及其子类,编译阶段不会出现错误提醒,运行时出现的异常(如:数组索引越界异常)
编译时异常:编译阶段就会出现错误提醒的。(如:日期解析异常)
运行时异常和编译时异常其实很好区分,当你在编码阶段没有报错,而点击运行代码后抛出了错误后这个其实就是运行错误
而编译错误就是在编码阶段就会提醒你的错误,所以编码错误也称检查错误
异常的处理
异常处理是通过try,catch,finally,thow,thows五个关键字实现的
- try-catch-finally语法格式:
1 2 3 4 5 6 7 8 9 10 11 12 13
| try { } catch (ExceptionType1 e1) { System.out.println("捕获到异常: " + e1.getMessage()); } catch (ExceptionType2 e2) { System.out.println("捕获到异常: " + e2.getMessage()); } finally { }
|
- 多异常处理机制:
多异常处理机制:一个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(); } }
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); }
}
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());
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("该文件不存在"); } }
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流原理
- I/O是Input/Output的缩写,I/0技术是非常实用的技术,用于处理数据传输。
如读/写文件,网络通讯等。
- Java程序中,对于数据的输入/输出操作以”流(stream)”的方式进行。
- java.io包下提供了各种“流”类和接口,用以获取不同种类的数据,并通过方法输入或输出数据
流的分类
流的分类
按操作数据单位不同分为:字节流(8 bit),字符流(按字符) 效率高一些
按数据流的流向不同分为:输入流,输出流
按流的角色的不同分为:节点流,处理流/包装流

IO流体系图-常用的类
InputStream:字节输入流
InputStream 抽象类是所有字节输入流的超类。
InputStream 常用的子类:
FileInputStream:文件输入流
BufferedInputStream:缓冲字节输入流
ObjectInputStream:对象字节输入流


这里讲了两种写法,第一种是循环读取一个一个的读取字节数并且转成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(); }
public void fileRead01() throws IOException { String filePath = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\InputStream_\\test.txt"; int read = 0;
FileInputStream fileInputstream = new FileInputStream(filePath);
while ((read = fileInputstream.read()) != -1) { System.out.println((char) read); } 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(); }
public void fileRead01() throws IOException { String filePath = "D:\\JAVA\\newJava\\new1\\src\\IO\\File\\InputStream_\\test.txt";
byte[] buf = new byte[8]; int readLenth = 0; FileInputStream fileInputstream = new FileInputStream(filePath); while ((readLenth = fileInputstream.read(buf)) != -1) { System.out.println(new String(buf, 0, readLenth)); System.out.println(readLenth); } fileInputstream.close(); } }
|
FileOutputStream

其实跟输入流大差不差的,就是将数据写入到文件中
- 同样的是先测试一个字节的写入,同时注意,这里是直接覆盖了文件原有的内容
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(); } }
|
- 写入多个内容的时候,这里用字符串输入,然后用字符串自带的转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 str = "hello word"; fileOutputStream.write(str.getBytes()); fileOutputStream.close(); } }
|
- 要想不覆盖原有的内容也很简单
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]; 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
常用的类
继承自 InputStreamReader
使用系统默认字符编码
继承自 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) { try (FileWriter writer1 = new FileWriter("output.txt")) { writer1.write("这是第一行 - 覆盖模式\n"); writer1.write("原有内容会被覆盖"); } catch (IOException e) { e.printStackTrace(); } try (FileWriter writer2 = new FileWriter("output.txt", true)) { writer2.write("\n这是追加的内容 - 追加模式"); } catch (IOException e) { e.printStackTrace(); } try (FileWriter writer3 = new FileWriter("demo.txt")) { writer3.write(65); writer3.write('\n'); char[] chars = {'H', 'e', 'l', 'l', 'o'}; writer3.write(chars); writer3.write('\n'); writer3.write(chars, 1, 3); writer3.write('\n'); String text = "Hello World"; writer3.write(text); writer3.write('\n'); writer3.write(text, 6, 5); writer3.write('\n'); 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 (FileWriter writer = new FileWriter("file.txt")) { writer.write("内容"); }
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 2 3 4
| FileWriter writer = new FileWriter("file.txt"); writer.write("内容");
|
使用错误的模式
1 2
| FileWriter writer = new FileWriter("log.txt", true);
|
✅ 最佳实践
- 始终使用 try-with-resources
- 根据需求选择合适的模式(覆盖/追加)
- 及时关闭或刷新流
- 处理 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); fileWriter.write('H'); char[] chars = {'a', 'b', 'c'}; fileWriter.write(chars); fileWriter.write("成都东软学院".toCharArray(), 0, 3); fileWriter.write("你好北京"); fileWriter.close(); }
}
|
节点流和处理流
- 节点流可以从一个特定的数据源读写数据,如FileReader、FileWriter
- 处理流(也叫包装流)是“连接”在已存在的流(节点流或处理流)之上,为程序提供更为强大的读写功能,如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 (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确保资源释放
🎯 核心要点总结
- 节点流是基础:建立与数据源的直接连接
- 处理流是增强:提供缓冲、转换、数据类型处理等高级功能
- 灵活组合:可以根据需要多层包装处理流
- 性能优化:处理流通常通过缓冲机制提升I/O性能
- 资源管理:正确处理流的关闭顺序和异常情况
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 = new BufferedReader(new FileReader(filePath)); String line; 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; 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(); } }
|
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):字符流直接写字符串
|
节点流和处理流 - 对象流
需求场景
- 将
int num = 100
这个 int 数据保存到文件中,注意不是保存数字”100”,而是保存 int 类型的 100,并且能够从文件中直接恢复 int 100
- 将
Dog dog = new Dog("小黄", 3)
这个 dog 对象保存到文件中,并且能够从文件恢复
序列化和反序列化
序列化 (Serialization)
- 在保存数据时,保存数据的值和数据类型
- 将对象转换为字节序列的过程
反序列化 (Deserialization)
- 在恢复数据时,恢复数据的值和数据类型
- 将字节序列恢复为对象的过程
对象流类
ObjectOutputStream
实现序列化的条件
要让某个对象支持序列化机制,其类必须是可序列化的。必须实现以下接口之一:
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
| class Dog implements Serializable { private String name; private int age; public Dog(String name, int age) { this.name = name; this.age = age; } }
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); objectOutputStream.writeBoolean(true); objectOutputStream.write('a'); objectOutputStream.writeDouble(9.5); objectOutputStream.writeUTF("字符串形式");
objectOutputStream.writeObject(new Dog("旺财"));
} }
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)); 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); objectInputStream.close(); 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)进行对象序列化或反序列化时,需严格遵循以下规则。
读写顺序必须一致
序列化时写入对象的顺序,与反序列化时读取对象的顺序必须完全相同,否则会抛出EOFException
或ClassCastException
。
类必须实现Serializable接口
只有实现了java.io.Serializable
接口的类,其对象才能被序列化。该接口为标记接口,不含任何抽象方法,仅用于标识类具备序列化能力。
建议显式添加SerialVersionUID
显式声明private static final long serialVersionUID
,可固定类的版本标识。若不添加,JVM会根据类结构自动生成,类结构(如属性、方法)修改后会生成新值,导致旧版本序列化文件无法反序列化,影响版本兼容性。
static与transient修饰的成员不序列化
序列化时,默认对对象所有非静态、非瞬态属性进行序列化;static
修饰的静态属性属于类,transient
修饰的瞬态属性为临时数据,两者均不会被序列化。
属性类型需同样实现Serializable
若对象的某个属性为自定义类型,该自定义类型也必须实现Serializable
接口,否则序列化时会抛出NotSerializableException
。
序列化具备可继承性
若父类已实现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.out.println(System.in.getClass()); 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;
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"); 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(); } }
|