初探Java字符串
转载: 初探Java字符串
String印象
String是java中的无处不在的类,使用也很简单。初学java,就已经有字符串是不可变的盖棺定论,解释通常是:它是final的。
不过,String是有字面量这一说法的,这是其他类型所没有的特性(除原生类型)。另外,java中也有字符串常量池这个说法,用来存储字符串字面量,不是在堆上,而是在方法区里边存在的。
字面量和常量池初探
字符串对象内部是用字符数组存储的,那么看下面的例子:
String m = "hello,world";
String n = "hello,world";
String u = new String(m);
String v = new String("hello,world");
这些语句会发生什么事情? 大概是这样的:
- 会分配一个11长度的char数组,并在常量池分配一个由这个char数组组成的字符串,然后由m去引用这个字符串。
- 用n去引用常量池里边的字符串,所以和n引用的是同一个对象。
- 生成一个新的字符串,但内部的字符数组引用着m内部的字符数组。
- 同样会生成一个新的字符串,但内部的字符数组引用常量池里边的字符串内部的字符数组,意思是和u是同样的字符数组。
如果我们使用一个图来表示的话,情况就大概是这样的(使用虚线只是表示两者其实没什么特别的关系):

结论就是,m和n是同一个对象,但m,u,v都是不同的对象,但都使用了同样的字符数组,并且用equal判断的话也会返回true。
我们可以使用反射修改字符数组来验证一下效果,可以试试下面的测试代码:
@Test
public void test1() throws Exception {
String m = "hello,world";
String n = "hello,world";
String u = new String(m);
String v = new String("hello,world");
Field f = m.getClass().getDeclaredField("value");
f.setAccessible(true);
char[] cs = (char[]) f.get(m);
cs[0] = 'H';
String p = "Hello,world";
Assert.assertEquals(p, m);
Assert.assertEquals(p, n);
Assert.assertEquals(p, u);
Assert.assertEquals(p, v);
}
从上面的例子可以看到,经常说的字符串是不可变的,其实和其他的final类还是没什么区别,还是引用不可变的意思。 虽然String类不开放value,但同样是可以通过反射进行修改,只是通常没人这么做而已。 即使是涉及”修改”的方法,都是通过产生一个新的字符串对象来实现的,例如replace、toLower、concat等。 这样做的好处就是让字符串是一个状态不可变类,在多线程操作时没有后顾之忧。
当然,在字符串修改的时候,会产生一个新的对象,如果执行很频繁,就会导致大量对象的创建,性能问题也就随之而来了。 为了应付这个问题,通常我们会采用StringBuffer或StringBuilder类来处理。
另外,字符串常量通常是在编译的时候就确定好的,定义在类的方法区里边,也就是说,不同的类,即使用了同样的字符串, 还是属于不同的对象。所以才需要通过引用字符串常量来减少相同的字符串的数量。可以通过下面的代码来测试一下:
class A {
public void print() {
System.out.println("hello");
}
}
class B {
public void print() {
String s = "hello";
// 修改s的第一个字符为H
System.out.println("hello"); // 输出Hello
new A().print(); // 输出hello
}
}
字符串操作细节
String类内部处理有个字符数组之外,还使用偏移位置offset和长度count, 通过offset和count来确定字符数组的一部分,这部分才是这个字符串的真正的内容。 例如,有substring这个常用方法,看下面的例子:
String m = "hello,world";
String u = m.substring(2,10);
String v = u.substring(4,7);
按照上面的说法,m,n的数据结构就如下图所示:

可以发现,m,n,v是三个不同的字符串对象,但引用的value数组其实是同一个。 同样可以通过上述反射的代码进行验证,这里就不详述了。
但字符串操作时,可能需要修改原来的字符串数组内容或者原数组没法容纳的时候,就会使用另外一个新的数组,例如replace,concat,+等操作。另外,oracle的JDK实现中,String的构造方法,对于字符串参数只是引用部分字符数组的情况(count小于字符数组长度),采用的是拷贝新数组的方式,是比较特别的,不过这个构造方法也没什么机会使用到。
例如下面的代码:
String m = "hello,";
String u = m.concat("world");
String v = new String(m.substring(0,2));
得到的结构图如下:

可以发现,m,u,v内部的字符数组并不是同一个,有兴趣可以试验一下。
常量池中字符串的产生
常量池中的字符串通常是通过字面量的方式产生的,就像上述m语句那样。 并且他们是在编译的时候就准备好了,类加载的时候,顺便就在常量池生成。
可以通过javap命令检查一下class的字节码,可以发现下面的高亮部分(以上面代码为例):
javap -v StringTest
Compiled from "StringTest.java"
public class com.github.mccxj.StringTest extends java.lang.Object
SourceFile: "StringTest.java"
minor version: 0
major version: 50
Constant pool:
const #1 = Method #9.#28; // java/lang/Object."<init>":()V
+ const #2 = String #29; // hello,
+ const #3 = String #30; // world
...
+ const #46 = Asciz hello,;
+ const #47 = Asciz world;
...
大家不知有没有发现,上面的图中,u和v的字符数组没有被常量池里边的字符串引用到。 原因就是这些字符串(字符数组)都是运行时生成的,而常量池里边的字符串和字符数组是完整对应上的(count等于数组长度)。
即使是字符串的内容是一样的,都不能保证是同一个字符串数组。例如下面的代码:
String m = "hello,world";
String u = m + ".";
String v = "hello,world.";
u和v虽然是一样内容的字符串,但内部的字符数组不是同一个。画成图的话就是这样的:

另外有一点,如果让m声明为final,你就会发现u和v变成是同一个对象。画成图的话就是这样的:

这应该怎么解释的?这其实都是编译器搞的鬼,因为m是final的, u直接被编译成”hello,world.”了,如果使用javap查看的话,会发现下面一段逻辑:
const #2 = String #25; // hello,world
const #3 = String #26; // hello,world.
...
public void test1() throws java.lang.Exception;
Code:
Stack=1, Locals=4, Args_size=1
0: ldc #2; //String hello,world
2: astore_1
3: ldc #3; //String hello,world.
5: astore_2
6: ldc #3; //String hello,world.
8: astore_3
9: return
那么,如何让运行时产生的字符串放到常量池里边呢? 可以借助String类的intern方法。 例如下面的用法
String m = "hello,world";
String u = m.substring(0,2);
String v = u.intern();
上面我们已经知道m,n使用的是同一个字符数组,但intern方法会到常量池里边去寻找字符串”he”,如果找到的话,就直接返回该字符串, 否则就在常量池里边创建一个并返回,所以v使用的字符数组和m,n不是同一个。画成图的话就是这样的:

字符串的内存释放问题
像字面量字符串,因为存放在常量池里边,被常量池引用着,是没法被GC的。例如下面的代码:
String m = "hello,world";
String n = m.substring(0,2);
m = null;
n = null;
经过上述的操作,画成图的话就是这样的:

而经过上面的分析,我们知道像substring、split等方法得到的结果都是引用原字符数组的。 如果某字符串很大,而且不是在常量池里存在的,当你采用substring等方法拿到一小部分新字符串之后,长期保存的话(例如用于缓存等), 会造成原来的大字符数组意外无法被GC的问题。
关于这个问题,常见的解决办法就是使用new String(String original)或java.io.StreamTokenizer类。并且在网上已经有比较广泛的讨论,大家可以去阅读一下:
结论
- 任何时候,比较字符串内容都应该使用equals方法
- 修改字符串操作,应该使用StringBuffer,StringBuilder
- 可以使用intern方法让运行时产生字符串的复用常量池中的字符串
- 字符串操作可能会复用原字符数组,在某些情况可能造成内存泄露的问题
初探Java字符串的更多相关文章
- Java 字符串格式化详解
Java 字符串格式化详解 版权声明:本文为博主原创文章,未经博主允许不得转载. 微博:厉圣杰 文中如有纰漏,欢迎大家留言指出. 在 Java 的 String 类中,可以使用 format() 方法 ...
- (转)Java字符串
转自:http://blog.sina.com.cn/s/blog_899678b90101brz0.html 创建字符串有两种方式:两种内存区域(字符串池,堆)1," " 引号创 ...
- Java字符串split函数的注意事项
Java字符串的split方法可以分割字符串,但和其他语言不太一样,split方法的参数不是单个字符,而是正则表达式,如果输入了竖线(|)这样的字符作为分割字符串,会出现意想不到的结果, 如, Str ...
- 关于java字符串编译优化问题
情景一:不好的字符串拼接习惯 起因是这样的:一个大牛在写了一篇关于java字符串优化问题的讲解,他提到:不要使用strObj+otherValue的方法将otherValue转换为字符串形式,因 ...
- Java字符串排列算法
Java字符串排列算法 题目:现有ABCDE 5个球 构成的排列组合 可重复抽取 最多取到16个 共有多少种组合方式? 比如:取1个球可以构成的组合有 A B C D E 共5种,取2个球可以构成的组 ...
- Java字符串转换
public class StringConvertToInt{ public static void main(String[] args) { String a ="12a34bW()5 ...
- Java字符串null相加
Java字符串null相加 最近和同事讨论了下面的一段代码: String a = null; a += a; System.out.println(a); 运行结果: nullnull 本着学习的态 ...
- JAVA字符串格式化String.format()的使用
JAVA字符串格式化-String.format()的使用常规类型的格式化 String类的format()方法用于创建格式化的字符串以及连接多个字符串对象.熟悉C语言的同学应该记得C语言的sprin ...
- Java字符串的10大热点问题,你都懂吗?
转自 威哥干JAVA http://www.codingke.com 下面我为大家总结了10条Java开发者经常会提的关于Java字符串的问题,如果你也是Java初学者,仔细看看吧: 1.如何比较字符 ...
随机推荐
- 2015/10/9 Python基础(21):可调用和可执行对象
在Python中有多种运行外部程序的方法,比如,运行操作系统命令或另外的Python脚本,或执行一个磁盘上的文件,或通过网络来运行文件.这完全取决于想要干什么.特定的环境包括: 在当前脚本继续运行 创 ...
- FTP、SFTP文件下载内容校验
描述: 从FTP.SFTP下载的文件做MD5码校验,文件名和MD5码值存放在表格里,表格位置在FTP.SFTP服务器上. os模块只能遍历本地目录/文件,需要先连接FTP.SFTP服务器,将表格下载到 ...
- JavaScript定义类的几种方式
提起面向对象我们就能想到类,对象,封装,继承,多态.在<javaScript高级程序设计>(人民邮电出版社,曹力.张欣译.英文名字是:Professional JavaScript for ...
- 51nod1149 Pi的递推式
基准时间限制:1 秒 空间限制:131072 KB 分值: 640 F(x) = 1 (0 <= x < 4) F(x) = F(x - 1) + F(x - pi) (4 <= x ...
- 零基础讲解JavaScript函数
一 JavaScript函数1 什么是函数 函数是一组代码(指令)的集合,通常用来完成某个单一的功能.(书的目录和章节,电视剧剧集的名称等)2 为什么要使用函数 2.1 把复杂程序划分成不同的功能 ...
- filezilla显示隐藏文件
我们在习惯使用flashfxp等工具,但是随着主机商限制较多,这些老的FTP工具不怎么好用了,比如主机商会推荐使用到Filezilla等工具.但是比如息壤主机,我们在管理linux环境下htacess ...
- perl6 HTTP::UserAgent (3) JSON
如果一个 URL 要求POST数据是 JSON格式的, 那我们要怎么发送数据呢? 第一种: HTTP::Request 上一篇说到, 发送 POST 数据, 可以: . $ua.post(url, % ...
- C语言回调函数总结
/* Main program ---calls--> Library function ---calls--> Callback funtion */ #include <stdi ...
- S3C6410 SPI全双工读写流程分析(原创)【转】
转自:http://blog.csdn.net/hustyangju/article/details/21165721 原创博文,知识共享!转载请注明出处:http://blog.csdn.net/h ...
- 一个python爬虫工具类
写了一个爬虫工具类. # -*- coding: utf-8 -*- # @Time : 2018/8/7 16:29 # @Author : cxa # @File : utils.py # @So ...