字符串拼接及创建的案例分析案例一

String a = "test";
String b = "test";
System.out.println(a.equals(b)); // true
System.out.println(a == b); // true
System.out.println(System.identityHashCode(a)); // 1639705018
System.out.println(System.identityHashCode(b)); // 1639705018

案例二

String a = "test";
String b = new String("test");
System.out.println(a.equals(b)); // true
System.out.println(a == b); // false
System.out.println(System.identityHashCode(a)); // 1639705018
System.out.println(System.identityHashCode(b)); // 1627674070

案例三

String a = new String("test");
String b = new String("test");
System.out.println(a.equals(b)); // true
System.out.println(a == b); // false
System.out.println(System.identityHashCode(a)); // 1639705018
System.out.println(System.identityHashCode(b)); // 1627674070

案例四

String a = "test";
String b = "te"+"st";
System.out.println(a.equals(b)); // true
System.out.println(a == b); // true
System.out.println(System.identityHashCode(a)); // 1639705018
System.out.println(System.identityHashCode(b)); // 1639705018

与案例一不同的是,b的值由两个引号相加所得,但是结果和案例一是一样的,a与b指向的都是字符串常量池中的同一对象,这是因为形如”te”+”st”这种由多个字符串常量连接而成的字符串在编译期被认为是一个字符串常量

案例五

String a = new String("test");
String b = new String("te") + new String("st");
System.out.println(a.equals(b)); // true
System.out.println(a == b); // false
System.out.println(System.identityHashCode(a)); // 1639705018
System.out.println(System.identityHashCode(b)); // 1627674070

案例六

String a = "test";
String b = "te";
String c = "st";
String d = b + c;
System.out.println(a.equals(d)); // true
System.out.println(a == d); // false
System.out.println(System.identityHashCode(a)); // 1639705018
System.out.println(System.identityHashCode(d)); // 1627674070

对象d的初始化方式似乎跳出了之前所说的两种初始化方式,是由两个存在于字符串常量池中的字符串对象连接生成,但其实此时的d的初始化方式只是复杂了一些,并没有脱离之前所说的两种初始化方式,具体步骤如下:

也就是说在这个案例中字符串常量池中创建了三个对象,堆中创建了三个对象

案例七

String a = "test";
final String b = "te";
final String c = "st";
String d = b + c;
System.out.println(a.equals(d)); // true
System.out.println(a == d); // true
System.out.println(System.identityHashCode(a)); // 1639705018
System.out.println(System.identityHashCode(d)); // 1639705018

与案例六不同的是b、c都加了final修饰,在这种情况下,与案例四一样,编译器将b+c视为了字符串常量,所以d指向的是在字符串常量池中已存在的”test”对象

总结String、StringBuilder、StringBuffer

+号连接字符串的操作是通过StringBuilder的append和toString方法实现的

垃圾收集

当一个对象没有引用指向时java字符串拼接,垃圾收集器便会对它进行收集操作。看下面的一个事例:

public class ImmutableStrings
{
    public static void main(String[] args) {
        String one = "someString";
        String two = new String("someString");
        one = two = null;
    }
}

当 one = two = null时,只有一个对象会被回收,String对象总是有来自字符串常量池的引用,所以不会被回收

大家可能都知道String.intern()的作用,调用它时,如果常量池中存在当前字符串, 就会直接返回当前字符串. 如果常量池中没有此字符串, 会将此字符串放入常量池中后, 再返回。 但是一些稍复杂的例子,可能就说不清它的运行结果,而且这结果跟jdk版本有关。本篇通过理论和例子让你对String.intern()的有更深入的理解,以及其中的原理。这不仅仅是笔试面试中常考得点,也是对技术深入探究的态度。

intern方法常量池

Class文件中除了有关的版本、字段、方法、接口等描述信息外,还有一项信息是常量池java字符串拼接,用于存放编译期生成的各种字面量和符号引用。其中字符串池(又名字符串规范化)是一个用一个共享的String替换几个具有相同值但不同身份的对象。你可以通过Map来自己实现此目标(根据要求可能有软或弱引用),或者可以使用String.intern()由JDK提供的方法。

Java 6中的String.intern()Java 7中的String.intern()

在之前的结论中,可以看到String对象被加入进字符串常量池中的条件似乎是必须在编译器可表示为字符串常量,而在运行期间确定的字符串对象时无法加入的,但是也不是绝对的,String类中提供的intern方法就是一种在运行期间加入字符串常量池的一种方法。

当调用intern方法时,如果字符串常量池中已经包含一个内容相同的String对象,则返回池中的对象。否则,将在字符串常量池中添加一个该对象在堆中引用地址,并返回该对象地址值(在jdk6中,是直接将此String对象添加到池中,并返回此String对象)。

如以下案例

String a = "test";
System.out.println(System.identityHashCode(a)); // 1639705018
a = a.intern();
System.out.println(System.identityHashCode(a)); // 1639705018

此时a初始化完成后指向的对象的位置是在字符串常量池中,执行a = a.intern()后,指向位置任是在字符串常量池中;所以看到打印的hashcode是一样的;

String a = new String("test");
System.out.println(System.identityHashCode(a)); // 1639705018
a = a.intern();
System.out.println(System.identityHashCode(a)); // 1627674070

此时a初始化完成后指向的对象的位置是在堆上的,同时在字符串常量池中有一个相同内容的String对象,执行a = a.intern()后,指向位置是在字符串常量池中;

String a = new String("te") + new String("st");
System.out.println(System.identityHashCode(a)); // 1639705018
a = a.intern();
System.out.println(System.identityHashCode(a)); // 1639705018

String的创建及拼接String的创建

字符串不属于基本类型,但是可以像基本类型一样,直接通过字面量赋值,当然也可以通过new来生成一个字符串对象。不过通过字面量赋值的方式和new的方式生成字符串有本质的区别:

excel 单元格字符拼接_stringbuffer拼接字符_java字符串拼接

String的拼接

直接多个字符串字面量值“+”操作,编译阶段直接会合成为一个字符串。

//等价于直接赋值"hello world"
String s = "hello "+"world";

变量进行字符串拼接的方式

String s1="world";
String s = "hello "+s1;

通过反编译可知以上代码相当于

String s1="world";
StringBuilder sb=new StringBuilder("hello");
sb.append(s1);
String s = sb.toString();

实际上是先创建StringBuilder,然后使用append()拼接,最后toString()赋值给s

变量采用final进行修饰

final String s1="world";
String s = "hello "+s1;

将s1用final修饰,则拼接也是在编译时完成,编译时会先把用常量值替换s1,再就是和第一种情况相同

String s=new String(“hello “) + new String(“world”);

这种也是用StringBuilder拼接

public class StringTest01 {
    public static void main(String[] args) {
        String baseStr = "baseStr";
        final String baseFinalStr = "baseStr";
        String str1 = "baseStr01";
        String str2 = "baseStr"+"01";
        String str3 = baseStr + "01";
        String str4 = baseFinalStr+"01";
        String str5 = new String("baseStr01").intern();
        System.out.println(str1 == str2);
        System.out.println(str1 == str3);
        System.out.println(str1 == str4);
        System.out.println(str1 == str5);
    }
}

按顺序依次讲解:

public class InternTest {
    public static void main(String[] args) {
        String str2 = new String("str")+new String("01");
        str2.intern();
        String str1 = "str01";
        System.out.println(str2==str1);
    }
}

在java 1.6运行结果:false 在java 1.7以及之后运行结果:true

因为str2和str1分别指向堆中对象和常量池中字符串,所以返回false。

所以,str2.intern();这句话不是没任何影响的,它会在常量池中生成一个对堆中的“str01”的引用,而在进行字面量赋值的时候,常量池中已经存在,所以直接返回该引用即可,因此str1和str2都指向堆中的字符串,返回true。

public class InternTest01 {
    public static void main(String[] args) {
        String str1 = "str01";
        String str2 = new String("str")+new String("01");
        str2.intern();
        System.out.println(str2 == str1);
    }
}

将str1的定义放在前面,则java 1.6,1.7都返回false

因为这次str2.intern();执行时,常量池中已经有了”str01″, 因此str1和str2引用不同。

String对象的创建和字符串常量池的放入

那到底什么时候会创建String 对象?什么时候引用放入到字符串常量池中呢?先需要提出三个常量池的概念:

静态常量池

常量池表(Constant Pool table,存放在Class文件中),也可称作为静态常量池,里面存放编译器生成的各种字面量和符号引用。

其中有两个重要的常量类型为CONSTANT_String_info和CONSTANT_Utf8_info类型

运行时常量池

运行时常量池属于方法区的一部分,常量池表中的内容会在类加载时存放在方法区的运行时常量池,运行时常量池相比于Class文件常量池一个重要特征是动态性,运行期间也可以将新的常量放入到 运行时常量池中。

字符串常量池执行过程

类加载时,会把静态常量池中的内容存放到方法区中的运行时常量池中,其中CONSTANT_Utf8_info类型在类加载的时候就会全部被创建出来,即说明了加载类的时候,那些字符串字面量会进入到当前类的运行时常量池,但是此时StringTable(字符串常量池)并没有相应的引用,在堆中也没有相应的对象产生;(No initialization)

遇到ldc字节码指令(该指令将int、float或String型常量值从常量池中推送至栈顶)之前会触发解析阶段,进入到解析阶段,若在解析的过程中发现StringTable已经有与CONSTANT_String_info一样的引用,则返回该引用,若没有,则在堆中创建一个对应内容的String对象,并在StringTable中保存创建的对象的引用,然后返回;

下面给出几个具体实例,来说下这个过程:

// 字面量的形式创建字符串
public class test{
  public static void main(String[] args){
    String name = "HB";
    String name2 = "HB";
  }
}

通过javap 反编译后的字节码代码如下所示

2 = String  #14 
#14 = utf8    HB
……
public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=3, args_size=1
         0: ldc           #2  // String HB
         2: astore_1
         3: ldc           #2  // String HB
         5: astore_2
         6: return
……

new 创建字符串
public class test2{
 public static void main(String[] args){
    String name = new String("HB");
    String name2 = new String("HB");
  }
}

通过javap 反编译后的字节码代码如下所示

 public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=3, args_size=1
         0: new           #2  // class java/lang/String
         3: dup
         4: ldc           #3 // String HB
         6: invokespecial #4 // Method java/lang/String."":(Ljava/lang/String;)V
         9: astore_1
        10: new           #2 // class java/lang/String
        13: dup
        14: ldc           #3 // String HB
        16: invokespecial #4 // Method java/lang/String."":(Ljava/lang/String;)V
        19: astore_2
        20: return

———END———
限 时 特 惠:本站每日持续更新海量各大内部创业教程,一年会员只需128元,全站资源免费下载点击查看详情
站 长 微 信:jiumai99

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注