1.分类
- 基本属性:String最终的属性就一个char[] value;
- 构造方法区域:比较复杂的就是通过unicode码和byte[]构造;
- 字符串比较:是否相等、大小(排序);
- 查询:indexOf、startsWith、endWith、contains
- 截取:subString、
- 工具方法:格式化打印、unicode 码点相关的位操作方法;
2. 基础
面向对象的封装思想为什么屏蔽了很多底层细节,String类作为我们使用频率最高的类,帮我们实现了很多功能,
2.1 不可变性
JDK中有很多不可变的类,所谓的不可变类不是表现在字符串声明为final
,而是内部的value是不可以被修改的,所有对value的操作都是返回一个新的对象,比如BigDecimal
,下面是一些错误的方法
String str = " hello,你好 ";
str.trim();
str.replace(',',',');//这行是编译不过去的,中文的char是有问题的
System.out.println(str);
2.2 字符串常量池
JVM为了提升8种基本类型(short/int/long/double/float/char/boolean/byte)和String的效率,建立了常量池的概念。这些变量在编译为class文件后,放入到class文件的常量池部分。JVM运行时会建立字符串常量池,预编译的字符串会加入常量池,通过intern也可以将字符串加入常量池;
public class StringPoolTest {
public static final String str1 = "a"; // a进入常量池,str1使用常量池引用
public final String str2 = "b"; // b进入常量池,str2使用常量池引用
public String str3 = "c"; // c进入常量池,str3 使用常量池引用
public final String str4 = str2 + str3; // bc不进入常量池, 实际上是 str4 = new StringBuilder(str2).append(str3).toString();
public static void main(String[] args) {
String str5 = "d"; // d进入常量池
final String str7 = "f"; // f 进入常量池
final String str8 = str5 + "j"; //dj不进入常量池 实际上是str8 = new StringBuilder(str5).append(j).toString()
}
}
使用 javap -v
做反编译查看字节码的详细信息如下
public class com.saillen.demo.lang.StringPoolTest
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
……
#4 = String #44 // c
//这里很明确引入了StringBuilder,但是我们的代码中并没有import
#6 = Class #46 // java/lang/StringBuilder
……
//表示d的引用值在#51定义
#12 = String #51 // d
#13 = String #52 // e
#14 = String #53 // f
#15 = String #54 // j
……
//
#17 = Utf8 str1
#20 = String #56 // a
#21 = Utf8 str2
#22 = Utf8 str3
#23 = Utf8 str4
#35 = Utf8 str5
#36 = Utf8 str6
#37 = Utf8 str7
#38 = Utf8 str8
……
#42 = Utf8 b
#44 = Utf8 c
#51 = Utf8 d
#52 = Utf8 e
#53 = Utf8 f
#54 = Utf8 j
#56 = Utf8 a
{
//类的初始化指令
public com.saillen.demo.lang.StringPoolTest();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=1, args_size=1
//加载b,str1的代码被优化掉了,因为没有被使用,还是很智能的
//这里JDK8优化了无用的code,但是JDK7、6不一定
//优化掉了但是常量池中存在,因为在其他类中可以使用str1,
4: aload_0
5: ldc #2 // String b
7: putfield #3 // Field str2:Ljava/lang/String;
//加载c
10: aload_0
11: ldc #4 // String c
13: putfield #5 // Field str3:Ljava/lang/String;
//str4 实际是,
//先调用new StringBuilder()
//然后调用append(str2).append(str3)
//然后调用toString()
//然后赋值给str4
16: aload_0
17: new #6 // class java/lang/StringBuilder
20: dup
21: invokespecial #7 // Method java/lang/StringBuilder."<init>":()V
//加载b做append(str2)
24: ldc #2 // String b
26: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
//加载c做append(str3)
29: aload_0
30: getfield #5 // Field str3:Ljava/lang/String;
33: invokevirtual #9 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
//toString给str4
36: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
39: putfield #11 // Field str4:Ljava/lang/String;
……
//其他str5、str6、str7、str8同理
}
SourceFile: "StringPoolTest.java"
可以参考:http://blog.csdn.net/sugar_rainbow/article/details/68150249
NOTE:涉及到这种‘基本的’,‘JVM’层次的要注意JDK版本,还有有些结论不一定“正确”;
Unicode 和 UTF-16
- Java使用unicode字符集表示内存中的每个字符(不仅仅是字母和汉字);
- unicode标准中为每个字符确定了唯一的 码点(codePoint);
- unicode标准将码点以16进制表示即\uxxxx形式;
- unicode标准将码点分区域管理,称为平面(plane);
- unicode将最前面的65536字符称为 基本平面(BMP);
- unicode仅仅是一个标准,不管实际怎么存储的;
- UTF-16仅仅是unicode的一种实现方案;
unicode的几个问题:
- 码点如何表示:比如codePoint为1,那么1个字节就够了,但是如果是10万,那么1个字节肯定不够;
- 如何存储数据?最简单 or 占用空间最小?
可以参考:http://blog.csdn.net/gjb724332682/article/details/43229075
问题:为什么1byte = 8bit ? 为什么1 byte 不用 4bit或者 16bit?
2.3Java中UTF-16
- java使用utf-16存储和表示字符 – 在JVM中每个字符都是2个字节;
- 2个字节能表示的codePoint 范围:256 * 256 = 65536,能表示BMP的内容 O~0xFFFF范围;
- Charset类是char的包装类也提供了很多Unicode的操作方法,保存了最大和最小字符数量;
- 如果字符比如汉字codePoint的值很大怎么办 - 用2个char也就是4个字节来表示;
- 在class中为了减小文件大小,字符常量都是以UTF8编码的,但是JVM内存中是UTF16的,即在load的时候是有编码转换的;
到底几个字节?
- char到底是几个字节,很明确char是2个字节,那么汉字为什么是2个字符,4个字节,如何定义?
- char的实质存储的是int值,如果codePoint在2个字节内能表示下,那么就用一个char就够了,在显示打印的时候读取一个char然后找打对应的字符去打印;
- 如果codePoint需要4个字节,2个字符,那么就读取两个连续的char来打印即可。
// 下面的代码段不要用IDE编辑会报错。
char c = ''; //不要用IDE编辑。
String s = ""; // char[]的长度为2;
2,4 Java中char和String到底存储的是什么
- String:存储的是char[]数组;
- char:存储的是字符的unicode的codePoint的int值;
- java中编码转换的实质就是:读取目标编码的codePoint的值转换为unicode的codePoint值然后按照UTF-16的格式存储到内存或其他地方;
3 源码
3.1 属性
// 字符串是常量,字符串被创建后,字符串的值不可以被修改,
// final的不可修改
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
/** 存储了原始字符 */
private final char value[];
/** 因为字符串的不可变性,所以可以缓存一个hash值 */
private int hash; // Default to 0
private static final long serialVersionUID = -6849794470754667710L;
//序列化使用
private static final ObjectStreamField[] serialPersistentFields =
new ObjectStreamField[0];
// 内部方法用来比较字符串大小
public static final Comparator<String> CASE_INSENSITIVE_ORDER
= new CaseInsensitiveComparator();
3.2 常用方法
//判断长度就是char[]的长度
public int length() {
return value.length;
}
//是不是空串,空格不算空串;
public boolean isEmpty() {
return value.length == 0;
}
//第index个字符,要注意不在BMP内的字符取出的char可能无法转换为一个完整字符;
public char charAt(int index) {
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
return value[index];
}
//这里copy了一个新的char,防止String的不可变性被破坏
void getChars(char dst[], int dstBegin) {
System.arraycopy(value, 0, dst, dstBegin, value.length);
}
//只copy部分,还是arraycopy
//做数组copy的时候还是多使用arraycopy
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin){
if (srcBegin < 0) {
throw new StringIndexOutOfBoundsException(srcBegin);
}
if (srcEnd > value.length) {
throw new StringIndexOutOfBoundsException(srcEnd);
}
if (srcBegin > srcEnd) {
throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
}
System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
}
3.3 相等性比较、hash
相等的比较思路最终都是:挨个循环比较char[],如果不一致则false
//equals方法挨个比较char字符是否一样
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
//可以看到是挨个字符计算一个最终的hash
//如果计算了一遍了则不再计算
//对于""的字符串,hashCode就是0
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
//将unicode码参与到计算中
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
3.4 多种valueOf方法
valueOf(Object obj)的时候不会有null的问题,可以替代我们的的obj.toString();
public static String valueOf(Object obj) {
return (obj == null) ? "null" : obj.toString();
}
public static String valueOf(char data[]) {
return new String(data);
}
public static String valueOf(char data[], int offset, int count) {
return new String(data, offset, count);
}
public static String copyValueOf(char data[], int offset, int count) {
return new String(data, offset, count);
}
public static String copyValueOf(char data[]) {
return new String(data);
}
public static String valueOf(boolean b) {
return b ? "true" : "false";
}
public static String valueOf(char c) {
char data[] = {c};
return new String(data, true);
}
public static String valueOf(int i) {
return Integer.toString(i);
}
public static String valueOf(long l) {
return Long.toString(l);
}
public static String valueOf(float f) {
return Float.toString(f);
}
public static String valueOf(double d) {
return Double.toString(d);
}