Java 进阶
注意
以下内容均基于 Java JDK 8 版本编写,不排除在更高版本中有部分改动的可能性.
更高速的输入输出
Scanner 和 System.out.print 在最开始会工作得很好,但是在处理更大的输入的时候会降低效率,因此我们会需要使用一些方法来提高 IO 速度.
使用 Kattio + StringTokenizer 作为输入
最常用的方法之一是使用来自 Kattis 的 Kattio.java 来提高 IO 效率.1这个方法会将 StringTokenizer 与 PrintWriter 包装在一个类中方便使用.而在具体进行解题的时候(假如赛会/组织方允许)可以直接使用这个模板.
下方即为应包含在代码中的 IO 模板,由于 Kattis 的原 Kattio 包含一些并不常用的功能,下方的模板经过了一些调整(原 Kattio 使用 MIT 作为协议).
---|---
而下方代码简单展示了 Kattio 的使用:
---|---
使用 StreamTokenizer 作为输入
在某些情况使用 StringTokenizer 会导致 MLE(Memory Limit Exceeded,超过内存上限),此时我们需要使用 StreamTokenizer 作为输入.
---|---
### Kattio + StringTokenizer 的方法与 StreamTokenizer 的方法之间的分析与对比
1. `StreamTokenizer` 相较于 `StringTokenizer` 使用的内存较少,当 Java 标程 MLE 时可以尝试使用 `StreamTokenizer`,但是 `StreamTokenizer` 会丢失精度,读入部分数据时会出现问题;
* `StreamTokenizer` 源码存在 `Type`,该 `Type` 根据输入内容来决定类型,如果输入类似于 `123oi` 以 **数字开头** 的字符串,他会强制认为的类型是 `double` 类型,因此在读入中以 `double` 类型去读 `String` 类型便会抛出异常;
* `StreamTokenizer` 在读入 `1e14` 以上大小的数字会丢失精度;
2. 在使用 `PrintWriter` 情况下,需注意在程序结束最后 `close()` 关闭输出流或在需要输出的时候使用 `flush()` 清除缓冲区,否则内容将不会被写入到控制台/文件中.
3. `Kattio` 是继承自 `PrintWriter` 类,自身对象具有了 `PrintWriter` 的功能,因此可以直接调用 `PrintWriter` 类的函数输出,同时将 `StringTokenizer` 作为了自身的成员变量来修改.而第二种 `Main` 是同时将 `StreamTokenizer` 与 `PrintWriter` 作为了自身的成员变量,因此在使用上有些许差距.
综上所述,在大部分情况下,`StringTokenizer` 的使用处境要优越于 `StreamTokenizer`,在极端 MLE 的情况下可以尝试 `StreamTokenizer`,同时 `int` 范围以上的数据 `StreamTokenizer` 处理是无能为力的.
## BigInteger 与数论
`BigInteger` 是 Java 提供的高精度计算类,可以很方便地解决高精度问题.
### 初始化
`BigInteger` 常用创建方式有如下二种:
---|---
基本运算
以下均用 this 代替当前 BigIntger:
| 函数名 | 功能 |
|---|---|
abs() | 返回 this 的绝对值 |
negate() | 返回 this 的相反数 |
add(BigInteger val) | 返回 this 和 val 的和 |
subtract(BigInteger val) | 返回 this 和 val 的差 |
multiply(BigInteger val) | 返回 this 和 val 的积 |
divide(BigInteger val) | 返回 this 和 val 的商 |
remainder(BigInteger val) | 返回 this 除以 val 的余数 |
mod(BigInteger val) | 返回 this 对 val 取模的值 |
pow(int val) | 返回 this 的 val 次方 |
and(BigInteger val) | 返回 this 和 val 的按位与 |
or(BigInteger val) | 返回 this 和 val 的按位或 |
not() | 返回 this 的按位取反 |
xor(BigInteger val) | 返回 this 和 val 的按位异或 |
shiftLeft(int n) | 返回 this 左移 n 位 |
shiftRight(int n) | 返回 this 右移 n 位 |
max(BigInteger val) | 返回 this 与 val 的较大值 |
min(BigInteger val) | 返回 this 与 val 的较小值 |
bitCount() | 返回 this 的二进制中不包括符号位的 1 的个数 |
bitLength() | 返回 this 的二进制中不包括符号位的长度 |
getLowestSetBit() | 返回 this 的二进制中最右边的位置 |
compareTo(BigInteger val) | 比较 this 和 val 值大小 |
toString() | 返回 this 的十进制字符串表示形式 |
toString(int radix) | 返回 this 的 raidx 进制字符串表示形式 |
使用案例如下:
---|---
### 数学运算
以下均用 `this` 代替当前 `BigIntger`:
函数名| 功能
---|---
`gcd(BigInteger val)`| 返回 `this` 的绝对值与 `val` 的绝对值的最大公约数
`isProbablePrime(int val)`| 返回一个表示 `this` 是否是素数的布尔值
`nextProbablePrime()`| 返回第一个大于 `this` 的素数
`modPow(BigInteger b, BigInteger p)`| 返回 `this` 的 `b` 次方模 `p` 的值
`modInverse(BigInteger p)`| 返回 `this` 在模 `p` 意义下的乘法逆元
使用案例如下:
---|---
关于米勒罗宾相关知识可以查阅 Miller–Rabin 素性测试.
基本数据类型与包装数据类型
简介
由于基本类型没有面向对象的特征,为了他们参加到面向对象的开发中,Java 为八个基本类型提供了对应的包装类,分别是 Byte、Double、Float、Integer、Long、Short、Character 和 Boolean.两者之间的对应关系如下:
| 基本数据类型 | 包装数据类型 |
|---|---|
byte | Byte |
short | Short |
boolean | Boolean |
char | Character |
int | Integer |
long | Long |
float | Float |
double | Double |
区别
此处以 int 与 Integer 举例:
Integer是int的包装类,int则是 Java 的一种基本类型数据.Integer类型实例后才能使用,而int类型不需要.Integer实际对应的引用,当new一个Integer时,实际上生成了一个对象,而int则是直接存储数据.Integer的默认值是null,可接受null和int类型的数据,int默认值是 0,不能接受null类型的数据.Integer判定二个变量是否相同使用==可能会导致不正确的结果,只能使用equals(),而int可以直接使用==.
装箱与拆箱
此处以 int 与 Integer 举例:
Integer 的本质是对象,int 是基本类型,两个类型之间是不能直接赋值的.需要转换时,应将基础类型转换为包装类型,这种做法称为装箱,反过来则称为拆箱.
---|---
Java 5 引入了自动装箱拆箱机制:
---|---
注意
虽然 JDK 增加了自动装箱拆箱的机制,但在声明变量时请选择合适的类型,因为包装类型 Integer 可以接受 null,而基本类型 int 不能接受 null.因此,对使用 null 值的包装类型进行拆箱操作时,会抛出异常.如下代码展示了这一行为.
---|---
## 继承
基于已有的设计创造新的设计,就是面向对象程序设计中的继承.在继承中,新的类不是凭空产生的,而是基于一个已经存在的类而定义出来的.通过继承,新的类自动获得了基础类中所有的成员,包括成员变量和方法,包括各种访问属性的成员,无论是 `public` 还是 `private`.显然,通过继承来定义新的类,远比从头开始写一个新的类要简单快捷和方便.继承是支持代码重用的重要手段之一.
在 Java 中,继承的关键字为 `extends`,且 Java 只支持单继承,但可以实现多接口.
在 Java 中,所有类都是 `Object` 类的子类.
子类继承父类,所有的父类的成员,包括变量和方法,都成为了子类的成员,除了构造方法.构造方法是父类所独有的,因为它们的名字就是类的名字,所以父类的构造方法在子类中不存在.除此之外,子类继承得到了父类所有的成员.
每个成员有不同的访问属性,子类继承得到了父类所有的成员,但是不同的访问属性使得子类在使用这些成员时有所不同:有些父类的成员直接成为子类的对外的界面,有些则被深深地隐藏起来,即使子类自己也不能直接访问.
下表列出了不同访问属性的父类成员在子类中的访问属性:
父类成员访问属性| 在父类中的含义| 在子类中的含义
---|---|---
`public`| 对所有类开放| 对所有类开放
`protected`| 只有包内其它类、自己和子类可以访问| 只有包内其它类、自己和子类可以访问
缺省(`default`)| 只有包内其它类可以访问| 如果子类与父类在同一个包内,只有包内其它类可以访问;否则相当于 `private`,不能访问
`private`| 只有自己可以访问| 不能访问
## 多态
在 Java 中当把一个对象赋值给一个变量时,对象的类型必须与变量的类型相匹配.但由于 Java 有继承的概念,便可重新定义为 **一个变量可以保存其所声明的类型或该类型的任何子类型** .
如果一个类型实现了接口,也可以称之为该接口的子类型.
Java 中保存对象类型的变量是多态变量.「多态」这个术语(字面意思是许多形态)是指一个变量可以保存不同类型(即其声明的类型或任何子类型)的对象.
多态变量:
1. Java 的对象变量是多态的,它们能保存不止一种类型的对象.
2. 它们可以保存的是声明类型的对象,或声明类型子类的对象.
3. 当把子类的对象赋给父类的变量的时候,就发生了向上转型.
## 泛型
泛型指在类定义时不设置类中的属性或方法参数的具体类型,而是在使用(或创建对象)时再进行类型的定义.泛型本质是参数化类型,即所操作的数据类型被指定为一个参数.
泛型提供了编译时类型安全检测的机制,该机制允许编译时检测非法类型.
## 接口
### 简介
接口(Interface)在 Java 中是一个抽象类型,是抽象方法的集合,通常以 `interface` 来声明.一个类通过实现接口的方式,从而来继承接口的抽象方法.
接口并不是类,编写接口的方式和类很相似,但是它们属于不同的概念.类描述对象的属性和方法.接口则包含类要实现的方法.
除非实现接口的类是抽象类,否则该类要定义接口中的所有方法.
接口无法被实例化,但是可以被实现.一个实现接口的类,必须实现接口内所描述的所有方法,否则就必须声明为抽象类.另外,在 Java 中,接口类型可用来声明一个变量,他们可以成为一个空指针,或是被绑定在一个以此接口实现的对象.
### 与类的区别
1. 接口不能用于实例化对象.
2. 接口没有构造方法.
3. 接口中所有的方法必须是抽象方法,Java 8 之后接口中可以使用 `default` 关键字修饰的非抽象方法.
4. 接口不能包含成员变量,除了 static 和 final 变量.
5. 接口不是被类继承了,而是要被类实现.
6. 接口支持多继承,类不支持多继承.
### 声明
---|---
实现
---|---
## Lambda 表达式
### 简介
lambda 表达式也可称为闭包,是 Java 8 的最重要的新特性.
lambda 表达式允许把函数作为一个方法的参数(函数作为参数传递进方法中).
使用 lambda 表达式可以使代码变的更加简洁紧凑.
### 语法
* 可选类型声明:不需要声明参数类型,编译器可以统一识别参数值.
* 可选的参数圆括号:一个参数无需定义圆括号,但多个参数需要定义圆括号.
* 可选的大括号:如果主体包含了一个语句,就不需要使用大括号.
* 可选的返回关键字:如果主体只有一个表达式返回值则编译器会自动返回值,大括号需要指定表达式返回了一个数值.
lambda 表达式声明方式如下:
---|---
以字符串数组按长度排序的自定义比较器为例,lambda 表达式可以按如下形式应用.
---|---
也可以类似下面的例子在 lambda 表达式中使用多条语句.
---|---
其中,-> 是一个推导符号,表示前面的括号接收到参数,推导后面的返回值(其实就是传递了方法).
函数式接口
- 是一个接口,符合 Java 接口定义.
- 只包含一个抽象方法的接口.
- 因为只有一个未实现的方法,所以 lambda 表达式可以自动填上去.
函数式接口使用方式如下:
输出长度为 2 的倍数的字符串
---|---
实现加减乘除四则运算
---|---
Collection
Collection 是 Java 中的接口,被多个泛型容器接口所实现.在这里,Collection 是指代存放对象类型的数据结构.
Java 中的 Collection 元素类型定义时必须为对象,不能为基本数据类型.
以下内容用法均基于 Java 里多态的性质,均是以实现接口的形式出现.
常用的接口包括 List、Queue、Set 和 Map.
容器定义
当定义泛型容器类时,需要在定义时指定数据类型.如果不指定数据类型,而当成 Object 类型随意添加数据,在 Java 8 中虽能编译通过,但会有很多警告风险.
例如,如下定义方式是安全的,容器中只接受 Integer 类型.
---|---
而如下定义方式会出现警告.
---|---
因此,如果没有特殊需求的话不推荐第 2 种行为,编译器无法帮忙检查存入的数据是否安全.list.get(index) 取值时无法明确数据的类型(取到的数据类型都为 Object),需要手动转回原来的类型,稍有不慎可能出现误转型异常.
如果是明确了类型如 List<Integer>,此时编译器会检查放入的数据类型,只能放入整数的数据.声明集合变量时只能使用包装类型 List<Integer> 或者自定义的 Class,而不能是基本类型如 List<int>.
List
ArrayList
ArrayList 是支持可以根据需求动态生长的数组,初始长度默认为 10.如果超出当前长度便扩容 3232.
初始化
---|---
#### LinkedList
`LinkedList` 是双链表.
##### 初始化
---|---
常用方法
以下均用 this 代替当前 List<Integer>:
| 函数名 | 功能 |
|---|---|
size() | 返回 this 的长度 |
add(Integer val) | 在 this 尾部插入 val 元素 |
add(int idx, Integer e) | 在 this 的 idx 位置插入 e 元素 |
get(int idx) | 返回 this 中第 idx 位置的值,若越界则抛出异常 |
set(int idx, Integer e) | 修改 this 中第 idx 位置的值为 e |
使用案例及区别对比:
---|---
#### 遍历
---|---
注意
不要在 for 或 foreach 遍历 List 的过程中删除其中的元素,否则会抛出异常.
原因也很简单,list.size() 改变了,但在循环中已循环的次数却是没有随之变化.原来预计在下一个 index 的数据因为删除的操作变成了当前 index 的数据,运行下一个循环时操作的会变为原来预计在下下个 index 的数据,最终会导致操作的数据不符合预期.
Queue
LinkedList
可以使用 LinkedList 实现普通队列,底层是链表模拟队列.
初始化
---|---
`LinkedList` 底层实现了 `List` 接口与 `Deque` 接口,而 `Deque` 接口继承自 `Queue` 接口,所以 `LinkedList` 可以同时实现 `List` 与 `Queue`.
#### ArrayDeque
可以使用 `ArrayDeque` 实现普通队列,底层是数组模拟队列.
##### 初始化
---|---
ArrayDeque 底层实现了 Deque 接口,而 Deque 接口继承自 Queue 接口,所以 ArrayDeque 可以实现 Queue.
LinkedList 与 ArrayDeque 在实现 Queue 接口上的区别
- 数据结构:在数据结构上,
ArrayDeque和LinkedList都实现了 Java Deque 双端队列接口.但ArrayDeque没有实现了 Java List 列表接口,所以不具备根据索引位置操作的行为. - 线程安全:
ArrayDeque和LinkedList都不考虑线程同步,不保证线程安全. - 底层实现:在底层实现上,
ArrayDeque是基于动态数组的,而LinkedList是基于双向链表的. - 在遍历速度上:
ArrayDeque是一块连续内存空间,基于局部性原理能够更好地命中 CPU 缓存行,而LinkedList是离散的内存空间对缓存行不友好. - 在操作速度上:
ArrayDeque和LinkedList的栈和队列行为都是 𝑂(1)O(1)时间复杂度,
ArrayDeque的入栈和入队有可能会触发扩容,但从均摊分析上看依然是 𝑂(1)O(1)时间复杂度.
- 额外内存消耗上:
ArrayDeque在数组的头指针和尾指针外部有闲置空间,而LinkedList在节点上增加了前驱和后继指针.
PriorityQueue
PriorityQueue 是优先队列,默认是小根堆.
初始化
---|---
#### 常用方法
下表中队列定义为 `Queue<Integer>`.
函数名| 功能
---|---
`size()`| 返回当前队列长度
`add(Integer val)`| 将 `val` 插入队列,如果插入时违反了队列的容量限制,将抛出异常
`offer(Integer val)`| 将 `val` 插入队列,如果插入时违反了队列的容量限制,则插入失败,但不会抛出异常
`isEmpty()`| 判断队列是否为空,为空则返回 `true`
`peek()`| 返回队头元素,若队列为空返回 `null`
`poll()`| 返回并删除队头元素,若队列为空返回 `null`
使用案例及区别对比:
---|---
遍历
---|---
### Deque
`Deque` 是 `Java` 中的双端队列,我们通常用其进行队列的操作以及栈的操作.
#### 主要函数
下表中队列定义为 `Deque<Integer>`.
函数名| 功能
---|---
`addFirst(Integer val)`| 将 `val` 插入队头,如果插入时违反了队列的容量限制,将抛出异常
`offerFirst(Integer val)`| 将 `val` 插入队头,如果插入时违反了队列的容量限制,则插入失败,但不会抛出异常
`removeFirst()`| 返回并删除队头元素,如果队列为空,将抛出异常
`pollFirst()`| 返回并删除队头元素,如果队列为空,则返回 `null`
`peekFirst()`| 返回队头元素,如果队列为空,则返回 `null`
`push(Integer val)`| 将 `val` 插入队头,等效于 `addFirst`
`pop()`| 返回并删除队头元素,等效于 `removeFirst`
`remove()`| 删除队头元素,等效于 `removeFirst`
`poll()`| 删除队头元素,等效于 `pollFirst`
`addLast(Integer val)`| 将 `val` 插入队尾,如果插入时违反了队列的容量限制,将抛出异常
`offerLast(Integer val)`| 将 `val` 插入队尾,如果插入时违反了队列的容量限制,则插入失败,但不会抛出异常
`removeLast()`| 返回并删除队尾元素,如果队列为空,将抛出异常
`pollLast()`| 返回并删除队尾元素,如果队列为空,则返回 `null`
`peekLast()`| 返回队尾元素,如果队列为空,则返回 `null`
`add(Integer val)`| 将 `val` 插入队尾,等效于 `addLast`
`offer(Integer val)`| 将 `val` 插入队尾,等效于 `offerLast`
#### 栈的操作
---|---
双端队列的操作
---|---
### Set
`Set` 是保持容器中的元素不重复的一种数据结构.
#### HashSet
随机位置插入的 `Set`.
##### 初始化
---|---
LinkedHashSet
保持插入顺序的 Set.
初始化
---|---
#### TreeSet
保持容器中元素有序的 `Set`,默认为升序.
##### 初始化
---|---
TreeSet 的更多使用
这些方法是 TreeSet 新创建并实现的,我们无法使用 Set 接口调用以下方法,因此我们创建方式如下:
---|---
下表中均用 `this` 代替当前 `TreeSet<Integer>`.
函数名| 功能
---|---
`first()`| 返回 `this` 中第一个元素,无则返回 `null`
`last()`| 返回 `this` 中最后一个元素,无则返回 `null`
`floor(Integer val)`| 返回 `this` 中小于等于 `val` 的第一个元素,无则返回 `null`
`ceiling(Integer val)`| 返回 `this` 中大于等于 `val` 的第一个元素,无则返回 `null`
`higher(Integer val)`| 返回 `this` 中大于 `val` 的第一个元素,无则返回 `null`
`lower(Integer val)`| 返回 `this` 中小于 `val` 的第一个元素,无则返回 `null`
`pollFirst()`| 返回并删除 `this` 中第一个元素,无则返回 `null`
`pollLast()`| 返回并删除 `this` 中最后一个元素,无则返回 `null`
代码示例:
---|---
Set 常用方法
| 函数名 | 功能 |
|---|---|
size() | 返回当前集合的大小 |
add(Integer val) | 将 val 插入集合 |
contains(Integer val) | 判断集合中是否有元素 val |
addAll(Collection e) | 将容器 e 里的所有元素添加进当前集合 |
retainAll(Collection e) | 删除当前集合中未出现在容器 e 中的元素,即求当前集合与 e 的交集 |
removeAll(Collection e) | 删除当前集合中出现在容器 e 中的元素,即求当前集合与 e 的差集 |
---|---
#### 遍历
---|---
Map
Map 是维护键值对 <Key, Value> 的一种数据结构,其中 Key 唯一.
HashMap
随机位置插入的 Map.
初始化
---|---
#### LinkedHashMap
保持插入顺序的 `Map`.
##### 初始化
---|---
TreeMap
保持 key 有序的 Map,默认升序.
初始化
---|---
#### 常用方法
以下均用 `this` 代替当前 `Map<Integer, Integer>`:
函数名| 功能
---|---
`put(Integer key, Integer value)`| 将 `<key, value>` 插入 `this`
`size()`| 返回 `this` 的大小
`containsKey(Integer key)`| 判断 `this` 中是否有存在某个元素的键为 `key`
`get(Integer key)`| 返回 `this` 中键为 `key` 的元素对应的值
`keySet()`| 将 `this` 中所有元素的键作为集合返回
使用案例:
---|---
遍历
---|---
当然,键值的类型也可以更改.例如 `Map` 也可以定义为:
---|---
Arrays
Arrays 是 java.util 中对数组操作的一个工具类.方法均为静态方法,可使用类名直接调用.
Arrays.sort()
Arrays.sort() 是对数组进行的排序的方法,主要重载方法如下:
---|---
序号所对应的重载方法含义:
1. 对数组 `a` 进行排序,默认升序.
2. 对数组 `a` 的指定位置进行排序,默认升序,排序区间为左闭右开 `[firstIdx, lastIdx)`.
3. 对数组 `a` 以自定义的形式排序,第二个参数 `-` 第一个参数为降序,第一个参数 `-` 第二个参数为升序,当自定义排序比较器时,数组元素类型必须为对象类型.
4. 对数组 `a` 的指定位置进行自定义排序,排序区间为左闭右开 `[firstIdx, lastIdx)`,当自定义排序比较器时,数组元素类型必须为对象类型.
5. 和 3 同理,用 Lambda 表达式优化了代码长度.
6. 和 4 同理,用 Lambda 表达式优化了代码长度.
`Arrays.sort()` 底层函数
1. 当 `Arrays.sort` 的参数数组元素类型为基本数据类型(`byte`、`short`、`char`、`int`、`long`、`double`、`float`)时,默认为 `DualPivotQuicksort`(双轴快排),复杂度最坏可以达到 𝑂(𝑛2)O(n2).
2. 当 `Arrays.sort` 的参数数组元素类型为非基本数据类型时,则默认为 `legacyMergeSort` 和 `TimSort`(归并排序),复杂度为 𝑂(𝑛log𝑛)O(nlogn).
可以通过如下代码验证:
[Codeforces 1646B - Quality vs Quantity](https://codeforces.com/problemset/problem/1646/B)
有 𝑛n 个整数,你需要将其分为两组,是否能存在某一组的长度小于另一组,同时和大于它.
例题代码
---|---
Arrays.binarySearch()
Arrays.binarySearch() 是对数组连续区间进行二分搜索的方法,前提是数组必须有序,时间复杂度为 𝑂(log𝑛)O(logn),主要重载方法如下:
---|---
源码如下:
---|---
序号所对应的重载方法含义:
- 从数组 a 中二分查找是否存在
key,如果存在,便返回其下标.若不存在,则返回一个负数. - 从数组 a 中二分查找是否存在
key,如果存在,便返回其下标,搜索区间为左闭右开[firstIdx,lastIdx).若不存在,则返回一个负数.
Arrays.fill()
Arrays.fill() 方法将数组中连续位置的元素赋值为统一元素.其接受的参数为数组、fromIndex、toIndex 和需要填充的数.方法执行后,数组左闭右开区间 [firstIdx,lastIdx) 内的所有元素的值均为需要填充的数.
Collections
Collections 是 java.util 中对集合操作的一个工具类.方法均为静态方法,可使用类名直接调用.
Collections.sort()
Collections.sort() 底层原理为将其中所有元素转化为数组调用 Arrays.sort(),完成排序后再赋值给原本的集合.又因为 Java 中 Collection 的元素类型均为对象类型,所以始终是归并排序去处理.
该方法无法对集合指定区间排序.
底层源码:
---|---
### Collections.binarySearch()
`Collections.binarySearch()` 是对集合中指定区间进行二分搜索,功能与 `Arrays.binarySearch()` 相同.
---|---
该方法无法对指定区间进行搜索.
Collections.swap()
Collections.swap() 的功能是交换集合中指定二个位置的元素.
---|---
## 其他
### 数值比较问题
在 Java 中,如果单纯是数值类型,`-0.0 = 0.0`.若是对象类型,则 `-0.0 != 0.0`.如果尝试用 `Set` 统计斜率数量时,这个问题就会带来麻烦.提供的解决方式是在所有的斜率加入 `Set` 前将值增加 `0.0`.
---|---
参考资料
本页面最近更新: 2026/1/7 08:56:54,更新历史 发现错误?想一起完善?在 GitHub 上编辑此页! 本页面贡献者:Tiphereth-A, 1804040636, aofall, HeRaNO, shuzhouliu, yusancky, c-forrest, caopengrun, CCXXXI, ImpleLee, megakite, optimize-2, Qubik65536, untitledunrevised, ZnPdCo 本页面的全部内容在CC BY-SA 4.0 和 SATA 协议之条款下提供,附加条款亦可能应用