不可小视的String字符串
String印象
String是java中的无处不在的类,使用也很简单。初学java,就已经有字符串是不可变的盖棺定论,解释通常是:它是final的。
不过,String是有字面量这一说法的,这是其他类型所没有的特性(除原生类型)。另外,java中也有字符串常量池这个说法,用来存储字符串字面量,不是在堆上,而是在方法区里边存在的。
字面量和常量池初探
字符串对象内部是用字符数组存储的,那么看下面的例子:
|
1
2
3
4
|
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。
我们可以使用反射修改字符数组来验证一下效果,可以试试下面的测试代码:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
@Testpublic 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类来处理。
另外,字符串常量通常是在编译的时候就确定好的,定义在类的方法区里边,也就是说,不同的类,即使用了同样的字符串, 还是属于不同的对象。所以才需要通过引用字符串常量来减少相同的字符串的数量。可以通过下面的代码来测试一下:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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这个常用方法,看下面的例子:
|
1
2
3
|
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小于字符数组长度),采用的是 拷贝新数组的方式,是比较特别的,不过这个构造方法也没什么机会使用到。
例如下面的代码:
|
1
2
3
|
String m = "hello,";String u = m.concat("world");String v = new String(m.substring(0,2)); |
得到的结构图如下:

可以发现,m,u,v内部的字符数组并不是同一个,有兴趣可以试验一下。
常量池中字符串的产生
常量池中的字符串通常是通过字面量的方式产生的,就像上述m语句那样。 并且他们是在编译的时候就准备好了,类加载的时候,顺便就在常量池生成。
可以通过javap命令检查一下class的字节码,可以发现下面的高亮部分(以上面代码为例):
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
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等于数组长度)。
即使是字符串的内容是一样的,都不能保证是同一个字符串数组。例如下面的代码:
|
1
2
3
|
String m = "hello,world";String u = m + ".";String v = "hello,world."; |
u和v虽然是一样内容的字符串,但内部的字符数组不是同一个。画成图的话就是这样的:

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

这应该怎么解释的?这其实都是编译器搞的鬼,因为m是final的, u直接被编译成”hello,world.”了,如果使用javap查看的话,会发现下面一段逻辑:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
const #2 = String #25; // hello,worldconst #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方法。 例如下面的用法
|
1
2
3
|
String m = "hello,world";String u = m.substring(0,2);String v = u.intern(); |
上面我们已经知道m,n使用的是同一个字符数组,但intern方法会到常量池里边去寻找字符串”he”,如果找到的话,就直接返回该字符串, 否则就在常量池里边创建一个并返回,所以v使用的字符数组和m,n不是同一个。画成图的话就是这样的:

字符串的内存释放问题
像字面量字符串,因为存放在常量池里边,被常量池引用着,是没法被GC的。例如下面的代码:
|
1
2
3
4
5
|
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方法让运行时产生字符串的复用常量池中的字符串
- 字符串操作可能会复用原字符数组,在某些情况可能造成内存泄露的问题
不可小视的String字符串的更多相关文章
- Java String字符串/==和equals区别,str。toCharAt(),getBytes,indexOf过滤存在字符,trim()/String与StringBuffer多线程安全/StringBuilder单线程—— 14.0
课程概要 String 字符串 String字符串常用方法 StringBuffer StringBuilder String字符串: 1.实例化String对象 直接赋值 String str=& ...
- [CareerCup] 1.3 Permutation String 字符串的排列
1.3 Given two strings, write a method to decide if one is a permutation of the other. 这道题给定我们两个字符串,让 ...
- 03-Java String字符串详解
1.Java字符串String A.实例化String字符串:直接赋值(更合理一些,使用较多).使用关键字new. B.String内容的比较 // TODO Auto-generated metho ...
- C++学习38 string字符串的增删改查
C++ 提供的 string 类包含了若干实用的成员函数,大大方便了字符串的增加.删除.更改.查询等操作. 插入字符串 insert() 函数可以在 string 字符串中指定的位置插入另一个字符串, ...
- C++学习37 string字符串的访问和拼接
访问字符串中的字符 string 字符串也可以像字符串数组一样按照下标来访问其中的每一个字符.string 字符串的起始下标仍是从 0 开始.请看下面的代码: #include <iostrea ...
- java String字符串——进度1
String字符串 在JAVA中提供了多种创建字符串对象的方法,这里介绍最简单的两种, 第一种是直接赋值, 第二种是使用String类的构造方法: 如下所示: Strin ...
- 关于String字符串反转
这是网上看到的一篇java面试题中的问题: 问题是: 如何将一个String字符串反转. String str = "1234567"; int length = str.leng ...
- JavaScript的内置对象(Date日期+string字符串)基础语法总结
1.Date日期对象可以储存任意一个日期,并且可以精确到毫秒数(1/1000 秒). 1)定义一个时间对象 : var Udate=new Date(); //注意:使用关键字new,Date()的首 ...
- 【转】String字符串相加的问题
String字符串相加的问题 前几天同事跟我说我之前写的代码中在操作字符串时候,使用字符串相加的方式而不是使用StringBuffer或者StringBuilder导致内存开销很大.这个问题一直在困扰 ...
随机推荐
- Putty CentOS SSH 总是自动断开连接
/********************************************************************** * Putty CentOS SSH 总是自动断开连接 ...
- 当Jaxb遇到泛型
前言: 最近的工作内容跟银行有些交互, 对方提供的数据格式采用xml(不是预期的json/protobuf). 为了开发方便, 需要借助jaxb来实现xml和java对象之间的映射. 它还是有点像ja ...
- 九度OJ1020-最小正方形-判大小
题目1020:最小长方形 时间限制:1 秒 内存限制:32 兆 特殊判题:否 提交:7410 解决:3521 题目描述: 给定一系列2维平面点的坐标(x, y),其中x和y均为整数,要求用一个 ...
- HDU 2048:神、上帝以及老天爷(错排公式,递推)
神.上帝以及老天爷 Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others) Total ...
- FTP相关、用vsftpd搭建ftp、xshell使用xftp传输文件、使用pure-ftpd搭建ftp服务
1.FTP相关(file transfer protocol,文件传输协议) 2.用vsftpd搭建ftp安装:yum install vsftpd -y创建一个虚拟用户:useradd vft ...
- PTA——时间转换
PTA 7-14 然后是几点 #include<stdio.h> int main() { int a,b,hour,min; scanf("%d%d",&a, ...
- net框架平台下RPC框架选型
net RPC框架选型 近期开始研究分布式架构,会涉及到一个最核心的组件:RPC(Remote Procedure Call Protocol).这个东西的稳定性与性能,直接决定了分布式架构系统的好坏 ...
- java-方法重写的注意事项
1.父类中私有方法不能被重写.因为父类的私有方法子类根本就无法继承. 2.子类重写父类方法时,访问权限不能更低.最好就一致. 3.父类静态方法,子类也必须通过静态方法进行重写.其实这个算不上方法重写, ...
- 【HDOJ1051】【排序+LIS】【贪心】
http://acm.hdu.edu.cn/showproblem.php?pid=1051 Wooden Sticks Time Limit: 2000/1000 MS (Java/Others) ...
- hdu 1556 A - Color the ball 数状数组做法
#include<bits/stdc++.h> using namespace std; ; int n; int c[maxn]; int lowbit(int x) { return ...