首页 > 代码库 > 004 Java字符串的几个特性
004 Java字符串的几个特性
在本系列教材的上一篇(003 Java字符串)中,对Java语言中String类的一些基本情况和整体架构进行了讲解和分析,相信大家已经很好地掌握了。本篇教程主要是补充一些String类的重要特性,帮助大家避免掉使用String过程中的一些陷阱。
首先,补充一个在JDK中使用非常频繁的概念:不可变类。所谓的不可变类是指该类的对象在生成以后就不会被改变了,关于不可变类的优点、缺点,特别是在Java并发编程时的优势,此处暂时略过不讲。那么如何定义一个不可变类呢?如果你有仔细观察String类的源码,你肯定能发现所有String类的所有可能修改字符串对象的方法都新建了一个字符串对象,而不是在原有的字符串对象上做修改。所以String类就是一个非常标准的不可变类。
如果曾经做过Java的面试题,那么你对下面的题目不会陌生:
String a = "test";
String b = "te" + "st";
System.out.println(a == b);
System.out.println(a.equals(b));
强烈建议大家先自己思考一下,得出答案后再接着往下看。正确答案是true和true,恭喜你答对了。答案虽然单纯,背后的机制却并不简单,导致此种结果的主要原因有三:一是通过双引号定义的字符串是字符串常量,存储在JVM内存模型中的方法区(常量区);二是String是一个不可变类,JVM对字符串常量进行了特殊处理:缓存处理,也就是说一个字符串常量在整个JVM中只存在一份;三是Java编译器对编译时就能确定的字符串常量进行了一定的优化,也就是自动进行了合并操作。我绘制了一个字符串在简化版的JVM内存模型中的布局情况简图:
从此图中可以看出,对于字符串常量,JVM将其存储于方法区。
? ?接下来,再看看如下的代码:
String a = "test";
String b = new String(a);
String c = new String(a);
System.out.println(a == b);
System.out.println(b == c);
同样建议大家先自己思考得出答案。如果的答案不是false和false,那么接下来的内容你需要边看边思考了。我们发现此处使用new运算法来生成两个对象,它们就不再符合上面说的字符串常量的情况,它们在JVM简化内存模型中的布局如下图所示:
从此图可以得出,如果使用new运算符来生成String对象,那么该String对象就一定是作为一个新生成的对象,存储于堆空间内。
? ?关于String对象的内存布局情况,我们再来看一下String类中的intern函数:
public native String intern();
如果你的英文水平不差,建议仔细看一遍该方法的注释,从中可以获得该方法的大部分特性。看完之后并思考之后,可以通过下面这个代码来测试一下是否完全理解,代码如下:
String a = "test";
String b = new String(a);
System.out.println(a == a.intern());
System.out.println(b == b.intern());
System.out.println(a == b);
System.out.println(a.intern() == b.intern());
如果你给出的答案是true、false、false和true,那么恭喜你,以后关于String内存布局的笔试面试题都难不住你了。如果答案没有完全匹配,请仔细查看我给出JVM内存布局模型简图并思考:
intern这个本地方法,对于存储于堆内存和方法区中的字符串对象,该方法都是获得字符串在JVM中的实际存储位置。一个Java应用中使用到的所有字符串值,实际上都只在JVM方法区的常量区中存储,堆内存中的String对象实际上并没有真正存储字符串值。如果理解了这个,上述代码的答案就显而易见了。
? ?最后,大家需要注意一下String类中trim()方法,源码如下:
public String trim() {
int len = value.length;
int st = 0;
char[] val = value; /* avoid getfield opcode */
while ((st < len) && (val[st] <= ‘ ‘)) {
st++;
}
while ((st < len) && (val[len - 1] <= ‘ ‘)) {
len--;
}
return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}
我相信大部分Java程序员学的第一门编程语言都是C\C++,那么自然就以为String类的trim()方法也是用来去除字符串的前后空白字符的。当仔细看一下trim()方法的源码,你可能会发现Java版本trim()方法去除的不仅仅是空白字符(空格、换行、制表符),而是所有Unicode码小于等于32(空格的Unicode码)的字符。
? ?通过本篇和上一篇教程对String类的讲解,相信你对JDK1.7中String类的构造结构和基本特性已经有了比较深入的理解。总结起来,String类中最关键的两个难点是字符编码和内存布局,相信各位读者都已经很好地掌握了。如果还有疑问和建议,欢迎给我留言。
本系列文档会在本人的微信公众号发布,欢迎大家扫码关注。
004 Java字符串的几个特性