Java String类详解
1.字符串常量(字面量)是匿名对象
java中字符串"xxx"
形式,是String类的匿名对象的形式存在的,字符串是String类的一种简写,字符串是常量,字符串不等于String类,字符串是不可变的."xxx"
就一直表示字符串xxx,是常量,而且当这种字符串被声明出来后,就会被存储在字符串常量池中,下一次在使用"xxx"
时,就会直接从常量池中去取。注意:(字符串 和 String类对象不是同一个意思,一定要区分)
字符串是匿名对象的验证:
public class StringDemo {
public static void main(String args[]) {
String str = "Hello" ;
// 通过字符串调用方法
System.out.println("Hello".equals(str)) ;//true
}
}
匿名对象可以调用类中的方法与属性,而以上的字符串调用了equals()方法,所以它一定是一个对象。
2. String类对象的创建方式解析
字符串创建方式主要有两种,一是直接赋值法,二是通过new创建对象;
1.直接赋值实例化:
String s = "mahao";
分析:
通过这种方式创建的String对象s,指向了一个常量池中的字符串“mahao”。创建过程是,"mahao"
是字符串,是匿名的String对象,当"mahao"
被声明时,会先去字符串常量池中(位于堆内存中)查询是够存在该字符串了,如果存在将会返回常量池中的字符串引用地址,将此赋值给s。如果没有,将会把字符串"mahao"
存入到常量池中,然后进行再返回地址,下次再使用该字符串时,直接返回该字符串再常量池中的地址即可。这种创建对象的方式叫做享元模式
,在Interger
类中对[-128~127)之间的数值也是使用了该模式。
2.使用构造方法实例化:
String s = new String("mahao");
分析:
凡是经过 new
创建出来的对象,都会在堆内存中分配新的空间,创建新的对象,所以s是String类新创建的对象。但是也并发开辟空间,把数据 “mahao”
存入到堆内存空间那么简单,下面看String源码分析。
3. String源码简单分析
3.1 声明
/*
String类被定义为final类型,表名该类不允许被继承;
实现了序列化接口,可以被序列化,
实现Comparable接口,实现了compareTo方法,可以被比较
是CharSequence的实现类
*/
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence
Comparable接口实现
//比较元素的每个字符
public int compareTo(String anotherString) {
int len1 = value.length;
int len2 = anotherString.value.length;
int lim = Math.min(len1, len2);
char v1[] = value;
char v2[] = anotherString.value;
int k = 0;
while (k < lim) {
char c1 = v1[k];
char c2 = v2[k];
if (c1 != c2) {
return c1 - c2;
}
k++;
}
return len1 - len2;
}
3.2 数据结构
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
String类维护的值只有两个,一个是被定义为final类型的value[]
数组,另一个是数组的hash
值,可以看出String类就是char数组用来实现的,通过内部维护一个数组,来实现String类中的方法。
3.3 构造方法
public String() {
this.value = "".value;
}
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
构造方法有多个,只分析两个,对于构造方法 public String(String original)
而言,value
是char数组类型,操作this.value = original.value;
所代表的意义是重要的。数组类型赋值,赋值的值是对象的引用地址,**所以value是接收的是 original对象内部维护的数组的地址,**但是original
如何而来?如果是通过
String s = new String(new String());
创建,则可以理解为,通过传入String对象,获取到String对象的value属性的值,作为新创建的对象的值,可以这样解释。
但是对于这种形式:
String s = new String("123");
则创建就不一样了,首先“123”是一个字符串字面量,在编译时就会被存放在String pool中,在构造方法中使用了这个字面量,会获取字面量的地址值,将字符串“123”内部的数组value的地址,传给新创建对象的value,所以,这个关系是,s的value值,是“123”的内部数组的地址值。引用关系如图了:
4. 字符串比较
public class StringDemo {
public static void main(String args[]) {
String str1 = "Hello" ;
String str2 = new String("Hello") ;
String str3 = str2 ; // 引用传递
System.out.println(str1 == str2) ;
System.out.println(str1 == str3) ;
System.out.println(str2 == str3) ;
}
}
运行结果:
false
false
true
发现使用“==”好象最终的判断结果是不一样的,为什么呢?下面通过内存关系图来分析(在str2图中,直接将值值写入到堆对象中,没有表示value指向常量池中的对象):
通过以上分析可以发现,“”比较的不是字符串对象包含的内容,而是两个对象所在的的内存对象的数值。所以“”属于数值比较,比较的是内存地址。
5. intern()
详解
public static void main(String[] args) {
String s1 = "abc";
String s2 = new String("abc");
String s3 = new String("abc").intern();
System.out.println(s1 == s2);
System.out.println(s1 == s3);
System.out.println(s2 == s3);
System.out.println(s1 == "abc");
System.out.println(s3 == "abc");
}
false
true
false
true
true
分析:
intern()
方法是能使一个位于堆中的字符串在运行期间动态地加入到字符串常量池中(字符串常量池的内容是程序启动的时候就已经加载好了),如果字符串常量池中有该对象对应的字面量,则返回该字面量在字符串常量池中的引用,否则,创建复制一份该字面量到字符串常量池并返回它的引用。
String s1 = "abc";
String s2 = new String("abc");
String s3 = new String("abc").intern();//s3其实是字符串常量"abc"
/*
s1是常量池中的对象,s2是堆中对象,是不同对象
*/
System.out.println(s1 == s2);//false
//两者都是表示字符串常量abc,所以是true
System.out.println(s1 == s3);//true
//s3是常量池中的对象abc,s2是堆中对象,是不同对象
System.out.println(s2 == s3);
//都表示一个值abc
System.out.println(s1 == "abc");
System.out.println(s3 == "abc");
6. 字符串的内容一旦定义则不可改变
public class StringDemo {
public static void main(String args[]) {
String str = "Hello " ;
str += "World " ;
str = str + "!!!" ;
System.out.println(str) ;
}
}
结果:
Hello World !!!
分析:
字符串内容的更改,实际上改变的是字符串对象的引用过程,并且会伴随有大量的垃圾出现,在实际开发中应该避免。
好处:
-
可以缓存hash值
String的值是不允许改变的,因此hash值就可以计算出来,缓存起来,不要要每次使用都计算。
-
String pool的需要
如果一个String对象已经创建过了,那么将会从String pool中取得引用,只有String不可变,才能保证其他使用该对象是安全的。
-
线程安全
String 不可变性天生具备线程安全,可以在多个线程中安全地使用。
-
安全性
String经常作为参数,String的不可变特性可以保证参数不可变。列如在作为网络连接参数的情况下,如果String可变,那么在网络连接过程中,String被改变,改变String对象的哪一方以为现在连接的是其他主机,而实际情况却不是。
根据方法参数传递分析:
可以只看String类型的那个参数:
package dataType;
import java.util.Arrays;
/**
* java中的值传递问题。
*/
public class Demo1 {
public static void main(String[] args) {
int a = 1;
Integer b = 1;
String s = "??";
int[] arr = {1, 2};
change(a, b, s, arr);
System.out.println("change method be call after");
/*
1.a是基本数据类型,在一个方法中,基本数据类型的值,存放在栈帧的局部变量表中,
当基本类型作为参数传递到其他方法中时,在其他方法的局部变量表中,会创建一个局部变量
这个局部变量的数值,就是基本数据类型的值。所以通过描述可以明白,两个方法中的值,不是同一个,
所以方法中的数据更改,不会影响到调用方法的数值。
*/
System.out.println(a);//a=1
/*
2.b是Integer的对象,所以b传递的是b在堆内存中的对象地址,也就是change方法中b指向的对象,
和main方法中b指向的对象是同一个。对象存在于堆内存中,堆内存中的数据是共享的,当对象改变时,
其他引用变量都能感知出来。但是在这里,输出结果仍然是1,原因是change方法改变的值不是main方法中
b指向的对象,而是新创建了一个对象。Integer,String等基本数据的包装类型,都被定义成final,意味
着他们对象的值,都是不允许改变的,所以main方法中的b,指向的任然是1的包装类型,而change方法中的
b,指向的对象是新创建的对象2,两个引用指向的不是同一个对象。 检测可以通过打印b的地址值,看出不是
同一个对象。
*/
System.out.println(b);//b=1
/*
3.s是String类型,s创建的数据也是不可变的,s是String类型,String在设计时,考录到安全和效率,将
String设置为不可变,当s被创建后,对象"??"就作为常量,被存放在常量池中,作为常量。当其他字符串创建时,
会先去常量池中查询是否存在字符串常量,有了直接返回该常量地址,否则,重新创建一个字符串,然后放入常量池
中。当发生String类型的更改时,比如change中s的更改,main中s和change中s是指向同一个常量字符串,当change中
发生更改时,会重新创建一个字符串对象“??--”,然后将该对象的引用地址传给s引用变量,"??"仍然存在,被main方法中的
s所指向着,没有地方更改main中s指向的对象,所以数据还是"??"; 因为string代表的数据类型不可变特性,所以特别适合
做map的key,key对象不会发生更改,String也重写了equals方法,也适合判断。
*/
System.out.println(s);//??
/*
4.arr是数组类型,数据是可变的,arr是对象,change改变了对象的值,所以会发生数据变化。
*/
System.out.println(Arrays.toString(arr));//{0,1}
}
private static void change(int a, Integer b, String s, int[] arr) {
a++;
b = b++;
s = s + "--";
arr[0] = 0;
System.out.println(a);
System.out.println(b);
System.out.println(s);
System.out.println(Arrays.toString(arr));
}
}
其他博客看的题目:
public class Main {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "hello";
String s3 = "he" + "llo";
String s4 = "hel" + new String("lo");
String s5 = new String("hello");
String s6 = s5.intern();
String s7 = "h";
String s8 = "ello";
String s9 = s7 + s8;
System.out.println(s1==s2);//true
System.out.println(s1==s3);//true
System.out.println(s1==s4);//false
System.out.println(s1==s9);//false
System.out.println(s4==s5);//false
System.out.println(s1==s6);//true
}
}
String类的final
修饰的,以字面量的形式创建String变量时,jvm会在编译期间就把该字面量hello
放到字符串常量池中,由Java程序启动的时候就已经加载到内存中了。这个字符串常量池的特点就是有且只有一份相同的字面量,如果有其它相同的字面量,jvm则返回这个字面量的引用,如果没有相同的字面量,则在字符串常量池创建这个字面量并返回它的引用。
由于s2指向的字面量hello
在常量池中已经存在了(s1先于s2),于是jvm就返回这个字面量绑定的引用,所以s1==s2
。
s3中字面量的拼接其实就是hello,jvm在编译期间就已经对它进行优化,所以s1和s3也是相等的。
s4中的new String("lo")
生成了两个对象,lo,new String("lo")
,lo
存在字符串常量池,new String("lo")
存在堆中,String s4 = "hel" + new String("lo")
实质上是两个对象的相加,编译器不会进行优化,相加的结果存在堆中,而s1存在字符串常量池中,当然不相等。s1==s9
的原理一样。
s4==s5
两个相加的结果都在堆中,不用说,肯定不相等。
s1==s6
中,s5.intern()
方法能使一个位于堆中的字符串在运行期间动态地加入到字符串常量池中(字符串常量池的内容是程序启动的时候就已经加载好了),如果字符串常量池中有该对象对应的字面量,则返回该字面量在字符串常量池中的引用,否则,创建复制一份该字面量到字符串常量池并返回它的引用。因此s1==s6
输出true。