Java基礎知識複習(二)
- 2、Java面向對象
- 2.1 面向對象和麪向過程的區別
- 2.2 構造器
- 2.1.1. 構造器Constructor是否可被override
- 2.1.2. 爲什麼要在一個類中定義一個不做事的無參構造方法
- 2.1.3. 一個類的構造方法的作用是什麼。若沒有在類中定義構造方法,對象是否能夠被正確創建
- 2.1.4. 在調用子類構造方法之前會先調用父類沒有參數的構造方法,其目的是?
- 2.2 面向對象的特性
- 2.3 抽象類和接口
- 2.4 內部類
- 2.5. 修飾符
- 2.6 其他
- 2.6.1 String StringBuffer和StringBuilder的區別是什麼?String爲什麼是不可變的?
- 2.6.2 Object類的常見方法
- 2.6.3 hashcode()與equals()
- 2.6.4 Java序列化中如果有些字段不想進行序列化,怎麼辦?
- 2.6.5 自動拆箱和自動裝箱
- 3 Java在內存中的佈局
- 4 Java多線程
2、Java面向對象
2.1 面向對象和麪向過程的區別
- 面向過程:面向過程性能比面向對象高。因爲類的調用需要實例化,開銷比較大,比較消耗資源,所以當性能是最重要的考量因素時,如單片機、嵌入式開發、Linux/Unix等一般採用面向過程開發。
- 面向對象:面向對象易維護、易複用、易擴展。所以面向對象比面向過程更加靈活或、更加容易維護。但是性能沒有面向過程高。
2.2 構造器
2.1.1. 構造器Constructor是否可被override
Constructor不可以被override
,但是可以被overload
,所以可以在一個類中看到多個構造方法的情況。
2.1.2. 爲什麼要在一個類中定義一個不做事的無參構造方法
一個類在被初始化時,如果沒有用super()
來指定調用父類的某個構造方法(如果沒有父類,默認是Object),則會調用父類中的無參構造方法(默認有一個隱式的super()
,所謂隱式就是看不見)。所以,如果父類中只定義了有參構造方法,沒有定義無參構造方法,此時就會發生編譯錯誤。因爲Java程序在父類中找不到可執行的無參構造方法。解決辦法是在父類定義一個無參構造方法。
2.1.3. 一個類的構造方法的作用是什麼。若沒有在類中定義構造方法,對象是否能夠被正確創建
作用是完成對類對象的初始化工作。若沒有在類中定義構造方法,也會默認有一個隱式的無參構造方法。但如果我們自己寫了構造方法(無論是否有參),Java就不會默認爲我們添加無參的構造方法了。而且,如果我們只定義了有參構造方法,那麼在創建對象的時候必須要傳遞參數了。所以我們創建一個類的時候,最好手動的把無參構造方法寫出來,不管它是否有用。
2.1.4. 在調用子類構造方法之前會先調用父類沒有參數的構造方法,其目的是?
幫助子類做初始化工作。
2.2 面向對象的特性
這裏這裏之前寫過了,就不再贅述。
2.3 抽象類和接口
2.3.1. 接口和抽象類的區別
- 接口需要被實現,抽象類需要被繼承
- 一個類允許實現多個接口,但只允許繼承一個抽象父類
- 接口中的方法都是默認公共抽象的,抽象類中則既允許定義抽象方法也允許普通方法(jdk8中,接口被允許定義默認方法,jdk9中還允許定義私有方法)。
- 接口是對類的規範,規範的是行爲能力。抽象類是對類的抽象,抽象的是邏輯。
2.4 內部類
2.4.1 內部類的特點
- 內部類方法可以訪問該類定義治所在作用域中的數據,包括私有的數據。
- 內部類可以對同一個包中的其他類隱藏起來。
- 每一個內部類都能繼承或實現類或接口,無論外部類是否已經繼承過這個類或接口。
2.4.2 爲什麼內部類能夠直接使用外部類的變量?
創建一個帶有內部類的java程序
public class OutterClass {
private InnerClass inner = null;
public OutterClass() {
}
public InnerClass getInnerInstance() {
if(inner == null)
inner = new InnerClass();
return inner;
}
public class InnerClass{
public InnerClass() {
}
}
public static void main(String[] args) {
OutterClass.InnerClass outterClass = new OutterClass().getInnerInstance();
}
}
編譯帶有內部類的java文件,會得到兩個.class文件:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-f7SmmIrg-1592146816644)(G:\java文檔\image\內部類.png)]
反編譯 OutterClass$InnerClass.class
:
public class generics.test.OutterClass$InnerClass {
final generics.test.OutterClass this$0;
public generics.test.OutterClass$InnerClass(generics.test.OutterClass);
Code:
0: aload_0
1: aload_1
2: putfield #1 // Field this$0:Lgenerics/test/OutterClass;
5: aload_0
6: invokespecial #2 // Method java/lang/Object."<init>":()V
9: return
}
可以看到,雖然我們沒有聲明有參構造方法,但是,Java還是給給我們默認傳遞了一個外部類的實例,這個外部類實例就是用來調用外部類的數據的。
2.4.3 成員內部類中爲什麼不能定義靜態變量和靜態方法?
1)、如果內部類擁有靜態數據,但是這個內部類並不是靜態內部類,那麼jvm在加載外部類的時候就不會加載這個內部類。
2)、static類型的屬性和方法,在類加載的時候就會存在於內存中。
3)、要是用某個類的靜態變量或者方法,這個類必須先存在於jvm內存中。
2.4.4 什麼是局部內部類,能不能被外部類訪問?
局部內部類是定義在方法中,不能用public
和private
關鍵字聲明的內部類,它的作用域被限定在聲明這個局部內部類的方法體中,所以它並不能被外部類所訪問。
2.5. 修飾符
2.5.1. 在一個靜態方法內調用一個非靜態成員爲什麼是非法的?
由於靜態方法可以不通過對象進行調用,因此在靜態方法裏,不能調用其他非靜態變量,也不可以訪問非靜態變量成員。
即不能調用和訪問一個不存在的變量。
2.5.2. 靜態方法和實例方法有何不同
- 在外部調用靜態方法時,可以使用"類名.方法名"的方式,也可以使用"對象名.方法名"的方式。而實例方法只有後面這種方式。也就是說,調用靜態方法可以無需創建對象。
- 靜態方法在訪問本類的成員時,只允許訪問靜態成員(即靜態成員變量和靜態方法),而不允許訪問實例成員變量和實例方法;實例方法則無此限制。
2.6 其他
2.6.1 String StringBuffer和StringBuilder的區別是什麼?String爲什麼是不可變的?
String
類中使用final
關鍵字修飾字符數組來保存字符串(在Java9之後,使用byte數組存儲字符串)
StringBuilder
與StringBuffer
都繼承自AbstractStringBuilder
類,在 AbstractStringBuilder
中也是使用字符數組保存字符串char[]value
但是沒有用 final
關鍵字修飾,所以這兩種對象都是可變的(Java9後也是變成byte數組)。
StringBuilder
與StringBuffer
中的所有方法實現基本上都是直接調用父類方法。
StringBuffer
中的部分方法:
@Override
public synchronized char charAt(int index) {
return super.charAt(index);
}
/**
* @throws IndexOutOfBoundsException {@inheritDoc}
* @since 1.5
*/
@Override
public synchronized int codePointAt(int index) {
return super.codePointAt(index);
}
/**
* @throws IndexOutOfBoundsException {@inheritDoc}
* @since 1.5
*/
@Override
public synchronized int codePointBefore(int index) {
return super.codePointBefore(index);
}
線程安全
String
中的對象都是不可變的,可以理解爲常量,線程安全。AbstractStringBuilder
是 StringBuilder
與 StringBuffer
的公共父類,定義了一些字符串的基本操作,如insert
,append
等,Stirngbuffer
對重寫的方法都加上了同步鎖或對調用的方法加上了同步鎖,所以是線程安全的。StringBuilder
並沒有對方法進行枷鎖,所以它是非線程安全的。
性能
String
由於是不可變的,每次操作都需要重新創建一個對象,所以效率比較低,StringBuffer
和StringBuilder
都是可變的字符串,但由於StringBuffer
是線程安全的,所以在單線程環境下,效率會比StringBuilder
稍微低點。
三者的使用:
操作少量數據,可以考慮用Stirng
在單線程環境下,使用StringBuilder
在多線程環境下,保證線程安全,使用StringBuffer
2.6.2 Object類的常見方法
public final native Class<?> getClass();
public native int hashCode();
public boolean equals(Object obj)
protected native Object clone() throws CloneNotSupportedException;
public String toString()
public final native void notify()
public final native void notifyAll()
public final void wait()
public final native void wait(long timeoutMillis) throws InterruptedException; //讓當前對象進入TIMED_WATING狀態
public final void wait(long timeoutMillis, int nanos) throws InterruptedException //讓當前對象進入TIMED_WATING狀態
protected void finalize() throws Throwable
==和equals的區別
==
:它的作用是判斷兩個對象的地址是不是相等。即判斷兩個對象是不是屬於同一個對象(引用同一塊內存地址)。(基本數據類==比較的是值,引用數據類型比較的是內存地址)
Java中只有值傳遞,所以,對於==來說,不管是比較基本數據類,或者是引用數據類型,其本質都是比較值,只是引用數據類型的值是對象的內存地址。
equals()
:它的作用是判斷兩個對象是否相等,他不能用於比較基本數據類型。equals()
方法存在於Object
,Object
是所有類的直接或間接父類。
Object
的equals()
:
public boolean equals(Object obj) {
return (this == obj);
}
從上面可以看出,如果需要使用equals()
來比較兩個對象是否相等,需要重寫equals()
。
舉個例子:
public class Test1 {
public static void main(String[] args) {
String a =new String("ab"); // 創建了兩個對象,一個字符串"ab"對象,一個String對象
String b = new String("ab");// 同理
String aa = "ab"; // 創建了一個字符串"ab"放在常量池中
String bb = "ab"; // 從常量池中查找,否則自己創建一個新的
if (aa == bb) // true
System.out.println("aa==bb");
if (a == b) // false,非同一對象
System.out.println("a==b");
if (a.equals(b)) // true
System.out.println("aEQb");
if (42 == 42.0) { // true
System.out.println("true");
}
}
}
說明:
- String類已經重寫了
equals()
,因爲Object
的equals
方法是比較的對象的內存地址,而String
的equals
方法比較的是對象的值。
String的equals()
:
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;
}
2.6.3 hashcode()與equals()
1. hashcode()
hashCode() 的作用是獲取哈希碼,也稱爲散列碼;它實際上是返回一個int整數。這個哈希碼的作用是確定該對象在哈希表中的索引位置。hashCode() 定義在JDK的Object.java中,這就意味着Java中的任何類都包含有hashCode() 方法。hashcode方法一個Java本地方法,也就是說,這個方法可能是由c或c++語言實現的,用來將對象的地址計算後返回。
但是,只有當創建某個類的散列表
時,hashcode()
纔有作用,其他情況下並沒有作用。散列表存儲的是鍵值對(key-value),它的特點是:能根據“鍵”快速的檢索出對應的“值”。(可以快速找到所需要的對象)
爲什麼重寫 equals
時必須重寫 hashCode
方法?
如果兩個對象相等,則 hashcode 一定也是相同的。兩個對象相等,對兩個對象分別調用 equals 方法都返回 true。但是,兩個對象有相同的 hashcode 值,它們也不一定是相等的 。因此,equals 方法被覆蓋過,則 hashCode
方法也必須被覆蓋。
hashCode()
的默認行爲是對堆上的對象產生獨特值。如果沒有重寫hashCode()
,則該 class 的兩個對象無論如何都不會相等(即使這兩個對象指向相同的數據)
爲什麼兩個對象有一樣的hashcode值,但兩個對象不一定相等
因爲hashcode()
使用的算法可能會使不同的對象,恰巧計算出相等hashcode值,當兩個對象hashcode值一樣時,散列表會調用對象的equals()
來比較兩個對象的內容是否相等。也就是說hashcode
只是用來縮小查找成本。
重寫equals() 的規範
1)、自反性:對於任何非空引用x,x.equals(x)應該返回true。
2)、對稱性:對於任何引用x和y。只有當y.equals(x)返回true,x.equals(y)也應該返回true。
3)、傳遞性:對於任何引用x、y和z,如果x.equals(y)返回true,y.equals(z)也應該返回true。
4)、一致性:如果x和y引用的對象沒有發送變化,返回調用x.equals(y)應該返回同樣的結果。
5)、對於任意非空引用x,x.equals(null)應該返回false。
2.6.4 Java序列化中如果有些字段不想進行序列化,怎麼辦?
對於不想進行序列化的變量,使用transient
關鍵字修飾。
transient
關鍵字的作用是:阻止實例中那些用此關鍵字修飾的變量序列化;當對象被反序列化時,被transient
修飾的變量值不會被持久化和恢復。transient
只能修飾變量,不能修飾類和方法。
2.6.5 自動拆箱和自動裝箱
自動拆箱和自動裝箱時Java編譯器的一個語法糖。
自動裝箱是指:將基本數據類型轉爲對應的包裝類對象的過程。
自動拆箱是指:將包裝類對象轉爲對應基本數據類型的過程。
自動裝箱實際上調用了包裝類對象的一個方法:valueof()
自動拆箱實際上調用了包裝類對象的一個方法:intvalue()
在自動裝箱時,處於節約內存的考慮,JVM會緩存處於緩存值範圍內的對象。
Integer,Byte,Short,Long,Character包裝類型具有緩存池, 而其他三種:Float,Double,Boolean不具有緩存池。
包裝類的緩存池範圍都在 -127~127之間,除了Character 在 0~127之間。
3 Java在內存中的佈局
一個Java對象在內存中分爲3個部分
- 對象頭
- 實例數據
- 對齊填充
其中實例數據和對齊填充是不固定的。
3.1.對象頭
對象頭是jvm在每次GC時管理的對象的通用結構,包含了對象的佈局,類型(Class Type),GC狀態,同步狀態和hashcode等信息,在數組對象中,還會跟隨數據的長度。Java對象和vm對象都具有通用的對象頭格式。
對象頭主要由兩部分組成
- Mark Word
- 類型指針
- 數組長度(如果是數組纔有)
3.1.1 Mark Word
Mark Word 的定義:
Mark Word 用於存儲對象自身的運行時數據,如哈希碼(hashcode)、GC 狀態、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等。這部分數據的長度在32位和64位的虛擬機中分別爲32 bit 和 64 bit。
問題:如果對象需要存儲的運行時數據很多,超過了所在操作系統Bitmap 結構所能記錄的限度應該怎麼辦? (對象頭是與對象本身無關的額外存儲成本)
考慮到虛擬機的空間效率,Mark Word 被設計成一個非固定的數據結構,以便在極小的空間內儘可能存儲更多的信息,它會根據對象的狀態
複用
自己的存儲空間。
Mark Word組成
鎖狀態 | 鎖標誌 | markword組成 |
---|---|---|
無鎖 | 01 | 由hashcode,分代年齡,是否偏向鎖(1位),鎖標誌位組成 |
偏向鎖 | 01 | 由偏向線程的ID,偏向時間戳(epoch),是否偏向鎖 |
輕量級鎖 | 00 | 由指向棧中鎖的記錄和鎖標誌位組成 |
膨脹鎖 | 10 | 由指向鎖的指針和鎖標誌位組成 |
GC | 11 | 無數據 |
3.1.2 類型指針
類型指針即對象指向它類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。
3.1.3 數組長度
如果對象是一個Java 數組,那在對象頭中還必須有一塊用於記錄數組長度的數據,因爲虛擬機可以通過普通Java 對象的元數據確定Java 對象的大小,但是從數組的元數據無法確定數組的大小。
3.1.4 實例數據
實例數據存儲着對象在程序中被定義的各個字段的數據,也就是對象的字段數據。如果一個類沒有字段,那麼也就不存在實例數據,所以實例數據並不固定。
3.1.5 對齊填充
Java對象的大小必須是8字節的倍數,像13,15這種非8的倍數對象的大小,不足或多餘的部分就要使用對齊填充數據補齊。如果Java對象大小正好是8的倍數,那麼就無需填充數據。
4 Java多線程
進程和線程
什麼是進程,什麼是線程
把進程比喻爲火車,把線程比喻爲火車車廂
- 線程只能在某一個進程下進行(單獨一個車廂無法運行)
- 一個進程可以有多個線程(一列火車可以有多節車廂)
- 進程之間,數據共享複雜(A火車上的乘客很難換到另外一輛火車上)
- 線程之間,數據共享簡單(A車廂的乘客可以隨意走動到其他車廂)
- 進程之間不會互相影響,但是一個線程崩潰會導致整個進程崩潰(不同的火車之間是獨立的,但是一輛貨車上的車廂着火了,整輛火車都不能行駛)
- 進程使用的內存地址可以上鎖,即一個線程使用某些共享內存時,其他線程必須等它結束,才能使用這一塊內存。(比如火車上的洗手間)-“互斥鎖”
併發和並行
1、併發:多個線程任務被一個或多個cpu輪流執行。併發強調的是計算機應用程序有處理多個任務的能力。
2、並行:多個線程被一個或多個cpu執行。並行強調的是計算機應用程序擁有同時處理多任務的能力。
多線程的利弊
利:
- 線程可以比作輕量化的進程,cpu在線程間的切換和調度的成本遠遠小於進程。
- 現在是多核CPU時代,意味着多個線程可以同時進行,那麼如果我們可以利用好線程,就可以編寫出高併發的程序。
弊:
- 雖然多線程帶來的好處很多,但是想要併發編程並不容易 ,如果不能很好的控制線程,那麼就可能造成死鎖,資源閒置,內存泄漏等問題。
什麼是上下文切換
上下文切換(有時候也被稱作進程切換或任務切換)是指CPU從一個進程(或線程)切換到另一個進程(或線程)的操作。上下文指的是某一個時間點CPU寄存器和程序計數器的內容。
寄存器是CPU內部的少量的速度很快的閃存,通常存儲和訪問計算過程的中間值提高計算機程序的運行速度。
程序計數器是一個專用的寄存器,用於表明指令序列中的CPU正在執行的位置。
上下文切換前,會先保存當前線程的狀態,以便下次切換回這個任務時,可以再加載這個任務的狀態。保存線程的狀態再到重新加載回線程的狀態的這個過程就叫做上下文切換。
線程的優先級
再Java中可以通過Thread
類的setPriority方法來設置線程的優先級,雖然通過這樣的方式可以設置線程的優先級,但是線程執行的先後順序並不是依賴優先級決定的,換句話說,線程的優先級不能保證線程的執行順序。
線程的狀態
線程可以有如下6種狀態:
- New(新創建的線程)
- Runnable(可運行的線程)
- Blocked(被阻塞的線程)
- Waiting(等待中的線程)
- Timed waiting(在一個限定時間內等待的線程)
- Terminated(終止的線程)
Blocked、Waiting、Timed waiting 的區別
Blocked
Java文檔官方定義BLOCKED狀態是:“這種狀態是指一個阻塞線程在等待monitor鎖。”
比如有一個方法,這個方法被加上了synchronized
關鍵字,現在有兩個線程,一個線程B獲取到synchronized
鎖,然後在執行方法。但是呢,線程A也想執行這個方法,但是由於獲取不到鎖,只能等待,並嘗試獲取monitor鎖。
Waiting
Java文檔官方定義WAITING狀態是:“一個線程在等待另一個線程執行一個動作時在這個狀態”
在某一個對象線程上調用的Object.wait( )
,這個線程就會進入Waiting狀態,當有另外一個線程在這個對象上調用Object.notify()
或者調用了Object.notifyAll()
,這個線程纔會恢復。一個調用了Thread.join()的線程也會進入WAITING狀態直到一個特定的線程來結束。
Timed waiting
Java文檔官方定義TIMED_WAITING狀態爲:“一個線程在一個特定的等待時間內等待另一個線程完成一個動作會在這個狀態”
比如你在開車,經過一個紅綠燈路口,現在紅綠燈的狀態是紅燈,所以你需要停下並等待紅燈變爲綠燈,你纔可以繼續行駛。
調用了以下方法的線程會進入TIMED_WAITING:
Thread.sleep()
Object.wait()
並加了超時參數Thread.join()
並加了超時參數LockSupport.parkNanos()
LockSupport.parkUntil()
Sleep方法和wait方法的區別
從方法調用上:
Sleep方法是Thread類的方法,wait方法是Object類的方法。
從功能上:
- 調用Sleep方法會使當前線程讓出cpu的調度資源,使其他線程獲得被執行的機會。但是Sleep方法不會讓當前線程釋放鎖
- 調用wait方法會使當前線程直接進入
waiting
狀態並且釋放鎖,不參與鎖的競爭,使其他等待資源的線程有機會獲得鎖,等待其他線對其調用notify()
或者調用notifyAll()
,纔會重新與其他線程爭奪資源。
stop,suspend,resume等方法爲什麼會被遺棄
- stop:stop方法的作用是強行終止線程的執行,不管線程的run方法是否正在執行,資源是否已經釋放,它都會終止線程的運行,並釋放所有資源(包括鎖)。所以這個方法並不是很合理。
- suspend和resume:suspend方法用於阻塞一個線程,但並不釋放鎖, 而resume方法的作用只是爲了恢復被suspend的線程。 假設A,B線程都爭搶同一把鎖,A線程成功的獲得了鎖, 然後被suspend阻塞了,卻並沒有釋放鎖,它需要其他線程來喚醒, 但此時B線程需要獲得這把鎖才能喚醒A,所以此時就陷入了死鎖。
interrupt,interrupted,isInterrupted方法區別
- interrupt:這個方法是不中斷線程,但是給當前線程設置一箇中斷狀態
- isInterupted:當線程調用interrupt方法後,線程就有了一箇中斷狀態,而使用isInterupted方法就可以檢測線程的中斷狀態
- interrupted: 這個方法用於清除interrupt方法設置的中斷狀態。 如果一個線程之前調用了interrupt方法設置了中斷狀態, 那麼interrupted方法就可以清除這個中斷狀態。
join方法
join方法的作用是讓指定線程加入到當前線程中執行。
下面這段代碼,t線程會在main方法執行前執行。
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(()->{
try
{
TimeUnit.SECONDS.sleep(1);
}catch (Exception ignored){}
System.out.println("thread join");
});
t.start();
t.join();
System.out.println("main");
}
join方法的底層是wait方法,調用A線程(子線程)的join方法實際上是讓main線程wait, 等A線程執行完後,才能繼續執行後面的代碼。
yield方法
yield屬於Thread的靜態方法, 它的作用是讓當前線程讓出cpu調度資源。
yield方法其實就和線程的優先級一樣,你雖然指定了, 但是最後的結果不由得你說了算, 即使調用了yield方法,最後仍然可能是這個線程先執行, 只不過說別的線程可能先執行的機會稍大一些。