String类概述
java.lang.String类代表字符串。Java程序中所有的字符串文字(例如“abc”)都可以被看作是实现此类的实力。
类String中包括用于检查各个字符串的方法,比如用于比较字符串、搜索字符串、提取子字符串以及创建具有翻译为大写/小写的所有字符的字符串的副本。
特点
字符串不变:字符串的值在创建后不能被更改
String s1 = "abc";
s1 += "d";
System.out.println(s1);
//内存中有“abc”,“abcd”两个对象,s1从指向“abc”,改变指向,指向了“abcd”。
因为String对象是不可变的,所以它们可以被共享。
字符串效果上相当于是char[]字符串数组,但是底层原理是byte[]字节数组。
那么,String类不可变吗?
public class App{
public static void main(String[] args){
String a = "111";
a = "222";
System.out.println(a);
}
}
打印结果:
222
不是不变吗?
其实在JVM的运行中,会单独给一块地分给String。
上面的:
String a = "111";
我们知道字符串的分配和其他对象分配一样,是需要消耗高昂的时间和空间的,而且字符串我们使用的非常多。JVM为了提高性能和减少内存开销,在实例化字符串的时候进行了一些优化:
使用字符串常量池。每当我们创建字符串常量时,JVM会首先检查字符串常量池,如果该字符串已经存在常量池中,那么就直接返回常量池中的实例引用。如果字符串不存在常量池中,就会实例化该字符串并且将其放到常量池中。由于String字符串的不可变性,我们可以十分肯定常量池中一定不存在两个相同的字符串。
这里我们先去JVM常量池里找,找到了就不用创建对象了,直接把对象的引用地址赋给a。找不到会重新创建一个对象,然后把对象的引用地址赋给a。同理a = "222";也是先找,找不到就会重新创建一个对象,然后把对象的引用赋给a。
这里插一句:Object obj = new Object(); 很多人喜欢称obj为对象,其实obj不是对象,它只是一个变量,然后这个变量里保存了一个Objcet对象的引用地址罢了。引用类型声明的变量是指该变量在内存中实际存储的是一个引用地址,实体在堆中。
所以上面String a = "111";表达的是变量a里保存了“111”这个对象的引用地址。变量是可以变的,不能变的是“111”。
String为什么是不可变的呢?
简单来说,String类中使用final关键字字符数组保存字符串。
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
public String() {
this.value = "".value;
}
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
}
从上面的这段源代码中可以看出三点:
- String类是final修饰
- String存储内容使用的是char数组
- char数组是final修饰
复习一下final关键字
- 当用final修饰一个类时,表明这个类不能被继承。final类中的成员变量可以根据需要设为final,但是要注意的是final类中的所有成员方法都会隐式地被指定为final方法。
- final修饰方法表示此方法已经是“最后的额、最终的”含义,亦即不能被重写(可以重载多个final修饰的方法)。此处需要注意的一点是:因为重写的前提是子类可以从父类中继承此方法,如果父类中final修饰的方法同时访问控制权限为private,将会导致子类中不能直接继承到此方法,因此,此时可以在子类中定义相同的方法名和参数,此时不再产生重写与final的矛盾,而是子类中重新定义了新的方法。(注:类的private方法会隐式地被指定为final方法。)
- 当final修饰一个基本数据类型时,表示该基本数据类型的值一旦在初始化后便不能发生变化。如果final修饰一个引用类型时,则在对其初始化之后便不能再让其指向其他对象了,但是该引用所指向的对象的内容是可以发生变化的。本质上是一回事,因为引用的值是一个地址,final要求值,即地址的值不发生变化。另外final修饰一个成员变量(属性),必须要显式初始化。这里有两种初始化方式:
- 在申明的时候给其赋值,否则必须在其类的所有构造方法中都要为其赋值,比如:
public class FinalDemo {
private final String name;
public FinalDemo(String name) {
this.name = name;
}
public FinalDemo() {
}
}
继续看String的案例
public class StringDemo {
public static void main(String[] args) {
String s = "Hello";
s.concat("!");
System.out.println(s);
System.out.println(s.concat("!"));
}
}
输出:
Hello
Hello!
String中几个常用方法源码:
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
//啥都没有,就直接把当前字符串给你
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
//看到了吗?返回的居然是新的String对象
return new String(buf, true);
}
void getChars(char dst[], int dstBegin) {
System.arraycopy(value, 0, dst, dstBegin, value.length);
}
public String replace(char oldChar, char newChar) {
//如果两个是一样的,那就必要替换了,所以返回this
if (oldChar != newChar) {
int len = value.length;
int i = -1;
//把当前的char数组复制给val,然后下面基于val来操作
char[] val = value;
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
//创建一个新的char数组
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
//创建一个新的String对象
return new String(buf, true);
}
}
return this;
}
public String substring(int beginIndex, int endIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
if (endIndex > value.length) {
throw new StringIndexOutOfBoundsException(endIndex);
}
int subLen = endIndex - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
//正常返回的都是新new出来的String对象
return ((beginIndex == 0) && (endIndex == value.length)) ? this
: new String(value, beginIndex, subLen);
}
public String trim() {
int len = value.length;
int st = 0;
char[] val = value; /* avoid getfield opcode */
while ((st < len) && (val[st] <= ' ')) {
st++;
}
while ((st < len) && (val[len - 1] <= ' ')) {
len--;
}
//如果是该字符串中包含了空格,调用substring方法,否则就是啥都没干原本返回
//就是如果字符串里有空格,那么还是新生一个String对象返回
return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}
无论是concat、replace、substring还是trim方法的操作都不是在原有的字符串上进行的,而是重新生成了一个新的字符串对象。也就是说进行这些操作后,最原始的字符串并没有被改变。
得出两个结论:
String对象一旦被创建就是固定不变的了,对String对象的任何改变都不影响到原对象,相关的任何变化性的操作都会生成新的对象。
String对象每次有变化性操作的时候,都会重新new一个String对象(这里指的是有变化的情况)。
继续看例子:
//String a = "111";相当于
char data [] ={'1','1','1'};
Stirng a = new String(data);
//a = "222";
char data [] ={'2','2','2'};
a = new String(data);
这里变量a里保存的是“222”对应String对象的引用。
继续看:
public class StringDemo {
public static void main(String[] args) {
String s1 = "111";
String s2 = "111";
String s3 = new String("111");
System.out.println(s1 == s2);
System.out.println(s2 == s3);
System.out.println(s1.hashCode());
System.out.println(s2.hashCode());
System.out.println(s3.hashCode());
System.out.println(new String("111").hashCode());
}
}
打印结果:
true
false
48657
48657
48657
48657
第一个输出true,说明s1和s2两个变量保存的引用地址是同一个。
第二个也输出false,说明s3与s1、s2的地址是不一样的。
诡异的是,它们的hashCode()返回值都是一样的,下面来解释看似矛盾的两种结果。
首先,从源码可以明确,Java中“==”的比较,比较的是地址。
字符串本质上是final修饰的字符数组,也就是说,当创建字符串对象时,字符串的引用是常量,但它每一个对象的值可以改变。
很明显s1和s2的地址相同,它们与s3的地址不相同,但是s3通过方法intern(),可以强制入池,强制入池后,s3与s1、s2的地址相同。
“==”方法判断的是对象的物理地址(即是否是同一个对象),所以尽管哈希值相同,使用该方法判断所得的结果仍然不一定为true。
结论
- 字符串:默认为常量-----------进常量池
- String val = “xxx”;--------------------默认入池
- String val = new String(“xxx”);------------默认入堆,但可以通过intern()强制入池(堆里的对象还在)