Java 是很多人一直在用的程式語言,但是有些 Java 概念是非常難以理解的,哪怕是一些多年的老手,對某些 Java 概念也存在一些混淆和困惑。
所以,在這篇文章裡,會介紹四個 Java 中最難理解的四個概念,去幫助開發者更清晰的理解這些概念:
- 匿名內部類的用法
- 多執行緒
- 如何實現同步
- 序列化
匿名內部類
匿名內部類又叫匿名類,它有點像區域性類(Local Class)或者內部類(Inner Class),只是匿名內部類沒有名字,我們可以同時宣告並例項化一個匿名內部類。
一個匿名內部類僅適用在想使用一個區域性類並且只會使用這個區域性類一次的場景。
匿名內部類是沒有需要明確宣告的建構函式的,但是會有一個隱藏的自動宣告的建構函式。
建立匿名內部類有兩種辦法:
- 通過繼承一個類(具體或者抽象都可以)去建立出匿名內部類
- 通過實現一個介面建立出匿名內部類
我們們看看下面的例子:
// 介面:程式設計師
interface Programmer {
void develop();
}
public class TestAnonymousClass {
public static Programmer programmer = new Programmer() {
@Override
public void develop() {
System.out.println("我是在類中實現了介面的匿名內部類");
}
};
public static void main(String[] args) {
Programmer anotherProgrammer = new Programmer() {
@Override
public void develop() {
System.out.println("我是在方法中實現了介面的匿名內部類");
}
};
TestAnonymousClass.programmer.develop();
anotherProgrammer.develop();
}
}
從上面的例子可以看出,匿名類既可以在類中也可以在方法中被建立。
之前我們也提及匿名類既可以繼承一個具體類或者抽象類,也可以實現一個介面。所以在上面的程式碼裡,我建立了一個叫做 Programmer 的介面,並在 TestAnonymousClass 這個類中和 main() 方法中分別實現了介面。
Programmer除了介面以外既可以是一個抽象類也可以是一個具體類。
抽象類,像下面的程式碼一樣:
public abstract class Programmer {
public abstract void develop();
}
具體類程式碼如下:
public class Programmer {
public void develop() {
System.out.println("我是一個具體類");
}
}
OK,繼續深入,那麼如果 Programmer 這個類沒有無參建構函式怎麼辦?我們可以在匿名類中訪問類變數嗎?我們如果繼承一個類,需要在匿名類中實現所有方法嗎?
public class Programmer {
protected int age;
public Programmer(int age) {
this.age = age;
}
public void showAge() {
System.out.println("年齡:" + age);
}
public void develop() {
System.out.println("開發中……除了異性,他人勿擾");
}
public static void main(String[] args) {
Programmer programmer = new Programmer(38) {
@Override
public void showAge() {
System.out.println("在匿名類中的showAge方法:" + age);
}
};
programmer.showAge();
}
}
- 構造匿名類時,我們可以使用任何建構函式。上面的程式碼可以看到我們使用了帶引數的建構函式。
- 匿名類可以繼承具體類或者抽象類,也能實現介面。所以訪問修飾符規則同普通類是一樣的。子類可以訪問父類中的 protected 限制的屬性,但是無法訪問 private 限制的屬性。
- 如果匿名類繼承了具體類,比如上面程式碼中的 Programmer 類,那麼就不必重寫所有方法。但是如果匿名類繼承了一個抽象類或者實現了一個介面,那麼這個匿名類就必須實現所有沒有實現的抽象方法。
- 在一個匿名內部類中你不能使用靜態初始化,也沒辦法新增靜態變數。
- 匿名內部類中可以有被 final 修飾的靜態常量。
匿名類的典型使用場景:
- 臨時使用:我們有時候需要新增一些類的臨時實現去修復一些問題或者新增一些功能。為了避免在專案裡新增java檔案,尤其是僅使用一次這個類的時候,我們就會使用匿名類。
- UI Event Listeners:在java的圖形介面程式設計中,匿名類最常使用的場景就是去建立一個事件監聽器。比如:
button.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
}
});
上面的程式碼中,我們通過匿名類實現了 setOnClickListener 介面,當使用者點選按鈕的時候,就會觸發我們實現的 onClick 方法。
多執行緒
Java 中的多執行緒就是利用多個執行緒共同完成一個大任務的執行過程,使用多執行緒可以最大程度的利用CPU。
使用多執行緒的使用執行緒而不是程式來做任務處理,是因為執行緒比程式更加輕量,執行緒是一個輕量級的程式,是程式執行的最小單元,並且執行緒和執行緒之間是共享主記憶體的,而程式不是。
執行緒生命週期
正如上圖所示,執行緒生命週期一共有六種狀態。我們現在依次對這些狀態進行介紹。
- New:當我們構造出一個執行緒例項的時候, 這個執行緒就擁有了 New 狀態。這個狀態是執行緒的第一個狀態。此時,執行緒並沒有準備執行。
- Runnable:當呼叫了執行緒類的 start() 方法, 那麼這個執行緒就會從 New 狀態轉換到 Runnable 狀態。這就意味著這個執行緒要準備執行了。但是,如果執行緒真的要執行起來,就需要執行緒排程器來排程執行這個執行緒。但是執行緒排程器可能忙於在執行其他的執行緒,從而不能及時去排程執行這個執行緒。執行緒排程器是基於 FIFO 策略去從執行緒池中挑出一個執行緒來執行的。
- Blocked:執行緒可能會因為不同的情況自動的轉為 Blocked 狀態。比如,等候 I/O 操作,等候網路連線等等。除此之外,任意的優先順序比當前正在執行的執行緒高的執行緒都可能會使得正在執行的執行緒轉為 Blocked 狀態。
- Waiting:在同步塊中呼叫被同步物件的 wait 方法,當前執行緒就會進入 Waiting 狀態。如果在另一個執行緒中的同一個物件被同步的同步塊中呼叫 notify()/notifyAll(),就可能使得在 Waiting 的執行緒轉入 Runnable 狀態。
- Timed_Waiting:同 Waiting 狀態,只是會有個時間限制,當超時了,執行緒會自動進入 Runnable 狀態。
- Terminated:執行緒線上程的 run() 方法執行完畢後或者異常退出run()方法後,就會進入 Terminated 狀態。
為什麼要使用多執行緒
大白話講就是通過多執行緒同時做多件事情讓 Java 應用程式跑的更快,使用執行緒來實行並行和併發。如今的 CPU 都是多核並且頻率很高,如果單獨一個執行緒,並沒有充分利用多核 CPU 的優勢。
重要的優勢
- 可以更好地利用 CPU
- 可以更好地提升和響應性相關的使用者體驗
- 可以減少響應時間
- 可以同時服務多個客戶端
建立執行緒有兩種方式
- 通過繼承Thread類建立執行緒
這個繼承類會重寫 Thread 類的 run() 方法。一個執行緒的真正執行是從 run() 方法內部開始的,通過 start() 方法會去呼叫這個執行緒的 run() 方法。
public class MultithreadDemo extends Thread {
@Override
public void run() {
try {
System.out.println("執行緒 " + Thread.currentThread().getName() + " 現在正在執行");
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
MultithreadDemo multithreadDemo = new MultithreadDemo();
multithreadDemo.start();
}
}
}
- 通過實現Runnable介面建立執行緒
我們建立一個實現了 java.lang.Runnable 介面的新類,並實現其 run() 方法。然後我們會例項化一個 Thread 物件,並呼叫這個物件的 start() 方法。
public class MultithreadDemo implements Runnable {
@Override
public void run() {
try {
System.out.println("執行緒 " + Thread.currentThread().getName() + " 現在正在執行");
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(new MultithreadDemo());
thread.start();
}
}
}
兩種建立方式對比
- 如果一個類繼承了 Thread 類,那麼這個類就沒辦法繼承別的任何類了。因為 Java 是單繼承,不允許同時繼承多個類。多繼承只能採用介面的方式,一個類可以實現多個介面。所以,使用實現 Runnable 介面在實踐中比繼承 Thread 類更好一些。
- 第一種建立方式,可以重寫 yield()、interrupt() 等一些可能不太常用的方法。但是如果我們使用第二種方式去建立執行緒,則 yield() 等方法就無法重寫了。
同步
同步只有在多執行緒條件下才有意義,一次只能有一個執行緒執行同步塊。
在 Java 中,同步這個概念非常重要,因為 Java 本身就是一門多執行緒語言,在多執行緒環境中,做合適的同步是極度重要的。
為什麼要使用同步
在多執行緒環境中執行程式碼,如果一個物件可以被多個執行緒訪問,為了避免物件狀態或者程式執行出現錯誤,對這個物件使用同步是非常必要的。
在深入講解同步概念之前,我們先來看看同步相關的問題。
class Production {
//沒有做方法同步
void printProduction(int n) {
for (int i = 1; i <= 5; i++) {
System.out.print(n * i+" ");
try {
Thread.sleep(400);
} catch (Exception e) {
System.out.println(e);
}
}
}
}
class MyThread1 extends Thread {
Production p;
MyThread1(Production p) {
this.p = p;
}
public void run() {
p.printProduction(5);
}
}
class MyThread2 extends Thread {
Production p;
MyThread2(Production p) {
this.p = p;
}
public void run() {
p.printProduction(100);
}
}
public class SynchronizationTest {
public static void main(String args[]) {
Production obj = new Production(); //多執行緒共享同一個物件
MyThread1 t1 = new MyThread1(obj);
MyThread2 t2 = new MyThread2(obj);
t1.start();
t2.start();
}
}
執行上面的程式碼後,由於我們沒有加同步,可以看到執行結果非常混亂。
Output:
100 5 10 200 15 300 20 400 25 500
接下來,我們給 printProduction 方法加上同步:
class Production {
//做了方法同步
synchronized void printProduction(int n) {
for (int i = 1; i <= 5; i++) {
System.out.print(n * i+" ");
try {
Thread.sleep(400);
} catch (Exception e) {
System.out.println(e);
}
}
}
}
當我們對 printProduction() 加上了同步(synchronized)後, 已有一個執行緒執行的情況下,是不會有任何一個執行緒可以再次執行這個方法。這次加了同步後的輸出結果是有次序的。
Output:
5 10 15 20 25 100 200 300 400 500
類似於對方法做同步,你也可以去同步 Java 類和物件。
注意:其實有時候我們可以不必去同步整個方法。出於效能原因,我們其實可以僅同步方法中我們需要同步的部分程式碼。被同步的這部分程式碼就是方法中的同步塊。
序列化
Java 的序列化就是將一個 Java 物件轉化為一個位元組流的一種機制。從位元組流再轉回 Java 物件叫做反序列化,是序列化的反向操作。
序列化和反序列化是和平臺無關的,也就是說你可以在 Linux 系統序列化,然後在 Windows 作業系統做反序列化。
如果要序列化物件,需要使用 ObjectOutputStream 類的 writeObject() 方法。如果要做反序列化,則要使用 ObjectOutputStream 類的 readObject() 方法。
如下圖所示,物件被轉化為位元組流後,被儲存在了不同的介質中。這個流程就是序列化。在圖的右邊,也可以看到從不同的介質中,比如記憶體,獲得位元組流並轉化為物件,這叫做反序列化。
為什麼使用序列化
如果我們建立了一個 Java 物件,這個物件的狀態在程式執行完畢或者退出後就消失了,不會得到儲存。
所以,為了能解決這類問題,Java 提供了序列化機制。這樣,我們就能把物件的狀態做臨時儲存或者進行持久化,以供後續當我們需要這個物件時,可以通過反序列化把物件還原回來。
下面給出一些程式碼看看我們是怎麼來做序列化的。
import java.io.Serializable;
public class Player implements Serializable {
private static final long serialVersionUID = 1L;
private String serializeValueName;
private transient String nonSerializeValuePos;
public String getSerializeValueName() {
return serializeValueName;
}
public void setSerializeValueName(String serializeValueName) {
this.serializeValueName = serializeValueName;
}
public String getNonSerializeValueSalary() {
return nonSerializeValuePos;
}
public void setNonSerializeValuePos(String nonSerializeValuePos) {
this.nonSerializeValuePos = nonSerializeValuePos;
}
@Override
public String toString() {
return "Player [serializeValueName=" + serializeValueName + "]";
}
}
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
public class SerializingObject {
public static void main(String[] args) {
Player playerOutput = null;
FileOutputStream fos = null;
ObjectOutputStream oos = null;
playerOutput = new Player();
playerOutput.setSerializeValueName("niubi");
playerOutput.setNonSerializeValuePos("x:1000,y:1000");
try {
fos = new FileOutputStream("Player.ser");
oos = new ObjectOutputStream(fos);
oos.writeObject(playerOutput);
System.out.println("序列化資料被存放至Player.ser檔案");
oos.close();
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Output:
序列化資料被存放至Player.ser檔案
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public class DeSerializingObject {
public static void main(String[] args) {
Player playerInput = null;
FileInputStream fis = null;
ObjectInputStream ois = null;
try {
fis = new FileInputStream("Player.ser");
ois = new ObjectInputStream(fis);
playerInput = (Player) ois.readObject();
System.out.println("從Player.ser檔案中恢復");
ois.close();
fis.close();
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
System.out.println("player名字為 : " + playerInput.getSerializeValueName());
System.out.println("player位置為 : " + playerInput.getNonSerializeValuePos());
}
}
Output:
從Player.ser檔案中恢復
player名字為 : niubi
player位置為 : null
關鍵特性
- 如果父類實現了 Serializable 介面那麼子類就不必再實現 Serializable 介面了。但是反過來不行。
- 序列化只支援非 static 的成員變數
- static 修飾的變數和常量以及被 transient 修飾的變數是不會被序列化的。所以,如果我們不想要序列化某些非 static 的成員變數,直接用 transient 修飾它們就好了。
- 當反序列化物件的時候,是不會呼叫物件的建構函式的。
- 如果一個物件被一個要序列化的物件引用了,這個物件也會被序列化,並且這個物件也必須要實現 Serializable 介面。
總結
首先,我們介紹了匿名類的定義,使用場景和使用方式。
其次,我們討論了多執行緒和其生命週期以及多執行緒的使用場景。
再次,我們瞭解了同步,知道同步後,僅同時允許一個執行緒執行被同步的方法或者程式碼塊。當一個執行緒在執行被同步的程式碼時,別的執行緒只能在佇列中等待直到執行同步程式碼的執行緒釋放資源。
最後,我們知道了序列化就是把物件狀態儲存起來以供後續使用。
最最後,我準備了一些純手打的高質量PDF,有好友贊助的也有我自己的,大家可以免費領取:
深入淺出Java多執行緒、HTTP超全彙總、Java基礎核心總結、程式設計師必知的硬核知識大全、簡歷面試談薪的超全乾貨。
別看數量不多,但篇篇都是乾貨,看完的都說很肝。
領取方式:掃碼關注後,在公眾號後臺回覆:666