美文网首页Java
String 类型

String 类型

作者: Alex90 | 来源:发表于2019-02-16 21:38 被阅读0次

    字符串(String)广泛应用于 Java 编程中,在 Java 中字符串不属于基础类型,属于对象,Java 提供了 String 类来创建和操作字符串。

    Java 的可变类(Mutable)与不可变类(Immutable)

    不可变类,是指这个类的实例一旦创建完成后,就不能改变其成员变量值。如JDK内部自带的很多不可变类:Interger、Long 等。

    可变类,相对于不可变类,类的实例创建后可以改变其成员变量值,开发中创建的大部分类都属于可变类。

    参考 String 和 StringBuilder,String 是不可变的,每次对于 String 对象的修改都将产生一个新的 String 对象,而原来的对象保持不变。StringBuilder 是可变的,每次对于 StringBuilder 对象的修改都作用于该对象本身,并没有产生新的对象。

    如何创建不可变对象

    • 类添加 final 修饰符,保证类不被继承,或者所有方法都添加 final 修饰,保证不可被重写。
    • 保证所有成员变量必须私有(private),并且添加 final 修饰符。
    • 只提供访问器方法(getter),不提供修改器方法(setter)
    • 通过构造器初始化所有成员,如果包含可变成员变量,进行深拷贝作为初始化的值。
    • 如果成员变量是可变的,添加 final 修饰符,值依然会被修改。在 getter 方法返回该成员变量时,返回对象的一个深拷贝,这也是 Java 的最佳实践之一。

    String 类的设计细节

    首先看一下 String 类的源码

    public final class String
        implements java.io.Serializable, Comparable<String>, CharSequence {
        /** The value is used for character storage. */
        private final char value[];
    
        /** Cache the hash code for the string */
        private int hash; // Default to 0
    
        public String(String original) {
            this.value = original.value;
            this.hash = original.hash;
        }
        
        public String(char value[]) {
            this.value = Arrays.copyOf(value, value.length);
        }
        
        public String substring(int beginIndex) {
            if (beginIndex < 0) {
                throw new StringIndexOutOfBoundsException(beginIndex);
            }
            int subLen = value.length - beginIndex;
            if (subLen < 0) {
                throw new StringIndexOutOfBoundsException(subLen);
            }
            return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
        }
        
        ........
    }
    

    可以看到以下几点:

    1. String 类满足以上不可变类的几条设计原则。
    2. String 类内部是通过 char[] 来保存字符串的。
    3. subString、concat 和 replace 等操作都重新生成了一个新的字符串对象进行操作,最原始的字符串并没有被改变。

    字符串常量池

    Java 中字符串的使用是非常高频的,而字符串和其他对象一样,创建需要消耗时间和空间,JVM为了提高性能和减少内存的开销,在实例化字符串的通过使用==字符串常量池==进行优化。

    创建字符串常量时,JVM会优先检查字符串常量池,如果该字符串已经存在,那么返回常量池中的实例引用。如果字符串不存在,就会实例化该字符串并且将其放到常量池中。

    字符串不可变的特性能够很好的支持这一优化,可以保证常量池中一定不存在两个相同的字符串。如果字符串是可变的,此时相同内容的字符串指向常量池的同一个内存空间,当某个变量改变了该内存的值时,其他变量的值也会发生改变。

    Java 中的常量池有两种:静态常量池和运行时常量池。
    静态常量池,即 *.class 文件中的常量池,class 文件中的常量池不仅仅包含字符串(数字)字面量,还包含类、方法的信息,占用 class 文件绝大部分空间。
    运行时常量池,是JVM虚拟机在完成类装载操作后,将 class 文件中的常量池载入到内存中,并保存在方法区中,通常说的常量池,就是指方法区中的运行时常量池。

    字符串不可变的其他好处

    同一个字符串实例可以被多个线程共享,由于不可变的特性可以不用担心线程安全。

    类加载器要用到字符串(根据类的完整路径名字加载),不可变性提供了安全性,以便正确的类被加载。

    能够很好支持 hash 映射,在它创建的时候 hashcode 就被缓存了(不可变),不需要重新计算,使得字符串很适合作为 Map 中的键,字符串的处理速度要快过其它的键对象。

    String 类并不是所有情况下都不可变,可以通过反射机制的手段改变其值。

    String 类方法

    创建字符串

    创建字符串的方式有多种方式,总结有两种处理方式:
    (1)使用 "" 引号创建字符串,String str = "hello";
    (2)使用 new 关键字创建字符串,String str = new String("hello");
    (3)使用只包含常量的字符串连接符,String str = "hello " + "world";
    (4)使用包含变量的字符串连接符,String str = "hello " + s1;

    方式(1)和(3)创建的字符串都是常量,编译期就已经确定存储到 String Pool 中,方式(2)和(4)创建的对象会在运行时创建,存储到堆中。

    使用 new 关键字创建字符串时,首先查看池中是否存在,如果有,则拷贝一份到堆中,然后返回堆中的地址;如果没有,则在堆中创建一份,然后返回堆中的地址。注意,此时不需要从堆中复制到池中,浪费池的空间。如果要将对象放入常量池,需要调用 String.intern() 方法。

    注意,在使用方式(1)或(3)创建字符串时,对象并不一定创建,可能只是指向一个先前已经创建的对象。只有通过 new 关键字的方法才能保证每次都创建一个新的对象。

    String.intern()

    当调用 intern() 方法时,如果常量池中已经包含一个等于此 String 对象的字符串,则返回池中的字符串。否则,将此 String 对象添加到池中,并返回此 String 对象的引用。String.intern() 方法就是扩充常量池的一个方法

    对于任意两个字符串 s 和 t,当且仅当 s.equals(t) == true 时,s.intern() == t.intern() 才为 true。

    String s0 = "aaa"; // s0 在常量池中
    String s1 = new String("aaa"); 
    String s2 = new String("aaa"); 
    System.out.println(s0 == s1); // false
    
    s1.intern(); // 虽然执行了 s1.intern(),但它的返回值没有赋给 s1
    s2 = s2.intern(); //把常量池中 "aaa" 的引用赋给 s2 
    
    System.out.println(s0 == s1); // false,s1 引用的是 new 的字符串 "aaa"
    System.out.println(s0 == s2); // true,s2 引用的是常量池中的 "aaa"
    
    == 和 equal()

    对于 == 操作符,如果作用于基本数据类型的变量(byte,short,char,int,long,float,double,boolean),则比较的是其存储的"值"是否相等;如果作用于引用类型的变量(包括 String),则比较的是所指向的对象的地址(即是否指向同一个对象)

    对于 equal() 方法,在基类 Object 类中,equals() 方法默认是用来比较两个对象的引用是否相等(即是否指向同一个对象)。equals() 方法不能作用于基本数据类型的变量。

    String 类对 equals() 方法进行了重写,用来比较指向的字符串对象所存储的字符串是否相等。其他的一些类诸如 Double,Date,Integer 等,也对 equals() 方法进行了重写用来比较指向的对象所存储的内容是否相等。

    +

    参考 http://www.cnblogs.com/xiaoxi/p/6036701.html

    对以下代码段编译运行

    public void test() {
        String a = "aa";
        String b = "bb";
        String c = "xx" + "yy " + a + "zz" + "mm" + b;
        System.out.println(c);
    }
    

    查看字节码

    public static main([Ljava/lang/String;)V
       L0
        LINENUMBER 5 L0
        LDC "aa"
        ASTORE 1
       L1
        LINENUMBER 6 L1
        LDC "bb"
        ASTORE 2
       L2
        LINENUMBER 7 L2
        NEW java/lang/StringBuilder
        DUP
        LDC "xxyy "
        INVOKESPECIAL java/lang/StringBuilder.<init> (Ljava/lang/String;)V
        ALOAD 1
        INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
        LDC "zz"
        INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
        LDC "mm"
        INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
        ALOAD 2
        INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
        INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
        ASTORE 3
       L3
        LINENUMBER 8 L3
        GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
        ALOAD 3
        INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
       L4
        LINENUMBER 9 L4
        RETURN
       L5
        LOCALVARIABLE args [Ljava/lang/String; L0 L5 0
        LOCALVARIABLE a Ljava/lang/String; L1 L5 1
        LOCALVARIABLE b Ljava/lang/String; L2 L5 2
        LOCALVARIABLE c Ljava/lang/String; L3 L5 3
        MAXSTACK = 3
        MAXLOCALS = 4
    }
    
    1. String 中使用 + 进行字符串连接时,编译后将尽可能多的直接将字符串常量连接起来,形成新的字符串常量参与后续连接(限于开头,中间的多个字符串常量不会自动拼接),"xx" + "yy " 转变为 "xxyy ","zz" + "mm" 并没有变化。

    2. 字符串连接是从左向右依次进行,对于不同的字符串,首先以最左边的字符串为参数创建 StringBuilder 对象,然后依次对右边进行 append() 操作,最后将 StringBuilder 对象通过 toString() 方法转换成 String 对象。

    String c = "xx" + "yy " + a + "zz" + "mm" + b; 实现过程是 String c = new StringBuilder("xxyy ").append(a).append("zz").append("mm").append(b).toString();

    代码分析:

    String s = "a" + "b" + "c"; 
    String s1  =  "a"; 
    String s2  =  "b"; 
    String s3  =  "c"; 
    String s4  =  s1  +  s2  +  s3;
    

    变量 s 的创建等价于 String s = "abc";,而变量 s4 不能在编译期进行优化,其对象创建相当于:new StringBuilder(s1).append(s2).append(s3);。因此使用 + 时容易产生低效的代码,看下面的例子:

    String s = null;
    for(int i = 0; i < 100; i++) {
        s += "a";
    }
    

    每做一次 + 连接就产生一个 StringBuilder 对象,append() 一次后就不再使用。下次循环再到达时重新产生 StringBuilder 对象,重复执行以上步骤直至循环结束。 如果直接采用 StringBuilder 对象进行 append() 的话,可以节省 N - 1 次创建和销毁对象的时间。

    所以对于在循环中要进行字符串连接的应用,一般都是用 StringBuffer 或 StringBulider 对象来进行操作。

    String、StringBuilder、StringBuffer 的对比

    1. 可变与不可变
      String 是==不可变==字符串对象。
      StringBuilder 和 StringBuffer 是==可变字符串==对象。

    2. 线程安全性
      String 是不可变的,==线程安全==。
      StringBuffer 与 StringBuilder 中的方法和功能完全是等价的,
      StringBuffer 中的方法大都采用了 synchronized 关键字修饰,因此是==线程安全==的,StringBuilder 没有这个修饰,可以被认为是==非线程安全==的。

    3. 效率
      一般来说,StringBuilder > StringBuffer > String。而在某些情况下,String 的操作会比 StringBuffer 操作快。例如,String s1 = "hello " + "world"; 会明显好于 StringBuffer sb = new StringBuffer("hello ").append("world");
      在JVM眼里,前者等效于 String s1 = "hello world";

    总结

    • 当字符串相加操作或者改动较少的情况下,建议直接使用 String;
    • 当字符串相加操作较多的情况下,建议使用 StringBuilder;
    • 如果在多线程环境中,建议使用 StringBuffer。

    FAQ

    (a). String str = new String("abc") 创建了多少个对象?

    在运行期间确实只创建了一个对象,即在堆上创建了 "abc" 对象。在类加载的过程中,在运行时常量池中创建了一个 "abc" 对象。


    相关文章

      网友评论

        本文标题:String 类型

        本文链接:https://www.haomeiwen.com/subject/araieqtx.html