Java基础底层-String类基础
一、概念
String是所有语言中最常用的一个类;是不可变的,是最终类(final),最终类不能被继承。
核心特点 对String对象的任何改变都不影响到原对象,相关的任何操作都会生成新的对象
二、声明方式
2.1 指令码分析
2.1.1 直接赋值
在编译期间,会将等号右边的abc常量放入常量池(ldc),在程序运行时,会将str1变量压栈,栈中的str1变量直接指向字符串常量池的abc项,没有经过堆内存
2.1.2构造方法实现实例化
在编译期间,会将等号右边的abc常量放入常量池,在程序运行时,现在堆中创建一个String对象,该对象的内容指向常量池中“abc”项,然后将str2变量压栈(如图所示)
2.2 图解
三、String对象为什么不可变
3.1demo代码层面
- demo代码
String str = "abc";
String str1 = "abc";
System.out.println("str="+str);
System.out.println("str的地址="+System.identityHashCode(str));
System.out.println("str1的地址="+System.identityHashCode(str1));
System.out.println("对str值进行修改之后");
str = str + "ABC";
System.out.println("str="+str);
System.out.println("str的地址="+System.identityHashCode(str));
System.out.println("str1的地址="+System.identityHashCode(str1));
str.replace("b","B");
- 运行结果
str=abc
str的地址=1639705018
str1的地址=1639705018
对str值进行修改之后
str=abcABC
str的地址=1627674070
str1的地址=1639705018
- 解释:
str1 从开始到最后都没有改变,指向常量池中的值一直不变;str从刚开始是“abc”,到最后的“abcABC”,虽然表面是对变量str进行了修改,但是通过运行结果可以知道,地址发生了改变。此处实际上是重新创建了一个“abcABC”的对象,让str重新指向了这个值;而原先的“abc”一直没变。
3.2源码层面(jdk7之后)
从上述代码知,在java中String类其实就是对字符数组的封装。在jdk7中,只有一个value变量,也就是value中所有的字符都属于String这个对象。在java中,数组实际上只是一个引用,它指向一个真正的数组对象,也就是如下布局(数组是对象,此处的value实际上是指向对象的一个引用而已)
value这个变量是private的,也没有提供对应的set方法,所以String类的外部无法修改String,也就是说String一旦初始化就不能被修改。
此外这个变量是final的类型,也就是说在String类的内部,一旦变量初始化了,也不能被修改。所以可以认为String对象不可变。
replace,substring等方法解释
源码中存在一些方法,调用后可以得到改变后的值(replace,replaceAll,toLowerCase等);通过源码可以知道,这些值表面上改变了,实际上方法内部创建了一个新的String对象并把这些对象重新赋值给当前的引用。
四、String对象真的不可变
由上可知,String的成员变量都是用private final修饰的,初始化后不可以改变。但是value比较特殊,它是一个引用变量,并不是真正的对象。value是final修饰的,也就是说value不能再指向其他数组对象,但我们可以改变value指向的数组中的值。
使用普通的代码无法做到,因为我们不能访问到value的引用,更不能通过引用去修改数组。我们可以使用反射访问私有成员,进而改变value所指向数组的结构
- 代码demo演示
public static void main(String[] args) throws Exception {
//创建字符串"Hello World", 并赋给引用s
String s = "Hello World";
System.out.println("s = " + s); //Hello World
System.out.println("修改前的str的内存地址" + System.identityHashCode(s));
//获取String类中的value字段
Field valueFieldOfString = String.class.getDeclaredField("value");
//改变value属性的访问权限
valueFieldOfString.setAccessible(true);
//获取s对象上的value属性的值
char[] value = (char[]) valueFieldOfString.get(s);
//改变value所引用的数组中的第5个字符
value[5] = '_';
System.out.println("s = " + s); //Hello_World
System.out.println("修改前的str的内存地址" + System.identityHashCode(s));
}
- 运行结果
s = Hello World
修改前的str的内存地址1639705018
s = Hello_World
修改前的str的内存地址1639705018
- 结论:
通过反射是可以实现修改String值的同时而不改变String的地址,实现真正意义上的修改
五 、常量池和堆(intern)
常量池和堆的区别如上的声明方式
字符串常量池: 属于堆内存中的一部分,是存放字符串常量的地方
堆内存: 创建对象的操作都会经过堆内存,堆中的对象都会指向字符串常量池中的值;
区别:实际上使用赋值和构造方法实现实例化两个操作都是指向常量池中的值,但是构造的变量是指向堆中的对象,堆中的对象在指向常量池中的值。
intern方法:主要的作用是先在常量池中找是否有对应的常量,如果有则返回该常量的地址,如果没有则创建新的常量值然后返回该常量的地址;即通过intern方法获取的变量所指向的地址就是常量池中的地址
- demo代码演示
/**
* str5 在编译后会直接去常量池中str3的值和地址
* str4 和str6 都是 在堆中创建一个新的对象
* @param args
*/
public static void main(String[] args) {
String str1 = "a";
String str2 = "b";
String str3 = "ab";
String str4 = str1 + str2;
String str5 = "a"+"b";
String str6 = new String("ab");
System.out.println(str3==str5);
System.out.println(str3==str4);
System.out.println(str5==str4);
System.out.println(str3==str6);
System.out.println(str4==str6);
/**
* intern 引用常量池中与value相等的常量地址,没有则新增value到常量池中然后引用该地址
*/
System.out.println(str4.intern() == str5);
System.out.println(str6.intern() == str3);
}
- 运行结果
六、String,StringBuffer,StringBuilder
本文地址:https://blog.csdn.net/Huang1178387848/article/details/109838287
上一篇: 控制反转( IoC)和依赖注入(DI)
下一篇: XML解析错误:未组织好 的解决办法