JAVA核心类
整理BY:Misayaas
内容来自廖雪峰的官方网站: https://www.liaoxuefeng.com/
字符串和编码
String
字符串在String
内部是通过一个char[]
数组表示的1
String s2 = new String(new char[] {'H', 'e', 'l', 'l', 'o', '!'});
Java提供了"..."
这种字符串字面量表示方法
Java字符串的一个重要特点就是字符串不可变。这种不可变性是通过内部的private final char[]
字段,以及没有任何修改char[]
的方法实现的
字符串比较
当我们想要比较两个字符串是否相同时,要特别注意,我们实际上是想比较字符串的内容是否相同。必须使用equals()
方法而不能用==
1
2
3
4
5
6
7
8public class Main {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
}
}
从表面上看,两个字符串用==
和equals()
比较都为true
,但实际上那只是Java编译器在编译期,会自动把所有相同的字符串当作一个对象放入常量池,自然s1
和s2
的引用就是相同的
- 所以,这种
==
比较返回true
纯属巧合
换一种写法,==
比较就会失败1
2
3
4
5
6
7
8public class Main {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "HELLO".toLowerCase();
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
}
}
要忽略大小写比较,使用equalsIgnoreCase()
方法
String
类还提供了多种方法来搜索子串、提取子串。常用的方法有:1
2// 是否包含子串:
"Hello".contains("ll"); // true
- 注意到
contains()
方法的参数是CharSequence
而不是String
,因为CharSequence
是String
实现的一个接口
搜索子串的更多的例子:1
2
3
4"Hello".indexOf("l"); // 2
"Hello".lastIndexOf("l"); // 3
"Hello".startsWith("He"); // true
"Hello".endsWith("lo"); // true
提取子串的例子:1
2"Hello".substring(2); // "llo"
"Hello".substring(2, 4); "ll"
去除首尾空白字符
使用trim()
方法可以移除字符串首尾空白字符。空白字符包括空格,\t
,\r
,\n
1
" \tHello\r\n ".trim(); // "Hello"
- ==注意:
trim()
并没有改变字符串的内容,而是返回了一个新字符串==
另一个strip()
方法也可以移除字符串首尾空白字符。它和trim()
不同的是,类似中文的空格字符\u3000
也会被移除:1
2
3"\u3000Hello\u3000".strip(); // "Hello"
" Hello ".stripLeading(); // "Hello "
" Hello ".stripTrailing(); // " Hello"
String
还提供了isEmpty()
和isBlank()
来判断字符串是否为空和空白字符串:1
2
3
4"".isEmpty(); // true,因为字符串长度为0
" ".isEmpty(); // false,因为字符串长度不为0
" \n".isBlank(); // true,因为只包含空白字符
" Hello ".isBlank(); // false,因为包含非空白字符
替换子串
1.根据字符或字符串替换1
2
3String s = "hello";
s.replace('l', 'w'); // "hewwo",所有字符'l'被替换为'w'
s.replace("ll", "~~"); // "he~~o",所有子串"ll"被替换为"~~"
2.通过正则表达式替换1
2String s = "A,,B;C ,D";
s.replaceAll("[\\,\\;\\s]+", ","); // "A,B,C,D"
- 上面的代码通过正则表达式,把匹配的子串统一替换为
","
分割字符串
要分割字符串,使用split()
方法,并且传入的也是正则表达式1
2String s = "A,B,C,D";
String[] ss = s.split("\\,"); // {"A", "B", "C", "D"}
拼接字符串
拼接字符串使用静态方法join()
,它用指定的字符串连接字符串数组1
2String[] arr = {"A", "B", "C"};
String s = String.join("***", arr); // "A***B***C"
格式化字符串
字符串提供了formatted()
方法和format()
静态方法,可以传入其他参数,替换占位符,然后生成新的字符串1
2
3
4
5
6
7public class Main {
public static void main(String[] args) {
String s = "Hi %s, your score is %d!";
System.out.println(s.formatted("Alice", 80));
System.out.println(String.format("Hi %s, your score is %.2f!", "Bob", 59.5));
}
}
如果你不确定用啥占位符,那就始终用%s
,因为%s
可以显示任何数据类型
要查看完整的格式化语法,请参考JDK文档
类型转换
要把任意基本类型或引用类型转换为字符串,可以使用静态方法valueOf()
1
2
3
4String.valueOf(123); // "123"
String.valueOf(45.67); // "45.67"
String.valueOf(true); // "true"
String.valueOf(new Object()); // 类似java.lang.Object@636be97c
把字符串转换为int
类型1
2int n1 = Integer.parseInt("123"); // 123
int n2 = Integer.parseInt("ff", 16); // 按十六进制转换,255
把字符串转换为boolean
类型:1
2boolean b1 = Boolean.parseBoolean("true"); // true
boolean b2 = Boolean.parseBoolean("FALSE"); // false
要特别注意,Integer
有个getInteger(String)
方法,它不是将字符串转换为int
,而是把该字符串对应的系统变量转换为Integer
:1
Integer.getInteger("java.version"); // 版本号,11
转换为char[]
String
和char[]
类型可以互相转换,方法是:1
2char[] cs = "Hello".toCharArray(); // String -> char[]
String s = new String(cs); // char[] -> String
如果修改了char[]
数组,String
并不会改变1
2
3
4
5
6
7
8
9public class Main {
public static void main(String[] args) {
char[] cs = "Hello".toCharArray();
String s = new String(cs);
System.out.println(s);
cs[0] = 'X';
System.out.println(s);
}
}
- 这是因为通过
new String(char[])
创建新的String
实例时,它并不会直接引用传入的char[]
数组,而是会复制一份,所以,修改外部的char[]
数组不会影响String
实例内部的char[]
数组,因为这是两个不同的数组 - 从
String
的不变性设计可以看出,如果传入的对象有可能改变,我们需要复制而不是直接引用
例如,下面的代码设计了一个Score
类保存一组学生的成绩1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class Main {
public static void main(String[] args) {
int[] scores = new int[] { 88, 77, 51, 66 };
Score s = new Score(scores);
s.printScores();
scores[2] = 99;
s.printScores();
}
}
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
类的字段
字符编码
在早期的计算机系统中,为了给字符编码,美国国家标准学会(American National Standard Institute:ANSI)制定了一套英文字母、数字和常用符号的编码,它占用一个字节,编码范围从0
到127
,最高位始终为0
,称为ASCII
编码。例如,字符'A'
的编码是0x41
,字符'1'
的编码是0x31
GB2312
标准使用两个字节表示一个汉字,其中第一个字节的最高位始终为1
,以便和ASCII
编码区分开。例如,汉字'中'
的GB2312
编码是0xd6d0
。
为了统一全球所有语言的编码,全球统一码联盟发布了Unicode
编码,它把世界上主要语言都纳入同一个编码,这样,中文、日文、韩文和其他语言就不会冲突。
Unicode
编码需要两个或者更多字节表示
英文字符'A'
的ASCII
编码和Unicode
编码:1
2
3
4
5
6 ┌────┐
ASCII: │ 41 │
└────┘
┌────┬────┐
Unicode: │ 00 │ 41 │
└────┴────┘
- 英文字符的
Unicode
编码就是简单地在前面添加一个00
字节
中文字符'中'
的GB2312
编码和Unicode
编码:1
2
3
4
5
6 ┌────┬────┐
GB2312: │ d6 │ d0 │
└────┴────┘
┌────┬────┐
Unicode: │ 4e │ 2d │
└────┴────┘
因为英文字符的Unicode
编码高字节总是00
,包含大量英文的文本会浪费空间,所以,出现了UTF-8
编码,它是一种变长编码,用来把固定长度的Unicode
编码变成1~4字节的变长编码。通过UTF-8
编码,英文字符'A'
的UTF-8
编码变为0x41
,正好和ASCII
码一致,而中文'中'
的UTF-8
编码为3字节0xe4b8ad
UTF-8
编码的另一个好处是容错能力强。如果传输过程中某些字符出错,不会影响后续字符,因为UTF-8
编码依靠高字节位来确定一个字符究竟是几个字节,它经常用来作为传输编码
char
类型实际上就是两个字节的Unicode
编码。如果我们要手动把字符串转换成其他编码,可以这样做:1
2
3
4byte[] b1 = "Hello".getBytes(); // 按系统默认编码转换,不推荐
byte[] b2 = "Hello".getBytes("UTF-8"); // 按UTF-8编码转换
byte[] b2 = "Hello".getBytes("GBK"); // 按GBK编码转换
byte[] b3 = "Hello".getBytes(StandardCharsets.UTF_8); // 按UTF-8编码转换
==注意:转换编码后,就不再是char
类型,而是byte
类型表示的数组==
如果要把已知编码的byte[]
转换为String
,可以这样做:1
2
3byte[] b = ...
String s1 = new String(b, "GBK"); // 按GBK转换
String s2 = new String(b, StandardCharsets.UTF_8); // 按UTF-8转换
Java的String
和char
在内存中总是以Unicode编码表示
延伸阅读
早期JDK版本的String
总是以char[]
存储1
2
3
4
5public final class String {
private final char[] value;
private final int offset;
private final int count;
}
较新的JDK版本的String
则以byte[]
存储1
2
3public final class String {
private final byte[] value;
private final byte coder; // 0 = LATIN1, 1 = UTF16
- 如果
String
仅包含ASCII字符,则每个byte
存储一个字符,否则,每两个byte
存储一个字符,这样做的目的是为了节省内存,因为大量的长度较短的String
通常仅包含ASCII字符
StringBuilder
为了能高效拼接字符串,Java标准库提供了StringBuilder
,它是一个可变对象,可以预分配缓冲区,这样,往StringBuilder
中新增字符时,不会创建新的临时对象1
2
3
4
5
6StringBuilder sb = new StringBuilder(1024);
for (int i = 0; i < 1000; i++) {
sb.append(',');
sb.append(i);
}
String s = sb.toString();StringBuilder
还可以进行链式操作:1
2
3
4
5
6
7
8
9
10public 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());
}
}
- 进行链式操作的关键是,定义的
append()
方法会返回this
,这样,就可以不断调用自身的其他方法
==注意:对于普通的字符串+
操作,并不需要我们将其改写为StringBuilder
,因为Java编译器在编译时就自动把多个连续的+
操作编码为StringConcatFactory
的操作==
StringJoiner
很多时候,我们拼接的字符串像这样:1
2
3
4
5
6
7
8
9
10
11
12
13
14public class Main {
public static void main(String[] args) {
String[] names = {"Bob", "Alice", "Grace"};
var sb = new StringBuilder();
sb.append("Hello ");
for (String name : names) {
sb.append(name).append(", ");
}
// 注意去掉最后的", ":
sb.delete(sb.length() - 2, sb.length());
sb.append("!");
System.out.println(sb.toString());
}
}
用分隔符拼接数组的需求很常见,所以Java标准库还提供了一个StringJoiner
来干这个事:1
2
3
4
5
6
7
8
9
10public 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());
}
}
用StringJoiner
的结果少了前面的"Hello "
和结尾的"!"
!遇到这种情况,需要给StringJoiner
指定“开头”和“结尾”1
2
3
4
5
6
7
8
9
10public 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());
}
}
String,join()
String
还提供了一个静态方法join()
,这个方法在内部使用了StringJoiner
来拼接字符串,在不需要指定“开头”和“结尾”的时候,用String.join()
更方便1
2String[] names = {"Bob", "Alice", "Grace"};
var s = String.join(", ", names);
包装类型
想要把int
基本类型变成一个引用类型,我们可以定义一个Integer
类,它只包含一个实例字段int
,这样,Integer
类就可以视为int
的包装类(Wrapper Class)1
2
3
4
5
6
7
8
9
10
11public class Integer {
private int value;
public Integer(int value) {
this.value = value;
}
public int intValue() {
return this.value;
}
}
定义好了Integer
类,我们就可以把int
和Integer
互相转换1
2
3Integer n = null;
Integer n2 = new Integer(99);
int n3 = n2.intValue();
因为包装类型非常有用,Java核心库为每种基本类型都提供了对应的包装类型
|基本类型|对应的引用类型
|—-|—-|
|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|
我们可以直接使用,不需要自己定义1
2
3
4
5
6
7
8
9
10
11
12public 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());
}
}
Auto Boxing
Java编译器可以帮助我们自动在int
和Integer
之间转型:1
2Integer n = 100; // 编译器自动使用Integer.valueOf(int)
int x = n; // 编译器自动使用Integer.intValue()
这种直接把int
变为Integer
的赋值写法,称为自动装箱(Auto Boxing),反过来,把Integer
变为int
的赋值写法,称为自动拆箱(Auto Unboxing)
- ==注意:自动装箱和自动拆箱只发生在编译阶段,目的是为了少写代码==
- 自动拆箱执行时可能会报
NullPointerException
- 装箱和拆箱会影响代码的执行效率,因为编译后的
class
代码是严格区分基本类型和引用类型的
不变类
所有的包装类型都是不变类1
2
3public final class Integer {
private final int value;
}
因此,一旦创建了Integer
对象,该对象就是不变的
对两个Integer
实例进行比较要特别注意:绝对不能用==
比较,因为Integer
是引用类型,必须使用equals()
比较
我们自己创建Integer
的时候,以下两种方法:
方法1:Integer n = new Integer(100);
- 方法2:
Integer n = Integer.valueOf(100);
我们把能创建“新”对象的静态方法称为静态工厂方法
Integer.valueOf()
就是静态工厂方法,它尽可能地返回缓存的实例以节省内存
==创建新对象时,优先选用静态工厂方法而不是new操作符==
进制转换
Integer
类本身还提供了大量方法,例如,最常用的静态方法parseInt()
可以把字符串解析成一个整数:1
2int x1 = Integer.parseInt("100"); // 100
int x2 = Integer.parseInt("100", 16); // 256,因为按16进制解析Integer
还可以把整数格式化为指定进制的字符串:1
2
3
4
5
6
7
8
9public 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进制
}
}
我们经常使用的System.out.println(n);
是依靠核心库自动把整数格式化为10进制输出并显示在屏幕上,使用Integer.toHexString(n)
则通过核心库自动把整数格式化为16进制
这里我们注意到程序设计的一个重要原则:数据的存储和显示要分离
ava的包装类型还定义了一些有用的静态变量
// boolean只有两个值true/false,其包装类型只需要引用Boolean提供的静态字段:1
2
3
4
5
6
7
8
9Boolean 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
,因此,可以非常方便地直接通过包装类型获取各种基本类型:1
2
3
4
5
6
7
8// 向上转型为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();
处理无符号整型
在Java中,并没有无符号整型(Unsigned)的基本数据类型
- 无符号整型和有符号整型的转换在Java中就需要借助包装类型的静态方法完成类似的,可以把一个
1
2
3
4
5
6
7
8public 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
}
}short
按unsigned转换为int
,把一个int
按unsigned转换为long
JavaBean
如果读写方法符合以下这种命名规范:1
2
3
4// 读方法:
public Type getXyz()
// 写方法:
public void setXyz(Type value)
那么这种class
被称为JavaBean
boolean
字段比较特殊,它的读方法一般命名为isXyz()
:1
2
3
4// 读方法:
public boolean isChild()
// 写方法:
public void setChild(boolean value)
我们通常把一组对应的读方法(getter
)和写方法(setter
)称为属性(property
)。例如,name
属性:
- 对应的读方法是
String getName()
- 对应的写方法是
setName(String)
只有getter
的属性称为只读属性(read-only)
只有setter
的属性称为只写属性(write-only)
- 只读属性很常见,只写属性不常见
- 属性只需要定义
getter
和setter
方法,不一定需要对应的字段
例如,child
只读属性定义如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14public 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;
}
}
可以看出,getter
和setter
也是一种数据封装的方法
JavaBean的作用
JavaBean主要用来传递数据,即把一组数据组合成一个JavaBean便于传输
JavaBean可以方便地被IDE工具分析,生成读写属性的代码,主要用在图形界面的可视化设计中1
2
3
4public class Person {
private String name;
private int age;
}
点击右键,在弹出的菜单中选择“Source”,“Generate Getters and Setters”,在弹出的对话框中选中需要生成getter
和setter
方法的字段,点击确定即可由IDE自动完成所有方法代码
枚举JavaBean属性
要枚举一个JavaBean的所有属性,可以直接使用Java核心库提供的Introspector
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
31public 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;
}
}
运行上述代码,可以列出所有的属性,以及对应的读写方法
- ==注意
class
属性是从Object
继承的getClass()
方法带来的==
枚举类
enum
为了让编译器能自动检查某个值在枚举的集合内,并且,不同用途的枚举需要不同的类型来标记,不能混用,我们可以使用enum
来定义枚举类1
2
3
4
5
6
7
8
9
10
11
12
13
14public class Main {
public static void main(String[] args) {
Weekday day = Weekday.SUN;
if (day == Weekday.SAT || day == Weekday.SUN) {
System.out.println("Work at home!");
} else {
System.out.println("Work at office!");
}
}
}
enum Weekday {
SUN, MON, TUE, WED, THU, FRI, SAT;
}
enum
常量本身带有类型信息,即Weekday.SUN
类型是Weekday
,编译器会自动检查出类型错误1
2
3int day = 1;
if (day == Weekday.SUN) { // Compile error: bad operand types for binary operator '=='
}- 不可能引用到非枚举的值,因为无法通过编译
- 不同类型的枚举不能互相比较或者赋值,因为类型不符
1
2Weekday x = Weekday.SUN; // ok!
Weekday y = Color.RED; // Compile error: incompatible types
enum的比较
使用enum
定义的枚举类是一种引用类型
引用类型比较,要始终使用equals()
方法,但enum
类型可以例外
- 这是因为
enum
类型的每个常量在JVM中只有一个唯一实例,所以可以直接用==
比较1
2
3
4if (day == Weekday.FRI) { // ok!
}
if (day.equals(Weekday.SUN)) { // ok, but more code!
}
enum类型
enum
定义的类型就是class
,只不过它有以下几个特点:
- 定义的
enum
类型总是继承自java.lang.Enum
,且无法被继承 - 只能定义出
enum
的实例,而无法通过new
操作符创建enum
的实例 - 定义的每个实例都是引用类型的唯一实例
- 可以将
enum
类型用于switch
语句
因为enum
是一个class
,每个枚举的值都是class
实例,因此,这些实例有一些方法:
name()
返回常量名,例如:1
String s = Weekday.SUN.name(); // "SUN"
ordinal()
返回定义的常量的顺序,从0开始计数,例如:改变枚举常量定义的顺序就会导致1
int n = Weekday.MON.ordinal(); // 1
ordinal()
返回值发生变化
如果在代码中编写了类似if(x.ordinal()==1)
这样的语句,就要保证enum
的枚举顺序不能变。新增的常量必须放在最后。
实现enum和int之间的转化:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class Main {
public static void main(String[] args) {
Weekday day = Weekday.SUN;
if (day.dayValue == 6 || day.dayValue == 0) {
System.out.println("Work 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;
}
}
- 这样就无需担心顺序的变化,新增枚举常量时,也需要指定一个
int
值
==注意:枚举类的字段也可以是非final类型,即可以在运行期修改,但是不推荐这样做!==
==注意:判断枚举常量的名字,要始终使用name()方法,绝不能调用toString()!==
switch
1 | public class Main { |
纪录类
假设我们希望定义一个Point
类,有x
、y
两个变量,同时它是一个不变类,可以这么写:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public 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()
方法,这样才能在集合类中正常使用。
record
把上述Point
类改写为Record
类,代码如下:1
2
3
4
5
6
7
8
9
10public 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) {}
把上述定义改写为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
28final 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() {
...
}
}
换句话说,使用record
关键字,可以一行写出一个不变类
- 和
enum
类似,我们自己不能直接从Record
派生,只能通过record
关键字由编译器实现继承
构造方法
假设Point
类的x
、y
不允许负数,我们就得给Point
的构造方法加上检查逻辑:1
2
3
4
5
6
7public record Point(int x, int y) {
public Point {
if (x < 0 || y < 0) {
throw new IllegalArgumentException();
}
}
}
注意到方法public Point {...}
被称为Compact Constructor,它的目的是让我们编写检查逻辑,编译器最终生成的构造方法如下:1
2
3
4
5
6
7
8
9
10
11
12public 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;
}
...
}
作为record
的Point
仍然可以添加静态方法。一种常用的静态方法是of()
方法,用来创建Point
:1
2
3
4
5
6
7
8public 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);
}
}
BigInteger
如果我们使用的整数范围超过了long
型怎么办?这个时候,就只能用软件来模拟一个大整数。java.math.BigInteger
就是用来表示任意大小的整数。BigInteger
内部用一个int[]
数组来模拟一个非常大的整数:1
2BigInteger bi = new BigInteger("1234567890");
System.out.println(bi.pow(5)); // 2867971860299718107233761438093672048294900000
对BigInteger
做运算的时候,只能使用实例方法,例如,加法运算:1
2
3BigInteger i1 = new BigInteger("1234567890");
BigInteger i2 = new BigInteger("12345678901234567890");
BigInteger sum = i1.add(i2); // 12345678902469135780
BigInteger
不会有范围限制,但缺点是速度比较慢
也可以把BigInteger
转换成long
型:1
2
3BigInteger 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()
方法时,如果超出了long
型的范围,会抛出ArithmeticException
BigInteger
和Integer
、Long
一样,也是不可变类,并且也继承自Number
类。因为Number
定义了转换为基本类型的几个方法:
- 转换为
byte
:byteValue()
- 转换为
short
:shortValue()
- 转换为
int
:intValue()
- 转换为
long
:longValue()
- 转换为
float
:floatValue()
- 转换为
double
:doubleValue()
如果BigInteger
表示的范围超过了基本类型的范围,转换时将丢失高位信息,即结果不一定是准确的。
如果需要准确地转换成基本类型,可以使用intValueExact()
、longValueExact()
等方法,在转换时如果超出范围,将直接抛出ArithmeticException
异常
BigDecimal
和BigInteger
类似,BigDecimal
可以表示一个任意大小且精度完全准确的浮点数
BigDecimal
用scale()
表示小数位数,例如:1
2
3
4
5
6BigDecimal 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
的stripTrailingZeros()
方法,可以将一个BigDecimal
格式化为一个相等的,但去掉了末尾0的BigDecimal
:1
2
3
4
5
6
7
8
9BigDecimal 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
可以对一个BigDecimal
设置它的scale
,如果精度比原始值低,那么按照指定的方法进行四舍五入或者直接截断:1
2
3
4
5
6
7
8
9public 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);
}
}
对BigDecimal
做加、减、乘时,精度不会丢失,但是做除法时,存在无法除尽的情况,这时,就必须指定精度以及如何进行截断:1
2
3
4BigDecimal 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,因为除不尽
还可以对BigDecimal做除法的同时求余数1
2
3
4
5
6
7
8
9public 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
的值是否相等时,要特别注意,使用equals()
方法不但要求两个BigDecimal
的值相等,还要求它们的scale()
相等
必须使用compareTo()
方法来比较,它根据两个值的大小分别返回负数、正数和0
,分别表示小于、大于和等于
- ==总是使用compareTo()比较两个BigDecimal的值,不要使用equals()!==
如果查看BigDecimal
的源码,可以发现,实际上一个BigDecimal
是通过一个BigInteger
和一个scale
来表示的,即BigInteger
表示一个完整的整数,而scale
表示小数位数1
2
3
4public class BigDecimal extends Number implements Comparable<BigDecimal> {
private final BigInteger intVal;
private final int scale;
}
常用工具类
Math
求绝对值:1
2Math.abs(-100); // 100
Math.abs(-7.8); // 7.8
取最大或最小值:1
2Math.max(100, 99); // 100
Math.min(1.2, 2.3); // 1.2
计算xy次方:1
Math.pow(2, 10); // 2的10次方=1024
计算√x:1
Math.sqrt(2); // 1.414...
计算ex次方:1
Math.exp(2); // 7.389...
计算以e为底的对数:1
Math.log(4); // 1.386...
计算以10为底的对数:1
Math.log10(100); // 2
三角函数:1
2
3
4
5Math.sin(3.14); // 0.00159...
Math.cos(3.14); // -0.9999...
Math.tan(3.14); // -0.0015...
Math.asin(1.0); // 1.57079...
Math.acos(1.0); // 0.0
Math还提供了几个数学常量:1
2
3double pi = Math.PI; // 3.14159...
double e = Math.E; // 2.7182818...
Math.sin(Math.PI / 6); // sin(π/6) = 0.5
生成一个随机数x,x的范围是0 <= x < 1
:1
Math.random(); // 0.53907... 每次都不一样
如果我们要生成一个区间在[MIN, MAX)的随机数,可以借助Math.random()实现,计算如下:1
2
3
4
5
6
7
8
9
10
11
12// 区间在[MIN, MAX)的随机数
public class Main {
public static void main(String[] args) {
double x = Math.random(); // x的范围是[0,1)
double min = 10;
double max = 50;
double y = x * (max - min) + min; // y的范围是[10,50)
long n = (long) y; // n的范围是[10,50)的整数
System.out.println(y);
System.out.println(n);
}
}
Java标准库还提供了一个StrictMath
,它提供了和Math
几乎一模一样的方法
StrictMath
保证所有平台计算结果都是完全相同的,而Math
会尽量针对平台优化计算速度
HexFormat
要将byte[]
数组转换为十六进制字符串,可以用formatHex()
方法:1
2
3
4
5
6
7
8
9import 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
实例:1
2
3// 分隔符为空格,添加前缀0x,大写字母:
HexFormat hf = HexFormat.ofDelimiter(" ").withPrefix("0x").withUpperCase();
hf.formatHex("Hello".getBytes())); // 0x48 0x65 0x6C 0x6C 0x6F
从十六进制字符串到byte[]
数组转换,使用parseHex()
方法:1
byte[] bs = HexFormat.of().parseHex("48656c6c6f");
Random
Random
用来创建伪随机数。所谓伪随机数,是指只要给定一个初始的种子,产生的随机数序列是完全一样的
要生成一个随机数,可以使用nextInt()、nextLong()、nextFloat()、nextDouble():1
2
3
4
5
6Random 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
- 如果不给定种子,就使用系统当前时间戳作为种子,因此每次运行时,种子不同,得到的伪随机数序列就不同
如果我们在创建Random
实例时指定一个种子,就会得到完全确定的随机数序列1
2
3
4
5
6
7
8
9public 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...
}
}
- 前面我们使用的
Math.random()
实际上内部调用了Random
类,所以它也是伪随机数,只是我们无法指定种子
SecureRandom
。实际上真正的真随机数只能通过量子力学原理来获取,而我们想要的是一个不可预测的安全的随机数,SecureRandom
就是用来创建安全的随机数的1
2SecureRandom sr = new SecureRandom();
System.out.println(sr.nextInt(100));SecureRandom
无法指定种子,它使用RNG(random number generator)算法
实际使用的时候,可以优先获取高强度的安全随机数生成器,如果没有提供,再使用普通等级的安全随机数生成器:1
2
3
4
5
6
7
8
9
10
11
12
13public 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));
}
}SecureRandom
的安全性是通过操作系统提供的安全的随机种子来生成随机数。这个种子是通过CPU的热噪声、读写磁盘的字节、网络流量等各种随机事件产生的“熵”
==需要使用安全随机数的时候,必须使用SecureRandom,绝不能使用Random!==