Java学习笔记–Java核心类
Java的核心类包括:
- 字符串
- StringBuilder
- StringJoiner
- 包装类型
- JavaBean
- 枚举
- 常用工具类
一、字符串和编码
1.1、String
在Java中,String
是一个引用类型,它本身也是一个class
。但是,java编译器对String
有特殊处理,既可以直接用"..."
来表示一个字符串:
String str = "Hello!";
实际上字符串在String
内部是通过一个char[]
数组表示的,因此下方的写法也是可以的:
String s2 = new String(new char[] {'H', 'e', 'l', 'l', 'o', '!'});
因为String
太常用了,所以Java提供了"..."
这种字符串字面量表示方法。
Java字符串的一个重要特点就是字符串不可变
。这种不可变性是通过内部的private final char[]
字段,以及没有任何修改char[]
的方法实现的。
1.2、字符串比较
当我们想要比较两个字符串是否相同时,要特别注意,我们实际上是想比较字符串的内容是否相同。必须使用equals()
方法而不能用==
public 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));
}
}
虽然结果都为true
,但这是因为Java编译器在编译期,会自动把所有相同的字符串当作一个对象放入常量池,自然s1
和s2
的引用就是相同的。
换一种写法就会看出两种区别:
public 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));
}
}
结论:两个字符串比较,必须使用equals()
方法
要忽略大小写比较,使用equalsIgnoreCase()
方法。
1.3、搜索、提取子串
String
类还提供了多种方法来搜索、提取子串。常用的方法有:
str.contains('xx')
判断是否包含
// 是否包含子串
"hello".contains("ll"); //true
注意:
contains
方法的参数是CharSequence
而不是String
,因为CharSequence
是String
的父类。
- 搜索子串
"Hello".indexOf("l"); // 2
"Hello".lastIndexOf("l"); // 3
"Hello".startsWith("He"); // true
"Hello".endsWith("lo"); // true
- 提取子串
"Hello".substring(2); // "llo"
"Hello".substring(2, 4); "ll"
注意索引号是从0
开始的
1.4、去除首位空白字符
使用trim()
方法可以移除字符串首尾空白字符。空白字符包括空格、\t
、\r
、\n
:
" \tHello\r\n ".trim(); // "Hello"
trim()
并没有改变字符串的内容,而是放回了一个新字符串
另一个strip()
方法可以移除字符串首尾空白字符。他和trim()
不同的是,类似中午空格字符\u3000
也会被移除:
"\u3000Hello\u3000".strip(); // "Hello"
" Hello ".stripLeading(); // "Hello "
" Hello ".stripTrailing(); // " Hello"
1.5、判断空白/空字符串
String
提供isEmpty()
和isBlank()
来判断字符串是否为空白字符串:
"".isEmpty(); // true,因为字符串长度为0
" ".isEmpty(); // false,因为字符串长度不为0
" \n".isBlank(); // true,因为只包含空白字符
" Hello ".isBlank(); // false,因为包含非空白字符
1.6、替换子串
要在字符串中替换子串,有两种方法。
- 根据字符或字符串替换:
String s = "hello";
s.replace('l','w'); //"hewwo",所有字符'l'被替换为'w'
s.replace('ll','aa'); // "heaao",所有子串"ll"被替换为"aa"
- 通过正则表达式替换:
String s = "A,,B;C ,D";
s.replaceAll("[\\,\\;\\s]+", ","); // "A,B,C,D"
上面的代码通过正则表达式,把匹配的子串统一替换为","
。
1.7、分割字符串
使用split()
方法,并且传入的也是正则表达式:
String s = "A,B,C,D";
String[] ss = s.split("\\,"); // {"A", "B", "C", "D"}
1.8、拼接字符串
使用静态方法join()
,它用指定的字符串连接字符串数组:
String[] arr = {"A", "B", "C"};
String s = String.join("***", arr); // "A***B***C"
1.9、格式化字符串
字符串提供了formatted()
方法和format()
静态方法,可以传入其他参数,替换占位符%?
,然后生成新的字符串:
public 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
:显示字符串;%d
:显示整数;%x
:显示十六进制整数;%f
:显示浮点数。
占位符还可以带格式,例如%.2f
表示显示两位小数。如果你不确定用啥占位符,那就始终用%s
,因为%s
可以显示任何数据类型。要查看完整的格式化语法,请参考JDK文档。
1.10、类型转换
要把任意基本类型或引用类型转换为字符串,可以使用静态方法valueOf()
。这是一个重载方法,编译器会根据参数自动选择合适的方法:
String.valueOf(123); //"123"
String.valueOf(45.67); // "45.67"
String.valueOf(true); // "true"
String.valueOf(new Object()); // 类似java.lang.Object@636be97c
要把字符串转换为其他类型,就需要根据情况,例如,把字符串转换为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
1.11、转换为char[]
String
和char[]
类型可以相互转换
char[] cs = "Hello".toCharArray(); //String -> char[]
String s = new String(cs); //char[] -> String
如果修改了char[]
,String
并不会改变:
public 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
的不变性设计可以看出,如果传入的对象有可能改变,我们需要复制而不是直接引用。使用Arrays.copyOf(数组,复制长度)
进行复制。
public 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; //这样是直接引用的scores地址,scores改变实例变量也会改变
this.scores = Arrays.copyOf(scores,scores.length); //进行复制,为实例变量分配新地址
}
public void printScores() {
System.out.println(Arrays.toString(scores));
}
}
1.12、字符编码
编码介绍:字符串和编码 - 廖雪峰的官方网站 (liaoxuefeng.com)
在Java中,char
类型实际上就是两个字节的Unicode
编码。可以手动将字符串转换成其他编码:
byte[] 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
:
byte[] b = ...
String s1 = new String(b, "GBK"); // 按GBK转换
String s2 = new String(b, StandardCharsets.UTF_8); // 按UTF-8转换
1.13、延伸(了解
对于不同版本的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
存储一个字符,这样做的目的是为了节省内存,因为大量的长度较短的String
通常仅包含ASCII字符:
public final class String {
private final byte[] value;
private final byte coder; // 0 = LATIN1, 1 = UTF16
对于使用者来说,String
内部的优化不影响任何已有代码,因为它的public
方法签名是不变的。
二、StringBuilder
使用StringBuilder,在对字符串循环新增字符时,可以预分配缓冲区,不会创建新的临时对象
Java编译器对String
做了特殊处理,使得我们可以直接用+
拼接字符串。
考察下面的循环代码:
String s = "";
for (int i = 0; i < 1000; i++) {
s = s + "," + i;
}
虽然可以直接拼接字符串,但是,在循环中,每次循环都会创建新的字符串对象,然后扔掉旧的字符串。这样,绝大部分字符串都是临时对象,不但浪费内存,还会影响GC效率。
为了能高效拼接字符串,Java标准库提供了StringBuilder
,它是一个可变对象,可以预分配缓冲区,这样,往StringBuilder
中新增字符时,不会创建新的临时对象:
StringBuilder sb = new StringBuilder(1024);
for (int i = 0; i < 1000; i++) {
sb.append(',');
sb.append(i);
}
String s = sb.toString();
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());
}
}
这是因为append()
方法会返回this
,这一样就可以不断调用自身其他方法。
注意:对于普通的字符串+
操作,并不需要我们将其改写为StringBuilder
,因为Java编译器在编译时就自动把多个连续的+
操作编码为StringConcatFactory
的操作。在运行期,StringConcatFactory
会自动把字符串连接操作优化为数组复制或者StringBuilder
操作。
你可能还听说过StringBuffer
,这是Java早期的一个StringBuilder
的线程安全版本,它通过同步来保证多个线程操作StringBuffer
也是安全的,但是同步会带来执行速度的下降。
StringBuilder
和StringBuffer
接口完全相同,现在完全没有必要使用StringBuffer
三、StringJoiner
要高效拼接字符串,应该使用StringBuilder
。
若要按照某种固定分隔符进行拼接,Java标准库还提供了一个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());
}
}
可以指定开头和结尾的字符串
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());
}
}
String
还提供了一个静态方法join()
,这个方法在内部使用了StringJoiner
来拼接字符串,在不需要指定“开头”和“结尾”的时候,用String.join()
更方便:
String[] names = {"Bob", "Alice", "Grace"};
var s = String.join(", ", names);
四、包装类型
Java的数据类型分为两种
- 基本类型:
byte
,short
,int
,long
,boolean
,float
,double
,char
- 引用类型:所有
class
和interface
类型
引用类型可以赋值为null
,表示空,但基本类型不能赋值为null
:
String str = null;
int n = null; // 编译报错
4.1、包装类
如何把一个基本类型视为对象(引用类型)
如把int
基本类型变成一个引用类型,可以定义一个Integer
类。它只包含一个是实例字段int
,这样Integer
类就可以视为int
的包装类(Wrapper Class):
public class Integer{
private int value;
public Interger(int value){
this.value = value;
}
public int intValue(){
return this.value;
}
}
定义好了Integer
类,就可以把int
和Integer
相互转换:
Integer 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 |
可以直接使用,不需要和上面一样去自己定义:
public class WrapperClass {
public static void main(String[] args){
int i = 100;
// 通过new操作符创建Integer实例(不推荐,会警告
// Integer n1 = new Integer(i);
// 通过静态方法valueOf(int)创建Integer实例
Integer n2 = Integer.valueOf(i);
System.out.println(n2.intValue());
// 通过静态方法valueOf(String)创建Integer实例
Integer n3 = Integer.valueOf("100");
System.out.println(n3.intValue());
}
}
4.2、Auto Boxing
因为int
和Integer
可以相互转换:
int i = 100;
Integer n = Integer.valueOf(i);
int x = n.intValue();
所以,Java编译器可以自动在int
和Integer
之间转型:
Integer n = 100; // 编译器自动使用Integer.valueOf(int)
int x = n; // 编译器自动使用Integer.intValue()
这种直接把int
变为Integer
的赋值写法,被称为自动装箱(Auto Boxing),反过来,把Integer
变为int
的赋值写法。被称为自动拆箱(Auto Unboxing)
注意:自动装箱和自动拆箱只发生在编译阶段,目的是为了少写代码。
装箱和拆箱会影响代码的执行效率,因为编译后的class
代码是严格区分基本类型和引用类型。并且自动拆箱执行时可能会报NullPointerException
4.3、不变类
所有的包装类型都是不变类,查看Integer
的源码可知,它的核心代码如下:
public final class Integer {
private final int value;
}
因此,一旦创建了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
,而较大的数返回false
,
因为Integer
是不变类,为了节省内存,Integer.valueOf()
对于较小的数,始终返回相同的实例,但做对比时必须使用equals()
4.4、静态工厂方法
因为Integer.valueOf()
可能始终返回同一个Integer
实例,因此,在我们自己创建Integer
的时候,以下两种方法:
- 方法1:
Integer n = new Integer(100);
- 方法2:
Integer n = Integer.valueOf(100);
方法2更好,因为方法1总是创建新的Integer
实例,方法2把内部优化留给Integer
的实现者去做,
把能创建"新"对象的静态方法称为静态工厂方法。Integer.valueOf()
就是静态工厂方法,它尽可能的返回缓存的实例以节省内存。
创建新对象时,优先选用静态工厂方法,而不是new操作符
如果我们考察Byte.valueOf()
方法的源码,可以看到,标准库返回的Byte
实例全部是缓存实例,但调用者并不关心静态工厂方法以何种方式创建新实例还是直接返回缓存的实例。
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
,在计算机内存中,只用二进制表示,不存在十进制或十六进制的表示方法。
int n = 100
在内存中总是以4字节的二进制表示:
┌────────┬────────┬────────┬────────┐
│00000000│00000000│00000000│01100100│
└────────┴────────┴────────┴────────┘
经常使用的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();
4.6、处理无符号整型
在Java中,并没有无符号整型(Unsigned)的基本数据类型。byte
、short
、int
和long
都是带符号整型,最高位是符号位。而C语言则提供了CPU支持的全部数据类型,包括无符号整型。无符号整型和有符号整型的转换在Java中就需要借助包装类型的静态方法完成。
例如,byte是有符号整型,范围是-128
`+127`,但如果把`byte`看作无符号整型,它的范围就是`0`255
。我们把一个负的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
。
类似的,可以把一个short
按unsigned转换为int
,把一个int
按unsigned转换为long
。
五、JavaBean
5.1、JavaBean
在Java中,有很多class
的定义都符合这样的规范:
- 若干
private
实例字段 - 通过
public
方法来读写实例字段
public class Person {
private String name;
private int age;
public String setName(){
return this.name;
}
public void setName(String name){
this.name = name;
}
public String setAge(){
return this.age;
}
public void setAge(String age){
this.age = age;
}
}
如果读写方法符合以下这种命名规范:
//读方法
public Type getXyz(){...};
//写方法
public void setXyz(){...};
那么这种class
被称为JavaBean
5.2、属性
上面的字段是xyz
,那么读写方法名分别以get
和set
开头,并且后接大写字母开头的字段名Xyz
,因此两个读写方法名分别是getXyz()
和setXyz()
。
boolean
字段比较特殊,他的读方法一般命名为isXyz()
:
// 读方法
public boolean isXyz(){...};
// 写方法:
public void setChild(boolean value){...}
通常把一组对应的读方法(getter
)和写方法(setter
)称为属性(property
)
例如,name
属性:
- 对应的读方法是
String getName()
- 对应的写方法是
setName(String)
只有getter
的属性称为只读属性(read-only),只有setter
的属性称为只写属性(write-only)
只读属性很常见,只写属性不常见。
属性只需要定义getter
和setter
方法,不一定需要对应的字段。例如,child
只读属性定义如下:
public class Person{
private int age;
public int getAge() {
return this.age;
}
public void setAge(int age) {
this.age = age;
}
public boolean isChild() {
return age <= 6;
}
}
由此可以看出,getter
和setter
也是一种数据封装的方法。
5.3、JavaBean的作用
JavaBean主要用来传递数据,即把一组数据组合成一个JavaBean便于传输。此外,JavaBean可以方便地被IDE工具分析,生成读写属性的代码,主要用在图形界面的可视化设计中。
5.4、枚举JavaBean属性
要枚举一个JavaBean的所有属性,可以直接使用Java核心库提供的Introspector
:
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;
}
}
age
public int Person.getAge()
public void Person.setAge(int)
class
public final native java.lang.Class java.lang.Object.getClass()
null
name
public java.lang.String Person.getName()
public void Person.setName(java.lang.String)
运行上述代码,可以列出所有的属性,以及对应的读写方法。注意class
属性是从Object
继承的getClass()
方法带来的。
六、枚举类
在Java中,我们可以通过static final
来定义常量,例如定义周一到周日这7个常量,可以用7个不同的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: work at home
}
也可以把常量定义为字符串类型,例如定义3种颜色的常量:
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
值; - 定义的常量仍可与其他变量比较,但其用途并非是枚举星期值。
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("Work 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 x = Weekday.SUN; // ok!
Weekday y = Color.RED; // Compile error: incompatible types
这样编译器可以在编译期自动检查出有可能的潜在错误。
6.2、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
类和普通clas
并没有区别。但是无法按定义class
那样来定义enum
,必须使用enum
关键字,这是Java语法规定
6.3、方法
因为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
的枚举顺序不能变。新增的常量必须放在最后。
^^^ 中间这部分不同明白枚举类 - 廖雪峰的官方网站 (liaoxuefeng.com)
- 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 + ". Work at home!");
break;
default:
throw new RuntimeException("cannot process " + day);
}
}
}
enum Weekday {
MON, TUE, WED, THU, FRI, SAT, SUN;
}
加上default
语句,可以在漏写某个枚举常量时自动报错,从而及时发现错误。
小结
- Java使用
enum
定义枚举类型,它被编译器编译为final class Xxx extends Enum { … }
; - 通过
name()
获取常量定义的字符串,注意不要使用toString()
; - 通过
ordinal()
返回常量定义的顺序(无实质意义); - 可以为
enum
编写构造方法、字段和方法 enum
的构造方法要声明为private
,字段强烈建议声明为final
;enum
适合用在switch
语句中。
七、纪录类
使用String
、Integer
等类型的时候,这些类型都是不变类,一个不变类具有一下特点:
- 定义class时使用
final
,无法派生子类; - 每个字段使用
final
,保证创建实例后无法修改任何字段
假设希望定义一个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()
方法,这样才能在集合类中正常使用。后续我们会详细讲解正确覆写equals()
和hashCode()
,这里演示Point
不变类的写法目的是,这些代码写起来都非常简单,但是很繁琐。
7.1、record
从Java 14开始,引入了新的Record
类,我们定义Record
类时,使用关键字record
。把上述Point
类改写为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);
}
}
public record Point(int x, int y){}
观察Point
的定义:
public record Point(int x, int y){}
若将上面的定义改写为class,相当于如下代码:
public 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
关键字由编译器实现继承
7.2、构造方法
编译器默认按照record
声明的变量顺序自动创建一个构造方法,并在方法内给字段赋值。那么问题来了,如果我们要检查参数,应该怎么办?
假设Point
类的x
、y
不允许负数,我们就得给Point
的构造方法加上检查逻辑:
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;
}
...
}
作为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);
小结
从Java 14开始,提供新的record
关键字,可以非常方便地定义Data Class:
- 使用
record
定义的是不变类; - 可以编写Compact Constructor对参数进行验证;
- 可以定义静态方法。
八、BigInteger
在Java中,由CPU原生提供的整型最大范围是64位long
型整数。使用long
型整数可以直接通过CPU指令进行计算,速度非常快。
如果使用的整数范围超过了long
型,就只能用软件来模拟一个大整数。
java.math.BigInteger
就是用来表示任意大小的整数。BigInteger
内部用一个int[]
数组来模拟一个非常大的整数:
BigInteger bi = new BigInteger("1234567890");
System.out.println(bi.pow(5)); // 计算 bi 的 5 次方
// 2867971860299718107233761438093672048294900000
对BigInteger
做运算的时候,只能使用实例方法,例如:
BigInteger i1 = new BigInteger("1234567890");
BigInteger i2 = new BigInteger("12345678901234567890");
BigInteger sum = i1.add(i2); // 12345678902469135780
和long
型整数运算比,BigInteger
不会有范围限制,但缺点是速度慢
也可以把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()
方法时,如果超过了long
型的范围,会抛出ArithmeticException
。
BigInteger
和Integer
、long
一样,也是不可变类,并且也继承自Number
类。因为Number
定义了转换为基本类型的几个方法:
- 转换为
byte
:byteValue()
- 转换为
short
:shortValue()
- 转换为
int
:intValue()
- 转换为
long
:longValue()
- 转换为
float
:floatValue()
- 转换为
double
:doubleValue()
通过上述方法,可以把BigInteger
转换成基本类型。
如果BigInteger
表示的范围超过了基本类型的范围,转换时将丢失高位信息,结果将不一定准确。
8.1、…ValueExact()
...ValueExact()
前方...
在使用时更换为具体的基本数据类型
如果需要准确地转换成基本类型,可以使用intValueExact()
、longValueExact()
等方法,在转换时如果超出范围,将直接抛出ArithmeticException
异常。
如果BigInteger
的值甚至超过了float
的最大范围(3.4x10^38),那将返回Infinity
小结
BigInteger
用于表示任意大小的整数;
BigInteger
是不变类,并且继承自Number
;
将BigInteger
转换成基本类型时可使用longValueExact()
等方法保证结果准确。
九、BigDecimal
和BigInteger
类似,BigDecimal
可以表示一个任意大小且精度完全准确的浮点数。
BigDecimal bd = new BigDecimal("123.4567");
System.out.println(bd.multiply(bd));
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
9.2、stripTrailingZeros()
通过BigDecimal
的stripTrailingZeros()
方法,可以将一个BigDecimal
格式化为一个相等的,但去掉了末尾0的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
,表示这个数为整数,但有末尾两个0
9.3、指定精度
可以对一个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);
}
}
对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,因为除不尽
还可以对BigDecimal
做除法的同时求余数:
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
}
}
9.4、比较BigDecimal
在比较两个BigDecimal
的值是否相等时,要特别注意,使用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变为2
System.out.println(d1.compareTo(d2)); // 0
必须使用compareTo()
方法来比较,他根据两个值得大小分别返回负数、正数和0
,分别表示大于、小于和等于。
总是使用compareTo()比较两个BigDecimal的值,不要使用equals()!
如果查看BigDecimal
的源码,可以发现,实际上一个BigDecimal
是通过一个BigInteger
和一个scale
来表示的,即BigInteger
表示一个完整的整数,而scale
表示小数位数:
public class BigDecimal extends Number implements Comparable<BigDecimal> {
private final BigInteger intVal;
private final int scale;
}
BigDecimal
也是从Number
继承的,也是不可变对象。
小结
BigDecimal
用于表示精确的小数,常用于财务计算;
比较BigDecimal
的值是否相等,必须使用compareTo()
而不能使用equals()
。
十、常用工具类
10.1、Math
Math
类就是用来进行数学计算的,它提供了大量的静态方法来便于实现数学计算:
- 求绝对值
Math.abs(-100); //100
Math.abs(-7.8); //7.8
- 取最大或最小值
Math.max(100,99); //100
Math.min(1.2,1.3); //1.2
- 计算x^y次方:
Math.pow(2,10); //2的10次方 1024
- 计算平方根√x:
Math.sqrt(2); // 1.414...
- 计算e^x次方:
Math.exp(2); //7.389...
- 计算以e为底的对数:
Math.log(4); // 1.386...
- 计算以10为底的对数:
Math.log10(100); // 2
- 三角函数:
Math.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还提供了几个数学常量:
double 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
:
Math.random(); // 0.53907... 每次都不一样
Java标准库还提供了一个StrictMath
,它提供了和Math
几乎一模一样的方法。这两个类的区别在于,由于浮点数计算存在误差,不同的平台(例如x86和ARM)计算的结果可能不一致(指误差不同),因此,StrictMath
保证所有平台计算结果都是完全相同的,而Math
会尽量针对平台优化计算速度,所以,绝大多数情况下,使用Math
就足够了。
10.2、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
上方代码因为没有给定种子,就会使用系统当前时间戳作为种子,因此每次运行时,种子不同,得到的伪随机数序列就不同。
如果我们在创建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(11));
}
// 51, 80, 41, 28, 55... 每次执行的结果都将是这个
}
}
前面的Math.random()
实际上内部调用了Random
类,所以它也是伪随机数,只是无法指定种子。
10.3、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));
}
}
SecureRandom
的安全性是通过操作系统提供的安全的随机种子来生成随机数。这个种子是通过CPU的热噪声、读写磁盘的字节、网络流量等各种随机事件产生的“熵”。
在密码学中,安全的随机数非常重要。如果使用不安全的伪随机数,所有加密体系都将被攻破。因此,时刻牢记必须使用SecureRandom
来产生安全的随机数。
需要使用安全随机数的时候,必须使用SecureRandom,绝不能使用Random!
小结
Java提供的常用工具类有:
- Math:数学计算
- Random:生成伪随机数
- SecureRandom:生成安全的随机数