上一篇记录了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 类不同的是,StringBuffer
和 StringBuilder
类的对象能够被多次的修改,并且不产生新的未使用对象。一般在代码中出现,多次使用 + 拼接字符串的时候,编译器会选择在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有内置的,将基本类型包装为引用类型的类
基本类型 | 对应的引用类型 |
---|---|
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 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)
属性只需要定义getter
和setter
方法,不一定需要对应的字段,也可以是通过已有字段计算得到。
可以看出,getter
和setter
也是一种数据封装的方法。
作用
主要用来传递数据,即把一组数据组合成一个JavaBean便于传输。此外,JavaBean可以方便地被IDE工具分析,生成读写属性的代码,主要用在图形界面的可视化设计中。
其他
通过IDE,可以快速生成getter
和setter
。
// 要枚举一个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
之类的东西。下面列举的这些,可以强行转换,但是会舍弃超出的高位,有时会有严重问题,谨慎。
- 转换为
byte
:byteValue()
- 转换为
short
:shortValue()
- 转换为
int
:intValue()
- 转换为
long
:longValue()
- 转换为
float
:floatValue()
- 转换为
double
:doubleValue()
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));
}
}