String的原理 不可变性

2023-10-21 09:59
文章标签 string 原理 可变性

本文主要是介绍String的原理 不可变性,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

原理 & 不可变性

内部

在 Java 8 中,String 内部使用 char 数组存储数据

public final class Stringimplements java.io.Serializable, Comparable<String>, CharSequence {/** The value is used for character storage. */private final char value[];
}

在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用 coder 来标识使用了哪种编码。

public final class Stringimplements java.io.Serializable, Comparable<String>, CharSequence {/** The value is used for character storage. */private final byte[] value;/** The identifier of the encoding used to encode the bytes in {@code value}. */private final byte coder;
}

不可变性

String对象是不可变的,即对象的状态(成员变量)在对象创建之后不再改变。
(一)不可变性实现
由String内部构造:
(1)String 被声明为 final,因此它不可被继承。(Integer 等包装类也不能被继承)
(2)value 数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组。
(3)String 内部没有改变 value 数组的方法。
可知String是不可变的。

补充:不可变的实现:
String类被final修饰,保证类不被继承。String内部所有成员都设置为私有变量,并且用final修饰符修饰,保证成员变量初始化后不被修改。不提供setter方法改变成员变量,即避免外部通过其他接口修改String的值。通过构造器初始化所有成员(value[])时,对传入对象进行深拷贝(deep copy),避免用户在String类以外通过改变这个对象的引用来改变其内部的值。在getter方法中,不要直接返回对象引用,而是返回对象的深拷贝,防止对象外泄。

(二)不可变的好处

满足字符串常量池的需要(有助于共享
可以将字符串对象保存在字符串常量池中以供与字面值相同字符串对象共享。
如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。
如果String对象是可变的,那就不能这样共享,因为一旦对某一个String类型变量引用的对象值改变,将同时改变一起共享字符串对象的其他 String类型变量所引用的对象的值。

线程安全考虑
同一个字符串实例可以被多个线程共享。字符串的不变性保证字符串本身便是线程安全的。

支持hash映射和缓存
因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。这就使得String很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。

缺点:String对象不适用于经常发生修改的场景,会创建大量的String对象。

 

(三)String 的 “改变”?

public static void main(String[] args) {String s = "ABCDEF";System.out.println("s = " + s);s = "123456";System.out.println("s = " + s);
}

String的改变实际上是创建了一个新的String对象"123456",并将引用指向了这个新的对象,同时原来的String对象"ABCDEF"并没有发生改变,仍保存在内存中

在这里插入图片描述

 

(四)String 的不可变 真的不可变?
通过反射获取value数组直接改变内存数组中的数据是可以修改所谓的"不可变"对象的。

public static void reflectString() throws Exception{// 创建字符串"ABCDEF"并赋给引用sString s = "ABCDEF";System.out.println("s = " + s);	// s = ABCDEFField valueField = s.getClass().getDeclaredField("value");    // 获取String类中value字段valueField.setAccessible(true);    // 改变value属性的访问权限char[] value = (char[]) valueField.get(s);		// 获取s对象上的value属性的值value[0] = 'a';		// 改变value所引用的数组中的某个位置字符value[2] = 'c';value[4] = 'e';System.out.println("s = " + s);	// s = aBcDeF
}

String && StringBuilder && StringBuffer

可变性
String 不可变
StringBuffer 和 StringBuilder 可变

线程安全
String 不可变,因此是线程安全的
StringBuilder 不是线程安全的
StringBuffer 是线程安全的,内部使用 synchronized 进行同步

 StringStringBufferStringBuilder
可变性不可变可变可变
线程安全安全(不可变)安全(Synchronized)不安全
执行效率低(Synchronized)
适用场景操作少量的数据,不需要频繁拼接多线程操作大量数据
只有在对线程安全要求高的情况下使用StringBuffer
单线程操作大量数据
备注

在字符串修改/拼接时,String是不可变的对象, 因此在每次对String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。不仅效率低下,还会大量浪费内存空间。

使用 StringBuffer/StringBuilder 类时,每次都会对 StringBuffer/StringBuilder 对象本身进行修改操作,而不产生新的未使用对象

内存中存储

对于String,其对象的引用都是存储在栈中的。

java中对String对象特殊对待,所以在heap区域分成了两块,一块是字符串常量池(String constant pool),用于存储java字符串常量对象,另一块用于存储普通对象及字符串对象

"abc"字符串常量/s.intern()——StringPool
编译期已经创建好(直接用双引号定义的"abc")的就存储在字符串常量池中。即jvm会在String constant pool中创建对象。字符串常量池(String Pool)保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定。不仅如此,还可以使用 String 的 intern() 方法在运行过程中将字符串添加到 String Pool 中。String Pool用于共享字符串字面量,防止产生大量String对象导致OOM。


jvm会首先在String constant pool 中寻找是否已经存在(equals)“abc"常量,如果没有则创建该常量,并且将此常量的引用返回给String a;如果已有"abc” 常量,则直接返回String constant pool 中“abc” 的引用给String a。


当一个字符串调用 intern() 方法时,如果 String Pool 中已经存在一个字符串和该字符串值相等(使用 equals() 方法进行确定),那么就会返回 String Pool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用。
equals相等(指向同一引用)的字符串在常量池中永远只有一份。

intern() 方法返回字符串对象的规范化表示形式,即一个字符串,内容与此字符串相同,但一定取自具有唯一字符串的池。
它遵循以下规则:对于任意两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true。

new String(“abc”)
运行期(new出来的 new String(s))才能确定的就存储在中。即jvm会直接在heap中非String constant pool 中创建字符串对象,然后把该对象引用返回给String b(并且不会把"abc” 加入到String constant pool中)。

new就是在堆中创建一个新的String对象,不管"abc"在内存中是否存在,都会在堆中开辟新空间。
equals相等的字符串在堆中可能有多份。

对于 new String(“abc”),使用这种方式一共会创建两个字符串对象(前提是 String Pool 中还没有 “abc” 字符串对象)。这两个字符串对象指向同一个value数组

“abc” 属于字符串字面量,因此编译时期会在 String Pool 中创建一个字符串对象,指向这个 “abc” 字符串字面量;

而使用 new 的方式会在堆中创建一个字符串对象。

String s1 = new String("aaa");
String s2 = new String("aaa");
System.out.println(s1 == s2);           // false,指向堆内不同引用
String s3 = s1.intern();
String s4 = s1.intern();
System.out.println(s3 == s4);           // true,指向字符串常量池中相同引用
String s5 = "bbb";
String s6 = "bbb";
System.out.println(s5 == s6);  			// true,指向字符串常量池中相同引用

 

字符串拼接方式 & 比较

 

拼接方式

"+" 拼接
加号拼接字符串jvm底层其实是调用StringBuilder来实现的,也就是说”a” + “b” + "c"等效于下面的代码片。

// String d = "a"+"b"+"c";等效于
String d = new StringBuilder().append("a").append("b").append("c").toString();

但并不是说直接用“+”号拼接就可以达到StringBuilder的效率了,因为每次使用 "+"拼接 都会新建一个StringBuilder对象并且最后toString()方法还会生成一个String对象。在循环拼接十万次的时候,就会生成十万个StringBuilder对象,会产生大量内存消耗

concat 拼接
concat其实就是申请一个char类型的buf数组,将需要拼接的字符串都放在这个数组里,最后再创建并返回一个新的String对象

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);}

StringBuilder/StringBuffer append
这两个类实现append的方法都是调用父类AbstractStringBuilder的append方法,只不过StringBuffer是的append方法加了sychronized关键字因此是线程安全的。append代码如下,他主要也是利用char数组保存字符,通过ensureCapacityInternal方法来保证数组容量可用还有扩容。

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;}

他扩容的方法的代码如下,可见,当容量不够的时候,数组容量右移1位(也就是翻倍)再加2

private int newCapacity(int minCapacity) {// overflow-conscious codeint newCapacity = (value.length << 1) + 2;if (newCapacity - minCapacity < 0) {newCapacity = minCapacity;}return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)? hugeCapacity(minCapacity): newCapacity;}

 

 

拼接比较

拼接方式+concatStringBuilder/StringBuffer
原理jvm采用append优化,每次执行都会新建一个StringBuilder和String对象申请一个char类型的buf数组,将需要拼接的字符串都放在这个数组里,最后再创建并返回一个新的String对象利用char数组保存字符,对Stringbuilder/StringBuffer直接修改,不生成新的String对象
比较最慢且效率最低,适用于书写方便场景适用于少量字符串拼接(会新建String对象)适用于多个字符串拼接,当不考虑线程的情况下,StringBuilder效率比StringBuffer(Synchronized)高

 

String a = “a”+“b”+“c”;在内存中创建了几个对象?

String a=“a”+“b”+"c"在内存中创建几个对象?——1个对象
String a = “a”+“b”+"c"经过编译器优化后得到的效果为String a = “abc”


java编译期会进行常量折叠全字面量字符串相加是可以折叠为一个字面常量,而且是进入常量池的


在JAVA虚拟机(JVM)中存在着一个字符串池,其中保存着很多String对象,并且可以被共享使用因此它提高了效率。由于String类是final的,它的值一经创建就不可改变,因此我们不用担心String对象共享而带来程序的混乱。字符串池由String类维护,我们可以调用intern()方法来访问字符串池。


对于String a=“abc”;,这行代码被执行的时候,JAVA虚拟机首先在字符串池中查找是否已经存在了值为"abc"的这么一个对象,它的判断依据是String类equals(Object obj)方法的返回值。如果有,则不再创建新的对象,直接返回已存在对象的引用;如果没有,则先创建这个对象,然后把它加入到字符串池中,再将它的引用返回。


字符串内部拼接:只有使用引号包含文本的方式创建的String对象之间使用“+”连接产生的新对象才会被加入字符串池中。对于所有包含new方式新建对象(包括null)的“+”连接表达式,它所产生的新对象都不会被加入字符串池中,

String s=new String(“abc”)创建了几个对象?——2个对象
new String(“abc”)可看成"abc"(创建String对象)和new String(String original)(String构造器,创建String对象)2个对象


我们正是使用new调用了String类的上面那个构造器方法创建了一个对象,并将它的引用赋值给了str变量。同时我们注意到,被调用的构造器方法接受的参数也是一个String对象,这个对象正是"abc"。

 

这篇关于String的原理 不可变性的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/253637

相关文章

ShardingProxy读写分离之原理、配置与实践过程

《ShardingProxy读写分离之原理、配置与实践过程》ShardingProxy是ApacheShardingSphere的数据库中间件,通过三层架构实现读写分离,解决高并发场景下数据库性能瓶... 目录一、ShardingProxy技术定位与读写分离核心价值1.1 技术定位1.2 读写分离核心价值二

深度解析Python中递归下降解析器的原理与实现

《深度解析Python中递归下降解析器的原理与实现》在编译器设计、配置文件处理和数据转换领域,递归下降解析器是最常用且最直观的解析技术,本文将详细介绍递归下降解析器的原理与实现,感兴趣的小伙伴可以跟随... 目录引言:解析器的核心价值一、递归下降解析器基础1.1 核心概念解析1.2 基本架构二、简单算术表达

深入浅出Spring中的@Autowired自动注入的工作原理及实践应用

《深入浅出Spring中的@Autowired自动注入的工作原理及实践应用》在Spring框架的学习旅程中,@Autowired无疑是一个高频出现却又让初学者头疼的注解,它看似简单,却蕴含着Sprin... 目录深入浅出Spring中的@Autowired:自动注入的奥秘什么是依赖注入?@Autowired

从原理到实战解析Java Stream 的并行流性能优化

《从原理到实战解析JavaStream的并行流性能优化》本文给大家介绍JavaStream的并行流性能优化:从原理到实战的全攻略,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的... 目录一、并行流的核心原理与适用场景二、性能优化的核心策略1. 合理设置并行度:打破默认阈值2. 避免装箱

C++ STL-string类底层实现过程

《C++STL-string类底层实现过程》本文实现了一个简易的string类,涵盖动态数组存储、深拷贝机制、迭代器支持、容量调整、字符串修改、运算符重载等功能,模拟标准string核心特性,重点强... 目录实现框架一、默认成员函数1.默认构造函数2.构造函数3.拷贝构造函数(重点)4.赋值运算符重载函数

Python中的filter() 函数的工作原理及应用技巧

《Python中的filter()函数的工作原理及应用技巧》Python的filter()函数用于筛选序列元素,返回迭代器,适合函数式编程,相比列表推导式,内存更优,尤其适用于大数据集,结合lamb... 目录前言一、基本概念基本语法二、使用方式1. 使用 lambda 函数2. 使用普通函数3. 使用 N

redis数据结构之String详解

《redis数据结构之String详解》Redis以String为基础类型,因C字符串效率低、非二进制安全等问题,采用SDS动态字符串实现高效存储,通过RedisObject封装,支持多种编码方式(如... 目录一、为什么Redis选String作为基础类型?二、SDS底层数据结构三、RedisObject

MyBatis-Plus 与 Spring Boot 集成原理实战示例

《MyBatis-Plus与SpringBoot集成原理实战示例》MyBatis-Plus通过自动配置与核心组件集成SpringBoot实现零配置,提供分页、逻辑删除等插件化功能,增强MyBa... 目录 一、MyBATis-Plus 简介 二、集成方式(Spring Boot)1. 引入依赖 三、核心机制

redis和redission分布式锁原理及区别说明

《redis和redission分布式锁原理及区别说明》文章对比了synchronized、乐观锁、Redis分布式锁及Redission锁的原理与区别,指出在集群环境下synchronized失效,... 目录Redis和redission分布式锁原理及区别1、有的同伴想到了synchronized关键字

Linux中的HTTPS协议原理分析

《Linux中的HTTPS协议原理分析》文章解释了HTTPS的必要性:HTTP明文传输易被篡改和劫持,HTTPS通过非对称加密协商对称密钥、CA证书认证和混合加密机制,有效防范中间人攻击,保障通信安全... 目录一、什么是加密和解密?二、为什么需要加密?三、常见的加密方式3.1 对称加密3.2非对称加密四、