string的基本特性

  • string:字符串,使用一对""来表示。
  • String s1=“hello”;//字面量的方式定义
  • String S2=new String(“hello”);
  • string声明为final的,不可以被继承
  • string实现了seriaizable接口,表示字符串是支持序列化的。实现了comparable接口:表示string是可以比较大小的
  • string在jdk8及以前底层定义了 final char[] value用于存储字符串数据,jdk9时改为byte[]

一个char是两个字节,即16位,而一些常用字母和符号在byte中就可以存储下,用char来定义的话会浪费近一半的内存

  • string:代表不可变的字符序列,简称:不可变性
  • 当对字符串重新赋值时,需要重写指定内存区域进行赋值,不能使用原有的value赋值
  • 当对现有字符串进行连接操作时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值
  • 当调用string的replace()方法修改指定字符或者字符串时,也需要重新指定内存区域赋值,不能使用原有的value进行赋值
  • 通过字面量的方式(区别于new)给一个字符串赋值,此时的字符串值声明在字符串常量池中

字符串常量池中是不允许存放相同的字符串的

  • string的string pool是一个固定大小的hashtable,默认大小长度是1009,如果放进string pool的string非常多,就会造成hash冲突严重,从而导致链表会很长,而链表长了之后直接会造成的影响就是当调用string.intern时性能会大幅下降
  • 使用-XX:StringTableSize可设置stringtable的长度
  • 在jdk6中stringtable是固定的,就是1009的长度,所以如果常量池中的字符串过多就会导致效率下降很快,stringtablesize设置没有要求
  • 在jdk7中,stringtable的长度默认值是60013,
  • jdk8开始,设置stringtable,1009是可设置的最小值

哈希碰撞的减少,可以提高性能

string的内存分配

  • 在java语言中有8种基本数据类型和一种比较特殊的类型string,这些类型为了他们在运行过程中速度更快,更节省内存,都提供了一种常量池的概念
  • 常量池就类似一个java系统级别提供的缓存,8中基本数据类型的常量池都是系统协调的,string类型的常量池比较特殊,它的主要使用方法有两种
  • 直接使用双引号声明出来的string对象会直接存储在常量池中。比如 String info=“cxf”;
  • 如果不是双引号声明的string对象,也可以使用string提供的intern()方法。这个后面重点谈

string的基本操作

先来看如下的代码

public class test {
    public static void main(String[] args) {
        System.out.println();
        System.out.println("1");//2829
        System.out.println("2");//2830
        System.out.println("3");//2831
        System.out.println("4");
        System.out.println("5");
        System.out.println("6");
        System.out.println("7");
        System.out.println("8");
        System.out.println("9");
        System.out.println("10");
        //如下的字符串“1”到“10”不会再次加载到stringtable
        System.out.println("1");//2838
        System.out.println("2");//2838
        System.out.println("3");
        System.out.println("4");
        System.out.println("5");
        System.out.println("6");
        System.out.println("7");
        System.out.println("8");
        System.out.println("9");
        System.out.println("10");//2838
    }
}

使用idea的debug功能来逐行查看java.lang.string的count的变化

整体变化如行后注释

java语言规范里要求完全相同的字符串字面量,应该包含同样的unicode字符序列(包含一份码点序列的常量),必须是指向同一个string类实例,即相同的字符串只会在stringtable中加载一次

再看这段代码分析下内存指向

package xx.cxf.feifei.test;

/**
 * @author feifei
 * @Date 2021/08/2021/8/9 15:45:24
 * @Version 1.0
 */
public class test {
    public static void main(String[] args) {
        int i=1;
        Object obj=new Object();
        test test = new test();
        test.foo(obj);
    }

    private void foo(Object parm) {
        String str=parm.toString();
        System.out.println(str);
    }
}

JAVA里的String口 java语言string_java

字符串拼接操作

  1. 常量与常量的拼接结果在常量池,原理是编译器优化
    这里属于是在.java文件编译成.class文件时的优化,这里通过反编译后的字节码文件和一段代码来看
package xx.cxf.feifei.test;
/**
 * @author feifei
 * @Date 2021/08/2021/8/9 15:45:24
 * @Version 1.0
 */
public class test {
    public static void main(String[] args) {
        String cxf="c"+"xf";
        String cxf1="cxf";
        System.out.println(cxf==cxf1);//true
        System.out.println(cxf.equals(cxf1));//true
    }
}

string的==同样是通过判断地址的 equals就不多了 重写的先地址后内容

我们再来看反编译后的字节码文件

JAVA里的String口 java语言string_JAVA里的String口_02

  1. 常量池中不会存在相同的常量
    这个在上边的字符串的基本操作中也有证明
  2. 只要其中有一个是变量,结果就在堆中。变量拼接的原理是stringbuilder
    同样的来看一段代码和运行结果
package xx.cxf.feifei.test;
/**
 * @author feifei
 * @Date 2021/08/2021/8/9 15:45:24
 * @Version 1.0
 */
public class test {
    public static void main(String[] args) {
        String s1="javaee";
        String s2="hadoop";

        String s3="javaeehadhop";
        String s4="javaee"+"hadoop";//编译期优化
        //如果拼接符号的前后出现了变量。则相当于在堆空间new string(),具体的内容位拼接的结果:javaeehadoop
        String s5=s1+"hadoop";
        String s6="javaee"+s2;
        String s7=s1+s2;

        System.out.println(s3==s4);//true
        System.out.println(s3==s5);//false
        System.out.println(s3==s6);//false
        System.out.println(s3==s7);//false
        System.out.println(s5==s6);//false
        System.out.println(s5==s7);//false
        System.out.println(s6==s7);//false
        //intern()判断字符串常量池中是否存在javaeehadoop值,如果存在,则返回常量池中javaeehadoop的地址
        //如果字符串常量池中不存在javaeehadoop,则在常量池中加载一份javaeehadoop,并返回次对象的地址
        String s8=s6.intern();
        System.out.println(s3==s8);//true
    }
}
  1. 如果拼接的结果调用intern()方法,则主动将常量池中还没有的字符串对象放入池中,并返回此对象地址
string拼接的底层原理

从上边的代码测试中可以得出如果拼接符号的前后出现了变量。则相当于在堆空间new string(),但具体的底层细节又是怎么样的呢

先看这段代码:

package xx.cxf.feifei.test;
/**
 * @author feifei
 * @Date 2021/08/2021/8/9 15:45:24
 * @Version 1.0
 */
public class test {
    public static void main(String[] args) {
        String str="cx";
        String str1="f";
        String str2="cxf";
        /*
        str+str1的执行细节
        StringBuilder s = new StringBuilder();
        s.append("cx");
        s.append("f");
        s.toString();------>是类似于  new string("ab")
         */
        String str3=str+str1;
        System.out.println(str2==str3);//false

    }
}

同样的可以看看反编译后的字节码

0 ldc #7 <cx>
 2 astore_1
 3 ldc #9 <f>
 5 astore_2
 6 ldc #11 <cxf>
 8 astore_3
 9 new #13 <java/lang/StringBuilder>
12 dup
13 invokespecial #15 <java/lang/StringBuilder.<init> : ()V>
16 aload_1
17 invokevirtual #16 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
20 aload_2
21 invokevirtual #16 <java/lang/StringBuilder.append : (Ljava/lang/String;)Ljava/lang/StringBuilder;>
24 invokevirtual #20 <java/lang/StringBuilder.toString : ()Ljava/lang/String;>
27 astore 4
29 getstatic #24 <java/lang/System.out : Ljava/io/PrintStream;>
32 aload_3
33 aload 4
35 if_acmpne 42 (+7)
38 iconst_1
39 goto 43 (+4)
42 iconst_0
43 invokevirtual #30 <java/io/PrintStream.println : (Z)V>
46 return

补充:在jdk5.0之后使用的是stringbuilder在jdk5.0之前使用的是stringbuffer

那么是不是所有的非字面量的拼接底层都是通过stringbuilder来进行的呢

同样的我们来看一段代码和对应的字节码文件

package xx.cxf.feifei.test;
/**
 * @author feifei
 * @Date 2021/08/2021/8/9 15:45:24
 * @Version 1.0
 */
public class test {
    public static void main(String[] args) {
        /*
        字符串拼接操作不一定使用的是stringbuilder
        如果拼接符号左右两边都是字符串常量或常量的引用,则任然使用编译期优化,即非stringbuilder的方式
         */
        final String s="cx";
        final String s1="f";
        String ss="cxf";
        String s3=s+s1;
        System.out.println(ss==s3);//true
    }
}

JAVA里的String口 java语言string_System_03


从字节码指令中可以看出在编译期间,已经将其拼接好了,即编译优化

由此可以得出结论:

如果拼接字符串时,等号的两边都是常量引用或者直接就是字面量的拼接,则会在编译期间进行优化,直接进行拼接

final修饰类、方法、基本数据类型、引用数据类型的量的结构时,能使用final就建议使用上

字符串拼接和append操作的效率对比

下边来看两端测试代码

package xx.cxf.feifei.test;
/**
 * @author feifei
 * @Date 2021/08/2021/8/9 15:45:24
 * @Version 1.0
 */
public class test {
    public static void main(String[] args) {
        String cxf="";
        long start = System.currentTimeMillis();
        for (int i = 0; i <100000 ; i++) {
            cxf+="a";
        }
        long end = System.currentTimeMillis();
        System.out.println("花费的时间为:"+(end-start));//5334
    }
}
package xx.cxf.feifei.test;
/**
 * @author feifei
 * @Date 2021/08/2021/8/9 15:45:24
 * @Version 1.0
 */
public class test {
    public static void main(String[] args) {
        StringBuilder stringBuilder = new StringBuilder();
        long start = System.currentTimeMillis();
        for (int i = 0; i <100000 ; i++) {
            stringBuilder.append("a");
        }
        long end = System.currentTimeMillis();
        System.out.println("花费的时间为:"+(end-start));//6
    }
}

每次进行string的拼接操作的时候,每次不仅会创建一个stringbuilder的对象,同样的stringbuilder.tostring还会创建一个string对象,而stringbuilder的操作只需要创建一个stringbuilder的对象,所以效率是远高于直接拼接的

同样的每次进行拼接都会创建对象,新的出现旧的就不在使用了,内存中创建了较多的stringbuilder和string的对象,一方面是内存的占用另一方面就是gc的话 还是要花费相应的时间

注意:

在实际的开发中,如果基本确定要前前后后添加的字符串长度不超过某个限定值highlevel的情况下,建议使用构造器实例化,避免不断的扩容,

stringbuilder s=new stringbuilder(highlevel);

intern()的使用

如果不是双引号声明的string对象,可以使用string提供的intern方法,intern方法会从字符串常量池中查询当前字符串是否存在,若不存在就会将当前字符串放入常量池中。

  • 比如 string myinfo=new string(“i love cxf”).intern();

也就是说,如果任意字符串上调用string.intern方法,那么其返回结果所指向的哪个类实例,必须和直接以常量形式出现的字符串实例完全相同。因此,下列表达式的值必定是true

(“a”+“b”+“c”).intern()==“abc”

通俗点讲,interned string 就是确保字符串在内存中只有一份拷贝,这样可以节约内存空间,加快字符串操作任务的执行速度。注意这个值会被存放在字符串内部池(string intern pool)

如何保证变量s指向的是字符串常量池中的数据呢?

有两种方式

方式一:

直接以字面量的形式定义

string s=“cxf”

方式二:

string s=new string(“cxf”).intern();

面试题

1.题目 new string(“ab”)会创建几个对象?

两个

一个对象是:new关键字在堆空间创建的

另一个对象是:字符串常量池中的对象 字节码指令ldc

2.new string(“a”)+new string(“b”)呢?

对象1:new stringbuilder()

对象2:new string(“a”)

对象3:常量池"a"

对象4:new string(“b”)

对象5:常量池中 的"b"

深入剖析: stringbuilder的tostring()

对象6:new string(“ab”)

强调以下tostring的调用,在字符串常量池中,没有生成"ab"

同样的如下代码进行分析

package xx.cxf.feifei.test;
/**
 * @author feifei
 * @Date 2021/08/2021/8/9 15:45:24
 * @Version 1.0
 */
public class test {
    public static void main(String[] args) {
        String s=new String("1");
        s.intern();
        String s2="1";
        System.out.println(s==s2);//false

        String s3=new String("1")+new String("1");
        s3.intern();
        String s4="11";
        System.out.println(s3==s4);//true

    }
}

对于s==s2为false,首先在创建的过程中会在堆中创建string对象,同样的也会在常量池中创建“1”对象,指向方向是s指向堆中的对象,堆中的对象在指向常量池中的对象,s.intern(); 此时常量池中是存在

“1”的返回的就是常量池的对象地址,但此时并没有接收,s指向的仍然是堆中的string,此时s指向的堆中的对象,s2指向的是常量池中的“1”,所以结果为false

而对于s3和s4的比较

首先s3的指向是最后的stringbuilder.tostring后的对象,此时常量池中存在"1" ,stringbuilder.tostring的对象是不会在常量池中的,所以常量池中并没有“11”,调用s3的intern()将“11”,可能加载进常量池,此时常量池中有“11”了,但注意此时的“11”并不是真的“11”,他只是stringbuilder.tostring的返回值的引用,为了堆空间的高效利用具体可查看如下的图,这个是1.7的,jdk1.6的则不相同,后面会叙述

JAVA里的String口 java语言string_java_04

同样的巩固下,看下面的代码,你是否能判断分析正确

package xx.cxf.feifei.test;
/**
 * @author feifei
 * @Date 2021/08/2021/8/9 15:45:24
 * @Version 1.0
 */
public class test {
    public static void main(String[] args) {
        String s3=new String("1")+new String("1");
        String s4="11";
        String s5 = s3.intern();
        System.out.println(s3==s4);//false
        System.out.println(s4==s5);//true
        
    }
}

同样的在进行完拼串操作后s3指向的是stringbuilder.tostring方法返回的string,而"11"是没有加载进常量池的,此时s4通过字面量赋值,"11"对象加载进常量池,且s4指向常量池的11,s3.intern()f返回s4的指向地址,并将它返回给s5,所以答案如题中的注释

总结string的intern()的使用

  • jdk1.6,将这个字符串对象尝试放入串池
  • 如果串池中有,则并不会放入,返回已有的串池的对象的地址
  • 如果没有,会把此对项复制一份放入串池,并返回串池中的对象地址
  • jdk1.7起,将这个字符串对象尝试放入串池
  • 如果串池中有,则并不会放入串池。
  • 如果串池中没有,则会将对象的引用地址复制一份,放入串池,并返回串池中的引用地址

懂了这个过后再来看到例题吧

package xx.cxf.feifei.test;
/**
 * @author feifei
 * @Date 2021/08/2021/8/9 15:45:24
 * @Version 1.0
 */
public class test {
    public static void main(String[] args) {
        String s=new String("a")+new String("b");
        //在上一行代码执行完以后,字符串常量池中并没有“ab”
        String s2=s.intern();

        System.out.println(s2=="ab");
        System.out.println(s=="ab");
    }
}

对于jdk1.6来说:

s.intern()是将“ab”加载进常量池,s2指向的是ab,而s指向的是堆中的string对象所以输出为 true false

对于jdk1.8来说:

s.intern()是将s指向的对象的地址的引用复制,放入堆中,所以输出为true true

stringtable的垃圾回收

-XX:+PrintStringTableStatistics 开启stringtable的维护明细

如下测试代码

package xx.cxf.feifei.test;
/**
 * @author feifei
 * @Date 2021/08/2021/8/9 15:45:24
 * @Version 1.0
 */
public class test {
    public static void main(String[] args) {
          
    }
}

JAVA里的String口 java语言string_System_05


维护有1774个字符串

package xx.cxf.feifei.test;
/**
 * @author feifei
 * @Date 2021/08/2021/8/9 15:45:24
 * @Version 1.0
 */
public class test {
    public static void main(String[] args) {
        for(int i=0;i<100000;i++){
            String.valueOf(i).intern();
        }
    }
}

JAVA里的String口 java语言string_java_06


字符串的数量明显低于我们添加进stringtable的同样也发生了ygc即minor gc

JAVA里的String口 java语言string_JAVA里的String口_07

G1的string去重操作

字符串常量池本身就不存在重复的字符串

这里是对堆中的string对象即数组对象来去重的

JAVA里的String口 java语言string_java_08


实现

  1. 当垃圾收集器工作的时候,会访问堆上存活的对象,对每一个访问的对象都会检查是否是候选的要去重的string对象
  2. 如果是,把这个对象的一个引用插入到队列中等待后续的处理,一个去重的线程在后台运行,处理这个队列,处理队列的一个元素意味着从队列删除这个元素,然后尝试去重它所引用的string对象
  3. 使用一个hashtable来记录所有的被string对象使用的不重复的char数组,当去重的时候,会查这个hashtable,来看堆上是否已经存在一个一模一样的char数组
  4. 如果存在,string对象会被调整引用那个数组,释放对原来数组的引用,最终会被垃圾收集器回收掉
  5. 如果查找失败,char数组会被插入到hashtable,这样以后就会被共享