⭐️ ⭐️上篇文章-《我要進大廠》- Java基礎奪命連環10問,你能堅持到第幾問?(面向對象基礎篇)

文章目錄
- 一、Object
- 1、Object 類的常見方法有哪些?
- 2、== 和 equals() 的差別
- 3、hashCode() 有什麼用?
- 4、為什麼要有 hashCode?
- 5、為什麼重寫 equals() 時必須重寫 hashCode() 方法?
- 6、重寫 `equals()` 時沒有重寫 `hashCode()` 方法的話,使用 `HashMap` 可能會出現什麼問題。
- 二、String
- 1、String、StringBuffer、StringBuilder 的差別?
- 2、String 為什麼是不可變的?
- 3、字元串拼接用“+” 還是 StringBuilder?
- 4、String#equals() 和 Object#equals() 有何差別?
- 5、字元串常量池的作用了解嗎?
- 6、String s1 = new String("abc");這句話建立了幾個字元串對象?
- 7、intern 方法有什麼作用?
- 8、String 類型的變量和常量做“+”運算時發生了什麼?
- 總結
一、Object
1、Object 類的常見方法有哪些?
Object 類是一個特殊的類,是所有類的父類。它主要提供了以下 11 個方法:
/**
* native 方法,用于傳回目前運作時對象的 Class 對象,使用了 final 關鍵字修飾,故不允許子類重寫。
*/
public final native Class<?> getClass()
/**
* native 方法,用于傳回對象的哈希碼,主要使用在哈希表中,比如 JDK 中的HashMap。
*/
public native int hashCode()
/**
* 用于比較 2 個對象的記憶體位址是否相等,String 類對該方法進行了重寫以用于比較字元串的值是否相等。
*/
public boolean equals(Object obj)
/**
* naitive 方法,用于建立并傳回目前對象的一份拷貝。
*/
protected native Object clone() throws CloneNotSupportedException
/**
* 傳回類的名字執行個體的哈希碼的 16 進制的字元串。建議 Object 所有的子類都重寫這個方法。
*/
public String toString()
/**
* native 方法,并且不能重寫。喚醒一個在此對象螢幕上等待的線程(螢幕相當于就是鎖的概念)。如果有多個線程在等待隻會任意喚醒一個。
*/
public final native void notify()
/**
* native 方法,并且不能重寫。跟 notify 一樣,唯一的差別就是會喚醒在此對象螢幕上等待的所有線程,而不是一個線程。
*/
public final native void notifyAll()
/**
* native方法,并且不能重寫。暫停線程的執行。注意:sleep 方法沒有釋放鎖,而 wait 方法釋放了鎖 ,timeout 是等待時間。
*/
public final native void wait(long timeout) throws InterruptedException
/**
* 多了 nanos 參數,這個參數表示額外時間(以毫微秒為機關,範圍是 0-999999)。 是以逾時的時間還需要加上 nanos 毫秒。。
*/
public final void wait(long timeout, int nanos) throws InterruptedException
/**
* 跟之前的2個wait方法一樣,隻不過該方法一直等待,沒有逾時時間這個概念
*/
public final void wait() throws InterruptedException
/**
* 執行個體被垃圾回收器回收的時候觸發的操作
*/
protected void finalize() throws Throwable { }
2、== 和 equals() 的差別
==
- 對于基本資料類型來說,
比較的是值。==
- 對于引用資料類型來說,
比較的是對象的記憶體位址。==
因為 Java 隻有值傳遞,是以,對于 == 來說,不管是比較基本資料類型,還是引用資料類型的變量,其本質比較的都是值,隻是引用類型變量存的值是對象的位址。
equals()
不能用于判斷基本資料類型的變量,隻能用來判斷兩個對象是否相等。
equals()
方法存在于
Object
類中,而
Object
類是所有類的直接或間接父類,是以所有的類都有
equals()
方法。
Object
類
equals()
方法:
public boolean equals(Object obj) {
return (this == obj);
}
equals()
方法存在兩種使用情況:
- 類沒有重寫
方法:通過equals()
比較該類的兩個對象時,等價于通過“==”比較這兩個對象,使用的預設是equals()
類Object
方法。equals()
- 類重寫了
方法:一般我們都重寫equals()
方法來比較兩個對象中的屬性是否相等;若它們的屬性相等,則傳回 true(即,認為這兩個對象相等)。equals()
舉個例子(這裡隻是為了舉例。實際上,你按照下面這種寫法的話,像 IDEA 這種比較智能的 IDE 都會提示你将
==
換成
equals()
):
String a = new String("ab"); // a 為一個引用
String b = new String("ab"); // b為另一個引用,對象的内容一樣
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 從常量池中查找
System.out.println(aa == bb);// true
System.out.println(a == b);// false
System.out.println(a.equals(b));// true
System.out.println(42 == 42.0);// true
String
中的
equals
方法是被重寫過的,因為
Object
的
equals
方法是比較的對象的記憶體位址,而
String
的
equals
方法比較的是對象的值。
當建立
String
類型的對象時,虛拟機會在常量池中查找有沒有已經存在的值和要建立的值相同的對象,如果有就把它賦給目前引用。如果沒有就在常量池中重新建立一個
String
對象。
String
類
equals()
方法:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;//強制類型轉換
int n = value.length;//字元串數組長度
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {//比較每一個字元是否相等
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
3、hashCode() 有什麼用?
hashCode() 的作用是擷取哈希碼(int 整數),也稱為散列碼。這個哈希碼的作用是确定該對象在哈希表中的索引位置。
hashCode()
定義在 JDK 的
Object
類中,這就意味着 Java 中的任何類都包含有
hashCode()
函數。另外需要注意的是:
Object
的
hashCode()
方法是本地方法(也就是native修飾),也就是用 C 語言或 C++ 實作的,該方法通常用來将對象的記憶體位址轉換為整數之後傳回。
public native int hashCode();
散清單存儲的是鍵值對(key-value),它的特點是:能根據“鍵”快速的檢索出對應的“值”。這其中就利用到了散列碼!(可以快速找到所需要的對象) 是以時間複雜度是O(1)
4、為什麼要有 hashCode?
我們以“
HashSet
如何檢查重複”為例子來說明為什麼要有
hashCode
?
下面這段内容摘自我的 Java 啟蒙書《Head First Java》:
當你把對象加入 時,
HashSet
會先計算對象的
HashSet
值來判斷對象加入的位置,同時也會與其他已經加入的對象的
hashCode
值作比較,如果沒有相符的
hashCode
,
hashCode
會假設對象沒有重複出現。但是如果發現有相同
HashSet
值的對象,這時會調用
hashCode
方法來檢查
equals()
相等的對象是否真的相同。如果兩者相同,
hashCode
就不會讓其加入操作成功。如果不同的話,就會重新散列到其他位置。這樣我們就大大減少了
HashSet
equals
的次數,相應就大大提高了執行速度。
**一句話:**向HashSet中插入元素時,會先判斷是否有相同的hashCode.如果有相同的,則使用equals判斷倆對象是否相同,如果不同則插入元素. 如果hashCode相同且equals結果為true,那說明就是同一個對象,那麼就不再進行插入. 如果hashCode不同或者equals結果為false,則插入元素.
其實,
hashCode()
和
equals()
都是用于比較兩個對象是否相等。
那為什麼 JDK 還要同時提供這兩個方法呢?
這是因為在一些容器(比如
HashMap
、
HashSet
)中,有了
hashCode()
之後,判斷元素是否在對應容器中的效率會更高(參考添加元素進
HashSet
的過程)!
我們在前面也提到了添加元素進
HashSet
的過程,如果
HashSet
在對比的時候,同樣的
hashCode
有多個對象,它會繼續使用
equals()
來判斷是否真的相同。也就是說
hashCode
幫助我們大大縮小了查找成本。
那為什麼不隻提供
hashCode()
方法呢?
這是因為兩個對象的
hashCode
值相等并不代表兩個對象就相等。
解釋:以HashMap為例,數組下标對應的位置是連結清單/紅黑樹,也就是說同一個hashCode值,可能對應多個元素.
那為什麼兩個對象有相同的
hashCode
值,它們也不一定是相等的?
因為
hashCode()
所使用的雜湊演算法也許剛好會讓多個對象傳回相同的哈希值,也就是會出現哈希碰撞現象。越糟糕的雜湊演算法越容易碰撞,但這也與資料值域分布的特性有關(所謂哈希碰撞也就是指的是不同的對象得到相同的
hashCode
)。
總結下來就是 :
- 如果兩個對象的
值相等,那這兩個對象不一定相等(哈希碰撞)。hashCode
- 如果兩個對象的
值相等并且hashCode
方法也傳回equals()
,我們才認為這兩個對象相等。true
- 如果兩個對象的
值不相等,我們就可以直接認為這兩個對象不相等。hashCode
相信大家看了我前面對
hashCode()
和
equals()
的介紹之後,下面這個問題已經難不倒你們了。
5、為什麼重寫 equals() 時必須重寫 hashCode() 方法?
因為兩個相等的對象的
hashCode
值必須是相等。也就是說如果
equals
方法判斷兩個對象是相等的,那這兩個對象的
hashCode
值也要相等。
如果重寫
equals()
時沒有重寫
hashCode()
方法的話就可能會導緻
equals
方法判斷是相等的兩個對象,
hashCode
值卻不相等。
6、重寫 equals()
時沒有重寫 hashCode()
方法的話,使用 HashMap
可能會出現什麼問題。
equals()
hashCode()
HashMap
重寫了equals方法,不重寫hashCode方法時,可能會出現equals方法傳回為true,而hashCode方法卻傳回false。這樣的一個後果會導緻在hashmap、hashSet等類中存儲多個一模一樣的對象,這與java的思想不符(因為:hashmap隻能有唯一的key,hashSet隻能有唯一的對象)
總結 :
-
方法判斷兩個對象是相等的,那這兩個對象的equals
值也要相等。hashCode
- 兩個對象有相同的
值,他們也不一定是相等的(哈希碰撞)。hashCode
更多關于
hashCode()
和
equals()
的内容可以檢視:Java hashCode() 和 equals()的若幹問題解答
二、String
1、String、StringBuffer、StringBuilder 的差別?
可變性
String
是不可變的(後面會詳細分析原因)。
StringBuilder 與 StringBuffer 都繼承自 AbstractStringBuilder 類,在 AbstractStringBuilder 中也是使用字元數組儲存字元串,不過沒有使用 final 和 private 關鍵字修飾,最關鍵的是這個 AbstractStringBuilder 類還提供了很多修改字元串的方法比如 append 方法。
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
//...
}
線程安全性
String
中的對象是不可變的,也就可以了解為常量,線程安全。
AbstractStringBuilder
是
StringBuilder
與
StringBuffer
的公共父類,定義了一些字元串的基本操作,如
expandCapacity
、
append
、
insert
、
indexOf
等公共方法。
StringBuffer
對方法加了同步鎖或者對調用的方法加了同步鎖,是以是線程安全的。
StringBuilder
并沒有對方法進行加同步鎖,是以是非線程安全的。
性能
每次對
String
類型進行改變的時候,都會生成一個新的
String
對象,然後将指針指向新的
String
對象。
StringBuffer
每次都會對
StringBuffer
對象本身進行操作,而不是生成新的對象并改變對象引用。相同情況下使用
StringBuilder
相比使用
StringBuffer
僅能獲得 10%~15% 左右的性能提升,但卻要冒多線程不安全的風險。
對于三者使用的總結:
- 操作少量的資料: 适用
String
- 單線程操作字元串緩沖區下操作大量資料: 适用
StringBuilder
- 多線程操作字元串緩沖區下操作大量資料: 适用
StringBuffer
2、String 為什麼是不可變的?
String
類中使用
final
關鍵字修飾字元數組來儲存字元串,是以
String
對象是不可變的。
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[];
//...
}
🐛 修正 : 我們知道被 關鍵字修飾的類不能被繼承,修飾的方法不能被重寫,修飾的變量是基本資料類型則值不能改變,修飾的變量是引用類型則不能再指向其他對象。是以,
final
關鍵字修飾的數組儲存字元串并不是
final
不可變的根本原因,因為這個數組儲存的字元串是可變的(
String
final
修飾引用類型變量的情況)。
真正不可變有下面幾點原因:
String
- 儲存字元串的數組被
修飾且為私有的,并且
final
類沒有提供/暴露修改這個字元串的方法。
String
-
類被
String
修飾導緻其不能被繼承,進而避免了子類破壞
final
不可變。
String
相關閱讀:如何了解 String 類型值的不可變? - 知乎提問
補充(來自issue 675):在 Java 9 之後,
、
String
與
StringBuilder
的實作改用
StringBuffer
byte
數組存儲字元串。
public final class String implements java.io.Serializable,Comparable<String>, CharSequence {
// @Stable 注解表示變量最多被修改一次,稱為“穩定的”。
@Stable
private final byte[] value;
}
abstract class AbstractStringBuilder implements Appendable, CharSequence {
byte[] value;
}
Java 9 為何要将
的底層實作由
String
改成了
char[]
byte[]
?
新版的 String 其實支援兩個編碼方案: Latin-1 和 UTF-16。如果字元串中包含的漢字沒有超過 Latin-1 可表示範圍内的字元,那就會使用 Latin-1 作為編碼方案。Latin-1 編碼方案下,
占一個位元組(8 位),
byte
占用 2 個位元組(16),
char
相較
byte
char
節省一半的記憶體空間。
JDK 官方就說了絕大部分字元串對象隻包含 Latin-1 可表示的字元。
如果字元串中包含的漢字超過 Latin-1 可表示範圍内的字元,![]()
《我要進大廠》- Java基礎奪命連環14問,你能堅持到第幾問?(Object類 | String類) 和
byte
char
所占用的空間是一樣的。
這是官方的介紹:https://openjdk.java.net/jeps/254 。
3、字元串拼接用“+” 還是 StringBuilder?
Java 語言本身并不支援運算符重載,“+”和“+=”是專門為 String 類重載過的運算符,也是 Java 中僅有的兩個重載過的元素符。
String str1 = "he";
String str2 = "llo";
String str3 = "world";
String str4 = str1 + str2 + str3;
對象引用和“+”的字元串拼接方式,實際上是通過
StringBuilder
調用
append()
方法實作的,拼接完成之後調用
toString()
得到一個
String
對象 。
不過,在循環内使用“+”進行字元串的拼接的話,存在比較明顯的缺陷:編譯器不會建立單個
StringBuilder
以複用,會導緻建立過多的
StringBuilder
對象。
String[] arr = {"he", "llo", "world"};
String s = "";
for (int i = 0; i < arr.length; i++) {
s += arr[i];
}
System.out.println(s);
StringBuilder
對象是在循環内部被建立的,這意味着每循環一次就會建立一個
StringBuilder
對象。
如果直接使用
StringBuilder
對象進行字元串拼接的話,就不會存在這個問題了。
4、String#equals() 和 Object#equals() 有何差別?
String
中的
equals
方法是被重寫過的,比較的是 String 字元串的值是否相等。
Object
的
equals
方法是比較的對象的記憶體位址。
5、字元串常量池的作用了解嗎?
字元串常量池 是 JVM 為了提升性能和減少記憶體消耗針對字元串(String 類)專門開辟的一塊區域,主要目的是為了避免字元串的重複建立。
// 在堆中建立字元串對象”ab“
// 将字元串對象”ab“的引用儲存在字元串常量池中
String aa = "ab";
// 直接傳回字元串常量池中字元串對象”ab“的引用
String bb = "ab";
System.out.println(aa==bb);// true
更多關于字元串常量池的介紹可以看一下 Java 記憶體區域詳解 這篇文章。
6、String s1 = new String(“abc”);這句話建立了幾個字元串對象?
會建立 1 或 2 個字元串對象。
1、如果字元串常量池中不存在字元串對象“abc”的引用,那麼會在堆中建立 2 個字元串對象“abc”。
示例代碼(JDK 1.8):
String s1 = new String("abc");
對應的位元組碼:
ldc
指令用于判斷字元串常量池中是否儲存了對應的字元串對象的引用,如果儲存了的話直接傳回,如果沒有儲存的話,會在堆中建立對應的字元串對象并将該字元串對象的引用儲存到字元串常量池中。
2、如果字元串常量池中已存在字元串對象“abc”的引用,則隻會在堆中建立 1 個字元串對象“abc”。
示例代碼(JDK 1.8):
// 字元串常量池中已存在字元串對象“abc”的引用
String s1 = "abc";
// 下面這段代碼隻會在堆中建立 1 個字元串對象“abc”
String s2 = new String("abc");
對應的位元組碼:
這裡就不對上面的位元組碼進行詳細注釋了,7 這個位置的
ldc
指令不會在堆中建立新的字元串對象“abc”,這是因為 0 這個位置已經執行了一次
ldc
指令,已經在堆中建立過一次字元串對象“abc”了。7 這個位置執行
ldc
指令會直接傳回字元串常量池中字元串對象“abc”對應的引用。
7、intern 方法有什麼作用?
String.intern()
是一個 native(本地)方法,其作用是将指定的字元串對象的引用儲存在字元串常量池中,可以簡單分為兩種情況:
- 如果字元串常量池中儲存了對應的字元串對象的引用,就直接傳回該引用。
- 如果字元串常量池中沒有儲存了對應的字元串對象的引用,那就在常量池中建立一個指向該字元串對象的引用并傳回。
示例代碼(JDK 1.8) :
// 在堆中建立字元串對象”Java“
// 将字元串對象”Java“的引用儲存在字元串常量池中
String s1 = "Java";
// 直接傳回字元串常量池中字元串對象”Java“對應的引用
String s2 = s1.intern();
// 會在堆中在單獨建立一個字元串對象
String s3 = new String("Java");
// 直接傳回字元串常量池中字元串對象”Java“對應的引用
String s4 = s3.intern();
// s1 和 s2 指向的是堆中的同一個對象
System.out.println(s1 == s2); // true
// s3 和 s4 指向的是堆中不同的對象
System.out.println(s3 == s4); // false
// s1 和 s4 指向的是堆中的同一個對象
System.out.println(s1 == s4); //true
8、String 類型的變量和常量做“+”運算時發生了什麼?
先來看字元串不加
final
關鍵字拼接的情況(JDK1.8):
String str1 = "str";
String str2 = "ing";
String str3 = "str" + "ing";
String str4 = str1 + str2;
String str5 = "string";
System.out.println(str3 == str4);//false
System.out.println(str3 == str5);//true
System.out.println(str4 == str5);//false
注意 :比較 String 字元串的值是否相等,可以使用 方法。
equals()
中的
String
方法是被重寫過的。
equals
的
Object
方法是比較的對象的記憶體位址,而
equals
的
String
方法比較的是字元串的值是否相等。如果你使用
equals
比較兩個字元串是否相等的話,IDEA 還是提示你使用
==
方法替換。
equals()
對于編譯期可以确定值的字元串,也就是常量字元串 ,jvm 會将其存入字元串常量池。并且,字元串常量拼接得到的字元串常量在編譯階段就已經被存放字元串常量池,這個得益于編譯器的優化。
在編譯過程中,Javac 編譯器(下文中統稱為編譯器)會進行一個叫做 常量折疊(Constant Folding) 的代碼優化。《深入了解 Java 虛拟機》中是也有介紹到:
常量折疊會把常量表達式的值求出來作為常量嵌在最終生成的代碼中,這是 Javac 編譯器會對源代碼做的極少量優化措施之一(代碼優化幾乎都在即時編譯器中進行)。
對于
String str3 = "str" + "ing";
編譯器會給你優化成
String str3 = "string";
。
并不是所有的常量都會進行折疊,隻有編譯器在程式編譯期就可以确定值的常量才可以:
- 基本資料類型(
、byte
、boolean
、short
、char
、int
、float
、long
)以及字元串常量。double
-
修飾的基本資料類型和字元串變量final
- 字元串通過 “+”拼接得到的字元串、基本資料類型之間算數運算(加減乘除)、基本資料類型的位運算(<<、>>、>>> )
引用的值在程式編譯期是無法确定的,編譯器無法對其進行優化。
對象引用和“+”的字元串拼接方式,實際上是通過
StringBuilder
調用
append()
方法實作的,拼接完成之後調用
toString()
得到一個
String
對象 。
String str4 = new StringBuilder().append(str1).append(str2).toString();
我們在平時寫代碼的時候,盡量避免多個字元串對象拼接,因為這樣會重新建立對象。如果需要改變字元串的話,可以使用
StringBuilder
或者
StringBuffer
。
不過,字元串使用
final
關鍵字聲明之後,可以讓編譯器當做常量來處理。
示例代碼:
final String str1 = "str";
final String str2 = "ing";
// 下面兩個表達式其實是等價的
String c = "str" + "ing";// 常量池中的對象
String d = str1 + str2; // 常量池中的對象
System.out.println(c == d);// true
被
final
關鍵字修改之後的
String
會被編譯器當做常量來處理,編譯器在程式編譯期就可以确定它的值,其效果就相當于通路常量。
如果 ,編譯器在運作時才能知道其确切值的話,就無法對其優化。
示例代碼(
str2
在運作時才能确定其值):
final String str1 = "str";
final String str2 = getStr();
String c = "str" + "ing";// 常量池中的對象
String d = str1 + str2; // 在堆上建立的新的對象
System.out.println(c == d);// false
public static String getStr() {
return "ing";
}
總結
OK,今天關于Java常見類之Object、String的總結就到這裡的,希望本篇文章能夠幫助到大家,同時也希望大家看後能學有所獲!!!