1、创建字符串
1.1、方式一:字面量声明
String str1 = "hello,mrz";
这种方式会直接将字符串放到字符串常量池中:
注意:在JDK1.7之后(包括1.7),字符串常量池已经从方法区移动到了堆区
当我们再次通过字面量声明一个内容相同的字符串时,不会新建对象,而是直接指向池中的地址,如下图: 1.2、方式二:new String()
String str2 = new String("hello,mrz");
使用这种方式创建字符串,要分两种情况:
-
如果字符串常量池中已经存在了相同内容的字符串对象,只创建一个对象(比如在new之前已经用字面量声明的方法创建了一个相同内容的字符串)
解释:
1、"abc"是字符串常量,应该在常量池中创建,JVM在常量池中检查发现已经存在有相同内容的字符串,所以不再创建
2、遇到new关键字,会在堆中创建一个String对象,返回给str2
2. 如果字符串常量池中不存在相同内容的常量
1、首先在字符串常量池创建一个"hello,mrz"对象
2、遇到new关键字,在堆中创建一个字符串对象,返回给str2
1.3、比较以上两种方式可以得到:
1、通过字面量声明的方式最多创建一个对象,最少不创建对象
- 如果常量池中没有,创建一个对象
- 如果常量池中已经存在,不创建对象,直接引用
2、通过new String()方式最少创建一个对象,最多创建两个对象
- 如果常量池中没有,在常量池中创建一个对象,在堆中创建一个对象
- 如果常量池中已经存在,则只会在堆中创建一个对象
- 绝对会在堆中创建一个字符串对象,常量池中可能创建可能不创建
1.4、扩展
Strin str = new String(“a”) + new String(“b”) 创建了几个对象?
一共创建了5个对象
解释:
在常量池中创建了2个字符串对象:a"字符串对象,"b"字符串对象
在堆中创建了三个字符串对象:"a"字符串对象,"b"字符串对象,"ab"字符串对象
注意:拼接是在堆中完成的,这里实际上是使用 StringBuilder.append 来完成字符串的拼接, 所以在堆内存中会存在一个 “ab”, 但是在字符串常量池中没有 “ab” 。如果要入池需要使用后文的intern()方法
2、字符串比较相等
2.1、看以下代码
-
例一 String str1 = "Hello";
String str2 = "Hello";
System.out.println(str1 == str2);
至于例一为什么这为true不再赘述,看1.1内存图便可明白:str1和str2指向字符串常量池中同一个地址,固然相等 -
例二 String str1 = new String("Hello,mrz");
String str2 = new String("Hello,mrz");
System.out.println(str1 == str2);
解释:new关键字都是在堆中开辟内存创建对象的,所以肯定地址肯定不同,也就是为false 可是我们想要的效果是:“只要两个字符串内容相同,就为true”;怎么办呢?那就要使用equals()方法进行判断了
2.2、equals()方法
记住一句话:
使用 == 比较的是两个引用是否指向同一个地址(“身份”是否相同),比较字符串的内容是否相等要使用equals方法
-
所以在2.1-例二中,如果是str1.equals(str2),那么结果就是true -
对于自定义的类,如果要比较两个对象的内容是否相同,一定要重写equals()方法 -
假如现在要比较str和"hello"内容是否相等,推荐下面这种写法: "hello".equals(str);
str.equals("hello");
3、String.intern()
3.1、先来看看intern()方法的介绍:
-
在jdk1.7之前 <1>如果池中有相同内容的字符串,则直接返回池中该字符串的引用(地址) <2>如果池中没有相同内容的字符串,则会把堆中这个字符串对象拷贝一份,放入常量池,然后返回池中字符串对象的引用
- 在jdk1.7之后(包含1.7)
<1>如果池中有相同内容的字符串,则直接返回池中该字符串的引用 <2>如果池中没有,则会把这个字符串对象的的地址复制一份,放入池中,并返回池中的引用(指向堆中的字符串对象)
3.2、可能文字并不好解释清楚,接下来我将通过几个案例演示(以jdk1.7为准)
案例一:
String str1 = new String("hello,mrz");
String str2 = "hello,mrz";
str1.intern();
System.out.println(str1 == str2);
解释:
new String(“hello,mrz”),首先在池中创建一个"hello,mrz"对象,然后在堆中创建了字符串对象引用池中对象,返回堆中对象的引用给str1
str2直接引用池中对象的0x444
str1.intern()入池,JVM检测到池中已有相同内容的字符串,返回池中字符串的引用,但这里没有接收,str1还是指向堆中的对象
所以为false
str1 = str1.intern();
案例二:
String str1 = new String("hello") + new String(",mrz");
String str2 = "hello,mrz";
str1.intern();
System.out.println(str1 == str2);
解释:
执行第一句:new String(“hello”)+new String(",mrz")
在堆中创建了"hello"对象、",mrz"对象,在常量池中创建了"hello"对象和",mrz"对象
然后A对象(上图中)和B对象进行拼接,在堆中出现了"hello,mrz"对象
此时常量池中有"hello",",mrz",堆中有A、B、C三个对象(拼接底层使用的是 StringBuilder.append 来完成的, 所以在堆内存中会存在一个 “hello,mrz”对象, 但是在字符串常量池中没有 “hello,mrz”)
执行第二句:str2=“hello,mrz”
因为池中没有"hello,mrz",所以会在池中创建一个"hello,mrz"对象,返回引用给str2
执行第三句:str1.intern()
入池,JVM检查发现池中已有相同内容的字符串,则返回池中对象的引用,这里同案例一,也是因为没有接收,所以str1还是指向堆中的对象
所以str1肯定不等于str2
案例三:
但是如果我们调换一下第二、第三句代码的顺序呢?这时候答案就为true了!!!
String str1 = new String("hello") + new String(",mrz");
str1.intern();
String str2 = "hello,mrz";
System.out.println(str1 == str2);
解释:
第一句代码执行结果同案例二;
第二句str1.intern()入池
此时第三句代码还未执行,池中还没有"hello,mrz"对象,当JVM检查发现池中没有相同内容对象时,就会把堆中C对象的引用复制一份放到池中,然后返回池中的引用地址(就是C的引用)
第三句执行str2=“hello,mrz”
JVM检查发现池中已经有了,则返回常量池中的引用(即str2也是指向C对象) 所以str1和str2都是指向C对象,那么就是true了
综上所述:new String(“a”) + new String(“b”)一共创建了5个对象(池中没有"a"和"b"的前提向下),堆中三个,池中两个
案例四:
4、如何理解字符串是不可变的?
4.1、看以下代码
String s1 = "East";
s1 = "South";
s1="South"是直接将s1的内容"East"修改为了"West"吗?
不是!
通过查看String类的源代码可以知道: String底层是用了一个private final修饰的字符数组:
- private修饰,表明外部类是访问不到value数组的,同时子类也无法访问
- final修饰的内容是不可以被改变的,所以对String来说,一旦初始化,value[]将无法再被修改
- 但是注意,通过反射可以打破封装,可以修改value[]
有人就会说:String既然不可变,那为什么String类还有很多方法都能操作字符串?
其实String类中的很多方法,如toLowerCase,都不是在原来的字符串上进行操作的,而是重新new了个字符串,返回的是新创建的字符串对象
4.2、为什么要求字符串不可变?
- 方便实现字符串对象池. 如果 String 可变, 那么对象池就需要考虑何时深拷贝字符串的问题了.
- 不可变对象是线程安全的.
- 不可变对象更方便缓存 hash code, 作为 key 时可以更高效的保存到 HashMap 中.
5、字符串拼接优化
5.1、看以下代码:
String str1 = "hellomrz";
String str2 = "hello" + "mrz";
System.out.println(str1 == str2);
解释:
第一:在池中创建对象"hello,mrz"
第二:编译器在编译时发现"hello" 和 "mrz"都是字符串,会自动进行拼接成"hellomrz"再存储
第三:拼接后存储时,JVM检测到常量池中已有"hellomrz",所以str2会直接引用常量池中的"hellomrz"
故str1中存的地址和str2相同,所以为true
通过反汇编也可以看出两种方法是相同的: 5.2、看以下代码:
String s1 = "hellomrz";
String s2 = "hello";
String s3 = s2 + "mrz";
System.out.println(s1 == s3);
为什么是false呢?首先我们需要先来了解常量和变量的区别:
简但来说:变量只有在运行时才知道其中存储的内容,而常量是在量是在编译时就已经知道存的多少
这里s2就是个变量,编译器在编译时并不知道s2是多少;所以s3在编译时也就是个变量,拼接时在堆中new一个"hello,mrz"对象,返回给s3;s1引用常量池中的"hello,mrz",两者当然不同
5.3、看以下代码:
String s1 = "hellomrz";
final String s2 = "hello";
String s3 = s2 + "mrz";
System.out.println(s1 == s3);
相比于5.2,这里s2用final修饰,结果就变为了true
首先我们需要了解的是:对于常量,编译器在编译时就会帮我们进行运算!
例如final修饰的两个int变量,进行相加,编译器在编译时就帮我相加了,即在编译时c就已经等于30,具体可以用javap命令反汇编验证:
final int a = 10;
final int b = 20;
int c = a + b;
Code:
0: bipush 10
2: istore_1
3: bipush 20
5: istore_2
6: bipush 30
8: istore_3
所以final修饰的s2也是常量!
那么s2 + "mrz"相当于 “hello” + “mrz”,编译时就优化成"hello,mrz"存储;JVM检测到池中已有相同内容的对象,就返回池中对象的引用,所以s1 == s3;
6、StringBuilder和StringBuffer
6.1、StringBuilder和StringBuffer大部分功能都是相同的,两者主要的区别就是StringBuffer是线程安全的,而StringBuilder是非线程安全的;后文以StringBuilder举例,StringBuffer同理
6.2、String和StringBuilder的区别?
String和StringBuilder最大的区别在于:String的内容无法修改,如果改变对象的内容,改变的其实是其引用的指向而已;而StringBuilder的内容可以修改
6.3、String进行拼接的原理是什么?
<1>执行下面的代码:
public static void main(String[] args) {
String str = "hello";
for (int i = 0; i < 10; i++) {
str = str + i;
}
System.out.println(str);
}
? <2>使用javap进行反汇编: 从图中可以看到,String在进行拼接时,首先会构造一个StringBuilder对象,然后调用其append()方法进行拼接,拼接完成后又调用其toString()方法赋值给str,这样str就完成了"一次拼接";
因为使用了for循环,所以第33行时"goto 5",回到第五行重新往下执行,又会重复一遍前面的步骤,直到结束循环退出循环
为了方便理解,这里用java代码来模拟一次拼接的过程:
public static void main(String[] args) {
String s1 = "hello";
String s2 = "mrz";
StringBuilder sb = new Stringbuilder();
sb.append(s1);
sb.append(s2);
String str = sb.toString();
}
综上所述:每拼接一次,都会在底层new一个StringBuilder对象
如果在一个程序中反复需要拼接字符串,那么就会在堆中产生大量的StringBuilder对象,浪费内存
6.4、StringBuilder拼接是什么样的?
<1>执行下面的代码:
public static void main(String[] args) {
StringBuilder sb = new StringBuilder();
sb.append("hello");
for (int i = 0; i < 10; i++) {
sb.append(i);
}
String str = sb.toString();
System.out.println(str);
}
<2>javap反汇编: 可以看到,虽然都是for循环拼接了10次,但是使用String拼接构建了10个StringBuilder对象,而使用StringBuilder拼接只创建了一个StringBuilder对象!!!(在17-32行循环并没有创建新的StringBuilder对象)
综合6.3和6.4可得:如果在程序中经常有拼接操作,我们应该选用StringBuilder,如果需要线程安全的,那应该选用StringBuffer
使用String进行拼接效率太低
7、String类一些方法注意事项
7.1、String类的两种构造方法使用
No | 方法名 | 类型 | 概述 |
---|
1 | String(char[] value) | 构造 | 以字符数组构建字符串 | 2 | String(byte[] bytes) | 构造 | 以字节数组构建字符串 |
可以看到,通过char数组或byte数组都可以构建一个新的字符串,那我们怎么选择呢?
回答:
byte[] 是把 数据按照一个字节一个字节的方式处理, 这种方式适合在网络传输, 数据存储这样的场景下使用. 更适合针对二进制数据来操作.
char[] 是把数据按照一个字符一个字符的方式处理, 更适合针对文本数据来操作, 尤其是包含中文的时候
7.2、字符串拆分
No | 方法名 | 类型 | 概述 |
---|
1 | String[] split(String regex) | 成员方法 | 全部拆分 | 2 | String[] split(String regex,int limit) | 成员方法 | 拆分成limit组 |
String str = "hello world hello mrz" ;
String[] result = str.split(" ") ;
for(String s: result) {
System.out.println(s);
}
/...
String[] result = str.split(" ", 3) ;
/...
拆分是一种常用到的操作,有一些特殊字符作为分隔符需要加上转义才能正常分割
举例:拆分IP地址
String str = "192.168.1.1";
String[] result = str.split(".");
for(String s: result) {
System.out.println(s);
}
正确的做法如下:
String str = "192.168.1.1";
String[] result = str.split("\\.");
for(String s: result) {
System.out.println(s);
}
注意事项:
- 字符 " | " , " * " , " + " 作为分隔符时,都得加上转义字符
- 如果一个字符串中有多个分隔符,可以用 " | " 作为连字符
举例:多次拆分
String str = "name=xiaoming&age=8" ;
String[] strings = str.split("&") ;
for (int i = 0; i < result.length; i++) {
String[] tStr = result[i].split("=") ;
System.out.println(tStr[0]+" = "+tStr[1]);
}
以上就是我个人关于String类及相关内容的一些总结。可能不是很全面,也有可能一些内容有错误,欢迎大家指正。
|