String 特性详解

目录

一、String的不可变性

        1. 原理

2. 不可变性

3. 不可变的好处

4. 不可变的缺点:

5. String “改变”的真相

6. String 不可变特性的破解

二、String 直接赋值和使用new的区别

1. 创建 String 对象的两种方式

2. 内存中的存储

         3. Java常量池:

4. String 直接赋值和使用new的区别

(1)String直接赋值

(2)String使用new 

三、字符串拼接方式及比较

1. "+" 拼接

2. concat 拼接

3. StringBuilder/StringBuffer append

4. 拼接比较

四、注意事项

五、常问案例分析


一、String的不可变性

1.原理

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

public final class String
    implements 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 String
    implements 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;
}

2. 不可变性

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

补充:不可变的实现:
String类被final修饰,保证类不被继承。如果String类可以被继承,子类可能会覆盖String的方法并改变其

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

3. 不可变的好处

(1) 安全性

         String的不可变性保证了其值不会被意外或恶意修改。在一些安全性敏感的场景中,如密码、加密算法等,使用不可变的String可以防止敏感信息被修改。

       String类在Java中被广泛使用,包括作为参数传递给方法、作为键或值存储在集合中等。由于String是不可变的,它可以被安全地共享和传递,不会受到外部修改的影响。如果String可以被继承并改变其值,那么可能会导致安全性问题和意外的行为。

(2)线程安全性
         同一个字符串实例可以被多个线程共享,由于String是不可变的,多个线程可以同时访问和共享String对象,而无需担心并发修改的问题,字符串的不变性保证字符串本身便是线程安全的。

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

          由于String是不可变的,可以被安全地缓存和重用。由于String不可变,可以进行一些性能优化。例如,字符串常量池(String Pool)中相同的字符串字面量只会存在一份,可以重复使用相同的字符串字面量,避免重复创建相同的字符串对象,从而节省内存和提高性能。

4.不可变的缺点:

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

5. 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"并没有发生改变,仍保存在内存中。如下图所示:

34c908d413a9440f8e8c119c05bcd677.png

 6.  String 不可变特性的破解

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

public static void reflectString() throws Exception{
    // 创建字符串"ABCDEF"并赋给引用s
    String s = "ABCDEF";
    System.out.println("s = " + s);	// s = ABCDEF
 
    Field 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 直接赋值和使用new的区别

1. 创建 String 对象的两种方式

a239812bf34d46a1b25585322ee6c8e7.png

a4b6b3ff23074301aabb247ea5dd1993.png

178c5a3c97d94ff6ac91503ffba5f917.png

2. 内存中的存储

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

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

3.Java常量池:

       类在加载完成之后,会在内存中存储类中的一些字面量(本身即是值如10,“abc”),对于字符串常量来说,Java会保证常量池中的字面量不会有多个副本,也就是说在常量池中的字符串不可能有两个字符串是相同的,但是Java代码中可能不同的变量的值是相同的,那么在编译期间,这两个变量值所在地址是相同的。而且Java在编译期间会对字符串进行一定的处理,如果一个字符串采用拼接的方式,并且拼接的内容都是字面量的话,那么会自动将字符串先拼接完再赋值,如果常量池中已经有了拼接完成之后的字面量,那么此变量的值的地址就是常量池中的完整字符串的地址。需要注意的是,String在赋值完成之后修改,是会产生新的变量的。

4. String 直接赋值和使用new的区别

(1)String直接赋值

String str = "reeves";
str = "ABC";

//String通过直接赋值的方式,实际上在常量池中存储了"reeves"和"ABC"两个字面值,
在字符串变量赋予新的值的时候并不会改变原先存储的值,
它会再新建一个字符串,而在栈中变量存储的值的地址是变了的。

       String str1 = “ABC”;可能创建一个或者不创建对象,如果”ABC”这个字符串在java String池里不存在,会在JVM的字符串池里创建一个String对象(“ABC”),然后str1指向这个内存地址,无论以后用这种方式创建多少个值为”ABC”的字符串对象,始终只有一个内存地址被分配,之后的都是String的拷贝,Java中称为“字符串驻留”,所有的字符串常量都会在编译之后自动地驻留。

       编译期已经创建好(直接用双引号定义的"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。

(2)String使用new 

String str2 = new String("ABC");

//上面语句实际上创建了2个字符串对象,
一个是“ABC”这个直接量对应的对象,
一个是new String()构造器返回的字符串对象。

        String str2 = new String(“ABC”);至少创建一个对象,也可能两个。因为用到new关键字,肯定会在heap中创建一个str2的String对象,它的value是“ABC”。同时如果这个字符串再java String池里不存在,会在java池里创建这个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,指向字符串常量池中相同引用

三、字符串拼接方式及比较

1. "+" 拼接

加号拼接字符串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对象,会产生大量内存消耗。
 

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

3. 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 code
        int newCapacity = (value.length << 1) + 2;
        if (newCapacity - minCapacity < 0) {
            newCapacity = minCapacity;
        }
        return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
            ? hugeCapacity(minCapacity)
            : newCapacity;
    }

4. 拼接比较

73b47d7c00354b33b1f56927bbe564a7.png

四、注意事项

       1. 在JVM里,考虑到垃圾回收(Garbage Collection)的方便,将heap(堆)划分为三部分:young generation(新生代)、tenured generation (old generation)(旧生代)、permanent generation(永生代)。 字符串为了解决字符串重复问题,生命周期长,存于pergmen中。

       2.

public class Test2 {
    public static void main(String[] args) {
        String s1 = new String("ABC");
        String s2 = new String("ABC");
        System.out.println(s1 == s2); //false
        System.out.println(s1.equals(s2)); //true
    }
}

   如果将一个字符串连接表达式赋给字符串变量,如果这个字符串连接表达式的值可以在编译时就确定下来,那么JVM会在编译时确定字符串变量的值,并让它指向字符串池中对应的字符串。

       但是如果程序使用了变量或者调用了方法,那就只有在运行时才能确定该字符串表达式的值,因此无法在编译时确定值,无法利用JVM的字符串池。

public class Test2 {
    public static void main(String[] args) {
        String s1 = "ABCDEF";
        String s2 = "ABC" + "DEF";
        System.out.println(s1 == s2);//true,可以在编译时确定
 
        String s3 = "DEF";
        String s4 = "ABC" + s3;
        System.out.println(s1 == s4);//false,无法在编译时确定
 
        final String S5 = "DEF";
        String s6 = "ABC" + S5;
        System.out.println(s1 == s6);//true,使用final关键字,在编译时将对S5进行宏替换
    }
}

       当程序中需要使用字符串、基本数据类型包装实例时,应该尽量使用字符串直接量、基本数据类型的直接量,避免通过new String()、new Integer()等形式来创建字符串、基本数据类型包装类实例,这样能保证较好的性能。

五、常问案例分析


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"。

d4a3bb0f7e864836be3fea451b9c1961.png

0cff8aa4e03e4266b31642d90f1c3eaa.png

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值