Java基礎知識篇(2020最新版) 準備放進收藏夾吃灰的勿進

java斗帝之路 發佈 2020-05-26T03:22:48+00:00

1. Java 基本功 1.1. Java 入門(基礎概念與常識) 1.1.1. Java 語言有哪些特點?

1. Java 基本功 1.1. Java 入門(基礎概念與常識) 1.1.1. Java 語言有哪些特點?

  1. 簡單易學;
  2. 面向對象(封裝,繼承,多態);
  3. 平台無關性( Java 虛擬機實現平台無關性);
  4. 可靠性;
  5. 安全性;
  6. 支持多線程( C++ 語言沒有內置的多線程機制,因此必須調用作業系統的多線程功能來進行多線程程序設計,而 Java 語言卻提供了多線程支持);
  7. 支持網絡編程並且很方便( Java 語言誕生本身就是為簡化網絡編程設計的,因此 Java 語言不僅支持網絡編程而且很方便);
  8. 編譯與解釋並存;

C++11 開始(2011 年的時候),C++就引入了多線程庫,在 windows、linux、macos 都可以使用std::thread和std::async來創建線程。參考連結:www.cplusplus.com/reference/t…

1.1.2. 關於 JVM JDK 和 JRE 最詳細通俗的解答 1.1.2.1. JVM

Java 虛擬機(JVM)是運行 Java 字節碼的虛擬機。JVM 有針對不同系統的特定實現(Windows,Linux,macOS),目的是使用相同的字節碼,它們都會給出相同的結果。

什麼是字節碼?採用字節碼的好處是什麼?

在 Java 中,JVM 可以理解的代碼就叫做字節碼(即擴展名為 .class 的文件),它不面向任何特定的處理器,只面向虛擬機。Java 語言通過字節碼的方式,在一定程度上解決了傳統解釋型語言執行效率低的問題,同時又保留了解釋型語言可移植的特點。所以 Java 程序運行時比較高效,而且,由於字節碼並不針對一種特定的機器,因此,Java 程序無須重新編譯便可在多種不同作業系統的計算機上運行。

Java 程序從原始碼到運行一般有下面 3 步:

我們需要格外注意的是 .class->機器碼 這一步。在這一步 JVM 類加載器首先加載字節碼文件,然後通過解釋器逐行解釋執行,這種方式的執行速度會相對比較慢。而且,有些方法和代碼塊是經常需要被調用的(也就是所謂的熱點代碼),所以後面引進了 JIT 編譯器,而 JIT 屬於運行時編譯。當 JIT 編譯器完成第一次編譯後,其會將字節碼對應的機器碼保存下來,下次可以直接使用。而我們知道,機器碼的運行效率肯定是高於 Java 解釋器的。這也解釋了我們為什麼經常會說 Java 是編譯與解釋共存的語言。

HotSpot 採用了惰性評估(Lazy Evaluation)的做法,根據二八定律,消耗大部分系統資源的只有那一小部分的代碼(熱點代碼),而這也就是 JIT 所需要編譯的部分。JVM 會根據代碼每次被執行的情況收集信息並相應地做出一些優化,因此執行的次數越多,它的速度就越快。JDK 9 引入了一種新的編譯模式 AOT(Ahead of Time Compilation),它是直接將字節碼編譯成機器碼,這樣就避免了 JIT 預熱等各方面的開銷。JDK 支持分層編譯和 AOT 協作使用。但是 ,AOT 編譯器的編譯質量是肯定比不上 JIT 編譯器的。

總結: Java 虛擬機(JVM)是運行 Java 字節碼的虛擬機。JVM 有針對不同系統的特定實現(Windows,Linux,macOS),目的是使用相同的字節碼,它們都會給出相同的結果。字節碼和不同系統的 JVM 實現是 Java 語言「一次編譯,隨處可以運行」的關鍵所在。

1.1.2.2. JDK 和 JRE

JDK 是 Java Development Kit,它是功能齊全的 Java SDK。它擁有 JRE 所擁有的一切,還有編譯器(javac)和工具(如 javadoc 和 jdb)。它能夠創建和編譯程序。

JRE 是 Java 運行時環境。它是運行已編譯 Java 程序所需的所有內容的集合,包括 Java 虛擬機(JVM),Java 類庫,java 命令和其他的一些基礎構件。但是,它不能用於創建新程序。

如果你只是為了運行一下 Java 程序的話,那麼你只需要安裝 JRE 就可以了。如果你需要進行一些 Java 編程方面的工作,那麼你就需要安裝 JDK 了。但是,這不是絕對的。有時,即使您不打算在計算機上進行任何 Java 開發,仍然需要安裝 JDK。例如,如果要使用 JSP 部署 Web 應用程式,那麼從技術上講,您只是在應用程式伺服器中運行 Java 程序。那你為什麼需要 JDK 呢?因為應用程式伺服器會將 JSP 轉換為 Java servlet,並且需要使用 JDK 來編譯 servlet。

1.1.3. Oracle JDK 和 OpenJDK 的對比

可能在看這個問題之前很多人和我一樣並沒有接觸和使用過 OpenJDK 。那麼 Oracle 和 OpenJDK 之間是否存在重大差異?下面我通過收集到的一些資料,為你解答這個被很多人忽視的問題。

對於 Java 7,沒什麼關鍵的地方。OpenJDK 項目主要基於 Sun 捐贈的 HotSpot 原始碼。此外,OpenJDK 被選為 Java 7 的參考實現,由 Oracle 工程師維護。關於 JVM,JDK,JRE 和 OpenJDK 之間的區別,Oracle 博客帖子在 2012 年有一個更詳細的答案:

問:OpenJDK 存儲庫中的原始碼與用於構建 Oracle JDK 的代碼之間有什麼區別? 答:非常接近 - 我們的 Oracle JDK 版本構建過程基於 OpenJDK 7 構建,只添加了幾個部分,例如部署代碼,其中包括 Oracle 的 Java 插件和 Java WebStart 的實現,以及一些封閉的原始碼派對組件,如圖形光柵化器,一些開源的第三方組件,如 Rhino,以及一些零碎的東西,如附加文檔或第三方字體。展望未來,我們的目的是開源 Oracle JDK 的所有部分,除了我們考慮商業功能的部分。

總結:

  1. Oracle JDK 大概每 6 個月發一次主要版本,而 OpenJDK 版本大概每三個月發布一次。但這不是固定的,我覺得了解這個沒啥用處。
  2. OpenJDK 是一個參考模型並且是完全開源的,而 Oracle JDK 是 OpenJDK 的一個實現,並不是完全開源的;
  3. Oracle JDK 比 OpenJDK 更穩定。OpenJDK 和 Oracle JDK 的代碼幾乎相同,但 Oracle JDK 有更多的類和一些錯誤修復。因此,如果您想開發企業/商業軟體,我建議您選擇 Oracle JDK,因為它經過了徹底的測試和穩定。某些情況下,有些人提到在使用 OpenJDK 可能會遇到了許多應用程式崩潰的問題,但是,只需切換到 Oracle JDK 就可以解決問題;
  4. 在響應性和 JVM 性能方面,Oracle JDK 與 OpenJDK 相比提供了更好的性能;
  5. Oracle JDK 不會為即將發布的版本提供長期支持,用戶每次都必須通過更新到最新版本獲得支持來獲取最新版本;
  6. Oracle JDK 根據二進位代碼許可協議獲得許可,而 OpenJDK 根據 GPL v2 許可獲得許可。

1.1.4. Java 和 C++的區別?

我知道很多人沒學過 C++,但是面試官就是沒事喜歡拿咱們 Java 和 C++ 比呀!沒辦法!!!就算沒學過 C++,也要記下來!

  • 都是面向對象的語言,都支持封裝、繼承和多態
  • Java 不提供指針來直接訪問內存,程序內存更加安全
  • Java 的類是單繼承的,C++ 支持多重繼承;雖然 Java 的類不可以多繼承,但是接口可以多繼承。
  • Java 有自動內存管理機制,不需要程式設計師手動釋放無用內存
  • 在 C 語言中,字符串或字符數組最後都會有一個額外的字符『\0』來表示結束。但是,Java 語言中沒有結束符這一概念。 這是一個值得深度思考的問題

1.1.5. 什麼是 Java 程序的主類 應用程式和小程序的主類有何不同?

一個程序中可以有多個類,但只能有一個類是主類。在 Java 應用程式中,這個主類是指包含 main()方法的類。而在 Java 小程序中,這個主類是一個繼承自系統類 JApplet 或 Applet 的子類。應用程式的主類不一定要求是 public 類,但小程序的主類要求必須是 public 類。主類是 Java 程序執行的入口點。

1.1.6. Java 應用程式與小程序之間有哪些差別?

簡單說應用程式是從主線程啟動(也就是 main() 方法)。applet 小程序沒有 main() 方法,主要是嵌在瀏覽器頁面上運行(調用init()或者run()來啟動),嵌入瀏覽器這點跟 flash 的小遊戲類似。

1.1.7. import java 和 javax 有什麼區別?

剛開始的時候 JavaAPI 所必需的包是 java 開頭的包,javax 當時只是擴展 API 包來使用。然而隨著時間的推移,javax 逐漸地擴展成為 Java API 的組成部分。但是,將擴展從 javax 包移動到 java 包確實太麻煩了,最終會破壞一堆現有的代碼。因此,最終決定 javax 包將成為標準 API 的一部分。 所以,實際上 java 和 javax 沒有區別。這都是一個名字。

1.1.8. 為什麼說 Java 語言「編譯與解釋並存」?

高級程式語言按照程序的執行方式分為編譯型和解釋型兩種。簡單來說,編譯型語言是指編譯器針對特定的作業系統將原始碼一次性翻譯成可被該平台執行的機器碼;解釋型語言是指解釋器對源程序逐行解釋成特定平台的機器碼並立即執行。比如,你想閱讀一本英文名著,你可以找一個英文翻譯人員幫助你閱讀, 有兩種選擇方式,你可以先等翻譯人員將全本的英文名著(也就是源碼)都翻譯成漢語,再去閱讀,也可以讓翻譯人員翻譯一段,你在旁邊閱讀一段,慢慢把書讀完。 Java 語言既具有編譯型語言的特徵,也具有解釋型語言的特徵,因為 Java 程序要經過先編譯,後解釋兩個步驟,由 Java 編寫的程序需要先經過編譯步驟,生成字節碼(*.class 文件),這種字節碼必須由 Java 解釋器來解釋執行。因此,我們可以認為 Java 語言編譯與解釋並存。

1.2. Java 語法 1.2.1. 字符型常量和字符串常量的區別?

  1. 形式上: 字符常量是單引號引起的一個字符; 字符串常量是雙引號引起的若干個字符
  2. 含義上: 字符常量相當於一個整型值( ASCII 值),可以參加表達式運算; 字符串常量代表一個地址值(該字符串在內存中存放位置)
  3. 占內存大小 字符常量只占 2 個字節; 字符串常量占若干個字節 (注意: char 在 Java 中占兩個字節)

java 編程思想第四版:2.2.2 節

1.2.2. 關於注釋?

Java 中的注釋有三種:

  1. 單行注釋
  2. 多行注釋
  3. 文檔注釋。

在我們編寫代碼的時候,如果代碼量比較少,我們自己或者團隊其他成員還可以很輕易地看懂代碼,但是當項目結構一旦複雜起來,我們就需要用到注釋了。注釋並不會執行,是我們程式設計師寫給自己看的,注釋是你的代碼說明書,能夠幫助看代碼的人快速地理清代碼之間的邏輯關係。因此,在寫程序的時候隨手加上注釋是一個非常好的習慣。 本斗帝認為:

代碼的注釋不是越詳細越好。實際上好的代碼本身就是注釋,我們要儘量規範和美化自己的代碼來減少不必要的注釋。 若程式語言足夠有表達力,就不需要注釋,儘量通過代碼來闡述。 舉個例子: 去掉下面複雜的注釋,只需要創建一個與注釋所言同一事物的函數即可 // check to see if the employee is eligible for full benefits if ((employee.falgs & HOURLY_FLAG) && (employee.age > 65)) 複製代碼 應替換為 if (employee.isEligibleForFullBenefits()) 複製代碼

1.2.3. 標識符和關鍵字的區別是什麼?

在我們編寫程序的時候,需要大量地為程序、類、變量、方法等取名字,於是就有了標識符,簡單來說,標識符就是一個名字。但是有一些標識符,Java 語言已經賦予了其特殊的含義,只能用於特定的地方,這種特殊的標識符就是關鍵字。因此,關鍵字是被賦予特殊含義的標識符。比如,在我們的日常生活中 ,「警察局」這個名字已經被賦予了特殊的含義,所以如果你開一家店,店的名字不能叫「警察局」,「警察局」就是我們日常生活中的關鍵字。

1.2.4. Java中有哪些常見的關鍵字?

1.2.5. 自增自減運算符

在寫代碼的過程中,常見的一種情況是需要某個整數類型變量增加 1 或減少 1,Java 提供了一種特殊的運算符,用於這種表達式,叫做自增運算符(++)和自減運算符(--)。 ++和--運算符可以放在操作數之前,也可以放在操作數之後,當運算符放在操作數之前時,先自增/減,再賦值;當運算符放在操作數之後時,先賦值,再自增/減。例如,當「b=++a」時,先自增(自己增加 1),再賦值(賦值給 b);當「b=a++」時,先賦值(賦值給 b),再自增(自己增加 1)。也就是,++a 輸出的是 a+1 的值,a++輸出的是 a 值。用一句口訣就是:「符號在前就先加/減,符號在後就後加/減」。

1.2.6. continue、break、和return的區別是什麼?

在循環結構中,當循環條件不滿足或者循環次數達到要求時,循環會正常結束。但是,有時候可能需要在循環的過程中,當發生了某種條件之後 ,提前終止循環,這就需要用到下面幾個關鍵詞:

  1. continue :指跳出當前的這一次循環,繼續下一次循環。
  2. break :指跳出整個循環體,繼續執行循環下面的語句。

return 用於跳出所在方法,結束該方法的運行。return 一般有兩種用法:

  1. return; :直接使用 return 結束方法執行,用於沒有返回值函數的方法
  2. return value; :return 一個特定值,用於有返回值函數的方法

1.2.7. Java泛型了解麼?什麼是類型擦除?介紹一下常用的通配符?

Java 泛型(generics)是 JDK 5 中引入的一個新特性, 泛型提供了編譯時類型安全檢測機制,該機制允許程式設計師在編譯時檢測到非法的類型。泛型的本質是參數化類型,也就是說所操作的數據類型被指定為一個參數。 Java的泛型是偽泛型,這是因為Java在編譯期間,所有的泛型信息都會被擦掉,這也就是通常所說類型擦除 。 更多關於類型擦除的問題,可以查看這篇文章:《Java泛型類型擦除以及類型擦除帶來的問題》 。

1.泛型類

如何實例化泛型類:

2.泛型接口

實現泛型接口,不指定類型:

實現泛型接口,指定類型:

3.泛型方法

使用:

常用的通配符為: T,E,K,V,?

  • ? 表示不確定的 java 類型
  • T (type) 表示具體的一個java類型
  • K V (key value) 分別代表java鍵值中的Key Value
  • E (element) 代表Element

更多關於Java 泛型中的通配符可以查看這篇文章:《聊一聊-JAVA 泛型中的通配符 T,E,K,V,》

1.2.8. ==和equals的區別

== : 它的作用是判斷兩個對象的地址是不是相等。即判斷兩個對象是不是同一個對象。(基本數據類型==比較的是值,引用數據類型==比較的是內存地址)

因為 Java 只有值傳遞,所以,對於 == 來說,不管是比較基本數據類型,還是引用數據類型的變量,其本質比較的都是值,只是引用類型變量存的值是對象的地址。

equals() : 它的作用也是判斷兩個對象是否相等,它不能用於比較基本數據類型的變量。equals()方法存在於Object類中,而Object類是所有類的直接或間接父類。

Object類equals()方法:

public boolean equals(Object obj) {

return (this == obj);

} equals() 方法存在兩種使用情況:

  • 情況 1:類沒有覆蓋 equals()方法。則通過equals()比較該類的兩個對象時,等價於通過「==」比較這兩個對象。使用的默認是 Object類equals()方法。
  • 情況 2:類覆蓋了 equals()方法。一般,我們都覆蓋 equals()方法來兩個對象的內容相等;若它們的內容相等,則返回 true(即,認為這兩個對象相等)。

舉個例子:

說明:

  • String 中的 equals 方法是被重寫過的,因為 Object 的 equals 方法是比較的對象的內存地址,而 String 的 equals 方法比較的是對象的值。
  • 當創建 String 類型的對象時,虛擬機會在常量池中查找有沒有已經存在的值和要創建的值相同的對象,如果有就把它賦給當前引用。如果沒有就在常量池中重新創建一個 String 對象。

String類equals()方法:

1.2.9. hashCode()與 equals()

面試官可能會問你:「你重寫過 hashcode 和 equals麼,為什麼重寫 equals 時必須重寫 hashCode 方法?」

1)hashCode()介紹:

hashCode() 的作用是獲取哈希碼,也稱為散列碼;它實際上是返回一個 int 整數。這個哈希碼的作用是確定該對象在哈希表中的索引位置。hashCode()定義在 JDK 的 Object 類中,這就意味著 Java 中的任何類都包含有 hashCode() 函數。另外需要注意的是: Object 的 hashcode 方法是本地方法,也就是用 c 語言或 c++ 實現的,該方法通常用來將對象的 內存地址 轉換為整數之後返回。

散列表存儲的是鍵值對(key-value),它的特點是:能根據「鍵」快速的檢索出對應的「值」。這其中就利用到了散列碼!(可以快速找到所需要的對象)

2)為什麼要有 hashCode?

我們以「HashSet 如何檢查重複」為例子來說明為什麼要有 hashCode?

當你把對象加入 HashSet 時,HashSet 會先計算對象的 hashcode 值來判斷對象加入的位置,同時也會與其他已經加入的對象的 hashcode 值作比較,如果沒有相符的 hashcode,HashSet 會假設對象沒有重複出現。但是如果發現有相同 hashcode 值的對象,這時會調用 equals()方法來檢查 hashcode 相等的對象是否真的相同。如果兩者相同,HashSet 就不會讓其加入操作成功。如果不同的話,就會重新散列到其他位置。(摘自我的 Java 啟蒙書《Head fist java》第二版)。這樣我們就大大減少了 equals 的次數,相應就大大提高了執行速度。

3)為什麼重寫 equals 時必須重寫 hashCode 方法?

如果兩個對象相等,則 hashcode 一定也是相同的。兩個對象相等,對兩個對象分別調用 equals 方法都返回 true。但是,兩個對象有相同的 hashcode 值,它們也不一定是相等的 。因此,equals 方法被覆蓋過,則 hashCode 方法也必須被覆蓋。

4)為什麼兩個對象有相同的 hashcode 值,它們也不一定是相等的?

在這裡解釋一位小夥伴的問題。以下內容摘自《Head Fisrt Java》。

因為 hashCode() 所使用的雜湊算法也許剛好會讓多個對象傳回相同的雜湊值。越糟糕的雜湊算法越容易碰撞,但這也與數據值域分布的特性有關(所謂碰撞也就是指的是不同的對象得到相同的 hashCode。

我們剛剛也提到了 HashSet,如果 HashSet 在對比的時候,同樣的 hashcode 有多個對象,它會使用 equals() 來判斷是否真的相同。也就是說 hashcode 只是用來縮小查找成本。 1.3. 基本數據類型 1.3.1. Java中的幾種基本數據類型是什麼?對應的包裝類型是什麼?各自占用多少字節呢?

Java有8種基本數據類型,分別為:

  1. 6種數字類型 :byte、short、int、long、float、double
  2. 1種字符類型:char
  3. 1中布爾型:boolean。

這八種基本類型都有對應的包裝類分別為:Byte、Short、Integer、Long、Float、Double、Character、Boolean

對於boolean,官方文檔未明確定義,它依賴於 JVM 廠商的具體實現。邏輯上理解是占用 1位,但是實際中會考慮計算機高效存儲因素。 注意:

  1. Java 里使用 long 類型的數據一定要在數值後面加上 L,否則將作為整型解析:
  2. char a = 'h'char :單引號,String a = "hello" :雙引號

1.3.2. 自動裝箱與拆箱

  • 裝箱:將基本類型用它們對應的引用類型包裝起來;
  • 拆箱:將包裝類型轉換為基本數據類型;

1.3.3. 8種基本類型的包裝類和常量池

Java 基本類型的包裝類的大部分都實現了常量池技術,即 Byte,Short,Integer,Long,Character,Boolean;前面 4 種包裝類默認創建了數值[-128,127] 的相應類型的緩存數據,Character創建了數值在[0,127]範圍的緩存數據,Boolean 直接返回True Or False。如果超出對應範圍仍然會去創建新的對象。

兩種浮點數類型的包裝類 Float,Double 並沒有實現常量池技術。**

Integer 緩存原始碼:

應用場景:

  1. Integer i1=40;Java 在編譯的時候會直接將代碼封裝成 Integer i1=Integer.valueOf(40);,從而使用常量池中的對象。
  2. Integer i1 = new Integer(40);這種情況下會創建新的對象。

Integer 比較更豐富的一個例子:

結果:

解釋: 語句 i4 == i5 + i6,因為+這個操作符不適用於 Integer 對象,首先 i5 和 i6 進行自動拆箱操作,進行數值相加,即 i4 == 40。然後 Integer 對象無法與數值進行直接比較,所以 i4 自動拆箱轉為 int 值 40,最終這條語句轉為 40 == 40 進行數值比較。

1.4. 方法(函數) 1.4.1. 什麼是方法的返回值?返回值在類的方法裡的作用是什麼?

方法的返回值是指我們獲取到的某個方法體中的代碼執行後產生的結果!(前提是該方法可能產生結果)。返回值的作用是接收出結果,使得它可以用於其他的操作!

1.4.2. 為什麼 Java 中只有值傳遞?

首先回顧一下在程序設計語言中有關將參數傳遞給方法(或函數)的一些專業術語。按值調用(call by value)表示方法接收的是調用者提供的值,而按引用調用(call by reference)表示方法接收的是調用者提供的變量地址。一個方法可以修改傳遞引用所對應的變量值,而不能修改傳遞值調用所對應的變量值。 它用來描述各種程序設計語言(不只是 Java)中方法參數傳遞方式。

Java 程序設計語言總是採用按值調用。也就是說,方法得到的是所有參數值的一個拷貝,也就是說,方法不能修改傳遞給它的任何參數變量的內容。 下面通過 3 個例子來給大家說明

結果:

解析:

在 swap 方法中,a、b 的值進行交換,並不會影響到 num1、num2。因為,a、b 中的值,只是從 num1、num2 的複製過來的。也就是說,a、b 相當於 num1、num2 的副本,副本的內容無論怎麼修改,都不會影響到原件本身。 通過上面例子,我們已經知道了一個方法不能修改一個基本數據類型的參數,而對象引用作為參數就不一樣,請看 example2.

結果:

解析:

array 被初始化 arr 的拷貝也就是一個對象的引用,也就是說 array 和 arr 指向的是同一個數組對象。 因此,外部對引用對象的改變會反映到所對應的對象上。 通過 example2 我們已經看到,實現一個改變對象參數狀態的方法並不是一件難事。理由很簡單,方法得到的是對象引用的拷貝,對象引用及其他的拷貝同時引用同一個對象。 很多程序設計語言(特別是,C++和 Pascal)提供了兩種參數傳遞的方式:值調用和引用調用。有些程式設計師(甚至本書的作者)認為 Java 程序設計語言對對象採用的是引用調用,實際上,這種理解是不對的。由於這種誤解具有一定的普遍性,所以下面給出一個反例來詳細地闡述一下這個問題。

結果:

解析:

交換之前:



交換之後:



通過上面兩張圖可以很清晰的看出: 方法並沒有改變存儲在變量 s1 和 s2 中的對象引用。swap 方法的參數 x 和 y 被初始化為兩個對象引用的拷貝,這個方法交換的是這兩個拷貝 Java 程序設計語言對對象採用的不是引用調用,實際上,對象引用是按 值傳遞的。 下面再總結一下 Java 中方法參數的使用情況:

  • 一個方法不能修改一個基本數據類型的參數(即數值型或布爾型)。
  • 一個方法可以改變一個對象參數的狀態。
  • 一個方法不能讓對象參數引用一個新的對象。

參考: 《Java 核心技術卷 Ⅰ》基礎知識第十版第四章 4.5 小節

1.4.3. 重載和重寫的區別

重載就是同樣的一個方法能夠根據輸入數據的不同,做出不同的處理 重寫就是當子類繼承自父類的相同方法,輸入數據一樣,但要做出有別於父類的響應時,你就要覆蓋父類方法

1.4.3.1. 重載

發生在同一個類中,方法名必須相同,參數類型不同、個數不同、順序不同,方法返回值和訪問修飾符可以不同。

1.4.3.2. 重寫

重寫發生在運行期,是子類對父類的允許訪問的方法的實現過程進行重新編寫。

  1. 返回值類型、方法名、參數列表必須相同,拋出的異常範圍小於等於父類,訪問修飾符範圍大於等於父類。
  2. 如果父類方法訪問修飾符為 private/final/static 則子類就不能重寫該方法,但是被 static 修飾的方法能夠被再次聲明。
  3. 構造方法無法被重寫

綜上:重寫就是子類對父類方法的重新改造,外部樣子不能改變,內部邏輯可以改變

1.4.4. 深拷貝 vs 淺拷貝

  1. 淺拷貝:對基本數據類型進行值傳遞,對引用數據類型進行引用傳遞般的拷貝,此為淺拷貝。
  2. 深拷貝:對基本數據類型進行值傳遞,對引用數據類型,創建一個新的對象,並複製其內容,此為深拷貝。

1.4.5. 方法的四種類型

1、無參數無返回值的方法

2、有參數無返回值的方法

3、有返回值無參數的方法

4、有返回值有參數的方法

5、return 在無返回值方法的特殊使用

2. Java 面向對象 2.1. 類和對象 2.1.1. 面向對象和面向過程的區別

  • 面向過程面向過程性能比面向對象高。 因為類調用時需要實例化,開銷比較大,比較消耗資源,所以當性能是最重要的考量因素的時候,比如單片機、嵌入式開發、Linux/Unix 等一般採用面向過程開發。但是,面向過程沒有面向對象易維護、易復用、易擴展。
  • 面向對象面向對象易維護、易復用、易擴展。 因為面向對象有封裝、繼承、多態性的特性,所以可以設計出低耦合的系統,使系統更加靈活、更加易於維護。但是,面向對象性能比面向過程低

這個並不是根本原因,面向過程也需要分配內存,計算內存偏移量,Java 性能差的主要原因並不是因為它是面向對象語言,而是 Java 是半編譯語言,最終的執行代碼並不是可以直接被 CPU 執行的二進位機械碼。 而面向過程語言大多都是直接編譯成機械碼在電腦上執行,並且其它一些面向過程的腳本語言性能也並不一定比 Java 好。

2.1.2. 構造器 Constructor 是否可被 override?

Constructor 不能被 override(重寫),但是可以 overload(重載),所以你可以看到一個類中有多個構造函數的情況。

2.1.3. 在 Java 中定義一個不做事且沒有參數的構造方法的作用

Java 程序在執行子類的構造方法之前,如果沒有用 super()來調用父類特定的構造方法,則會調用父類中「沒有參數的構造方法」。因此,如果父類中只定義了有參數的構造方法,而在子類的構造方法中又沒有用 super()來調用父類中特定的構造方法,則編譯時將發生錯誤,因為 Java 程序在父類中找不到沒有參數的構造方法可供執行。解決辦法是在父類里加上一個不做事且沒有參數的構造方法。

2.1.4. 成員變量與局部變量的區別有哪些?

  1. 從語法形式上看:成員變量是屬於類的,而局部變量是在方法中定義的變量或是方法的參數;成員變量可以被 public,private,static 等修飾符所修飾,而局部變量不能被訪問控制修飾符及 static 所修飾;但是,成員變量和局部變量都能被 final 所修飾。
  2. 從變量在內存中的存儲方式來看:如果成員變量是使用static修飾的,那麼這個成員變量是屬於類的,如果沒有使用static修飾,這個成員變量是屬於實例的。而對象存在於堆內存,局部變量則存在於棧內存。
  3. 從變量在內存中的生存時間上看:成員變量是對象的一部分,它隨著對象的創建而存在,而局部變量隨著方法的調用而自動消失。
  4. 成員變量如果沒有被賦初值:則會自動以類型的默認值而賦值(一種情況例外:被 final 修飾的成員變量也必須顯式地賦值),而局部變量則不會自動賦值。

2.1.5. 創建一個對象用什麼運算符?對象實體與對象引用有何不同?

new 運算符,new 創建對象實例(對象實例在堆內存中),對象引用指向對象實例(對象引用存放在棧內存中)。一個對象引用可以指向 0 個或 1 個對象(一根繩子可以不系氣球,也可以系一個氣球);一個對象可以有 n 個引用指向它(可以用 n 條繩子系住一個氣球)。

2.1.6. 一個類的構造方法的作用是什麼? 若一個類沒有聲明構造方法,該程序能正確執行嗎? 為什麼?

主要作用是完成對類對象的初始化工作。可以執行。因為一個類即使沒有聲明構造方法也會有默認的不帶參數的構造方法。如果我們自己添加了類的構造方法(無論是否有參),Java 就不會再添加默認的無參數的構造方法了,這時候,就不能直接 new 一個對象而不傳遞參數了,所以我們一直在不知不覺地使用構造方法,這也是為什麼我們在創建對象的時候後面要加一個括號(因為要調用無參的構造方法)。如果我們重載了有參的構造方法,記得都要把無參的構造方法也寫出來(無論是否用到),因為這可以幫助我們在創建對象的時候少踩坑。

2.1.7. 構造方法有哪些特性?

  1. 名字與類名相同。
  2. 沒有返回值,但不能用 void 聲明構造函數。
  3. 生成類的對象時自動執行,無需調用。

2.1.8. 在調用子類構造方法之前會先調用父類沒有參數的構造方法,其目的是?

幫助子類做初始化工作。

2.1.9. 對象的相等與指向他們的引用相等,兩者有什麼不同?

對象的相等,比的是內存中存放的內容是否相等。而引用相等,比較的是他們指向的內存地址是否相等。

2.2. 面向對象三大特徵 2.2.1. 封裝

封裝是指把一個對象的狀態信息(也就是屬性)隱藏在對象內部,不允許外部對象直接訪問對象的內部信息。但是可以提供一些可以被外界訪問的方法來操作屬性。就好像我們看不到掛在牆上的空調的內部的零件信息(也就是屬性),但是可以通過遙控器(方法)來控制空調。如果屬性不想被外界訪問,我們大可不必提供方法給外界訪問。但是如果一個類沒有提供給外界訪問的方法,那麼這個類也沒有什麼意義了。就好像如果沒有空調遙控器,那麼我們就無法操控空凋製冷,空調本身就沒有意義了(當然現在還有很多其他方法 ,這裡只是為了舉例子)。

2.2.2. 繼承

不同類型的對象,相互之間經常有一定數量的共同點。例如,小明同學、小紅同學、小李同學,都共享學生的特性(班級、學號等)。同時,每一個對象還定義了額外的特性使得他們與眾不同。例如小明的數學比較好,小紅的性格惹人喜愛;小李的力氣比較大。繼承是使用已存在的類的定義作為基礎建立新類的技術,新類的定義可以增加新的數據或新的功能,也可以用父類的功能,但不能選擇性地繼承父類。通過使用繼承,可以快速地創建新的類,可以提高代碼的重用,程序的可維護性,節省大量創建新類的時間 ,提高我們的開發效率。 關於繼承如下 3 點請記住:

  1. 子類擁有父類對象所有的屬性和方法(包括私有屬性和私有方法),但是父類中的私有屬性和方法子類是無法訪問,只是擁有
  2. 子類可以擁有自己屬性和方法,即子類可以對父類進行擴展。
  3. 子類可以用自己的方式實現父類的方法。(以後介紹)。

2.2.3. 多態

多態,顧名思義,表示一個對象具有多種的狀態。具體表現為父類的引用指向子類的實例。 多態的特點:

  • 對象類型和引用類型之間具有繼承(類)/實現(接口)的關係;
  • 對象類型不可變,引用類型可變;
  • 方法具有多態性,屬性不具有多態性;
  • 引用類型變量發出的方法調用的到底是哪個類中的方法,必須在程序運行期間才能確定;
  • 多態不能調用「只在子類存在但在父類不存在」的方法;
  • 如果子類重寫了父類的方法,真正執行的是子類覆蓋的方法,如果子類沒有覆蓋父類的方法,執行的是父類的方法。

2.3. 修飾符 2.3.1. 在一個靜態方法內調用一個非靜態成員為什麼是非法的?

由於靜態方法可以不通過對象進行調用,因此在靜態方法裡,不能調用其他非靜態變量,也不可以訪問非靜態變量成員。

2.3.2. 靜態方法和實例方法有何不同

  1. 在外部調用靜態方法時,可以使用"類名.方法名"的方式,也可以使用"對象名.方法名"的方式。而實例方法只有後面這種方式。也就是說,調用靜態方法可以無需創建對象。
  2. 靜態方法在訪問本類的成員時,只允許訪問靜態成員(即靜態成員變量和靜態方法),而不允許訪問實例成員變量和實例方法;實例方法則無此限制。

2.3.3. 常見關鍵字總結:static,final,this,super 詳見這篇文章:Java#架構師#網際網路#資料#書籍#面試寶典#全集視頻#

2.4. 接口和抽象類 2.4.1. 接口和抽象類的區別是什麼?

  1. 接口的方法默認是 public,所有方法在接口中不能有實現(Java 8 開始接口方法可以有默認實現),而抽象類可以有非抽象的方法。
  2. 接口中除了 static、final 變量,不能有其他變量,而抽象類中則不一定。
  3. 一個類可以實現多個接口,但只能實現一個抽象類。接口自己本身可以通過 extends 關鍵字擴展多個接口。
  4. 接口方法默認修飾符是 public,抽象方法可以有 public、protected 和 default 這些修飾符(抽象方法就是為了被重寫所以不能使用 private 關鍵字修飾!)。
  5. 從設計層面來說,抽象是對類的抽象,是一種模板設計,而接口是對行為的抽象,是一種行為的規範。

總結一下 jdk7~jdk9 Java 中接口概念的變化:

  1. 在 jdk 7 或更早版本中,接口裡面只能有常量變量和抽象方法。這些接口方法必須由選擇實現接口的類實現。
  2. jdk8 的時候接口可以有默認方法和靜態方法功能。
  3. Jdk 9 在接口中引入了私有方法和私有靜態方法。

2.5. 其它重要知識點 2.5.1. String StringBuffer 和 StringBuilder 的區別是什麼? String 為什麼是不可變的?

簡單的來說:String 類中使用 final 關鍵字修飾字符數組來保存字符串,private final char value[],所以String 對象是不可變的。

而 StringBuilder 與 StringBuffer 都繼承自 AbstractStringBuilder 類,在 AbstractStringBuilder 中也是使用字符數組保存字符串char[]value 但是沒有用 final 關鍵字修飾,所以這兩種對象都是可變的。

StringBuilder 與 StringBuffer 的構造方法都是調用父類構造方法也就是AbstractStringBuilder 實現的,大家可以自行查閱源碼。

AbstractStringBuilder.java

線程安全性

String 中的對象是不可變的,也就可以理解為常量,線程安全。AbstractStringBuilder 是 StringBuilder 與 StringBuffer 的公共父類,定義了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 對方法加了同步鎖或者對調用的方法加了同步鎖,所以是線程安全的。StringBuilder 並沒有對方法進行加同步鎖,所以是非線程安全的。

性能

每次對 String 類型進行改變的時候,都會生成一個新的 String 對象,然後將指針指向新的 String 對象。StringBuffer 每次都會對 StringBuffer 對象本身進行操作,而不是生成新的對象並改變對象引用。相同情況下使用 StringBuilder 相比使用 StringBuffer 僅能獲得 10%~15% 左右的性能提升,但卻要冒多線程不安全的風險。

對於三者使用的總結:

  1. 操作少量的數據: 適用 String
  2. 單線程操作字符串緩衝區下操作大量數據: 適用 StringBuilder
  3. 多線程操作字符串緩衝區下操作大量數據: 適用 StringBuffer

2.5.2. Object 類的常見方法總結


 Object 類是一個特殊的類,是所有類的父類。它主要提供了以下 11 個方法:

public final native Class<?> getClass()//native方法,用於返回當前運行時對象的Class對象,使用了final關鍵字修飾,故不允許子類重寫。 public native int hashCode() //native方法,用於返回對象的哈希碼,主要使用在哈希表中,比如JDK中的HashMap。 public boolean equals(Object obj)//用於比較2個對象的內存地址是否相等,String類對該方法進行了重寫用戶比較字符串的值是否相等。 protected native Object clone() throws CloneNotSupportedException//naitive方法,用於創建並返回當前對象的一份拷貝。一般情況下,對於任何對象 x,表達式 x.clone() != x 為true,x.clone().getClass() == x.getClass() 為true。Object本身沒有實現Cloneable接口,所以不重寫clone方法並且進行調用的話會發生CloneNotSupportedException異常。 public String toString()//返回類的名字@實例的哈希碼的16進位的字符串。建議Object所有的子類都重寫這個方法。 public final native void notify()//native方法,並且不能重寫。喚醒一個在此對象監視器上等待的線程(監視器相當於就是鎖的概念)。如果有多個線程在等待只會任意喚醒一個。 public final native void notifyAll()//native方法,並且不能重寫。跟notify一樣,唯一的區別就是會喚醒在此對象監視器上等待的所有線程,而不是一個線程。 public final native void wait(long timeout) throws InterruptedException//native方法,並且不能重寫。暫停線程的執行。注意:sleep方法沒有釋放鎖,而wait方法釋放了鎖 。timeout是等待時間。 public final void wait(long timeout, int nanos) throws InterruptedException//多了nanos參數,這個參數表示額外時間(以毫微秒為單位,範圍是 0-999999)。 所以超時的時間還需要加上nanos毫秒。 public final void wait() throws InterruptedException//跟之前的2個wait方法一樣,只不過該方法一直等待,沒有超時時間這個概念 protected void finalize() throws Throwable { }//實例被垃圾回收器回收的時候觸發的操作 複製代碼
2.5.3. == 與 equals(重要)
 == : 它的作用是判斷兩個對象的地址是不是相等。即,判斷兩個對象是不是同一個對象(基本數據類型==比較的是值,引用數據類型==比較的是內存地址)。
 equals() : 它的作用也是判斷兩個對象是否相等。但它一般有兩種使用情況:
 

2.5.3. == 與 equals(重要) == : 它的作用是判斷兩個對象的地址是不是相等。即,判斷兩個對象是不是同一個對象(基本數據類型==比較的是值,引用數據類型==比較的是內存地址)。 equals() : 它的作用也是判斷兩個對象是否相等。但它一般有兩種使用情況:

  • 情況 1:類沒有覆蓋 equals() 方法。則通過 equals() 比較該類的兩個對象時,等價於通過「==」比較這兩個對象。
  • 情況 2:類覆蓋了 equals() 方法。一般,我們都覆蓋 equals() 方法來比較兩個對象的內容是否相等;若它們的內容相等,則返回 true (即,認為這兩個對象相等)。

舉個例子:

說明:

  • String 中的 equals 方法是被重寫過的,因為 object 的 equals 方法是比較的對象的內存地址,而 String 的 equals 方法比較的是對象的值。
  • 當創建 String 類型的對象時,虛擬機會在常量池中查找有沒有已經存在的值和要創建的值相同的對象,如果有就把它賦給當前引用。如果沒有就在常量池中重新創建一個 String 對象。

2.5.4. hashCode 與 equals (重要)

面試官可能會問你:「你重寫過 hashcode 和 equals 麼,為什麼重寫 equals 時必須重寫 hashCode 方法?」

2.5.4.1. hashCode()介紹

hashCode() 的作用是獲取哈希碼,也稱為散列碼;它實際上是返回一個 int 整數。這個哈希碼的作用是確定該對象在哈希表中的索引位置。hashCode() 定義在 JDK 的 Object.java 中,這就意味著 Java 中的任何類都包含有 hashCode() 函數。 散列表存儲的是鍵值對(key-value),它的特點是:能根據「鍵」快速的檢索出對應的「值」。這其中就利用到了散列碼!(可以快速找到所需要的對象)

2.5.4.2. 為什麼要有 hashCode

我們先以「HashSet 如何檢查重複」為例子來說明為什麼要有 hashCode: 當你把對象加入 HashSet 時,HashSet 會先計算對象的 hashcode 值來判斷對象加入的位置,同時也會與該位置其他已經加入的對象的 hashcode 值作比較,如果沒有相符的 hashcode,HashSet 會假設對象沒有重複出現。但是如果發現有相同 hashcode 值的對象,這時會調用 equals()方法來檢查 hashcode 相等的對象是否真的相同。如果兩者相同,HashSet 就不會讓其加入操作成功。如果不同的話,就會重新散列到其他位置。(摘自我的 Java 啟蒙書《Head first java》第二版)。這樣我們就大大減少了 equals 的次數,相應就大大提高了執行速度。

通過我們可以看出:hashCode() 的作用就是獲取哈希碼,也稱為散列碼;它實際上是返回一個 int 整數。這個哈希碼的作用是確定該對象在哈希表中的索引位置。hashCode()在散列表中才有用,在其它情況下沒用。在散列表中 hashCode() 的作用是獲取對象的散列碼,進而確定該對象在散列表中的位置。

2.5.4.3. hashCode()與 equals()的相關規定

  1. 如果兩個對象相等,則 hashcode 一定也是相同的
  2. 兩個對象相等,對兩個對象分別調用 equals 方法都返回 true
  3. 兩個對象有相同的 hashcode 值,它們也不一定是相等的
  4. 因此,equals 方法被覆蓋過,則 hashCode 方法也必須被覆蓋
  5. hashCode() 的默認行為是對堆上的對象產生獨特值。如果沒有重寫 hashCode(),則該 class 的兩個對象無論如何都不會相等(即使這兩個對象指向相同的數據)

2.5.5. Java 序列化中如果有些欄位不想進行序列化,怎麼辦?

對於不想進行序列化的變量,使用 transient 關鍵字修飾。

transient 關鍵字的作用是:阻止實例中那些用此關鍵字修飾的的變量序列化;當對象被反序列化時,被 transient 修飾的變量值不會被持久化和恢復。transient 只能修飾變量,不能修飾類和方法。

2.5.6. 獲取用鍵盤輸入常用的兩種方法

方法 1:通過 Scanner

方法 2:通過 BufferedReader

3. Java 核心技術 3.1. 集合 3.1.1. Collections 工具類和 Arrays 工具類常見方法總結 3.2. 異常 3.2.1. Java 異常類層次結構圖

在 Java 中,所有的異常都有一個共同的祖先 java.lang 包中的 Throwable 類。Throwable: 有兩個重要的子類:Exception(異常)Error(錯誤) ,二者都是 Java 異常處理的重要子類,各自都包含大量子類。

Error(錯誤):是程序無法處理的錯誤,表示運行應用程式中較嚴重問題。大多數錯誤與代碼編寫者執行的操作無關,而表示代碼運行時 JVM(Java 虛擬機)出現的問題。例如,Java 虛擬機運行錯誤(Virtual MachineError),當 JVM 不再有繼續執行操作所需的內存資源時,將出現 OutOfMemoryError。這些異常發生時,Java 虛擬機(JVM)一般會選擇線程終止。

這些錯誤表示故障發生於虛擬機自身、或者發生在虛擬機試圖執行應用時,如 Java 虛擬機運行錯誤(Virtual MachineError)、類定義錯誤(NoClassDefFoundError)等。這些錯誤是不可查的,因為它們在應用程式的控制和處理能力之 外,而且絕大多數是程序運行時不允許出現的狀況。對於設計合理的應用程式來說,即使確實發生了錯誤,本質上也不應該試圖去處理它所引起的異常狀況。在 Java 中,錯誤通過 Error 的子類描述。

Exception(異常):是程序本身可以處理的異常。Exception 類有一個重要的子類 RuntimeException。RuntimeException 異常由 Java 虛擬機拋出。NullPointerException(要訪問的變量沒有引用任何對象時,拋出該異常)、ArithmeticException(算術運算異常,一個整數除以 0 時,拋出該異常)和 ArrayIndexOutOfBoundsException (下標越界異常)。

注意:異常和錯誤的區別:異常能被程序本身處理,錯誤是無法處理。

3.2.2. Throwable 類常用方法

  • public string getMessage():返回異常發生時的簡要描述
  • public string toString():返回異常發生時的詳細信息
  • public string getLocalizedMessage():返回異常對象的本地化信息。使用 Throwable 的子類覆蓋這個方法,可以生成本地化信息。如果子類沒有覆蓋該方法,則該方法返回的信息與 getMessage()返回的結果相同
  • public void printStackTrace():在控制台上列印 Throwable 對象封裝的異常信息

3.2.3. try-catch-finally

  • try 塊: 用於捕獲異常。其後可接零個或多個 catch 塊,如果沒有 catch 塊,則必須跟一個 finally 塊。
  • catch 塊: 用於處理 try 捕獲到的異常。
  • finally 塊: 無論是否捕獲或處理異常,finally 塊里的語句都會被執行。當在 try 塊或 catch 塊中遇到 return 語句時,finally 語句塊將在方法返回之前被執行。

在以下 4 種特殊情況下,finally 塊不會被執行:

  1. 在 finally 語句塊第一行發生了異常。 因為在其他行,finally 塊還是會得到執行
  2. 在前面的代碼中用了 System.exit(int)已退出程序。 exit 是帶參函數 ;若該語句在異常語句之後,finally 會執行
  3. 程序所在的線程死亡。
  4. 關閉 CPU。

注意: 當 try 語句和 finally 語句中都有 return 語句時,在方法返回之前,finally 語句的內容將被執行,並且 finally 語句的返回值將會覆蓋原始的返回值。如下:

如果調用 f(2),返回值將是 0,因為 finally 語句的返回值覆蓋了 try 語句塊的返回值。

3.2.4. 使用 try-with-resources 來代替try-catch-finally

《Effecitve Java》中明確指出:

Java 中類似於InputStream、OutputStream、Scanner、PrintWriter等的資源都需要我們調用close()方法來手動關閉,一般情況下我們都是通過try-catch-finally語句來實現這個需求,如下:

使用Java 7之後的try-with-resources語句改造上面的代碼:

當然多個資源需要關閉的時候,使用 try-with-resources 實現起來也非常簡單,如果你還是用try-catch-finally可能會帶來很多問題。

通過使用分號分隔,可以在try-with-resources塊中聲明多個資源:

3.3. 多線程 3.3.1. 簡述線程、程序、進程的基本概念。以及他們之間關係是什麼?

線程與進程相似,但線程是一個比進程更小的執行單位。一個進程在其執行的過程中可以產生多個線程。與進程不同的是同類的多個線程共享同一塊內存空間和一組系統資源,所以系統在產生一個線程,或是在各個線程之間作切換工作時,負擔要比進程小得多,也正因為如此,線程也被稱為輕量級進程。

程序是含有指令和數據的文件,被存儲在磁碟或其他的數據存儲設備中,也就是說程序是靜態的代碼。

進程是程序的一次執行過程,是系統運行程序的基本單位,因此進程是動態的。系統運行一個程序即是一個進程從創建,運行到消亡的過程。簡單來說,一個進程就是一個執行中的程序,它在計算機中一個指令接著一個指令地執行著,同時,每個進程還占有某些系統資源如 CPU 時間,內存空間,文件,輸入輸出設備的使用權等等。換句話說,當程序在執行時,將會被作業系統載入內存中。 線程是進程劃分成的更小的運行單位。線程和進程最大的不同在於基本上各進程是獨立的,而各線程則不一定,因為同一進程中的線程極有可能會相互影響。從另一角度來說,進程屬於作業系統的範疇,主要是同一段時間內,可以同時執行一個以上的程序,而線程則是在同一程序內幾乎同時執行一個以上的程序段。

3.3.2. 線程有哪些基本狀態?

Java 線程在運行的生命周期中的指定時刻只可能處於下面 6 種不同狀態的其中一個狀態(圖源《Java 並發編程藝術》4.1.4 節)。

線程在生命周期中並不是固定處於某一個狀態而是隨著代碼的執行在不同狀態之間切換。Java 線程狀態變遷如下圖所示(圖源《Java 並發編程藝術》4.1.4 節):

由上圖可以看出: 線程創建之後它將處於 NEW(新建) 狀態,調用 start() 方法後開始運行,線程這時候處於 READY(可運行) 狀態。可運行狀態的線程獲得了 cpu 時間片(timeslice)後就處於 RUNNING(運行) 狀態。

當線程執行 wait()方法之後,線程進入 **WAITING(等待)**狀態。進入等待狀態的線程需要依靠其他線程的通知才能夠返回到運行狀態,而 TIME_WAITING(超時等待) 狀態相當於在等待狀態的基礎上增加了超時限制,比如通過 sleep(long millis)方法或 wait(long millis)方法可以將 Java 線程置於 TIMED WAITING 狀態。當超時時間到達後 Java 線程將會返回到 RUNNABLE 狀態。當線程調用同步方法時,在沒有獲取到鎖的情況下,線程將會進入到 BLOCKED(阻塞) 狀態。線程在執行 Runnable 的run()方法之後將會進入到 TERMINATED(終止) 狀態。

3.4. 文件與 I\O 流 3.4.1. Java 中 IO 流分為幾種?

  • 按照流的流向分,可以分為輸入流和輸出流;
  • 按照操作單元劃分,可以劃分為字節流和字符流;
  • 按照流的角色劃分為節點流和處理流。

Java Io 流共涉及 40 多個類,這些類看上去很雜亂,但實際上很有規則,而且彼此之間存在非常緊密的聯繫, Java I0 流的 40 多個類都是從如下 4 個抽象類基類中派生出來的。

  • InputStream/Reader: 所有的輸入流的基類,前者是字節輸入流,後者是字符輸入流。
  • OutputStream/Writer: 所有輸出流的基類,前者是字節輸出流,後者是字符輸出流。

3.4.1.1. 既然有了字節流,為什麼還要有字符流? 問題本質想問:不管是文件讀寫還是網絡發送接收,信息的最小存儲單元都是字節,那為什麼 I/O 流操作要分為字節流操作和字符流操作呢? 回答:字符流是由 Java 虛擬機將字節轉換得到的,問題就出在這個過程還算是非常耗時,並且,如果我們不知道編碼類型就很容易出現亂碼問題。所以, I/O 流就乾脆提供了一個直接操作字符的接口,方便我們平時對字符進行流操作。如果音頻文件、圖片等媒體文件用字節流比較好,如果涉及到字符的話使用字符流比較好。 3.4.1.2. BIO,NIO,AIO 有什麼區別?

  • BIO (Blocking I/O): 同步阻塞 I/O 模式,數據的讀取寫入必須阻塞在一個線程內等待其完成。在活動連接數不是特別高(小於單機 1000)的情況下,這種模型是比較不錯的,可以讓每一個連接專注於自己的 I/O 並且編程模型簡單,也不用過多考慮系統的過載、限流等問題。線程池本身就是一個天然的漏斗,可以緩衝一些系統處理不了的連接或請求。但是,當面對十萬甚至百萬級連接的時候,傳統的 BIO 模型是無能為力的。因此,我們需要一種更高效的 I/O 處理模型來應對更高的並發量。
  • NIO (Non-blocking/New I/O): NIO 是一種同步非阻塞的 I/O 模型,在 Java 1.4 中引入了 NIO 框架,對應 java.nio 包,提供了 Channel , Selector,Buffer 等抽象。NIO 中的 N 可以理解為 Non-blocking,不單純是 New。它支持面向緩衝的,基於通道的 I/O 操作方法。 NIO 提供了與傳統 BIO 模型中的 Socket 和 ServerSocket 相對應的 SocketChannel 和 ServerSocketChannel 兩種不同的套接字通道實現,兩種通道都支持阻塞和非阻塞兩種模式。阻塞模式使用就像傳統中的支持一樣,比較簡單,但是性能和可靠性都不好;非阻塞模式正好與之相反。對於低負載、低並發的應用程式,可以使用同步阻塞 I/O 來提升開發速率和更好的維護性;對於高負載、高並發的(網絡)應用,應使用 NIO 的非阻塞模式來開發
  • AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改進版 NIO 2,它是異步非阻塞的 IO 模型。異步 IO 是基於事件和回調機制實現的,也就是應用操作之後會直接返回,不會堵塞在那裡,當後台處理完成,作業系統會通知相應的線程進行後續的操作。AIO 是異步 IO 的縮寫,雖然 NIO 在網絡操作中,提供了非阻塞的方法,但是 NIO 的 IO 行為還是同步的。對於 NIO 來說,我們的業務線程是在 IO 操作準備好時,得到通知,接著就由這個線程自行進行 IO 操作,IO 操作本身是同步的。查閱網上相關資料,我發現就目前來說 AIO 的應用還不是很廣泛,Netty 之前也嘗試使用過 AIO,不過又放棄了。
關鍵字: