9 - Java常见类
1. 字符串和编码
String
在Java中,String是一个引用类型,本身也是一个class。实际上字符串在String内部是通过一个char[]数组表示的。Java字符串的一个重要特点就是字符串不可变,其不可变特性是通过内部的private final char[]字段,以及没有任何修改char[]的方法实现。
字符串比较
当比较两个字符串是否相同时,实际上是比较字符串的内容是否相同,必须使用equals()方法而不能使用“==”。要忽略大小写比较,可使用equalsIgnoreCase()方法。
搜索子串方法:
"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。该方法没有改变字符串的内容,而是返回了一个新字符串。
使用strip()方法也可以移除字符串首尾空白字符,与trim()不同的是,它也会移除类似中文的空格字符\u3000。
String还提供了isEmpty()和isBlank()来判断字符串是否为空和空白字符串。
替换字串
方法一:根据字符或字符串替换,s.replace("sub1", "sub2"),将字符串里的子串"sub1"替换为子串"sub2";
方法二:通过正则表达式替换,s.replaceAll("re", "sub"),将字符串里符合re的子串替换为sub。
分割字符串
使用split()方法,传入的也是正则表达式,s.split("re")。
拼接字符串
使用静态方法join(),用指定的字符串连接字符串数组,String.join("str", arr),使用str连接数组arr。
格式化数组
使用方法formatted()和静态方法format(),可以传入其他参数,替换占位符,然后生成新的字符串。
占位符:%s(显示字符串),%d(显示整数),%x(显示十六进制整数),%f(显示浮点数)。
类型转换
要把任意基本类型或引用类型转换为字符串,可以使用静态方法valueOf()。这是一个重载方法,编译器会根据参数自动选择合适的方法。
要把字符串转换为其他类型,就需要根据情况。
转换为char[]
String和char[]可以相互转换,方法如下:
char[] cs = "Hello".toCharArray(); // String -> char[] String s = new String(cs); // char[] -> String
如果修改了char[]数组,String并不会改变,因为new String(char[])创建新的String实例时,它并不会直接引用传入的char[]数组,而是会复制一份,所以修改外部的char[]数组不会影响String实例内部的char[]数组,因为这是两个不同的数组。
从String的不变性设计可以看出,如果传入的对象有可能改变,需要复制而不是直接引用。函数引用组数时,如果没有改变内部的需求,最好使用克隆方法(在构造函数中使用this.cs = cs.clone(),其中cs是一个char[]数组),否则会造成代码逻辑的混乱。
字符编码
ASCII码:美国国家标准学会(ANSI)制定的一套英文字符、数字和常用符号的编码,占用一个字节,编码范围从0到127,最高位始终为0;
GB2312标准:使用两个字节表示一个汉字,其中第一个字节的最高位始终为1,以便和ASCII编码区分开。类似的,日文有shift_JIS编码,韩文有EUC_KR编码,由于编码标准不统一,同时使用就会产生冲突。
Unicode编码:把世界上主要语言都纳入同一个编码,这样中文、日文、韩文和其他语言就不会冲突。需要两个或者更多字节表示。
UTF-8编码:因为英文字符的Unicode编码高字节总是00,包含大量英文的文本会浪费空间,所以出现了UTF-8。它是一种变长编码,用来把固定长度的Unicode编码变成1~4字节的变长编码。其另一个好处是容错能力强,如果传输过程中某些字符出错,不会影响后续字符,因为它依靠高字节位来确定一个字符究竟是几个字节,经常用来作为传输编码。
在Java中,char类型实际上就是两个字节的Unicode编码。可以使用s.getBytes("编码方式")来手动把字符串转换成其他编码,但是转换后就不是char类型而是byte类型表示的数组了。
注意:对于不同版本的JDK,String类在内存中有不同的优化方式。具体来说,早期JDK版本的String总是以char[]存储;而较新的JDK版本的String则以byte[]存储:如果String仅包含ASCII字符,则每个byte存储一个字符,否则,每两个byte存储一个字符,这样做的目的是为了节省内存,因为大量的长度较短的String通常仅包含ASCII字符。
2. StringBuilder
Java编译器对String做了特殊处理,使得我们可以直接用"+"拼接字符串。虽然可以直接拼接字符串,但是,在循环中,每次循环都会创建新的字符串对象,然后扔掉旧的字符串。这样,绝大部分字符串都是临时对象,不但浪费内存,还会影响GC效率。为了能高效拼接字符串,Java标准库提供了StringBuilder,它是一个可变对象,可以预分配缓冲区,这样,往StringBuilder中新增字符时(s.append("str")),不会创建新的临时对象。StringBuilder可以进行链式操作。
3. StringJoiner
类似用分隔符拼接数组的需求,Java标准库提供了一个StringJoiner("s1", "s2", "s3")来实现,其中s1是分隔符,s2是开头字符串,s3是结尾字符串。
4. 包装类型
Java的数据类型分为两种:
-
基本类型:byte, short, int, long, boolean, float, double, char;
-
引用类型:所有class和interface类型。
引用类型可以赋值为null,但基本类型不能。要把基本类型视为对象(引用类型),就需要包装类型。如,想要把int基本类型变成一个引用类型,可以定义一个Integer类,它只包含一个实例字段int,这样,Integer类就可以视为int的包装类(Wrapper Class)。Java核心库为每种基本类型都提供了对应的包装类型。
Auto Boxing
因为int和Integer可以互相转换,所以Java编译器可以帮助我们自动在int和Integer之间转型:
Integer n = 100; // 编译器自动使用Integer.valueOf(int) int x = n; // 编译器自动使用Integer.intValue()
自动装箱(Auto Boxing):直接把int变成Integer的赋值写法;
自动拆箱(Auto Unboxing):与自动装箱相反。
自动装箱和自动拆箱只发生在编译阶段,目的是为了少写代码。它们会影响代码的执行效率,因为编译后的class代码是严格区分基本类型和引用类型的。
不变类
所有包装类型都是不变类。一旦创建了Integer对象,该对象就是不变的。对两个Integer实例进行比较绝对不能使用“==”,因为它是引用类型,而应该使用equals()方法。
创建Integer实例的两种方法:
Integer n1 = new Integer(100); Integer n2 = Integer.valueOf(100);
把能创建“新”对象的静态方法称为静态工厂方法,方法二就是静态工厂方法,它尽可能地返回缓存地实例以节省空间。创建对象时,优先选用静态工厂方法而不是new操作符。
进制转换
Integer类本身还提供了大量的方法,如最常用的静态方法parseInt()可以把字符串解析成一个整数,且可以把整数格式化为指定进制的字符串。
int x1 = Integer.parseInt("100"); // 100 int x2 = Integer.parseInt("100", 16); // 256 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进制
Java的包装类型还定义了一些有用的静态变量:
-
Boolean.TRUE/Boolean.FALSE
-
Integer.MAX_VALUE/Integer.MIN_VALUE:2147483647/-2147483648
-
Long.SIZE/Long.BYTES:64(bits)/8(bytes)
所有的整数和浮点数的包装类型都继承自Number,因此可以非常方便地直接通过包装类型获取各种基本类型:
Number num = new Integer(999); // 向上转型为Number byte b = num.byteValue(); // 获取各种基本类型 int i = num.intValue(); long l = num.longValue(); float f = num.floatValue(); double d = num.doubleValue();
处理无符号整型
Java中没有无符号整型地基本数据类型。无符号整型和有符号整型地转换在Java中需要借助包装类型地静态方法完成。如,byte是有符号整型,范围为-128~127,将其看作无符号整型,其范围就是0~255,把一个负的byte按无符号整型转换为int:
byte b1 = -1; byte b2 = 127; Byte.toUnsignedInt(b1); // 255 Byte.toUnsignedInt(b2); // 127
类似的,可以将short按无符号整型转换为int,将int按无符号整型转换为long。
5. JavaBean
在Java中,有很多class的定义都符合这样的规范:若干private实例字段+通过public方法来读写实例字段。读写符合这种规范的class被称为JavaBean。
public class User{ private String name; private int id; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getId() { return id; } public void setId(int id) { this.id = id; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }
通常把一组对应的读方法getter和写方法setter称为属性,只有getter的属性称为只读属性,只有setter的属性称为只写属性。属性只需要定义getter和setter方法,不一定需要对应的字段。getter和setter也是一种数据封装的方法。
JavaBean的作用
JavaBean主要用来传递数据,即把一组数据组合成一个JavaBean便于传输。此外,JavaBean可以方便地被IDE工具分析,生成读写属性地代码,主要用在图形界面的可视化设计中。
枚举JavaBean属性
要枚举一个JavaBean的所有属性,可以直接使用Java核心库提供的Introspector:
package com.wang.baseclasses; import java.beans.*; public class Test01 { public static void main(String[] args) throws IntrospectionException { BeanInfo info = Introspector.getBeanInfo(User.class); for (PropertyDescriptor pd : info.getPropertyDescriptors()) { System.out.println(pd.getName()); System.out.println(" " + pd.getReadMethod()); System.out.println(" " + pd.getWriteMethod()); } } } class User{ private String name; private int id; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getId() { return id; } public void setId(int id) { this.id = id; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } } // 执行结果: age public int com.wang.baseclasses.User.getAge() public void com.wang.baseclasses.User.setAge(int) class public final native java.lang.Class java.lang.Object.getClass() null id public int com.wang.baseclasses.User.getId() public void com.wang.baseclasses.User.setId(int) name public java.lang.String com.wang.baseclasses.User.getName() public void com.wang.baseclasses.User.setName(java.lang.String)
其中,class属性是从Object继承的getCalss()方法带来的。
6. 枚举类
在Java中可以通过static final来定义常量。如使用7个不同的int,定义周一到周日7个常量:
public class Weekday{ public static final int SUN = 0; public static final int MON = 1; public static final int TUE = 2; public static final int WED = 3; public static final int THU = 4; public static final int FRI = 5; public static final int SAT = 6; } // 常量引用方式 int day = 0; if (day == Weekday.SAT || day == wekday.SUN){ // TODO }
使用这些常量来表示一组枚举值的时候,编译器无法检查每个值的合理性。
enum
为了让编译器能自动检查某个值是否在枚举的集合内,并且,不同用途的枚举需要不同的类型来标记,不能混用,可以使用enum来定义枚举类。
enum Weekday{ SUN, MON, TUE, WED, THU, FRI, SAT; } // 枚举类中常量的引用方式 Weekday day = Weekday.SUN; if (day == Weekday.SAT || day == wekday.SUN){ // TODO }
定义枚举类是通过关键字enum实现的,我们只需要依次列出枚举的常量名即可。和int定义的常量相比,使用枚举类的好处如下,使得编译器可以在编译期自动检查出所有可能的潜在错误:
①enum常量本身带有类型信息,即Weekday.SUN类型是Weekday,编译器会自动检查出类型错误;
②不可能引用到非枚举的值,因为无法通过编译;
③不同类型的枚举不能互相比较或者赋值,因为类型不符。
enum的比较
使用enum定义的枚举类是一种引用类型。引用类型的比较要使用equals()方法,如果使用“==”比较,它比较的是两个引用类型的变量是否是同一个对象。但enum类型的比较可以例外,这是因为enum类型的每个常量在JVM中只有一个唯一实例,所以可以直接用“==”比较。
enum类型
通过enum定义的枚举类,和其他class没有区别,只不过它有以下特点:
①定义的enum类型总是继承自java.lang.Enum,且无法被继承;
②只能定义出enum的实例,而无法通过new操作符创建enum的实例;
③定义的每个实例都是引用类型的唯一实例;
④可以将enum类型用于switch语句。
enum是一个class,每个枚举的值都是class实例,因此这些实例有以下方法:
-
name(),返回常量名,toString()也会返回一样的字符串,但是toString()可以被覆写以达到在输出时更有可读性的目的,而name()则不行;
-
ordinal(),返回定义的常量的顺序(无实质意义),从0开始计数。可以定义private的构造方法,给每个枚举常量添加字段,来避免不小心修改了枚举的顺序而编译器无法检查这种逻辑错误造成的问题,但是在新增枚举常量时,也需要指定一个int值。
switch
因为枚举类具有类型信息和有限个枚举常量,所以比int、String类型更适合在switch语句中使用。注意加上default语句,可以在漏写某个枚举变量时自动报错,从而及时发现错误。
7. 记录类
使用String、Integer等类型的时候,这些类型都是不变类,具有以下特点:
①定义class时使用final,无法派生子类;
②每个字段使用final,保证创建实例后无法修改任何字段。
record
不变类代码简单但很繁琐,从Java14开始,引入新的Record类,使用关键字record进行定义。使用record关键字,可以一行写出一个不变类,将其改写成class,除了用final修饰类和每个字段以外,编译器还自动创建了构造方法、和字段名同名的方法以及覆写toString()、equals()和hashCode()方法。
和enum类似,我们不能直接从Record派生,只能通过record关键字由编译器实现继承。
构造方法
编译器默认按照record声明的变量顺序自动创建一个构造方法,并在方法内给字段赋值。如果要检查参数,就得给构造方法加上逻辑检查。
record定义的类仍然可以添加静态方法,一种常用的静态方法是of()方法,用来创建类的实例。
8. BigInteger
在Java中,由CPU原生提供的整型最大范围是64位long型整数。使用long型整数可以直接通过CPU指令进行运算,速度非常快。当使用的整数超过long型,就只能使用软件模拟一个大整数。java.math.BigInteger可以用来表示任意大小的整数。
BigInteger内部用一个int[]数组来模拟一个非常大的整数。对BigInteger做运算时,只能使用实例方法。和long型整数运算比,BigInteger不会有范围限制,但缺点是速度比较慢。
BigInteger和Integer、Long一样,也是不可变类,并且也继承自Number类。Number类中定义了转换为基本类型的方法:byteValue(), shortValue(), intValue(), longValue(), floatValue(), doubleValue()。如果BigInteger表示的范围超过了基本类型的范围,转换时将丢失高位信息,即结果不一定是准确的。如果需要准确地转换成基本类型,可以使用intValueExact(), longValueExact()等方法,在转换时若超出范围,将直接抛出ArithmeticException异常。如果BigInteger的值甚至超过了float的最大范围(3.4×10^38),那么返回的float为Infinity。
9. BigDecimal
和BigInteger类似,BigDecimal可以表示一个任意大小且精度完全准确的浮点数(常用于财务计算)。
BigDecimal使用scale()表示小数位数,如果一个BigDecimal的scale()返回负数,如-2表示这个数是个整数,并且末尾有2个0。可以对一个BigDecimal使用setScale(scale, mode)设置它的scale,如果精度比原始低,那么按照指定的方法进行四舍五入(RoundingMode.HALF_UP)或者直接截断(RoundingMode.DOWN)。对BigDecimal做加、减、乘时,精度不会丢失,但是做除法时,存在无法除尽的情况,这时就必须指定精度以及如何进行截断。
BigDecimal通过stripTrailingZeros()方法将一个BigDecimal格式化为一个相等的,但去掉末尾0的BigDecimal。
对BigDecimal做除法同时求余数使用divideAndRemainder()方法,返回的数组包含两个BigDecimal,分别是商和余数,其中商总是整数,余数不会大于除数(利用这个方法可以判断两个BigDecimal是否是整数倍数)。
比较BigDecimal
使用equals()方法,不但要求两个BigDecimal的值相等,还要求它们的scale()相等。
使用compareTo()方法,根据两个值的大小分别返回负数、正数和0,分别表示小于、大于和等于。
注意:总是使用compareTo()方法比较两个BigDecimal的值,不要使用equals()!
BigDecimal也是从Number继承的,也是不可变对象。
10. 常用工具类
Math
数学计算,提供了大量的静态方法来便于我们实现数学计算。
-
求绝对值:Math.abs(x)
-
取最大或最小值:Math.max(x, y), Math.min(x, y)
-
计算x^y次方:Math.pow(x, y)
-
计算根号x:Math.sqrt(x)
-
计算e^x次方:Math.exp(x)
-
计算以e为底的对数:Math.log(x)
-
计算以10为底的对数:Math.log10(x)
-
三角函数:Math.sin(x), Math.cos(x), Math.tan(x), Math.asin(x), Math.acos(x)
-
数学常量:Math.PI, Math.E
-
生成一个随机数x,x的范围是0<=x<1:Math.random()
Java标准库还提供了一个StrictMath,提供了和Math几乎一模一样的方法。由于浮点数计算存在误差,不同平台计算的结果可能不一致,因此,StrictMath保证所有平台计算结果都是完全相同的,而Math会尽量针对平台优化计算速度。
Random
生成伪随机数,只要给定一个初始的种子,产生的随机数序列是完全一样的。
要生成一个随机数,可以使用nextInt(), nextLong(), nextFloat(), nextDouble()。
如果创建Random实例时不给定种子,就使用系统当前时间戳作为种子,因此每次运行时,种子不同,得到的伪随机数序列不同。如果创建Random实例时指定一个种子,就会得到完全确定的随机数序列。
SecureRandom
生成安全的随机数
实际上真正的真随机数只能通过量子力学原理来获取,而我们想要的是一个不可预测的安全的随机数,secureRandom就是用来创建安全的随机数的。
secureRandom无法指定种子,使用RNG(random number generator)算法。JDK对其有多种不同的底层实现,有的使用安全随机种子加上伪随机算法来产生安全的随机数,有的使用真正的随机数生成器。