Java核心类
约 11853 字大约 40 分钟
2025-07-14
2.1 字符串和编码
2.1.1 String 类
在 Java 中,String 是一个引用类型,本质上是一个类 ( class )。Java 编译器对 String 有特殊处理,允许使用 " 来直接表示字符串字面量。
String s1 = "Hello!";实际上,字符串在 String 内部是以 char[] 数组的形式存储的,因此以下写法是等效的:
String s2 = new String(new char[] {'H', 'e', 'l', 'l', 'o', '!'});由于 String 的常用性,Java 提供了使用双引号 " 表示字符串字面量的方式。
2.1.2 字符串的不可变性
Java 字符串的一个关键特性是 不可变性 。这意味着一旦 String 对象被创建,它的值就不能被修改。
这种不可变性是通过以下方式实现的:
- 内部使用
private final char[]字段存储字符。 - 没有提供任何修改
char[]数组内容的方法。
下面是一个示例,展示了字符串的不可变性:
// String
public class Main {
public static void main(String[] args) {
String s = "Hello";
System.out.println(s); // 输出:Hello
s = s.toUpperCase();
System.out.println(s); // 输出:HELLO
}
}代码解释:
上述代码中,s.toUpperCase() 看起来像是修改了 s 的值,但实际上它创建了一个新的 String 对象,并将新对象的引用赋值给 s。原始的 String 对象 "Hello" 并没有发生改变。
2.1.3 字符串比较
在比较两个字符串是否相等时,务必使用 equals() 方法,而不是 == 运算符。equals() 方法比较的是字符串的内容,而 == 比较的是对象的引用 (即内存地址)。
以下示例说明了使用 equals() 方法的重要性:
// String
public class Main {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); // 输出:true (可能为 true,但不可靠)
System.out.println(s1.equals(s2)); // 输出:true (始终为 true,推荐使用)
}
}在上面的例子中,s1 == s2 的结果是 true,这仅仅是因为 Java 编译器在编译时将相同的字符串字面量放入了常量池,使得 s1 和 s2 指向同一个对象。
但是,在以下情况下,== 比较会失败:
// String
public class Main {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "HELLO".toLowerCase();
System.out.println(s1 == s2); // 输出:false
System.out.println(s1.equals(s2)); // 输出:true
}
}因此,结论是:比较字符串内容必须始终使用 equals() 方法。
要忽略大小写比较字符串,可以使用 equalsIgnoreCase() 方法。
2.1.4 字符串操作
String 类提供了多种方法用于搜索子串和提取子串。一些常用的方法包括:
| 方法名称 | 描述 | 示例 |
|---|---|---|
contains(CharSequence s) | 检查字符串是否包含指定的子序列。CharSequence 是 String 实现的一个接口。 | "Hello".contains("ll"); // true |
indexOf(String str) | 返回子字符串第一次出现的索引。 | "Hello".indexOf("l"); // 2 |
lastIndexOf(String str) | 返回子字符串最后一次出现的索引。 | "Hello".lastIndexOf("l"); // 3 |
startsWith(String prefix) | 检查字符串是否以指定的前缀开始。 | "Hello".startsWith("He"); // true |
endsWith(String suffix) | 检查字符串是否以指定的后缀结束。 | "Hello".endsWith("lo"); // true |
substring(int beginIndex) | 返回从指定索引开始到字符串结尾的子字符串。 | "Hello".substring(2); // "llo" |
substring(int beginIndex, int endIndex) | 返回从指定的 beginIndex 开始到 endIndex - 1 的子字符串。 | "Hello".substring(2, 4); // "ll" |
注意:Java 字符串的索引从 0 开始。
2.1.5 去除首尾空白字符
| 方法名称 | 描述 | 示例 |
|---|---|---|
trim() | 移除字符串首尾的空白字符,包括空格、制表符 (\t)、回车符 (\r) 和换行符 (\n)。 | " \tHello\r\n ".trim(); // "Hello" |
strip() | 移除字符串首尾的空白字符,与 trim() 的不同之处在于,strip() 还可以移除 Unicode 空白字符,例如中文空格 (\u3000)。 | "\u3000Hello\u3000".strip(); // "Hello" |
stripLeading() | 移除字符串开头的空白字符。 | " Hello ".stripLeading(); // "Hello " |
stripTrailing() | 移除字符串结尾的空白字符。 | " Hello ".stripTrailing(); // " Hello" |
注意:trim() 和 strip() 方法都不会改变原始字符串的内容,而是返回一个新的字符串。
String 类还提供了 isEmpty() 和 isBlank() 方法用于判断字符串是否为空或仅包含空白字符:
| 方法名称 | 描述 | 示例 |
|---|---|---|
isEmpty() | 判断字符串的长度是否为 0。 | "".isEmpty(); // true, " ".isEmpty(); // false |
isBlank() | 判断字符串是否为空或仅包含空白字符。 | " \n".isBlank(); // true, " Hello ".isBlank(); // false |
2.1.6 替换子串
String 类提供了两种替换子串的方法:
| 方法名称 | 描述 | 示例 |
|---|---|---|
replace(char oldChar, char newChar) | 将字符串中所有出现的 oldChar 替换为 newChar。 | "hello".replace('l', 'w'); // "hewwo" |
replace(CharSequence target, CharSequence replacement) | 将字符串中所有出现的 target 子串替换为 replacement 子串。 | "hello".replace("ll", "~~"); // "he~~o" |
replaceAll(String regex, String replacement) | 使用正则表达式 regex 匹配字符串中的子串,并将匹配的子串替换为 replacement。 | "A,,B;C ,D".replaceAll("[\\,\\;\\s]+", ","); // "A,B,C,D" |
其中,正则表达式 [\\,\\;\\s]+ 表示匹配一个或多个逗号、分号或空白字符。
2.1.7 分割字符串
使用 split(String regex) 方法可以根据指定的分隔符将字符串分割成字符串数组。该方法接受一个正则表达式作为参数。
示例:
String s = "A,B,C,D";
String[] ss = s.split("\\,"); // {"A", "B", "C", "D"}代码解释:
上述代码使用逗号 , 作为分隔符将字符串 s 分割成一个包含四个元素的字符串数组。注意,由于 split() 方法接受正则表达式作为参数,因此需要使用 \\, 来转义逗号。
2.1.8 拼接字符串
使用静态方法 join(CharSequence delimiter, CharSequence... elements) 可以将一个字符串数组连接成一个字符串,并使用指定的分隔符分隔每个字符串。
示例:
String[] arr = {"A", "B", "C"};
String s = String.join("***", arr); // "A***B***C"2.1.9 格式化字符串
String 类提供了 formatted() 方法和 format() 静态方法,用于将其他类型的数据格式化为字符串。
示例:
// String
public class Main {
public static void main(String[] args) {
String s = "Hi %s, your score is %d!";
System.out.println(s.formatted("Alice", 80)); // 输出:Hi Alice, your score is 80!
System.out.println(String.format("Hi %s, your score is %.2f!", "Bob", 59.5)); // 输出:Hi Bob, your score is 59.50!
}
}常用的占位符包括:
| 格式说明符 | 描述 |
|---|---|
%s | 用于显示字符串。 |
%d | 用于显示整数。 |
%x | 用于显示十六进制整数。 |
%f | 用于显示浮点数。 |
占位符还可以包含格式信息,例如 %.2f 表示显示两位小数。 如果不确定使用哪种占位符,可以使用 %s,因为它可以显示任何数据类型。
2.1.10 类型转换
可以使用静态方法 valueOf() 将任意基本类型或引用类型转换为字符串。valueOf() 方法是一个重载方法,编译器会根据参数类型自动选择合适的方法。
示例:
String.valueOf(123); // "123"
String.valueOf(45.67); // "45.67"
String.valueOf(true); // "true"
String.valueOf(new Object()); // 类似java.lang.Object@636be97c要将字符串转换为其他类型,需要使用相应类型的 parse 方法。例如,
将字符串转换为
int类型:int n1 = Integer.parseInt("123"); // 123 int n2 = Integer.parseInt("ff", 16); // 按十六进制转换,255将字符串转换为
boolean类型:boolean b1 = Boolean.parseBoolean("true"); // true boolean b2 = Boolean.parseBoolean("FALSE"); // false
需要注意的是,Integer 类有一个 getInteger(String) 方法,它不是将字符串转换为 int,而是将字符串对应的系统属性值转换为 Integer 对象。
示例:
Integer.getInteger("java.version"); // 版本号,例如 112.1.11 转换为 char[]
String 和 char[] 类型可以相互转换:
String转换为char[]: 使用toCharArray()方法。char[] cs = "Hello".toCharArray(); // String -> char[]char[]转换为String: 使用new String(char[])构造方法。String s = new String(cs); // char[] -> String
当通过 new String(char[]) 创建新的 String 实例时,会复制一份 char[] 数组的内容,而不是直接引用传入的 char[] 数组。因此,修改原始的 char[] 数组不会影响 String 实例。
示例:
// String <-> char[]
public class Main {
public static void main(String[] args) {
char[] cs = "Hello".toCharArray();
String s = new String(cs);
System.out.println(s); // 输出:Hello
cs[0] = 'X';
System.out.println(s); // 输出:Hello
}
}从 String 的不变性设计可以看出,如果传入的对象有可能改变,我们需要复制而不是直接引用。
例如,以下代码展示了一个 Score 类,用于保存一组学生的成绩:
// int[]
import java.util.Arrays;
public class Main {
public static void main(String[] args) {
int[] scores = new int[] { 88, 77, 51, 66 };
Score s = new Score(scores);
s.printScores(); // [88, 77, 51, 66]
scores[2] = 99;
s.printScores(); // [88, 77, 99, 66]
}
}
class Score {
private int[] scores;
public Score(int[] scores) {
this.scores = scores;
}
public void printScores() {
System.out.println(Arrays.toString(scores));
}
}观察两次输出,由于 Score 内部直接引用了外部传入的 int[] 数组,这会造成外部代码对 int[] 数组的修改,影响到 Score 类的字段。如果外部代码不可信,这就会造成安全隐患。
修改建议
为了避免外部修改影响 Score 对象,应该在 Score 类的构造方法中复制一份 scores 数组。
修改后的 Score 类如下:
class Score {
private int[] scores;
public Score(int[] scores) {
this.scores = Arrays.copyOf(scores, scores.length); // 复制数组
}
public void printScores() {
System.out.println(Arrays.toString(scores));
}
}通过使用 Arrays.copyOf() 方法,Score 对象内部持有一份独立的数组副本,从而避免了外部修改对 Score 对象的影响。
2.1.12 字符编码
常见编码方式如下:
- ASCII 编码: 美国国家标准学会 (ANSI) 制定,用于编码英文字母、数字和常用符号。使用一个字节表示,编码范围从 0 到 127,最高位始终为 0。
- GB2312 编码: 用于编码汉字,使用两个字节表示一个汉字,其中第一个字节的最高位始终为 1,以便和 ASCII 编码区分开。
- Shift_JIS 编码: 用于编码日文。
- EUC-KR 编码: 用于编码韩文。
- Unicode 编码: 统一全球所有语言的编码,将世界上主要语言都纳入同一个编码,避免了不同编码之间的冲突。Unicode 编码需要两个或更多字节表示。
- UTF-8 编码: 一种变长编码,用于将固定长度的 Unicode 编码变成 1~4 字节的变长编码。UTF-8 编码兼容 ASCII 编码,且具有较强的容错能力。
以下是一些字符在不同编码方式下的示例:
英文字符
'A'的 ASCII 编码和 Unicode 编码:┌────┐ ASCII: │ 41 │ └────┘ ┌────┬────┐ Unicode: │ 00 │ 41 │ └────┴────┘英文字符的 Unicode 编码就是在 ASCII 编码前添加一个
00字节。中文字符
'中'的 GB2312 编码和 Unicode 编码:┌────┬────┐ GB2312: │ d6 │ d0 │ └────┴────┘ ┌────┬────┐ Unicode: │ 4e │ 2d │ └────┴────┘
在 Java 中,char 类型实际上就是两个字节的 Unicode 编码。
将字符串转换为其他编码的
byte[]数组:byte[] b1 = "Hello".getBytes(); // 按系统默认编码转换,不推荐 byte[] b2 = "Hello".getBytes("UTF-8"); // 按UTF-8编码转换 byte[] b3 = "Hello".getBytes("GBK"); // 按GBK编码转换 byte[] b4 = "Hello".getBytes(StandardCharsets.UTF_8); // 按UTF-8编码转换注意:转换编码后,就不再是
char类型,而是byte类型表示的数组。将已知编码的
byte[]数组转换为字符串:byte[] b = ... String s1 = new String(b, "GBK"); // 按GBK转换 String s2 = new String(b, StandardCharsets.UTF_8); // 按UTF-8转换
始终牢记
Java 的 String 和 char 在内存中总是以 Unicode 编码表示。
2.1.13 JDK 版本与 String 存储优化
不同版本的 JDK 对 String 类的内部存储方式进行了不同的优化:
早期 JDK 版本:
String总是以char[]存储。public final class String { private final char[] value; private final int offset; private final int count; }较新的 JDK 版本:
String以byte[]存储。如果String仅包含 ASCII 字符,则每个byte存储一个字符;否则,每两个byte存储一个字符。这种优化旨在节省内存,因为大量的短字符串通常仅包含 ASCII 字符。public final class String { private final byte[] value; private final byte coder; // 0 = LATIN1, 1 = UTF16 }对于使用者来说,
String内部的优化不影响任何已有代码,因为其public方法签名是不变的。
2.2 StringBuilder
Java 编译器对 String 做了特殊处理,允许我们直接使用 + 拼接字符串。然而,在循环中使用 + 拼接字符串会产生大量临时对象,影响性能。
2.2.1 循环拼接字符串的性能问题
考虑以下代码:
String s = "";
for (int i = 0; i < 1000; i++) {
s = s + "," + i;
}这段代码虽然简洁,但效率低下。每次循环都会创建一个新的 String 对象,并将旧的字符串对象抛弃。大量的临时对象会占用内存,并影响垃圾回收 (GC) 的效率。
2.2.2 StringBuilder 的高效拼接
为了高效拼接字符串,Java 标准库提供了 StringBuilder 类。StringBuilder 是一个可变对象,可以预先分配缓冲区。向 StringBuilder 中新增字符时,不会创建新的临时对象,从而避免了性能问题。
示例代码:
StringBuilder sb = new StringBuilder(1024);
for (int i = 0; i < 1000; i++) {
sb.append(',');
sb.append(i);
}
String s = sb.toString();这段代码使用 StringBuilder 避免了循环中产生大量临时对象的问题,提高了字符串拼接的效率。
对于普通的字符串 + 操作,无需手动改写为 StringBuilder。Java 编译器在编译时会自动将多个连续的 + 操作编码为 StringConcatFactory 的操作。在运行时,StringConcatFactory 会自动将字符串连接操作优化为数组复制或者 StringBuilder 操作。
String a = "a";
String b = "b";
String c = "c";
String result = a + b + c; // 编译器优化,相当于 "abc"
String longString = "";
for (int i = 0; i < 10; i++) {
longString = longString + a + b + c; // 循环内,每次迭代都会创建新的 String 对象,性能差
}2.2.3 链式操作
StringBuilder 支持链式操作,使得代码更加简洁易读。
// 链式操作
public class Main {
public static void main(String[] args) {
var sb = new StringBuilder(1024);
sb.append("Mr ")
.append("Bob")
.append("!")
.insert(0, "Hello, ");
System.out.println(sb.toString()); // 输出:Hello, Mr Bob!
}
}上述代码展示了 StringBuilder 的链式操作。通过连续调用 append() 和 insert() 方法,可以方便地构建字符串。实现链式操作的关键在于 append() 方法返回 this,使得可以连续调用自身的方法。
2.2.4 自定义链式操作类
可以仿照 StringBuilder,设计支持链式操作的类。例如,一个可以不断增加的计数器:
// 链式操作
public class Main {
public static void main(String[] args) {
Adder adder = new Adder();
adder.add(3)
.add(5)
.inc()
.add(10);
System.out.println(adder.value());
}
}
class Adder {
private int sum = 0;
public Adder add(int n) {
sum += n;
return this;
}
public Adder inc() {
sum ++;
return this;
}
public int value() {
return sum;
}
}在这个 Adder 类中,add() 和 inc() 方法都返回 this,从而支持链式操作。
2.2.5 StringBuffer
StringBuffer 是 Java 早期的一个 StringBuilder 的线程安全版本。它通过同步来保证多个线程操作 StringBuffer 的安全性,但是同步会带来执行速度的下降。StringBuilder 和 StringBuffer 接口完全相同,现在完全没有必要使用 StringBuffer。
2.3 StringJoiner
在 Java 中,高效拼接字符串通常使用 StringBuilder。然而,当需要以特定分隔符连接字符串数组时,StringBuilder 的实现较为繁琐,需要手动处理分隔符以及去除末尾多余分隔符的问题。为了简化此类操作,Java 提供了 StringJoiner 类。
2.3.1 StringJoiner 的基本用法
StringJoiner 专门用于构造以分隔符分隔的字符串序列。
import java.util.StringJoiner;
public class Main {
public static void main(String[] args) {
String[] names = {"Bob", "Alice", "Grace"};
var sj = new StringJoiner(", ");
for (String name : names) {
sj.add(name);
}
System.out.println(sj.toString()); // 输出:Bob, Alice, Grace
}
}上述代码展示了 StringJoiner 的基本用法:首先创建一个 StringJoiner 对象,指定分隔符为 ", ";然后,循环遍历字符串数组,依次调用 add() 方法将字符串添加到 StringJoiner 中;最后,调用 toString() 方法获取拼接后的字符串。
2.3.2 StringJoiner 指定前缀和后缀
如果需要在拼接的字符串序列的开头和结尾添加特定的字符串,可以在创建 StringJoiner 对象时指定 prefix(前缀)和 suffix(后缀):
import java.util.StringJoiner;
public class Main {
public static void main(String[] args) {
String[] names = {"Bob", "Alice", "Grace"};
var sj = new StringJoiner(", ", "Hello ", "!");
for (String name : names) {
sj.add(name);
}
System.out.println(sj.toString()); // 输出:Hello Bob, Alice, Grace!
}
}在这个例子中,创建 StringJoiner 对象时,除了指定分隔符 ", " 外,还指定了前缀 "Hello " 和后缀 "!"。
2.3.3 String.join() 的使用
当不需要指定前缀和后缀时,可以使用 String 类的静态方法 join() 来简化字符串拼接操作。String.join() 方法在内部使用了 StringJoiner。
String[] names = {"Bob", "Alice", "Grace"};
var s = String.join(", ", names);2.4 包装类型
在 Java 中,数据类型分为基本类型和引用类型。基本类型包括 byte、short、int、long、boolean、float、double、char;引用类型则是所有的 class 和 interface 类型。引用类型可以赋值为 null,表示空,但基本类型不能赋值为 null。
那么,如何将一个基本类型视为对象(引用类型)呢? 这就引出了包装类型的概念。
2.4.1 包装类的概念
为了能够像操作对象一样操作基本数据类型,Java 提供了对应的包装类型(Wrapper Class)。 每种基本类型都对应一个包装类型:
| 基本类型 | 对应的引用类型 |
|---|---|
| boolean | java.lang.Boolean |
| byte | java.lang.Byte |
| short | java.lang.Short |
| int | java.lang.Integer |
| long | java.lang.Long |
| float | java.lang.Float |
| double | java.lang.Double |
| char | java.lang.Character |
例如,int 的包装类型是 Integer。 我们可以通过 Integer 类将 int 类型转换为引用类型,并进行相关操作。
public class Integer {
private int value;
public Integer(int value) {
this.value = value;
}
public int intValue() {
return this.value;
}
}上述代码展示了一个简单的 Integer 类的实现,它包含一个 int 类型的实例字段 value,以及构造方法和 intValue() 方法,用于将 Integer 对象转换为 int 类型。
2.4.2 包装类的使用
可以直接使用 Java 核心库提供的包装类型,而无需自己定义。 例如:
public class Main {
public static void main(String[] args) {
int i = 100;
// 通过new操作符创建Integer实例(不推荐使用,会有编译警告):
Integer n1 = new Integer(i);
// 通过静态方法valueOf(int)创建Integer实例:
Integer n2 = Integer.valueOf(i);
// 通过静态方法valueOf(String)创建Integer实例:
Integer n3 = Integer.valueOf("100");
System.out.println(n3.intValue()); // 100
}
}这段代码演示了如何使用 Integer 类创建实例。 其中,Integer.valueOf(int) 和 Integer.valueOf(String) 是创建 Integer 实例的常用方法。
2.4.3 自动装箱与自动拆箱
Java 编译器可以自动在基本类型和包装类型之间进行转换,这就是自动装箱(Auto Boxing)和自动拆箱(Auto Unboxing)。
- 自动装箱: 将基本类型自动转换为对应的包装类型。 例如:
Integer n = 100; // 编译器自动使用Integer.valueOf(int) - 自动拆箱: 将包装类型自动转换为对应的基本类型。 例如:
int x = n; // 编译器自动使用Integer.intValue()
需要注意的是,自动装箱和自动拆箱只发生在编译阶段,目的是为了减少代码量。 此外,自动拆箱可能会导致 NullPointerException,例如:
public class Main {
public static void main(String[] args) {
Integer n = null;
int i = n; // NullPointerException
}
}上述代码中,由于 n 为 null,在自动拆箱时会调用 n.intValue(),从而导致 NullPointerException。
2.4.4 不变类
所有的包装类型都是不变类(Immutable Class)。 以 Integer 为例,其源码如下:
public final class Integer {
private final int value;
}Integer 类被声明为 final,且其内部的 value 字段也被声明为 final,这意味着一旦创建了 Integer 对象,就无法修改其值。
在比较两个 Integer 实例时,绝对不能使用 ==,因为 Integer 是引用类型,必须使用 equals() 方法进行比较。
public class Main {
public static void main(String[] args) {
Integer x = 127;
Integer y = 127;
Integer m = 99999;
Integer n = 99999;
System.out.println("x == y: " + (x==y)); // true
System.out.println("m == n: " + (m==n)); // false
System.out.println("x.equals(y): " + x.equals(y)); // true
System.out.println("m.equals(n): " + m.equals(n)); // true
}
}上述代码中,使用 == 比较较小的 Integer 实例时,结果可能为 true,这是因为 Integer.valueOf() 方法对较小的数会返回相同的缓存实例。 但是,绝不能依赖这种实现细节,必须使用 equals() 方法进行比较。
创建 Integer 对象时,推荐使用静态工厂方法 Integer.valueOf(),而不是 new Integer()。 因为 Integer.valueOf() 方法可能会返回缓存的实例,从而节省内存。 这种能创建“新”对象的静态方法称为静态工厂方法。
2.4.5 进制转换
Integer 类提供了许多实用的静态方法,例如,parseInt() 方法可以将字符串解析为整数:
int x1 = Integer.parseInt("100"); // 100
int x2 = Integer.parseInt("100", 16); // 256,因为按16进制解析Integer 类还可以将整数格式化为指定进制的字符串:
public class Main {
public static void main(String[] args) {
System.out.println(Integer.toString(100)); // "100",表示为10进制
System.out.println(Integer.toString(100, 36)); // "2s",表示为36进制
System.out.println(Integer.toHexString(100)); // "64",表示为16进制
System.out.println(Integer.toOctalString(100)); // "144",表示为8进制
System.out.println(Integer.toBinaryString(100)); // "1100100",表示为2进制
}
}需要注意的是,上述方法的输出都是 String 类型。 在计算机内存中,数据总是以二进制形式存储。 我们经常使用的 System.out.println(n) 是依靠核心库自动将整数格式化为 10 进制输出并显示在屏幕上,而 Integer.toHexString(n) 则将整数格式化为 16 进制。
数据存储和显示要分离,这是程序设计的一个重要原则。
Java 的包装类型还定义了一些有用的静态变量,例如:
// boolean只有两个值true/false,其包装类型只需要引用Boolean提供的静态字段:
Boolean t = Boolean.TRUE;
Boolean f = Boolean.FALSE;
// int可表示的最大/最小值:
int max = Integer.MAX_VALUE; // 2147483647
int min = Integer.MIN_VALUE; // -2147483648
// long类型占用的bit和byte数量:
int sizeOfLong = Long.SIZE; // 64 (bits)
int bytesOfLong = Long.BYTES; // 8 (bytes)所有的整数和浮点数的包装类型都继承自 Number,因此,可以方便地通过包装类型获取各种基本类型:
// 向上转型为Number:
Number num = new Integer(999);
// 获取byte, int, long, float, double:
byte b = num.byteValue();
int n = num.intValue();
long ln = num.longValue();
float f = num.floatValue();
double d = num.doubleValue();2.4.6 处理无符号整型
Java 没有无符号整型(Unsigned)的基本数据类型。 byte、short、int 和 long 都是带符号整型,最高位是符号位。 要处理无符号整型,需要借助包装类型的静态方法。
例如,Byte.toUnsignedInt(byte) 方法可以将一个 byte 转换为无符号的 int:
public class Main {
public static void main(String[] args) {
byte x = -1;
byte y = 127;
System.out.println(Byte.toUnsignedInt(x)); // 255
System.out.println(Byte.toUnsignedInt(y)); // 127
}
}因为 byte 的 -1 的二进制表示是 11111111,以无符号整型转换后的 int 就是 255。
2.5 JavaBean
JavaBean 是一种特殊的 Java 类,它遵循特定的命名规范,主要用于封装数据。一个标准的 JavaBean 通常包含以下几个部分:
- 若干私有(
private)的实例字段。 - 通过公有(
public)方法来访问和修改这些字段。
2.5.1 命名规范
JavaBean 的核心在于其命名规范,这使得 JavaBean 能够被工具和框架自动识别和处理。
Getter 方法:用于读取属性值。
- 对于非
boolean类型的属性xyz,getter 方法的命名为getXyz()。 - 对于
boolean类型的属性xyz,getter 方法的命名通常为isXyz()。
- 对于非
Setter 方法:用于设置属性值。
- 对于属性
xyz,setter 方法的命名为setXyz(Type value),其中Type是属性的类型。
- 对于属性
符合以上命名规则的类,就被认为是 JavaBean。
2.5.2 属性
属性是 JavaBean 中非常重要的概念,它实际上是对 getter 和 setter 方法的一种抽象。
- 属性:一组对应的读方法(
getter)和写方法(setter)。例如,name属性对应getName()和setName(String)方法。 - 只读属性 (Read-only Property):只有
getter方法而没有setter方法的属性。 - 只写属性 (Write-only Property):只有
setter方法而没有getter方法的属性(不常见)。
需要注意的是,属性并不一定需要对应一个实际的字段,getter 和 setter 方法内部可以进行一些计算或逻辑处理。例如,可以根据 age 字段的值来判断 isChild 属性。
public class Person {
private String name;
private int age;
public String getName() { return this.name; }
public void setName(String name) { this.name = name; }
public int getAge() { return this.age; }
public void setAge(int age) { this.age = age; }
public boolean isChild() {
return age <= 6;
}
}2.5.3 JavaBean 的作用
JavaBean 的主要作用包括:
- 数据封装:将一组相关的数据封装成一个对象,便于在不同的组件之间传递。
- 工具支持:JavaBean 可以被 IDE 工具分析,自动生成读写属性的代码,方便可视化设计和开发。
2.5.4 枚举 JavaBean 属性
Java 提供了 Introspector 类,可以用来枚举 JavaBean 的所有属性,并获取对应的读写方法。
import java.beans.*;
public class Main {
public static void main(String[] args) throws Exception {
BeanInfo info = Introspector.getBeanInfo(Person.class);
for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
System.out.println(pd.getName());
System.out.println(" " + pd.getReadMethod());
System.out.println(" " + pd.getWriteMethod());
}
}
}
class Person {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}代码解释:
Introspector.getBeanInfo(Person.class)方法会返回一个BeanInfo对象,其中包含了Person类的属性信息。info.getPropertyDescriptors()方法返回一个PropertyDescriptor数组,每个PropertyDescriptor对象描述了Person类的一个属性。- 在循环中,
pd.getName()用于获取属性的名称,pd.getReadMethod()用于获取属性的getter方法,pd.getWriteMethod()用于获取属性的setter方法。 - 这里需要注意的是,即使你没有显式地定义某个属性,
Introspector也会将其从父类(例如Object类的getClass()方法)中继承过来。
2.6 枚举类
在 Java 中,我们通常使用 static final 来定义常量。例如,用不同的 int 值表示周一到周日:
public class Weekday {
public static final int SUN = 0;
public static final int MON = 1;
public static final int TUE = 2;
public static final int WED = 3;
public static final int THU = 4;
public static final int FRI = 5;
public static final int SAT = 6;
}使用这些常量时,可以这样引用:
if (day == Weekday.SAT || day == Weekday.SUN) {
// TODO: rest at home
}也可以将常量定义为字符串类型,例如,定义三种颜色:
public class Color {
public static final String RED = "r";
public static final String GREEN = "g";
public static final String BLUE = "b";
}使用常量时,可以这样引用:
String color = ...
if (Color.RED.equals(color)) {
// TODO:
}但是,无论是 int 常量还是 String 常量,当使用它们来表示一组枚举值时,存在一个严重的问题:编译器无法检查每个值的合理性。例如:
if (weekday == 6 || weekday == 7) {
if (tasks == Weekday.MON) {
// TODO:
}
}上述代码编译和运行均不会报错,但存在两个问题:
Weekday定义的常量范围是0~6,并不包含7,编译器无法检查不在枚举中的int值;- 定义的常量仍可与其他变量比较,但其用途并非是枚举星期值。
2.6.1 enum
为了让编译器能自动检查某个值是否在枚举的集合内,并且为了区分不同用途的枚举类型,我们可以使用 enum 来定义枚举类:
public class Main {
public static void main(String[] args) {
Weekday day = Weekday.SUN;
if (day == Weekday.SAT || day == Weekday.SUN) {
System.out.println("Rest at home!");
} else {
System.out.println("Work at office!");
}
}
}
enum Weekday {
SUN, MON, TUE, WED, THU, FRI, SAT;
}定义枚举类通过关键字 enum 实现,只需依次列出枚举的常量名。
和 int 定义的常量相比,使用 enum 定义枚举有如下好处:
enum常量本身带有类型信息,例如Weekday.SUN的类型是Weekday,编译器会自动检查出类型错误。例如,下面的语句不可能编译通过:int day = 1; if (day == Weekday.SUN) { // Compile error: bad operand types for binary operator '==' }不可能引用到非枚举的值,因为无法通过编译。
不同类型的枚举不能互相比较或者赋值,因为类型不符。例如,不能给一个
Weekday枚举类型的变量赋值为Color枚举类型的值:Weekday x = Weekday.SUN; // ok! Weekday y = Color.RED; // Compile error: incompatible types
这就使得编译器可以在编译期自动检查出所有可能的潜在错误。
2.6.2 enum 的比较
使用 enum 定义的枚举类是一种引用类型。引用类型比较,通常要使用 equals() 方法,但 enum 类型可以例外。
这是因为 enum 类型的每个常量在 JVM 中只有一个唯一实例,所以可以直接用 == 比较:
if (day == Weekday.FRI) { // ok!
}
if (day.equals(Weekday.SUN)) { // ok, but more code!
}2.6.3 enum 类型
通过 enum 定义的枚举类,和其他的 class 有什么区别?
答案是本质上没有区别。enum 定义的类型就是 class,只不过它有以下几个特点:
- 定义的
enum类型总是继承自java.lang.Enum,且无法被继承; - 只能定义出
enum的实例,而无法通过new操作符创建enum的实例; - 定义的每个实例都是引用类型的唯一实例;
- 可以将
enum类型用于switch语句。
例如,定义的 Color 枚举类:
public enum Color {
RED, GREEN, BLUE;
}编译器编译出的 class 大概就像这样:
public final class Color extends Enum { // 继承自Enum,标记为final class
// 每个实例均为全局唯一:
public static final Color RED = new Color();
public static final Color GREEN = new Color();
public static final Color BLUE = new Color();
// private构造方法,确保外部无法调用new操作符:
private Color() {}
}所以,编译后的 enum 类和普通 class 并没有任何区别。但是我们自己无法按定义普通 class 那样来定义 enum,必须使用 enum 关键字,这是 Java 语法规定的。
因为 enum 是一个 class,每个枚举的值都是 class 实例,因此,这些实例有一些方法:
name()
返回常量名,例如:
String s = Weekday.SUN.name(); // "SUN"ordinal()
返回定义的常量的顺序,从 0 开始计数,例如:
int n = Weekday.MON.ordinal(); // 1改变枚举常量定义的顺序就会导致 ordinal() 返回值发生变化。例如:
public enum Weekday {
SUN, MON, TUE, WED, THU, FRI, SAT;
}和
public enum Weekday {
MON, TUE, WED, THU, FRI, SAT, SUN;
}的 ordinal 就是不同的。如果在代码中编写了类似 if(x.ordinal()==1) 这样的语句,就要保证 enum 的枚举顺序不能变。新增的常量必须放在最后。
因此,在实际开发中,不应该依赖 ordinal() 来实现枚举常量和 int 转换。一个不好的例子是,以下代码使用 ordinal() 来构造文件名:
String task = Weekday.MON.ordinal() + "/ppt";
saveToFile(task);如果 Weekday 枚举类型的常量顺序发生变化,Weekday.MON.ordinal() 的返回值也会改变,导致生成错误的文件名。
为了避免 ordinal() 方法可能带来的问题,建议为枚举常量添加自定义字段。由于 enum 本身就是一个 class,所以可以定义 private 的构造方法,并为每个枚举常量添加字段。以下代码展示了如何通过自定义 dayValue 字段来表示星期几:
public class Main {
public static void main(String[] args) {
Weekday day = Weekday.SUN;
if (day.dayValue == 6 || day.dayValue == 0) {
System.out.println("Rest at home!");
} else {
System.out.println("Work at office!");
}
}
}
enum Weekday {
MON(1), TUE(2), WED(3), THU(4), FRI(5), SAT(6), SUN(0);
public final int dayValue;
private Weekday(int dayValue) {
this.dayValue = dayValue;
}
}在上述代码中,我们为 Weekday 枚举类添加了一个整数字段 dayValue,并在构造方法中初始化它。每个枚举常量在声明时,都需要传入一个对应的 int 值。通过这种方式,可以将枚举常量与特定的数值关联起来,从而避免了依赖 ordinal() 方法可能带来的问题。
这样就无需担心顺序的变化,新增枚举常量时,也需要指定一个 int 值。
警告
枚举类的字段也可以是非 final 类型,即可以在运行期修改,但是不推荐这样做!
默认情况下,对枚举常量调用 toString() 会返回和 name() 一样的字符串。但是,toString() 可以被覆写,而 name() 则不行。我们可以给 Weekday 添加 toString() 方法:
public class Main {
public static void main(String[] args) {
Weekday day = Weekday.SUN;
if (day.dayValue == 6 || day.dayValue == 0) {
System.out.println("Today is " + day + ". Rest at home!");
} else {
System.out.println("Today is " + day + ". Work at office!");
}
}
}
enum Weekday {
MON(1, "星期一"), TUE(2, "星期二"), WED(3, "星期三"), THU(4, "星期四"), FRI(5, "星期五"), SAT(6, "星期六"), SUN(0, "星期日");
public final int dayValue;
private final String chinese;
private Weekday(int dayValue, String chinese) {
this.dayValue = dayValue;
this.chinese = chinese;
}
@Override
public String toString() {
return this.chinese;
}
}覆写 toString() 的目的是在输出时更有可读性。
警告
判断枚举常量的名字,要始终使用 name() 方法,绝不能调用 toString()!
2.6.4 switch
最后,枚举类可以应用在 switch 语句中。因为枚举类天生具有类型信息和有限个枚举常量,所以比 int、String 类型更适合用在 switch 语句中:
public class Main {
public static void main(String[] args) {
Weekday day = Weekday.SUN;
switch(day) {
case MON:
case TUE:
case WED:
case THU:
case FRI:
System.out.println("Today is " + day + ". Work at office!");
break;
case SAT:
case SUN:
System.out.println("Today is " + day + ". Rest at home!");
break;
default:
throw new RuntimeException("cannot process " + day);
}
}
}
enum Weekday {
MON, TUE, WED, THU, FRI, SAT, SUN;
}加上 default 语句,可以在漏写某个枚举常量时自动报错,从而及时发现错误。
2.7 记录类
在 Java 中,String 和 Integer 这样的类型都是不变类 (Immutable Class)。不变类具有以下特点:
- 类定义使用
final关键字,使其无法被继承。 - 类的每个字段都使用
final关键字,保证实例创建后,任何字段的值都不能被修改。
2.7.1 不变类的传统实现方式
假设我们需要定义一个 Point 类,它有两个变量 x 和 y,并且希望它是一个不变类,通常会这样编写:
public final class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int x() {
return this.x;
}
public int y() {
return this.y;
}
}为了保证不变类的比较,还需要正确地覆写 equals() 和 hashCode() 方法,以便在集合类中能够正常使用。虽然这些代码写起来很简单,但是很繁琐。
2.7.2 使用 record 关键字简化不变类定义
从 Java 14 开始,引入了新的 record 类。使用 record 关键字可以更简洁地定义不变类。将上述 Point 类改写为 record 类,代码如下:
// Record
public class Main {
public static void main(String[] args) {
Point p = new Point(123, 456);
System.out.println(p.x());
System.out.println(p.y());
System.out.println(p);
}
}
record Point(int x, int y) {}上述 Point 类的 record 定义,相当于以下代码:
final class Point extends Record {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int x() {
return this.x;
}
public int y() {
return this.y;
}
public String toString() {
return String.format("Point[x=%s, y=%s]", x, y);
}
public boolean equals(Object o) {
...
}
public int hashCode() {
...
}
}除了使用 final 修饰 class 以及每个字段外,编译器还自动为我们创建了构造方法、和字段名同名的方法,以及覆写 toString()、equals() 和 hashCode() 方法。换句话说,使用 record 关键字,可以一行写出一个不变类。和 enum 类似,我们不能直接从 Record 派生,只能通过 record 关键字由编译器实现继承。
2.7.3 record 类的构造方法
编译器默认按照 record 声明的变量顺序自动创建一个构造方法,并在方法内给字段赋值。如果我们需要对参数进行检查,可以使用 Compact Constructor。假设 Point 类的 x 和 y 不允许为负数,可以这样编写:
public record Point(int x, int y) {
public Point {
if (x < 0 || y < 0) {
throw new IllegalArgumentException();
}
}
}上述代码中的 public Point {...} 被称为 Compact Constructor,它的目的是让我们编写检查逻辑。编译器最终生成的构造方法如下:
public final class Point extends Record {
public Point(int x, int y) {
// 这是我们编写的Compact Constructor:
if (x < 0 || y < 0) {
throw new IllegalArgumentException();
}
// 这是编译器继续生成的赋值代码:
this.x = x;
this.y = y;
}
...
}2.7.4 静态方法
作为 record 的 Point 仍然可以添加静态方法。一种常用的静态方法是 of() 方法,用来创建 Point:
public record Point(int x, int y) {
public static Point of() {
return new Point(0, 0);
}
public static Point of(int x, int y) {
return new Point(x, y);
}
}这样我们可以写出更简洁的代码:
var z = Point.of();
var p = Point.of(123, 456);2.8 BigInteger
在 Java 中,CPU 原生支持的最大整数范围是 64 位 long 型。long 型整数的运算由 CPU 直接执行,速度非常快。但是,当需要表示超过 long 型范围的整数时,就需要使用 java.math.BigInteger 类。BigInteger 内部使用 int[] 数组来模拟表示任意大小的整数。
BigInteger bi = new BigInteger("1234567890");
System.out.println(bi.pow(5)); // 2867971860299718107233761438093672048294900000对 BigInteger 对象进行运算时,必须使用实例方法。例如,进行加法运算:
BigInteger i1 = new BigInteger("1234567890");
BigInteger i2 = new BigInteger("12345678901234567890");
BigInteger sum = i1.add(i2); // 12345678902469135780与 long 型整数运算相比,BigInteger 不受范围限制,但其运算速度较慢。
2.8.1 BigInteger 转换为 long 型
可以将 BigInteger 对象转换为 long 型:
BigInteger i = new BigInteger("123456789000");
System.out.println(i.longValue()); // 123456789000
System.out.println(i.multiply(i).longValueExact()); // java.lang.ArithmeticException: BigInteger out of long range当使用 longValueExact() 方法进行转换时,如果 BigInteger 的值超出了 long 型的范围,会抛出 ArithmeticException 异常。
2.8.2 BigInteger 与 Number 类
BigInteger 与 Integer、Long 一样,都是不可变类,并且继承自 Number 类。Number 类定义了转换为基本类型的方法:
- 转换为
byte:byteValue() - 转换为
short:shortValue() - 转换为
int:intValue() - 转换为
long:longValue() - 转换为
float:floatValue() - 转换为
double:doubleValue()
通过上述方法,可以将 BigInteger 对象转换为基本类型。需要注意的是,如果 BigInteger 表示的范围超过了基本类型的范围,转换时会丢失高位信息,导致结果不准确。如果需要准确地转换为基本类型,可以使用 intValueExact()、longValueExact() 等方法,这些方法在转换时如果超出范围,会直接抛出 ArithmeticException 异常。
2.8.3 BigInteger 转换为 float 类型
如果 BigInteger 的值超过了 float 的最大范围(3.4x1038),那么转换为 float 时会返回 Infinity。
// BigInteger to float
import java.math.BigInteger;
public class Main {
public static void main(String[] args) {
BigInteger n = new BigInteger("999999").pow(99);
float f = n.floatValue();
System.out.println(f); // Infinity
}
}2.9 BigDecimal
java.math.BigDecimal 类用于表示任意大小且精度完全准确的浮点数,这与 BigInteger 类类似,后者用于表示任意大小的整数。
BigDecimal bd = new BigDecimal("123.4567");
System.out.println(bd.multiply(bd)); // 15241.55677489上述代码展示了如何创建一个 BigDecimal 对象,并使用 multiply() 方法进行乘法运算。 由于 BigDecimal 能够精确表示浮点数,因此在进行需要高精度的运算时非常有用。
2.9.1 Scale
BigDecimal 使用 scale() 方法来表示小数位数。
BigDecimal d1 = new BigDecimal("123.45");
BigDecimal d2 = new BigDecimal("123.4500");
BigDecimal d3 = new BigDecimal("1234500");
System.out.println(d1.scale()); // 2,两位小数
System.out.println(d2.scale()); // 4
System.out.println(d3.scale()); // 0不同的 BigDecimal 对象即使数值相等,也可能因为小数位数不同而具有不同的 scale() 值。
2.9.2 stripTrailingZeros()
stripTrailingZeros() 方法用于去除 BigDecimal 末尾的零,并返回一个数值相等但去除末尾零的 BigDecimal 对象。
BigDecimal d1 = new BigDecimal("123.4500");
BigDecimal d2 = d1.stripTrailingZeros();
System.out.println(d1.scale()); // 4
System.out.println(d2.scale()); // 2,因为去掉了00
BigDecimal d3 = new BigDecimal("1234500");
BigDecimal d4 = d3.stripTrailingZeros();
System.out.println(d3.scale()); // 0
System.out.println(d4.scale()); // -2值得注意的是,如果一个 BigDecimal 的 scale() 返回负数,例如 -2,这表示该数是一个整数,并且末尾有 2 个 0。
2.9.3 setScale()
可以通过 setScale() 方法设置 BigDecimal 的 scale。 如果设置的精度低于原始值,则可以指定使用四舍五入或直接截断的方式进行处理。
import java.math.BigDecimal;
import java.math.RoundingMode;
public class Main {
public static void main(String[] args) {
BigDecimal d1 = new BigDecimal("123.456789");
BigDecimal d2 = d1.setScale(4, RoundingMode.HALF_UP); // 四舍五入,123.4568
BigDecimal d3 = d1.setScale(4, RoundingMode.DOWN); // 直接截断,123.4567
System.out.println(d2);
System.out.println(d3);
}
}2.9.4 divide()
在对 BigDecimal 进行加、减、乘法运算时,精度不会丢失。 但是,在进行除法运算时,如果无法除尽,则必须指定精度和截断方式。
BigDecimal d1 = new BigDecimal("123.456");
BigDecimal d2 = new BigDecimal("23.456789");
BigDecimal d3 = d1.divide(d2, 10, RoundingMode.HALF_UP); // 保留10位小数并四舍五入
// BigDecimal d4 = d1.divide(d2); // 报错:ArithmeticException,因为除不尽如果不对除法设置精度和截断方式,当出现无限循环小数时,会抛出 ArithmeticException 异常。
2.9.5 divideAndRemainder()
BigDecimal 还提供了 divideAndRemainder() 方法,用于同时进行除法和求余运算。
import java.math.BigDecimal;
public class Main {
public static void main(String[] args) {
BigDecimal n = new BigDecimal("12.345");
BigDecimal m = new BigDecimal("0.12");
BigDecimal[] dr = n.divideAndRemainder(m);
System.out.println(dr[0]); // 102
System.out.println(dr[1]); // 0.105
}
}divideAndRemainder() 方法返回一个包含两个 BigDecimal 对象的数组,第一个元素是商(整数部分),第二个元素是余数。 商总是整数,余数不会大于除数。 通过检查余数是否为零,可以判断两个 BigDecimal 是否是整数倍数。
BigDecimal n = new BigDecimal("12.75");
BigDecimal m = new BigDecimal("0.15");
BigDecimal[] dr = n.divideAndRemainder(m);
if (dr[1].signum() == 0) {
// n是m的整数倍
}signum() 方法用于获取 BigDecimal 的符号,如果 BigDecimal 的值为 0,则返回 0;如果大于 0,则返回 1;如果小于 0,则返回 -1。
2.9.6 比较 BigDecimal
在比较两个 BigDecimal 的值是否相等时,需要特别注意 equals() 方法。 equals() 方法不但要求两个 BigDecimal 的值相等,还要求它们的 scale() 相等。
BigDecimal d1 = new BigDecimal("123.456");
BigDecimal d2 = new BigDecimal("123.45600");
System.out.println(d1.equals(d2)); // false,因为scale不同
System.out.println(d1.equals(d2.stripTrailingZeros())); // true,因为d2去除尾部0后scale变为3
System.out.println(d1.compareTo(d2)); // 0 = 相等, -1 = d1 < d2, 1 = d1 > d2因此,比较 BigDecimal 的值是否相等,必须使用 compareTo() 方法。 compareTo() 方法根据两个值的大小分别返回负数、正数和 0,分别表示小于、大于和等于。
务必记住,总是使用 compareTo() 比较两个 BigDecimal 的值,而不要使用 equals()!
2.9.7 BigDecimal 的内部表示
通过查看 BigDecimal 的源码,可以发现一个 BigDecimal 实际上是通过一个 BigInteger 和一个 scale 来表示的。 BigInteger 表示一个完整的整数,而 scale 表示小数位数。
public class BigDecimal extends Number implements Comparable<BigDecimal> {
private final BigInteger intVal;
private final int scale;
}BigDecimal 也是从 Number 类继承的,并且是不可变对象。
2.10 常用工具类
本节将介绍 Java 核心库中几个常用的工具类,它们分别是 Math、HexFormat、Random 和 SecureRandom。
2.10.1 Math 类
Math 类主要用于数学计算,它提供了大量的静态方法,方便进行各种数学运算。
常用方法如下:
| 方法名称 | 描述 |
|---|---|
Math.abs(x) | 返回 x 的绝对值。 |
Math.max(x, y) | 返回 x 和 y 中的最大值。 |
Math.min(x, y) | 返回 x 和 y 中的最小值。 |
Math.pow(x, y) | 返回 x 的 y 次方。 |
Math.sqrt(x) | 返回 x 的平方根。 |
Math.exp(x) | 返回 e 的 x 次方。 |
Math.log(x) | 返回以 e 为底 x 的对数。 |
Math.log10(x) | 返回以 10 为底 x 的对数。 |
Math.sin(x) | 返回 x 的正弦值。 |
Math.cos(x) | 返回 x 的余弦值。 |
Math.tan(x) | 返回 x 的正切值。 |
Math.asin(x) | 返回 x 的反正弦值。 |
Math.acos(x) | 返回 x 的反余弦值。 |
Math.random() | 生成一个 [0, 1) 范围内的随机数。 |
此外,Math 类还提供了一些常用的数学常量:
| 常量名称 | 描述 |
|---|---|
Math.PI | 圆周率 π 的值。 |
Math.E | 自然常数 e 的值。 |
Java 标准库还提供了一个 StrictMath 类,它提供了和 Math 类几乎一样的方法。StrictMath 类保证在所有平台上计算结果都是完全相同的,而 Math 类会尽量针对平台优化计算速度。因此,在绝大多数情况下,使用 Math 类就足够了。
2.10.2 HexFormat
在处理 byte[] 数组时,经常需要与十六进制字符串进行转换。Java 标准库提供了 HexFormat 类,可以方便地进行这种转换。
要将 byte[] 数组转换为十六进制字符串,可以使用 formatHex() 方法:
import java.util.HexFormat;
public class Main {
public static void main(String[] args) throws InterruptedException {
byte[] data = "Hello".getBytes();
HexFormat hf = HexFormat.of();
String hexData = hf.formatHex(data); // 48656c6c6f
}
}HexFormat.of() 创建了一个默认的 HexFormat 实例,然后使用 formatHex() 方法将 byte[] 数组转换为十六进制字符串。
如果需要定制转换格式,可以使用定制的 HexFormat 实例:
// 分隔符为空格,添加前缀0x,大写字母:
HexFormat hf = HexFormat.ofDelimiter(" ").withPrefix("0x").withUpperCase();
hf.formatHex("Hello".getBytes())); // 0x48 0x65 0x6C 0x6C 0x6F上述代码使用 ofDelimiter() 方法设置分隔符为空格,使用 withPrefix() 方法添加前缀 "0x",使用 withUpperCase() 方法将字母转换为大写。
从十六进制字符串到 byte[] 数组的转换,可以使用 parseHex() 方法:
byte[] bs = HexFormat.of().parseHex("48656c6c6f");parseHex() 方法将十六进制字符串转换为 byte[] 数组。
2.10.3 Random
Random 类用于创建伪随机数。伪随机数是指只要给定一个初始的种子,产生的随机数序列是完全一样的。
要生成一个随机数,可以使用 nextInt()、nextLong()、nextFloat() 和 nextDouble() 等方法:
Random r = new Random();
r.nextInt(); // 2071575453,每次都不一样
r.nextInt(10); // 5,生成一个[0,10)之间的int
r.nextLong(); // 8811649292570369305,每次都不一样
r.nextFloat(); // 0.54335...生成一个[0,1)之间的float
r.nextDouble(); // 0.3716...生成一个[0,1)之间的double这些方法分别用于生成 int、long、float 和 double 类型的随机数。其中,nextInt(int bound) 方法可以生成一个 [0, bound) 之间的 int 类型随机数。
每次运行程序,生成的随机数都是不同的,这是因为在创建 Random 实例时,如果不给定种子,就使用系统当前时间戳作为种子,因此每次运行时,种子不同,得到的伪随机数序列就不同。
如果在创建 Random 实例时指定一个种子,就会得到完全确定的随机数序列:
import java.util.Random;
public class Main {
public static void main(String[] args) {
Random r = new Random(12345);
for (int i = 0; i < 10; i++) {
System.out.println(r.nextInt(100));
}
// 51, 80, 41, 28, 55...
}
}上述代码创建了一个以 12345 为种子的 Random 实例,然后生成 10 个 [0, 100) 之间的随机数。由于种子是固定的,因此每次运行程序,生成的随机数序列都是一样的。
实际上,前面使用的 Math.random() 方法内部调用了 Random 类,因此它也是伪随机数,只是无法指定种子。
2.10.4 SecureRandom
有伪随机数,就有真随机数。实际上真正的真随机数只能通过量子力学原理来获取,而我们想要的是一个不可预测的安全的随机数,SecureRandom 类就是用来创建安全的随机数的:
SecureRandom sr = new SecureRandom();
System.out.println(sr.nextInt(100));SecureRandom 类无法指定种子,它使用 RNG (random number generator) 算法。JDK 的 SecureRandom 实际上有多种不同的底层实现,有的使用安全随机种子加上伪随机数算法来产生安全的随机数,有的使用真正的随机数生成器。
实际使用的时候,可以优先获取高强度的安全随机数生成器,如果没有提供,再使用普通等级的安全随机数生成器:
import java.util.Arrays;
import java.security.SecureRandom;
import java.security.NoSuchAlgorithmException;
public class Main {
public static void main(String[] args) {
SecureRandom sr = null;
try {
sr = SecureRandom.getInstanceStrong(); // 获取高强度安全随机数生成器
} catch (NoSuchAlgorithmException e) {
sr = new SecureRandom(); // 获取普通的安全随机数生成器
}
byte[] buffer = new byte[16];
sr.nextBytes(buffer); // 用安全随机数填充buffer
System.out.println(Arrays.toString(buffer));
}
}上述代码首先尝试获取高强度的安全随机数生成器,如果获取失败,则使用普通的安全随机数生成器。然后,使用 nextBytes() 方法生成 16 个随机字节,并将它们存储在 buffer 数组中。
SecureRandom 的安全性是通过操作系统提供的安全的随机种子来生成随机数。这个种子是通过 CPU 的热噪声、读写磁盘的字节、网络流量等各种随机事件产生的“熵”。
在密码学中,安全的随机数非常重要。如果使用不安全的伪随机数,所有加密体系都将被攻破。因此,必须使用 SecureRandom 类来产生安全的随机数。