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"); // 版本号,例如 11
2.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
类来产生安全的随机数。