「Java」核心类
一. 字符串和编码
1. String
1 | String s1 = "Hello!"; |
1.1 字符串比较
想比较字符串的内容是否相同。必须使用equals()
方法而不能用==
。
要忽略大小写比较,使用equalsIgnoreCase()
方法。
1.2 子串
1 | // 是否包含子串: |
1 | "Hello".indexOf("l"); // 2 |
1.2.1 提取子串
1 | "Hello".substring(2); // "llo" |
1.2.2 替换子串
一种是根据字符或字符串替换:
1 | String s = "hello"; |
一种是通过正则表达式替换:
1 | String s = "A,,B;C ,D"; |
1.3 空白字符
使用trim()
方法可以移除字符串首尾空白字符。空白字符包括空格,\t
,\r
,\n
:
1 | " \tHello\r\n ".trim(); // "Hello" |
strip()
方法也可以移除字符串首尾空白字符。和trim()
不同的是,类似中文的空格字符\u3000
也会被移除:
1 | "\u3000Hello\u3000".strip(); // "Hello" |
String
还提供了isEmpty()
和isBlank()
来判断字符串是否为空和空白字符串:
1 | "".isEmpty(); // true,因为字符串长度为0 |
1.4 分割拼接字符串
分割使用split()
方法,并且传入的也是正则表达式:
1 | String s = "A,B,C,D"; |
拼接字符串使用静态方法join()
,它用指定的字符串连接字符串数组:
1 | String[] arr = {"A", "B", "C"}; |
1.5 格式化字符号串
字符串提供了formatted()
方法和format()
静态方法,可以传入其他参数,替换占位符,然后生成新的字符串:
1 | String s = "Hi %s, your score is %d!"; |
1.6 类型转换
要把任意基本类型或引用类型转换为字符串,可以使用静态方法valueOf()
。这是一个重载方法,编译器会根据参数自动选择合适的方法:
1 | String.valueOf(123); // "123" |
要把字符串转换为其他类型,就需要根据情况。例如,把字符串转换为int
类型:
1 | int n1 = Integer.parseInt("123"); // 123 |
把字符串转换为boolean
类型:
1 | boolean b1 = Boolean.parseBoolean("true"); // true |
要特别注意,Integer
有个getInteger(String)
方法,它不是将字符串转换为int
,而是把该字符串对应的系统变量转换为Integer
:
1 | Integer.getInteger("java.version"); // 版本号,11 |
String
和char[]
类型可以互相转换,方法是:
1 | char[] cs = "Hello".toCharArray(); // String -> char[] |
通过new String(char[])
创建新的String
实例时,它并不会直接引用传入的char[]
数组,而是会复制一份,所以,修改外部的char[]
数组不会影响String
实例内部的char[]
数组
2. 字符编码
早期的计算机系统中,为了给字符编码,制定了ASCII
编码。为了统一全球所有语言的编码,全球统一码联盟发布了Unicode
编码,把世界上主要语言都纳入同一个编码。但Unicode
在包含大量英文的文本会浪费空间。
因此出现了UTF-8
编码,它是一种变长编码,用来把固定长度的Unicode
编码变成1~4字节的变长编码。UTF-8
编码的另一个好处是容错能力强。如果传输过程中某些字符出错,不会影响后续字符,因为UTF-8
编码依靠高字节位来确定一个字符究竟是几个字节,它经常用来作为传输编码。
2.1 字符串转编码
在Java中,char
类型实际上就是两个字节的Unicode
编码。如果要手动吧字符串转化成其他编码,可以:
1 | byte[] b1 = "Hello".getBytes(); // 按系统默认编码转换,不推荐 |
转换编码后,就不再是char
类型,而是byte
类型表示的数组。
编码转字符串
1 | byte[] b = ... |
注意:Java的String
和char
在内存中总是以Unicode编码表示。
3. StringBuilder
可以使用+
拼接字符串,但Java标准库提供的StringBuilder
可以预分配缓冲区,拼接更搞笑
1 | StringBuilder sb = new StringBuilder(1024); |
链式操作:
1 | var sb = new StringBuilder(1024); |
进行链式操作的关键是,定义的append()
方法会返回this
,这样,就可以不断调用自身的其他方法。
注意:对于普通的字符串+
操作,并不需要我们将其改写为StringBuilder
,因为Java编译器在编译时就自动把多个连续的+
操作编码为StringConcatFactory
的操作。在运行期,StringConcatFactory
会自动把字符串连接操作优化为数组复制或者StringBuilder
操作。
4. StringJoiner
方便字符串间的拼接字符
1 | String[] names = {"Bob", "Alice", "Grace"}; |
String.join()
在不需要指定“开头”和“结尾”的时候,用String.join()
更方便:
1 | String[] names = {"Bob", "Alice", "Grace"}; |
二. 包装类型
Java的数据类型分两种:
- 基本类型:
byte
,short
,int
,long
,boolean
,float
,double
,char
- 引用类型:所有
class
和interface
类型
把一个基本类型视为对象(引用类型)?
比如,想要把int
基本类型变成一个引用类型,我们可以定义一个Integer
类,它只包含一个实例字段int
,这样,Integer
类就可以视为int
的包装类(Wrapper Class):
1 | public class Integer { |
这样可以把int
和Integer
互相转换:
1 | Integer n = null; |
基本类型 | 对应的引用类型 |
---|---|
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
可以互相转换:
1 | int i = 100; |
Java编译器可以帮助我们自动在int
和Integer
之间转型:
1 | Integer n = 100; // 编译器自动使用Integer.valueOf(int) |
这种直接把int
变为Integer
的赋值写法,称为自动装箱(Auto Boxing),反过来,把Integer
变为int
的赋值写法,称为自动拆箱(Auto Unboxing)
1. 不变类
所有的包装类型都是不变类,即一旦创建了Integer
对象,该对象就是不变的。
因为Integer
是引用类型,必须使用equals()
比较,不能使用==
2. 包装类型方法
1 | int x1 = Integer.parseInt("100"); // 100 |
有用的静态变量
1 | // boolean只有两个值true/false,其包装类型只需要引用Boolean提供的静态字段: |
处理无符号整型
无符号整型和有符号整型的转换在Java中就需要借助包装类型的静态方法完成。
1 | byte x = -1; |
byte的-1
的二进制表示是11111111
,以无符号整型转换后的int
就是255
.
三. 枚举类enum
Java使用enum
定义枚举类型,它被编译器编译为final class Xxx extends Enum { … }
;
通过name()
获取常量定义的字符串,注意不要使用toString()
;
通过ordinal()
返回常量定义的顺序(无实质意义);
可以为enum
编写构造方法、字段和方法
enum
的构造方法要声明为private
,字段强烈建议声明为final
;
enum
适合用在switch
语句中。
为了让编译器能自动检查某个值在枚举的集合内,并且,不同用途的枚举需要不同的类型来标记,不能混用,我们可以使用enum
来定义枚举类:
1 | public class Main { |
因为enum
类型的每个常量在JVM中只有一个唯一实例,所以可以直接用==
比较。
因为enum
是一个class
,每个枚举的值都是class
实例,因此,这些实例有一些方法:
name()
返回常量名,例如:
1 | String s = Weekday.SUN.name(); // "SUN" |
对枚举常量调用toString()
会返回和name()
一样的字符串。但是,toString()
可以被覆写,而name()
则不行。
1 | public class Main { |
ordinal()
返回定义的常量的顺序,从0开始计数。
1 | int n = Weekday.MON.ordinal(); // 1 |
如果不小心修改了枚举的顺序,编译器是无法检查出这种逻辑错误的。我们可以定义private
的构造方法,并且,给每个枚举常量添加字段:
1 | public class Main { |
这样就无需担心顺序的变化,新增枚举常量时,也需要指定一个int
值。
switch
枚举类可以应用在switch
语句中。因为枚举类天生具有类型信息和有限个枚举常量,所以比int
、String
类型更适合用在switch
语句中:
1 | public class Main { |
default
语句,可以在漏写某个枚举常量时自动报错,从而及时发现错误。
四. 记录类
使用String
、Integer
等类型的时候,这些类型都是不变类,一个不变类具有以下特点:
- 定义class时使用
final
,无法派生子类; - 每个字段使用
final
,保证创建实例后无法修改任何字段。
Java 14开始,引入了新的Record
类。我们定义Record
类时,使用关键字record
。
1 | public class Main { |
除了用final
修饰class以及每个字段外,编译器还自动为我们创建了构造方法,和字段名同名的方法,以及覆写toString()
、equals()
和hashCode()
方法。
构造方法
假设Point
类的x
、y
不允许负数,需要给Point
的构造方法加上检查逻辑。[ 方法public Point {...}
被称为Compact Constructor ]
1 | public record Point(int x, int y) { |
作为record
的Point
仍然可以添加静态方法。一种常用的静态方法是of()
方法,用来创建Point
:
1 | public record Point(int x, int y) { |
使用:
1 | var z = Point.of(); |
五. 常用工具类
1. BigInteger
在Java中,由CPU原生提供的整型最大范围是64位long
型整数。使用long
型整数可以直接通过CPU指令进行计算,速度非常快。
java.math.BigInteger
可以表示任意大小的整数。对BigInteger
做运算的时候,只能使用实例方法。
1 | BigInteger i1 = new BigInteger("1234567890"); |
BigInteger
和Integer
、Long
一样,也是不可变类,并且也继承自Number
类。可以把BigInteger
转换成基本类型。如果BigInteger
表示的范围超过了基本类型的范围,转换时将丢失高位信息,即结果不一定是准确的。如果需要准确地转换成基本类型,可以使用intValueExact()
、longValueExact()
等方法,在转换时如果超出范围,将直接抛出ArithmeticException
异常。
1 | BigInteger i = new BigInteger("123456789000"); |
2. BigDecimal
import java.math.BigDecimal;
BigDecimal
可以表示一个任意大小且精度完全准确的浮点数。
1 | BigDecimal bd = new BigDecimal("123.4567"); |
scale()
表示小数位数:
1 | BigDecimal d1 = new BigDecimal("123.45"); |
stripTrailingZeros()
方法,可以将一个BigDecimal
格式化为一个相等的,但去掉了末尾0的BigDecimal
:
1 | BigDecimal d1 = new BigDecimal("123.4500"); |
可以对一个BigDecimal
设置它的scale
:
import java.math.RoundingMode;
1 | BigDecimal d1 = new BigDecimal("123.456789"); |
对BigDecimal
做加、减、乘时,精度不会丢失,但是做除法时,存在无法除尽的情况,这时,就必须指定精度以及如何进行截断:
1 | BigDecimal d1 = new BigDecimal("123.456"); |
divideAndRemainder()
方法,返回的数组包含两个BigDecimal
,分别是商和余数
1 | BigDecimal n = new BigDecimal("12.75"); |
比较
使用equals()
方法不但要求两个BigDecimal
的值相等,还要求它们的scale()
相等
1 | BigDecimal d1 = new BigDecimal("123.456"); |
使用compareTo()
方法来比较,它根据两个值的大小分别返回负数、正数和0
,分别表示小于、大于和等于。
3. Math
求绝对值
1 | Math.abs(-7.8); // 7.8 |
求最值
1 | Math.max(100, 99); // 100 |
求$x^y$
1 | Math.pow(2, 10); // 2的10次方=1024 |
对数
1 | Math.log(4); // 以e为底的对数 1.386... |
三角函数
1 | Math.sin(3.14); // 0.00159... |
数字常量
1 | double pi = Math.PI; // 3.14159... |
随机数($0\le x< 1$)
1 | Math.random(); // 0.53907... 每次都不一样 |
StrictMath
库提供了和Math
几乎一模一样的方法。这两个类的区别在于,由于浮点数计算存在误差,不同的平台(例如x86和ARM)计算的结果可能不一致(指误差不同),因此,StrictMath
保证所有平台计算结果都是完全相同的,而Math
会尽量针对平台优化计算速度
4. Random
import java.util.Random;
用来创建伪随机数。所谓伪随机数,是指只要给定一个初始的种子,产生的随机数序列是完全一样的。
1 | Random r = new Random(); |
创建Random
实例时,如果不给定种子,就使用系统当前时间戳作为种子;
5. SecureRandom
用于产生安全随机数。
SecureRandom
无法指定种子,它使用RNG(random number generator)算法。JDK的SecureRandom
实际上有多种不同的底层实现,有的使用安全随机种子加上伪随机数算法来产生安全的随机数,有的使用真正的随机数生成器。其种子是通过CPU的热噪声、读写磁盘的字节、网络流量等各种随机事件产生的“熵”。
1 | import java.util.Arrays; |