您的位置:首页 > 编程语言 > Java开发

Java中字符串String总结

2017-06-01 15:54 337 查看

1.String是不可变类

       这句话其实大家都很熟悉了,那么具体什么是不可变类呢?一般认为:当对象一旦创建完成后,在正常情况下,对象的状态不会因外界的改变而改变(对象的状态是指对象的属性,包括属性的类型及属性值)。

首先看一个基本的例子:

String s = "abc";
System.out.println("s:" + s); // 输出s:abc
s = "def";
System.out.println("s:" + s); // 输出s:def

       此时,初看上去,输出的结果变了,发现s的值发生了变化,那么这与上面的说法——String类是不可变类是否矛盾呢?答案是否定的,因为s只是指向堆内存中的引用,存储的是对象在堆中的地址,而非对象本身,s本身存储在栈内存中。

       实际上,此时堆内存中依然存在着"abc"和"def"对象。对于"abc"对象本身而言,对象的状态是没有发生任何变化的。

       那么为什么String类具有不可变性呢,显然,既然不可变说明String类中肯定没有提供对外可setters方法。接下来来具体看一下String类的定义。
       下面是String类中主要属性的定义(Java 1.7源码):

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

}

        与之前版本的Java String源码相比,String类减少了int offset 和 int count的定义。这样变化的结果主要体现在:

        1.避免之前版本的String对象subString时可能引起的内存泄露问题;

        2.新版本的subString时间复杂度将有O(1)变为O(n);

        具体分析可见文章:

        http://www.importnew.com/7656.html
        http://www.importnew.com/14105.html
        通过上面String类的定义,类名前面用了final class修饰,因此,String类不能被继承。对于其属性定义,可以看出,属性value[]和hash都是被定义成private类型,且由于没有提供对外的public setters方法,String类属性不可被改变。

        其中,需要重点关注属性value[],其被final char修饰,因此字符型数组value只会被赋值一次就不可修改。其存储内容正好是String中的单个字符内容。

2. String相关的+

        String中的 + 常用于字符串的连接。此处为了说明清楚这个问题,首先可以安装Eclipse 查看字节码插件ByteCode Outline。

        在线安装网址: http://zipeditor.sourceforge.net/update/ Disabled。Help >> install new software >> 输入网址 >> 选择 bytecode outline >> ... ... 安装成功。

        Window >> show view >> other >> java >> ByteCode即可在Eclipse下方面板栏中查看。

        看下面一个简单的例子:
class D {

public static void main(String[] args) {

String a = "aa";
String b = "bb";
String c = "xx" + "yy " + a + "zz" + "mm" + b;
System.out.println(c);
}
}
        编译运行后,点击ByteCode查看,主要字节码部分如下:
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中使用 + 字符串连接符进行字符串连接时,连接操作最开始时如果都是字符串常量,编译后将尽可能多的直接将字符串常量连接起来,形成新的字符串常量参与后续连接(通过反编译工具jd-gui也可以方便的直接看出);

        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();

        由于得出结论:当使用+进行多个字符串连接时,实际上是产生了一个StringBuilder对象和一个String对象。

3. String到底创建了多少个对象

String str=new String("aaa");
        这行代码究竟创建了几个String对象呢?答案是2个,而不是3个。由于new String("aaa")相当于"aaa"与一个就是创建出来的放在堆时原实例对象,而另一个就是放在常量池中的 "aaa" 对象,当然这里的str本身只是一个引用,放在栈里,用来指向堆中创建出来的对象。

        常量池(constant pool)指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。它包括了关于类、方法、接口等中的常量,也包括字符串常量。
String str="aaa";

       只创建1个对象。这里涉及到字符串常量池,在JVM中有一个字符串池,它用来保存很多可以被共享的String对象,这样如果我们在使用同样字面字符串时,它就使用字符串池中同字面的字符串。当然我们可以使用String对象的intern()方法来访问String对象在字符串池中所对应的常量对象。

      上面这行代码被执行的时候,JVM先到字符串池中查找,看是否已经存在值为"aaa"的对象,如果存在,则不再创建新的对象,直接返回已存在对象的引用;如果不存在,则先创建这个对象,然后把它加入到字符串池中,再将它的引用返回。
String str1="aaa";
String str2="aaa";
      也只创建1个对象。能过上面的解释这个就更清楚了,在执行第二行代码时,aaa字符串对象在池中已存在,所以直接返回池中已存在的那个字符串对象。
String str="aaa"+"bbb";
      还是只创建1个对象。由于常量字符串是在编译的时候就也被确定的,又因"aaa"和"bbb"都是常量,因此变量str的值在编译时就可以确定。这行代码编译后的与String str="aaabbb";是一样的,这与我们平时好像不太一样啊?一般使用“+”连接两个字符串都会产生另一个新的字符对象。下面我们看一下例子就明白了:

String str1 = "aaa";
String str2 = "bbb";
String str3 = "aaabbb";

String str4 = "aaa" + "bbb";//不会产生新的字符串对象
System.out.println(str3 == str4);//true

str4 = str1 + "bbb";//会产生新的字符串对象
System.out.println(str3 == str4);//false

str4 = str1 + str2;//会产生新的字符串对象
System.out.println(str3 == str4);//false
        从上面例子我们就可以得出:使用“+”连接的两个字符串本身就是字面常量字符串时,如果池中存在这样连接后的字符串,则是不会重新创建对象,而是直接引用池中的字符串对象;如果“+”连接的两字符串中只要有一个不是字面常量串(即定义过的),是会产生新的字符串对象。

凡事也有例外,这个也不例外:如果“+”连接的字符串中两个或一个不是“字面常量”,但如果定义成常量字符串时,情况又有变化:
final String str1 = "aaa";
final String str2 = "bbb";
String str3 = "aaabbb";

/*
* 因为str1与str2都定义成了常量,所以编译时就能确定,编译时就会将常量替换,等同于
* str4 = "aaa"+"bbb",因此不产生新对象
*/
String str4 = str1 + str2;
System.out.println(str3 == str4);//true
        但如果先定义final字符串,但未在定义处初始化,而初始化在块中,如下:
//此时str1与str2相当于变量,而不是常,因为块是在运行时才能确定,在编译时不能确定
final static String str1;
final static String str2;
static {
str1 ="aaa";
str2 ="bbb";
}
public static void main(String[] args){
String str3 = str1 + str2;
String str4 ="aaabbb";
System.out.println(str3==str4);   //输出为false
}
String str=" ";与String str=new String();
str=" "会放入池中,但new String()不会放入池中。

4. String的intern()方法 

       “当调用 intern 方法时,如果池已经包含一个等于此 String 对象的字符串(该对象由 equals(Object) 方法确定),则返回池中的字符串;否则,将此 String 对象添加到池中,并且返回此 String 对象的引用。它遵循对于任何两个字符串 s 和 t,当且仅当 s.equals(t) 为 true 时,s.intern() == t.intern() 才为 true”,这是jdk文档原文注解。

      
        现在我们回到最开头的那个例子,为什么String str=new String("aaa");会产生2个对象?一个是"aaa"又作为字符串构造函数的参数,但"aaa"自己就是一个字符串,在传进构造函数前就已创建了一个字符对象,实质上与就好比是第二个实例:String str="aaa"; ,它创建的字符串对象会放入到池中,并且引用的也是池中的那个字符串对象;另一个就是通过new String()构造函数创建的。所以new String("aaa")会产生两个对象,也就是说通过此种方式创建字符串对象时,会先将字符串参数对象放入对象池,然后另外创建一个字符串对象。

       理解完new String("aaa")为什么会产生两个对象,我们再来看看new String(char value[])这样的方式创建时,又会不会把字符串对象放入到池中呢?答案是不会的。因为传递给构造函数的是一个字符数组,而不是像前面传递的是一个字面常量字符串参数那样,将字符串参数本身放入池中。那么我们现在如果证明new String(char value[])未将字符串对象放入池中,我们可以写一个简单的测试,运行时打开XP的任务管理器,查看操作系统的内存使用情况就可以初步确认:

int size = 10000000;
char c[] = new char[size];
for (int i = 0; i < size; i++) {
c[i] = 'a';
}
//使用带字符数组参数构造函数创建字符串时,字符串对象不会放入字符串池
String str1 = new String(c);
System.out.println("String字符串对象创建完毕...");
Thread.sleep(5000);
str1.intern();//到里会看见内存增加
System.out.println("第一次调用intern()完毕...");
Thread.sleep(5000);
str1.intern();//再过5秒将看不到内存增长,因为池中有了,不会再放入,所以内存无变化
System.out.println("第二次调用intern()完毕...");
Thread.sleep(5000);
       所以创建字符串对象放入并放入池中有二种方式:第一种就是直接使用字面常量定义字符串时,如 String str="aaa"; ,str会引用放入池中的对象;第二种就是使用带字符串参数的字符串构造函数,并且此时传入的参数值要是字符串常量形式,而不能是变量的形式,也就是说只能是 String str=new String("aaa");形式,而不能是先定义 String s = "aaa",然后再使用 String str=new String(s);来创建对象,new
String(s);此时只创建一个对象,但如果池中不存在时我们可以使用intern方法将它放入池中。当然上面放入池中的前提是池中还不存在这些字符串对象。

其实,当我们仔细研究时,发现放入池中只实质上只存在一种时机,那就是:直接使用字面常量字符串时。上面所说的两种时机实质上就是直接使用了字面常的字符串而将其放入池中的。

       上面多处提到了JVM中的堆栈,下面小结一下各自原作用:

        栈用来保存基本类型与对象的引用的,基本型在创建前会查看Stack中是否已经有, 有则指向, 没有则创建。

        String内部是以字符串数组来存储字符串的,因此可以认为与char[]等同, String a= "abc",首先在Heap中创一个对象,再到Stack中找char[]是否存在,有则指向该地址, 无则在Stack中创建数组。

        new出来的都是在Heap中,堆是用于存储对象的。

5. String中的常用方法

       1). 与String类中value[]数组存储直接相关的有:

       int length(); // 返回String长度,亦即value[]数组长度;

       char charAt(int index); // 返回指定位置字符;

       int indexOf(int ch, int fromIndex); //从fromIndex位置开始,查找ch字符在字符串中首次出现的位置。fromIndex默认为0,ch直接传入字符即可。如'C',区分大小写,未查找到返回-1;

      char[] toCharArray() ;   // 将字符串转换成一个新的字符数组

       2). 与其他字符串即字串相关的方法有:

       int indexOf(String str, int fromIndex) ;

       与indexOf含义相反有lastIndexOf(..),反向索引。

       boolean contains(String str); //实际上 contains内部实现也是调用的indexOf,然后将其结果与-1相比较。

       boolean startsWith(String str); // 判断字符串是否以str开头

       boolean endsWith(String str); //.....是否以str结尾

       String replace(CharSequence target, CharSequence replacement) ;  // 替换

       String substring(int beginIndex,  int endIndex);  //字符串截取,不传第二个参数则表示直接截取到字符串末尾

       String[] split(String regex);  // 字符串分割

6. String/StringBuilder/StringBuffer区别

       1)String是不可变字符串对象,StringBuilder和StringBuffer是可变字符串对象(其内部的字符数组长度可变)
       2)StringBuffer线程安全,StringBuilder非线程安全。

7. 既然String是不可变字符串对象,如何才能改变让其可变

        既然String对象中没有对外提供可用的public setters等方法,因此只能通过Java中的反射机制实现。因此,前文中说到的String是不可变字符串对象只是针对“正常情况下”。而非必然

public static void stringReflection() throws Exception {

String s = "Hello World";

System.out.println("s = " + s); //Hello World

//获取String类中的value字段
Field valueField = String.class.getDeclaredField("value");

//改变value属性的访问权限
valueField.setAccessible(true);

char[] value = (char[]) valueField.get(s);

//改变value所引用的数组中的第5个字符
value[5] = '_';

System.out.println("s = " + s); //Hello_World
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: