java 补全之「java 核心类」
面向对象编程-java核心类
1.字符串
字符串比较
当我们想要比较两个字符串是否相同时,要特别注意,我们实际上是想比较字符串的内容是否相同。必须使用equals()方法而不能用==。
我们看下面的例子:
// String
public class Main {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
}
}从表面上看,两个字符串用==和equals()比较都为true,但实际上那只是Java编译器在编译期,会自动把所有相同的字符串当作一个对象放入常量池,自然s1和s2的引用就是相同的。
所以,这种==比较返回true纯属巧合。换一种写法,==比较就会失败:
// String
public class Main {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "HELLO".toLowerCase();
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
}
}结论:两个字符串比较,必须总是使用equals()方法。
要忽略大小写比较,使用equalsIgnoreCase()方法。
字符串编辑:去除首尾空白字符、提取子串、替换子串、分割字符串、拼接字符串
使用trim()方法可以移除字符串首尾空白字符。空白字符包括空格,\t,\r,\n:
" \tHello\r\n ".trim(); // "Hello"注意:trim()并没有改变字符串的内容,而是返回了一个新字符串。
另一个strip()方法也可以移除字符串首尾空白字符。它和trim()不同的是,类似中文的空格字符\u3000也会被移除:
"\u3000Hello\u3000".strip(); // "Hello"
" Hello ".stripLeading(); // "Hello "
" Hello ".stripTrailing(); // " Hello"String还提供了isEmpty()和isBlank()来判断字符串是否为空和空白字符串:
"".isEmpty(); // true,因为字符串长度为0
" ".isEmpty(); // false,因为字符串长度不为0
" \n".isBlank(); // true,因为只包含空白字符
" Hello ".isBlank(); // false,因为包含非空白字符String类还提供了多种方法来搜索子串、提取子串。常用的方法有:
// 是否包含子串:
"Hello".contains("ll"); // true注意到contains()方法的参数是CharSequence而不是String,因为CharSequence是String实现的一个接口。
搜索子串的更多的例子:
"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"要在字符串中替换子串,有两种方法。一种是根据字符或字符串替换:
String s = "hello";
s.replace('l', 'w'); // "hewwo",所有字符'l'被替换为'w'
s.replace("ll", "~~"); // "he~~o",所有子串"ll"被替换为"~~"另一种是通过正则表达式替换:
String s = "A,,B;C ,D";
s.replaceAll("[\\,\\;\\s]+", ","); // "A,B,C,D"上面的代码通过正则表达式,把匹配的子串统一替换为","。关于正则表达式的用法我们会在后面详细讲解。
要分割字符串,使用split()方法,并且传入的也是正则表达式:
String s = "A,B,C,D";
String[] ss = s.split("\\,"); // {"A", "B", "C", "D"}拼接字符串使用静态方法join(),它用指定的字符串连接字符串数组:
String[] arr = {"A", "B", "C"};
S格式化字符串
字符串提供了formatted()方法和format()静态方法,可以传入其他参数,替换占位符,然后生成新的字符串:
// String
public class Main {
public static void main(String[] args) {
String s = "Hi %s, your score is %d!";
System.out.println(s.formatted("Alice", 80));
System.out.println(String.format("Hi %s, your score is %.2f!", "Bob", 59.5));
}
}有几个占位符,后面就传入几个参数。参数类型要和占位符一致。我们经常用这个方法来格式化信息。常用的占位符有:
%s:显示字符串;%d:显示整数;%x:显示十六进制整数;%f:显示浮点数。
占位符还可以带格式,例如%.2f表示显示两位小数。如果你不确定用啥占位符,那就始终用%s,因为%s可以显示任何数据类型。要查看完整的格式化语法,请参考JDK文档。
任意类型转字符串-String.valueOf()
要把任意基本类型或引用类型转换为字符串,可以使用静态方法valueOf()。这是一个重载方法,编译器会根据参数自动选择合适的方法:
String.valueOf(123); // "123"
String.valueOf(45.67); // "45.67"
String.valueOf(true); // "true"
String.valueOf(new Object()); // 类似java.lang.Object@636be97c要把字符串转换为其他类型,就需要根据情况。例如,把字符串转换为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[]-toCharArray()
String和char[]类型可以互相转换,方法是:
toCharArray()。
char[] cs = "Hello".toCharArray(); // String -> char[]
String s = new String(cs); // char[] -> String编码
因为英文字符的Unicode编码高字节总是00,包含大量英文的文本会浪费空间,所以,出现了UTF-8编码,它是一种变长编码,用来把固定长度的Unicode编码变成1~4字节的变长编码。通过UTF-8编码,英文字符'A'的UTF-8编码变为0x41,正好和ASCII码一致,而中文'中'的UTF-8编码为3字节0xe4b8ad。
UTF-8编码的另一个好处是容错能力强。如果传输过程中某些字符出错,不会影响后续字符,因为UTF-8编码依靠高字节位来确定一个字符究竟是几个字节,它经常用来作为传输编码。
在Java中,char类型实际上就是两个字节的Unicode编码。如果我们要手动把字符串转换成其他编码,可以这样做:
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,可以这样做:
byte[] b = ...
String s1 = new String(b, "GBK"); // 按GBK转换
String s2 = new String(b, StandardCharsets.UTF_8); // 按UTF-8转换始终牢记:Java的String和char在内存中总是以Unicode编码表示。
JDK对字符串存储的优化
对于不同版本的JDK,String类在内存中有不同的优化方式。具体来说,早期JDK版本的String总是以char[]存储,它的定义如下:
public final class String {
private final char[] value;
private final int offset;
private final int count;
}而较新的JDK版本的String则以byte[]存储:如果String仅包含ASCII字符,则每个byte存储一个字符,否则,每两个byte存储一个字符,这样做的目的是为了节省内存,因为大量的长度较短的String通常仅包含ASCII字符:
public final class String {
private final byte[] value;
private final byte coder; // 0 = LATIN1, 1 = UTF16对于使用者来说,String内部的优化不影响任何已有代码,因为它的public方法签名是不变的。
2.StringBuilder
考察下面的循环代码:
String s = "";
for (int i = 0; i < 1000; i++) {
s = s + "," + i;
}虽然可以直接拼接字符串,但是,在循环中,每次循环都会创建新的字符串对象,然后扔掉旧的字符串。这样,绝大部分字符串都是临时对象,不但浪费内存,还会影响GC效率。
为了能高效拼接字符串,Java标准库提供了StringBuilder,它是一个可变对象,可以预分配缓冲区,这样,往StringBuilder中新增字符时,不会创建新的临时对象:
StringBuilder sb = new StringBuilder(1024);
for (int i = 0; i < 1000; i++) {
sb.append(',');
sb.append(i);
}
String s = sb.toString();StringBuilder还可以进行链式操作:
// 链式操作
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());
}
}如果我们查看StringBuilder的源码,可以发现,进行链式操作的关键是,定义的append()方法会返回this,这样,就可以不断调用自身的其他方法。
仿照StringBuilder,我们也可以设计支持链式操作的类。
StringBuffer,这是Java早期的一个StringBuilder的线程安全版本,它通过同步来保证多个线程操作StringBuffer也是安全的,但是同步会带来执行速度的下降。
StringBuilder和StringBuffer接口完全相同,现在完全没有必要使用StringBuffer。
3.StringJoiner
Java标准库提供了一个StringJoiner来解决类似用分隔符拼接数组的需求。
String[] names = {"Bob", "Alice", "Grace"};
var sj = new StringJoiner(", ");
for (String name : names) {
sj.add(name);
}
System.out.println(sj.toString());先new一个Stringjoiner,然后遍历数组并add(),最后打印时需要toString()。
可以先给StringJoiner指定“开头”和“结尾”:
var sj = new StringJoiner(", ", "Hello ", "!");
for (String name : names) {
sj.add(name);
}String还提供了一个静态方法join(),这个方法在内部使用了StringJoiner来拼接字符串,在不需要指定“开头”和“结尾”的时候,用String.join()更方便:
String[] names = {"Bob", "Alice", "Grace"};
var s = String.join(", ", names);这个不需要toString()。
4.包装类型(自动装箱拆箱)
int和Integer可以互相转换:
int i = 100;
Integer n = Integer.valueOf(i);
int x = n.intValue();Java编译器可以帮助我们自动在int和Integer之间转型:
Integer n = 100; // 编译器自动使用Integer.valueOf(int)
int x = n; // 编译器自动使用Integer.intValue()这种直接把int变为Integer的赋值写法,称为自动装箱(Auto Boxing),反过来,把Integer变为int的赋值写法,称为自动拆箱(Auto Unboxing)。
自动装箱和自动拆箱只发生在编译阶段。装箱和拆箱会影响执行效率,且拆箱时可能发生NullPointerException;
所有的包装类型都是不变类。我们查看Integer的源码可知,它的核心代码如下:
public final class Integer {
private final int value;
}因此,一旦创建了Integer对象,该对象就是不变的。
对两个Integer实例进行比较要特别注意:绝对不能用==比较,因为Integer是引用类型,必须使用equals()比较。
- 方法1:
Integer n = new Integer(100); - 方法2:
Integer n = Integer.valueOf(100);
我们把能创建“新”对象的静态方法称为静态工厂方法。Integer.valueOf()就是静态工厂方法,它尽可能地返回缓存的实例以节省内存。
Integer类本身还提供了大量方法,例如,最常用的静态方法parseInt()可以把字符串解析成一个整数:
int x1 = Integer.parseInt("100"); // 100
int x2 = Integer.parseInt("100", 16); // 256,因为按16进制解析Integer还可以把整数格式化为指定进制的字符串:
// Integer:
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,在计算机内存中,只用二进制表示,不存在十进制或十六进制的表示方法。int n = 100在内存中总是以4字节的二进制表示:
┌────────┬────────┬────────┬────────┐
│00000000│00000000│00000000│01100100│
└────────┴────────┴────────┴────────┘我们经常使用的System.out.println(n);是依靠核心库自动把整数格式化为10进制输出并显示在屏幕上,使用Integer.toHexString(n)则通过核心库自动把整数格式化为16进制。
这里我们注意到程序设计的一个重要原则:数据的存储和显示要分离。
所有的整数和浮点数的包装类型都继承自Number。
5.enum枚举类
为了让编译器能自动检查某个值在枚举的集合内,并且,不同用途的枚举需要不同的类型来标记,不能混用,我们可以使用enum来定义枚举类:
// enum
public class Main {
public static void main(String[] args) {
Weekday day = Weekday.SUN;
if (day == Weekday.SAT || day == Weekday.SUN) {
System.out.println("Work at home!");
} else {
System.out.println("Work at office!");
}
}
}
enum Weekday {
SUN, MON, TUE, WED, THU, FRI, SAT;
}引用类型比较,要始终使用equals()方法,但enum类型可以例外。
这是因为enum类型的每个常量在JVM中只有一个唯一实例,所以可以直接用==比较。
enum定义的类型就是class,只不过它有以下几个特点:
- 定义的
enum类型总是继承自java.lang.Enum,且无法被继承; - 只能定义出
enum的实例,而无法通过new操作符创建enum的实例; - 定义的每个实例都是引用类型的唯一实例;
- 可以将
enum类型用于switch语句。
我们定义的Color枚举类:
public enum Color {
RED, GREEN, BLUE;
}编译器编译出的class大概就像这样:
public final class Color extends Enum { // 继承自Enum,标记为final class
// **每个实例均为全局唯一**:
public static final Color RED = new Color();
public static final Color GREEN = new Color();
public static final Color BLUE = new Color();
// private构造方法,确保外部无法调用new操作符:
private Color() {}
}name()
返回常量名,例如:
String s = Weekday.SUN.name(); // "SUN"ordinal()
返回定义的常量的顺序,从0开始计数,例如:
int n = Weekday.MON.ordinal(); // 1改变枚举常量定义的顺序就会导致ordinal()返回值发生变化。
我们可以定义private的构造方法,并且,给每个枚举常量添加字段:
// enum
public class Main {
public static void main(String[] args) {
Weekday day = Weekday.SUN;
if (day.dayValue == 6 || day.dayValue == 0) {
System.out.println("Work at home!");
} else {
System.out.println("Work at office!");
}
}
}
enum Weekday {
MON(1), TUE(2), WED(3), THU(4), FRI(5), SAT(6), SUN(0);
public final int dayValue;
private Weekday(int dayValue) {
this.dayValue = dayValue;
}
}这样就无需担心顺序的变化,新增枚举常量时,也需要指定一个int值。
枚举类的字段也可以是非final类型,即可以在运行期修改,但是不推荐这样做。
默认情况下,对枚举常量调用toString()会返回和name()一样的字符串。但是,toString()可以被覆写,而name()则不行。我们可以给Weekday添加toString()方法:
枚举类很适合用于switch语句中:
public class Main {
public static void main(String[] args) {
Weekday day = Weekday.SUN;
switch(day) {
case MON: // 已经知道day是Weekday类型了,所以不用写成Weekday.MON了
case TUE:
case WED:
case THU:
case FRI:
System.out.println("Today is " + day + ". Work at office!");
break;
case SAT:
case SUN:
System.out.println("Today is " + day + ". Work at home!");
break;
default:
throw new RuntimeException("cannot process " + day);
}
}
}
enum Weekday {
MON, TUE, WED, THU, FRI, SAT, SUN;
}6.记录类(不变类)
和enum类似,我们自己不能直接从Record派生,只能通过record关键字由编译器实现继承。
record Point(int x, int y) {}把上述定义改写为class,相当于以下代码:
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() {
...
}
}编译器默认按照record声明的变量顺序自动创建一个构造方法,并在方法内给字段赋值。那么问题来了,如果我们要检查参数,应该怎么办?
假设Point类的x、y不允许负数,我们就得给Point的构造方法加上检查逻辑:
public record Point(int x, int y) {
public Point {
if (x < 0 || y < 0) {
throw new IllegalArgumentException();
}
}
}注意到方法public Point {...}被称为Compact Constructor,它的目的是让我们编写检查逻辑,编译器最终生成的构造方法如下:
public final class Point extends Record {
public Point(int x, int y) {
// 这是我们编写的Compact Constructor:
if (x < 0 || y < 0) {
throw new IllegalArgumentException();
}
// 这是编译器继续生成的赋值代码:
this.x = x;
this.y = y;
}
...
}作为record的Point仍然可以添加静态方法。一种常用的静态方法是of()方法,用来创建Point:
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);
}
}这样我们可以写出更简洁的代码:
var z = Point.of();
var p = Point.of(123, 456);这是 Java 14 提供的新特性。
7.大数运算-BigInteger、BigDecimal
java.math.BigInteger就是用来表示任意大小的整数。BigInteger内部用一个int[]数组来模拟一个非常大的整数:
BigInteger bi = new BigInteger("1234567890");对BigInteger做运算的时候,只能使用实例方法:
BigInteger sum = i1.add(i2);BigInteger和Integer、Long一样,也是不可变类,并且也继承自Number类。
和BigInteger类似,BigDecimal可以表示一个任意大小且精度完全准确的浮点数。
BigDecimal bd = new BigDecimal("123.4567");BigDecimal用scale()表示小数位数,例如:
BigDecimal d1 = new BigDecimal("123.45");
System.out.println(d1.scale()); // 2,两位小数通过BigDecimal的stripTrailingZeros()方法,可以将一个BigDecimal格式化为一个相等的,但去掉了末尾0的BigDecimal:
BigDecimal d1 = new BigDecimal("123.4500");
BigDecimal d2 = d1.stripTrailingZeros();
System.out.println(d1.scale()); // 4
System.out.println(d2.scale()); // 2,因为去掉了00BigDecimal d3 = new BigDecimal("1234500");
BigDecimal d4 = d3.stripTrailingZeros();
System.out.println(d3.scale()); // 0
System.out.println(d4.scale()); // -2d3.scale()- 原始值是 1234500,没有小数点
scale()= 0(小数点后的位数)
d4.scale()- 去掉了末尾的两个零后,数字变成了
12345 × 10^2 - 这相当于
12345小数点向左移动了 2 位 - 所以
scale()= -2(负数表示小数点向左移动)
- 去掉了末尾的两个零后,数字变成了
可以对一个BigDecimal设置它的scale,如果精度比原始值低,那么按照指定的方法进行四舍五入或者直接截断:
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做除法的同时求余数:
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的值是否相等时,要特别注意,使用equals()方法不但要求两个BigDecimal的值相等,还要求它们的scale()相等:
必须使用compareTo()方法来比较,它根据两个值的大小分别返回负数、正数和0,分别表示小于、大于和等于。
如果查看BigDecimal的源码,可以发现,实际上一个BigDecimal是通过一个BigInteger和一个scale来表示的,即BigInteger表示一个完整的整数,而scale表示小数位数:
public class BigDecimal extends Number implements Comparable<BigDecimal> {
private final BigInteger intVal;
private final int scale;
}BigDecimal也是从Number继承的,也是不可变对象。
8.常用工具类
- Math:数学计算
- HexFormat:格式化十六进制数
- Random:生成伪随机数
- SecureRandom:生成安全的随机数
笔记所有内容摘取自廖雪峰java教程 https://liaoxuefeng.com/books/java/introduction/index.html
本文系作者 @xiin 原创发布在To Future$站点。未经许可,禁止转载。
暂无评论数据