环境:SpringBoot3.2.5
1. replace是否会替换所有字符?
在处理字符串时,我们经常需要替换字符串中的字符,比如在字符串 "ACDAB$%^&A*Y" 中将 A 替换为 X。首先想到的方法可能就是使用 replace 方法。
如果目的是将所有出现的 A 都替换为 X,那么使用 replaceAll 方法似乎很直观。方法名本身就清楚地表明了它的用途。
于是问题来了:replace 方法会替换所有匹配的字符吗?
JDK文档说明:
图片
翻译:该方法将此字符串中每个与目标字面量序列相匹配的子字符串替换为指定的替换字面量序列。替换操作从字符串的开头到结尾依次进行,例如,在字符串 "aaa" 中将 "aa" 替换为 "b" 将得到 "ba" 而不是 "ab"。
那么 replace 与 replaceAll 的区别?
replace 方法有2个重载的方法:
String str = "ACDAB$%^&A*Y" ;
System.err.println(str.replace('A', 'X')) ;
System.err.println(str.replace("A", "X")) ;
replaceAll 方法签名:
public String replaceAll(String regex, String replacement)
可以通过正则表达式的方式进行替换。如下示例:
String str = "ACDAB$%^&A*Y" ;
// 简单字符串替换
System.err.println(str.replaceAll("A", "X")) ;
// 正则替换,替换 '*' 字符,需要转义
System.err.println(str.replaceAll("\\*", "XO")) ;
如果仅仅是将 '*' 进行替换,那么使用replace更简单
System.err.println(str.replace("*", "XO")) ;
以上都是替换整个字符串中匹配的,如果你只希望替换第一个出现的,那么可以使用如下方法:
System.err.println(str.replaceFirst("A", "-")) ;
第一个参数接受的是正则表达式。
2. Integer类型不要用 "==" 判断
这不是绝对的,需要看情况,你比较的数值大小了。如下示例:
Integer a = 1 ;
Integer b = 1 ;
System.err.printf("a == b ? %s%n", a == b) ;
a = 128 ;
b = 128 ;
System.err.printf("a == b ? %s%n", a == b) ;
a = -128 ;
b = -128 ;
System.err.printf("a == b ? %s%n", a == b) ;
a = -129 ;
b = -129 ;
System.err.printf("a == b ? %s%n", a == b) ;
输出结果:
a == b ? true
a == b ? false
a == b ? true
a == b ? false
为什么这样?通过javap反编译后
图片
当我们将值赋给Integer类型变量时调用的是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);
}
这里的low与high取值如下:
图片
默认[low, high] = [-128, 127],也就是说默认Integer缓存了这个范围的数字,只要取值在这个范围,那么都将返回缓存的数据。这也就是上面输出结果的原因了。
通过上面的源码我们也看到了,我们是可以通过jvm参数来改变这默认缓存大小的,运行程序时添加如下的jvm参数:
-Djava.lang.Integer.IntegerCache.high=128
这样设置后,我们在运行上面程序,128的比较将打印true 。
3. 使用BigDecimal能否避免精度损失?
通常,对于涉及小数(例如金额)的字段,我们会将它们定义为BigDecimal而不是Double,以避免精度损失。考虑以下使用Double的场景:
double a = 0.02;
double b = 0.03;
System.out.println(a - b);
最终结果我们期望的是0.01,但实际是:
0.009999999999999998
这是因为两个double值的减法运算会被转换为二进制形式,而double的有效数字精度限制为16位,这可能导致小数位的存储不足,从而产生误差。
那么使用BigDecimal是否能解决呢?
BigDecimal a1 = new BigDecimal(0.02) ;
BigDecimal b1 = new BigDecimal(0.03) ;
System.err.println(b1.subtract(a1)) ;
执行结果
0.0099999999999999984734433411404097569175064563751220703125
为什么?我们先看看BigDecimal的构造函数说明:
图片
我们看上面的第一点即可:
翻译:这个构造函数的结果可能会有些不可预测。人们可能会认为,在Java中写new BigDecimal(0.1)会创建一个完全等于0.1的BigDecimal(未缩放值为1,精度为1),但实际上它等于0.1000000000000000055511151231257827021181583404541015625。这是因为0.1在双精度浮点数(或者任何有限长度的二进制小数)中无法被精确表示。因此,尽管表面上看起来如此,但传递给构造函数的值并不完全等于0.1。
这也说明了,我们直接通过构造函数传入的double类型进行计算是有风险的。
接着我们看第二点:
翻译:String 构造函数是完全可预测的:写 new BigDecimal("0.1") 会创建一个完全等于 0.1 的 BigDecimal,正如人们所期望的那样。因此,通常建议优先使用 String 构造函数而不是这个(指直接使用 double 值的)构造函数。
我们将上面的代码改为如下:
BigDecimal aa = new BigDecimal("0.02") ;
BigDecimal bb = new BigDecimal("0.03") ;
System.err.println(bb.subtract(aa)) ;
// 0.01
输出正确
我们还可以通过如下的方式:
aa = BigDecimal.valueOf(0.02) ;
bb = BigDecimal.valueOf(0.03) ;
System.err.println(bb.subtract(aa)) ;
// 0.01
此种方式是不是更加方便。其BigDecimal#valueOf内如如下:
图片
关于BigDecimal更多内容请查看下面文章:
不想被坑?快来了解BigDecimal的陷阱。
4. 是否真的不能使用 "+" 拼接字符串?
字符串值被视为不可变的序列。这意味着一旦定义了字符串对象,其数据就不能被修改。如果需要进行修改,则会创建一个新的对象。如下示例:
String a = "123" ;
String b = "456" ;
String c = a + b ;
System.out.println(c) ;
在涉及大量字符串拼接的场景中,使用String对象会创建许多不必要的中间对象。这不仅浪费内存空间,还会降低效率。
在这种情况下,我们可以使用更高效的可变字符序列,如StringBuilder或StringBuffer来定义对象。
那么,StringBuilder和StringBuffer有什么区别呢?
主要区别在于,StringBuffer在其主要方法上添加了synchronized关键字,而StringBuilder则没有。因此:
- StringBuffer是线程安全的。
- StringBuilder不是线程安全的。
在大多数情况下,建议使用StringBuilder进行字符串拼接,触发你需要在多线程环境下进行字符串的操作。
StringBuilder中的append方法可以在不创建中间对象的情况下拼接字符串,因此它更高效,而且它不是同步的。
String a = "123";
String b = "456";
StringBuilder c = new StringBuilder();
c.append(a).append(b);
System.out.println(c);
那么使用String进行字符串拼接是否总是比使用StringBuilder效率低?
首先,我们通过javap反编译上面使用StringBuilder的代码:
图片
通过反编译,定义了2个String变量,创建一个StringBuilder对象,最后使用了2次append方法。
最后,我们再反编译使用 "+" 操作符的方式:
图片
对比下,基本一样啊。
注意:从JDK 5开始,Java对String类型的字符串的+操作进行了优化。这个操作在编译成字节码文件时,+操作会被转换成StringBuilder的append方法调用,以提高效率。
5. isEmpty & isBlank区别
当我们执行字符串操作时,经常需要检查字符串是否为空。如果我们不使用任何工具,通常会像这样进行检查:
public static void check(String source) {
if (null != source && !"".equals(source)) {
System.out.println("not empty");
}
}
如果我们每次都需要进行这样的检查,那可能会非常繁琐。推荐使用Apache Commons Lang 3中的StringUtils类,它包含了许多有用的空值检查方法:isEmpty、isBlank、isNotEmpty、isNotBlank,以及其他字符串处理方法。
接下来, 我们来看看isEmpty与isBlank的区别。
StringUtils.isEmpty(null) ;
StringUtils.isEmpty("") ;
StringUtils.isEmpty(" ") ;
StringUtils.isEmpty("bob") ;
StringUtils.isEmpty(" bob ") ;
使用isBlank
StringUtils.isBlank(null) = true
StringUtils.isBlank("") = true
StringUtils.isBlank(" ") = true
StringUtils.isBlank("bob") = false
StringUtils.isBlank(" bob ") = false
这两种方法的关键区别在于,对于空字符串 " " 的情况,isEmpty 返回 false,而 isBlank 返回 true。
6. Mapper返回的集合List是否进行Null检查?
如下代码,是否需要进行null检查?
List<User> list = userMapper.query(search);
if (CollectionUtils.isNotEmpty(list)) {
List<Long> idList = list.stream().map(User::getId).collect(Collectors.toList());
}
注:CollectionUtils使用的是commons-collections4包。内部如下调用
public static boolean isEmpty(final Collection<?> coll) {
return coll == null || coll.isEmpty();
}
现在我们要确定的是如果基于MyBatis查询返回的集合是否需要进行null检查呢?
查看MyBatis源码,DefaultResultSetHandler#handleResultSets方法。
图片
collapseSingleResultList方法
private List<Object> collapseSingleResultList(List<Object> multipleResults) {
return multipleResults.size() == 1 ? (List<Object>) multipleResults.get(0) : multipleResults;
}
通过查看源码得知,我们没有必要进行null的检查,得到结果后可以直接进行使用。
7. 正确使用indexOf方法
首先,我们先来看看下面的代码:
String source = "#ABBXXXOOO*pack";
if (source.indexOf("#") > 0) {
System.out.println("success") ;
// TODO
}
此代码并不会输出任何东西。indexOf如果不存在,那么返回 -1。该方法的说明:
图片
指定子字符串第一次出现的索引,如果没有这样的出现,则返回-1。
indexOf方法返回指定元素在字符串中的位置,从0开始计数。在上面的例子中,#位于字符串的第一个位置,所以indexOf方法返回的值实际上是0。
所以,这里我们应该这样判断
if (source.indexOf("#") > -1) {
// ...
}
但是,我觉得下面的方法更好:
if (source.contains("#")) {
System.err.println("contains success") ;
}