String是一个字符串类型的类,使用""
定义的内容都是字符串,但是String在使用上有一点特殊,它有两种定义方式,相信所有java程序员都知道,但是有些细节却很容易被忽略,我们接下来从内存关系上来分析一下。
相信很多人在初学程序的时候都写过hello word!
,它是一个字符串,那么我们通过第一种直接赋值的方式来定义一个hello world!
ublic class StringTest {
public static void main(String args[]){
String str = "hello world!"; //直接赋值
System.out.println(str);
}
}
运行结果图
hello world!
通过这样一个代码String类对象已经实例化了,并且其中有内容,就是hello world!
。
String对象也是可以通过关键字new
来进行实例化的,接下来我们看个简单的例子。
public class StringTest {
public static void main(String args[]){
String str = new String("hello world!");
System.out.println(str);
}
}
运行结果图
hello world!
不难看出来这个是通过构造方法来给String对象赋值的,在String类中的构造方法是这样写的:
/**
* Initializes a newly created {@code String} object so that it represents
* the same sequence of characters as the argument; in other words, the
* newly created string is a copy of the argument string. Unless an
* explicit copy of {@code original} is needed, use of this constructor is
* unnecessary since Strings are immutable.
*
* @param original
* A {@code String}
*/
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
对于哈希我们后面再仔细解读,先看这个value,可以看出String对象的属性有一个是value,当通过构造函数传入一个字符串时该对象的value将被赋值,并且构造方法传入的对象也是String类,相当于自己作为参数传进去,这样的做法在java中是允许的,那么传进去的String又是哪儿来的呢?我们继续来看。
在解决这个问题之前我们先看一下字符串比较,通过这个来引入。
我们来看一下代码
public class StringTest {
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(str2 == str3);
System.out.println(str3 == str1);
}
}
运行结果
false
true
false
以上三个String类对象的内容完全一样,但是结果有的是true有的是false,原因就是在java中String类的比较用==
并不是比较其内容,而是比较其所在堆内存中的地址值,并非比较其数值。我们来分析一下内存关系图
我们通过最后一个内存图可以看出str1和str2指向的地址不一样,所以第一个输出false;str2和str3指向的地址都是XO0020
,所以第二个输出true,str3和str1指向的地址不一样,所以第三个输出false。
如果在String中想比较大小要用到String类中的equals()
方法,该方法比较的就是对象中所存的值。
public class StringTest {
public static void main(String args[]){
String str1 = "hello";
String str2 = new String("hello");
String str3 = str2; //引用传递
System.out.println(str1.equals(str2));
System.out.println(str2.equals(str3));
System.out.println(str3.equals(str1));
}
}
运行结果
true
true
true
在平时使用的时候很容易对这两个搞混淆,一般使用eauals的会比较多。
不难看出在字符串比较时有比较内存地址和内容值之分,回顾之前写的一篇文章java实例化对象过程中的内存分配
,我们继续来通过内存分配的方式分析上面讲的两个String定义的方式。
在java中,如果直接用双引号里面加上字符串,就是实例化了一个String匿名类对象,此过程就会在堆内存中开辟一个空间。
String str = "hello world!";
那么在这个过程中会开辟几个堆内存几个栈内存呢?我们来看一下内存图
此过程中开辟了一块堆内存一块栈内存,但是如果我们声明多个字符串并且都赋值同一内容的字符串呢?
String str = new String("hello world!");
以上的代码其实可以将其初略地划分为三个步骤:
str
"hello world!"
"hello world!"
的堆内存空间我们来看一下其内存关系
通过这个图可以看出此种方法创建String对象的缺陷,每次都会产生一块垃圾空间,所以建议在平时开发中尽量使用第一种方式。
我们来看一下代码
public class StringTest {
public static void main(String args[]){
String str1 = "hello";
String str2 = "hello";
String str3 = str2; //引用传递
System.out.println(str1 == str2);
System.out.println(str2 == str3);
System.out.println(str3 == str1);
}
}
运行结果
true
true
true
我们发现上面代码运行结果都是true,说明三个String对象所指向的堆内存是同一个地址,我们来看一下其内存关系图
那么如果我们再加一个str4指向“world”呢?
public class StringTest {
public static void main(String args[]){
String str1 = "hello";
String str2 = "hello";
String str3 = str2; //引用传递
String str4 = "world";
System.out.println(str1 == str2);
System.out.println(str2 == str3);
System.out.println(str3 == str1);
System.out.println(str3 == str4);
}
}
运行结果
true
true
true
false
我们可以得到,实例化的匿名对象如果内容相同则使用的是同一个堆内存空间,并不是说实例化了三个"hello"就会开辟三个堆内存空间,如果内容不同则会开辟新的堆内存空间。
但是按道理来说应该是每实例化一个对象就开辟一个空间,之所以会出现这种情况是因为JVM对象池的原因,它用到了一个共享设计模式
,目的是为了节省资源消耗。
共享设计模式: 在JVM的底层实际上会存在有一个对象池(不一定只保存String对象,其他对象也可保存),当代码之中通过直接赋值的方式定义了String对象时,会将此字符串对象所使用的匿名对象入池保存,而后如果后续还有其他String对象也采用了直接赋值的方式,并且设置了同样内容的时候并不会开辟新的堆内存空间,而是使用已有的对象进行引用的分配,从而继续使用。
那么如果通过构造方法来创建String对象能使用对象池吗?我们来看一段代码
public class StringTest {
public static void main(String args[]){
String str1 = new String("hello");
String str2 = new String("hello");
System.out.println(str1 == str2);
}
}
运行结果
false
很明显通过构造方法来赋值的方式并没有将其存入对象池,其原因是使用了关键字new开辟的新内存。如果希望开辟的新内存也可以利用对象池,这个时候我们就需要手动入池,用String类中的方法intern()
。
我们来看看使用手工入池的代码
public class StringTest {
public static void main(String args[]){
// 使用构造方法定义了新的内存空间,而后入池
String str1 = new String("hello").intern();
String str2 = new String("hello").intern();
String str3 = "hello";
System.out.println(str1 == str2);
System.out.println(str1 == str3);
}
}
运行结果
true
true
通过上面的分析,如果以后面试问到“String类对象两种实例化方式区别是什么?”,那么我们就能清楚的回答啦~
字符串一旦被定义就不可改变,但是我们不能从平时编写的代码表面地去理解它,要从内存分析上才能理解它为什么是不可改变的。
我们来看一下下面的代码
public class StringTest {
public static void main(String args[]){
String str = "hello ";
str = str + "world ";
str += "!";
System.out.println(str);
}
}
运行结果
hello world !
如果按照代码来理解可能认为str的内容被改变了,并且被改变了两次!之前记得有人问过我类似的问题:上面的代码str对象赋值过程中进行了几步操作?当时我也不是很清楚,不过经过这次学习就能解释这个问题了。
以上操作可以看到,所谓的字符串的内容实际上并未改变(Java定义好了String的内容不能改变),改变的是地址的指向。对于字符串对象内容的改变,是利用了引用关系的改变而实现的,但是每一次的变化都会产生垃圾空间。
其实我们可以从jdk中对String对象的定义中找到其注释可以发现这一规定,下面是String类定义完整注释,在前面就可以看到这一句Strings are constant; their values cannot be changed after they are created. String buffers support mutable strings.
对于这些知识平时使用时可能不太注意,但是了解之后对以后开发是有很大帮助的,最好的学习方法:多读读API和jdk源码吧~~~
END