Java中关于String类型的一些思考
作为初学者在学习Java的时候,变量类型是不可避免会遇到的,在以往我们的印象中字符串String都是作为基本类型而存在的,但是在Java中String类型确是一个实实在在的引用类型,是可以通过new关键字来实例化的,只不过我们在使用的过程中很少使用这种方式去操作,但这并不能否定他是一个引用类型。然而在使用String类型的过程中,也发现了一些有意思的现象,下面就让我们来具体看看这个String类型是如何在基本类型中脱颖而出的。
String类型的内部实现
使用基本类型的时候,在对其赋予不同值的前后,变量的地址会发生变化,所以基本数据类型一旦赋值便不可再次更改。Java中并不存在字符串基本类型,只存在一个叫String的引用类型,但这个引用类型却可以当成和基本类型字符串一样使用,这是如何办到的呢?我们先来看一下String类型的源码实现(在Eclipse中使用Ctrl+鼠标点击即可查看String类型的源代码,用起来还是比较方便的)
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence,
Constable, ConstantDesc {
/**
* The value is used for character storage.
*
* @implNote This field is trusted by the VM, and is a subject to
* constant folding if String instance is constant. Overwriting this
* field after construction will cause problems.
*
* Additionally, it is marked with {@link Stable} to trust the contents
* of the array. No other facility in JDK provides this functionality (yet).
* {@link Stable} is safe here, because value is never null.
*/
@Stable
private final byte[] value;
打开源码我们发现存在上述的片段,里面有很关键的一句代码private final byte[] value;
,就是这句代码导致存储的字节数组不可以被修改,因为有final关键字修饰,至于为何被final修饰即可保证不被改变,需要详细查看下final的实现机制,在这里不做过多解释。
有趣现象:String类型之间的==操作
在使用String类型的过程中,经常会涉及到判断两个字符串是否相等这种操作,而==则是其中的一种实现方式,下面我们来看下使用这种方式会引发哪些有趣的现象
public static void main(String[] args) {
String str = new String("abc");
String str1 = "abc";
System.out.println(str==str1); //false
String str2 ="abc";
System.out.println(str2==str1); //true
}
看下这段代码的输出结果,是否感到有些疑问,为何同样的字符串在进行操作的时候结果确是两个完全不一样的呢。首先我们就要从运算符下手了,需要知道到底是如何判断两个变量是否相等的,它的依据又是什么。这里我直接揭秘答案吧,在Java中操作实际比较的是两个对象的值,知道了这一点我们先来看第一个输出的结果为什么是false。str在进行初始化的时候使用了new关键字的形式来实例化。在Java中每次new对象都会开辟新的地址空间,str在new实例化的过程中完成了两件事:为new String对象和"abc" 开辟地址,并将String内部字节数组指向"abc"的地址,返回new String对象的地址给str变量;而str1则是直接将"abc"的地址赋值给了它,答案就显而易见了,两个变量虽然值是一样的但是地址却不一样,这就导致输出结果为false。那为什么str1和str2的地址又是相等的呢?在正常情况下,每有一个新变量的时候都会重新开辟地址空间,虽然str2的变量值与str1的相同,也会是为这个新的"abc"开辟新的地址空间的,但是java中的JVM在内存管理的时候优化了这一点,在对str2赋值的时候发现"abc"已经存在了,没必要在重复创建了,所以直接就将其的地址返回给了str2,这才导致str1和str2变量值的==操作返回为true(关于String类型的地址是如何分配的会涉及到常量池,属于JVM关于内存地址的分配和管理问题,感兴趣的朋友可以去看看,对于理解一些原理和现象还是很有用的)。
有趣现象:String类型之间的equals操作
出了上述现象中的==操作可以用来判断两个对象是否相等,还有equals方法也可以用来判断两个对象是否相等,那么使用equals的方式结果又会是如何呢?
public static void main(String[] args) {
String str = new String("abc");
String str1 = "abc";
System.out.println(str.equals(str1)); //true
String str2 ="abc";
System.out.println(str2.equals(str1)); //true
}
老规矩我们先来搞清楚equals方法是什么,是如何进行判等操作的?在“万事万物皆对象”的Java语言当中,几乎所有的类都继承自Object,并在Object中提供了一些公共的属性或方法,equals就在其中,下面我看来看下源码(同样是按住Ctrl+鼠标点击)
public class Object {
private static native void registerNatives();
static {
registerNatives();
}
/**
* Constructs a new object.
*/
@HotSpotIntrinsicCandidate
public Object() {}
@HotSpotIntrinsicCandidate
public native int hashCode();
public boolean equals(Object obj) {
return (this == obj);
}
为了看起来方便我去掉了部分的注释。咦?在equals的内部实现竟然是使用==操作来判断两个对象是否相等的,既然是这样,那结果应该是第一个现象相同才对啊!是的,单纯看Object类的实现的确是这样的,但是Object作为父类被继承,其中的方法就有可能被重写,之所以会出现如上现象就是因为String 类型重写了这个方法,我们来看下源码,到底是如何实现的。
//String类中的部分代码片段
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence,
Constable, ConstantDesc {
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String aString = (String)anObject;
if (coder() == aString.coder()) {
return isLatin1() ? StringLatin1.equals(value, aString.value)
: StringUTF16.equals(value, aString.value);
}
}
return false;
}
//StringLatin1类中的部分代码片段
final class StringLatin1 {
@HotSpotIntrinsicCandidate
public static boolean equals(byte[] value, byte[] other) {
if (value.length == other.length) {
for (int i = 0; i < value.length; i++) {
if (value[i] != other[i]) {
return false;
}
}
return true;
}
return false;
}
//StringUTF16类中的部分代码片段
final class StringUTF16 {
@HotSpotIntrinsicCandidate
public static boolean equals(byte[] value, byte[] other) {
if (value.length == other.length) {
int len = value.length >> 1;
for (int i = 0; i < len; i++) {
if (getChar(value, i) != getChar(other, i)) {
return false;
}
}
return true;
}
return false;
}
终于找到原因了,原来重写后的equals方法并不进行地址判断而是对其所存储的值进行遍历,既然对象内存储的值都是相同的那么自然就是相等,返回true也是意料之中。
总结
我们粗略的看了下String在实际应用中不同的用法所展现出来的不同结果并阐述了现象的原因,因此我们在日常使用的过程中,对String类型的判等操作需要使用equals来实现,以免发生预期以外的结果。为什么Java会把String类型设置为引用类型呢?在逐渐接触Java的过程中也开始慢慢的理解了,在Java中存在8大基础数据类型(int,double,long,byte等),但是对基础类型之间的转换操作基础类型是不提供这些方法的,所以Java又提供了每个基础数据类型对应的引用类型(Integer,Long,Double,Byte等)来包装基础数据类型并对其扩充方法,包括数据类型之间的相互转换等。而这些对应的引用类型在使用的过程中却可以自动拆装箱来达到和基本类型一样的使用方式。