【Java面试基础篇】浅谈字符串

浅谈字符串

字符串操作是计算机程序设计中最常见的行为

1.String

String对象的基本特性

  • String对象是不可变的,通过查看JDK源码可以发现,String类中所有关于字符串的修改方法,都是创建了一个新的String对象。而原String对象并没有任何变化。

    ​ 先看一段代码,代码中有两个String对象,name和newName。我们把name作为参数,传给了lowerCase()方法。实际上我们传递的是对象引用的一个拷贝,引用实际指向的对象并没有任何变化。

        public static String lowerCase(String str){
             return str.toLowerCase();
        }
        public static void main(String[] args){
            String name = "Admin";
            System.out.println("name is" + name);
            String newName = lowerCase(name);
            System.out.println("name is :" + name);
            System.out.println("newName is:" + newName);
        }

    ​ 运行结果:

      name isAdmin
      name is :Admin
      newName is:admin
  • String 是 Immutable 类的典型实现,原生的保证了基础线程安全,因为你无法对它内部数据进行任何修改,这种便利甚至体现在拷贝构造函数中,由于不可变,Immutable 对象在拷贝时不需要额外复制数据。

  • 针对常量池的优化。

    • 当2个String对象拥有相同的值时,他们只引用常量池中的同一个拷贝。当同一个字符串反复出现时,这个技术可以大幅度节省内存空间。
  • 字符串缓存

    • String 在 jdk1.6 以后提供了 intern() 方法,目的是提示 JVM 把相应字符串缓存起来,以备重复使用。在我们创建字符串对象并调用 intern() 方法的时候,如果已经有缓存的字符串,就会返回缓存里的实例,否则将其缓存起来。一般来说,JVM 会将所有的类似“abc”这样的文本字符串,或者字符串常量之类缓存起来。

    • jdk1.6中,字符串缓存被放在永久代,一般不会被GC,这样会导致OOM

    • Jdk1.6以后这个缓存被放置在堆中,这样就极大避免了永久代占满的问题。

    • jdk1.8中,永久代被MetaSpace(元数据区)替代了。而且,默认缓存大小也不断地扩大。

      打印缓存大小
      -XX:+PrintStringTableStatistics
      设置缓存大小为N
      -XX:StringTableSize=N
    • Intern 是一种显式地排重机制,需要开发者手动调用。但是在实际开发中,往往无法预估字符串的重复情况。好在jdk8u20后,可以使用G1 GC 下的字符串排重。它是通过将相同数据的字符串指向同一份数据来实现,无需手动调用,通过jvm底层来实现。开发人员只需要指定使用G1 GC 并设置参数即可

      -XX:+UseStringDeduplication

String对象的重载

  • 我们可以使用”+“来实现Sting对象的重载操作

    代码如下

      public static void main(String[] args) {
              String str1 = "hello";
              System.out.println(str1);
              str1 = str1 + "world";
              System.out.println(str1);
          }

    运行结果

    hello
    helloworld

    ​ 通过以上操作我们可以看到str1被修改为“helloworld”,这就是字符串的重载,我们可以通过javap反编译看看这段代码做了些什么

    public static void main(java.lang.String[]);
      Code:
         0: ldc           #2                  // String hello
         2: astore_1
         3: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         6: aload_1
         7: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        10: new           #5                  // class java/lang/StringBuilder
        13: dup
        14: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
        17: aload_1
        18: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        21: ldc           #8                  // String world
        23: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        26: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        29: astore_1
        30: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
        33: aload_1
        34: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        37: return
    }

    ​ 通过以上字节码我们可以看到,编译器帮我们自动引入了java/lang/StringBuilder类。虽然我们没有主动使用它,但是编译器对字符串重载做了优化。通过创建了StringBuilder对象,使用它的append()方法来提高程序性能。减少中间生成的垃圾对象。

2.StringBuffer和StringBuilder

  • 相同点
    1.对象的值都可以改变
    2.都实现了AbstractStringBuilder抽象类
    3.与String在内存中的存储方式相同,都是一个有序的字符序列(char类型的数组)进行存储
    4.底层实现都是通过字符数组的操作来实现的,如果涉及到较大的数据量扩容会有一定的性能问题。

  • 不同点:
    1.StringBuffer是线程安全的
    2.StringBuilder是线程不安全的
    3.StringBuffer的性能要远低于StringBuilder(StringBuffer类中方法定义前面都会有synchronize关键字)

     @Override
        public synchronized int length() {
            return count;
        }
        @Override
        public synchronized int capacity() {
            return value.length;
        }
        @Override
        public synchronized void ensureCapacity(int minimumCapacity) {
            super.ensureCapacity(minimumCapacity);
        }
        /**
         * @since      1.5
         */

        @Override
        public synchronized void trimToSize() {
            super.trimToSize();
        }

3.重提字符串压缩

众所周知jdk1.6时,虚拟机参数-XX:+UseCompressedStrings可以设置使用字节数组来存储字符串,但是在1.7中被废弃了。时过境迁,在1.9中又引入了Compact String。

  • 字符串实现的改变

    private final byte[] value;  
    private final byte coder; 

    coder取值
    static final byte LATIN1 = 0;
    static final byte UTF16 = 1;
  • coder的作用
    如果字符串只包含 LATIN-1编码的字符,coder 变量的值将为 0,因此字符串的长度就为字节数组的长度一致,否则 coder 变量的值为1,采用 UTF-16 编码,那么字符串的长度为字节数组长度的一半。

    public int length() {
      return value.length >> coder;
    }

笔者比较菜,目前还在1.8上做开发,以后有使用心得再分享吧