大家好,又见面了,我是你们的朋友全栈君。
文章目录
-
-
-
- 字符串常量池
- String 与 CharSequence区别
- 概述String,StringBuffer与StringBuilder的区别
- 细说一下String
- 创建了几个对象
- 到底相等不相等
-
- String有没有线程安全问题
- 总结
-
-
字符串常量池
- 什么是字符串常量池?
JVM为了减少字符串对象的重复创建,其维护了一块特殊的内存,这段内存被称为字符串常量池(存储在
方法区
中)。 - 具体实现
当代码中出现字符串时,JVM首先会对其进行检查。
- 如果字符串常量池中存在相同内容的字符串对象,如果有,则不再创建,直接返回
这个对象的地址返回
。 - 如果字符串常量池中不存在相同内容的字符串对象,则
创建一个新的字符串对象并放入常量池
,并返回新创建的字符串的引用地址。 - new String(“str”)时,首先也会去检查常量池是否存在“str”(存在则不创建、不存在则在常量池先创建一个),然后在堆空间再开辟一块内存区域创建字符串对象 。
- 如果字符串常量池中存在相同内容的字符串对象,如果有,则不再创建,直接返回
String 与 CharSequence区别
- String 继承于CharSequence,也就是说String也是CharSequence类型。
- CharSequence是一个接口,它只包括
length()
,charAt(int index)
,subSequence(int start, int end)
这几个API接口。 - 除了String实现了CharSequence之外,
StringBuffer和StringBuilder
也实现了CharSequence接口。 - CharSequence就是字符串,String, StringBuilder和StringBuffer本质上都是通过字符数组实现的!
- CharSequence与String都能用于定义字符串,但CharSequence的值是
可读可写字符串
,而String的值是只读字符串
。
概述String,StringBuffer与StringBuilder的区别
String在java编程中广泛应用,首先从源码进行分析
String底层是一个final类型的字符数组
,所以String的值是不可变的
,每次对String的操作都会生成新的String对象
,造成内存浪费 而StringBuffer和StringBuilder
就不一样了,他们两都继承了AbstractStringBuilder抽象类
,从AbstractStringBuilder抽象类中我们可以看到
他们的底层都是可变的字符数组
,所以在进行频繁的字符串操作时,建议使用StringBuffer和StringBuilder来进行操作。
接着看一下他们的继承结构以及部分源码实现
StringBuffer.apped()方法是线程安全的
StringBuilder.apped()方法线程不安全
从这三张图我们不难得知:
- StringBuilder 类在 Java 5 中被提出,它和 StringBuffer 之间的最大不同在于 StringBuilder 的方法不是线程安全的(不能同步访问)
- 由于 StringBuilder 相较于 StringBuffer 有速度优势,所以
多数情况下建议使用 StringBuilder 类
。然而在应用程序要求线程安全的情况下,则必须使用 StringBuffer 类。
细说一下String
- String并不是基本数据类型,而是一个对象。
- 字符串为对象,那么在初始化之前,它的值为null,到这里就有必要提下””、null、new String()三者的区别。
null 表示string还没有new
,也就是说对象的引用还没有创建
,也没有分配内存空间给他
,- 而””、new String()则说明了已经new了,只不过内部为空,但是它创建了对象的引用,是需要分配内存空间的。
java的虚拟机在内存中开辟出一块单独的区域,用来存储字符串对象,这块内存区域被称为字符串常量池(字符串缓冲池)
。那个java的字符串缓冲池是如何工作的呢?
String a = "abc";
String b = "abc";
String c = new String("xyz");
代码语言:javascript复制String a = "abc";
- 创建字符串的时候先查找
字符串常量池
中有没有相同的对象
,如果有
相同的对象就直接返回该对象的引用
,如果没有
相同的对象就在字符串常量池中创建该对象
,然后将该对象的引用返回
。对于这一步而言,缓冲池中没有abc这个字符串对象,所以首先创建一个字符串对象,然后将对象引用返回给a。
String b = "abc";
- 这一句也是想要创建一个
对象引用变量b
使其指向abc
这一对象。这时,首先查找字符串常量池
,发现abc这个对象已经有了
,这是就直接将这个对象的引用返回给b
,此时a和b就共用
了一个对象abc,不过不用担心,a改变了字符串不会影响b,因为字符串都是常量
,一旦创建就没办法修改了,除非创建一个新的对象。
String c = new String("xyz");
String c = new String(“xyz”); JVM首先是在字符串常量池中找”xyz” 字符串,如果没有创建字符串常量,然后放到常量池中,若已存在,则不需要创建;当遇到 new 时,还会在内存(不是字符串常量池中)上创建一个新的String对象,存储”Hello”,并将内存上的String对象引用地址返回。
从上边的分析可以看出,当new一个字符串时并不一定是创建了一个新的对象
,有可能是与别的引用变量共同使用了同一个对象
。下面看几个常见的有关字符串常量池的问题。
创建了几个对象
代码语言:javascript复制String a = "abc";
String b = "abc";
String c = new String("xyz");
String d = new String("xyz");
String e = "ab" "cd";
这个程序与上边的程序比较相似,我们分比来看一下:
- String a = “abc”;这一句由于常量池中没有abc这个字符串对象,所以会创建一个对象;
- String b = “abc”;由于常量池中已经有了abc这个对象,所以不会再创建新的对象;
- String c = new String(“xyz”);由于常量池没有xyz这个字符串对象,所以会首先创建一个xyz的对象,然后放到常量池中,然后new的时候,在内存中(不是常量池中)又创建了一个新的字符串对象,所以一共创建了两个对象;
4、String d = new String(“xyz”);常量池中已有该字符串对象,则常量池中不再创建该对象,然后会在new的时候内存中创建一个新的字符串对象,所以只创建了一个对象;
5、String e = ”ab” ”cd”;由于常量的值在编译的时候就被确定了。所以这一句等价于String e = ”abcd”;所以创建了一个对象;
所以创建的对象的个数分别是:1,0,2,1,1
。
了解了String类的工作原理,回归问题本身。 在String的工作原理中,已经提到了,new 一个String对象,是需要先在字符串常量中查找相同值或创建一个字符串常量,然后再在内存中创建一个String对象,所以 String str = new String(“xyz”); 会创建两个对象。
到底相等不相等
我们知道两个字符串对象相等的判断要用equal
而不能使用==
,但是学习了字符串常量池以后,应该知道为什么不能用==
, 什么情况下==
和equal
是等价
的>
首先,必须知道的是
- equal比较的是两个字符串的
值
是否相等 - ==比较的是两个对象的
内存地址
是否相等
实例一:
代码语言:javascript复制public static void main(String[] args) {
String s1 = "money";
String s2 = "money";
if (s1 == s2) {
System.out.println("s1 == s2");
} else {
System.out.println("s1 != s2");
}
}
执行结果: s1 == s2
分析: 通过对字符串常量池的了解,我们知道s1和s2都是指向字符串常量池中的同一个对象
,所以内存地址是一样的
,所以用==
可以判断两个字符串是否相等。
实例二:
代码语言:javascript复制public static void main(String[] args) {
String s1 = "money";
String s2 = new String("money");
if (s1 == s2) {
System.out.println("s1 == s2");
} else {
System.out.println("s1 != s2");
}
if (s1.equals(s2)) {
System.out.println("s1 equals s2");
} else {
System.out.println("s1 not equals s2");
}
}
执行结果:
s1 != s2
s1 equals s2
分析: String s2 = new String(“money”);这一句话没有
在字符串常量池
中创建新的对象,但是会在内存的其他位置创建一个新的对象
,所以s1是指向字符串常量池的
,s2是指向内存的其他位置
,两者的内存地址不同的
。
实例三:
代码语言:javascript复制public static void main(String[] args) {
String s1 = "money";
String s2 = new String("money");
s2 = s2.intern();
if (s1 == s2) {
System.out.println("s1 == s2");
}else {
System.out.println("s1 != s2");
}
if (s1.equals(s2)) {
System.out.println("s1 equals s2");
}else {
System.out.println("s1 not equals s2");
}
}
输出结果:
s1 == s2
s1 equals s2
分析: 先来说说intern()
这个方法的作用吧,这个方法的作用是返回在字符串常量池中的对象的引用
,所以s2指向的也是字符串常量池中的地址
,和s1是相等的。
intern()方法:返回在字符串常量池中的对象的引用
实例四:
代码语言:javascript复制public static void main(String[] args) {
String Monday = "Monday";
String Mon = "Mon";
String day = "day";
System.out.println(Monday == "Mon" "day");
System.out.println(Monday == "Mon" day);
}
输出结果:
true
false
分析: 第一个为什么等于true我们已经说过了,因为两者都是常量所以在编译阶段就已经能确定了
,在第二个中,day是一个变量,所以不能提前确定他的值,所以两者不相等,从这个例子我们可以看出,只有 连接的两边都是字符串常量时,引用才会指向字符串常量池
,否则都是指向内存中的其他地址。
实例五:
代码语言:javascript复制public static void main(String[] args) {
String Monday = "Monday";
String Mon = "Mon";
final String day = "day";
System.out.println(Monday == "Mon" "day");
System.out.println(Monday == "Mon" day);
}
输出结果:
true
true
分析: 加上final后day也变成了常量,所以第二句的引用也是指向的字符串常量池。
String有没有线程安全问题
String类是一个不可变对象,其它有两层意思:
- 一是String类是一个final类,不能产生一个String的子类;
- 二是在String类中提供的所有方法中,如果有String返回值就会创建一个String对象,不对原对象进行修改,这就保证了原对象不可改变。
总结
- Java中String对象是
不可变的
- Java支持通
过构造方法或字面常量
创建字符串 - 字符串对象存放的位置
可能在堆内存,也可能在字符串常量池(和创建方法以及JDK的版本有关)
。使用构造方法构建的字符串对象一定在堆内存,如果堆该字符串对象调用String.intern()方法,则可以将该字符串移入字符串常量池。 - 字符串常量池在JVM底层本质上是一个
Hashtable
- 字符串上支持很多操作API,例如字符串连接、截取字符串、trim、替换字符等等,这些操作看似是写操作,实际上都会
返回一个新的字符串
- 字符串的连接方式:
“ ”运算符重载
,底层是依靠StringBuilder实现的;String.contact()方法
,底层是依赖Array.copy实现的;ringBuilder
,通过预先分配一个字符缓冲区来进行字符串的连接,适合大批量字符串连接的情况 - StringBuilder是
JDK1.5提供的
,目的是补充StringBuffer用在单线程环境下——不必要且性能低的不足。 - String、StringBuilder和StringBuffer的
底层数据结构都是char[]数组
,不同的是String将该char数组设置成了不可变的(final)
- String的intern()方法调用该方法时,如果字符串常量池中包括了一个等于此String对象的字符串(由equals方法确定),则返回池中的字符串的引用。否则,
将此String对象添加到池中,并且返回此池中对象的引用
。在JDK6中,不推荐大量使用intern方法
,因为这个版本字符串缓存在永久代
中,这个空间是有限了,除了FullGC之外不会被清楚,所以大量的缓存在这容易OutOfMemoryError。
- 如果要操作少量的数据用 String;
- 多线程操作字符串常量池下操作大量数据 StringBuffer;
- 单线程操作字符串常量池下操作大量数据 StringBuilder。
字符串在JDK1.6—JDK1.8的区别
- 在
JDK1.6及以前
,常量池在方法区,
这时的方法区也叫做永久代
,其中存放的是字符串的实例(字符串存在永久代中,容易出现性能问题和内存溢出。) - 在
JDK1.7(含)方法区合并到了堆内存中
,这时的常量池也可以说是在堆内存中,存储的是字符串对象的引用,字符串实例是在堆中
1.6之后的版本把字符串放入了堆中,避免了永久代被挤满。 JDK1.8 已移除永久代
,方法区又从堆内存中剥离出来了
,字符串常量池是在本地内存当中,存储的也只是引用
。但实现方式与之前的永久代不同,这时的方法区被叫做元空间
,常量池就存储在元空间
。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/193656.html原文链接:https://javaforall.cn