一、String的基本特性

  • String类声明是final的,不可以被继承。
  • String实现了Serializable接口(序列号接口),Comparable接口(比较接口)
  • String在JDK8中是用final char[]存储字符串数据的,JDK9后改为final byte[]存储数据的。因为char类型占两个字节,很多时候的数据只需要一个字节就能存放的,因此会浪费掉一半的空间,之后改成byte[]类型,同时为了能够存放两个字节的数据(例如汉字),会将在使用前判断字符集。
    例子:
public class StringExer {
    String str = "good";

    public static void change1(String str) {
        str = "test";
    }

    public static void change2(StringExer stringExer) {
        stringExer.str = "test";
    }

    public static void main(String[] args) {
        StringExer ex = new StringExer();
        StringExer.change1(ex.str);
        System.out.println(ex.str);//good
        StringExer.change2(ex);
        System.out.println(ex.str);//test
    }

}

这里不管是change1(),还是change2()方法都是引用类型的传递。

change1()方法:由于是引用类型的传递,所以传递的时候内存会再复制出指向"good"字符串的句柄。这时候有两个指向"good"的句柄,在方法内中的str改变成"test",我们知道字符串是不可改变的,所以它实际上是在常量池中创建了"test"字符串,然后复制来的那份句柄指向了新的地址,也就是"test",然而外面的对象还是指向原来的字符串,也就是为什么还是打应good的原因。

Java中string类型的时间怎么加两个小时 java中string的长度_java

change2()方法:通用ex实例的句柄,对str属性进行修改,所以str属性会发生改变

Java中string类型的时间怎么加两个小时 java中string的长度_jdk_02

总结:change1()方法是对参数本身的修改,但是change2()却是对对象的数据的修改

二、字符串常量池

  • 字符串常量池使用一个固定大小的HashTable来实现的。
  • 在JDK6及之前长度默认为1009,如果字符串经常出现指针碰撞则会导致String类操作的效率变慢(尤其是String.intern()方法),所以在JDK7时默认被修改为60013,而在JDK8及后最短长度不得低于1009。
  • 可以通过"-XX:StringTableSize"参数来设置字符串常量池的大小。
  • 通过字面量定义的字符串存放在字符串常量池,通过new出来的字符串存放在堆空间。
  • 字符串常量池位置变化的原因:
  • 永久代的空间较小,容易出现OOM异常。
  • 永久代垃圾回收频率低,但字符串大多数生命都是短暂的。

三、字符串的拼接操作

  • 使用字面量创建字符串是在字符串常量池中,使用new关键字创建的对象是在先创建一个String对象然后在字符串常量池中寻找是否有该字符串常量,若没有则创建该对象,最后让这个String对象字符串常量。
  • 两个字面量字符串拼接会在编译期进行优化。
  • 含有变量的字符串拼接:
  • 如果在JDK 5之后是先创建StringBuilder对象,再通过append()方法进行拼接,最后通过toString()方法返回新的字符串对象(跟new String()一致),所以最后返回的对象是创建在堆中的。
  • 在JDK 5之前使用StringBuffer,其余一样。
  • 补充:StringBuilder没有syn关键字,StringBuffer内的方法有syn关键字,因此StringBuilder是线程不安全的,StringBuffer是线程安全的,速度上StringBuilder>StringBuffer
  • 使用intern()方法:若该字符串不存在常量池中,则在常量池中创建该字符串。最后返回常量池中的字符串。
  • 使用final修饰的对象进行拼接的时候会将其认为字面量
@Test
public void StringTest() {
    String s1 = "a";
    String s2 = "ab";
    String s3 = "a" + "b";
    String s4 = s1 + "b";
    String s5 = new String("ab");
    String s6 = s5.intern();

    final String s7 = "c";
    String s8 = "cd";
    String s9 = s7 + "d";

    //在字节码文件中的s3被优化成s3="ab"
    System.out.println(s2 == s3);//true
    //因为s1是变量,所以最后是通过toString方法返回,对象是创建在堆中的
    System.out.println(s3 == s4);//false
    System.out.println(s3 == s5);//false
    //s6是通过intern从字符串常量池中获取的
    System.out.println(s3 == s6);//true

    System.out.println(s8 == s9);//true
}

String、StringBuffer、StringBuilder的拼接操作

@Test
    public void test6() {

        long start = System.currentTimeMillis();

//        method1(100000);//4014
//        method2(100000);//5
        method3(100000);//9
        
        long end = System.currentTimeMillis();

        System.out.println("花费的时间为:" + (end - start));
    }

    public void method1(int highLevel) {
        String src = "";
        for (int i = 0; i < highLevel; i++) {
            src = src + "a";
        }
    }

    public void method2(int highLevel) {
        StringBuilder src = new StringBuilder();
        for (int i = 0; i < highLevel; i++) {
            src.append("a");
        }
    }

    public void method3(int highLevel) {
        StringBuffer src = new StringBuffer();
        for (int i = 0; i < highLevel; i++) {
            src.append("a");
        }
    }

String的拼接:创建一个StringBuilder,然后再根据append进行追加,再创建String返回。

StringBuilder的拼接:直接进行append拼接,没有syn关键字。

StringBuffer的拼接:直接进行append拼接,有syn关键字。

所以速度上StringBuilder>StringBuffer>String

如果我们已经能预知到字符串的长度,可以在创建的时候就给它更大的空间,省去扩容的花费的时间。

StringBuilder默认长度是16,扩容是2倍。

可以通过用参数是整形的构造器来让它初始化指定大小的空间。

public StringBuilder(int capacity) {
    super(capacity);
}

四、String相关面试题

  1. 在字符串常量池中没有以下创建的字符串,String s = new String(“ab”)和String s = new String(“a”) + new String(“b”)分别创建了几个对象:
    答:前者创建了两个对象,在堆中创建对象,然后在池中创建"ab",再让堆中的对象指向池中的"ab"字符串。后者先是先创建一个StringBuilder对象①,再创建String对象②,再在池中创建字符串"a"③,让②指向③,再创建第二个String对象④与字符串"b"⑤,最后通过StringBuilder中的append()操作后,返回使用toString()返回。在这个toString中实际上是返回new String(value,0,count)⑥,因此是创建了6个对象。
    注意:StringBuilder的toString()是通过传递value(byte或者char数组)创建的,所以并不会再去字符串常量池中添加。
public String(char value[], int offset, int count) {
    if (offset < 0) {
        throw new StringIndexOutOfBoundsException(offset);
    }
    if (count <= 0) {
        if (count < 0) {
            throw new StringIndexOutOfBoundsException(count);
        }
        if (offset <= value.length) {
            this.value = "".value;
            return;
        }
    }
    // Note: offset or count might be near -1>>>1.
    if (offset > value.length - count) {
        throw new StringIndexOutOfBoundsException(offset + count);
    }
    this.value = Arrays.copyOfRange(value, offset, offset+count);
}
  1. 关于String中的intern的问题:
    分析以下代码输出的内容分别是什么:
public class StringInternTest {
    public static void main(String[] args) {
        String s1 = new String("a");
        s1.intern();
        String s2 = "a";
        System.out.println(s1 == s2);//false

        String s3 = new String("a") + new String("b");
        s3.intern();
        String s4 = "ab";
        System.out.println(s3 == s4);//在jdk6及之前为false,jdk7及之后为true

        String s5 = new String("cd");
        s5 = s5.intern();
        String s6 = "cd";
        System.out.println(s5 == s6);//true,因为intern()方法返回的是常量池的对象

        String s7 = new String("c") + new String("d");
        s7.intern();
        System.out.println(s6 == s7);//false,因为在进行创建s5的同时将"cd“加入常量池,所以此时情况跟s1、s2一致
    }
}

因为画的有点乱所以就先分析下s1、s2以及s3、s4的内存结构

Java中string类型的时间怎么加两个小时 java中string的长度_System_03

总结:

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