Java基礎整理

目錄:
一、Java 基礎
二、容器
三、多線程
四、反射
五、對象拷貝
六、異常
七、設計模式
八、網絡編程

歡迎評論留言,文章持續更新優化

一、Java 基礎

1. JDK 和 JRE 有什么區別?

  • JDK:Java Development Kit 的簡稱,java 開發工具包,提供了 java 的開發環境和運行環境。
  • JRE:Java Runtime Environment 的簡稱,java 運行環境,為 java 的運行提供了所需環境。

具體來說 JDK 其實包含了 JRE,同時還包含了編譯 java 源碼的編譯器 javac,還包含了很多 java 程序調試和分析的工具。簡單來說:如果你需要運行 java 程序,只需安裝 JRE 就可以了,如果你需要編寫 java 程序,需要安裝 JDK。

2. == 和 equals 的區別是什么?

數據類型劃分.png

對于基本類型和引用類型 == 的作用效果是不同的,如下所示:

  • 基本類型:比較的是值是否相同;
  • 引用類型:比較的是引用是否相同;

==屬于關系運算符,equals 是一個方法,equals從本質上來說就是 == ,這個看 equals 源碼就知道了,源碼如下:

public boolean equals(Object obj) {
    return (this == obj);
}

String 和 Integer 等重寫了 equals 方法,把它變成了值比較,當我們進入 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;
}

總結 :== 對于基本類型來說是值比較,對于引用類型來說是比較的是引用;而 equals 默認情況下是引用比較,只是很多類重寫了 equals 方法,比如 String、Integer 等把它變成了值比較,所以一般情況下 equals 比較的是值是否相等。

3. String 屬于基礎的數據類型嗎?

String 不屬于基礎類型,基礎類型有 8 種:byte、boolean、char、short、int、float、long、double,而 String 屬于對象。

java基本數據類型.png

4. java 中的 Math.round(-1.5) 等于多少?

等于 -1,因為在數軸上取值時,中間值(0.5)向右取整,所以正 0.5 是往上取整,負 0.5 是直接舍棄。

延申Math類常用方法:

public class Test {
    public static void main(String[] args) {
        /**
         * abs求絕對值
         */
        System.out.println(Math.abs(-10.4));

        /**
         * ceil天花板的意思,就是返回大的值,注意一些特殊值
         */
        System.out.println(Math.ceil(-10.1));   //-10.0
        System.out.println(Math.ceil(10.7));    //11.0
        System.out.println(Math.ceil(-0.7));    //-0.0
        System.out.println("my name is -0.5 "+Math.ceil(-0.5));

        /**
         * floor地板的意思,就是返回小的值
         */
        System.out.println(Math.floor(-10.1));  //-11.0
        System.out.println(Math.floor(10.7));   //10.0
        System.out.println(Math.floor(-0.7));   //-1.0
        System.out.println(Math.floor(0.0));    //0.0
        System.out.println(Math.floor(-0.0));   //-0.0
        
        /**
         * max 兩個中返回大的值,min和它相反,就不舉例了
         */
        System.out.println(Math.max(-10.1, -10));   //-10.0
        System.out.println(Math.max(10.7, 10));     //10.7
        System.out.println(Math.max(0.0, -0.0));    //0.0

        /**
         * random 取得一個大于或者等于0.0小于不等于1.0的隨機數
         */
        System.out.println(Math.random());  //0.08417657924317234
        System.out.println(Math.random());  //0.43527904004403717
        
       /**
         * rint 四舍五入,返回double值
         * 注意.5的時候會取偶數
         */
        System.out.println(Math.rint(10.1));    //10.0
        System.out.println(Math.rint(10.7));    //11.0
        System.out.println(Math.rint(11.5));    //12.0
        System.out.println(Math.rint(10.5));    //10.0
        System.out.println(Math.rint(10.51));   //11.0
        System.out.println(Math.rint(-10.5));   //-10.0
        System.out.println(Math.rint(-11.5));   //-12.0
        System.out.println(Math.rint(-10.51));  //-11.0
        System.out.println(Math.rint(-10.6));   //-11.0
        System.out.println(Math.rint(-10.2));   //-10.0

        /**
         * round 四舍五入,float時返回int值,double時返回long值
         */
        System.out.println(Math.round(10.1));   //10
        System.out.println(Math.round(10.7));   //11
        System.out.println(Math.round(10.5));   //11
        System.out.println(Math.round(10.51));  //11
        System.out.println(Math.round(-10.5));  //-10
        System.out.println(Math.round(-10.51)); //-11
        System.out.println(Math.round(-10.6));  //-11
        System.out.println(Math.round(-10.2));  //-10

    }
}

5.final 在 java 中有什么作用?

  • final 修飾的類叫最終類,該類不能被繼承。
  • final 修飾的方法不能被重寫。
  • final 修飾的變量叫常量,常量必須初始化,初始化之后值就不能被修改。
    第一種情況,修飾基本類型(非引用類型)。這時參數的值在方法體內是不能被修改的,即不能被重新賦值。
    第二種情況,修飾引用類型,這時可以改變值,但是不能重新賦值,引用類型變量所指的引用是不能夠改變的,但是引用類型的成員變量的值是可以改變。

6. java 中操作字符串都有哪些類?它們之間有什么區別?

操作字符串的類有:String、StringBuffer、StringBuilder。

String 和 StringBuffer、StringBuilder 的區別在于 String 聲明的是不可變的對象,每次操作都會生成新的 String 對象,然后將指針指向新的 String 對象,而 StringBuffer、StringBuilder 可以在原有對象的基礎上進行操作,所以在經常改變字符串內容的情況下最好不要使用 String。

StringBuffer 和 StringBuilder 最大的區別在于,StringBuffer 是線程安全的,而 StringBuilder 是非線程安全的,但 StringBuilder 的性能卻高于 StringBuffer,所以在單線程環境下推薦使用 StringBuilder,多線程環境下推薦使用 StringBuffer。

7. 兩個對象的 hashCode()相同,則 equals()也一定為 true,對嗎?

不對,兩個對象的 hashCode()相同,equals()不一定 true。詳細請看下面的網址介紹。
hashCode和equals的區別

擴展:在 JDK 中,Object 的 hashcode 方法是本地方法,也就是用 c 語言或 c++ 實現的,該方法直接返回對象的 內存地址。hashcode()方法可以重寫,例如String l類的hashcode()方法就重寫了。

8. String str="i"與 String str=new String("i")一樣嗎?

不一樣,因為內存的分配方式不一樣。String str="i"的方式,java 虛擬機會將其分配到常量池中;而 String str=new String("i") 則會被分到堆內存中。

延申:String str="i"與 String str=new String("i") 創建了幾個對象 ?
1、String str = “abc”; 創建了幾個對象? 0個 或者 1個
2、String str = new String(“abc”);創建了幾個對象? 1個或2個

String str = "abc";和String str =new String("abc");到底分別創建了幾個對象?
java引用與對象的理解

9. 普通類和抽象類有哪些區別?

普通類不能包含抽象方法,抽象類可以包含抽象方法。
抽象類不能直接實例化,普通類可以直接實例化。

10. 接口和抽象類有什么區別?

  • 實現:抽象類的子類使用 extends 來繼承;接口必須使用 implements 來實現接口。
  • 構造函數:抽象類可以有構造函數;接口不能有。
  • main 方法:抽象類可以有 main 方法,并且我們能運行它;接口不能有 main 方法。
  • 實現數量:類可以實現很多個接口;但是只能繼承一個抽象類。
  • 修飾符:接口中的所有屬性默認為:public static final ****;接口中的所有方法默認為:public abstract ****;抽象類中的方法可以是任意訪問修飾符。

11. java 中 IO 流分為幾種?

按功能來分:輸入流(input)、輸出流(output)。
按類型來分:字節流和字符流。
字節流和字符流的區別是:字節流按 8 位傳輸以字節為單位輸入輸出數據,字符流按 16 位傳輸以字符為單位輸入輸出數據。

12. 什么是包裝類?為什么要有包裝類?基本類型與包裝類如何轉換?

Java 中有 8 個基本類型,分別對應的包裝類如下
byte -- Byte
boolean -- Boolean
short -- Short
char -- Character
int -- Integer
long -- Long
float -- Float
double -- Double

為什么要有包裝類
基本數據類型方便、簡單、高效,但泛型不支持、集合元素不支持
不符合面向對象思維
包裝類提供很多方法,方便使用,如 Integer 類 toHexString(int i)、parseInt(String s) 方法等等

基本數據類型和包裝類之間的轉換
包裝類-->基本數據類型:包裝類對象.xxxValue()
基本數據類型-->包裝類:new 包裝類(基本類型值)
JDK1.5 開始提供了自動裝箱(autoboxing)和自動拆箱(autounboxing)功能, 實現了包裝類和基本數據類型之間的自動轉換
包裝類可以實現基本類型和字符串之間的轉換,字符串轉基本類型:parseXXX(String s);基本類型轉字符串:String.valueOf(基本類型)

13. 如何理解Java的多態?其中,重載和重寫有什么區別?

多態是同一個行為具有多個不同表現形式或形態的能力,多態是同一個接口,使用不同的實例而執行不同操作,多態就是程序運行期間才確定,一個引用變量倒底會指向哪個類的實例對象,該引用變量發出的方法調用到底是哪個類中實現的方法。
多態存在的三個必要條件是:繼承,重寫,父類引用指向子類引用。
多態的三個實現方式是:重寫,接口,抽象類和抽象方法

重寫(Override)和重載(Overload)的區別

區別點 重載 重寫
參數列表 必須修改 不能修改
返回類型 可以修改 不能修改
異常 可以修改 可以減少或刪除,一定不能拋出新的或者更廣的異常
訪問 可以修改 一定不能做更嚴格的限制(可以降低限制)

14.內部類都有哪些?

有四種:靜態內部類、非靜態內部類、局部內部類、匿名內部類。

靜態內部類:靜態內部類不依賴于外部類實例而被實例化,靜態內部類不需要持有外部類的引用,靜態內部類不能訪問外部類的非靜態成員變量和非靜態方法。

非靜態內部類:非靜態內部類需要在外部類實例化后才可以被實例化,非靜態內部類需要持有對外部類的引用,非靜態內部類能夠訪問外部類的靜態和非靜態成員和方法。

局部內部類:在外部類的方法中定義的類,其作用的范圍是所在的方法內。他不能被public、private、protected來修飾。他只能訪問方法中定義的final類型的局部變量。(很少使用)

匿名內部類:是一種沒有類名的內部類。
需要注意的是:
1、匿名內部類一定是在new的后面,這個匿名內部類必須繼承一個父類或實現一個接口
2、匿名內部類不能有構造函數
3、只能創建匿名內部類的一個實例
4、在Java8之前,如果匿名內部類需要訪問外部類的局部變量,則必須用final修飾外部類的局部變量。在現在Java8已結取消了這個限制。

除了靜態內部類,其它類型的內部類都默認持有外部類的引用,這在我們分析內存泄漏的時候是需要注意的地方。


二、容器

1. java 容器都有哪些?

常用容器的圖錄:


Collection 與 Map 是頂層容器接口,對于set 、List、Queue和Map 四種集合,最常用的類在上圖以灰色背景覆蓋,分別是HashSet、TreeSet、ArrayList、ArrayDeque、LinkedList和HashMap、TreeMap等實現類。這里暫時不涉及并發集合。

2. Collection 和 Collections 有什么區別?

  • java.util.Collection 是一個集合接口(集合類的一個頂級接口)。它提供了對集合對象進行基本操作的通用接口方法。Collection接口在Java 類庫中有很多具體的實現。Collection接口的意義是為各種具體的集合提供了最大化的統一操作方式,其直接繼承接口有List、Set與Queue。
  • Collections則是集合類的一個工具類/幫助類,其中提供了一系列靜態方法,用于對集合中元素進行排序、搜索以及線程安全等各種操作。

3. List、Set、Map 之間的區別是什么?

List:有序集合,元素可重復,Vector線程安全,實現Collection接口。
Set:不重復集合,LinkedHashSet按照插入排序,SortedSet可排序,HashSet無序,實現Collection接口。
Map:鍵值對集合,存儲鍵、值和之間的映射;Key無序,唯一;value 不要求有序,允許重復。Hashtable線程安全,實現Map接口。

注意:
在java中我們通常說的集合有序無序針對的是插入順序,是指在插入元素時,插入的順序是否保持,當遍歷集合時它是否會按照插入順序展示。像TreeSet和TreeMap這樣的集合主要實現了自動排序,我們稱之為排序,而根據前面的定義它不一定是有序的

4. HashMap 和 Hashtable 有什么區別?

  • hashTable同步的,而HashMap是非同步的,效率上比hashTable要高。
  • hashMap允許空鍵值,而hashTable不允許。

5. 如何決定使用 HashMap 還是 TreeMap?

  • HashMap不支持排序;TreeMap默認是按照Key值升序排序的,可指定排序的比較器,主要用于存入元素時對元素進行自動排序。
  • HashMap大多數情況下有更好的性能,尤其是讀數據。在沒有排序要求的情況下,使用HashMap。

都是非線程安全。

進一步分析:

HashMap和TreeMap區別詳解以及底層實現

6. 說一下 HashMap 的實現原理?

HashMap概述: HashMap是基于哈希表的Map接口的非同步實現。此實現提供所有可選的映射操作,并允許使用null值和null鍵。此類不保證映射的順序,特別是它不保證該順序恒久不變。

HashMap的數據結構: 在java編程語言中,最基本的結構就是兩種,一個是數組,另外一個是模擬指針(引用),所有的數據結構都可以用這兩個基本結構來構造的,HashMap也不例外。

HashMap 基于 Hash 算法實現,通過 put(key,value) 存儲,get(key) 來獲取 value
當傳入 key 時,HashMap 會根據 key,調用 hash(Object key) 方法,計算出 hash 值,根據 hash 值將 value 保存在 Node 對象里,Node 對象保存在數組里,當計算出的 hash 值相同時,稱之為 hash 沖突,HashMap 的做法是用鏈表和紅黑樹存儲相同 hash 值的 value,當 hash 沖突的個數:小于等于 8 使用鏈表;大于 8 時,使用紅黑樹解決鏈表查詢慢的問題

ps:
上述是 JDK 1.8 HashMap 的實現原理,并不是每個版本都相同,Jdk 1.8中對HashMap的實現做了優化,當鏈表中的節點數據超過八個之后,該鏈表會轉為紅黑樹來提高查詢效率,從原來的O(n)到O(logn),JDK 1.7 的 HashMap 是基于數組 + 鏈表實現,所以 hash 沖突時鏈表的查詢效率低。hash(Object key) 方法的具體算法是 (h = key.hashCode()) ^ (h >>> 16),經過這樣的運算,讓計算的 hash 值分布更均勻

7. 說一下 HashSet 的實現原理?

  • HashSet底層由HashMap實現。
  • HashSet的值存放于HashMap的key上,HashMap 是支持 key 為 null 值的,所以 HashSet 支持添加 null 值。
  • HashSet的value統一為PRESENT。

8. ArrayList 和 LinkedList 的區別是什么?

最明顯的區別是 ArrrayList底層的數據結構是數組,支持隨機訪問,而 LinkedList 的底層數據結構是雙向循環鏈表,不支持隨機訪問。使用下標訪問一個元素,ArrayList 的時間復雜度是 O(1),而 LinkedList 是 O(n)。ArrayList 添加元素時,存在擴容問題,擴容時需要復制數組,消耗性能。LinkedList 只需要將元素添加到鏈表最后一個元素的下一個即可。

9. 如何實現數組和 List 之間的轉換?

  • List轉換成為數組:調用List的toArray方法。
  • 數組轉換成為List:調用Arrays的asList方法。

10. ArrayList 和 Vector 的區別是什么?

  • Vector是同步的,而ArrayList不是。然而,如果你尋求在迭代的時候對列表進行改變,你應該使用CopyOnWriteArrayList。
  • ArrayList比Vector快,它因為有同步,不會過載。
  • ArrayList更加通用,因為我們可以使用Collections工具類輕易地獲取同步列表和只讀列表。

11. Array 和 ArrayList 有何區別?

  • Array可以容納基本類型和對象,而ArrayList只能容納對象。
  • 定義一個 Array 時,必須指定數組的數據類型及數組長度,即數組中存放的元素個數固定并且類型相同;ArrayList 是動態數組,長度動態可變,會自動擴容。不使用泛型的時候,可以添加不同類型元素。
  • Array沒有提供ArrayList那么多功能,比如addAll、removeAll和iterator等。

12. 哪些集合類是線程安全的?

  • vector:就比arraylist多了個同步化機制(線程安全),因為效率較低,現在已經不太建議使用。在web應用中,特別是前臺頁面,往往效率(頁面響應速度)是優先考慮的。
  • statck:堆棧類,先進后出。
  • hashtable:就比hashmap多了個線程安全。
  • enumeration:枚舉,相當于迭代器。
  • java.util.concurrent 包下所有的集合類 ArrayBlockingQueue、ConcurrentHashMap、ConcurrentLinkedQueue、ConcurrentLinkedDeque...

13. 迭代器 Iterator 是什么?

迭代器是一種設計模式,它是一個對象,它可以遍歷并選擇序列中的對象,而開發人員不需要了解該序列的底層結構。迭代器通常被稱為“輕量級”對象,因為創建它的代價小。

14. Iterator 怎么使用?有什么特點?

Java中的Iterator功能比較簡單,并且只能單向移動:

  • 使用方法iterator()要求容器返回一個Iterator。第一次調用Iterator的next()方法時,它返回序列的第一個元素。注意:iterator()方法是java.lang.Iterable接口,被Collection繼承。

  • 使用next()獲得序列中的下一個元素。

  • 使用hasNext()檢查序列中是否還有元素。

  • 使用remove()將迭代器新返回的元素刪除。

Iterator是Java迭代器最簡單的實現,為List設計的ListIterator具有更多的功能,它可以從兩個方向遍歷List,也可以從List中插入和刪除元素。

15. Iterator 和 ListIterator 有什么區別?

  • Iterator可用來遍歷Set和List集合,但是ListIterator只能用來遍歷List。
  • Iterator對集合只能是前向遍歷,ListIterator既可以前向也可以后向。
  • ListIterator實現了Iterator接口,并包含其他的功能,比如:增加元素,替換元素,獲取前一個和后一個元素的索引,等等。

16.Java 8 新增特性詳解(Predicate和Stream)

http://www.lxweimin.com/p/38a58aa36ccb


三、多線程

1. 并行和并發有什么區別?

  • 并行是指兩個或者多個事件在同一時刻發生;而并發是指兩個或多個事件在同一時間間隔發生。

  • 并行沒有對 CPU 資源的搶占;并發執行的線程需要對 CPU 資源進行搶占。

  • 并行執行的線程之間不存在切換;并發操作系統會根據任務調度系統給線程分配線程的 CPU 執行時間,線程的執行會進行切換。

Java 中的多線程

  • Java 中多線程運行的程序可能是并發也可能是并行,取決于操作系統對線程的調度和計算機硬件資源( CPU 的個數和 CPU 的核數)。
  • CPU 資源比較充足時,多線程被分配到不同的 CPU 資源上,即并行;CPU 資源比較緊缺時,多線程可能被分配到同個 CPU 的某個核上去執行,即并發。
  • 不管多線程是并行還是并發,都是為了提高程序的性能。

2. 線程和進程的區別?

進程:

  • 程序執行時的一個實例
  • 每個進程都有獨立的內存地址空間
  • 系統進行資源分配和調度的基本單位
  • 進程里的堆,是一個進程中最大的一塊內存,被進程中的所有線程共享的,進程創建時分配,主要存放 new 創建的對象實例
  • 進程里的方法區,是用來存放進程中的代碼片段的,是線程共享的
  • 在多線程 OS 中,進程不是一個可執行的實體,即一個進程至少創建一個線程去執行代碼

為什么要有線程?
每個進程都有自己的地址空間,即進程空間。一個服務器通常需要接收大量并發請求,為每一個請求都創建一個進程系統開銷大、請求響應效率低,因此操作系統引進線程。

線程:

  • 進程中的一個實體
  • 進程的一個執行路徑
  • CPU 調度和分派的基本單位
  • 線程本身是不會獨立存在
  • 當前線程 CPU 時間片用完后,會讓出 CPU 等下次輪到自己時候在執行
  • 系統不會為線程分配內存,線程組之間只能共享所屬進程的資源
  • 線程只擁有在運行中必不可少的資源(如程序計數器、棧)
  • 線程里的程序計數器就是為了記錄該線程讓出 CPU 時候的執行地址,待再次分配到時間片時候就可以從自己私有的計數器指定地址繼續執行
  • 每個線程有自己的棧資源,用于存儲該線程的局部變量和調用棧幀,其它線程無權訪問

關系:

  • 一個程序至少一個進程,一個進程至少一個線程,進程中的多個線程是共享進程的資源
  • Java 中當我們啟動 main 函數時候就啟動了一個 JVM 的進程,而 main 函數所在線程就是這個進程中的一個線程,也叫做主線程
  • 一個進程中有多個線程,多個線程共享進程的堆和方法區資源,但是每個線程有自己的程序計數器,棧區域

區別:

  • 本質:進程是操作系統資源分配的基本單位;線程是任務調度和執行的基本單位
  • 內存分配:系統在運行的時候會為每個進程分配不同的內存空間,建立數據表來維護代碼段、堆棧段和數據段;除了 CPU 外,系統不會為線程分配內存,線程所使用的資源來自其所屬進程的資源
  • 資源擁有:進程之間的資源是獨立的,無法共享;同一進程的所有線程共享本進程的資源,如內存,CPU,IO 等
  • 開銷:每個進程都有獨立的代碼和數據空間,程序之間的切換會有較大的開銷;線程可以看做輕量級的進程,同一類線程共享代碼和數據空間,每個線程都有自己獨立的運行程序計數器和棧,線程之間切換的開銷小
  • 通信:進程間 以IPC(管道,信號量,共享內存,消息隊列,文件,套接字等)方式通信 ;同一個進程下,線程間可以共享全局變量、靜態變量等數據進行通信,做到同步和互斥,以保證數據的一致性
  • 調度和切換:線程上下文切換比進程上下文切換快,代價小
  • 執行過程:每個進程都有一個程序執行的入口,順序執行序列;線程不能夠獨立執行,必須依存在應用程序中,由程序的多線程控制機制控制
  • 健壯性:每個進程之間的資源是獨立的,當一個進程崩潰時,不會影響其他進程;同一進程的線程共享此線程的資源,當一個線程發生崩潰時,此進程也會發生崩潰,穩定性差,容易出現共享與資源競爭產生的各種問題,如死鎖等
  • 可維護性:線程的可維護性,代碼也較難調試,bug 難排查

3.進程與線程的選擇?

  • 需要頻繁創建銷毀的優先使用線程。因為進程創建、銷毀一個進程代價很大,需要不停的分配資源;線程頻繁的調用只改變 CPU 的執行
  • 線程的切換速度快,需要大量計算,切換頻繁時,用線程
  • 耗時的操作使用線程可提高應用程序的響應
  • 線程對 CPU 的使用效率更優,多機器分布的用進程,多核分布用線程
  • 需要跨機器移植,優先考慮用進程
  • 需要更穩定、安全時,優先考慮用進程
  • 需要速度時,優先考慮用線程
  • 并行性要求很高時,優先考慮用線程

4. 守護線程、守護進程是什么?守護線程與守護進程的區別?

https://www.cnblogs.com/chanyuli/p/11552384.html

5. 創建線程有哪幾種方式?

1.繼承Thread類創建線程類

  • 定義Thread類的子類,并重寫該類的run方法,該run方法的方法體就代表了線程要完成的任務。因此把run()方法稱為執行體。
  • 創建Thread子類的實例,即創建了線程對象。
  • 調用線程對象的start()方法來啟動該線程。

2.通過Runnable接口創建線程類

  • 定義runnable接口的實現類,并重寫該接口的run()方法,該run()方法的方法體同樣是該線程的線程執行體。
  • 創建 Runnable實現類的實例,并依此實例作為Thread的target來創建Thread對象,該Thread對象才是真正的線程對象。
  • 調用線程對象的start()方法來啟動該線程。

3.通過Callable和Future創建線程

  • 創建Callable接口的實現類,并實現call()方法,該call()方法將作為線程執行體,并且有返回值。
  • 創建Callable實現類的實例,使用FutureTask類來包裝Callable對象,該FutureTask對象封裝了該Callable對象的call()方法的返回值。
  • 使用FutureTask對象作為Thread對象的target創建并啟動新線程。
  • 調用FutureTask對象的get()方法來獲得子線程執行結束后的返回值。

6. 說一下 runnable 和 callable 有什么區別?

  • Runnable接口中的run()方法的返回值是void,它做的事情只是純粹地去執行run()方法中的代碼而已;
  • Callable接口中的call()方法是有返回值的,是一個泛型,和Future、FutureTask配合可以用來獲取異步執行的結果。

7. 線程有哪些狀態?

線程通常都有五種狀態,創建、就緒、運行、阻塞和死亡。

  • 創建狀態。在生成線程對象,并沒有調用該對象的start方法,這是線程處于創建狀態。
  • 就緒狀態。當調用了線程對象的start方法之后,該線程就進入了就緒狀態,但是此時線程調度程序還沒有把該線程設置為當前線程,此時處于就緒狀態。在線程運行之后,從等待或者睡眠中回來之后,也會處于就緒狀態。
  • 運行狀態。線程調度程序將處于就緒狀態的線程設置為當前線程,此時線程就進入了運行狀態,開始運行run函數當中的代碼。
  • 阻塞狀態。線程正在運行的時候,被暫停,通常是為了等待某個時間的發生(比如說某項資源就緒)之后再繼續運行。sleep,suspend,wait等方法都可以導致線程阻塞。
  • 死亡狀態。如果一個線程的run方法執行結束或者調用stop方法后,該線程就會死亡。對于已經死亡的線程,無法再使用start方法令其進入就緒

8. sleep() 和 wait() 有什么區別?

sleep():方法是線程類(Thread)的靜態方法,讓調用線程進入睡眠狀態,讓出執行機會給其他線程,等到休眠時間結束后,線程進入就緒狀態和其他線程一起競爭cpu的執行時間。因為sleep() 是static靜態的方法,他不能改變對象的機鎖,當一個synchronized塊中調用了sleep() 方法,線程雖然進入休眠,但是對象的機鎖沒有被釋放,其他線程依然無法訪問這個對象。

wait():wait()是Object類的方法,當一個線程執行到wait方法時,它就進入到一個和該對象相關的等待池,同時釋放對象的機鎖,使得其他線程能夠訪問,可以通過notify,notifyAll方法來喚醒等待的線程。

9. notify()和 notifyAll()有什么區別?

  • 如果線程調用了對象的 wait()方法,那么線程便會處于該對象的等待池中,等待池中的線程不會去競爭該對象的鎖。
  • 當有線程調用了對象的 notifyAll()方法(喚醒所有 wait 線程)或 notify()方法(只隨機喚醒一個 wait 線程),被喚醒的的線程便會進入該對象的鎖池中,鎖池中的線程會去競爭該對象鎖。也就是說,調用了notify后只有一個線程會由等待池進入鎖池,而notifyAll會將該對象等待池內的所有線程移動到鎖池中,等待鎖競爭。
  • 優先級高的線程競爭到對象鎖的概率大,假若某線程沒有競爭到該對象鎖,它還會留在鎖池中,唯有線程再次調用 wait()方法,它才會重新回到等待池中。而競爭到對象鎖的線程則繼續往下執行,直到執行完了 synchronized 代碼塊,它會釋放掉該對象鎖,這時鎖池中的線程會繼續競爭該對象鎖。

10. 線程的 run()和 start()有什么區別?

每個線程都是通過某個特定Thread對象所對應的方法run()來完成其操作的,方法run()稱為線程體。通過調用Thread類的start()方法來啟動一個線程。

start()方法來啟動一個線程,真正實現了多線程運行。這時無需等待run方法體代碼執行完畢,可以直接繼續執行下面的代碼; 這時此線程是處于就緒狀態, 并沒有運行。 然后通過此Thread類調用方法run()來完成其運行狀態, 這里方法run()稱為線程體,它包含了要執行的這個線程的內容, Run方法運行結束, 此線程終止。然后CPU再調度其它線程。

run()方法是在本線程里的,只是線程里的一個函數,而不是多線程的。 如果直接調用run(),其實就相當于是調用了一個普通函數而已,直接待用run()方法必須等待run()方法執行完畢才能執行下面的代碼,所以執行路徑還是只有一條,根本就沒有線程的特征,所以在多線程執行時要使用start()方法而不是run()方法。

11.什么是線程池?為什么要使用線程池?

什么是線程池?

線程池就是創建若干個可執行的線程放入一個池(容器)中,有任務需要處理時,會提交到線程池中的任務隊列,處理完之后線程并不會被銷毀,而是仍然在線程池中等待下一個任務。

為什么要使用線程池?
因為 Java 中創建一個線程,需要調用操作系統內核的 API,操作系統要為線程分配一系列的資源,成本很高,所以線程是一個重量級的對象,應該避免頻繁創建和銷毀。使用線程池就能很好地避免頻繁創建和銷毀。

JDK 中提供的最核心的線程池工具類 ThreadPoolExecutor,在 JDK 1.8 中這個類最復雜的構造方法有 7 個參數。

ThreadPoolExecutor(
    int corePoolSize,
    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue,
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler)
  • corePoolSize:線程池保有的最小線程數。
  • maximumPoolSize:線程池創建的最大線程數。
  • keepAliveTime:上面提到項目根據忙閑來增減人員,那在編程世界里,如何定義忙和閑呢?很簡單,一個線程如果在一段時間內,都沒有執行任務,說明很閑,keepAliveTime 和 unit 就是用來定義這個“一段時間”的參數。也就是說,如果一個線程空閑了keepAliveTime & unit這么久,而且線程池的線程數大于 corePoolSize ,那么這個空閑的線程就要被回收了。
  • unit:keepAliveTime 的時間單位
  • workQueue:任務隊列
  • threadFactory:線程工廠對象,可以自定義如何創建線程,如給線程指定name。
  • handler:自定義任務的拒絕策略。線程池中所有線程都在忙碌,且任務隊列已滿,線程池就會拒絕接收再提交的任務。handler 就是拒絕策略,包括 4 種(即RejectedExecutionHandler 接口的 4個實現類)。
    1.AbortPolicy:默認的拒絕策略,throws RejectedExecutionException
    2.CallerRunsPolicy:提交任務的線程自己去執行該任務
    3.DiscardPolicy:直接丟棄任務,不拋出任何異常
    4.DiscardOldestPolicy:丟棄最老的任務,加入新的任務

12. 創建線程池有哪幾種方式?

  1. newFixedThreadPool(int nThreads)

創建一個固定長度的線程池,每當提交一個任務就創建一個線程,直到達到線程池的最大數量,這時線程規模將不再變化,當線程發生未預期的錯誤而結束時,線程池會補充一個新的線程。

  1. newCachedThreadPool()

創建一個可緩存的線程池,如果線程池的規模超過了處理需求,將自動回收空閑線程,而當需求增加時,則可以自動添加新線程,線程池的規模不存在任何限制。

  1. newSingleThreadExecutor()

這是一個單線程的Executor,它創建單個工作線程來執行任務,如果這個線程異常結束,會創建一個新的來替代它;它的特點是能確保依照任務在隊列中的順序來串行執行。

  1. newScheduledThreadPool(int corePoolSize)

創建了一個固定長度的線程池,而且以延遲或定時的方式來執行任務,類似于Timer。

5.newSingleThreadScheduledExecutor

單線程可執行周期性任務的線程池

6.newWorkStealingPool

任務竊取線程池,不保證執行順序,適合任務耗時差異較大。

線程池中有多個線程隊列,有的線程隊列中有大量的比較耗時的任務堆積,而有的線程隊列卻是空的,就存在有的線程處于饑餓狀態,當一個線程處于饑餓狀態時,它就會去其它的線程隊列中竊取任務。解決饑餓導致的效率問題。

默認創建的并行 level 是 CPU 的核數。主線程結束,即使線程池有任務也會立即停止。

13. 線程池都有哪些狀態?

線程池有5種狀態:Running、ShutDown、Stop、Tidying、Terminated。

見 ThreadPoolExecutor 源碼

// runState is stored in the high-order bits
    private static final int RUNNING    = -1 <<COUNT_BITS;
    private static final int SHUTDOWN   =  0 <<COUNT_BITS;
    private static final int STOP       =  1 <<COUNT_BITS;
    private static final int TIDYING    =  2 <<COUNT_BITS;
    private static final int TERMINATED =  3 <<COUNT_BITS;
  1. RUNNING:線程池一旦被創建,就處于 RUNNING 狀態,任務數為 0,能夠接收新任務,對已排隊的任務進行處理。

  2. SHUTDOWN:不接收新任務,但能處理已排隊的任務。調用線程池的 shutdown() 方法,線程池由 RUNNING 轉變為 SHUTDOWN 狀態。

  3. STOP:不接收新任務,不處理已排隊的任務,并且會中斷正在處理的任務。調用線程池的 shutdownNow() 方法,線程池由(RUNNING 或 SHUTDOWN ) 轉變為 STOP 狀態。

  4. TIDYING:

  • SHUTDOWN 狀態下,任務數為 0, 其他所有任務已終止,線程池會變為 TIDYING 狀態,會執行 terminated() 方法。線程池中的 terminated() 方法是空實現,可以重寫該方法進行相應的處理。
  • 線程池在 SHUTDOWN 狀態,任務隊列為空且執行中任務為空,線程池就會由 SHUTDOWN 轉變為 TIDYING 狀態。
  • 線程池在 STOP 狀態,線程池中執行中任務為空時,就會由 STOP 轉變為 TIDYING 狀態。
  1. TERMINATED:線程池徹底終止。線程池在 TIDYING 狀態執行完 terminated() 方法就會由 TIDYING 轉變為 TERMINATED 狀態。

線程池各個狀態切換框架圖:

14. 線程池中 submit()和 execute()方法有什么區別?

  • 接收的參數不一樣
  • submit有返回值,而execute沒有
  • submit方便Exception處理

15. 在 java 程序中怎么保證多線程的運行安全?

線程安全在三個方面體現:

  • 原子性:提供互斥訪問,同一時刻只能有一個線程對數據進行操作,
    (atomic,synchronized,lock);
  • 可見性:一個線程對主內存的修改可以及時地被其他線程看到,(synchronized,volatile,lock);
  • 有序性:一個線程觀察其他線程中的指令執行順序,由于指令重排序,該觀察結果一般雜亂無序,(synchronized,lock)。

16. 多線程鎖的升級原理是什么?

在Java中,鎖共有4種狀態,鎖的級別從低到高:

無鎖 -> 偏向鎖 -> 輕量級鎖 -> 重量級鎖

這幾個狀態會隨著競爭情況逐漸升級。鎖可以升級但不能降級。

鎖分級別原因:

沒有優化以前,synchronized 是重量級鎖(悲觀鎖),使用 wait 和 notify、notifyAll 來切換線程狀態非常消耗系統資源;線程的掛起和喚醒間隔很短暫,這樣很浪費資源,影響性能。所以 JVM 對 synchronized 關鍵字進行了優化,把鎖分為 無鎖、偏向鎖、輕量級鎖、重量級鎖 狀態。鎖升級的目的是為了減低鎖帶來的性能消耗,在 Java 6 之后優化 synchronized 為此方式。

無鎖:沒有對資源進行鎖定,所有的線程都能訪問并修改同一個資源,但同時只有一個線程能修改成功,其他修改失敗的線程會不斷重試直到修改成功。

偏向鎖:對象的代碼一直被同一線程執行,不存在多個線程競爭,該線程在后續的執行中自動獲取鎖,降低獲取鎖帶來的性能開銷。偏向鎖,指的就是偏向第一個加鎖線程,該線程是不會主動釋放偏向鎖的,只有當其他線程嘗試競爭偏向鎖才會被釋放。

偏向鎖的撤銷,需要在某個時間點上沒有字節碼正在執行時,先暫停擁有偏向鎖的線程,然后判斷鎖對象是否處于被鎖定狀態。如果線程不處于活動狀態,則將對象頭設置成無鎖狀態,并撤銷偏向鎖;

如果線程處于活動狀態,升級為輕量級鎖的狀態。

輕量級鎖:輕量級鎖是指當鎖是偏向鎖的時候,被第二個線程 B 所訪問,此時偏向鎖就會升級為輕量級鎖,線程 B 會通過自旋的形式嘗試獲取鎖,線程不會阻塞,從而提高性能。

當前只有一個等待線程,則該線程將通過自旋進行等待。但是當自旋超過一定的次數時,輕量級鎖便會升級為重量級鎖;當一個線程已持有鎖,另一個線程在自旋,而此時又有第三個線程來訪時,輕量級鎖也會升級為重量級鎖。

重量級鎖:指當有一個線程獲取鎖之后,其余所有等待獲取該鎖的線程都會處于阻塞狀態。

重量級鎖通過對象內部的監視器(monitor)實現,而其中 monitor 的本質是依賴于底層操作系統的 Mutex Lock 實現,操作系統實現線程之間的切換需要從用戶態切換到內核態,切換成本非常高。

synchronized 鎖升級的過程:

1.在鎖對象的對象頭里面有一個 threadid 字段,未訪問時 threadid 為空
2.第一次訪問 jvm 讓其持有偏向鎖,并將 threadid 設置為其線程 id
3.再次訪問時會先判斷 threadid 是否與其線程 id 一致。如果一致則可以直接使用此對象;如果不一致,則升級偏向鎖為輕量級鎖,通過自旋循環一定次數來獲取鎖
4.執行一定次數之后,如果還沒有正常獲取到要使用的對象,此時就會把鎖從輕量級升級為重量級鎖

synchronized四種鎖狀態的升級

17. 什么是線程死鎖?

線程死鎖是指由于兩個或者多個線程互相持有所需要的資源,導致這些線程一直處于等待其他線程釋放資源的狀態,無法繼續執行,如果線程都不主動釋放所占有的資源,將產生死鎖。當線程處于這種僵持狀態時,若無外力作用,它們都將無法再向前推進。

18. 怎么防止死鎖?

某個任務在等待另一個任務,而后者又等待別的任務,這樣一直下去,直到這個鏈條上的任務又在等待第一個任務釋放鎖。這得到了一個任務之間互相等待的連續循環,沒有哪個線程能繼續。這被稱之為死鎖。當以下四個條件同時滿足時,就會產生死鎖:
(1) 互斥條件。在一段時間內某資源只由一個進程占用。
(2) 請求和保持條件。指進程已經保持至少一個資源,但又提出了新的資源請求,而該資源已被其它進程占有,此時請求進程阻塞,但又對自己已獲得的其它資源保持不放。
(3) 不剝奪條件。指進程已獲得的資源,在未使用完之前,不能被剝奪,只能在使用完時由自己釋放。
(4) 環路等待條件。一個任務正在等待另一個任務所持有的資源,后者又在等待別的任務所持有的資源,這樣一直下去,直到有一個任務在等待第一個任務所持有的資源,使得大家都被鎖住。
要解決死鎖問題,必須打破上面四個條件的其中之一。在程序中,最容易打破的往往是第四個條件。
關于如何手寫死鎖和定位方法,可參考這篇博客

19. ThreadLocal 是什么?有哪些使用場景?

線程局部變量是局限于線程內部的變量,屬于線程自身所有,不在多個線程間共享。Java提供ThreadLocal類來支持線程局部變量,是一種實現線程安全的方式。但是在管理環境下(如 web 服務器)使用線程局部變量的時候要特別小心,在這種情況下,工作線程的生命周期比任何應用變量的生命周期都要長。任何線程局部變量一旦在工作完成后沒有釋放,Java 應用就存在內存泄露的風險。

20.說一下 synchronized 底層實現原理?

https://blog.csdn.net/javazejian/article/details/72828483

21. synchronized 和 volatile 的區別是什么?

作用:

  • synchronized 表示只有一個線程可以獲取作用對象的鎖,執行代碼,阻塞其他線程。
  • volatile 表示變量在 CPU 的寄存器中是不確定的,必須從主存中讀取。保證多線程環境下變量的可見性;禁止指令重排序。

區別:

  • synchronized 可以作用于變量、方法、對象;volatile 只能作用于變量。
  • synchronized 可以保證線程間的有序性(個人猜測是無法保證線程內的有序性,即線程內的代碼可能被 CPU 指令重排序)、原子性和可見性;volatile 只保證了可見性和有序性,無法保證原子性。
  • synchronized 線程阻塞,volatile 線程不阻塞。
  • volatile 本質是告訴 jvm 當前變量在寄存器中的值是不安全的需要從內存中讀取;sychronized 則是鎖定當前變量,只有當前線程可以訪問到該變量其他線程被阻塞。
  • volatile 標記的變量不會被編譯器優化;synchronized 標記的變量可以被編譯器優化。

22. synchronized 和 Lock 有什么區別?

  • 實現層面不一樣。synchronized 是 Java 關鍵字,JVM層面 實現加鎖和釋放鎖;Lock 是一個接口,在代碼層面實現加鎖和釋放鎖
  • 獲取鎖成功是否可知。synchronized無法判斷是否獲取鎖的狀態,Lock可以判斷是否獲取到鎖;
  • 是否自動釋放鎖。synchronized會自動釋放鎖(a 線程執行完同步代碼會釋放鎖 ;b 線程執行過程中發生異常會釋放鎖),Lock需在finally中手工釋放鎖(unlock()方法釋放鎖),否則容易造成線程死鎖;
  • 是否一直等待。用synchronized關鍵字的兩個線程1和線程2,如果當前線程1獲得鎖,線程2線程等待。如果線程1阻塞,線程2則會一直等待下去,而Lock鎖就不一定會等待下去,如果嘗試獲取不到鎖,線程可以不用一直等待就結束了;
    +功能復雜性, synchronized的鎖可重入、不可中斷、非公平,而Lock鎖可重入、可判斷、可公平(兩者皆可);
  • Lock鎖適合大量同步的代碼的同步問題,synchronized鎖適合代碼少量的同步問題。

23. synchronized 和 ReentrantLock 區別是什么?

synchronized是和if、else、for、while一樣的關鍵字,ReentrantLock是類,這是二者的本質區別。既然ReentrantLock是類,那么它就提供了比synchronized更多更靈活的特性,可以被繼承、可以有方法、可以有各種各樣的類變量,ReentrantLock比synchronized的擴展性體現在幾點上:

  • ReentrantLock可以對獲取鎖的等待時間進行設置,這樣就避免了死鎖
  • ReentrantLock可以獲取各種鎖的信息
  • ReentrantLock可以靈活地實現多路通知

另外,二者的鎖機制其實也是不一樣的:ReentrantLock底層調用的是Unsafe的park方法加鎖,synchronized操作的應該是對象頭中mark word。

24. 說一下 atomic 的原理?

Atomic包中的類基本的特性就是在多線程環境下,當有多個線程同時對單個(包括基本類型及引用類型)變量進行操作時,具有排他性,即當多個線程同時對該變量的值進行更新時,僅有一個線程能成功,而未成功的線程可以向自旋鎖一樣,繼續嘗試,一直等到執行成功。

Atomic系列的類中的核心方法都會調用unsafe類中的幾個本地方法。我們需要先知道一個東西就是Unsafe類,全名為:sun.misc.Unsafe,這個類包含了大量的對C代碼的操作,包括很多直接內存分配以及原子操作的調用,而它之所以標記為非安全的,是告訴你這個里面大量的方法調用都會存在安全隱患,需要小心使用,否則會導致嚴重的后果,例如在通過unsafe分配內存的時候,如果自己指定某些區域可能會導致一些類似C++一樣的指針越界到其他進程的問題。


四、反射

1.什么是反射?

對于任意一個對象,都能夠調用它的任意方法和屬性;這種動態獲取信息以及動態調用對象方法的功能稱為java語言的反射機制。

Java反射機制主要提供了以下功能:

  • 獲取任意類的名稱、package 信息、所有屬性、方法、注解、類型、類加載器、modifiers(public、static)、父類、現實接口等
  • 獲取任意對象的屬性,并且能改變對象的屬性
  • 調用任意對象的方法
  • 判斷任意一個對象所屬的類
  • 實例化任意一個類的對象
  • 生成動態代理。

Java 的動態就體現在反射。通過反射我們可以實現動態裝配,降低代碼的耦合度;動態代理等。反射的過度使用會嚴重消耗系統資源

2.反射的使用場景、作用及優缺點?

使用場景
在編譯時無法知道該對象或類可能屬于哪些類,程序在運行時獲取對象和類的信息

作用
通過反射可以使程序代碼訪問裝載到 JVM 中的類的內部信息,獲取已裝載類的屬性信息、方法信息

優點
提高了 Java 程序的靈活性和擴展性,降低耦合性,提高自適應能力。
允許程序創建和控制任何類的對象,無需提前硬編碼目標類
應用很廣,測試工具、框架都用到了反射

缺點
性能問題:反射是一種解釋操作,遠慢于直接代碼。因此反射機制主要用在對靈活性和擴展性要求很高的系統框架上,普通程序不建議使用
模糊程序內部邏輯:反射繞過了源代碼,無法再源代碼中看到程序的邏輯,會帶來維護問題
增大了復雜性:反射代碼比同等功能的直接代碼更復雜

3. 動態代理是什么?有哪些應用?

動態代理:當想要給實現了某個接口的類中的方法,加一些額外的處理。比如說加日志,加事務等。可以給這個類創建一個代理,故名思議就是創建一個新的類,這個類不僅包含原來類方法的功能,而且還在原來的基礎上添加了額外處理的新類。這個代理類并不是定義好的,是動態生成的。具有解耦意義,靈活,擴展性強。

動態代理實現:首先必須定義一個接口,還要有一個InvocationHandler(將實現接口的類的對象傳遞給它)處理類。再有一個工具類Proxy(習慣性將其稱為代理類,因為調用他的newInstance()可以產生代理對象,其實他只是一個產生代理對象的工具類)。利用到InvocationHandler,拼接代理類源碼,將其編譯生成代理類的二進制碼,利用加載器加載,并將其實例化產生代理對象,最后返回。

動態代理的應用:Spring的AOP,加事務,加權限,加日志。


五、對象拷貝

1. 為什么要使用克隆?

想對一個對象進行處理,又想保留原有的數據進行接下來的操作,就需要克隆了,Java語言中克隆針對的是類的實例。

  • 方法需要 return 引用類型,但又不希望自己持有引用類型的對象被修改。
  • 程序之間方法的調用時參數的傳遞。有些場景為了保證引用類型的參數不被其他方法修改,可以使用克隆后的值作為參數傳遞。

2. 深拷貝和淺拷貝區別是什么?

  • 淺拷貝只是復制了對象的引用地址,兩個對象指向同一個內存地址,所以修改其中任意的值,另一個值都會隨之變化,這就是淺拷貝

  • 深拷貝是將對象及值復制過來,兩個對象修改其中任意的值另一個值不會改變,這就是深拷貝(例:JSON.parse()和JSON.stringify(),但是此方法無法復制函數類型)

3. 什么是 java 序列化?什么情況下需要序列化?

簡單說就是為了保存在內存中的各種對象的狀態(也就是實例變量,不是方法),并且可以把保存的對象狀態再讀出來。雖然你可以用你自己的各種各樣的方法來保存object states,但是Java給你提供一種應該比你自己好的保存對象狀態的機制,那就是序列化。

Java 序列化是指把 Java 對象轉換為字節序列的過程;
Java 反序列化是指把字節序列恢復為 Java 對象的過程;

什么情況下需要序列化:

  • 當你想把的內存中的對象狀態保存到一個文件中或者數據庫中時候;
  • 當你想用套接字在網絡上傳送對象的時候;

4. 如何實現對象克隆?

有兩種方式:

  • 實現Cloneable接口并重寫Object類中的clone()方法,不實現 Cloneable 接口,會報 CloneNotSupportedException 異常;
public class TestClone {
 
    public static void main(String[] args) throws CloneNotSupportedException {
        Person p1 = new Person(1, "ConstXiong");//創建對象 Person p1
        Person p2 = (Person)p1.clone();//克隆對象 p1
        p2.setName("其不答");//修改 p2的name屬性,p1的name未變
        System.out.println(p1);
        System.out.println(p2);
    }
    
}
 

class Person implements Cloneable {
    
    private int pid;
    
    private String name;
    
    public Person(int pid, String name) {
        this.pid = pid;
        this.name = name;
        System.out.println("Person constructor call");
    }
 
    public int getPid() {
        return pid;
    }
 
    public void setPid(int pid) {
        this.pid = pid;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
 
    @Override
    public String toString() {
        return "Person [pid:"+pid+", name:"+name+"]";
    }
    
}

打印結果
Person constructor call
Person [pid:1, name:ConstXiong]
Person [pid:1, name:其不答]
  • Object 的 clone() 方法是淺拷貝,即如果類中屬性有自定義引用類型,只拷貝引用,不拷貝引用指向的對象,使用clone方法也是可以的,但是會很麻煩。這時我們可以用序列化的方式來實現對象的深克隆。實現Serializable接口,通過對象的序列化和反序列化實現克隆,可以實現真正的深度克隆。
//對象的屬性的Class 也實現 Cloneable 接口,在克隆對象時也手動克隆屬性
public class TestManalDeepClone {
 
    public static void main(String[] args) throws Exception {
        DPerson p1 = new DPerson(1, "ConstXiong", new DFood("米飯"));//創建Person 對象 p1
        DPerson p2 = (DPerson)p1.clone();//克隆p1
        p2.setName("其不答");//修改p2的name屬性
        p2.getFood().setName("面條");//修改p2的自定義引用類型 food 屬性
        System.out.println(p1);//修改p2的自定義引用類型 food 屬性被改變,p1的自定義引用類型 food 屬性也隨之改變,說明p2的food屬性,只拷貝了引用,沒有拷貝food對象
        System.out.println(p2);
    }
    
}
 
class DPerson implements Cloneable {
    
    private int pid;
    
    private String name;
    
    private DFood food;
    
    public DPerson(int pid, String name, DFood food) {
        this.pid = pid;
        this.name = name;
        this.food = food;
        System.out.println("Person constructor call");
    }
 
    public int getPid() {
        return pid;
    }
 
    public void setPid(int pid) {
        this.pid = pid;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    @Override
    public Object clone() throws CloneNotSupportedException {
        DPerson p = (DPerson)super.clone();
        p.setFood((DFood)p.getFood().clone());
        return p;
    }
 
    @Override
    public String toString() {
        return "Person [pid:"+pid+", name:"+name+", food:"+food.getName()+"]";
    }
 
    public DFood getFood() {
        return food;
    }
 
    public void setFood(DFood food) {
        this.food = food;
    }
    
}
 
class DFood implements Cloneable{
    
    private String name;
    
    public DFood(String name) {
        this.name = name;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
    
}

打印結果:
Person constructor call
Person [pid:1, name:ConstXiong, food:米飯]
Person [pid:1, name:其不答, food:面條]
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
 //結合 java.io.Serializable 接口,完成深拷貝
public class TestSeriazableClone {
 
    public static void main(String[] args) {
        SPerson p1 = new SPerson(1, "ConstXiong", new SFood("米飯"));//創建 SPerson 對象 p1
        SPerson p2 = (SPerson)p1.cloneBySerializable();//克隆 p1
        p2.setName("其不答");//修改 p2 的 name 屬性
        p2.getFood().setName("面條");//修改 p2 的自定義引用類型 food 屬性
        System.out.println(p1);//修改 p2 的自定義引用類型 food 屬性被改變,p1的自定義引用類型 food 屬性未隨之改變,說明p2的food屬性,只拷貝了引用和 food 對象
        System.out.println(p2);
    }
    
}
 
class SPerson implements Cloneable, Serializable {
    
    private static final long serialVersionUID = -7710144514831611031L;
 
    private int pid;
    
    private String name;
    
    private SFood food;
    
    public SPerson(int pid, String name, SFood food) {
        this.pid = pid;
        this.name = name;
        this.food = food;
        System.out.println("Person constructor call");
    }
 
    public int getPid() {
        return pid;
    }
 
    public void setPid(int pid) {
        this.pid = pid;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    /**
     * 通過序列化完成克隆
     * @return
     */
    public Object cloneBySerializable() {
        Object obj = null;
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            oos.writeObject(this);
            ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bais);
            obj = ois.readObject();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return obj;
    }
 
    @Override
    public String toString() {
        return "Person [pid:"+pid+", name:"+name+", food:"+food.getName()+"]";
    }
 
    public SFood getFood() {
        return food;
    }
 
    public void setFood(SFood food) {
        this.food = food;
    }
    
}
 
class SFood implements Serializable {
    
    private static final long serialVersionUID = -3443815804346831432L;
    
    private String name;
    
    public SFood(String name) {
        this.name = name;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
    
}
打印結果
Person constructor call
Person [pid:1, name:ConstXiong, food:米飯]
Person [pid:1, name:其不答, food:面條]

注意:基于序列化和反序列化實現的克隆不僅僅是深度克隆,更重要的是通過泛型限定,可以檢查出要克隆的對象是否支持序列化,這項檢查是編譯器完成的,不是在運行時拋出異常,這種是方案明顯優于使用Object類的clone方法克隆對象。讓問題在編譯的時候暴露出來總是優于把問題留到運行時。

Java中的深克隆和淺克隆的原理及三種方式實現深克隆
Java提高篇——對象克隆
深拷貝、淺拷貝和clone、new方法效率對比


六、異常

1. JAVA 異常類型結構?

Error 和 Exeption

Error

Error 描述了 JAVA 程序運行時系統的內部錯誤,通常比較嚴重,除了通知用戶和盡力使應用程序安全地終止之外,無能為力,應用程序不應該嘗試去捕獲這種異常。通常為一些虛擬機異常,如 StackOverflowError 等。

Exception

Exception 類型下面又分為兩個分支,一個分支派生自 RuntimeException,這種異常通常為程序錯誤導致的異常;另一個分支為非派生自 RuntimeException 的異常,這種異常通常是程序本身沒有問題,由于像 I/O 錯誤等問題導致的異常,每個異常類用逗號隔開。

受查異常和非受查異常

受查異常

受查異常會在編譯時被檢測。如果一個方法中的代碼會拋出受查異常,則該方法必須包含異常處理,即 try-catch 代碼塊,或在方法簽名中用 throws 關鍵字聲明該方法可能會拋出的受查異常,否則編譯無法通過。如果一個方法可能拋出多個受查異常類型,就必須在方法的簽名處列出所有的異常類。

非受查異常

非受查異常不會在編譯時被檢測。JAVA 中 Error 和 RuntimeException 類的子類屬于非受查異常,除此之外繼承自 Exception 的類型為受查異常。

2. throw 和 throws 的區別?

throws是用來聲明一個方法可能拋出的所有異常信息,throws是將異常聲明但是不處理,而是將異常往上傳,誰調用我就交給誰處理。而throw則是指拋出的一個具體的異常類型。

3. final、finally、finalize 有什么區別?

  • final可以修飾類、變量、方法,修飾類表示該類不能被繼承、修飾方法表示該方法不能被重寫、修飾變量表示該變量是一個常量不能被重新賦值。

  • finally一般作用在try-catch代碼塊中,在處理異常的時候,通常我們將一定要執行的代碼方法放在finally代碼塊中,表示不管是否出現異常,該代碼塊都會執行,一般用來存放一些關閉資源的代碼。

  • finalize是一個方法,屬于Object類的一個方法,而Object類是所有類的父類,該方法一般由垃圾回收器來調用,當我們調用System的gc()方法的時候,由垃圾回收器調用finalize(),回收垃圾。

4. try-catch-finally 中哪個部分可以省略?

答:
以下三種情況都是可以的:
try-catch
try-finally
try-catch-finally
可以省略catch或者finally。catch和finally不可以同時省略。

5. try-catch-finally 中,如果 catch 中 return 了,finally 還會執行嗎?

答:會執行,在 return 前執行。
代碼示例1:

/*
 * java面試題--如果catch里面有return語句,finally里面的代碼還會執行嗎?
 */
public class FinallyDemo2 {
    public static void main(String[] args) {
        System.out.println(getInt());
    }
 
    public static int getInt() {
        int a = 10;
        try {
            System.out.println(a / 0);
            a = 20;
        } catch (ArithmeticException e) {
            a = 30;
            return a;
            /*
             * return a 在程序執行到這一步的時候,這里不是return a 而是 return 30;這個返回路徑就形成了
             * 但是呢,它發現后面還有finally,所以繼續執行finally的內容,a=40
             * 再次回到以前的路徑,繼續走return 30,形成返回路徑之后,這里的a就不是a變量了,而是常量30
             */
        } finally {
            a = 40;
        }
 
//      return a;
    }
}

執行結果:30

package com.java_02;
 
/*
 * java面試題--如果catch里面有return語句,finally里面的代碼還會執行嗎?
 */
public class FinallyDemo2 {
    public static void main(String[] args) {
        System.out.println(getInt());
    }
 
    public static int getInt() {
        int a = 10;
        try {
            System.out.println(a / 0);
            a = 20;
        } catch (ArithmeticException e) {
            a = 30;
            return a;
            /*
             * return a 在程序執行到這一步的時候,這里不是return a 而是 return 30;這個返回路徑就形成了
             * 但是呢,它發現后面還有finally,所以繼續執行finally的內容,a=40
             * 再次回到以前的路徑,繼續走return 30,形成返回路徑之后,這里的a就不是a變量了,而是常量30
             */
        } finally {
            a = 40;
            return a; //如果這樣,就又重新形成了一條返回路徑,由于只能通過1個return返回,所以這里直接返回40
        }
 
//      return a;
    }
}

執行結果:40

6.NoClassDefFoundError 和 ClassNotFoundException 區別?

NoClassDefFoundError 是一個 Error 類型的異常,是由 JVM 引起的,不應該嘗試捕獲這個異常。

引起該異常的原因是 JVM 或 ClassLoader 嘗試加載某類時在內存中找不到該類的定義,該動作發生在運行期間,即編譯時該類存在,但是在運行時卻找不到了,可能是編譯后被刪除了等原因導致;

ClassNotFoundException 是一個受查異常,需要顯式地使用 try-catch 對其進行捕獲和處理,或在方法簽名中用 throws 關鍵字進行聲明。當使用 Class.forName, ClassLoader.loadClass 或 ClassLoader.findSystemClass 動態加載類到內存的時候,通過傳入的類路徑參數沒有找到該類,就會拋出該異常;另一種拋出該異常的可能原因是某個類已經由一個類加載器加載至內存中,另一個加載器又嘗試去加載它。

7. JVM 是如何處理異常的?

在一個方法中如果發生異常,這個方法會創建一個異常對象,并轉交給 JVM,該異常對象包含異常名稱,異常描述以及異常發生時應用程序的狀態。創建異常對象并轉交給 JVM 的過程稱為拋出異常。可能有一系列的方法調用,最終才進入拋出異常的方法,這一系列方法調用的有序列表叫做調用棧。

JVM 會順著調用棧去查找看是否有可以處理異常的代碼,如果有,則調用異常處理代碼。當 JVM 發現可以處理異常的代碼時,會把發生的異常傳遞給它。如果 JVM 沒有找到可以處理該異常的代碼塊,JVM 就會將該異常轉交給默認的異常處理器(默認處理器為 JVM 的一部分),默認異常處理器打印出異常信息并終止應用程序。

8. 常見的異常類有哪些?

  • NullPointerException:當應用程序試圖訪問空對象時,則拋出該異常。
  • SQLException:提供關于數據庫訪問錯誤或其他錯誤信息的異常。
  • IndexOutOfBoundsException:指示某排序索引(例如對數組、字符串或向量的排序)超出范圍時拋出。
  • NumberFormatException:當應用程序試圖將字符串轉換成一種數值類型,但該字符串不能轉換為適當格式時,拋出該異常。
  • FileNotFoundException:當試圖打開指定路徑名表示的文件失敗時,拋出此異常。
  • IOException:當發生某種I/O異常時,拋出此異常。此類是失敗或中斷的I/O操作生成的異常的通用類。
  • ClassCastException:當試圖將對象強制轉換為不是實例的子類時,拋出該異常。
  • ArrayStoreException:試圖將錯誤類型的對象存儲到一個對象數組時拋出的異常。
  • IllegalArgumentException:拋出的異常表明向方法傳遞了一個不合法或不正確的參數。
  • ArithmeticException:當出現異常的運算條件時,拋出此異常。例如,一個整數“除以零”時,拋出此類的一個實例。
  • NegativeArraySizeException:如果應用程序試圖創建大小為負的數組,則拋出該異常。
  • NoSuchMethodException:無法找到某一特定方法時,拋出該異常。
  • SecurityException:由安全管理器拋出的異常,指示存在安全侵犯。
  • UnsupportedOperationException:當不支持請求的操作時,拋出該異常。
  • RuntimeException:是那些可能在Java虛擬機正常運行期間拋出的異常的超類(父類)。

阿里巴巴異常處理規約,純建議

1、【強制】 Java 類庫中定義的可以通過預檢查方式規避的 RuntimeException 異常不應該通過catch 的方式來處理,比如: NullPointerException, IndexOutOfBoundsException 等等。

說明:無法通過預檢查的異常除外,比如,在解析字符串形式的數字時,不得不通過 catch NumberFormatException 來實現。

正例:if (obj != null) {…}

反例:try { obj.method(); } catch (NullPointerException e) {…}

2、【強制】 異常不要用來做流程控制,條件控制。

說明: 異常設計的初衷是解決程序運行中的各種意外情況,且異常的處理效率比條件判斷方式要低很多

3、【強制】 catch 時請分清穩定代碼和非穩定代碼,穩定代碼指的是無論如何不會出錯的代碼。對于非穩定代碼的 catch 盡可能進行區分異常類型,再做對應的異常處理。

說明: 對大段代碼進行 try-catch,使程序無法根據不同的異常做出正確的應激反應,也不利于定位問題,這是一種不負責任的表現。
正例: 用戶注冊的場景中,如果用戶輸入非法字符, 或用戶名稱已存在, 或用戶輸入密碼過于簡單,在程序上作出分門別類的判斷,并提示給用戶。

4、【強制】 捕獲異常是為了處理它,不要捕獲了卻什么都不處理而拋棄之,如果不想處理它,請將該異常拋給它的調用者。最外層的業務使用者,必須處理異常,將其轉化為用戶可以理解的內容。

5、【強制】 有 try 塊放到了事務代碼中, catch 異常后,如果需要回滾事務,一定要注意手動回滾事務。

6、【強制】 finally 塊必須對資源對象、流對象進行關閉,有異常也要做 try-catch。

說明: 如果 JDK7 及以上,可以使用 try-with-resources 方式。

7、【強制】 不要在 finally 塊中使用 return。

說明: finally 塊中的 return 返回后方法結束執行,不會再執行 try 塊中的 return 語句。

8、【強制】 捕獲異常與拋異常,必須是完全匹配,或者捕獲異常是拋異常的父類。

說明: 如果預期對方拋的是繡球,實際接到的是鉛球,就會產生意外情況。

9、【推薦】 方法的返回值可以為 null,不強制返回空集合,或者空對象等,必須添加注釋充分說明什么情況下會返回 null 值。

說明: 本手冊明確防止 NPE 是調用者的責任。即使被調用方法返回空集合或者空對象,對調用者來說,也并非高枕無憂,必須考慮到遠程調用失敗、 序列化失敗、 運行時異常等場景返回null 的情況。

10、【推薦】 防止 NPE,是程序員的基本修養,注意 NPE 產生的場景:

1)返回類型為基本數據類型, return 包裝數據類型的對象時,自動拆箱有可能產生 NPE。
反例: public int f() { return Integer 對象}, 如果為 null,自動解箱拋 NPE。
2) 數據庫的查詢結果可能為 null。
3) 集合里的元素即使 isNotEmpty,取出的數據元素也可能為 null。
4) 遠程調用返回對象時,一律要求進行空指針判斷,防止 NPE。
5) 對于 Session 中獲取的數據,建議 NPE 檢查,避免空指針。
6) 級聯調用 obj.getA().getB().getC(); 一連串調用,易產生 NPE。

正例: 使用 JDK8 的 Optional 類來防止 NPE 問題。

11、【推薦】 定義時區分 unchecked / checked 異常,避免直接拋出 new RuntimeException(),更不允許拋出 Exception 或者 Throwable,應使用有業務含義的自定義異常。

推薦業界已定義過的自定義異常,如: DAOException / ServiceException 等。

12、【參考】 對于公司外的 http/api 開放接口必須使用“錯誤碼”; 而應用內部推薦異常拋出;跨應用間 RPC 調用優先考慮使用 Result 方式,封裝 isSuccess()方法、 “錯誤碼”、 “錯誤簡短信息”。

說明: 關于 RPC 方法返回方式使用 Result 方式的理由:
1) 使用拋異常返回方式,調用方如果沒有捕獲到就會產生運行時錯誤。
2) 如果不加棧信息,只是 new 自定義異常,加入自己的理解的 error message,對于調用端解決問題的幫助不會太多。如果加了棧信息,在頻繁調用出錯的情況下,數據序列化和傳輸的性能損耗也是問題。

13、【參考】 避免出現重復的代碼(Don’t Repeat Yourself) ,即 DRY 原則。

說明: 隨意復制和粘貼代碼,必然會導致代碼的重復,在以后需要修改時,需要修改所有的副本,容易遺漏。必要時抽取共性方法,或者抽象公共類,甚至是組件化。

正例: 一個類中有多個 public 方法,都需要進行數行相同的參數校驗操作,這個時候請抽取:private boolean checkParam(DTO dto) {…}


七、設計模式

1. 說一下你熟悉的設計模式?

參考:常用的設計模式匯總,超詳細!


八、網絡編程

計算機網絡基礎知識總結

java網絡編程面試題

歡迎下方評論留言,文章持續更新優化

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。