有的同学要问了,Object和String是我们这一块儿日常一直在使用的东西,为什么要单独拎出来讲呢?其实,他们使用起来虽然简单,但比如Object类是位于java.lang包中的,java.lang是包含了Java最基础和核心的类,在编译时会自动导入的。Object类同时是所有Java类的祖先。每个类都使用 Object 作为超类。所有对象(包括数组)都实现这个类的方法。可以使用类型为Object的变量指向任意类型的对象。而String也是我们平时使用最广泛的一个对象,同时也属于java.lang,但它在使用过程中也有着不易察觉的特殊性。
1、Object中,我们主要看一下equals(),hashCode(),clone(),toString()这四个方法。首先equals(),它的默认实现是return (this == obj);,这是比较两个对象的引用地址是否相等。我们常把equals 和== 拿在一起来比较。首先基本类型的==代表比较==前后的两个基本类型的值是否相等,而基本类型本身没有equals方法。而引用对象的==则代表比较前后两个对象的引用地址是否相等,这个意思就是说,前后两个对象是否是同一个。引用对象的equals方法则常实现为,两个对象的引用地址可以是不同的,但他们的属性(状态数据)要是相同的。如下,我们在每一个类中,都可以自己实现equals方法,用自己的方式来定义等价。
@Override
public boolean equals(Object m) {
if (this == m) return true;
if (m == null || getClass() != m.getClass()) return false;
//这里还可以对比具体的值,或者写上任何想要的逻辑
}
我们要注意,hashCode本身只是一个对象的散列值(特征值),等价的两个对象,散列值一定相同,所以如果我们重写equals则应该同时也重写hashCode方法。HashSet 和 HashMap 等集合类使用了 hashCode() 方法来计算对象应该存储的位置,因此要将对象添加到这些集合类中,需要让对应的类实现 hashCode() 方法,这样可以实现等价的对象只会添加一次。
clone方法,clone方法首先会判对象是否实现了Cloneable接口,若无则抛出CloneNotSupportedException,接下来clone内部调用了一个native方法,native方法的效率一般来说都是远高于Java中的非native方法。这也解释了为什么要用Object中clone()方法而不是先new一个类,然后把原始对象中的信息复制到新对象中,虽然这也实现了clone功能。Object中的源码如下:
protected Object clone() throws CloneNotSupportedException {
if (!(this instanceof Cloneable)) {
throw new CloneNotSupportedException("Class " + getClass().getName() +
" doesn't implement Cloneable");
}
return internalClone();
}
@FastNative
private native Object internalClone();
浅拷贝和深拷贝:浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。即默认拷贝构造函数只是对对象进行浅拷贝复制(逐个成员依次拷贝),即只复制对象空间而不复制资源。深拷贝,在拷贝引用类型成员变量时,为引用类型的数据成员另辟了一个独立的内存空间,实现真正内容上的拷贝。
toString方法,默认返回 ToStringExample@4554617c 这种形式,其中 @ 后面的数值为散列码的无符号十六进制表示。一般情况下,各个类都会有对应的重写。
2、String。String跟基本类型的包装类一样都是定义为final的,不可继承的。在 Java 8 中,String 内部使用 char 数组存储数据。
public final class String
implements java.io.Serializable, Comparable, CharSequence {
private final char value[];
}
在 Java 9 之后,String 类的实现改用 byte 数组存储字符串,同时使用
coder
来标识使用了哪种编码。
public final class String
implements java.io.Serializable, Comparable, CharSequence {
private final byte[] value;
private final byte coder;
}
value 数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value 数组的方法,因此可以保证 String 不可变。所以不可变也带来了好处:1. 可以缓存 hash 值,因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。2. String Pool 的需要,如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。3. 安全性,String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 的那一方以为现在连接的是其它主机,而实际情况却不一定是。4. 线程安全,String 不可变性天生具备线程安全,可以在多个线程中安全地使用。
String是不可变的,但Java中还有StringBuilder和StringBuffer是可变的。举个例子,我们在日常的代码中,粗心的小伙伴们可能写过这样的代码:
String s = "abc";//字符串字面量赋值,"abc"置入字符串池。
String l = "efg";//字符串字面量赋值,"efg"置入字符串池。
String k = s + l;//运行时计算,会创建一个新的对象,置入堆中。
String m = "abc" + "efg";//编译时计算,直接将结果置入字符串池。
public class Test{
public static String test = "abc";//字符串字面量赋值,"abc"置入字符串池。
}
System.out.println(s == "abc"); // true
System.out.println(Test.test == s); // true
System.out.println(Test.test == "abc"); // true
System.out.println(s == "a" + "b" + "c"); // true
System.out.println(m == "a" + "b" + "c" + "e" + "f" + "g"); // true
System.out.println(m == s + l); // false
System.out.println(m == (s + l).intern); // true
在这样String k的赋值中,我们实际上做了什么呢?首先我们创建了两个String的对象,分别是"abc" 和"efg",这两个对象都在jvm堆中拥有自己的内存空间。然后将这两个对象的值拼接起来,开辟一个新的堆空间,存储为“abcefg”然后将此对象的引用地址赋值给k。我们创建了两个无用的对象,并丢失在内存中,可见,这个过程是多么的浪费。
那么我们应该怎么做?StringBuffer 和 StringBuilder 类的对象能够被多次的修改,并且不产生新的未使用对象。
StringBuilder非线程安全的。但是有速度优势。
StringBuffer线程安全的,内部使用 synchronized 进行同步。
JVM为了提升性能和减少内存开销,避免字符串的重复创建,其维护了一块特殊的内存空间,即字符串池(String Pool)。字符串池由String类私有的维护。举个例子,在Java中有两种创建字符串对象的方式:1、采用字面值的方式赋值 2、采用new关键字新建一个字符串对象。这两种方式在性能和内存占用方面存在着差别:
采用字面值String str = "aaa";的方式创建一个字符串时,JVM首先会去字符串池中查找是否存在"aaa"这个对象,如果不存在,则在字符串池中创建"aaa"这个对象,然后将池中"aaa"这个对象的引用地址返回给字符串常量str,这样str会指向池中"aaa"这个字符串对象;如果存在,则不创建任何对象,直接将池中"aaa"这个对象的地址返回,赋给字符串常量。
采用new关键字String str2 = new String ("abc");新建一个字符串对象时,JVM首先在字符串池中查找有没有"abc"这个字符串对象,如果有,则不在池中再去创建"abc"这个对象了,直接在堆中创建一个"abc"字符串对象,然后将堆中的这个"abc"对象的地址返回赋给引用str2,这样,str2就指向了堆中创建的这个"abc"字符串对象;如果没有,则首先在字符串池中创建一个"abc"字符串对象,然后再在堆中创建一个"abc"字符串对象,然后将堆中这个"abc"字符串对象的地址返回赋给str2引用,这样,str2指向了堆中创建的这个"abc"字符串对象。
intern方法使用:一个初始为空的字符串池,它由类String独自维护。当调用 intern方法时,如果池已经包含一个等于此String对象的字符串(用equals(oject)方法确定),则返回池中的字符串。否则,将此String对象添加到池中,并返回此String对象的引用。 对于任意两个字符串s和t,当且仅当s.equals(t)为true时,s.instan() == t.instan才为true。所有字面值字符串和字符串赋值常量表达式都使用 intern方法进行操作。
由于String Pool一直持有着创建过的字符串对象,所以即使我们在代码中调用str2 = null;之前创建的字符串对象依旧不会被垃圾回收。
我们回到上面图中的示例:我们可以发现几个特质:1、字符串字面量的所有拼接,都是在编译时完成,它们的值只要是一致的,那么就是指向的同一个对象。2、而通过字符串对象计算拼接而成的,都是在运行时创建了新的对象。3、通过计算生成的字符串显示调用intern方法后产生的结果与原来存在的同样内容的字符串常量引用是一样的。
作者:Liu_dede