这就是又能写安卓,又能写mc mod的函数名一长串的语言啊!
简介
分类
java分为三个版本Java SE、Java EE(现更名为Jakarta EE)、Java ME,他们之间关系就好像Python 和 MicroPython。
特性 |
JSE JRE |
JEE JRE |
JME JRE |
定位 |
标准版 |
企业版 |
微型版 |
库和 API |
核心 Java 库 |
核心库 + 企业级 API |
精简库 + 设备特定 API |
JVM |
标准 JVM |
标准 JVM |
定制化 JVM(如 CLDC HotSpot) |
适用场景 |
桌面应用、小型服务器 |
企业应用、Web 服务 |
移动设备、嵌入式系统 |
- JSE JRE 只能运行基于 JSE 开发的程序。
- JEE JRE 可以运行 JSE 和 JEE 程序,因为它包含了 JSE 的所有功能。
- JME JRE 只能运行基于 JME 开发的程序,因为它的库和 JVM 是专门为资源受限设备设计的。
如官网地址显示的就是**JDK 21 is the latest Long-Term Support (LTS) release of the Java SE Platform.**正常我们用的就是java se。上述内容不重要。
我学的好像是java8的老版本,但是为了fabric 1.21.4,我使用jdk21来进行实操。
C++的过渡
- Java没有指针,有引用,但是它的引用和C++的引用不是相同的概念
- Java能够自动管理内存
- Java多平台数据类型是统一大小的
- Java类似python,声明和实现在一起,不需要头文件h和实现cpp这种形式
- 没有define这种宏
- 不支持多重继承,即不能有多个爹,为了解决钻石继承,引入了接口这个性质
- 没有全局变量,作用域最大就是类里面
- 没有struct和union,有class就足够了
- 没有goto
运行
xxx.java编译成字节码xxx.class,在不同平台上使用不同的解释器来运行
JRE = JVE + API
JDK = JRE + Tools
基本工具如通过javac.exe编译,java.exe运行。
引入
hello
先写一个经典的hello world
1
2
3
4
5
|
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello World"); // 输出 Hello World
}
}
|
注意:类名和文件名要相同
对于一个**.java文件**我们要知道其组成部分:
- package (0 or 1)
- import (>= 0)
- class (>= 1) 但是 public class (= 1 并且与文件同名)
注:
- package 的作用就是 c++ 的 namespace 的作用,防止名字相同的类产生冲突。
- java因强制要求类名(唯一的public类)和文件名统一,因此在引用其它类时无需显式声明。在编译时,编译器会根据类名去寻找同名文件。
from runoob
注释
和c差不多,Javadoc提供根据文档注释生成文档的功能。
1
2
3
4
5
6
7
8
9
10
11
|
// 单行注释
/*
多行注释
*/
/**
* 文档注释
* @author cbksb
* @version 1.2
*/
|
类和对象
- 类 Class :含有 方法method (成员函数), 字段 field (成员变量)
- 对象 Object
- 继承 通过 class A extends B {…}
- 封装:通过public方法访问私有字段
- 多态
- 抽象:类似纯虚函数
- 接口:定义类必须实现的方法,支持多重继承。
- 重载:同c++,同名不同参
可变参数
基本语法
1
2
3
4
5
6
7
8
9
10
|
public void methodName(Type... parameterName) {
// 方法体
}
public static void printInfo(String message, int... values) {
System.out.println(message);
for (int value : values) {
System.out.println(value);
}
}
|
- 使用三个点(
...
)表示可变参数
- 可变参数必须是方法参数列表中的最后一个参数
- 在方法内部,可变参数被当作数组处理
和数组参数的对比
特性 |
可变参数 |
数组参数 |
声明方式 |
Type... parameterName |
Type[] parameterName |
调用方式 |
可以传递多个单独参数或数组 |
必须传递数组对象 |
注意一下特殊情况:
- 当不传参时,默认为空数组,不是 null
- 当传入一个 null 时,参数为 null
引用
Java中没有真正的引用传递,如C++中的&。所以通常就是返回一个值,然后重新赋值。
对于基本类型我们可以通过数组来实现。
1
2
3
4
5
6
7
8
9
10
11
|
public class Main {
public static void modify(int[] arr) {
arr[0] = 100; // 修改数组元素
}
public static void main(String[] args) {
int[] num = {10};
modify(num);
System.out.println(num[0]); // 输出100
}
}
|
对于其他的类型如String
或者自己定义的类,传入是原始值的内存地址,因为Java一切皆引用,所以本身被传入的那个变量也不过是指向内存中的真正变量的位置,这种情况下函数内的修改会对外面也产生影响。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public class Main {
public static void main(String[] args) {
Person p = new Person();
String bob = "Bob";
p.setName(bob); // 传入bob变量
System.out.println(p.getName()); // "Bob"
bob = "Alice"; // bob改名为Alice
System.out.println(p.getName()); // "Bob"还是"Alice"?
}
}
class Person {
private String name;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
}
|
上面这段程序两次输出都是"Bob",虽然它确实传递的是引用类型但是String,但是String对象一旦创建便不可修改。bob = "Alice"
实际上是创建新字符串对象,而非修改原"Bob"的内容。因此,p.name
引用的"Bob"始终未被改变。
最后解释一下经典的引用类型参数传递的是引用的拷贝,这里的引用相当于一个指针,指向真正类变量存储存储的地方,引用的拷贝也就是相当于另一个指针同样指向那个地方,所以进行操作也可以对其值进行修改。
数据类型
Java由于没有指针,所以全是引用,除了基本的数据类型,全是引用数据类型。
内置
1. 整数类型
用于存储整数值,包括正数、负数和零。
数据类型 |
大小(字节) |
取值范围 |
默认值 |
byte |
1 |
-128 到 127 |
0 |
short |
2 |
-32,768 到 32,767 |
0 |
int |
4 |
-2³¹ 到 2³¹-1(约 -2.1亿 到 2.1亿) |
0 |
long |
8 |
-2⁶³ 到 2⁶³-1 |
0L |
long在用数字表示的时候后面一定要加L,如123456789123L,否则视作int
2. 浮点类型
用于存储带小数部分的数值。
数据类型 |
大小(字节) |
取值范围 |
默认值 |
float |
4 |
约 ±3.4e-38 到 ±3.4e38 |
0.0f |
double |
8 |
约 ±1.7e-308 到 ±1.7e308 |
0.0d |
3. 字符类型
用于存储单个字符。
数据类型 |
大小(字节) |
取值范围 |
默认值 |
char |
2 |
0 到 65,535(Unicode 字符) |
‘\u0000’ |
4. 布尔类型
用于存储逻辑值,只有两个可能的值:true
或 false
。
不可以0或非0的整数替代true和false ,boolean不能与整数类型相互转换。
数据类型 |
大小(字节) |
取值范围 |
默认值 |
boolean |
1(实际大小取决于 JVM 实现,可能被优化为 1 位) |
true 或 false |
false |
引用
类,接口,数组都是引用类型。
1
2
|
Person p = new Person(); // Person的实例在堆区,p在栈区,p指向它
Person p2 = p; // 复制的是引用
|
数组
支持两种写法
1
2
3
4
5
6
7
8
9
10
11
12
13
|
数据类型[] 数组名; // 推荐写法
或
数据类型 数组名[]; // 不推荐,兼容 C/C++ 风格
int[] numbers; // 声明一个整型数组
String[] names; // 声明一个字符串数组
int[] numbers = {1, 2, 3, 4, 5}; // 静态初始化
String[] names = {"Alice", "Bob", "Charlie"};
int[] numbers = new int[5]; // 动态初始化,长度为 5
String[] names = new String[3]; // 动态初始化,长度为 3
//动态初始化时,数组的元素会被赋予默认值
|
可以通过 数组名.length
获取数组的长度。
遍历数组,一种是下标,一种是类似python的for i in xxx,和c++11之后的for auto i : xxx那样的,但是是只读的。
1
2
3
4
5
6
7
8
|
int[] numbers = {10, 20, 30, 40, 50};
for (int i = 0; i < numbers.length; i++) {
System.out.println(numbers[i]);
}
for (int num : numbers) {
System.out.println(num);
}
|
二维数组
1
2
3
4
5
6
|
int[][] matrix = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; // 静态初始化
System.out.println(matrix[1][2]); // 输出: 6
int[][] matrix2 = new int[3][3]; // 动态初始化
matrix2[0][0] = 1;
matrix2[1][1] = 5;
|
以下是和C++不同的不规则数组(锯齿数组)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
int[][] t = new int[3][]; // 声明一个二维数组,第一维长度为 3,第二维长度未定
t[0] = new int[2]; // 第一行长度为 2
t[1] = new int[3]; // 第二行长度为 3
t[2] = new int[4]; // 第三行长度为 4
// 访问
for (int i = 0; i < t.length; i++) {
for (int j = 0; j < t[i].length; j++) {
System.out.print(t[i][j] + " ");
}
System.out.println();
}
// 所以下面的C++写法在Java是不行的
// int[][] t = new int[][3];
|
数组拷贝
1
2
3
4
5
6
7
|
public static void arraycopy(
Object src, // 源数组
int srcPos, // 源数组的起始位置
Object dest, // 目标数组
int destPos, // 目标数组的起始位置
int length // 要复制的元素个数
)
|
System.arraycopy是最高效的!
流程控制
同c++
switch也需要break
还有我的大括号不换行的码风是Java毋庸置疑的正统!
变量常量
1
|
type identifier [ = value][, identifier [= value] ...] ;
|
- type – 数据类型。
- identifier – 是变量名,可以使用逗号 , 隔开来声明多个同类型变量。
按Java惯例, 类名首字母用大写( Pascal)
其余的(包名、方法名、变量名) 首字母都小写(camel)
静态 static 常量 final 还可以组合 static final 数据类型 常量名 = 值;
类和接口
虽然说你要是用C++那一套也都能听懂,但是吧人家都有翻译规范了就规范点吧。
以下是Java类相关的术语中英文对照:
对照
-
类 (Class)
-
对象 (Object)
-
实例 (Instance)
-
属性/域/字段 (Attribute/Field)
-
方法 (Method)
-
构造函数 (Constructor)
-
继承 (Inheritance)
-
父类/超类 (Superclass/Parent Class)
-
子类 (Subclass/Child Class)
-
封装 (Encapsulation)
-
多态 (Polymorphism)
-
抽象类 (Abstract Class)
-
接口 (Interface)
-
重载 (Overloading)
-
重写/覆盖 (Overriding)
-
访问修饰符 (Access Modifier)
- public
- private
- protected
- default (package-private)
-
静态 (Static)
-
final
-
this
-
super
-
包 (Package)
-
导入 (Import)
-
成员变量 (Member Variable)
-
局部变量 (Local Variable)
-
实例变量 (Instance Variable)
-
类变量 (Class Variable)
-
方法签名 (Method Signature)
-
参数 (Parameter)
-
返回值 (Return Value)
-
异常 (Exception)
-
泛型 (Generics)
-
注解 (Annotation)
-
枚举 (Enum)
-
内部类 (Inner Class)
-
匿名类 (Anonymous Class)
-
Lambda表达式 (Lambda Expression)
-
流 (Stream)
-
集合 (Collection)
-
数组 (Array)
-
反射 (Reflection)
-
序列化 (Serialization)
-
反序列化 (Deserialization)
-
线程 (Thread)
-
同步 (Synchronization)
-
异步 (Asynchronous)
类
1
2
3
4
5
6
7
8
9
10
11
12
|
[public] [abstract|final] class className [extends superClass]
[implements InterfaceNameList] { // 类声明
[public | protected | private] [static] [final] [transient] [volatile] type variableName;
// 成员变量声明,可为多个
[public | protected | private] [static] [final | abstract] [native] [synchronized] returnType methodName ( [paramList] )
// 方法定义及实现,可为多个
[throws exceptionList] {
statements
}
}
|
1. 访问修饰符和类修饰符
-
public
:表示该类可以被任何其他类访问。如果省略,则默认为包级私有(仅在同一个包内可见)。
-
abstract
:表示这是一个抽象类,不能被实例化,只能被继承。抽象类可以包含抽象方法和具体方法。
-
final
:表示这是一个最终类,不能被继承。所有方法默认也是final
的,不能被子类重写。
2. 类声明
-
class className
:定义一个名为className
的类。
-
extends superClass
:表示当前类继承自superClass
。一个类只能继承一个父类。
-
implements InterfaceNameList
:表示当前类实现了InterfaceNameList
中列出的一个或多个接口。一个类可以实现多个接口,接口之间用逗号分隔。
3. 成员变量声明
4. 方法声明与定义
1
2
3
|
[returnType] methodName ( [paramList] ) [throws exceptionList] {
statements
}
|
-
修饰符:
public
、protected
、private
:控制方法的访问权限。
static
:表示该方法属于类本身,可以通过类名直接调用。
final
:表示该方法不能被子类重写。
abstract
:表示该方法没有实现,必须在子类中被实现(仅适用于抽象类)。
native
:表示该方法的实现由本地代码(如C/C++)提供。
synchronized
:表示该方法在同一时间只能被一个线程访问,用于线程同步。
-
returnType
:方法的返回类型,如void
、int
、String
等。如果是void
,表示方法不返回任何值。
-
methodName
:方法的名称。
-
paramList
:方法的参数列表,多个参数用逗号分隔。例如:(int a, String b)
。
-
throws exceptionList
:声明方法可能抛出的异常类型,多个异常用逗号分隔。例如:throws IOException, SQLException
。
-
statements
:方法体内的代码块,包含具体的操作和逻辑。
示例
以下是一个结合上述元素的完整示例:
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
|
public final class Person implements Serializable, Cloneable {
private static final long serialVersionUID = 1L;
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public synchronized void setAge(int age) {
if(age >= 0){
this.age = age;
}
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
|
抽象类
如果父类的方法本身不需要实现任何功能,仅仅是为了定义方法签名,目的是让子类去覆写它,那么,可以把父类的方法声明为抽象方法。
抽象方法用abstract
修饰。因为无法执行抽象方法,因此这个类也必须申明为抽象类(abstract class)。
使用abstract
修饰的类就是抽象类。我们无法实例化一个抽象类。
所以抽象类只是单纯为了被继承。
多态
针对某个类型的方法调用,其真正执行的方法取决于运行时期实际类型的方法。
和cpp一样,派生类重载基类的方法时,是基类引用指向派生类,通过基类引用调用,然后根据其类型类动态调用。
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
|
class Animal {
public void speak() {
System.out.println("Animal speaks");
}
}
class Dog extends Animal {
@Override
public void speak() { // 重写父类方法
System.out.println("Dog barks");
}
}
class Cat extends Animal {
@Override
public void speak() { // 重写父类方法
System.out.println("Cat meows");
}
}
public class Main {
public static void main(String[] args) {
Animal animal; // 父类引用
animal = new Dog(); // 指向子类对象
animal.speak(); // 输出: Dog barks(动态调用Dog的speak)
animal = new Cat(); // 指向另一个子类对象
animal.speak(); // 输出: Cat meows(动态调用Cat的speak)
}
}
|
接口
接口其实就是某种程度上的抽象类,抽象的不能再抽象,但是它不再是具有相同特征的一类事物,而是类似一些事物具有相同的特点,我们再在子类中实现这些它的特性。侧重于行为的定义。
1
2
3
4
5
6
7
|
[public] interface InterfaceName [extends superInterfaceList] {
// 常量声明,可为多个
type CONSTANT_NAME = value;
// 方法声明,可为多个
returnType methodName([paramList]);
}
|
示例
1
2
3
4
5
6
7
8
9
10
11
|
public interface Animal {
String NAME = "Animal"; // 常量声明
void eat(); // 抽象方法声明
void sleep(int hours); // 另一个抽象方法声明
}
public interface Mammal extends Animal {
void giveBirth(); // 继承自Animal接口,并添加新的抽象方法
}
|
注意,在Java 8之前,接口中的所有方法都是抽象的(即没有方法体)。Java 8之后,可以有默认方法和静态方法。
三个默认方法
1
2
3
4
5
6
7
8
9
10
11
12
|
// (1) 构造方法,声明为:
className( [paramlist] ){
}
// (2) main( )方法,声明为:
public static void main ( String args[ ] ){
}
//3. finalize( )方法,声明为:
protected void finalize( ) throws throwable{
}
|
注意从Java9开始finalize
就被标记成弃用,Java18正式干掉它。
继承
子类:subclass
父类/超类:superclass
Java只支持单继承: 一个类只能有一个直接父类
在面向对象编程中,继承(Inheritance) 是代码复用和逻辑分层的重要机制。Java 的继承机制遵循以下核心要点:
🧬 继承的本质
- 子类(subclass) 继承 父类/超类(superclass) 的属性和方法(
private
成员除外)
- 子类可以 扩展(extend) 父类的功能,添加新属性和方法
- 子类可以 重写(override) 父类的方法(非
private
和非 final
的方法)
⚠️ Java 单继承限制
- 一个类只能有一个直接父类(通过
extends
关键字)
- 但可以通过接口(
implements
)实现多重继承的效果
- 继承关系形成树状层次结构(非网状结构)
1
2
3
4
5
6
|
// 单继承示例
class Animal { /* 父类 */ }
class Dog extends Animal { /* 子类 */ }
// 错误示例:尝试多继承
// class Cat extends Animal, Mammal {} // 编译错误
|
🔑 关键语法要素
-
extends
关键字:
1
|
class Subclass extends Superclass { ... }
|
-
super
关键字:
1
2
3
4
5
6
7
8
9
10
11
|
public class Dog extends Animal {
public Dog(String name) {
super(name); // 调用父类构造器
}
@Override
public void eat() {
super.eat(); // 调用父类方法
System.out.println("狗狗在啃骨头");
}
}
|
-
构造器调用规则:
- 子类构造器必须首先调用父类构造器
- 默认调用父类无参构造器(若父类没有无参构造器,必须显式调用)
📜 访问权限
Java的继承只有extends这一种方式,和cpp不同,访问权限只由父类的修饰符决定。
修饰符 |
同类访问 |
同包访问 |
子类访问(不同包) |
不同包非子类 |
public |
✅ |
✅ |
✅ |
✅ |
protected |
✅ |
✅ |
✅ |
❌ |
默认(无修饰符) |
✅ |
✅ |
❌ |
❌ |
private |
✅ |
❌ |
❌ |
❌ |
🧩 继承中的访问规则
1️⃣ public
成员
-
完全开放访问
-
子类可以直接访问父类的 public
成员
-
示例:
1
2
3
4
5
6
7
8
9
|
class Parent {
public String publicField = "Public";
}
class Child extends Parent {
void print() {
System.out.println(publicField); // 直接访问
}
}
|
2️⃣ protected
成员
3️⃣ 默认(包私有)成员
-
同包访问限制
-
只有相同包内的子类可以访问
-
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// Parent.java(包 com.example)
class Parent {
String packageField = "Package-private";
}
// ChildSamePackage.java(包 com.example)
public class ChildSamePackage extends Parent {
void print() {
System.out.println(packageField); // 同包子类可访问
}
}
// ChildDiffPackage.java(包 com.test)→ 无法访问
|
4️⃣ private
成员
🔧 继承中的构造方法
💡 关键注意事项
-
跨包继承时:
protected
成员在不同包子类中可以直接访问
- 但不同包的非子类不能访问
protected
成员
-
方法重写规则:
- 重写方法的访问权限不能 缩小(如父类方法是
public
,子类重写方法不能改为 protected
)
-
成员变量访问:
- 子类可以定义与父类同名的成员变量(但不推荐,会产生隐藏现象)
-
Java 没有 C++ 式的访问继承:
1
2
|
// Java 不支持这种写法!
// class Child extends private Parent {} // 非法语法
|
-
**构造方法是不能继承的 **
-
**super访问父类的域 **
- 通过super.{field_name}来访问
- 使用super可以访问被子类所隐藏了的同名变量。
- 当覆盖父类的同名方法的同时,又要调用父类的方法,就必须使用super。
反射
反射有什么用,目前是不知道的,但是先学着再说。最基本的比如说访问私有字段,私有方法,这个在mc fabric 1.21.3到1.21.4模组升级的时候ai给出过代码,原本public方法在麻将瞎更新后变成private了,这样直接通过反射暴力升级一波,反正也能用,虽然看作者代码最后是api变了,但是咱也不会写模组,能用就行。
然后就是典中典的Spring
依赖注入,所谓依赖注入看起来很高级,但是就是cpp当中聚合类,然后将其他的类作为参数传入构造函数而已,不过就是这样听起来很高级,然后通过一个叫做Anotation
的东西,Spring
就可以识别出这个类里面的字段,然后一顿赋值,就非常的省心。
再有就是测试,也是通过Anotation
来标记,然后使用反射来发现和执行测试方法。
原理
众所周知把Java运行起来需要两步,显示转换成字节码,然后才是变成机器码。反射是在字节码层面上实现的。
因为Java是一个纯度拉满的面向对象语言,所以一切皆对象,所有的对象都是从Object继承而来的,所有的类也是从Class继承而来的。
但是基本类型(int
, double
, char
等)不是对象,但它们的包装类(Integer
, Double
, Character
)是对象。
数组是对象,即使是 int[]
也是 Object
的子类。
1
2
3
4
5
6
7
8
9
10
11
|
Object
│
├── String
├── Integer
├── MyCustomClass
├── ...
└── Class // Class 本身也是 Object 的子类
│
├── Class<MyClass1> // 每个类的 Class 对象
├── Class<MyClass2>
└── ...
|
当 JVM 加载一个类(如 com.example.MyClass
)时:
-
类加载器(ClassLoader) 读取 .class
字节码文件。
-
解析字节码,在 方法区(Method Area) 存储类的元数据(字段、方法、父类、接口等)。
-
在 堆(Heap) 中生成一个 Class
对象,作为该类的运行时表示。
Class
对象 是反射的入口,它包含:
- 类名、修饰符(
public
/final
等)
- 字段(
Field
信息)
- 方法(
Method
信息)
- 构造器(
Constructor
信息)
- 注解(
Annotation
信息)
获取对象
类名.class
对象.getClass()
Class.forname("**全限定类名**(Fully Qualified Name)")
注 : 包含包名和类名的完整类名。
方式 |
Class.forName("全类名") |
类名.class |
对象.getClass() |
使用场景 |
动态加载类,常用于配置文件读取类名后加载 |
编译时已知类名,直接获取Class对象 |
已有对象实例,获取其Class对象 |
类是否加载 |
会执行类的静态代码块(初始化类) |
不会初始化类(静态代码块不执行) |
对象已存在,类肯定已加载并初始化 |
性能 |
相对较慢(需查找类路径并可能初始化类) |
最快(直接获取) |
快(对象已存在) |
适用性 |
最灵活,类名可作为字符串参数传递 |
需要编译时知道具体类 |
必须有对象实例 |
异常处理 |
需要处理ClassNotFoundException |
无检查异常 |
无检查异常 |
数组类型 |
不能直接获取数组类型的Class(需特殊处理) |
可以直接获取(如int[].class ) |
可以直接获取数组对象的Class |
基本类型 |
不能用于基本类型 |
可以用于基本类型(如int.class ) |
不能用于基本类型(需包装类对象) |
示例代码 |
Class cls = Class.forName("java.lang.String"); |
Class cls = String.class; |
String s = ""; Class cls = s.getClass(); |
下面提及一下int.class
, int.class
乍一看还是很抽象的,明明int就不是类为什么还会有class呢,其主要用途之一就是作为反射的一些方法的参数,如你的类有一个方法参数正好是int, 就可以getMethod("methodName", int.class)
,其存在还是有一定必要性的.
方法
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
|
// 获取 Class 对象
Class.forName(String className)
Object.getClass()
.class 语法 (如 String.class)
// 获取类信息
String getName()
String getSimpleName()
String getCanonicalName()
Class<? super T> getSuperclass()
Class<?>[] getInterfaces()
int getModifiers() // 配合 Modifier 类使用
Package getPackage()
// 创建实例
T newInstance() // 已废弃,Java 9+
Constructor<T> getConstructor(Class<?>... parameterTypes)
Constructor<?>[] getConstructors()
// 获取字段
Field getField(String name)
Field[] getFields() // 仅公共字段
Field getDeclaredField(String name)
Field[] getDeclaredFields() // 所有字段
// 获取方法
Method getMethod(String name, Class<?>... parameterTypes)
Method[] getMethods() // 包括继承的公共方法
Method getDeclaredMethod(String name, Class<?>... parameterTypes)
Method[] getDeclaredMethods() // 仅本类声明的方法
// 获取接口
boolean isInterface() // 判断是否为接口
Class<?>[] getInterfaces() // 获取类实现的所有接口(对于接口则返回其扩展的接口)
default Method[] getDefaultMethods() // 获取接口的默认方法(Java 8+)
// 获取注解
<A extends Annotation> A getAnnotation(Class<A> annotationClass) // 获取指定类型的注解
Annotation[] getAnnotations() // 获取所有注解
Annotation[] getDeclaredAnnotations() // 获取直接声明的注解(不包括继承的)
boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) // 检查是否存在指定类型的注解
// 其他
boolean isArray()
boolean isEnum()
boolean isInterface()
boolean isPrimitive()
Class<?> getComponentType() // 数组类型)
|
最后补充一下想到的一个问题,怎么获得父类的父类甚至祖祖祖祖祖爷爷的私有方法?其实就是一直getSuperClass
到为null
就可以.
例子
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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
|
import java.lang.annotation.*;
import java.lang.reflect.*;
import java.util.Arrays;
// 定义一个自定义注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@interface MyAnnotation {
String value() default "default";
int version() default 1;
}
// 定义一个接口
interface MyInterface {
void interfaceMethod();
}
// 父类
class ParentClass {
private String parentPrivateField = "parent private";
protected String parentProtectedField = "parent protected";
public void parentPublicMethod() {
System.out.println("Parent public method");
}
}
// 子类,实现接口并使用注解
@MyAnnotation(value = "ClassAnnotation", version = 2)
class ChildClass extends ParentClass implements MyInterface {
private String privateField = "private value";
public String publicField = "public value";
@MyAnnotation("FieldAnnotation")
public String annotatedField = "annotated field";
public ChildClass() {}
public ChildClass(String privateField) {
this.privateField = privateField;
}
@Override
public void interfaceMethod() {
System.out.println("Implemented interface method");
}
@MyAnnotation("MethodAnnotation")
public void publicMethod() {
System.out.println("Public method");
}
private void privateMethod() {
System.out.println("Private method: " + privateField);
}
public void methodWithArgs(String arg1, int arg2) {
System.out.println("Method with args: " + arg1 + ", " + arg2);
}
// 静态方法
public static void staticMethod() {
System.out.println("Static method");
}
}
public class ReflectionExample {
public static void main(String[] args) throws Exception {
// 1. 获取Class对象的几种方式
Class<?> clazz1 = ChildClass.class;
Class<?> clazz2 = Class.forName("ChildClass");
ChildClass obj = new ChildClass();
Class<?> clazz3 = obj.getClass();
System.out.println("Class name: " + clazz1.getName());
System.out.println("Simple name: " + clazz1.getSimpleName());
System.out.println("Canonical name: " + clazz1.getCanonicalName());
// 2. 获取父类和接口
System.out.println("\nSuperclass: " + clazz1.getSuperclass().getName());
System.out.println("Interfaces: " + Arrays.toString(clazz1.getInterfaces()));
// 3. 注解操作
System.out.println("\nClass annotations:");
Annotation[] annotations = clazz1.getAnnotations();
for (Annotation annotation : annotations) {
if (annotation instanceof MyAnnotation) {
MyAnnotation myAnnotation = (MyAnnotation) annotation;
System.out.println("MyAnnotation value: " + myAnnotation.value() +
", version: " + myAnnotation.version());
}
}
// 检查类是否有特定注解
boolean hasAnnotation = clazz1.isAnnotationPresent(MyAnnotation.class);
System.out.println("Has MyAnnotation: " + hasAnnotation);
// 4. 字段操作
System.out.println("\nFields:");
// 获取所有public字段(包括继承的)
Field[] publicFields = clazz1.getFields();
System.out.println("Public fields: " + Arrays.toString(publicFields));
// 获取所有声明的字段(不包括继承的)
Field[] declaredFields = clazz1.getDeclaredFields();
System.out.println("Declared fields: " + Arrays.toString(declaredFields));
// 获取特定字段并访问值
Field publicField = clazz1.getField("publicField");
System.out.println("Public field value: " + publicField.get(obj));
// 访问私有字段
Field privateField = clazz1.getDeclaredField("privateField");
privateField.setAccessible(true); // 设置可访问
System.out.println("Private field value: " + privateField.get(obj));
// 修改字段值
publicField.set(obj, "new public value");
System.out.println("Modified public field value: " + publicField.get(obj));
// 获取字段上的注解
Field annotatedField = clazz1.getField("annotatedField");
MyAnnotation fieldAnnotation = annotatedField.getAnnotation(MyAnnotation.class);
System.out.println("Field annotation value: " + fieldAnnotation.value());
// 5. 方法操作
System.out.println("\nMethods:");
// 获取所有public方法(包括继承的)
Method[] publicMethods = clazz1.getMethods();
System.out.println("Public methods: " + Arrays.toString(publicMethods));
// 获取所有声明的方法(不包括继承的)
Method[] declaredMethods = clazz1.getDeclaredMethods();
System.out.println("Declared methods: " + Arrays.toString(declaredMethods));
// 调用public方法
Method publicMethod = clazz1.getMethod("publicMethod");
publicMethod.invoke(obj);
// 调用private方法
Method privateMethod = clazz1.getDeclaredMethod("privateMethod");
privateMethod.setAccessible(true);
privateMethod.invoke(obj);
// 调用带参数的方法
Method methodWithArgs = clazz1.getMethod("methodWithArgs", String.class, int.class);
methodWithArgs.invoke(obj, "test", 123);
// 调用静态方法
Method staticMethod = clazz1.getMethod("staticMethod");
staticMethod.invoke(null); // 静态方法可以传null作为对象
// 获取方法上的注解
MyAnnotation methodAnnotation = publicMethod.getAnnotation(MyAnnotation.class);
System.out.println("Method annotation value: " + methodAnnotation.value());
// 6. 构造器操作
System.out.println("\nConstructors:");
// 获取所有public构造器
Constructor<?>[] publicConstructors = clazz1.getConstructors();
System.out.println("Public constructors: " + Arrays.toString(publicConstructors));
// 获取所有声明的构造器
Constructor<?>[] declaredConstructors = clazz1.getDeclaredConstructors();
System.out.println("Declared constructors: " + Arrays.toString(declaredConstructors));
// 使用反射创建对象
Constructor<?> noArgConstructor = clazz1.getConstructor();
Object newObj1 = noArgConstructor.newInstance();
System.out.println("Created object with no-arg constructor: " + newObj1);
Constructor<?> stringArgConstructor = clazz1.getConstructor(String.class);
Object newObj2 = stringArgConstructor.newInstance("constructor value");
System.out.println("Created object with String arg constructor: " + newObj2);
// 7. 其他反射操作
System.out.println("\nOther operations:");
// 检查修饰符
int modifiers = clazz1.getModifiers();
System.out.println("Is public: " + Modifier.isPublic(modifiers));
System.out.println("Is abstract: " + Modifier.isAbstract(modifiers));
// 检查数组类型
System.out.println("Is array: " + clazz1.isArray());
// 创建数组实例
Object array = Array.newInstance(String.class, 3);
Array.set(array, 0, "first");
Array.set(array, 1, "second");
System.out.println("Array length: " + Array.getLength(array));
System.out.println("Array[1]: " + Array.get(array, 1));
// 8. 访问父类的成员
System.out.println("\nAccessing parent members:");
Class<?> parentClass = clazz1.getSuperclass();
Field parentProtectedField = parentClass.getDeclaredField("parentProtectedField");
System.out.println("Parent protected field: " + parentProtectedField.get(obj));
try {
// 尝试访问父类私有字段
Field parentPrivateField = parentClass.getDeclaredField("parentPrivateField");
parentPrivateField.setAccessible(true);
System.out.println("Parent private field: " + parentPrivateField.get(obj));
} catch (Exception e) {
System.out.println("Cannot access parent private field: " + e.getMessage());
}
}
}
|
注解
注释是写给人看的,那么注解就是写给编译器看的。
Java一共有三类注解:
一、内置注解
- @Override - 表示方法覆盖了父类的方法
- @Deprecated - 表示元素已过时,不推荐使用
- @SuppressWarnings - 抑制编译器警告
- @SafeVarargs - 表示方法不会对其可变参数执行不安全的操作
- @FunctionalInterface - 表示接口是函数式接口(Java 8)
对于一些注解他们不会进入到class文件中,编译之后就会被抛弃,这取决于它们的 @Retention
策略。
SOURCE
级别注解(如 @Override
、@SuppressWarnings
):
- 仅在 编译阶段 有效,编译后不会进入
.class
文件。
- 主要用于 静态检查(如防止拼写错误、抑制警告)。
RUNTIME
级别注解(如 @Deprecated
、@SafeVarargs
、@FunctionalInterface
):
- 会保留到 运行时,可以通过 反射 读取。
- 常用于 框架(如 Spring、JUnit)和 运行时动态处理。
CLASS
级别注解(较少见):
- 会进入
.class
文件,但 运行时不可见(除非使用字节码工具如 ASM)
二、元注解
元注解是指用于注解其他注解的注解,如上面的@Retention
其实就是。
- @Target - 指定注解可以应用的位置
- @Retention - 指定注解的保留策略
- @Documented - 表示注解应该包含在Javadoc中
- @Inherited - 表示注解可以被继承
- @Repeatable (Java 8+) - 表示注解可以重复应用在同一元素上
三、自定义注解
定义注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
import java.lang.annotation.*;
// 自定义注解
// 元注解部分
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
// 自己的部分
public @interface CodeInfo {
String author();
String date();
int version() default 1; // 有默认值
}
// 使用注解
public class MyClass {
@CodeInfo(author = "Alice", date = "2023-10-01", version = 2)
public void myMethod() {
System.out.println("Hello, Annotation!");
}
}
|
元注解
下面展开自定义注解中用得到的元注解部分
-
**@Target
**用于指定注解作用的位置
参数:ElementType[] value()
,指定目标元素类型数组
只有一个参数的例子:
@Target(ElementType.METHOD)
多个作用位置:
1
2
3
4
|
@Target({
ElementType.METHOD,
ElementType.FIELD
})
|
1
2
3
4
5
6
7
8
|
ElementType.TYPE:能修饰类、接口或枚举类型
ElementType.FIELD:能修饰成员变量
ElementType.METHOD:能修饰方法
ElementType.PARAMETER:能修饰参数
ElementType.CONSTRUCTOR:能修饰构造器
ElementType.LOCAL_VARIABLE:能修饰局部变量
ElementType.ANNOTATION_TYPE:能修饰注解
ElementType.PACKAGE:能修饰包
|
-
**@Retention
**指定注解的保留策略
参数:RetentionPolicy value()
RetentionPolicy 取值:
SOURCE
:仅在源代码中保留,编译时被丢弃
CLASS
:在class文件中保留,但运行时不可获取(默认值)
RUNTIME
:在class文件中保留,运行时可通过反射获取
-
**@Inherited
**表示注解可以继承
如果一个类使用了带有@Inherited
的注解,那么它的子类将自动继承该注解。
注意:
- 只对类注解(
@Target(ElementType.TYPE)
)有效
- 对接口或方法注解无效
-
**@Repeatable
**表示注解可重复定义
@Repeatable
表示注解可以在同一个元素上重复使用。需要定义一个容器注解来存储重复的注解。
参数:Class<? extends Annotation> value()
,指定容器注解类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// 定义可重复注解
@Repeatable(MyAnnotations.class)
public @interface MyAnnotation {
String value();
}
// 定义容器注解
public @interface MyAnnotations {
MyAnnotation[] value();
}
// 使用方式
@MyAnnotation("foo")
@MyAnnotation("bar")
public class MyClass {
// ...
}
|
注解使用
注解和反射密不可分,具体的method前面的反射中已经提及过了。无非就是如果这个方法或者域被注解了,然后我就对其进行一些判断和操作。
综合案例
注解
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
package com.itranswarp.learnjava;
import java.lang.annotation.ElementType;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Range {
int min() default 0;
int max() default 255;
}
|
类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
package com.itranswarp.learnjava;
public class Person {
@Range(min = 1, max = 20)
public String name;
@Range(max = 10)
public String city;
@Range(min = 1, max = 100)
public int age;
public Person(String name, String city, int age) {
this.name = name;
this.city = city;
this.age = age;
}
@Override
public String toString() {
return String.format("{Person: name=%s, city=%s, age=%d}", name, city, age);
}
}
|
主函数
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
|
package com.itranswarp.learnjava;
import java.lang.reflect.Field;
/**
* Learn Java from https://www.liaoxuefeng.com/
*
* @author liaoxuefeng
*/
public class Main {
public static void main(String[] args) throws Exception {
Person p1 = new Person("Bob", "Beijing", 20);
Person p2 = new Person("", "Shanghai", 20);
Person p3 = new Person("Alice", "Shanghai", 199);
for (Person p : new Person[] { p1, p2, p3 }) {
try {
check(p);
System.out.println("Person " + p + " checked ok.");
} catch (IllegalArgumentException e) {
System.out.println("Person " + p + " checked failed: " + e);
}
}
}
static void check(Person person) throws IllegalArgumentException, ReflectiveOperationException {
for (Field field : person.getClass().getFields()) {
Range range = field.getAnnotation(Range.class);
if (range != null) {
Object value = field.get(person);
if (value instanceof String) {
String str = (String) value;
if (str.length() < range.min() || str.length() > range.max()) {
throw new IllegalArgumentException("Field " + field.getName() + " out of range: " + str);
}
} else if (value instanceof Integer) {
int intValue = (Integer) value;
if (intValue < range.min() || intValue > range.max()) {
throw new IllegalArgumentException("Field " + field.getName() + " out of range: " + intValue);
}
}
}
}
}
}
|
泛型
泛型对应cpp当中的模板。相应的vector
对应java里面的ArrayList
,先简单看一下其使用方法。
1
2
3
4
5
6
7
|
ArrayList<String> fruits = new ArrayList<>();
// Add elements
fruits.add("Apple");
fruits.add("Banana");
fruits.add("Orange");
// 整数型
ArrayList<Integer> numbers = new ArrayList<>(100); // initial capacity 100
|
泛型类
单参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
public class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() { ... }
public T getLast() { ... }
/** 写法1
public static <T> Pair<T> create(T first, T last) {
return new Pair<T>(first, last);
}
**/
public static <TT> Pair<TT> create(TT first, TT last) {
return new Pair<TT>(first, last);
}
}
|
注意:这里的静态方法写法1是合理的,也是能编译通过的,但是和这个类的T是毫无关系的,等效于下面的写法。所以静态方法最好是要独立地写出。
多参数
1
2
3
4
5
6
7
8
9
10
11
12
|
public class Pair<T, K> {
private T first;
private K last;
public Pair(T first, K last) {
this.first = first;
this.last = last;
}
public T getFirst() { ... }
public K getLast() { ... }
}
// use
Pair<String, Integer> p = new Pair<>("test", 123);
|
实现原理和局限
Java 的泛型是通过 类型擦除(Type Erasure) 实现的,这意味着泛型信息仅在编译时存在,而在运行时会被擦除。所以折腾半天其实和Object
直接实现没什么区别,只是变得更安全。
泛型信息在编译后被擦除,替换为 上界类型(通常是 Object
或指定的边界)。这也就导致运行时,通过对于不同的<T>
反射getClass
,返回的也是相同的。
特性 |
使用 Object |
使用泛型(List<T> ) |
类型安全 |
❌ 需手动检查,可能 ClassCastException |
✅ 编译时检查,避免运行时错误 |
代码可读性 |
❌ 类型不明确,维护困难 |
✅ 类型清晰,增强可读性 |
编译时检查 |
❌ 运行时才暴露错误 |
✅ 编译时发现错误 |
强制转换 |
❌ 每次取值都要手动转换 |
✅ 自动转换,减少冗余代码 |
泛型方法 |
❌ 返回 Object ,需手动转换 |
✅ 自动推断类型 |
继承与多态 |
❌ 需手动检查类型 |
✅ 编译器生成桥接方法 |
存在下列问题
局限性 |
问题描述 |
替代方案 |
运行时类型擦除 |
无法直接获取 T 的实际类型 |
传递 Class<T> 或反射 |
不能 new T() |
无法实例化泛型类型 |
反射或工厂模式 |
不能 new T[] |
无法创建泛型数组 |
Array.newInstance() |
不支持基本类型 |
List<int> 不合法 |
使用包装类(Integer ) |
不能继承 Throwable |
泛型异常类不可行 |
非泛型异常 |
方法重载冲突 |
类型擦除导致签名相同 |
修改方法名或参数 |
静态变量限制 |
静态变量不能依赖泛型 |
使用独立泛型方法 |
泛型不变性 |
List<String> 不是 List<Object> |
通配符(? extends / ? super ) |
通配符
extends
首先回顾一下,子类可以转换成父类,因为父类有的子类一定也有,然后Number
是Integer
的父类.
然后要明确这个关键字的目的,加入一个泛型方法,其参数是泛型的Number
按照常理,如果我们传入一个Integer
应该是合法的,但是这确实是无法编译的,所以这是应该被解决的。
注意Pair<? extends Number> p
这个参数可以实现上面的功能。Pair<Number> p
则无法通过编译。
这种使用<? extends Number>
的泛型定义称之为上界通配符(Upper Bounds Wildcards),即把泛型类型T
的上界限定在Number
了。
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
|
public class Main {
public static void main(String[] args) {
Pair<Integer> p = new Pair<>(123, 456);
int n = add(p);
System.out.println(n);
}
static int add(Pair<? extends Number> p) {
Number first = p.getFirst();
Number last = p.getLast();
return first.intValue() + last.intValue();
}
}
class Pair<T> {
private T first;
private T last;
public Pair(T first, T last) {
this.first = first;
this.last = last;
}
public T getFirst() {
return first;
}
public T getLast() {
return last;
}
public void setFirst(T first) {
this.first = first;
}
public void setLast(T last) {
this.last = last;
}
}
|
然而这就完美了吗?并非如此。Pair同上省略
1
2
3
4
5
6
7
8
9
10
11
12
|
public class Main {
public static void main(String[] args) {
Pair<Integer> p = new Pair<>(123, 456);
add(p);
}
static void add(Pair<? extends Number> p) {
Number first = p.getFirst();
Number last = p.getLast();
p.setFirst(new Integer(first.intValue() + 100));
p.setLast(new Integer(last.intValue() + 100));
}
}
|
这样也是会报错的。
? extends Number
是只读的,适合读取数据,但不能写入(编译器无法保证类型安全)。
- 如果需要修改数据,应该避免使用通配符,改用明确的泛型类型或返回新对象。
注意上面这个只读并不一定是坏事,也可以用来保证安全。
上面是对于参数的传入来讲的。对于类来讲,也可应通过这个来限制类的示例类型。如:
1
|
public class Pair<T extends Number> { ... }
|
这样泛型类型限定为Number
以及Number
的子类。
super
和extends
相反,这个是确定下界的(包含)—— 下界限定符。
特性 |
? extends T (上界) |
? super T (下界) |
读取 |
可以安全读取为 T |
只能当作 Object |
写入 |
不能写入(类型不确定) |
可以写入 T 或其子类 |
适用场景 |
生产者(Producer) |
消费者(Consumer) |
示例 |
List<? extends Number> |
List<? super Integer> |
Pair
的定义同上一条, 下面的代码时合法的
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public class Main {
public static void main(String[] args) {
Pair<Number> p1 = new Pair<>(12.3, 4.56);
Pair<Integer> p2 = new Pair<>(123, 456);
setSame(p1, 100);
setSame(p2, 200);
System.out.println(p1.getFirst() + ", " + p1.getLast());
System.out.println(p2.getFirst() + ", " + p2.getLast());
}
static void setSame(Pair<? super Integer> p, Integer n) {
p.setFirst(n);
p.setLast(n);
}
}
|
遵循 PECS 原则:
- 需要读取数据 → 用
? extends T
。
- 需要写入数据 → 用
? super T
。
如果既要读又要写,直接用明确的泛型类型(如 <T>
),不要用通配符。
无限定通配符
到这里,应该对反射的Class<?>
的写法有所理解了。
无限定通配符 <?>
表示泛型类型可以是任意类型,类似于 <? extends Object>
(因为所有类都继承自 Object
)。它主要用于不关心具体类型的场景,比如:
- 只调用与泛型无关的方法(如
List.size()
)。
- 读取数据时当作
Object
处理(因为类型未知)。
- 适用于泛型类的非泛型方法。
通配符类型 |
用途 |
读取 |
写入 |
<?> |
任意类型(只读或非泛型操作) |
Object |
❌ 不允许 |
<? extends T> |
安全读取 T 或其子类 |
T |
❌ 不允许 |
<? super T> |
安全写入 T 或其父类 |
Object |
T 或其子类 |
安全事项
一、Java 反射 API 广泛使用了泛型来提供类型安全:
1
2
3
4
5
6
7
8
9
10
|
// 类型安全的 Class 使用
Class<String> stringClass = String.class;
String str = stringClass.newInstance(); // 无需强制转换
// getSuperclass() 返回 Class<? super T>
Class<? super String> superClass = String.class.getSuperclass();
// Constructor 也是泛型的
Constructor<Integer> intConstructor = Integer.class.getConstructor(int.class);
Integer i = intConstructor.newInstance(123); // 类型安全
|
二、Java 不允许直接创建泛型数组,这是由类型擦除机制决定的: Java 在运行时无法知道 T
的具体类型,数组需要在创建时知道其确切的组件类型。
- 使用 Object 数组然后强制转型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
@SuppressWarnings("unchecked")
public class GenericArray<T> {
private T[] array;
public GenericArray(int size) {
array = (T[]) new Object[size]; // 创建 Object 数组然后转型
}
public void set(int index, T item) {
array[index] = item;
}
public T get(int index) {
return array[index];
}
public T[] getArray() {
return array;
}
}
|
注意:这种方法在从 getArray()
返回数组时可能会引发 ClassCastException
,因为实际类型是 Object[]
。
错误示范:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
public class Main {
public static void main(String[] args) {
String[] arr = asArray("one", "two", "three");
System.out.println(Arrays.toString(arr));
// ClassCastException:
String[] firstTwo = pickTwo("one", "two", "three");
System.out.println(Arrays.toString(firstTwo));
}
static <K> K[] pickTwo(K k1, K k2, K k3) {
return asArray(k1, k2);
}
static <T> T[] asArray(T... objs) {
return objs;
}
}
|
解释:
-
可变参数的泛型数组问题:当可变参数与泛型结合时,Java实际上会创建一个泛型数组。但由于Java泛型的类型擦除,运行时无法知道确切的类型信息。
-
类型擦除的影响:在pickTwo
方法中,K
的类型被擦除为Object
,所以asArray(k1, k2)
实际上创建的是一个Object[]
数组,而不是String[]
数组。
-
隐式类型转换失败:当尝试将这个Object[]
赋值给String[] firstTwo
时,Java需要进行隐式类型转换,但由于数组的协变性,这种转换在运行时失败,抛出ClassCastException
。
-
使用 Array.newInstance() 反射创建数组
1
2
3
4
5
6
7
8
9
10
|
public class GenericArrayWithReflection<T> {
private T[] array;
@SuppressWarnings("unchecked")
public GenericArrayWithReflection(Class<T> type, int size) {
array = (T[]) Array.newInstance(type, size); // 使用反射创建数组
}
// 其他方法同上
}
|
使用示例:
1
2
|
GenericArrayWithReflection<String> stringArray =
new GenericArrayWithReflection<String>(String.class, 10);
|
- 使用 List 替代数组
1
|
List<T> list = new ArrayList<T>();
|
这是最推荐的方案,因为:
- 避免了数组的所有问题
- 提供了更多的灵活性
- 是类型安全的
IO
目前先学同步IO, java.io包里面的
文件
Java提供了一个File
对象来操作文件和目录
构造形如下面, 路径既可以是绝对路径也可以是相对路径, 注意windows的路径需要\\
转译. 对于不同的平台可以通过File.separator
来输出\或者/,但是统一用/
就可以。
1
2
3
4
5
6
7
8
|
File(String pathname) // 通过路径名创建File对象
File(String parent, String child) // 通过父路径和子路径创建
File(File parent, String child) // 从父抽象路径名和子路径名字符串创建新实例
File file1 = new File("D:/example/1.txt");
File file2 = new File("D:/example", "1.txt");
File file3 = new File("D:/example");
File file4 = new File(file3, "1.txt");
|
常用方法(都是File对象的方法,所以是.调用哦)
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
|
boolean exists() // 判断文件或目录是否存在
boolean isFile() // 判断是否是文件
boolean isDirectory() // 判断是否是目录
String getName() // 获取文件或目录名称
String getPath() // 获取路径字符串
String getAbsolutePath() // 获取绝对路径
String getCanonicalPath() // 获取规范路径
long length() // 获取文件大小(字节数)
boolean canRead() // 是否可读
boolean canWrite() // 是否可写
boolean canExecute() //是否可执行
long lastModified() // 获取最后修改时间(毫秒值)
boolean createNewFile() // 创建新文件
boolean mkdir() // 创建单级目录
boolean mkdirs() // 创建多级目录
boolean delete() // 删除文件或空目录(注意目录必须为空)
boolean renameTo(File dest) // 重命名或移动文件
// 临时文件写法
File f = File.createTempFile("tmp", ".txt"); // 提供临时文件的前缀和后缀
f.deleteOnExit(); // JVM退出时自动删除
|
对于Path表示路径时,可以遍历目录文件和子目录
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
|
import java.io.*;
public class Main {
public static void main(String[] args) throws IOException {
File f = new File("C:\\Windows");
File[] fs1 = f.listFiles(); // 列出所有文件和子目录
printFiles(fs1);
File[] fs2 = f.listFiles(new FilenameFilter() { // 仅列出.exe文件
public boolean accept(File dir, String name) {
return name.endsWith(".exe"); // 返回true表示接受该文件
}
});
printFiles(fs2);
}
static void printFiles(File[] files) {
System.out.println("==========");
if (files != null) {
for (File f : files) {
System.out.println(f);
}
}
System.out.println("==========");
}
}
|
对于一些配置文件、资源文件我们可以把它们放在classpath下,它们的路径表示与读取如下:
1
2
3
4
5
6
7
8
9
10
|
try (InputStream is = getClass().getResourceAsStream("/template.html")) {
// 这个getClass是this.getClass()已经存在实例的情况下进行调用的
if (is == null) {
throw new FileNotFoundException("资源文件未找到");
}
String content = new String(is.readAllBytes(), StandardCharsets.UTF_8);
// 使用文件内容...
} catch (IOException e) {
e.printStackTrace();
}
|
流
流就是数据的一部分,流式处理就是来一点处理一点。
根据数据流方向的不同分为:输入流、输出流。根据处理的数据单位不同可分为:字节流、字符流。
|
字节流 |
字符流 |
输入流 |
InputStream |
Reader |
输出流 |
OutputStream |
Writer |
字节流
输入流
InputStream
是Java提供的最基本的输入流,它甚至只是一个抽象类。所以我们常用的是FileStream
1
2
3
4
5
6
7
8
9
10
11
12
|
public void readFile() throws IOException {
InputStream input = null;
try {
input = new FileInputStream("readme.txt");
int n;
while ((n = input.read()) != -1) { // 利用while同时读取并判断
System.out.println(n);
}
} finally {
if (input != null) { input.close(); }
}
}
|
当然还可以写的现代一些Java7+,编译器看try(resource = ...)
中的对象是否实现了java.lang.AutoCloseable
接口,如果实现了,就自动加上finally
语句并调用close()
方法。
1
2
3
4
5
6
7
8
|
public void readFile() throws IOException {
try (InputStream input = new FileInputStream("readme.txt")) {
int n;
while ((n = input.read()) != -1) {
System.out.println(n);
}
}
}
|
当然也可以一次性多读一些,read是可以有参数的,完全形态是int read(byte[] b, int off, int len)
1
2
3
4
5
6
7
8
|
File file = new File("1.txt");
try (InputStream inputStream = new FileInputStream(file)) {
int size = inputStream.available();
byte[] array = new byte[size];
inputStream.read(array);
result = new String(array);
}
|
还有其他的实现类ByteArrayInputStream
用于Byte数组转Stream可以用来测试
1
2
3
4
5
6
7
8
9
|
public static void main(String[] args) throws IOException {
byte[] data = { 72, 101, 108, 108, 111, 33 };
try (InputStream input = new ByteArrayInputStream(data)) {
int n;
while ((n = input.read()) != -1) {
System.out.println((char)n);
}
}
}
|
输出流
和输入流对应,同样的最基本的抽象类是OutputStream
常见的实现类也是FileOutputStream
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
OutputStream output = new FileOutputStream("readme.txt");
output.write(72); // H
output.write(101); // e
output.write(108); // l
output.write(108); // l
output.write(111); // o
output.close();
// 多个
public void writeFile() throws IOException {
OutputStream output = new FileOutputStream("readme.txt");
output.write("Hello".getBytes("UTF-8")); // Hello
output.close();
}
// 自动关闭 原理同输入
public void writeFile() throws IOException {
try (OutputStream output = new FileOutputStream("readme.txt")) {
output.write("Hello".getBytes("UTF-8")); // Hello
} // 编译器在此自动为我们写入finally并调用close()
}
|
还有ByteArrayOutputStream
,配合前面的相当于Byte[]
和Stream
互相转换圆满了。
1
2
3
4
5
6
7
8
9
10
11
|
public class Main {
public static void main(String[] args) throws IOException {
byte[] data;
try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
output.write("Hello ".getBytes("UTF-8"));
output.write("world!".getBytes("UTF-8"));
data = output.toByteArray();
}
System.out.println(new String(data, "UTF-8"));
}
}
|
try
的自动关闭是同时支持多个对象的。
1
2
3
4
5
|
try (InputStream input = new FileInputStream("input.txt");
OutputStream output = new FileOutputStream("output.txt"))
{
input.transferTo(output); // Java9+语法
}
|
装饰器模式
首先先了解它存在的背景是用来解决什么问题的。
- 需要为基本的输入/输出流添加多种功能(如缓冲、签名、加密等)
- 如果使用继承方式实现,会导致类爆炸(n 种功能组合需要 2^n 个子类)
- 功能组合的灵活性差,难以动态添加/移除功能
装饰器模式(Decorator Pattern)是一种结构型设计模式,它通过动态地包装对象来扩展功能,而不是通过继承。就好比从ABC的组合种类变成ABC的多选框一样,当元素增多时不会爆炸增长。
以流为例的实现方式:
- 基础组件:InputStream/OutputStream(抽象组件)
- 具体组件:FileInputStream、ByteArrayInputStream 等(具体实现)
- 装饰器基类:FilterInputStream/FilterOutputStream(保持组件接口)
- 具体装饰器:BufferedInputStream、GZIPInputStream 等(添加功能)
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
|
// 基础组件
public abstract class InputStream {
public abstract int read() throws IOException;
}
// 具体组件
public class FileInputStream extends InputStream {
public int read() { /* 实际文件读取 */ }
}
// 装饰器基类
public class FilterInputStream extends InputStream {
protected InputStream in;
protected FilterInputStream(InputStream in) {
this.in = in;
}
public int read() throws IOException {
return in.read();
}
}
// 具体装饰器
public class BufferedInputStream extends FilterInputStream {
private byte[] buffer = new byte[8192];
private int pos, count;
public BufferedInputStream(InputStream in) {
super(in);
}
public int read() throws IOException {
if(pos >= count) {
fillBuffer();
}
return buffer[pos++] & 0xff;
}
private void fillBuffer() { /* 填充缓冲区 */ }
}
|
1
2
3
4
5
6
7
|
// 多层装饰
InputStream input = new BufferedInputStream(
new GZIPInputStream(
new FileInputStream("test.gz")));
// 透明使用
int data = input.read(); // 会自动经过缓冲、解压等处理
|
本质在于其没有改变基础组件的属性,只是在不同的装饰类中添加一些方法修饰本身存在的域而已
解压缩
ZipInputStream
也是一种FilterInputStream
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
// 读取
try (ZipInputStream zip = new ZipInputStream(new FileInputStream(...))) {
ZipEntry entry = null;
while ((entry = zip.getNextEntry()) != null) {
String name = entry.getName();
if (!entry.isDirectory()) {
int n;
while ((n = zip.read()) != -1) {
...
}
}
}
}
// 写入
try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(...))) {
File[] files = ...
for (File file : files) {
zip.putNextEntry(new ZipEntry(file.getName()));
zip.write(Files.readAllBytes(file.toPath()));
zip.closeEntry();
}
}
|
序列化
终于看到序列化了,之前学类的时候一定看到过,但是不知道这是个什么玩应。
Java 序列化是将 Java 对象转换为字节流的过程,以便可以将其保存到文件中、通过网络传输或在内存中缓存。反序列化则是将字节流转换回 Java 对象的过程。
就是将对象和Byte []
互相转换的一个过程。
一个Java对象要能序列化,必须实现java.io.Serializable
接口:
1
2
|
public interface Serializable {
}
|
Serializable
接口没有定义任何方法,它是一个空接口。我们把这样的空接口称为“标记接口”(Marker Interface),实现了标记接口的类仅仅是给自身贴了个“标记”,并没有增加任何方法。
序列化
ObjectOutputStream
既可以写入基本类型,如int
,boolean
,也可以写入String
(以UTF-8编码),还可以写入实现了Serializable
接口的Object
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
import java.io.*;
import java.util.Arrays;
public class Main {
public static void main(String[] args) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try (ObjectOutputStream output = new ObjectOutputStream(buffer)) {
// 写入int:
output.writeInt(12345);
// 写入String:
output.writeUTF("Hello");
// 写入Object:
output.writeObject(Double.valueOf(123.456));
}
System.out.println(Arrays.toString(buffer.toByteArray()));
}
}
|
反序列化
1
2
3
4
5
|
try (ObjectInputStream input = new ObjectInputStream(...)) {
int n = input.readInt();
String s = input.readUTF();
Double d = (Double) input.readObject();
}
|
readObject()
可能抛出的异常有:
ClassNotFoundException
:没有找到对应的Class;出现于接收端没有这个类的定义。
InvalidClassException
:Class不匹配。出现于class变动。
为了避免这种class定义变动导致的不兼容,java引入了serialVersionUID机制
1
2
3
4
5
6
7
|
public class Person implements Serializable {
// 显式声明版本ID
private static final long serialVersionUID = 1L;
// 当类结构发生兼容性变更时,手动递增版本号
// private static final long serialVersionUID = 2L;
}
|
生成方式
- IDE 自动生成(通常基于类结构哈希)
- 手动指定简单序列号(从1L开始)
注意:反序列化时,由JVM直接构造出Java对象,不调用构造方法,构造方法内部的代码,在反序列化时根本不可能执行。
安全性
-
不要反序列化不受信任的数据:可能导致远程代码执行(RCE)
-
使用过滤机制(Java 9+):
1
2
3
4
5
6
7
|
ObjectInputFilter filter = info ->
info.serialClass() == null ? Status.UNDECIDED :
info.serialClass().getName().equals("com.example.Person") ?
Status.ALLOWED : Status.REJECTED;
ObjectInputStream ois = new ObjectInputStream(...);
ois.setObjectInputFilter(filter);
|
字符流
输入流
Read
和InputStream
相似,不过他是以char
为单位读取,那个是以Byte
为单位读取。
同样他也有相应的实现类FileReader CharArrayReader StringReader
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
public void readFile() throws IOException {
try (Reader reader = new FileReader("src/readme.txt", StandardCharsets.UTF_8)) {
char[] buffer = new char[1000];
int n;
while ((n = reader.read(buffer)) != -1) {
System.out.println("read " + n + " chars.");
}
}
}
try (Reader reader = new CharArrayReader("Hello".toCharArray())) {
}
try (Reader reader = new StringReader("Hello")) {
}
|
还有一个神奇的类叫做InputStreamReader,可以把一个InputStream转换成一个Reader。
1
2
3
4
5
6
7
8
9
10
11
|
try (InputStream is = new FileInputStream("file.txt");
InputStreamReader isr = new InputStreamReader(is, "UTF-8");
BufferedReader br = new BufferedReader(isr)) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
|
至于为什么呢?因为普通的Reader
实际上是基于InputStream
构造的,仍然是先读取字节流然后根据编码再转换为char
。
输出流
Writer
就是带编码转换器的OutputStream
,它把char
转换为byte
并输出。
已经学习了三个了,这剩下这最后一个明显可以同理可知。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
try (Writer writer = new FileWriter("readme.txt", StandardCharsets.UTF_8)) {
writer.write('H'); // 写入单个字符
writer.write("Hello".toCharArray()); // 写入char[]
writer.write("Hello"); // 写入String
}
try (CharArrayWriter writer = new CharArrayWriter()) {
writer.write(65);
writer.write(66);
writer.write(67);
char[] data = writer.toCharArray(); // { 'A', 'B', 'C' }
}
StringWriter writer = new StringWriter();
writer.write("Hello");
writer.write(", ");
writer.write("World!");
String result = writer.toString(); // "Hello, World!"
System.out.println(result);
try (Writer writer = new OutputStreamWriter(new FileOutputStream("readme.txt"), "UTF-8")) {
// TODO:
}
|
带格式化的流
PrintStream和PrintWriter和c里面的fscanf和fprintf功能是相似的。
然后就是最开始就用到的System.out
也终于知道这是什么东西了和cout
是一样的,也是个对象,同样也有System.err
表示错误输出。
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 PrintStreamExample {
public static void main(String[] args) {
try {
// 1. 创建文件输出流
FileOutputStream fos = new FileOutputStream("output.txt");
// 2. 创建PrintStream并关联到文件输出流
PrintStream ps = new PrintStream(fos);
// 3. 使用PrintStream输出数据
ps.println("Hello, PrintStream!");
ps.printf("当前时间: %tT", new java.util.Date());
ps.println();
ps.print("PI的值大约是: ");
ps.println(Math.PI);
// 4. 关闭流
ps.close();
System.out.println("数据已写入output.txt");
} catch (IOException e) {
e.printStackTrace();
}
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
import java.io.*;
public class Main {
public static void main(String[] args) {
StringWriter buffer = new StringWriter();
try (PrintWriter pw = new PrintWriter(buffer)) {
pw.println("Hello");
pw.println(12345);
pw.println(true);
}
System.out.println(buffer.toString());
}
}
|
Files
也是打脸了,前面说好不学.nio的,这个是Java7+的内容现在也不可能不兼容吧(。
Files
提供的读写方法,受内存限制,只能读写小文件,例如配置文件等,不可一次读入几个G的大文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
byte[] data = Files.readAllBytes(Path.of("/path/to/file.txt"));
// 默认使用UTF-8编码读取:
String content1 = Files.readString(Path.of("/path/to/file.txt"));
// 可指定编码:
String content2 = Files.readString(Path.of("/path", "to", "file.txt"), StandardCharsets.ISO_8859_1);
// 按行读取并返回每行内容:
List<String> lines = Files.readAllLines(Path.of("/path/to/file.txt"));
// 写入二进制文件:
byte[] data = ...
Files.write(Path.of("/path/to/file.txt"), data);
// 写入文本并指定编码:
Files.writeString(Path.of("/path/to/file.txt"), "文本内容...", StandardCharsets.ISO_8859_1);
// 按行写入文本:
List<String> lines = ...
Files.write(Path.of("/path/to/file.txt"), lines);
|
用到什么函数到时候再查吧。
单元测试
使用Junit
进行学习
测试驱动开发TDD
只存在于理论中的理想开发模式,
测试驱动开发(TDD,Test-Driven Development)的核心过程遵循 “红-绿-重构” 循环,具体步骤如下:
1. 编写失败的测试(Red)
- 先写测试:在写实现代码之前,先针对某个功能点编写测试用例。
- 测试应明确:定义输入、预期输出和边界条件。
- 运行测试:此时测试应该失败(因为功能尚未实现),进入“红”状态。
1
2
|
def test_add():
assert add(2, 3) == 5 # 假设 add() 还不存在
|
运行测试 → 失败(Red)
2. 编写最少代码使测试通过(Green)
- 只写能让测试通过的代码,不追求完美实现。
- 甚至可以硬编码返回值,先让测试变绿。
- 目标:快速验证测试逻辑是否正确。
1
2
|
def add(a, b):
return 5 # 硬编码,仅为了让测试通过
|
运行测试 → 通过(Green)
3. 重构代码(Refactor)
- 优化实现:去除硬编码,改进算法、提高可读性。
- 确保测试仍通过:重构后必须重新运行测试。
- 保持代码整洁:消除重复、优化结构。
1
2
|
def add(a, b):
return a + b # 正确的实现
|
运行测试 → 仍通过(Green)
4. 重复循环
- 继续下一个功能点,重复红→绿→重构流程。
- 逐步构建完整系统,确保每个功能都有测试覆盖。
JUnit初识
首先我们编写一个简单的阶乘类。
先建立一个软件包Example
1
2
3
4
5
6
7
8
9
10
11
|
package Example;
public class Factorial {
public static long fact(long n) {
if (n == 0) {
return 1;
} else {
return n * fact(n - 1);
}
}
}
|
在代码里面,对于Factorial
右键,点击生成,测试…,然后选择Junit5
,然后修复,IDEA会帮你把包自动下好,然后勾选需要测试的函数(这里只有这一个)生成即可。
你便得到了下面
1
2
3
4
5
6
7
8
9
10
11
12
|
package Example;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class FactorialTest {
@Test
void fact() {
}
}
|
然后你需要自己写@Test下面的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
package Example;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class FactorialTest {
@Test
void fact() {
assertEquals(1, Factorial.fact(0));
assertEquals(1, Factorial.fact(1));
assertEquals(2, Factorial.fact(2));
assertEquals(6, Factorial.fact(3));
assertEquals(24, Factorial.fact(4));
assertEquals(120, Factorial.fact(5));
assertEquals(114514, Factorial.fact(6));
}
}
|
这里故意错了一个,为了看看错误的输出是什么样子的
1
2
3
4
5
6
7
|
"C:\Program Files\Java\jdk-23\bin\java.exe" ...
org.opentest4j.AssertionFailedError:
预期:114514
实际:720
<点击以查看差异>
......
进程已结束,退出代码为 -1
|
一坨断言
在 JUnit 5 中,org.junit.jupiter.api.Assertions
类提供了丰富的断言方法
基本断言
• assertEquals(expected, actual)
:验证期望值和实际值相等。
1
2
|
assertEquals(5, 2 + 3);
assertEquals(double expected, double actual, double delta); // 浮点数方法重载,需要指定精度
|
• assertNotEquals(unexpected, actual)
:验证两个值不相等。
1
|
assertNotEquals(0, list.size());
|
• assertSame(expected, actual)
:验证两个对象引用同一个实例。
1
|
assertSame(obj1, obj1);
|
• assertNotSame(unexpected, actual)
:验证两个对象引用不同实例。
1
|
assertNotSame(obj1, obj2);
|
• assertTrue(condition)
:验证条件为 true
,支持 Lambda。
1
|
assertTrue(() -> x > 0);
|
• assertFalse(condition)
:验证条件为 false
。
1
|
assertFalse(list.isEmpty());
|
• assertNull(object)
:验证对象为 null
。
• assertNotNull(object)
:验证对象不为 null
。
集合和数组断言
• assertArrayEquals(expectedArray, actualArray)
:验证两个数组内容相同。
1
|
assertArrayEquals(new int[]{1, 2}, new int[]{1, 2});
|
• assertIterableEquals(expected, actual)
:验证两个 Iterable
的元素和顺序一致。
1
|
assertIterableEquals(List.of(1, 2), Arrays.asList(1, 2));
|
• assertLinesMatch(expectedLines, actualLines)
:验证字符串列表按行匹配,支持正则表达式。
1
|
assertLinesMatch(List.of("Hello", "\\d+"), List.of("Hello", "123"));
|
异常断言
• assertThrows(expectedExceptionType, executable)
:验证代码抛出指定异常,并返回异常实例。
1
2
3
4
|
Exception e = assertThrows(IllegalArgumentException.class, () -> {
throw new IllegalArgumentException("error");
});
assertEquals("error", e.getMessage());
|
• assertDoesNotThrow(executable)
:验证代码不抛出异常。
1
|
assertDoesNotThrow(() -> calculator.add(1, 2));
|
组合断言
• assertAll(heading, executables...)
:组合多个断言,全部执行后报告所有失败。
1
2
3
4
5
|
assertAll(
() -> assertEquals(2, 1 + 1),
() -> assertTrue(list.isEmpty()),
() -> assertNotNull(user)
);
|
超时断言
• assertTimeout(duration, executable)
:验证代码在指定时间内完成(阻塞式)。
1
2
3
|
assertTimeout(Duration.ofSeconds(2), () -> {
Thread.sleep(1000);
});
|
• assertTimeoutPreemptively(duration, executable)
:超时后立即终止测试(非阻塞式)。
1
2
3
|
assertTimeoutPreemptively(Duration.ofMillis(100), () -> {
Thread.sleep(200); // 超时失败
});
|
强制失败
• fail(message)
:直接使测试失败。
1
|
fail("Not implemented yet");
|
Text Fixture
想必在之前的例子当中,在创建测试的GUI界面中看到了setUp/@Before
和tearDown/@After
,下面就来讲解其用处。
之前的例子是静态方法,如果我们要测试一个类的方法,那么就需要先初始化一个对象,然后再调用其方法,假如有很多个方法,都要这么干,麻烦!所以将这种做准备和清理现场的事情函数化,测试的前后进行调用即可。
看一遍代码就知道有什么用了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
package Example;
public class Calculator {
private long result = 0;
public static int add(int a, int b) {
return a + b;
}
public static int subtract(int a, int b) {
return a - b;
}
}
|
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
|
package test;
import Example.Calculator;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
Calculator calculator;
@BeforeEach
void setUp() {
calculator = new Calculator();
}
@AfterEach
void tearDown() {
calculator = null;
}
@Test
void add() {
assertEquals(0, calculator.add(0));
assertEquals(1, calculator.add(1));
assertEquals(3, calculator.add(2));
assertEquals(6, calculator.add(3));
assertEquals(10, calculator.add(4));
}
@Test
void subtract() {
assertEquals(0, calculator.subtract(0));
assertEquals(-1, calculator.subtract(1));
assertEquals(-3, calculator.subtract(2));
assertEquals(-6, calculator.subtract(3));
assertEquals(-10, calculator.subtract(4));
}
}
|
除了@BeforeEach
和@AferEach
我们还有BeforeAll
和AfterAll
,它们运行在所有的@Test
前后,也就是最开始,和最后面,并且也因此只运行一次,这导致它只能初始化静态变量,它的意义就是如果类存在一个静态变量的初始化很慢,我们还总用到它,就能快一些,其他也没啥用。
异常测试
这里的异常不是说测试有异常,而是测试你的类抛出的Exception
。
还是之前的阶乘类,众所周知,负数没有阶乘,所以如果参数是负数,就应该抛出异常。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
package Example;
public class Factorial {
public static long fact(long n) {
if (n < 0) {
throw new IllegalArgumentException("Negative numbers are not allowed");
}
if (n == 0) {
return 1;
} else {
return n * fact(n - 1);
}
}
}
|
然后在对应的Test类添加函数,这里采用lamda匿名函数来写,之前的古老方法不太行,有点麻烦了,语法和js的匿名函数有点像,但是明显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
|
package Example;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Executable;
import static org.junit.jupiter.api.Assertions.*;
class FactorialTest {
@Test
void fact() {
assertEquals(1, Factorial.fact(0));
assertEquals(1, Factorial.fact(1));
assertEquals(2, Factorial.fact(2));
assertEquals(6, Factorial.fact(3));
assertEquals(24, Factorial.fact(4));
assertEquals(120, Factorial.fact(5));
}
@Test
void testNegative() {
assertThrows(IllegalArgumentException.class, () -> {
Factorial.fact(-1);
});
}
}
|