问题1:String 和 StringBuilder、StringBuffer 的区别
参考
创建成功的字符的串对象,它的的长度是固定的,内容也是不能被改变和编译的。虽然使用“+”运算符可以达到附加新字符或字符串的目的,但是“+”会产生一个新的String实例,会在内存中创建新的字符串对象。如果重复地对字符串进行修改,将会极大的增加系统的开销。
这时候可以使用StringBuffer或者StringBuilder
package com.dong.test;
public class StringTest {
public static void main(String[] args) {
String str= "";
long startTime = System.currentTimeMillis();
for(int i=0;i<100000;i++){
str = str+i;
}
long endTime = System.currentTimeMillis();
long time = endTime - startTime;
System.out.println("String 消耗的时间是"+time);
StringBuffer s = new StringBuffer();
startTime = System.currentTimeMillis();
for(int i=0;i<100000;i++){
s.append(i);
}
endTime = System.currentTimeMillis();
time = endTime - startTime;
System.out.println("StringBuffer 消耗的时间是"+time);
}
}
运行结果
String 消耗的时间是17044
StringBuffer 消耗的时间是3
通过这个例子可以看出,两个操作的执行时间差距很大,如果程序中需要频繁地附加字符串,建议使用StirngBuffer。
StringBuffer
构造一个其中不带字符的字符缓冲区,初始容量为16个字符。
特点:
- 可以对字符串内容进行修改
- 是一个容器
- 长度是可变的
- 缓冲区中可以存储任意类型的
- 最终需要变成字符串
StringBuilder
JDK1.5出现StringBuiler构造一个其中不带字符的字符串生产器,初始容量为16个字符。该类被设计用作StirngBuffer的一个简单替换,用在字符串缓冲区被单个线程使用时。
StringBuffer和StringBuilder的区别
- StringBuffer 是线程安全,通过synchronized同步锁实现对方法的加锁,
- StringBuilder 线程不安全;在考虑线程线程安全问题时单个线程操作使用StringBuilder效率高;多线程操作的时候使用StringBuffer安全。但是在开发过程中,如果在方法中定义StringBuilder去拼接字符串,此时是不会有线程安全性问题的,因为方法中的变量存在栈中,不是共享变量。
问题2:生成几个对象
我们知道几乎所有的对象否在堆中创建,但是String比较特殊,有一个字符串常量池的也是可以存放String对象。所以String对象可以在堆中也可以常量池中。下面具体看看两个实例
创建String对象方式1:
String a = "mark"; //a指向的是字符串常量池的地址
逻辑:先去字符串常量池中查找是否已经有此值,如果有则把返回引用,否则会先在常量池中创建,然后返回引用。这样方式只会在常量池创建一个对象
。
创建String对象方式2:
String str2 = new String("mark"); //而 String str 执行的是堆中的地址
在堆上创建一个字符串对象,然后再去常量池中查询此字符串的值是否已经存在,如果不存在会先在常量池中创建此字符串,然后把引用的值指向此字符串。所以可能
会创建两个对象
(堆一个,字符串常量一个(不存在则创建))
拓展:在日常中使用方式1定义String变量,以为可以使用字符串常量池的缓存功能,提高效率。
问题3:== 和 equals()的区别
参考
== 对于基本数据类型来说,是用于比较 “值”是否相等的;而对于引用类型来说,是用于比较引用地址是否相同的, equals() 方法属于Object类的方法,默认实现是比较引用地址,Object类中equals()方法如下:
public boolean equals(Object obj) {
return (this == obj);
}
String重写了该方法,比较的是两个字符串的每一个字符是否都相等,String类中equals()方法如下:
public boolean equals(Object anObject) {
if (this == anObject) { // 判断内存地址是否相同
return true;
}
if (anObject instanceof String) { // 待比较的对象是否是 String,如果不是 String,直接返回不相等
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) { // 两个字符串的长度是否相等,不等则直接返回不相等
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {// 依次比较每个字符是否相等,若有一个不等,直接返回不相等
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
分析1.
String str = "i"; //java 虚拟机会将其分配到常量池中
String str2 = new String("i"); //而 String str=new String(“i”) 则会被分到堆内存中。
System.out.println(str == str2); //false,堆栈中的地址和字符串常量池的地址相比肯定是不一样的
System.out.println(str.equals(str2)); //true
分析2
String str = "i"; //java 虚拟机会将其分配到常量池中
String str3 = "i"; //此时都是执行是常量池中同一个地址
System.out.println(str == str3); //true
String str2 = new String("i"); //而 String str=new String(“i”) 则会被分到堆内存中。
String str4 = new String("i"); //创建了另一个对象
System.out.println(str2 == str4); //false 不同的对象
分析3
String s = "hello";
char c[] = {'h', 'e', 'l', 'l', 'o'};
System.out.println("equal:" + c.equals(s)); //false 调用Object的equals()方法,比较的引用地址
System.out.println("equal:" + s.equals(c)); //false 调用的是String的equals()方法,c不是String类型,不会一次比较每个字符
分析4
String s1 = "hello";
String s2 = "he" + "llo";//JVM优化 被直接编译成了 "hello"
System.out.println(s1 == s2); //true
String s3 = "he";
String s4 = s3 + "llo"; //s4的值需要在运行时才能确定,e的值会存放在堆中
System.out.println(s1 == s4); //false
final String s5 = "he"; //final 修饰的变量,会被看作常量
String s6 = s5 + "llo";
System.out.println(s1 == s6); //true
final String s7 = getHe(); //final修饰方法时,编译期无法确定s7的值
String s8 = s7 + "llo";
System.out.println(s1 == s8); //false
private static String getHe() {
return "he";
}
问题4:String用final 修饰的好处
参考:
- 安全性:final类不可继承,这么就会有子类去修改原有的语义;
- 高效:更好地支持字符串常量池,字符串常量池的作用是缓存常量池。假设现在s1和s2都执行常量池的引用,如果String是可变的,s1的修改好,s2的值也会跟着改变,这就可能造成系统极大的不稳定,也不符合字符串常量池的设计思想了。
问题5: intern()方法
参考
在调用”str”.intern()方法的时候会返回”str”,但是这个方法会首先检查字符串池中是否有”str”这个字符串,如果存在则返回这个字符串的引用;否则就将这个字符串添加到字符串池中,然会返回这个字符串的引用。
注:JDK1.6版本和JDK1.6+后该方法有不同的语义
JDK 1.6:当调用intern方法时,如果字符串常量池先前已经创建好了该字符串的对象,则返回字符串常量池中的该对象的引用。否则,将此·字符串对象副本
添加到字符串常量池中,并且返回该字符串对象的引用。
JDK 1.6+:如果字符串常量池先前已经创建出该字符串的对象,则返回字符串常量池中对象的该字符串的引用。否则,如果该对象已经存在堆中,则将堆中的此对象的引用添加到字符串常量池中
,并且返回该引用,如果堆中不存在,则在池中创建该字符串并返回其引用
//先中创建对象,然后在符串常量池中创建“a”
String s = new String("a");
//JDK1.6或JDK1.6+:尝试把s中的“a”的副本放入常量池,此时常量池中已经存在“a”
s.intern();
String s2 = "a";
System.out.println(s == s2); //一个是堆的地址,一个是常量池的地址肯定是false
/*
1.此时常量池已经有"a"了
2.在堆中创建对象'aa'
* */
String s3 = new String("a") + new String("a");;
/*
* 此时常量池中没有‘aa’
* 1.JDK1.6:把堆引用 副本 放入常量池(副本和原来的地址对比还是不一样的)
* 2.JDK1.6+:把堆的引用直接放入常量池(此时地址一样的)
* */
s3.intern();
String s4 = "aa";
System.out.println(s3 == s4); //JDK1.6:false;JDK1.6+:true