Java核心类基础知识笔记:字符串与编码、StringBuilder、StringJoiner、包装类型、JavaBean、枚举类、BigInterger、BigDecimal、Math工具类Random工具类SecureRandom工具类
一、字符串与编码
1、在Java中,String
是一个引用类型,它本身也是一个class
。但是,因为String
太常用了,所以Java提供了"..."
这种字符串字面量表示方法。
String s1 = "Hello!";
// 实际上字符串在String内部是通过一个char[]数组表示的,因此,按下面的写法也是可以的
String s2 = new String(new char[] {'H', 'e', 'l', 'l', 'o', '!'});
2、Java字符串的一个重要特点就是字符串不可变。这种不可变性是通过内部的private final char[]
字段,以及没有任何修改char[]
的方法实现的。
3、字符串比较:当我们想要比较两个字符串是否相同时,要特别注意,我们实际上是想比较字符串的内容是否相同。必须使用equals()
方法而不能用==
。
要忽略大小写比较,使用equalsIgnoreCase()
方法
4、从String
的不变性设计可以看出,如果传入的对象有可能改变,我们需要复制而不是直接引用。
5、小结:
Java字符串String
是不可变对象;
字符串操作不改变原字符串内容,而是返回新字符串;
常用的字符串操作:提取子串、查找、替换、大小写转换等;
Java使用Unicode编码表示String
和char
;
转换编码就是将String
和byte[]
转换,需要指定编码;转换为byte[]
时,始终优先考虑UTF-8
编码。
二、StringBuilder 字符串缓冲区
1、Java编译器对String
做了特殊处理,使得我们可以直接用+
拼接字符串。但是在循环中,每次循环都会创建新的字符串对象,然后扔掉旧的字符串。这样,绝大部分字符串都是临时对象,不但浪费内存,还会影响GC效率。
2、为了能高效拼接字符串,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
还可以进行链式操作:sb.append("Mr ").append("Bob").append("!").insert(0, "Hello, ");
3、如果我们查看StringBuilder
的源码,可以发现,进行链式操作的关键是,定义的append()
方法会返回this
,这样,就可以不断调用自身的其他方法。
4、注意:对于普通的字符串 +
操作,并不需要我们将其改写为StringBuilder
,因为Java编译器在编译时就自动把多个连续的 +
操作编码为StringConcatFactory
的操作。
在运行期,StringConcatFactory
会自动把字符串连接操作优化为数组复制或者StringBuilder
操作。
你可能还听说过StringBuffer
,这是Java早期的一个StringBuilder
的线程安全版本,它通过同步来保证多个线程操作StringBuffer
也是安全的,但是同步会带来执行速度的下降。StringBuilder
和StringBuffer
接口完全相同,现在完全没有必要使用StringBuffer
。
三、StringJoiner
用指定分隔符拼接字符串数组时,使用StringJoiner
或者String.join()
更方便;
1、类似用分隔符拼接数组的需求很常见,所以Java标准库还提供了一个StringJoiner
来干这个事。同时可以给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());
}
}
2、String
还提供了一个静态方法join()
,这个方法在内部使用了StringJoiner
来拼接字符串,在不需要指定“开头”和“结尾”的时候,用String.join()
更方便:
四、包装类型
1、Java的数据类型分两种:
-
基本类型:
byte
,short
,int
,long
,boolean
,float
,double
,char
-
引用类型:所有
class
和interface
类型
引用类型可以赋值为null
,表示空,但基本类型不能赋值为null
。
2、如何把一个基本类型视为对象(引用类型)? 比如,想要把int
基本类型变成一个引用类型,我们可以定义一个Integer
类,它只包含一个实例字段int
,这样,Integer
类就可以视为int
的包装类(Wrapper Class)。
实际上,因为包装类型非常有用,Java核心库为每种基本类型都提供了对应的包装类型:
3、直接把int
变为Integer
的赋值写法,称为自动装箱(Auto Boxing),反过来,把Integer
变为int
的赋值写法,称为自动拆箱(Auto Unboxing)。
注意:自动装箱和自动拆箱只发生在编译阶段,目的是为了少写代码。
装箱和拆箱会影响代码的执行效率,因为编译后的class
代码是严格区分基本类型和引用类型的。并且,自动拆箱执行时可能会报NullPointerException
:
4、不变类:所有的包装类型都是不变类。我们查看Integer
的源码可知,它的核心代码如下:
public final class Integer {
private final int value;
}
因此,一旦创建了Integer
对象,该对象就是不变的。
5、对两个Integer
实例进行比较要特别注意:绝对不能用==
比较,因为Integer
是引用类型,必须使用equals()
比较。
代码示例解析:==
比较,较小的两个相同的Integer
返回true
,较大的两个相同的Integer
返回false
,这是因为Integer
是不变类,编译器把Integer x = 127;
自动变为Integer x = Integer.valueOf(127);
,为了节省内存,Integer.valueOf()
对于较小的数,始终返回相同的实例,因此,==
比较“恰好”为true
,但我们绝不能因为Java标准库的Integer
内部有缓存优化就用==
比较,必须用equals()
方法比较两个Integer
。
6、小结:
Java核心库提供的包装类型可以把基本类型包装为class
;
自动装箱和自动拆箱都是在编译期完成的(JDK>=1.5);装箱和拆箱会影响执行效率,且拆箱时可能发生NullPointerException
;
包装类型的比较必须使用equals()
;
整数和浮点数的包装类型都继承自Number
;
包装类型提供了大量实用方法。
五、JavaBean
JavaBean是一种符合命名规范的class
,它通过getter
和setter
来定义属性。
1、在Java中,有很多class
的定义都符合这样的规范:
- 若干
private
实例字段; - 通过
public
方法来读写实例字段。
如果读写方法符合以下这种命名规范:
// 读方法:
public Type getXyz()
// 写方法:
public void setXyz(Type value)
那么这种class
被称为JavaBean
。
注意:boolean
字段比较特殊,它的读方法一般命名为isXyz()
2、JavaBean主要用来传递数据,即把一组数据组合成一个JavaBean便于传输。
3、要枚举一个JavaBean的所有属性,可以直接使用Java核心库提供的Introspector
。
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());
}
}
}
六、枚举类
1、在Java中,我们可以通过static final
来定义常量。但是使用这些常量来表示一组枚举值的时候,有一个严重的问题就是,编译器无法检查每个值的合理性。
2、enum定义枚举类:为了让编译器能自动检查某个值在枚举的集合内,并且,不同用途的枚举需要不同的类型来标记,不能混用,我们可以使用enum
来定义枚举类:
enum Weekday {
SUN, MON, TUE, WED, THU, FRI, SAT;
}
注意到定义枚举类是通过关键字enum
实现的,我们只需依次列出枚举的常量名。
3、使用枚举类的好处:
enum
常量本身带有类型信息,即Weekday.SUN
类型是Weekday
,编译器会自动检查出类型错误。
不可能引用到非枚举的值,因为无法通过编译。
不同类型的枚举不能互相比较或者赋值,因为类型不符。
4、enum类型
通过enum
定义的枚举类,和其他的class
有什么区别?答案是没有任何区别。enum
定义的类型就是class
,只不过它有以下几个特点:
- 定义的
enum
类型总是继承自java.lang.Enum
,且无法被继承; - 只能定义出
enum
的实例,而无法通过new
操作符创建enum
的实例; - 定义的每个实例都是引用类型的唯一实例;
- 可以将
enum
类型用于switch
语句。
5、因为enum
是一个class
,每个枚举的值都是class
实例,因此存在一些实例方法。
6、小结:
Java使用enum
定义枚举类型,它被编译器编译为final class Xxx extends Enum { … }
;
通过name()
获取常量定义的字符串,注意不要使用toString()
;
通过ordinal()
返回常量定义的顺序(无实质意义);
可以为enum
编写构造方法、字段和方法
enum
的构造方法要声明为private
,字段强烈建议声明为final
;
enum
适合用在switch
语句中。
七、BigInterger
1、在Java中,由CPU原生提供的整型最大范围是64位long
型整数。使用long
型整数可以直接通过CPU指令进行计算,速度非常快。
如果我们使用的整数范围超过了long
型怎么办?这个时候,就只能用软件来模拟一个大整数。java.math.BigInteger
就是用来表示任意大小的整数。BigInteger
内部用一个int[]
数组来模拟一个非常大的整数:
2、对BigInteger
做运算的时候,只能使用实例方法,例如,加法运算:add()
3、和long
型整数运算比,BigInteger
不会有范围限制,但缺点是速度比较慢。
4、小结:
BigInteger
用于表示任意大小的整数;
BigInteger
是不变类,并且继承自Number
;
将BigInteger
转换成基本类型时可使用longValueExact()
等方法保证结果准确。
八、BigDecimal
和BigInteger
类似,BigDecimal
可以表示一个任意大小且精度完全准确的浮点数。
1、BigDecimal
用scale()
表示小数位数;通过BigDecimal
的stripTrailingZeros()
方法,可以将一个BigDecimal
格式化为一个相等的,但去掉了末尾0的BigDecimal。
2、在比较两个BigDecimal
的值是否相等时,要特别注意,使用equals()
方法不但要求两个BigDecimal
的值相等,还要求它们的scale()
相等。
3、必须使用compareTo()
方法来比较,它根据两个值的大小分别返回负数、正数和0
,分别表示小于、大于和等于。
注意:总是使用compareTo()比较两个BigDecimal的值,不要使用equals()!
4、如果查看BigDecimal
的源码,可以发现,实际上一个BigDecimal
是通过一个BigInteger
和一个scale
来表示的,即BigInteger
表示一个完整的整数,而scale
表示小数位数。
public class BigDecimal extends Number implements Comparable<BigDecimal> {
private final BigInteger intVal;
private final int scale;
}
5、小结:
BigDecimal
用于表示精确的小数,常用于财务计算;
比较BigDecimal
的值是否相等,必须使用compareTo()
而不能使用equals()
。
九、常用工具类
1、Math:Math
类就是用来进行数学计算的,它提供了大量的静态方法来便于我们实现数学计算
2、Java标准库还提供了一个StrictMath
,它提供了和Math
几乎一模一样的方法。
这两个类的区别在于,由于浮点数计算存在误差,不同的平台(例如x86和ARM)计算的结果可能不一致(指误差不同),因此,StrictMath
保证所有平台计算结果都是完全相同的,而Math
会尽量针对平台优化计算速度,所以,绝大多数情况下,使用Math
就足够了。
3、Random:Random
用来创建伪随机数。所谓伪随机数,是指只要给定一个初始的种子,产生的随机数序列是完全一样的。
有童鞋问,每次运行程序,生成的随机数都是不同的,没看出伪随机数的特性来。
这是因为我们创建Random
实例时,如果不给定种子,就使用系统当前时间戳作为种子,因此每次运行时,种子不同,得到的伪随机数序列就不同。
如果我们在创建Random
实例时指定一个种子,就会得到完全确定的随机数序列
4、SecureRandom:有伪随机数,就有真随机数。实际上真正的真随机数只能通过量子力学原理来获取,而我们想要的是一个不可预测的安全的随机数,SecureRandom
就是用来创建安全的随机数的。
SecureRandom
无法指定种子,它使用RNG(random number generator)算法。JDK的SecureRandom
实际上有多种不同的底层实现,有的使用安全随机种子加上伪随机数算法来产生安全的随机数,有的使用真正的随机数生成器。实际使用的时候,可以优先获取高强度的安全随机数生成器,如果没有提供,再使用普通等级的安全随机数生成器。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律