Java字符串是Unicode字符序列。如串"Java\u2122"有5个Unicode字符组成。
Java中字符串一般被认为是不可变的。当对字符串赋值时,实际是将原本的字符串引用指向了另一个字符串。这样做是否会降低运行效率呢?毕竟这样会产生大量的无用对象,况且修改一个代码单元要比创建一个新的字符串更加简洁。确实,通过拼接来创建字符串效率确实不高,但是不可变字符串却有一个优点:编译器可以让字符串共享。可以想象将各种字符串存放在公共的存储池中。字符串变量指向存储池中响应的位置。如果复制一个字符串变量,原始字符串与复制的字符串共享相同的字符。
总而言之,Java的设计者认为共享带来的高效率远远胜于提取、拼接字符串所带来的低效率(who knows?)。
s.equals(t)###
在这里将调用字符串的equals方法与目标字符串进行比较,比较的对象为字符串的值。注意一定不能使用==
运算符来检测字符串是否相等。这个运算符只能确定两个字符串是否放在同一个位置上。当然,如果字符串放置在同一个位置上,它们必然相等。但是完全有可能将内容相同的多个字符串的拷贝放置在不同的位置上。
如果虚拟机始终将相同的字符串共享,就可以使用==
运算符来检测是否相等。但实际上只有字符串常量是共享的,而+
或substring
等操作产生的结果并不是共享的。
C++中像Java中重载字符串相加的+
符号那样重载了==
符号用于字符串间的比较,但Java中并没有这样做。
空串和Null串###
有时要检查一个字符串既不是null也不为空串,可以使用以下条件
if (str != null && str.length() != 0)
切记要先检查不为null,因为对空对象调用length方法会引发异常。
代码点和代码单元###
Java字符串由char序列组成。char数据类型是一个采用UTF-16编码表示Unicode代码点的代码单元。代码点是指字符在Unicode编码映射表上对应的数字。大多数的常用Unicode字符使用一个代码单元就可以表示,而辅助字符需要一对代码单元表示。
length方法将返回采用UTF-16编码表示的给定字符串所需要的代码单元数量,例如:
String greeting = "hello";
int n = greeting.length(); // n = 5;
要想得到实际的长度,即代码点的数量,可以调用:
String greeting = "hello";
int cpCount = greeting.codePointCount(0, greeting.length()); // n = 5;
调用s.charAt(n)
将返回位置n的代码单元,n结余0~s.length() - 1之间,例如
char first = greeting.charAt(0); // 'H'
要想获得第i个代码点,应该使用下列语句:
int index = greeting.offsetByCodePoints(0, i);
int cp = greeting.codePointAt(index);
遍历代码点
for(int i = ; i < str.length; i++) {
int cp = str.codePointAt(i);
if (Character.isSupplementaryPoint(cp)) i++;
System.out.println(new String(Character.toChars(cp)));
}
构建字符串###
//:
当使用在字符串之间重载的+
运算符时,为了生成最终的String,产生了大量的需要GC回收的对象。
package com.cnsumi.l.string;
public class StringConcatTest {
public static void main(String[] args) {
String mango = "mango";
String s = "abc" + mango + "def" + 47;
System.out.println(s);
}
}
javac StringConcatTest.java
java -c StringConcatTest
public class com.cnsumi.l.string.StringConcatTest {
public com.cnsumi.l.string.StringConcatTest();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."
4: return
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String mango
2: astore_1
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."
10: ldc #5 // String abc
12: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
15: aload_1
16: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: ldc #7 // String def
21: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
24: bipush 47
26: invokevirtual #8 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
29: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
32: astore_2
33: getstatic #10 // Field java/lang/System.out:Ljava/io/PrintStream;
36: aload_2
37: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
40: return
}
在编译后的字节码中可以看到JVM自动引入了java.lang.StringBuilder类(#3),虽然在源码中我们并没有使用StringBuilder类,但是编译器却自作主张的使用了它,因为它更高效。
在这个例子中,编译器创建了一个StringBuilder对象,不断的调用其append方法,最终调用toString生成最终的结果。
虽然编译器在这个例子中优化我们的代码,但这 不代表着我们就可以随意使用+来拼接字符串,因为编译器并没有优化到那一层次。
package com.cnsumi.l.string;
public class WhitherStringBuilder {
public String implicit(String[] fields) {
String ret = "";
for (String string : fields) {
ret += string;
}
return ret;
}
public String explicit(String[] fields) {
StringBuilder ret = new StringBuilder();
for (String string : fields) {
ret.append(string);
}
return ret.toString();
}
}
`javac WhitherStringBuilder.java`
`javap -c WhitherStringBuilder`
public class com.cnsumi.l.string.WhitherStringBuilder {
public com.cnsumi.l.string.WhitherStringBuilder();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."
4: return
public java.lang.String implicit(java.lang.String[]);
Code:
0: ldc #2 // String
2: astore_2
3: aload_1
4: astore_3
5: aload_3
6: arraylength
7: istore 4
9: iconst_0
10: istore 5
12: iload 5
14: iload 4
16: if_icmpge 51
19: aload_3
20: iload 5
22: aaload
23: astore 6
25: new #3 // class java/lang/StringBuilder
28: dup
29: invokespecial #4 // Method java/lang/StringBuilder."
32: aload_2
33: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
36: aload 6
38: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
41: invokevirtual #6 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
44: astore_2
45: iinc 5, 1
48: goto 12
51: aload_2
52: areturn
public java.lang.String explicit(java.lang.String[]);
Code:
0: new #3 // class java/lang/StringBuilder
3: dup
4: invokespecial #4 // Method java/lang/StringBuilder."
7: astore_2
8: aload_1
9: astore_3
10: aload_3
11: arraylength
12: istore 4
14: iconst_0
15: istore 5
17: iload 5
19: iload 4
21: if_icmpge 43
24: aload_3
25: iload 5
27: aaload
28: astore 6
30: aload_2
31: aload 6
33: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
36: pop
37: iinc 5, 1
40: goto 17
43: aload_2
44: invokevirtual #6 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
47: areturn
}
可以看到第一个方法的字节码中创建StringBuilder对象是在循环之内,而第二个方法是在循环之外。可见,编译器或许只能优化同一句中的`+`操作。
###浅谈Java String###
原文地址http://www.importnew.com/21711.html
####常量池####
Java代码被编译成class文件时,会生成一个常量池(Constant pool)的数据结构,用以保存字面常量和符号引用(类名、方法名、接口名和字段名等)。
package com.ctrip.ttd.whywhy;
public class Test {
public static void main(String[] args) {
String test = "test";
}
}
很简单的一段代码,通过命令 javap -verbose 查看class文件中 Constant pool 实现:
Constant pool:
1 = Methodref #4.#13 // java/lang/Object."":()V
2 = String #14 // test
3 = Class #15 // com/ctrip/ttd/whywhy/test
4 = Class #16 // java/lang/Object
5 = Utf8
6 = Utf8 ()V
7 = Utf8 Code
8 = Utf8 LineNumberTable
9 = Utf8 main
10 = Utf8 ([Ljava/lang/String;)V
11 = Utf8 SourceFile
12 = Utf8 test.java
13 = NameAndType #5:#6 // "":()V
14 = Utf8 test
15 = Utf8 com/ctrip/ttd/whywhy/test
16 = Utf8 java/lang/Object
通过反编译出来的字节码可以看出字符串 "test" 在常量池中的定义方式:
2 = String #14 // test
14 = Utf8 test
在main方法字节码指令中,0 ~ 2行对应代码 String test = "test"; 由两部分组成:ldc #2 和 astore_1。
// main方法字节码指令
public static void main(java.lang.String[]);
Code:
0: ldc #2 // String test
2: astore_1
3: return
1、Test类加载到虚拟机时,”test”字符串在Constant pool中使用符号引用symbol表示,当调用 ldc #2 指令时,如果Constant pool中索引 #2 的symbol还未解析,则调用C++底层的 StringTable::intern 方法生成char数组,并将引用保存在StringTable和常量池中,当下次调用 ldc #2 时,可以直接从Constant pool根据索引 #2获取 “test” 字符串的引用,避免再次到StringTable中查找。
2、astore_1指令将”test”字符串的引用保存在局部变量表中。
常量池的内存分配 在 JDK6、7、8中有不同的实现:
1、JDK6及之前版本中,常量池的内存在永久代PermGen进行分配,所以常量池会受到PermGen内存大小的限制。
2、JDK7中,常量池的内存在Java堆上进行分配,意味着常量池不受固定大小的限制了。
3、JDK8中,虚拟机团队移除了永久代PermGen。
####字符串初始化####
字符串可以通过两种方式进行初始化:字面常量和String对象。
#####字面常量#####
public class StringTest {
public static void main(String[] args) {
String a = "java";
String b = "java";
String c = "ja" + "va";
}
}