Java

String 类型

2019-02-16  本文已影响0人  Alex90

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

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

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

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

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

如何创建不可变对象

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";

总结

FAQ

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

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


上一篇 下一篇

猜你喜欢

热点阅读