Wrapper class 包装类
- 什么是包装类?
- 包装类是 Java 提供的一种机制,用于将基本数据类型封装为对象类型。
- 为什么要使用包装类?
- 基本数据类型 int 等不是对象,无法通过向上转型获取到 Object 提供的方法
- 基本数据类型不支持与对象有关的特性,如:多态、反射、泛型
- 基本数据类型无法直接存入集合中,因为集合元素必须为对象
- 包装类
对象可以为 null 值
,这在集合/数据库/反序列化中可能很有用- DTO 中尽量使用包装类。由于基本类型不能表示 null 值,如果从 JSON 等数据源反序列化时遇到 null,尝试将其直接赋值给基本类型变量会导致
空指针异常
- DTO 中尽量使用包装类。由于基本类型不能表示 null 值,如果从 JSON 等数据源反序列化时遇到 null,尝试将其直接赋值给基本类型变量会导致
- 包装类提供了许多
有用的方法
大部分
包装类提供了缓存机制
,可以减少创建销毁对象的开销- 包装类提供了
自动装箱/拆箱机制
,使得基础数据类型与它们对应的包装类之间的转换更加简洁
- 包装类重写了 equals 方法,比较的是包装类的内容而不是地址
1. 包装类的比较
- 包装类重写了 equals 方法,比较的是包装类的内容而不是地址
- 但是,如果用 “==” 比较两个包装类,比较的是地址
- 作为对比,String 类型由于字符串常量池的存在,equals 和 “==” 比出来的效果实际上是一样。
2. 缓存机制
包装类通过缓存一些常见的值来优化性能,减少对象的频繁创建和内存开销。
除 float、double 外的 6 个包装类,都提供了对象的缓存。
实现方式是在类初始化时提前创建好会频繁使用的包装类对象,当需要使用某个包装类的对象时,如果该对象包装的值在缓存的范围内,就返回缓存的对象,否则就创建新的对象并返回。
3. 装/拆箱
装箱
: 基本类型 ——> 包装类型(或者叫对象类型,引用类型)拆箱
: 包装类型 ——> 基本类型
以下以 Integer 为例
3.1 手动装/拆箱
JDK5 之前,拆装箱均是手动完成的。
- 手动装箱:JDK 9 以前,可以使用包装类的构造器/包装类调用静态 valueOf 方法完成;JDK 9 以后,只能通过包装类调用静态 valueOf 方法完成。
public static void main(String[] args) {
//手动装箱(基本类型包装/引用类型)
// 第一种手动装箱,已于 JDK 9 之后废弃
// Integer integer_0 = new Integer(5);
// 第二种手动装箱,推荐使用
Integer integer_1 = Integer.valueOf(5);
}
- 手动拆箱:包装类调用静态 intValue 方法完成。
3.2 自动装/拆箱
JDK5 开始,提供了自动拆装箱的机制。(不需要手动调用构造器或者方法了)
自动拆箱
:实际上底层仍然调用了 valueOf 方法自动装箱
:实际上底层仍然调用了 intValue 方法- 自动装/拆箱都是在
编译阶段
完成的。
Integer integerValue = 10; // 自动装箱
int intValue = integerValue; // 自动拆箱
//集合类(如ArrayList)只能存储包装类对象,自动装箱可以免去手动类型转换
List<Integer> integerList = new ArrayList<>();
integerList.add(1); // 自动装箱
// 方法调用中的自动装箱:方法参数类型为包装类时,传入基本类型会自动装箱,方法内部使用时可以自动拆箱。
public static int sum(Integer a, Integer b) {
return a + b; // 自动拆箱
}
4. Integer 类型的转换
4.3.1 装箱 valueOf
// Integer 类中,valueOf方法的源码
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
IntegerCache
:
IntegerCache 是一个静态内部类,用于缓存一定范围内的 Integer 对象。该缓存的范围和缓存数组的初始化是在 IntegerCache 类中完成的。
缓存范围检查
:
在 valueOf 方法中,首先检查传入的 int 值是否在缓存范围内( -128~127 )。如果在范围内,则直接返回缓存数组中的相应 Integer 对象。
创建新对象
:
如果传入的 int 值不在缓存范围内,则使用 new Integer(i) 创建一个新的 Integer 对象。
5. String 类型的转换
String 类型可同时转为包装类、基本数据类型,下图只是 int 类型的转换,实质上八种基本类型都是类似的。
以下详细叙述
5.1 String ——> 7 种基本类型
- 通过各个包装类自带的静态方法 parseXxx,在满足被转类型可以接收的情况下,可以把 String 类型转成 7 个基本数据类型(排除 char)
- 显然,parseXxx 方法的返回值是基本数据类型。
byte temp_byte =Byte.parseByte("11");
short temp_short = Short.parseShort("141");
int temp_int =Integer.parseInt("430");
long temp_long =Long.parseLong("11211");
float temp_float =Float.parseFloat("66.66F");
double temp_double =Double.parseDouble("666.666");
boolean temp_boolean=Boolean.parseBoolean("true");
5.2 String ——> char
String 类型转 char,只能调用 String 的静态方法实现
转换功能的方法 —— char[] toCharArray() : 将字符串转换成字符数组
获取功能的方法 —— char charAt(int index) : 获取指定索引位置的字符
//定义一个字符串
String string = "Avocado";
//利用 toCharArray 方法将字符串转换为字符数组
char[] charArray = string.toCharArray();
System.out.println("已将字符串转为字符数组,下面从字符数组中读取头两个字符");
System.out.println("第1个字符是:" + charArray[0]);
System.out.println("第1个字符是:" + charArray[1]);
System.out.println("----");
// 利用 charAt 方法来直接获取字符串中的每一个字符元素
char temp_char_0 = string.charAt(0);
char temp_char_1 = string.charAt(1);
System.out.println("已将字符数组中的字符单个提取出为 char 类型的变量,下面输出头两个变量");
System.out.println("第1个字符是:" + temp_char_0);
System.out.println("第2个字符是:" + temp_char_1);
5.3 8 种基本类型 ——> String
基本类型要转字符串那就太简单了,最常见的两种方式——
- 基本类型数据直接与空字符串进行拼接
- 调用 String 类的静态 valueOf 方法。
// int 类型拼接空字符串转 String
int int_temp = 10;
String string_temp_1 = int_temp + "";
// ... 省略其余七种,完全一致
// 调用 String 类的静态 valueOf 方法转 String
String string_temp_2 = String.valueOf(int_temp);
// ... 省略其余七种,完全一致
5.4 String类 ——> 包装类
有两种方式,如下 :
- 调用包装类的静态方法 parseXXX,但是必须确保包装类能接收。
- 这种方法自 JDK 9 以后就不再支持。利用包装类的构造器,例如 : Integer integer_1 = new Integer(字符串类型变量);
// 调用包装类的静态方法 parseInt
String string_temp = "10"
// 这里比较复杂
// parseInt 返回值是 int 类型
// 但是由于检测到所需类型是包装类 Integer,启用自动装箱机制将 int 返回值转化成 Integer
Integer integer_temp = Integer.parseInt("10");
// 默认就是返回 int,可以直接赋值
int int_temp = Integer.parseInt("10");
// ... 省略其余七种,完全一致
5.5 包装类 ——> String类
有三种方式,如下 :
- 包装类型数据直接与空字符串进行拼接
- 调用 String 的 静态 valueOf 方法
- 调用包装类的 toString 方法
//方式一:包装类型数据直接与空字符串进行拼接
Integer integer_0 = 10;//自动装箱
String string_0 = integer_0 + "";
//方式二:调用 String 的 静态 valueOf 方法
Integer integer_2 = 431;
String string_2 = String.valueOf(integer_2) + " world";
// 实际上是非必要,由于自动拆箱机制,一句 integer_2 + " world" 已经足够
//方式三:
Integer integer_1 = 20;
String string_1 = Integer.toString(integer_1) + " hello";
// 实际上是非必要,由于自动拆箱机制,一句 integer_1 + " world" 已经足够
6. 包装类的常用方法
static int compare(int x, int y); 比较大小
static int max(int a, int b); 最大值
static int min(int a, int b); 最小值
static int parseInt(String s); 将字符串数字转换成数字类型。其它包装类也有这个方法:Double.parseDouble(String s)
int compareTo(Integer,anotherInteger); 比较大小,只不过传入的形参是包装类
boolean equals(Object obj); 包装类已经重写了equals()方法,比较的是内容而非地址
String toString(); 包装类已经重写了toString()方法,输出的是包装类的内容
int intValue(); 将包装类拆箱为基本数据类型
static String toString(int i); 将基本数据类型转换成字符串
static Integer valueOf(int i); 将基本数据类型转换成Integer
static Integer valueOf(String s) 将字符串转换成Integer(这个字符串必须是数字字符串才行,不然出现NumberFormatException)
大数类
包装类好处不再介绍,但需要知道的是,包装类并未解决基本类型遗留的问题:超过范围,例如 int 、Integer 最大值都是 2^31-1。
因此,对于 Integer 包装类,进一步引入 BigInteger;对于 Double 包装类,进一步引入 BigDecimal
- 大数类的取值范围原则上是没有上限的,取决于你的计算机的内存
- 使用这些大数类时,由于它们是
不可变对象
,因此进行任何算术运算都会返回一个新的实例,而不会改变原始对象的值。 - 区别于包装类,大数类没有自动装/拆箱机制,因此两个对象之间
不能直接做基本运算
,用add、subtract、mutiply、divide 等方法替代。 - 在进行除法或取余运算时,需要注意处理可能出现的除数为零的情况,以及在使用 divide 方法时指定舍入模式以避免ArithmeticException。
1. BigInteger
1. API – 构造器
BigInteger 与 String 有几分相似,然而它的初始化方式却没有 String 那么方便可以直接赋值,而是跟其他自定义的类一样,要调用构造器进行初始化
。
// 这个构造器通过一个表示整数的字符串来创建一个 BigInteger 对象。
// 字符串中的数字可以是正数、负数,也可以包含加号 + 或减号-作为前缀。
// 例如,new BigInteger("123456") 或 new BigInteger("-987654")。
public BigInteger(String val):
// radix 参数指定进制,不指定的情况下默认是 10
public BigInteger(String val, int radix):
// 这个构造器根据给定的符号( signum,其中 -1 表示负数,0 表示零,1 表示正数)
// 和一个无符号字节数组(magnitude)来构造一个 BigInteger。
//字节数组的最高有效字节在数组的开头。例如,一个表示整数 123 的大端字节数组为{0, 0, 0, 123}。
public BigInteger(int signum, byte[] magnitude)
// 这个构造器从指定的字节数组的一部分创建一个BigInteger。val是字节数组,off是数组中开始复制的偏移量,len是要使用的字节数。
// 同样地,字节数组被解释为无符号的,并且最高有效字节在前。
public BigInteger(byte[] val, int off, int len):
/**
这个构造器在处理与位宽相关的算法和协议(例如,在密码学中处理密钥或消息的填充)时特别有用,确保数据按照预期的位宽对齐和格式化。
这个构造器创建一个具有指定比特长度的新 BigInteger,传入的参数是一个 BigInteger 对象
如果 val 的二进制表示已经至少有 bitLength 位,那么构造器将直接返回 val,不做任何修改。
如果 val 的二进制表示少于 bitLength 位,构造器会在 val 的二进制表示的最左边补足 0,直到达到至少 bitLength 位。这实际上不改变数值的大小,只是改变了数值的表示形式,使其在位级操作中能够与指定宽度的其他数值对齐。
假设有一个 Integer 对象 val 其值为 1024,其二进制表示为10000000000(11位)。
如果我们想要得到一个至少有16位的二进制表示的大整数,可以这样调用构造器:
结果result的二进制形式将会是0000100000000000,保持数值不变,但确保了至少有16位。
*/
BigInteger result = new BigInteger(val, 16);
public BigInteger(BigInteger val, int bitLength):
1.2 API – 函数
BigInteger 中都是实例方法
,因此必须通过实例调用,返回的也是 BigInteger 对象
// 四种基本运算
BigInteger add / subtract / multiply / divide(BigInteger val);
double doubleValue()、float floatValue() 将 BigInteger 对象中的值以双精度等类型数返回返回。
long longValue() 、int intValue()
BigInteger max / min(BigInteger val) 返回两个大整数的最大 / 小者
String toString() 将当前大整数转换成十进制的字符串形式
2. BigDecimal
2.1 API
构造器:
BigDecimal(int)、 BigDecimal(long) 创建一个具有参数所指定 int/long 值的对象。
BigDecimal(String) 创建一个具有参数所指定以字符串表示的数值的对象。
//该构造方法比较特殊,存在潜在的精度丢失风险
BigDecimal(double)
//当传入的数值是 double 时,应该使用 BigDecimal.valueOf(double)
// 此外,BigDecimal.valueOf(xxx ) 是静态工厂类,永远优先于构造函数(摘自<<Effecitve java>>,)
BigDecimal d1 = BigDecimal.valueOf(12.3)//结果是12.3 你预期的
BigDecimal d2 = new BigDecimal(12.3) //结果是12.30000000000000071054...具体原因可查看底层源码
函数:
// 四种基本运算
BigDecimal add / subtract / multiply / divide(BigDecimal val);
toString() 将 BigDecimal 对象的数值转换成字符串。
double doubleValue()、float floatValue() 将 BigDecimal 对象中的值以双精度等类型数返回返回。
long longValue() 、int intValue()
// 尽管两个方法的作用都是比较大小,但推荐 BigDecimal 搭配 compareTo 使用
BigDecimal.compareTo(BigDecimal val)
BigDecimal.equals(BigDecimal val)
BigDecimal decimal1 = new BigDecimal("200");
BigDecimal decimal2 = new BigDecimal("200.0");
System.out.println(decimal1.equals(decimal2));
System.out.println(decimal1.compareTo(decimal2));
// out : false;0
// 可见 equals 方法同时关注了小数的位数,尽管二者值完全相同,但就是输出 false
3. Double 和 BigDecimal 的比较
3. 两种类型使用过程中都有可能出坑
- double 计算时容易出现不精确的问题
double 的小数部分容易出现使用二进制无法准确表示
如十进制的0.1,0.2,0.3 都不能准确表示成二进制;
-
对两个 double 进行" === " 比较容易出现不精确的问题
-
两个 BigDecimal 作除法,如果结果是无限小数,会抛出异常:ArithmeticException,因此,除法时必须每次都指定位数,避免异常
-
BigDecimal 是不可变对象
任何针对 BigDecimal 对象的修改都会产生一个新对象;
BigDecimal newValue = BigDecimal.valueOf(1.2222).add(BigDecimal.valueOf(2.33333));
BigDecimal newValue = BigDecimal.valueOf(1.2222).setScale(2);
总之每次修改都要重新指向新对象,才能保证计算结果是对的。
- BigDecimal 没有 equals 方法,
2. 优缺点总结
- double:
- double 在计算过程中容易出现丢失精度问题
- 使用方便,有包装类,可自动拆装箱,计算效率高
- BigDecimal:
- 精度准确,但做除法时要注意除不尽的异常
- BigDecimal 是对象类型,也没有自动拆封箱机制,操作起来总是有些不顺手
2. 使用场景
- 涉及到精准计算如金额,一定要使用 BigDecimal 或转成 long、int 计算、
- 若不需要精准的,如一些统计值:(本身就没有精确值)用户平均价格,店铺评分,用户经纬度等本身就没有精准值一说的推荐使用double、float,写代码更方便,计算效率也高得多;
- 如果 double、float 仅是用于传值,并不会有精度问题;但如果参与了计算就要小心了,要区分是不是需要精准值,如果需要精准值,需要转成 BigDecimal 计算以后再转成 double;
- 约定在 DTO 定义金额时使用 BigDecimal 或整形值,是为了避免 double 参与金额计算的机会,避免出 bug;
实例:
@ApiModel(value = "当前位置的信息")
public class LocationInfo {
@ApiModelProperty(value = "当前地址的经度", example = "121.471231")
private Double longitude;
@ApiModelProperty(value = "当前地址的维度", example = "31.231121")
private Double latitude;
// 如果只是整数时,可使用Integer
@ApiModelProperty(value = "店铺平均消费范围下限", example = "12")
private Double costFrom;
// 如果只是整数时,可使用Integer
@ApiModelProperty(value = "店铺平均消费范围内限", example = "22")
private Double costTo;
// 价格,这个就关键了,这个需要精确值,且常常用于订单购物车计算,因此要使用BigDecimal
@ApiModelProperty(value = "当前价格", example = "12.22")
private BigDecimal price;
}
通过以上代码可以看出,对于经纬度、平均消费额度等不需要精确到极致的数据,Double 已经完全够用了,强行用 Decimal 徒增功耗
2. MySQL 中如何选用这两种类型
- 与 Java 不同的,MS 是用来持久化数据的,而 Java 中使用的数据一般更多的是过一下内存;
- 数据库都要除了指定数据类型指外还需要指定精度,因此在 DB 中 Double 计算时精度的丢失比Java高得多;因为Java默认精确到15-16位了;
- 更改数据类型的成本,MS 比 Java 代码要高得多;
- 考虑到以上与 Java 中不同几点:
- 与商业金融相关字段要使用 Decimal 来表示,如金额,费率等字段;
- 参与各类计算如加,减,乘,除,sum,avg等等,也要使用Decimal;
- 经纬度,可以使用 double 来表示,这个可参考 Java,只要保证精度范围即可;
- 如果确实不确定使用什么 double 或 Decimal 哪种类型合适,那最好使用 Decimal,毕竟稳定,安全高于一切;
- 阿里的编码规范中强调带小数的类型一律使用Decimal类型,也是有道理的,使用 Decimal 可以大大减少计算踩坑的概率
数字格式化
https://blog.csdn.net/wdd1324/article/details/70153896
String 类
- Java 中没有内置的字符串类型,而是在 java.lang 包中提供了一个预定义的类 String,每个用双引号引起来的字符串都是 String 类的实例。
- Java 默认导入 java.lang 包下的所有类,因此仅从使用的角度说,可以认为 String 也被 Java 预先定义了。
- JDK 9 以前,String 底层是用 char 数组实现的
- 总的来说 Java 中规定了 String 不属于基本数据类型,只是代表一个类,属于引用类型,因此 String 的默认值是 null。
- 但 String 可以直接赋值,也可以 new 出实例。
这是因为 Java 有字符串常量池机制
- 但 String 可以直接赋值,也可以 new 出实例。
String str1 = "avocado";
String str2 = new String("avocado");
1. 字符串常量池机制
- 字符串常量池位于 JVM 的
堆
中
1.1 字符串常量池的设计思想
:
字符串属于引用数据类型,如果没有字符串常量池机制,那么分配一个 String 类型就和其他的对象分配一样,耗费高昂的时间与空间代价。这样,作为最基础的数据类型,大量频繁的创建字符串,就会极大程度地影响程序的性能。
- JVM 在实例化字符串常量的时候进行了一些
优化
:- 为字符串开辟一个字符串常量池,类似于缓存区。
- 创建字符串常量时,首先查找字符串常量池是否存在该字符串。
- 存在该字符串,返回引用实例,不存在,实例化该字符串并放入池中。
String a = "a";
String b = "a";
/* 由于字符串常量池的机制,尽管 String 是引用数据类型,== 仍会返回 true
* == 用于判断引用所指向的实例是否为同一个。如果比较两个对象,当然无论如何也不会相等
* 对于 String,由于字符串常量池的存在,a b两个引用指向的是同一个实例,因此自然相等
*/
if (a == b && a.equals(b) )
System.out.println("a != b");
else
System.out.println("a = b");
}
- 字符串常量池的
实现基础
- 实现该优化的基础是因为字符串是不可变的,可以不用担心数据冲突进行共享。
- 运行时实例创建的全局字符串常量池中有一个表,总是为池中每个唯一的字符串对象维护一个引用,这就意味着它们一直引用着字符串常量池中的对象,所以,在常量池中的这些字符串不会被垃圾收集器回收。
1.2 常量池机制何时生效?
- 仅当使用双引号声明字符串对象时,字符串常量池机制生效,具体过程上述。
- 使用 new 关键字创建的字符串对象会先从字符串常量池中找,如果没找到就创建一个,然后再在堆中创建字符串对象;如果找到了,就直接在堆中创建字符串对象。
// new String 时不生效,但仍可通过 intern 方法将创建的 String 加入到常量池
// 使用字面量赋值字符串
String str1 = "hello";
String str2 = "hello";
// 使用 new 关键字
String str3 = new String("hello");
String str4 = new String("hello");
// 手动调用 intern() 方法
String str5 = str3.intern();
String str6 = str4.intern();
System.out.println("str1 == str2: " + (str1 == str2)); // true
System.out.println("str3 == str4: " + (str3 == str4)); // false
System.out.println("str1 == str3: " + (str1 == str3)); // false
System.out.println("str1 == str5: " + (str1 == str5)); // true
System.out.println("str5 == str6: " + (str5 == str6)); // true
1.3 new String 的值存储过程
// 字符串字面量
String str1 = "avocado";
// 通过 new 关键字创建的字符串
String str2 = new String("avocado");
// 比较引用
System.out.println("str1 == str2: " + (str1 == str2)); // false
// 比较内容
System.out.println("str1.equals(str2): " + str1.equals(str2)); // true
- 第一步,首先检查字符串常量池中是否存在 avocado。这与 字面量赋值创建字符串是完全一致的
- 如果 “avocado” 不存在于字符串常量池中,首先会将这个字面量字符串 “avocado” 放入字符串常量池中。
- 如果 “avocado” 存在于字符串常量池中,则进入下一步
- 也就是说,即使通过 new 关键字创建字符串 avocado,但如果常量池中不存在 avocado,那么编译器依然会将字面量部分放入字符串常量池中 。
- 第二步,堆内存中创建一个新的字符串对象,其内容为 “avocado”。这个新对象是一个完全独立的对象,与字符串常量池中的 “avocado” 不是同一个对象。
- 第三步,str2 引用指向堆内存中的 avocado对象,而非常量池中的 hello 对象
最终,由于 str1 指向常量池中的 avocado,str2 指向堆内存中的 avocado 对象,因此 == 对比它们的引用会输出 false;
equals 对比他们的内容会输出 true。
// 这段代码创建了两个对象,一个存储在常量池中,一个存储在常量池外堆中
String str2 = new String("avocado");
1.5 字符串的比较
- 如果需要比较两个字符串的内容,必须用 equals 以避免潜在的错误。
- " == " 运算符比较的是两个对象的引用是否相同,即它们是否指向同一个内存地址。因此,如果如果两个字符串对象的引用不同,即使它们的内容相同,比较结果也会是 false。
- equals 方法比较的是两个对象的内容是否相同,即字符串的字符序列是否一致。
- 通过以下代码应当知道:尽量避免 new String 和 “==” ,尤其是后者。
// 使用字符串字面量
String str1 = "hello";
String str2 = "hello";
// 使用 new 关键字
String str3 = new String("hello");
String str4 = new String("hello");
//str3 和 str4 是用 new 创建的,因此常量池不生效,二者是不同的对象,
//因此尽管二者内容相同,但 “==” 返回的是 false
System.out.println("str3 == str4: " + (str3 == str4)); // false
// 而对于字面量赋值的 str1 和 str2 ,常量池机制生效,两者指向同一对象,“==”返回自然是 true。
System.out.println("str1 == str2: " + (str1 == str2)); // true
2. API
String 是 Java 预先定义好的一个类,自然,其内提供了不少方法。
length() 用于返回字符串长度。
isEmpty() 用于判断字符串是否为空。
charAt() 用于返回指定索引处的字符。
valueOf() 用于将其他类型的数据转换为字符串。
str1.concat(str2) 拼接字符串
String.join("所需连接符",str1,str2)
// 返回该字符串对应的 hash 值,且该值极大概率是唯一的.
// 因此 String 很适合来作为 HashMap(后面会细讲)的键值。
int hashCode()
// 可以从指定区间内截取,也可以从指定起点截取其后所有
String substring(int beginIndex)
String subString(int beginIndex,int endIndex)
// indexOf 方法用于查找一个子字符串在原字符串中第一次出现的位置,并返回该位置的索引。
int indexOf(int ch)
int indexOf(String str)
// 从指定位置开始查找查找子字符串的位置
indexOf(int ch, int fromIndex)
// 删除字符串两端的空白字符。这里的“空白字符”指的是空格、制表符(\t)、换行符(\n)以及其他形式的空白字符。
String trim()
// 将字符串按照给定的正则表达式拆分为字符串数组((https://blog.csdn.net/m0_46671240/article/details/135484151?csdn_share_tail=%7B%22type%22:%22blog%22,%22rType%22:%22article%22,%22rId%22:%22135484151%22,%22source%22:%22m0_46671240%22%7D))
String [] split(regex)
//subString 方法实例
String str = "avocado";
String prefix = str.substring(0, 5); // 提取前 5 个字符即 "avoca"
String suffix = str.substring(2); // 提取从第 2 个字符开始的所有字符,即 "ocado"
// 需求:将 " Hello world !" 提取出 H
String str = " Hello world! ";
String trimmed = str.trim(); // trim 去除字符串开头和结尾的空格
System.out.println(trimmed);
// split 按照指定的 regex 分割字符串,在这里 \\s 表示 所有空格符号,即遇到空格符号就分割
String[] words = trimmed.split("\\s+");
String string = Arrays.toString(words);
System.out.println(string);
String firstWord = words[0].substring(0, 1); // subString 提取第一个单词的首字母
System.out.println(firstWord); // 输出 "H"
3. 字符串拼接
3.1 +
和 StringBuilder.append 静态方法
String avocado = "牛油果";
String apple = "苹果";
System.out.println(avocado + apple); // out: avocadoapple
// 最简单拼接字符串的方法就是对两个字符串使用 + 号,但 + 号实质是一个语法糖,编译器会对其进行重新解释
// JDK 8 下,分析字节码可知,编译器把 “+” 号操作符替换成了 StringBuilder 的 append() 方法
System.out.println((new StringBuilder(avocado)).append(apple).toString());
3.2 某些地方可能不得不手动使用 append
尽管编译器会将 + 重构成 append 方法,但有些时候最好手动使用
- 在循环中进行字符串拼接时。
- 处理大量字符串拼接操作时。
// 每次循环都会创建新的 StringBuilder 对象和临时字符串
String result = "";
for (int i = 0; i < 10; i++) {
result += i;
}
// 只创建一个 StringBuilder 对象然后传入,整段代码只使用了一个对象
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10; i++) {
sb.append(i);
}
String result = sb.toString();
3.4 append 静态方法源码
// 位于 StringBuilder 类中,重写了父类 AbstractStringBuilder 的 append() 方法
public StringBuilder append(String str) {
super.append(str);
return this;
}
这 3 行代码其实没啥看的。我们来看父类 AbstractStringBuilder 的 append() 方法:
- 判断拼接的字符串是不是 null,如果是,当做字符串“null”来处理。appendNull() 方法的源码如下:
- 获取字符串的长度。
- 由于字符串内部是用数组实现的,所以需要先判断拼接后的字符数组长度是否超过当前数组的长度,如果超过,先对数组进行扩容,然后把原有的值复制到新的数组中。
- 将拼接的字符串 str 复制到目标数组 value 中。
- 更新数组的长度 count。
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
3.5 String.concat 实例方法
- String 类的 str1.concat(str2) 方法,有点像 StringBuilder 类的 append() 方法。
- 底层通过对字符数组扩容实现
- 缺点是:
- 如果传递的参数为 null,会抛出 `NullPointerException
- 只能拼接两个字符串,
String fruit1 = "avocado";
String fruit2 = "apple";
System.out.println(fruit1.concat(fruit2));
3.6 String.join 静态方法
- String.join 静态,即使参数之一为 null,也不会出现空指针异常
// String.join("String separator",str1,str2)
String fruit1 = null;
String fruit2 = "apple";
String fruit= String.join("", fruit1, fruit2);
System.out.println(fruit); // out: nullapple
由于所需连接符处没有指定,因此没有连接符,输出 nullapple。
3.7 StringUtils.join 静态方法
- 使用该方法必须引入依赖
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
- StringUtils.join 底层使用的仍然是 StringBuilder。
- StringUtils.join 可以确保不出现
空指针异常
- StringUtils.join 参数类型极其随意。既可以是各种类型的数组;又可以是各种类型的数据。弥补了 String.join 只能拼接 String/StringBuilder 的缺点。
// 传入对象类型数组
StringUtils.join(Object[] array, String separator);
// 其他基本类型一致
StringUtils.join(int[] array, String separator)
// 传入迭代器
StringUtils.join(Iterator<?> iterator, String separator);
// 传入各种类型的数据
StringUtils.join(int elements,...);
String fruit1 = "avocado";
String fruit2 = "apple";
String[] fruits = {"avocado", "apple"};
// 可接受参数的形式,随意到了令人发指的地步
String join1 = StringUtils.join(1,".avocado\n", 2,".apple");
String join2 = StringUtils.join(fruit1, fruit2);
String join3 = StringUtils.join(fruits, "");
System.out.println(join1);
System.out.println(join2);
System.out.println(join3);
4. 字符串拆分
字符串拆分,要么是 String.split,要么是 Regex 包下的 Pattern 和 matcher 联合使用,其内有探究皮毛
3. StringBuffer 和 StringBuilder
- 在字符串不经常发生变化的场景优先使用 String (代码更清晰简洁)。如常量的声明,少量的字符串操作(拼接,删除等)。
- 在单线程情况下,如有大量的字符串操作情况,应使用 StringBuilder 来操作字符串。避免产生大量无用的中间对象,耗费空间且执行效率低下(新建对象、回收对象花费大量时间)。如 JSON 的封装等。
- 在多线程情况下,如有大量的字符串操作情况,应使用 StringBuffer。如 HTTP 参数解析和封装等。
以下详细解释
不可变性
:每个 String 对象一旦创建,其内容就不能被改变。对一个 String 实例执行拼接操作(如使用 + 运算符)时,实际上是在创建一个新的 String 对象来存储拼接后的结果,而不是修改原有的字符串。这意味着每次拼接操作都会伴随至少一个新的字符串对象的创建。
相比之下,StringBuilder 或 StringBuffer 类通过在内部维护一个可变的字符数组来优化这一过程。它们在初始化时预分配一定的容量 ,并在必要时自动扩容,从而减少了内存分配的次数。在拼接字符串时,它们直接在现有字符数组上进行修改和追加,避免了频繁创建新对象的开销,大大提高了字符串操作的效率。
3.1 二者的区别
- 两个类,除了类名不同,StringBuffer 类中方法带有 synchronized 关键字,其他内容基本上完全一样。
- 尽管 StringBuilder 非线程安全,但如果要在多线程环境下修改字符串,仍可以使用 ThreadLocal 来避免多线程冲突。
因此实际开发中,StringBuilder 的使用频率远高于 StringBuffer,甚至可以说,StringBuilder 完全取代了 StringBuffer。 - 总结就是:
- 字符串少改变的,用 String
- 字符串多改变且不要求线程安全的,用 StringBuilder
- 字符串多改变且要求线程安全的,用 StringBuffer
3.2 StringBuilder
new String("avocado") + new String("apple")
// + 被编译器解释为以下代码:
new StringBuilder().append("avocado").append("apple").toString();
3.3.1 constructor
// 首先要知道,字符串对象底层是用数组存储字符实现的,因此容量就是指数组大小
// 无参数构造器:创建一个空的 StringBuilder 对象,初始容量通常为 16 个字符。
StringBuilder sb = new StringBuilder();
// 初始化一个指定内容的 StringBuilder 对象
// 其初始容量足以容纳提供的字符串加上额外的 16 个字符(默认情况)。
StringBuilder sb = new StringBuilder("Hello");
// 初始化一个指定容量、但内容为空的 StringBuilder 对象
StringBuilder sb = new StringBuilder(100);
// 初始化一个指定容量、指定内容的 StringBuilder 对象
// 确保底层数组的初始容量至少为指定的容量值。如果指定的容量小于字符串长度,则容量会自动调整为字符串长度加 16。
StringBuilder sb = new StringBuilder("Hello", 20);
3.3.2 API
int capacity() 返回当前容量。
char charAt(int index) 在指定的索引处返回此序列中的char值。
void ensureCapacity(int minimumCapacity) 确保容量至少等于指定的最小值。
void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin) 字符从此序列复制到目标字符数组dst中。
StringBuilder insert(int offset, String str) 将字符串插入此字符序列中。
int length() 返回长度(字符计数)。
String subString(int start, int end) 截取指定的子串
// StringBuilder 提供的 API,其方法体都是直接在原对象上进行修改,而不是创建一个新对象
StringBuilder append(String str) 将指定的字符串附加到此字符序列中。
StringBuilder reverse() 反转字符串。
StringBuilder replace(int start, int end , String str) 将此序列子字符串中的字符替换为指定String中的字符。
StringBuilder stringBuilder = new StringBuilder("avocado");
StringBuilder reverseBuilder stringBuilder.reverse();
System.out.println(reverseBuilder); // Prints "odacova"
String substring = stringBuilder.substring(0, 2); // 截取0,1个字符
System.out.println(substring); // Prints "od"
// 以上代码说明两个事实:
// 1. stringBuilder 对象截取字符串,但截取到的是逆转后的字符串,说明逆转操作是在原字符串上进行的.
// 2. stringBuilder 和 reverseBuilder 都指向同一个被修改后的对象.
// 因此可以说指定一个新的引用 reverseBuilder 是没有意义的
// 3. 对于在原有对象上进行修改的方法,既可以指定新的引用指向其,也可以不指定.