Java 干货之深入理解String
可以证明,字符串操作是计算机程序设计中最常见的行为,尤其是在Java大展拳脚的Web系统中更是如此。
---《Thinking in Java》
提到Java中的String,总是有说不完的知识点,它对于刚接触Java的人来说,有太多太多的值得研究的东西了,可是为什么Java中的String这么独特呢?今天我们来一探究竟。
基本数据类型
众所周知Java有八大基本数据类型,那么基本数据类型与对象有什么异同呢?
- 基本数据类型不是对象
- 基本数据类型能直接存储变量对应的值在堆栈中,存取更加高效
- 使用方便,不用new创建,每次表示都不用新建一个对象
字面量与赋值
什么叫字面值呢?考虑下面代码:
int a=3;
double d=3.32;
long l=102322332245L;
其中,3、3.32、102322332245L便叫做字面值。3默认是int类型,3.32默认是double类型,102322332245默认也是int类型,所以必须加一个L来将它修改为long类型,否则编译器就会报错,字面量可以直接在计算机中表示。
基本数据类型便可以直接通过字面值进行赋值
String与基本数据类型
话说了这么多,这和String有什么关系呢?正如本文最开始所说,因为Java需要常常与字符串打交道,因此Java的设计者想要将String类型在使用上和性能上尽量像基本数据类型一样。
也就是
int i=0;
String str="test";
那么问题来了,基本数据类型之所以叫基本数据类型,是因为这种类型可以直接在计算机中直接被表示,比如int i=0;
中,0作为字面值是可以直接被表示出来"0x00",而test
作为字符串如何直接被表示呢?
常量池
JVM的解决方案便是Class文件常量池。Class常量池中主要用于存放两大类常量: 字面量和符号引用量,其中字面量便包括了文本字符串。
也就是当我们在类中写下String str="test"
的时候,Class文件中,就会出现test
这个字面量。
而String
类型的特殊性在于它只需要一个utf8表示的内容即可。
这样便解决了String
直接赋值的问题,只要在JVM中,将str与test
字面量对应起来即可。
也就是类似
int a=0; //a 的值为数值0
String str="test" //str内容为常量池中的utf8 test
但是,问题就真的这么简单么?
可别忘了,String
也是一个对象,它也同时拥有所有一个对象应该拥有的特点,也就说
String str="test"
其中test
字面量不仅仅需要表示str
指向的内容是test
,它还应该将str指向一个对象,支持类似str.length(),str.replace()
等一切对象访问的操作。
将test
的内容写在 Class
文件中仅仅解决的是如果赋值的问题,那String对象是如何在内存中存在呢?
String创建过程
打开java.lang.String
文件,可以看到String
拥有不可变对象的所有特点, final
修饰的类, final
修饰的成员变量,因此任何看似对String内容进行操作的方法,实际上都是返回了一个新的String对象,这就造就了一个String对象的被创建后,就一直会保持不变。
正因为String
这样的特点,我们可以建立一个对String的对象的缓存池:String Pool
,用来缓存所有第一次出现的String
对象。
JVM规范中只要求了
String Pool
,但并没有要求如何实现,在Hot Spot JVM
中,是通过类似一个HashSet<String>
实现,里面存储是当前已存储的String对象的引用:
String str="test";
首先虚拟机会在String Pool
中查找是否有equals("test")
的String 引用,如果有,就把字符串常量池里面对"test"
对象的引用赋值给str
。如果不存在,就在堆中新建一个"test"对象,并将引用驻留在字符串常量池(String Pool
)中,同时将该引用复制给str
。
可以看到,Java在这里是使用的String缓存对象来解决“字面值”性能这个问题的。也就是说,"test"所对应的字面值其实是一个在字符串常量池的String对象这样做只要出现过一次的String对象,第二次就不再会被创建,节约了很大一笔开销,便解决了String类似基本数据类型的性能问题。
深入理解String
明白了String的前因后果,现在来梳理关于String的细节问题。
String str="test"
包含了3个“值”:
"test"
字面量,表示String对象所存储的内容,编译后存放在Class字节码中,运行时存放在Class
对象中,而Class
对象存储在JVM的方法区中test
对象,存储在堆中test
对象对应的引用,存储在String Pool
中。
如图所示:
其中
一定注意str所指向的对象是存放在堆中的,网上大多数说的不明白,更有误导
String Pool
中存储的是对象的说法。Java 对象,排除逃逸分析的优化,所有对象都是存储在堆中的。String Pool
位于JVM 的None Heap
中,并且String Pool
中的引用持有对堆中对应String对象的引用,因此不必担心堆中的String对象是被GC回收。网上很多文章还会说
test
字面值是存在Perm Gen
中,但是这样并不准确,永生代(“Perm Gen”)只是Sun JDK的一个实现细节而已,Java语言规范和Java虚拟机规范都没有规定必须有“Permanent Generation”这么一块空间,甚至没规定要用什么GC算法——不用分代式GC算法哪儿来的“永生代”? HotSpot的PermGen是用来实现Java虚拟机规范中的“方法区”(method area)的。前面说过,Java想将String向基本数据类型靠近,还能体现在对
final String
对象的处理,对于final String
,如果使用仅仅是字面值的作用,而并没有涉及到对象操作的话(使用对象访问操作符"."),编译器会直接将对应的值替换为相应字面值。举例:
对于final String str="hello";
String helloWorld=str+"world";
编译器会直接优化:
String helloWorld="helloworld";
对于
final String str="hello";
String hello=String.valueOf(str);
编译器会直接优化
String hello=String.valueOf("hello");
如果没有编译器的优化,就会涉及到操作数压栈出栈等操作,但是经过优化后的String,可以发现并不会有astore/aload等指令的出现.
new String()
其实new String
没什么好说的,new String()
表示将String
完全作为一个对象来看,放弃它的基本数据类型性质,也与String Pool
没有任何关系,但是String
包含的intern()
方法能将它与String Pool
关联起来。
- jdk 1.7之前,
intern()
表示若String Pool
中不存在该字符串,则 在堆中新建一个与调用intern()
对象的字面值相同的对象,并在String Pool
中保存该对象的引用,同时返回该对象,若存在则直接返回。 - jdk 1.7及1.7 之后,
intern()
表示将调用intern()
对象的引用直接复制一份到String Pool
中。
网上很多讨论涉及到几个对象
String str=new String("hello world");
下面图解分析:
需要明白的一点的是
new String("hello world")
并不是一个原子操作,可以将其分为两步,每个关键字负责不同的工作其中new
负责生成对象,String("hello world")
负责初始化new
生成的对象。
- 首先,执行
new
操作,在堆中分配空间,并生成一个String
对象。 - 其次,将
new
生成的对象的引用传递给String("hello world")
方法进行初始化,而此时参数中出现了"hello world"
字面量,JVM会先在字符串常量池里面检查是否有equals("hello world")
的引用,如果没有,就在堆中创建相应的对象,并生成一个引用指向这个对象,并将此引用存储在字符串常量池
中。
- 再次,复制常量池
hello world
指向的字面量对象传递给new String("hello world")
进行初始化。
第二点中提到了复制,其实最主要的就是复制
String
对象中value
所指向的地址,也就是将方法区中的"hello world"
的索引复制给新的对象,这也是为什么上图中,两个对象都指向方法区中同一个位置
下面的String str=new String("hello world")
进行反编译的结果:
0: new #2 // class java/lang/String
3: dup
4: ldc #3 // String hello world
6: invokespecial #4 // Method java/lang/String."<init>":(Ljava/lang/String;)V
9: astore_1
10: return
大概的指令应该都能看到,解释一下:
- new 执行new 操作,在堆中分配内存
- dup 将new 操作生成的对象压栈
- ldc 将String型常量值从常量池中推送至栈顶
- invokespecial 调用
new String()
并传入new 出来的对象了ldc的String值
ldc指令是什么东西?
简单地说,它用于将int、float或String型常量值从常量池中推送至栈顶,在这里也能看到,JVM是将String
和八大基本数据类型统一处理的。
ldc 还隐藏了一个操作:也就是"hello world"的resolve操作,也就是检测“hello world”是否已经在常量池中存在的操作。
传送门详见:Java 中new String("字面量") 中 "字面量" 是何时进入字符串常量池的?
有个很神奇的坑,《深入理解JVM》中曾经提到过这个问题,不过周志明老师是拿的"java"作为举例:
代码如下(jdk 1.7)
```
public class RuntimeConstantPoolOOM {
public static void main(String[] args) {
String str1 = new StringBuilder("计算机").append("软件").toString();
System.out.println(str1.intern() == str1);
String str2 = new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern() == str2);
}
}
```
结果
true
false
不明白为什么"java"字符串会在执行StringBuilder.toString()之前出现过?
其实是因为:Java 标准库在JVM启动的时候加载的某些类已经包含了java
字面量。
传送门:如何理解《深入理解java虚拟机》第二版中对String.intern()方法的讲解中所举的例子?
方法区
上面图中说了,“hello wold”
对象的value
的值是放在方法区中的。如何证明呢?
这里我们可以使用反射来干一些坏事。
虽然String
类是一个不可变对象,对外并没有提供如何修改value
的方法,但是反射可以。
String str1=new String("abcd");
String str2="abcd";
String str3="abcd";
Field valueField = String.class.getDeclaredField("value");
valueField.setAccessible(true);//设置访问权限
char[] value = (char[]) valueField.get(str2);
value[0] = '0';
value[1] = '1';
value[2] = '2';
value[3] = '3';
System.out.println(str1);
System.out.println(str2);
System.out.println(str3);
String str4="abcd";
System.out.println(str4);
System.out.println("abcd");
可以试一试,输出结果都是0123
,因为在编译的时候生成Class
对象的时候,str1,str2,str3,str4
都是指向的Class
文件中同一个位置,而在运行的时候这个Class
对象的值被修改后,所有和abcd
有关的对象的value
都会被改变。
相信理解了这一个例子的同学,能够对String
有一个更加深刻的理解
检验
说了这么多,你真的懂了么?来看看经常出现的一些关于String
的问题:
String str1 = new StringBuilder("Hel").append("lo").toString();
System.out.println(str1.intern() == str1);
String str = "Hello";
System.out.println(str == str1);
String str1="hello";
String str2=new String("hello");
System.out.println(str2 == str1);
final String str1="hell";
String str2="hello";
String str3=str1+"o";
System.out.println(str2 == str3);
String str1="hell";
String str2="hello";
String str3=str1+"o";
System.out.println(str2 == str3);
尊重原创,转载注明出处
参考文章:
《深入理解JVM》 第二版
java用这样的方式生成字符串:String str = "Hello",到底有没有在堆中创建对象?
R大:请别再拿“String s = new String("xyz");创建了多少个String实例”来面试了吧
Java 中new String("字面量") 中 "字面量" 是何时进入字符串常量池的?
Java 干货之深入理解String的更多相关文章
- Java 干货之深入理解Java内部类
可以将一个类定义在另一个类或方法中,这样的类叫做内部类 --<Thinking in Java> 说起内部类,大家并不陌生,并且会经常在实例化容器的时候使用到它.但是内部类的具体细节语法, ...
- Java 干货之深入理解Java泛型
一般的类和方法,只能使用具体的类型,要么是基本类型,要么是自定义的类.如果要编写可以应用多中类型的代码,这种刻板的限制对代码得束缚会就会很大. ---<Thinking in Java> ...
- Java基础系列2:深入理解String类
Java基础系列2:深入理解String类 String是Java中最为常用的数据类型之一,也是面试中比较常被问到的基础知识点,本篇就聊聊Java中的String.主要包括如下的五个内容: Strin ...
- 谈谈我对Java中CallBack的理解
谈谈我对Java中CallBack的理解 http://www.cnblogs.com/codingmyworld/archive/2011/07/22/2113514.html CallBack是回 ...
- java字符串函数及理解
Java中的字符串也是一连串的字符.但是与许多其他的计算机语言将字符串作为字符数组处理不同,Java将字符串作为String类型对象来处理.将字符串作为内置的对象处理允许Java提供十分丰富的功能特性 ...
- Java学习笔记20(String类应用、StringBuffer类、StringBuilder类)
1.获取指定字符串中大小写和数字的个数: package demo; public class StringTest { public static void main(String[] args) ...
- Java提高篇之理解java的三大特性——继承
在<Think in java>中有这样一句话:复用代码是Java众多引人注目的功能之一.但要想成为极具革命性的语言,仅仅能够复制代码并对加以改变是不够的,它还必须能够做更多的事情.在这句 ...
- 【转】java提高篇(二)-----理解java的三大特性之继承
[转]java提高篇(二)-----理解java的三大特性之继承 原文地址:http://www.cnblogs.com/chenssy/p/3354884.html 在<Think in ja ...
- Java 对象 引用,equal == string
以前确实一直没注意这个概念,这次看了帖子才知道. 转载于:https://zwmf.iteye.com/blog/1738574 Java对象及其引用 关于对象与引用之间的一些基本概念. 初学Java ...
随机推荐
- 超链接target属性的取值和作用?
<a>标签的target属性规定在何处打开连接文档 属性值 _black:点击一次打开一个新窗口 _new:始终在同一个新窗口中打开 _self:默认,在当前窗口打开 _parent:在父 ...
- e课表项目第二次冲刺周期第四天
昨天干了什么? 昨天,我在网上搜集了相关的资料,即连接安卓自带的数据库,查询了连接的方法,然后在电脑上,做了简单的练习,发现可以用,所以对我们的软件进行数据库的连接,设置了完成按钮的活动,即先保存到数 ...
- Spring中@Import的各种用法以及ImportAware接口
@Import 注解 @Import注解提供了和XML中<import/>元素等价的功能,实现导入的一个或多个配置类.@Import即可以在类上使用,也可以作为元注解使用. @Target ...
- 深度长文回顾web基础组件
什么是Serlvet ? 全称 server applet 运行在服务端的小程序: 首先来说,这个servlet是java语言编写的出来的应用程序,换句话说servlet拥有java语言全部的优点,比 ...
- Bran的内核开发教程(bkerndev)-02 准备工作
准备工作 内核开发是编写代码以及调试各种系统组件的漫长过程.一开始这似乎是一个让人畏惧的任务,但是并不需要大量的工具集来编写自己的内核.这个内核开发教程主要涉及使用GRUB将内核加载到内存中.GR ...
- shark恒破解笔记3-EAX决定胜负
PEID查壳 od载入 输入假的注册码 查找出错字符串 往上查找是否有关键跳转和关键call 可以看到此处有个je跳转 实现了跳转,并且跳过了我们注册成功的地址 网上查找这个跳转的关键call,这个c ...
- JSON:JSON对象和JSON数组混排的复杂字符串
在java中的一个好用的JSON工具包:net.sf.json.JSONObject 和 net.sf.json.JSONArray 一 解析JSON对象和JSON数组类型混排的复杂字符串 举个例子: ...
- Qt 找不到rc.exe
Qt在window下出现编译错误: LINK : fatal error LNK1158: 无法运行“rc.exe” 解决: 找到rc.exe的放置路径,比如我的在下面: C:\Program Fil ...
- PMBOK(第六版) PMP笔记-质量审计、风险审计、采购审计的区分
质量审计.风险审计.采购审计的区分 三个概念的相同之处: 都是审计的概念 都是特定知识领域的审计 三个概念的区别: (1)三个概念虽然都是审计,但分布在不同的管理过程组. ·质量审计:执行过程组, ...
- 单调队列与DP
算是一个总结吧! 先来一个模板: TYVJ 1305 最大子序和 题目描述 输入一个长度为n的整数序列,从中找出一段不超过M的连续子序列,使得整个序列的和最大. 例如 1,-3,5,1,-2,3 当m ...