String 相关

String 的实现

String 在老版本的 Java 中以 private final char[] 基本数据结构封装成为类,新版本则是换成了 private final byte[] (因为对于 ASCII 字符可以节省大量的空间)。里面封装了很多方法和属性可以使用。

在 Java 的编译期,会自动地把所有的字符常量都放入一个常量池中,然后 String 类型的变量其 实就是引用了一个常量的地址。那么在判断两个字符串是否相同其实就是在判断它们对于常量池的地址引用是否相同。所以对于涉及到可变字符串的比较(在程序运行中会对字符串进行改变),是不可以使用 == 来判断两个字符是否相等,反而应该使用 String 类内置的方法 String1.equals(String1) 来进行比较。 如果要忽略大小写则可以使用 equalsIgnoreCase() 方法来比较忽略大小写之后是否相同。

搜索和提取字串

String 类里面还内置了很多用来操作字符串的方法:

  • 判断是否包含子串:
    contains(String) 返回 boolean 为输入的 String 是否在当前的实例中包含。

  • 搜索子串:

    indexOf(String) 返回 int 为最前匹配到输入子串 String 的坐标。

    lastIndexOf(String) 返回 int 为最后匹配到输入子串 String 的坐标。

    startsWith(String) 返回 boolean 为是否以输入子串 String 为开头。

    endsWith(String) 返回 boolean 为是否以输入子串 String 为结尾。

  • 截取字串:

    substring(BeginIndex,EndIndex) 返回 String 为从开始坐标到结尾坐标的连续子串。(EndIndex可以省略,则默认为字符串结尾)

  • 去除首尾空白字符:

    trim() 返回一个去除了首尾空白字符的字符串。

    strip() 作用和 trim() 相同,但是类中文的空白字符也会被删除.

  • 替换子串:

    replace(String1,String2) 返回一个将实例的 String1 全部替换为 String2 的字符串。

    replaceAll(Regex,String2) 返回一个将实例中符合正则表达式 Regex 的地方全部替换为 String2 的字符串。

  • 分割字串

    split(Regex) 返回一个符合正则表达式 Regex 的地方分割出来的字符串数组。

  • 拼接字符串

    String.join(String1,StrArr) 此为 String 类的静态方法,将输入的字符串数组的元素之间加上 String1 然后拼接成一个新的字符串。

    String1+String2 更省力一点,直接使用 + 运算。

  • 格式化字符串

    占位符和 scanf 差别不多,%s %c %d %f 等等。

    formatted() 非静态方法,作用于已经格式化好的字符串,只需要在参数里面填充替换元素就可以了,最后返回替换完成的字符串。

    format() 静态方法,通过类 String 调用,需要在第一个参数位置填充格式化的字符串,然后后面的参数位置一次填充替换元素,最后返回替换完成的字符串。

String 和 char[] 转换

String 变量转化为 char[] 可以直接使用 String 类里面的 toCharArray(),此方法会返回一个字符串实例转化成的字符数组,比如:char[] Xorex="Xorex".toCharArray();

而 char[] 转换为 String 需要在实例化出来一个 String 的时候,将字符数组 char[] 作为构造参数加进去即可,这样返回的实例就是 char[] 转化的字符串,比如:String Xorex=new String(CharArray); ,需要注意的一点是这种转化形式虽说是引用类型传入了 String 类的构造方法,但是内部在生成新的字符串的时候,并不是对传入 char[] 类型数据的引用,而是复制,也就是说这是两个独立的数据,并不需要担心对一个的修改而造成对另外一个的改变。

String 编码

最开始的开始,计算机的字符是使用 ASCII 作为编码方式的,这让英语系国家们用的很开心,只需要 1 个字节就可以轻松的表示自己所有的字符。

但是后来有其他的非英语系国家为了使用自己国家语言文字的计算机,开始开发自己国家的字符编码,比如中国汉字的编码 GB2312-80GBK 。但是对于网上传输其他国家的文字,因为使用的文字编码不同,会导致无法解析,比如韩国电脑解析不出来 GBK 编码的文字,打开直接乱码。

为了统一编码方式,将所有的文字都制定一个标准,让所有人都能解析所有文字,于是 Unicode 就诞生了,这是一个立志于编写所有文字的标准。

在创造 Java 的时候,当时创造者认为两个字节的 Unicode 就可以容纳世界上所有的主流文字了,所以就给 Java 的 char 类型设置了两字节的空间,并使用 Unicode 作为 char 类型的编码方式。由于像汉字这样的表意文字大量加入 Unicode 家族,导致了 Unicode 的大小不断膨胀,一度到了四个字节的大小。

这时候英语系国家的人就不乐意了,因为他们的大量内容都是使用 ASCII 作为编码的,他们不但需要改变自己的编码方式,还要白白的多使用三个字节的空间来存储一个字符,于是基于 Unicode 字符集的编码方式: UTF-8 就诞生了。

UTF-8 的最大特性就是可变字符长度,对于 ASCII 的字符,编码方式和 ASCII 相同,都只占用一个字节的空间。这样就不但不需要改变原来使用 ASCII 编码的信息,还符合和其他文字一起用的标准,并且能节省大量的空间。由于这些优良的特性,使得 UTF-8 编码快速成为了互联网上最流行的文字编码形式。

类型转换

对于和 String 类有关的类型转换只有两种,一种是从其他类型按照长得样子转化为 String 类型,另外一种则是将 String 字符按照长的样子转化为所属于的类型。

  1. 对于其他类型转化为 String 类型,则使用 String 类的静态方法 valueOf(); 这是一个重载方法,尽管输入不同的类型就是了。比如 String.valueOf(3.14); 会返回一个字符串 "3.14"
  2. 对于 String 类型转化为其他类型,则需要使用要转化为类型所对应的类的静态方法,比如将字符串 “3.14” 转化为 double 类型的数据,则需要 Java 自带的类 Double,里面有一个方法 parseDouble() 可以将接受的字符串转化为字面的 double 类型的数据。需要注意的是,如果输入的字符串对于需要转换的数据类型来说不合法,那么就会报错。基本数据类型对应的类一般都是首字母大写的全称。
  3. 还有一种比较特殊的类型转换,那就是将 String 类型转化为字节类,使用的是 String 类的方法:.getBytes() ,会返回一个字节数组,里面的数组是 String 类里面的字符使用 Unicode 编码的存储。

StringBuilder

String 类可以使用 + 来将两个字符串进行拼接,对于常量的字符串来说,"Xorex"+"Tempest" 会被在编译的时候直接变成常量 "XorexTempest" ,速度会很快。但是对于非常量的字符来说,每一次使用 + 来运算 String 类的时候,就会经历创建新的字符串,扔掉旧的字符串这个过程。这不但会占用大量的内存,还会影响 Java GC(Garbage Collection)的效率。于是 Java 中就引入了全新的可变字符长度的类 StringBuilder 和 StringBuffer 。两者几乎相同同,唯一的不同是 StringBuffer 是线程安全的,因此效率要低一点。

StringBuilder 类的初始化,也就是构造方法有三种,一种是接受 String 作为初始化数据,一种是接受 CharSequence 作为初始化数据(CharSequence 不太了解就不管了),最后就是接受一个整形作为初始化容器的大小(默认大小是16)。

StringBuilder 里面有很多对字符串进行操作的方法,比如在字符串后面附加字符串 .append(String str); ,在指定位置插入字符串 .insert(int Pos,String str);,在进行大量的字符串操作中,因为使用的是可变的字符串,所以性能会快上很多。由于内置的所有操作方法都会返回实例本身 this,也就是说可以进行链式操作,将需要的一套操作接在后面即可。

如果预测或者花较小的代价知道需要操作的字符串最大的长度是多少,这个速度还能更快,因为这个类的默认容器大小是16,如果不够用,会重新创建一个两倍原来大小的容器来存放字符串。如果操作字符很大,那么就会有 log(n) 次容器的销毁和创建,如果在初始化的时候直接输入一个合适的大小,那么容器销毁和创建次数就会大大减少,从而提高性能。

StringJoiner

故名思意,这个类就是用来连接 String 类的,专门用来处理用分隔符拼接数组,首先需要给构造函数一个分隔符,new StringJoiner(String SplitSignal),当然也可以在添加分隔符的同时指定开头和结尾,只需要在 SplitSignal 后面依次加上两个字符串就可以了,比如:new StringJoiner(String SplitSignal,String StartSignal,String EndSignal);

String 类提供了一个静态方法 String.join(String SplitSignal,String[] StringArr) ,这个静态方法会返回一个用分隔符来拼接的数组。

包装类型

一般来说,引用类型是一个类,而基本类型则不是,但是我们也可以把 Java 的基本类型包装成引用类型。在 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
Integer n=1; // Integer n=Integer.valueOf(1);
int m=n; // int m=n.intValue();

上面的代码会被编译器自动优化成注释后面的代码,这被称为自动装箱和自动拆箱。

因为是个类,所以不可以使用 == 来判断是否相等,需要使用 equals() 来比较判断。

工厂方法

观察上面的代码,Integer.valueOf() 返回的是一个对象,也就是说,这个方法可以创建对象,我们把能够创建一个新的对象的 静态方法 称为工厂方法。查看 Integer.valueOf() 的源代码就可以发现,当输入的参数在一个特定的范围里面的时候,就会使用缓存技术来提高性能。当然这样我们就不需要考虑创建对象的具体细节,将这些任务交给工厂方法就可以了。

包装其他的方法

像 Integer 类里面还包含有很多用于进制转换的方法,比较好用的比如 Integer.toString(int i,int cimal) 然后这个方法就会返回将数字 i 转化为对应 cimal 进制的字符串。

Integer.parseInt(String Num) 则是会将一个长得像整形数字的字符串转化为一个整形并返回。如果再添加一个整形的进制参数,可以按照指定的进制进行解析字符串所表示的字符。

处理无符号整形

虽然 CPU 支持无符号整形,但是 Java 并没有提供这些。于是基本数据类型的包装方法加入了可以转化无符号数为有符号数的方法。

转化的方法就是按照当前数据的二进制补码看作无符号整形的编码来解析,比如:

1
2
short n=-1;
int m=Short.toUnsignedint(n);

结果这个 m 的值是 255,因为 -1 在计算机里的存储是 11111111 ,那么按照无符号来进行解析的话,它就是十进制的 255 。因为这个表示范围肯定超过了有符号数的范围,所以进行无符号转化的时候,必须转化为更大范围的基本数据,比如 Short 只有 toUnsignedInt()toUnsignedLong() 两个方法,Long 的无符号转化就只能转化为 String 类型。

JavaBean

JavaBean 的内容和 JavaBean 这个名称没有任何的关系,JavaBean 其实指的是一种设计模式,就是将类中的非方法字段,全部设置成 private ,对于外界想要改变和获取的非方法字段,单独设置 public 方法 getter() 和 setter() 来对这些非方法字段进行修改和读取。

为什么要这样干? 其实这是为了保证程序的兼容性,有些非方法字段如果随意开放,一旦在后期的程序迭代中改变了这个非方法字段,那么以前的程序就有运行的问题。所以保留一个 getter() 和 setter() 方法,这样只需要修改这两个方法里面的代码就可以解决兼容性的问题。

举个例子:

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
31
32
33
34
35
36
37
public class test
{
public static void main(String[] args)
{
Tempest Xorex;
Xorex.setHight(178);
Xorex.setWeight(100);
System.out.println(Xorex.getHight());
System.out.println(Xorex.getWeight());
}
}

class Tempest
{
private int hight;
private int weight;

public void setHight(int hight)
{
this.hight=hight;
}

public void setWeight(int weight)
{
this.weight=weight;
}

public int getHight()
{
return this.hight;
}

public int getWeight()
{
return this.weight;
}
}

上面的代码通过一组 set/get 方法将私有字段与外界联系起来,这样的一组方法和字段被称为类的一个属性。比如上面的代码 Tempest 类就设置了两个属性 hight 和 weight ,一定要记得属性是由三个字段组成,set 方法,get 方法和数据。

枚举类 enum

enum -> enumeration [ɪˌnjuːməˈreɪʃn] 枚举

enum 是一个特殊的标识符,表示将一个类声明为枚举类,枚举类是一系列常量元素的集合。这里的枚举要实现的目标就是一个常量集合,比如月份,星期这些。有的时候我们需要处理这些需要被枚举的常量的时候,一个枚举类的构造会大大规范我们代码的统一性。

而一个枚举类的定义也很简单,直接用 enum + 类名 即可:

1
2
3
4
enum Week
{
Mon,Tue,Wed,Thu,Fri,Sat,Sun;
}

上面的代码在编译的时候,会被自动转化为:

1
2
3
4
5
6
7
8
9
10
public final class Week extends Enum
{
public static final Week Mon=new Week();
public static final Week Tue=new Week();
public static final Week Wed=new Week();
public static final Week Thu=new Week();
public static final Week Fri=new Week();
public static final Week Sat=new Week();
public static final Week Sun=new Week();
}

因为枚举类是继承于 Enum 类,所以 Enum 类里面的很多很好玩的方法就可以使用了。

name() 不可重写方法,返回常量名。

toString() 可重写方法,默认返回常量名。

ordinal() 返回常量的顺序。

values() 静态方法,返回一个包含所有常量的数组。

下面一般是比较常用的 enum 枚举类的使用方式。

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
public class test {
public static void main(String[] args) {
for(Week i : Week.values())
System.out.println(i);
}
}

enum Week {

Mon("Monday"),
Tue("Tuesday"),
Wed("Wednesday"),
Thu("Thursday"),
Fri("Friday"),
Sat("Saturday"),
Sun("Sunday");

private String value;

private Week(String value) {
this.value=value;
}

@Override
public String toString() {
return this.value;
}

}

记录类 record

上面提到了枚举类 enum,用一个简单的语法来代替常用的设置类情形。同样的,在写代码的时候,会出现想要写一个类来专门保存一组不变的数据,对于拥有这种统一格式的数据,有一种专门的类叫作 record 记录类 来将这些数据记录下来。

record 的语法和 enum 很像:

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

这样就可以声明一个叫作 Point 的纪录类了,编译器会自动把这些代码都补全:

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 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() {
...
}
}

这样和 enum 一样,用了极少的代码完成了这些任务,语法糖真甜。

除了使用最基本的语法糖,我们当然可以在里面添加一些自定义的功能,比如补充构造方法,添加静态方法。

1
2
3
4
5
6
7
8
record Point(int x,int y) {
public Point {
if(x>y) throw new IllegalArgumentException();
}
public static void Out() {
System.out.println("Point");
}
}

如果想要添加普通的方法,那为什么不试试功能更加全面的普通类呢?这个 record 语法糖是在 Java 14 才更新的,而且还是 preview 功能,大概还没有多少人用过这个东西吧。

高精度 BigInteger

对于处理超过 long 表示的整数的时候,Java 贴心的准备了 BigInteger 类来处理这些内容,其内部是使用 final int[] 来实现的高精度,使用之前导入 java.math.BigInteger 类。

  • add() 两个实例之间相加。

  • subtract() 两个实例之间相减。

  • multiply() 两个实例之间相乘。

  • divide() 两个实例之间相除。

  • pow(int x) 将实例的 x 幂。

当然这里面还包括将 BigInteger 转化为基本数据类型的方法:

  • 转换为 byte:byteValue()
  • 转换为 short:shortValue()
  • 转换为 int:intValue()
  • 转换为 long:longValue()
  • 转换为 float:floatValue()
  • 转换为 double:doubleValue()

高精度 BigDecimal

不同于 Integer 这个 Decimal 表示的是浮点数,也就是高精度的浮点数,它可以保留任意精度。BigDecimal 里面有一些不太一样的方法,用来解决对于浮点数的一些需求。

  • scale() 返回 BigDecimal 实例的小数位数。
  • stripTrailingZeros() 字面意思 strip 脱去,Trail 末尾,即去掉末尾的零。
  • n.divideAndReminder(m) 让 n 除以 m 并且返回商和余数。返回类型为 BigDecimal[],里面有两个元素,第一个是商,第二个是余数。

将两个 BigDecimal 数据进行比对的时候,尽量不要使用 equals() 而是要使用 compareTo(),对于 equals() 比对的时候比较严格,需要两个BigDecimal 数据不仅数值上相等,还要求两者的 scale() 值也相等。但是 compareTo() 只要求前者相等即为相等。

compareTo() 会返回正数负数和零,分别表示大于小于和等于。

工具类

Math

数学工具人,里面含有各种静态的工具方法,下面大致说一下比较常用的,不常用的用到了再说。

  • 计算绝对值 Math.abs(Num)
  • 计算最大值/最小值 Math.max(Num) / Math.min(Num)
  • 计算 x 的 y 次方 Math.pow(x,y)
  • 计算开方 Math.sqrt(x)
  • 计算 e 的 x 次方 Math.exp(x)
  • 计算以 e 为底的对数 Math.log(x)
  • 计算以10 为底的对数 Math.log10(x)
  • 计算三角函数 Math.sin(x) / cos(x) / tan(x) …
  • 常量 Math.PI / Math.E (double 常量,非高精)
  • 随机数 Math.random() 返回一个 double 的随机数。

Random

一个专门生成伪随机数的一个类,可以创建实例的时候给 Random 构造函数一个种子,也可以不给,如果不给的话,每次在创建实例的时候,会把当前的时间戳作为种子,所以看起来就不是一个伪随机数了。

1
2
3
4
5
Random random=new Random(); //创建一个 Random 的实例,此时 random 的种子已确定。
System.out.println(random.nextInt()); //Int范围内一个随机整数
System.out.println(random.nextDouble()); // 0-1 范围内返回内一个随机浮点数
System.out.println(random.nextInt(100)); // 0-100 范围内返回一个随机整数

SecureRandom

真正的随机数我们是没有办法通过计算机获得的,但是我们只需要一个安全的随机数即可,所谓安全就是这个随机数不可被预测,而这个安全性的保障是通过随机事件所产生的 熵 来实现的。随机事件比如 CPU 的热噪声,读取磁盘的字节,网络流量等等。

正是因为 SecureRandom 的安全性实现是依靠不可被预测的随机熵实现的,所以它无法被设置种子。SecureRandom 有不同的底层实现方法,因为设备的不同使的支持的实现方法也不同,所以我们应该优先调用安全强度更高的安全算法,如果设备不提供的话,再使用普通的安全算法。

那么使用 try-catch 语句来尝试这个过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

public class test {
public static void main(String[] args) {
SecureRandom SR=null;
try {
SR=SecureRandom.getInstanceStrong();
System.out.println("Seccess!");
} catch (NoSuchAlgorithmException e) {
SR=new SecureRandom();
System.out.println("Failed!");
}
System.out.println(SR.nextInt());

}
}

上面的代码执行过程就是,尝试调用 SecureRandom 类中的静态方法 getInstanceStrong() 来获得更高安全性的随机算法。如果设备不支持,那么就使用普通的随机算法。

最后

拖延了一个多星期的核心类学习笔记终于完成了,这一遍地学习更多地是留下一个印象,知道有这些用法,以后肯定会对里面内在的原理之类进行研究的。下一个目标,在年前完成异常处理的学习吧!