字符串与编码

在Java中,String是一个引用类型,它本身也是一个class

  • 不可变:Java字符串的一个重要特点就是字符串不可变。这种不可变性是通过内部的private final char[]字段,以及没有任何修改char[]的方法实现的。

String (Java SE 11 & JDK 11 ) (runoob.com)

字符串比较

必须使用equals()方法而不能用==。要忽略大小写比较,使用equalsIgnoreCase()方法。

1
s1.equals(s2)

搜索/提取子串

1
2
3
4
5
6
7
8
9
10
// 是否包含子串:
"Hello".contains("ll"); // true

"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"

去除首尾空白字符

使用trim()方法可以移除字符串首尾空白字符。空白字符包括空格,\t\r\n(其代码点小于或等于 'U+0020' (空格字符)的任何字符。):

1
"  \tHello\r\n ".trim(); // "Hello"

注意:trim()并没有改变字符串的内容,而是返回了一个新字符串。

判断字符串是否为空/空白

  • 是否为空:string.isEmpty()
  • 是否为空白:string.isBlank()
1
2
3
4
"".isEmpty(); // true,因为字符串长度为0
" ".isEmpty(); // false,因为字符串长度不为0
" \n".isBlank(); // true,因为只包含空白字符
" Hello ".isBlank(); // false,因为包含非空白字符

替换子串

s.replace(A,B)把所有A换成B

  • 正则表达式

分割字符串

split()方法,并且传入的也是正则表达式:

1
2
String s = "A,B,C,D";
String[] ss = s.split("\\,"); // {"A", "B", "C", "D"}

拼接字符串

静态方法join(),它用指定的字符串连接字符串数组:

1
2
String[] arr = {"A", "B", "C"};
String s = String.join("***", arr); // "A***B***C"

格式化字符串

formatted()方法和format()静态方法,可以传入其他参数,替换占位符,然后生成新的字符串:

1
2
3
4
5
6
//1
String s = "Hi %s, your score is %d!";
String news = s.formatted("Alice", 80);
//2
String news = String.format("Hi %s, your score is %.2f!", "Bob", 59.5)

如果不确定用啥占位符,那就始终用%s,因为%s可以显示任何数据类型。要查看完整的格式化语法,请参考JDK文档

类型转换

  • 转为字符串

要把任意基本类型或引用类型转换为字符串,可以使用静态方法valueOf()。这是一个重载方法,编译器会根据参数自动选择合适的方法

1
2
String.valueOf(45.67); // "45.67"
String.valueOf(true); // "true"
  • 转为intInteger.parseInt("123")

  • 转为booleanBoolean.parseBoolean("true")

  • 要特别注意,Integer有个getInteger(String)方法,它不是将字符串转换为int,而是把该字符串对应的系统变量转换为Integer

    1
    Integer.getInteger("java.version"); // 版本号,11

转换为char[]

Stringchar[]类型可以互相转换,方法是:

1
2
3
char[] cs = "Hello".toCharArray(); // String -> char[]
String s = new String(cs); // char[] -> String
String s2 =Arrays.toString(cs) // [] -> String 需要import java.util.Arrays;

修改外部的char[]数组不会影响String实例内部的char[]数组,因为这是两个不同的数组。

String的不变性设计可以看出,如果传入的对象有可能改变,我们需要复制而不是直接引用。

1
2
3
4
5
6
7
8
9
class Score {
private int[] scores;
public Score(int[] scores) {
this.scores = Arrays.copyOf(scores, scores.length);
}
}

//或者Main里面调用的时候
Score s = new Score(Arrays.copyOf(scores, scores.length));

字符编码

在Java中,char类型实际上就是两个字节的Unicode编码。如果我们要手动把字符串转换成其他编码,可以这样做:

1
2
3
4
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,可以这样做:

1
2
3
byte[] b = ...
String s1 = new String(b, "GBK"); // 按GBK转换
String s2 = new String(b, StandardCharsets.UTF_8); // 按UTF-8转换

始终牢记:Java的Stringchar在内存中总是以Unicode编码表示。

StringBuilder

可以直接用+拼接字符串。但是,在循环中,每次循环都会创建新的字符串对象,然后扔掉旧的字符串。这样,绝大部分字符串都是临时对象,不但浪费内存,还会影响GC效率。

提供了StringBuilder,它是一个可变对象,可以预分配缓冲区,这样,往StringBuilder中新增字符时,不会创建新的临时对象:

1
2
3
4
5
6
StringBuilder sb = new StringBuilder(1024);
for (int i = 0; i < 1000; i++) {
sb.append(',');
sb.append(i);
}
String s = sb.toString();

对于普通的字符串+操作,并不需要我们将其改写为StringBuilder,因为Java编译器在编译时就自动把多个连续的+操作编码为StringConcatFactory的操作。在运行期,StringConcatFactory会自动把字符串连接操作优化为数组复制或者StringBuilder操作。

链式操作

进行链式操作的关键是,定义的append()方法会返回this,这样,就可以不断调用自身的其他方法。

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
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;
}
}

StringJoiner

import java.util.StringJoiner;

用分隔符拼接数组

可以指定开头结尾

1
2
3
4
var sj = new StringJoiner(", ", "Hello ", "!");
for (String name : names) {
sj.add(name);
}

在不需要指定“开头”和“结尾”的时候,用静态方法String.join()更方便

数据类型

Java的数据类型分两种:

  • 基本类型:byteshortintlongbooleanfloatdoublechar
  • 引用类型:所有classinterface类型

引用类型可以赋值为null,表示空,但基本类型不能赋值为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的赋值写法,称为自动装箱(Auto Boxing),反过来,把Integer变为int的赋值写法,称为自动拆箱(Auto Unboxing)。

注意:自动装箱和自动拆箱只发生在编译阶段,目的是为了少写代码。

装箱和拆箱会影响代码的执行效率,并且,自动拆箱执行时可能会报NullPointerException

intInteger互相转换一般写为

1
2
3
int i = 100;
Integer n = Integer.valueOf(i);
int x = n.intValue();

不变类

引用类型特性:创建即不变,只能用equals()比较

但因为Java标准库的Integer内部有缓存优化,编译器把Integer x = 127;自动变为Integer x = Integer.valueOf(127);,为了节省内存,Integer.valueOf()对于较小的数,始终返回相同的实例,因此,==比较“恰好”为true

创建Integer对象

  • 方法1:Integer n = new Integer(100);
  • 方法2:Integer n = Integer.valueOf(100);//更好,静态工厂方法,它尽可能地返回缓存的实例以节省内存。把内部优化留给Integer的实现者去做

进制转换

1
2
3
4
5
6
7
8
9
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

静态变量

1
2
3
4
5
6
7
8
9
// 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)

处理无符号整型

在Java中,并没有无符号整型(Unsigned)的基本数据类型。

无符号整型和有符号整型的转换在Java中就需要借助包装类型的静态方法完成。

例如:Byte.toUnsignedInt(-1)//输出255。byte是有符号整型,范围是-128~+127,但如果把byte看作无符号整型,它的范围就是0-255

类似的,可以把一个short按unsigned转换为int,把一个int按unsigned转换为long

JavaBean

在Java中,有很多class的定义都符合这样的规范:

  • 若干private实例字段;
  • 通过public方法来读写实例字段。

如果读写方法符合以下这种命名规范:

1
2
3
4
// 读方法:
public Type getXyz()
// 写方法:
public void setXyz(Type value)

那么这种class被称为JavaBean

通常把一组对应的读方法(getter)和写方法(setter)称为属性(property)。例如,name属性:

  • 对应的读方法是String getName()
  • 对应的写方法是setName(String)

只有getter的属性称为只读属性(read-only)

只有setter的属性称为只写属性(write-only)。

  • boolean字段比较特殊,它的读方法一般命名为isXyz()

通过IDE,可以快速生成gettersetter。IDEA中右键-生成。

image-20211227182415203

使用Introspector.getBeanInfo()可以获取属性列表。

枚举类enum

为了让编译器能自动检查某个值在枚举的集合内,并且,不同用途的枚举需要不同的类型来标记,不能混用,我们可以使用enum来定义枚举类

1
2
3
enum Weekday {
SUN, MON, TUE, WED, THU, FRI, SAT;
}
  • enum常量本身带有类型信息,即Weekday.SUN类型是Weekday,编译器会自动检查出类型错误。
  • 不可能引用到非枚举的值,因为无法通过编译。
  • 不同类型的枚举不能互相比较或者赋值,因为类型不符。
  • 是引用类型,但可以用==比较,因为enum类型的每个常量在JVM中只有一个唯一实例

enum类型

通过enum定义的枚举类,和其他的class有什么区别?

答案是没有任何区别。enum定义的类型就是class,只不过它有以下几个特点:

  • 定义的enum类型总是继承自java.lang.Enum,且无法被继承;
  • 只能定义出enum的实例,而无法通过new操作符创建enum的实例;
  • 定义的每个实例都是引用类型的唯一实例;
  • 可以将enum类型用于switch语句。

实例方法:

  • name():返回常量名
  • ordinal():返回定义的常量的顺序,从0开始计数
  • 类.values():将枚举类转变为一个枚举类型的数组

可以定义private的构造方法,并且,给每个枚举常量添加字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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()!

枚举类可以应用在switch语句中。

记录类

从Java 14开始,引入了新的Record类。

1
public record Point(int x, int y) {}

recordfinal修饰class以及每个字段外,编译器还自动为我们创建了构造方法,和字段名同名的方法,以及覆写toString()equals()hashCode()方法。

构造方法

可以编写Compact Constructor对参数进行验证(不写参数)

1
2
3
4
5
6
7
public record Point(int x, int y) {
public Point { //没有参数
if (x < 0 || y < 0) {
throw new IllegalArgumentException();
}
}
}

静态方法

仍然可以添加静态方法。一种常用的静态方法是of()方法,用来创建Point

1
2
3
4
5
6
7
8
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);
}
}

JDK15等使用record关键字,需要编译器打开–enable-preview

IDEA 点击File -> Project Structure -> Project Settings -> Project选项卡,选择Project language level中的15 (Preview)这一项,然后点击OK。

BigInteger

import java.math.BigInteger

java.math.BigInteger就是用来表示任意大小的整数。BigInteger内部用一个int[]数组来模拟一个非常大的整数

BigInteger做运算的时候,只能使用实例方法

BigInteger转换成基本类型时可使用longValueExact()等方法保证结果准确。(如果超出了long型的范围,会抛出ArithmeticException。)

BigIntegerIntegerLong一样,也是不可变类,并且也继承自Number类。因为Number定义了转换为基本类型的几个方法:

  • 转换为bytebyteValue()
  • 转换为shortshortValue()
  • 转换为intintValue()
  • 转换为longlongValue()
  • 转换为floatfloatValue()
  • 转换为doubledoubleValue()

因此,通过上述方法,可以把BigInteger转换成基本类型。如果BigInteger表示的范围超过了基本类型的范围,转换时将丢失高位信息,即结果不一定是准确的。如果需要准确地转换成基本类型,可以使用intValueExact()longValueExact()等方法,在转换时如果超出范围,将直接抛出ArithmeticException异常。

  • 如果BigInteger的值甚至超过了float的最大范围(3.4x1038),那么返回的float是Infinity

常见方法(运算)

BigInteger (Java SE 11 & JDK 11 ) (runoob.com)

add, subtract, multiply, divide(divideAndRemainder(BigInteger val),)

变量和类型 方法 描述
BigInteger add(BigInteger val) 返回一个值为 (this + val)的BigInteger。
BigInteger subtract(BigInteger val) 返回值为 (this - val)的BigInteger。
BigInteger multiply(BigInteger val) 返回值为 (this * val)的BigInteger。
BigInteger divide(BigInteger val) 返回值为 (this / val)的BigInteger。
BigInteger[] divideAndRemainder(BigInteger val) 返回两个BigIntegers的数组,其中包含 (this / val)后跟 (this % val)
BigInteger mod(BigInteger m) 返回一个值为 (this mod m的BigInteger。
BigInteger pow(int exponent) 返回值为 (thisexponent)的BigInteger。

BigDecimal

import java.math.BigDecimal;

BigDecimal可以表示一个任意大小且精度完全准确的浮点数。

BigDecimal (Java SE 11 & JDK 11 ) (runoob.com)

  • scale()表示小数位数,

  • stripTrailingZeros()方法,可以将一个BigDecimal格式化为一个相等的,但去掉了末尾0的BigDecimal

  • setScale()方法设置精度

    1
    2
    3
    4
    import java.math.RoundingMode;
    //...
    BigDecimal d2 = d1.setScale(4, RoundingMode.HALF_UP); // 四舍五入,123.4568
    BigDecimal d3 = d1.setScale(4, RoundingMode.DOWN); // 直接截断,123.4567
  • BigDecimal做加、减、乘时,精度不会丢失,但是做除法时,存在无法除尽的情况,这时,就必须指定精度以及如何进行截断:

    1
    2
    BigDecimal d3 = d1.divide(d2, 10, RoundingMode.HALF_UP); // 保留10位小数并四舍五入
    BigDecimal d4 = d1.divide(d2); // 报错:ArithmeticException,因为除不尽
  • 必须使用compareTo()方法来比较大小,它根据两个值的大小分别返回负数、正数和0,分别表示小于、大于和等于。

    • 不要使用equals()! 在比较两个BigDecimal的值是否相等时,要特别注意,使用equals()方法不但要求两个BigDecimal的值相等,还要求它们的scale()相等

常用工具类

Math

顾名思义,Math类就是用来进行数学计算的,它提供了大量的静态方法来便于我们实现数学计算:

计算ex次方:

1
Math.exp(2); // 7.389...

计算以e为底的对数:

1
Math.log(4); // 1.386...

计算以10为底的对数:

1
Math.log10(100); // 2

三角函数:

1
2
3
4
5
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还提供了几个数学常量:

1
2
3
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

1
Math.random(); // 0.53907... 每次都不一样

生成一个区间在[MIN, MAX)的随机数

1
2
double x = Math.random(); // x的范围是[0,1)
double y = x * (max - min) + min; // y的范围是[10,50)

Random

import java.util.Random;

Random用来创建伪随机数。所谓伪随机数,是指只要给定一个初始的种子,产生的随机数序列是完全一样的。

要生成一个随机数,可以使用nextInt()nextLong()nextFloat()nextDouble()

1
2
3
4
5
6
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实例时指定一个种子,就会得到完全确定的随机数序列Random r = new Random(12345);

SecureRandom

import java.security.SecureRandom;

有伪随机数,就有真随机数。实际上真正的真随机数只能通过量子力学原理来获取,而我们想要的是一个不可预测的安全的随机数,SecureRandom就是用来创建安全的随机数的:

1
2
SecureRandom sr = new SecureRandom();
System.out.println(sr.nextInt(100));

SecureRandom无法指定种子,它使用RNG(random number generator)算法。JDK的SecureRandom实际上有多种不同的底层实现,有的使用安全随机种子加上伪随机数算法来产生安全的随机数,有的使用真正的随机数生成器。实际使用的时候,可以优先获取高强度的安全随机数生成器,如果没有提供,再使用普通等级的安全随机数生成器: