适合人群 :Java基础已掌握,开始接触测试框架与反射机制的进阶学习者
涉及知识点 :JUnit单元测试、Assert断言、反射核心API、Class对象、Field字段操作、setAccessible权限突破**
阅读时长**:约15分钟
目录
- 一、单元测试:让代码自己证明自己是对的
- [1.1 什么是单元测试](#1.1 什么是单元测试)
- [1.2 StringUtil 工具类实现](#1.2 StringUtil 工具类实现)
- [1.3 JUnit测试类编写](#1.3 JUnit测试类编写)
- [1.4 测试结果与要点分析](#1.4 测试结果与要点分析)
- 二、反射机制:运行时透视对象的灵魂
- [2.1 什么是反射](#2.1 什么是反射)
- [2.2 ObjectFrame2 框架设计思路](#2.2 ObjectFrame2 框架设计思路)
- [2.3 核心代码实现](#2.3 核心代码实现)
- [2.4 测试类与运行结果](#2.4 测试类与运行结果)
- 三、知识点串联与总结
一、单元测试:让代码自己证明自己是对的
1.1 什么是单元测试
单元测试(Unit Testing)是指对软件中的最小可测试单元 进行检查和验证。在Java中,最小单元通常是一个方法。
| 对比项 | 传统 main 方法测试 | JUnit 单元测试 |
|---|---|---|
| 测试方式 | 手动 System.out.println 看输出 |
自动断言 Assert.assertEquals() |
| 结果判断 | 人眼比对 | 程序自动判定通过/失败 |
| 测试隔离 | 所有逻辑挤在 main 里 | 每个方法独立 @Test |
| 批量执行 | 不支持 | 一键运行全部测试 |
| CI/CD集成 | 不支持 | 完美对接持续集成 |
核心思想:测试代码和生产代码分离,用断言(Assert)自动验证结果,而非人工肉眼检查。
1.2 StringUtil 工具类实现
先编写被测试的目标类------一个字符串工具类,包含两个静态方法:
package org.example;
/**
* 字符串工具类
*/
public class StringUtil {
/**
* 打印字符串长度
* 如果参数为null,给出提示并返回
*/
public static void printNumber(String name) {
if (name == null) {
System.out.println("参数长度不能为null");
return;
}
System.out.println("名字长度是:" + name.length());
}
/**
* 求字符串的最大索引
* null返回-1,空字符串返回-1,正常字符串返回 length-1
*/
public static int getMaxIndex(String data) {
if (data == null) {
return -1;
}
return data.length() - 1;
}
}
设计要点:
| 方法 | 功能 | null处理 | 返回值 |
|---|---|---|---|
printNumber(String) |
打印字符串长度 | 检测null,打印提示后return | void |
getMaxIndex(String) |
获取最大索引 | 检测null,返回-1 | int |
注意 :
getMaxIndex对空字符串""返回-1(因为0 - 1 = -1),这是一个重要的边界情况,测试时必须覆盖。
1.3 JUnit测试类编写
引入JUnit依赖后,编写独立的测试类:
package org.example;
import org.junit.Assert;
import org.junit.Test;
public class StringUtilTest {
@Test
public void testPrintNumber() {
// 测试三种场景:null、空字符串、正常字符串
StringUtil.printNumber(null); // 预期:打印"参数长度不能为null"
StringUtil.printNumber(""); // 预期:打印"名字长度是:0"
StringUtil.printNumber("admin"); // 预期:打印"名字长度是:5"
}
@Test
public void testGetMaxIndex() {
// 场景1:null → 预期返回 -1
int i1 = StringUtil.getMaxIndex(null);
Assert.assertEquals("null测试失败!", -1, i1);
// 场景2:空字符串 → 预期返回 -1
int i2 = StringUtil.getMaxIndex("");
Assert.assertEquals("空字符串测试失败!", -1, i2);
// 场景3:正常字符串 "admin" → 预期返回 4
int i3 = StringUtil.getMaxIndex("admin");
Assert.assertEquals("admin测试失败!", 4, i3);
System.out.println("测试通过!");
}
}
JUnit核心注解与API一览:
| 注解/API | 作用 | 使用位置 |
|---|---|---|
@Test |
标记一个测试方法 | 方法上方 |
@Before |
每个测试方法执行前运行 | 初始化资源 |
@After |
每个测试方法执行后运行 | 清理资源 |
Assert.assertEquals(msg, expected, actual) |
断言期望值与实际值相等 | 方法体内 |
Assert.assertTrue(condition) |
断言条件为true | 方法体内 |
Assert.assertNull(object) |
断言对象为null | 方法体内 |
断言三要素 :
消息(失败时显示)+期望值+实际值。当 expected != actual 时,测试失败并抛出AssertionError。
1.4 测试结果与要点分析
运行结果:
参数长度不能为null
名字长度是:0
名字长度是:5
测试通过!
测试覆盖分析:
| 测试场景 | 输入值 | 期望结果 | 实际结果 | 是否通过 |
|---|---|---|---|---|
| null输入 | null |
打印提示 / 返回-1 | 打印提示 / 返回-1 | 通过 |
| 空字符串 | "" |
长度0 / 返回-1 | 长度0 / 返回-1 | 通过 |
| 正常字符串 | "admin" |
长度5 / 返回4 | 长度5 / 返回4 | 通过 |
测试原则 :每个方法至少覆盖 正常值、边界值、异常值 三种场景。本例中 null 是异常值,空字符串是边界值,"admin"是正常值,覆盖完整。
二、反射机制:运行时透视对象的灵魂
2.1 什么是反射
反射(Reflection)是Java在运行期间动态获取类信息、访问和修改对象字段、调用方法的机制。
没有反射时:
Student s = new Student("杳杉", 45, '男', 180, "篮球,冰球");
// 编译期就知道有哪些字段,直接点出来
s.name; // 编译期确定
有反射时:
Object obj = new Student("杳杉", 45, '男', 180, "篮球,冰球");
Class c = obj.getClass(); // 运行时获取Class对象
Field[] fields = c.getDeclaredFields(); // 运行时获取所有字段
// 不需要提前知道对象类型,就能遍历所有字段
一句话理解:反射就是在程序运行期间,"透视"任意对象内部结构的能力。
反射核心API速查表:
| API | 作用 | 返回类型 |
|---|---|---|
obj.getClass() |
获取运行时Class对象 | Class<?> |
c.getSimpleName() |
获取类名(不含包名) | String |
c.getDeclaredFields() |
获取所有声明字段(含private) | Field[] |
field.getName() |
获取字段名 | String |
field.setAccessible(true) |
突破private访问限制 | void |
field.get(obj) |
获取obj对象中该字段的值 | Object |
2.2 ObjectFrame2 框架设计思路
需求:把任意对象的字段名和值保存到文件中,不关心对象是什么类型。
设计思路:
输入:任意Java对象(Student / Teacher / 任何类)
↓
第一步:getClass() 获取Class对象
↓
第二步:getSimpleName() 获取类名(作为文件中的分隔标记)
↓
第三步:getDeclaredFields() 获取所有字段
↓
第四步:遍历字段,setAccessible(true) 突破权限
↓
第五步:field.get(obj) 取出每个字段的值
↓
输出:写入文件 格式为 字段名=值
实体类定义(Student 和 Teacher):
// Student.java
package org.example;
public class Student {
public String name;
public int age;
public char sex;
public double height;
public String hobbies;
public Student(String name, int age, char sex, double height, String hobbies) {
this.name = name;
this.age = age;
this.sex = sex;
this.height = height;
this.hobbies = hobbies;
}
}
// Teacher.java
package org.example;
public class Teacher {
public String name;
public double salary;
public Teacher(String name, double salary) {
this.name = name;
this.salary = salary;
}
}
2.3 核心代码实现
package org.example;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.lang.reflect.Field;
public class ObjectFrame2 {
/**
* 把任意对象的字段名和值保存到文件
* @param obj 任意Java对象
*/
public static void saveObject(Object obj) throws Exception {
// 1. 创建打印流,追加模式写入文件
PrintWriter pw = new PrintWriter(
new FileWriter("C:\\Users\\xxx\\Desktop\\day.txt", true)
);
// 2. 通过反射获取Class对象
Class c = obj.getClass();
String className = c.getSimpleName(); // 获取类名(不含包路径)
pw.println("===========================" + className + "==========================");
// 3. 获取Class对象中声明的所有字段(含private)
Field[] fields = c.getDeclaredFields();
for (Field field : fields) {
String name = field.getName(); // 字段名
field.setAccessible(true); // 突破private访问限制
String value = field.get(obj) + ""; // 字段值(转为字符串)
pw.println(name + "=" + value);
}
pw.close();
}
}
逐行解析关键代码:
| 代码行 | 作用 | 为什么需要 |
|---|---|---|
Class c = obj.getClass() |
获取运行时类对象 | 反射入口,所有操作始于Class |
c.getSimpleName() |
获取类名 | 只取类名不含包名,用于文件标记 |
c.getDeclaredFields() |
获取所有字段 | 含private字段,getFields()不含private |
field.setAccessible(true) |
取消访问检查 | private字段默认不可外部访问 |
field.get(obj) |
获取字段值 | 从obj对象中取出该字段的运行时值 |
getDeclaredFields() vs getFields():
getDeclaredFields():获取本类声明的所有字段(含private,不含继承的)getFields():获取所有public字段(含继承的)
2.4 测试类与运行结果
package org.example;
public class TestFrame {
public static void main(String[] args) throws Exception {
// 创建两个不同类型的对象
Student s1 = new Student("杳杉", 45, '男', 180, "篮球,冰球");
Teacher t1 = new Teacher("波妞", 999.0);
// 需求:把任意对象的字段名和其对应的值保存到文件中
ObjectFrame2.saveObject(s1);
ObjectFrame2.saveObject(t1);
}
}
运行后 day.txt 文件内容:
===========================Student===========================
name=杳杉
age=45
sex=男
height=180.0
hobbies=篮球,冰球
===========================Teacher===========================
name=波妞
salary=999.0
关键观察 :
saveObject()方法参数是Object类型,方法内部通过反射动态获取字段------无论传入Student还是Teacher,都能自动识别并保存。这就是反射的威力:一份代码,适配任意对象。
三、知识点串联与总结
3.1 两大知识点对照
| 维度 | 单元测试 JUnit | 反射 Reflection |
|---|---|---|
| 解决什么问题 | 验证代码正确性 | 运行时动态操作类和对象 |
| 核心思想 | 断言自动化验证 | 运行时透视对象结构 |
| 关键API | @Test Assert.assertEquals() |
getClass() getDeclaredFields() field.get() |
| 适用场景 | 方法级测试、回归测试 | 框架设计、ORM映射、序列化 |
| 工程价值 | 保障代码质量 | 灵活性与通用性 |
3.2 反射调用链路图
对象 obj
│
├── getClass() ──→ Class对象
│ │
│ ├── getSimpleName() ──→ 类名字符串
│ │
│ └── getDeclaredFields() ──→ Field[] 数组
│ │
│ ├── getName() → 字段名
│ ├── setAccessible() → 突破权限
│ └── get(obj) → 字段值
│
└── 写入文件:字段名=字段值
3.3 编程思维总结
| 思维要点 | 体现 |
|---|---|
| 测试先行意识 | 写完功能立刻写测试,不依赖main方法肉眼验证 |
| 边界值思维 | null、空字符串、正常值三种场景全覆盖 |
| 动态思维 | 反射打破"编译期必须知道类型"的限制,运行时动态处理 |
| 通用框架思维 | saveObject(Object obj) 用Object接收任意类型,反射动态解析 |
| 权限突破意识 | setAccessible(true) 是反射操作private字段的关键一步 |
进阶方向:反射是Spring IoC、MyBatis ORM、Jackson JSON序列化等主流框架的底层基石。掌握反射,就拿到了理解这些框架源码的钥匙。
如果本文对你有帮助,欢迎点赞收藏! 有问题欢迎评论区交流,我会一一回复。