当前位置:网站首页>震惊,99.9% 的同学没有真正理解字符串的不可变性
震惊,99.9% 的同学没有真正理解字符串的不可变性
2022-08-04 04:35:00 【明明如月学长】
一、你以为的常识
1.1 不可变性的理解
稍有些基础的同学都知道 Java 中 String 字符串是“不可变”的,想要使用“可变字符串”可以使用 StringBuilder
和 StringBuffer
。
大多数讲字符串不可变性的文章大同小异。
不可变的定义:
An immutable object is an object whose internal state remains constant after it has been entirely created. This means that once the object has been assigned to a variable, we can neither update the reference nor mutate the internal state by any means.
– 《Why String is Immutable in Java?》
所谓不可变对象,即对象创建之后内部状态保持不变。换句话说,一旦对象被赋值给一个变量,将不再允许通过任何方式改变引用、修改内部状态。
1.2 不可变性的实现
String “不可变性”的保障:
- (1) String 类被 final ,导致不继承;
- (2) 存储 String 的字符的 char 数组为 final 则引用不可改变。
- (3) 所有修改方法(如 concat)都会返回一个新的字符串对象。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
//省略其他
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
}
1.3 不可变性的好处
1.3.1 节省内存
由于字符串的不可变性,不同的字符串变量可以引用同一个示例来实现节省堆内存的目的。
String s1 = "明明如月学长";
String s2 = "明明如月学长");
String s3 = new String("明明如月学长");
assertThat(s1 == s2).isTrue();
assertThat(s1 == s3).isFalse();
1.3.2 更安全
字符串的不可变性保证了安全性。
请看下面的示例代码,先执行参数的合法性检查,然后执行一些次要的认为,最后执行重要的任务:
void criticalMethod(String userName) {
//1 执行安全检查
if (!isAlphaNumeric(userName)) {
throw new SecurityException();
}
//2 执行一些次要的任务
initializeDatabase();
//3 重要的任务
connection.executeUpdate("UPDATE Customers SET Status = 'Active' " +
" WHERE UserName = '" + userName + "'");
}
假如字符串可变,在第一步安全检查通过后字符串发生修改,代码运行可能出现不符合预期的结果,比如造成 SQL 注入等。
字符串的不可变性也保证了多线程访问时的现成安全性。
1.3.3 hashCode 缓存
大家可以看到 String 的 hashCode 的计算和构成字符串的字符有关,由于 String 的不可变性就可以将 hashCode 缓存起来。源码中也可以看出计算过之后,下次调用 hashCode 直接返回。
/** * Returns a hash code for this string. The hash code for a * {@code String} object is computed as * <blockquote><pre> * s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] * </pre></blockquote> * using {@code int} arithmetic, where {@code s[i]} is the * <i>i</i>th character of the string, {@code n} is the length of * the string, and {@code ^} indicates exponentiation. * (The hash value of the empty string is zero.) * * @return a hash code value for this object. */
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
二、怀疑人生
2.1 质疑
不知道你是否真正认真思考过,字符串真的不可变吗?
即使字符串类用 final 修饰,字符串值字符数组也用 final 修饰,所有修改方法都返回新的字符串对象,那么值一定无法修改吗?
答案是否定的!!
我们可以用反射来修改字符串对象的值。
2.2 验证
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
String name ="明明如月学长 admin";
System.out.println("name 修改前:"+name+", hashCode:"+name.hashCode());
String newName = "明明如月学长";
System.out.println("newName:"+newName+", hashCode:"+newName.hashCode());
replace(name,newName);
System.out.println("name 修改后: "+name+", hashCode:"+name.hashCode());
}
private static void replace(String name,String newName) throws NoSuchFieldException, IllegalAccessException {
// 去掉私有
Field value = String.class.getDeclaredField("value");
value.setAccessible(true);
// 去掉 final
Field mod = Field.class.getDeclaredField("modifiers");
mod.setAccessible(true);
mod.setInt(value, value.getModifiers() & ~Modifier.FINAL);
// 直接替换 value 字符数组
value.set(name, newName.toCharArray());
}
输出结果:
name 修改前:明明如月学长 admin, hashCode:557981902
newName:明明如月学长, hashCode:-292262689
name 修改后: 明明如月学长, hashCode:557981902
2.3 带来的问题
如果字符串的值可以修改,程序就可能出现不符合预期的行为,
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.concurrent.TimeUnit;
public class StringDemo {
public static void main(String[] args) throws InterruptedException {
String name ="明明如月学长 admin";
new Thread(()->{
try {
mockExecute(name);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
TimeUnit.SECONDS.sleep(2);
new Thread(()->{
try {
mockInject(name);
} catch (Exception e) {
e.printStackTrace();
}
}).start();
TimeUnit.SECONDS.sleep(10);
}
private static synchronized void mockInject(String name) throws InterruptedException, NoSuchFieldException, IllegalAccessException {
// sleep 让 子线程执行到 wait
TimeUnit.SECONDS.sleep(1);
// 检查前使用合规的名称
System.out.println("mockInject: 去掉 admin,绕过校验");
replace(name,"明明如月学长");
StringDemo.class.notifyAll();
System.out.println("mockInject: 恢复 admin");
// 检查后换成不合规的名称
replace(name,"明明如月学长 admin");
System.out.println("mockInject: 恢复 admin 完毕");
StringDemo.class.notifyAll();
}
private static void replace(String name,String newName) throws NoSuchFieldException, IllegalAccessException {
Field value = String.class.getDeclaredField("value");
value.setAccessible(true);
Field mod = Field.class.getDeclaredField("modifiers");
mod.setAccessible(true);
mod.setInt(value, value.getModifiers() & ~Modifier.FINAL);
value.set(name, newName.toCharArray());
}
private static synchronized void mockExecute(String name) throws InterruptedException {
System.out.println("mockExecute: [1] name.contains(\"admin\"):"+name.contains("admin"));
StringDemo.class.wait();
//1 参数检查
if(name.contains("admin")){
throw new IllegalArgumentException("参数检查失败");
}
System.out.println("mockExecute: 不含 admin 关键字,参数检查通过");
System.out.println("mockExecute: [2] name.contains(\"admin\"):"+name.contains("admin"));
//2 执行次要任务
System.out.println("mockExecute: 执行次要任务");
//3 执行重要人物
System.out.println("mockExecute: 执行重要任务");
}
}
这里简单使用 wait/notify 来模拟多线程情况下字符串修改带来的问题。
输出的结果:
mockExecute: [1] name.contains(“admin”):true
mockInject: 去掉 admin,绕过校验
mockInject: 恢复 admin
mockInject: 恢复 admin 完毕
mockExecute: 不含 admin 关键字,参数检查通过
mockExecute: [2] name.contains(“admin”):false
mockExecute: 执行次要任务
mockExecute: 执行重要任务
三、总结
字符串的不可变性是指通过 String 的方法来修改字符串都会产出新的字符串队形。但并非指字符串的字符一定无法被修改,我们可以通过反射一样可以对字符串的“状态/值” 进行修改。
正常情况下不会有人去这么做,否则会产出很多不出乎意料的 BUG。
通过本文想提醒大家,尽信书不如无书,对于看到的知识要有自己的思考。
创作不易,如果本文对你有帮助,欢迎点赞、收藏加关注,你的支持和鼓励,是我创作的最大动力。
边栏推荐
- 系统设计.秒杀系统
- 看DevExpress丰富图表样式,如何为基金公司业务创新赋能
- 3000 words, is take you understand machine learning!
- How class only static allocation and dynamic allocation
- Mockito unit testing
- 【Ryerson情感说话/歌唱视听数据集(RAVDESS) 】
- How to simplify the automation of modern e-procurement?
- 10 Convolutional Neural Networks for Deep Learning 3
- sql语句查询String类型字段小于10的怎么查
- 2022杭电多校联赛第五场 题解
猜你喜欢
随机推荐
How class only static allocation and dynamic allocation
TL431的基本特性以及振荡电路
小程序 + 电商,玩转新零售
2022 Hangzhou Electric Power Multi-School League Game 5 Solution
7-3 LVS+Keepalived集群叙述与部署
打造一份优雅的简历
软件测试如何系统规划学习呢?
Use serve to build a local server
The Shell function
一文详解DHCP原理及配置
系统设计.秒杀系统
本周四晚19:00知识赋能第4期直播丨OpenHarmony智能家居项目之设备控制实现
七夕节,我用代码制作了表白信封
大型连锁百货运维审计用什么软件好?有哪些功能?
How to systematically plan and learn software testing?
JVM笔记
劝退背后。
结构体函数练习
Oracle与Postgresql在PLSQL内事务回滚的重大差异
PL/SQL Some Advanced Fundamental