Java学习笔记——核心类

上一篇记录了Java的面向对象的语法,这篇来一点常用的核心类。

String类

感觉一个一个描述蛮蠢的,直接上表吧,方便快查

方法原型方法描述
static String copyValueOf(char[] data)返回指定数组中表示该字符序列的 String
static String copyValueOf(char[] data, int offset, int count)返回指定数组中表示该字符序列的 String
void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)将字符从此字符串复制到目标字符数组。
String intern()返回字符串对象的规范化表示形式

字符串比较

子串比较方法方法解释
int compareTo(Object o)把这个字符串和另一个对象比较
int compareTo(String other)按字典顺序比较两个字符串
int compareToIgnoreCase(String str)按字典顺序比较两个字符串,不考虑大小写
boolean equals(Object anObject)将此字符串与指定的对象比较。
boolean equalsIgnoreCase(String other)将此 String 与另一个 String 比较,不考虑大小写。
boolean contentEquals(StringBuffer sb)字符串与指定的StringBuffer有相同顺序的字符时候返回真

除了这些整体比较以及上文的正则表达式匹配之外,还有直接比较两个字符串子串的方法

boolean regionMatches(boolean ignoreCase, int toffset, String other, int ooffset, int len)
/* 测试两个字符串区域是否相等
    ignoreCase: 	是否忽略大小写
    toffset: 		使用方法的字符串中,待比较子串起始位置的偏移量
    other: 			与之比较的另一个字符串
    ooffset: 		other字符串中,待比较子串起始位置的偏移量
    len: 			比较的子串的长度 */
boolean regionMatches(int toffset, String other, int ooffset, int len)
// 同上,少了一个参数

StringBuffer与StringBuilder

和 String 类不同的是,StringBufferStringBuilder 类的对象能够被多次的修改,并且不产生新的未使用对象。一般在代码中出现,多次使用 + 拼接字符串的时候,编译器会选择在StringBuilder中进行拼接,以节约空间。

StringBuilder 的方法不是线程安全的(不能同步访问)。由于 StringBuilder 相较于 StringBuffer速度优势,所以多数情况下建议使用 StringBuilder 类。两者接口相同。然而在应用程序要求线程安全的情况下,则必须使用 StringBuffer 类。

以上两者都支持链式操作,原因是支持链式操作的方法都是返回了this…

感觉这个和之前写qqbot的时候,coolq提供的API的方法很像呢,有点golang基础看这个舒服多了。

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

‘==’与equals()的区别

使用“==”比较字符串,实际上是比较的字符串在内存中的地址。要比较内容只能是使用equals()方法。

String s1 = "abc";
String s2 = "abc";
s1 == s2 // true
// 这是因为Java编译器在编译期,会自动把所有相同的字符串当作一个对象放入常量池,自然s1和s2的引用就是相同的

拼接字符串

拼接方法方法解释
String concat(String str)将指定字符串连接到此字符串的结尾
String join(String link, String[] arr)用给定的link字符串连接字符串数组

在使用join方法的时候,内部实现是通过了一个StringJoin类,这个类是通过StringBuffer实现的。在使用时,可以比join多出一些功能,比如指定拼接后的传的开头与结尾。开头与拼接串,拼接串与结尾,直接连接,不加link,示例如下。

import java.util.StringJoiner;

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());
    }
} // Hello Bob, Alice, Grace!

类型转化

一般类型转换

// 返回给定data type类型x参数的字符串表示形式
static String valueOf(primitive data type x)
String.valueOf(45.67); // "45.67"

// 字符串转换为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

char[]与String类型转换

char[] cs = "Hello".toCharArray(); // String -> char[]
String s = new String(cs); // char[] -> String

通过char[]转出时String,是复制了一份char[]数组,修改外部char[]不会影响String中的char[]。

提取子串

提取子串方法方法解释
String substring(int beginIndex)返回一个新的字符串,它是此字符串的一个子字符串
String substring(int beginIndex, int endIndex)返回一个新字符串,它是此字符串的一个子字符串

编码

手动转变编码

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
String s1 = new String(b, "GBK"); // 按GBK转换
String s2 = new String(b, StandardCharsets.UTF_8); // 按UTF-8转换

去除首位空白字符

去除首位空白字符方法方法描述
String trim()返回字符串的副本,忽略前导和尾部的空白
String strip()同上,但是去除类似中文空格字符\u3000
String stripLeading()同上,只忽略前导空白
String stripTrailing()同上,只忽略尾部空白

判断是否为空

注意区别blank与empty的区别,前者为只包含空白字符,后者为长度为零

"".isEmpty(); // true,因为字符串长度为0
"  ".isEmpty(); // false,因为字符串长度不为0
"  \n".isBlank(); // true,因为只包含空白字符
" Hello ".isBlank(); // false,因为包含非空白字符

正则表达式相关

正则表达式相关方法方法解释
boolean matches(String regex)告知此字符串是否匹配给定的正则表达式。
String replaceAll(String regex, String replacement)用给定的串替换所有匹配给定的正则表达式的子字符串
String replaceFirst(String regex, String replacement)用给定的串替换首个匹配给定的正则表达式的子字符串
String[] split(String regex)根据给定正则表达式的匹配拆分此字符串

一般的普通替换可以使用

String replace(char oldChar, char newChar) 
// 返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。

子串索引

子串索引方法方法解释
int indexOf(int ch)返回指定字符在此字符串中第一次出现处的索引。
int indexOf(int ch, int fromIndex)同上,从指定的索引开始搜索。
int indexOf(String str)返回指定子字符串在此字符串中第一次出现处的索引。
int indexOf(String str, int fromIndex)同上,从指定的索引开始。
int lastIndexOf(int ch)返回指定字符在此字符串中最后一次出现处的索引。
int lastIndexOf(int ch, int fromIndex)同上,从指定的索引处开始进行反向搜索。
int lastIndexOf(String str)返回指定子字符串在此字符串中最右边出现处的索引。
int lastIndexOf(String str, int fromIndex)同上,从指定的索引开始反向搜索。
boolean startsWith(String prefix)测试此字符串是否以指定的前缀开始
boolean endsWith(String suffix)测试此字符串是否以指定的后缀结束

其他常用无参数方法

常用无参数方法方法解释
String toLowerCase()使用默认语言环境的规则将此 String 中的所有字符都转换为小写
String toUpperCase()使用默认语言环境的规则将此 String 中的所有字符都转换为大写。
int hashCode()返回此字符串的哈希码
int length()返回此字符串的长度

其他内容

早期JDK版本中,String 总是以 char[] 存储的,而较新版本JDK下的 String 是以 byte[] 存储的。如果String仅包含ASCII字符,则每个byte存储一个字符,否则,每两个byte存储一个字符,这样做的目的是为了节省内存,因为大量的长度较短的String通常仅包含ASCII字符

对于使用者来说,String内部的优化不影响任何已有代码,因为它的public方法签名是不变的。

包装类型

Java有内置的,将基本类型包装为引用类型的类

基本类型对应的引用类型
booleanjava.lang.Boolean
bytejava.lang.Byte
shortjava.lang.Short
intjava.lang.Integer
longjava.lang.Long
floatjava.lang.Float
double`java.lang.Double
charjava.lang.Character

创建包装类型

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

使用valueOf方法的时候,是把创建实例的工作留给Integer类内部去做,这些东西有可能会进行了优化,比直接使用new更优。我们把能创建“新”对象的静态方法称为静态工厂方法Integer.valueOf()就是静态工厂方法,它尽可能地返回缓存的实例以节省内存;而直接使用new不是方法,更不是静态工厂方法。

Auto Boxing

Integer n = 100; // 编译器自动使用Integer.valueOf(int)
int x = n; // 编译器自动使用Integer.intValue()

这样直接完成的,从基本类型到引用类型的转换叫做自动装箱(Auto Boxing),反向地,从引用类型到基本类型叫做自动拆箱(Auto Unboxing)。这两个过程只发生在编译阶段(这句话没怎么看懂),目的是为了少些代码。但是装箱与拆箱会影响代码执行效率,因为在编译之后,这两个东西是完全区分开的,而且,由于引用类型可以有null值,所以在自动拆箱的时候还有可能产生NullPointerException

不变类

所有的包装类型都是不变类,也就是源码中设置的private final int value,一旦创建,无法改变。

在比较包装类型的内容的时候,必须要使用equals()方法。此时使用==有些可以达成目的,原理同String类比较。

数的类型转换

所有的整数和浮点数的包装类型都继承自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();

Integer类

进制转换

Integer.toString(100) // "100",表示为10进制
Integer.toString(100, 36) // "2s",表示为36进制
(Integer.toHexString(100) // "64",表示为16进制
Integer.toOctalString(100) // "144",表示为8进制
Integer.toBinaryString(100) // "1100100",表示为2进制

上述方法输出的都是String。

字符串解析整数

int x1 = Integer.parseInt("100"); // 100
int x2 = Integer.parseInt("100", 16); // 256,因为按16进制解析

常用静态变量

// int可表示的最大/最小值:
int max = Integer.MAX_VALUE; // 2147483647
int min = Integer.MIN_VALUE; // -2147483648
// long类型占用的bit和byte数量:
int sizeOfLong = Integer.SIZE; // 64 (bits)
int bytesOfLong = Integer.BYTES; // 8 (bytes)

Boolean类

// boolean只有两个值true/false,其包装类型只需要引用Boolean提供的静态字段:
Boolean t = Boolean.TRUE;
Boolean f = Boolean.FALSE;

无符号整型

Java中没有提供无符号整型(Unsigned)的基本数据类型。但是通过包装类型中给出的方法进行转化,以 Byte 为例。

byte x = -1;
byte y = 127;
Byte.toUnsignedInt(x) // 255
Byte.toUnsignedInt(y) // 127

注意 -1 转换之后变成了255,原因是以补码保存整数。

JavaBean

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

// 读方法:
public Type getXyz()
// 写方法:
public void setXyz(Type value)

那么这种class被称为JavaBean

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

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

  • 对应的读方法是String getName()只有getter的属性称为只读属性(read-only)
  • 对应的写方法是setName(String)

属性只需要定义gettersetter方法,不一定需要对应的字段,也可以是通过已有字段计算得到。

可以看出,gettersetter也是一种数据封装的方法。

作用

主要用来传递数据,即把一组数据组合成一个JavaBean便于传输。此外,JavaBean可以方便地被IDE工具分析,生成读写属性的代码,主要用在图形界面的可视化设计中。

其他

通过IDE,可以快速生成gettersetter

// 要枚举一个JavaBean的所有属性,可以直接使用Java核心库提供的Introspector
import java.beans.*;
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;
    }
}

枚举类

枚举类型的定义与C语言中相似,可以使用enum来的定义。但是在其他方面有一些不同。

比较的时候要使用equals()方法,原因同前。但是也可以使用==, 因为enum类型的每个常量在JVM中只有一个唯一实例,所以可以直接使用==比较。编译后的enum类与普通的class没有区别,每个常量是全局唯一,即static final修饰。比较之下有以下特点:

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

常用方法

在已定义如下的枚举类的背景下:

enum Weekday {
    SUN, MON, TUE, WED, THU, FRI, SAT;
}
常用方法方法解释
name()返回常量名,该方法不可覆写
ordinal()返回定义的常量的顺序,从0开始计数
toString()返回和name一样的字符串,但是这个方法可以覆写

注意常量的 ordinal 与常量的定义顺序密切相关,容易出错,可以定义private的构造方法,在内部使用。

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()的目的是在输出时更有可读性

使用场景

switch语句搭配使用,同C语言。记得加上default语句,可以在漏写某个枚举常量的时候自动报错,及时发现错误。

记录类

Java14的新特性,一行写出不变类的定义。这里写一点例子吧,要是以后需要再去细看。

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

上文相当于写出了

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

可以加上检查逻辑,检查构造方法传入的参数是否正确。

public record Point(int x, int y) {
    public Point {
        if (x < 0 || y < 0) {
            throw new IllegalArgumentException();
        }
    }
}

此外,在record方法中,还可以新定义静态方法。

BigInteger

Java牛逼,内置高精度!废话不说了,直接上例子吧

import java.math.BigInteger;

BigInteger bi = new BigInteger("1234567890");
System.out.println(bi.pow(5)); // 2867971860299718107233761438093672048294900000

对大整数类做运算,只能使用实例方法了,支持链式操作,支持加减乘除平方。

将大整数类型转换为基本类型的时候,尽量使用exact的方法,比如intValueExact(),如果内容已经过了基本类型的范围,整数会返回ArithmeticException,而浮点数会返回Infinity之类的东西。下面列举的这些,可以强行转换,但是会舍弃超出的高位,有时会有严重问题,谨慎。

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

BigDecimal

高精度浮点数!不,不是,是任意大小,精度完全准确的浮点数!

import java.math.BigDecimal;
import java.math.RoundingMode;

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,表示是个整数,后面有两个0

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

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 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
// 返回的数组包含两个BigDecimal,分别是商和余数,其中商总是整数,余数不会大于除数。我们可以利用这个方法判断两个BigDecimal是否是整数倍数

在比较两个BigDecimal的值是否相等时,要特别注意,使用equals()方法不但要求两个BigDecimal的值相等,还要求它们的scale()相等。必须使用compareTo()方法来比较,它根据两个值的大小分别返回负数、正数和0,分别表示小于、大于和等于。

常用工具类

Math

静态方法

Math.abs(-100); // 100
Math.abs(-7.8); // 7.8

Math.max(100, 99); // 100
Math.min(1.2, 2.3); // 1.2

Math.pow(2, 10); // 2的10次方=1024
Math.sqrt(2); // 1.414...

Math.exp(2); // 7.389...
Math.log(4); // 1.386...
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

数学常量

double pi = Math.PI; // 3.14159...
double e = Math.E; // 2.7182818...
Math.random(); // 0.53907... 每次都不一样 范围是[0,1)

StrictMath

Java标准库还提供了一个StrictMath,它提供了和Math几乎一模一样的方法。这两个类的区别在于,由于浮点数计算存在误差,不同的平台(例如x86和ARM)计算的结果可能不一致(指误差不同),因此,StrictMath保证所有平台计算结果都是完全相同的,而Math会尽量针对平台优化计算速度,所以,绝大多数情况下,使用Math就足够了。

Random

import java.util.Random;

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

Math 中使用的随机数random就是调用的这个库

SecureRandom

上面随机数的加强版,使用RNG(random number generator)算法。

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

SecureRandom的安全性是通过操作系统提供的安全的随机种子来生成随机数。这个种子是通过CPU的热噪声读写磁盘的字节网络流量等各种随机事件产生的“熵”。在密码学中,安全的随机数非常重要。如果使用不安全的伪随机数,所有加密体系都将被攻破。因此,时刻牢记必须使用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));
    }
}