吐血整理!2萬字Java基礎面試題(帶答案)請收好!

後端精進之路發表於2023-02-27

熬夜整理了這麼多年來的Java基礎面試題,歡迎學習收藏,手機上可以點選這裡,效果更佳https://mp.weixin.qq.com/s/ncbEQqQdJo0UaogQSgA0bQ

1.1 Hashmap 與 concurrentHashMap (重點)

  1. hashMap 1.7、8 put過程

d669d29c.png

  1. concurrentHashMap 1.8 put過程

Screen Shot 2019-12-10 at 8.56.23 PM.png
Screen Shot 2019-12-10 at 9.17.20 PM.png@w=400

  1. 怎麼解決衝突的(連結串列或者紅黑樹)

  2. sizeCtl (concurrentHashMap 1.8 中的資料結構)等關鍵成員變數的作用:

Node:存放實際的key和value值。
sizeCtl:負數:表示進行初始化或者擴容,-1表示正在初始化,-N,表示有N-1個執行緒正在進行擴容
正數:0 表示還沒有被初始化,>0的數,初始化或者是下一次進行擴容的閾值。
TreeNode:用在紅黑樹,表示樹的節點, TreeBin是實際放在table陣列中的,代表了這個紅黑樹的根。

  1. concurrentHashmap 1.8為什麼放棄了分段鎖 (鎖的粒度更小,減小併發衝突機率)

  2. HashMap的時間複雜度?

HashMap容器O(1)的查詢時間複雜度只是其理想的狀態,而這種理想狀態需要由java設計者去保證。

jdk1.7中的hashMap在最壞情況下,退化成連結串列後,get/put時間複雜度均為O(n);jdk1.8中,採用紅黑樹,複雜度可以到O(logN);如果hash函式設計的較好,元素均勻分佈,可以達到理想的O(1)複雜度。

  1. Java8中的HashMap有什麼變化?

1). 資料結構不同:jdk7 陣列+單連結串列; jdk8 陣列+(單連結串列+紅黑樹)

JDK7 在hashcode特別差的情況下,比方說所有key的hashcode都相同,這個連結串列可能會很長,那麼put/get操作都可能需要遍歷這個連結串列。也就是說時間複雜度在最差情況下會退化到O(n)。

JDK8 如果同一個格子裡的key不超過8個,使用連結串列結構儲存。如果超過了8個,那麼會呼叫treeifyBin函式,將連結串列轉換為紅黑樹。那麼即使hashcode完全相同,由於紅黑樹的特點,查詢某個特定元素,也只需要O(log n)的開銷。也就是說put/get的操作的時間複雜度最差只有O(log n)。

2). 連結串列中元素位置不同:jdk7頭插法;jdk8 連結串列尾插。

頭插: 最近put的可能等下就被get,頭插遍歷到連結串列頭就匹配到了,併發resize可能產生迴圈鏈
尾插:保證了元素的順序,併發resize過程中可能發生資料丟失的情況。

3). 擴容的處理不同:jdk7中使用hash和newCapacity計算元素在新陣列中的位置;jdk8中利用新增的高位是否為1,來確定新元素的位置,因此元素要麼在原位置,要麼在原位置+擴容的大小值。

jkd7中,擴容時,直接判斷每個元素在新陣列中的位置,然後依次複製到新陣列;
jdk8中,擴容時,首先建立兩個連結串列high和low,然後根據新增的高位是否為0,將元素放到對應的連結串列後面。最後將對應的連結串列放在原位置或者原位置+擴容大小值的位置.

  1. 紅黑樹需要比較大小才能進行插入,是依據什麼進行比較的?

從下圖可以看到,是根據hash大小來確定左右子樹的位置的。

        final TreeNode<K,V> putTreeVal(int h, K k, V v) {
            Class<?> kc = null;
            boolean searched = false;
            for (TreeNode<K,V> p = root;;) {
                int dir, ph; K pk;
                if (p == null) {
                    first = root = new TreeNode<K,V>(h, k, v, null, null);
                    break;
                }
                else if ((ph = p.hash) > h)
                    dir = -1;
                else if (ph < h)
                    dir = 1;
                else if ((pk = p.key) == k || (pk != null && k.equals(pk)))
                    return p;
                else if ((kc == null &&
                          (kc = comparableClassFor(k)) == null) ||
                         (dir = compareComparables(kc, k, pk)) == 0) {
                    if (!searched) {
                        TreeNode<K,V> q, ch;
                        searched = true;
                        if (((ch = p.left) != null &&
                             (q = ch.findTreeNode(h, k, kc)) != null) ||
                            ((ch = p.right) != null &&
                             (q = ch.findTreeNode(h, k, kc)) != null))
                            return q;
                    }
                    dir = tieBreakOrder(k, pk);
                }

                TreeNode<K,V> xp = p;
                if ((p = (dir <= 0) ? p.left : p.right) == null) {
                    TreeNode<K,V> x, f = first;
                    first = x = new TreeNode<K,V>(h, k, v, f, xp);
                    if (f != null)
                        f.prev = x;
                    if (dir <= 0)
                        xp.left = x;
                    else
                        xp.right = x;
                    if (!xp.red)
                        x.red = true;
                    else {
                        lockRoot();
                        try {
                            root = balanceInsertion(root, x);
                        } finally {
                            unlockRoot();
                        }
                    }
                    break;
                }
            }
            assert checkInvariants(root);
            return null;
        }
  1. 其他Hash衝突解決方式?

開放定址法(線性探測法,平方探測法,雙雜湊)和再雜湊(選擇新的雜湊函式,在另外一個大約兩倍大的表,而且使用一個相關的新雜湊函式,掃描整個原始雜湊表,計算每個(未刪除的)元素的新雜湊值並將其插入到新表中。)

  1. HashMap為什麼不是執行緒安全的?怎麼讓HashMap變得執行緒安全?(加鎖)

1.7 hashmap 併發resize成環;1.8併發resize丟失資料。

  1. jdk1.8對ConcurrentHashMap做了哪些最佳化?

1、取消了segment陣列,引入了Node結構,直接用Node陣列來儲存資料,鎖的粒度更小,減少併發衝突的機率。
2、儲存資料時採用了連結串列+紅黑樹的形式,純連結串列的形式時間複雜度為O(n),紅黑樹則為O(logn),效能提升很大。什麼時候連結串列轉紅黑樹?當key值相等的元素形成的連結串列中元素個數超過8個的時候。

  1. 怎麼高效率的實現資料遷移?

jdk1.8中,resize資料要麼在原位置,要麼在原位置加上resize大小的位置。
concurrentHashMap在put或者remove操作時,發現正在進行擴容,會首先幫助擴容。

  1. resize過程

1.7 hashmap:新建new table,根據hash值計算在新table中的位置,依次移動到新table
1.8 hashmap:新建table,從舊table中複製元素,利用high和low來儲存不同位置的元素。

  1. 為什麼都是2的N次冪的大小。

1) 從上面的分析JDK8 resize的過程可以可能到,陣列長度保持2的次冪,當resize的時候,為了透過h&(length-1)計算新的元素位置,可以看到當擴容後只有一位差異,也就是多出了最左位的1,這樣計算 h&(length-1)的時候,只要h對應的最左邊的那一個差異位為0,就能保證得到的新的陣列索引和老陣列索引一致,否則index+OldCap。

1024555-20161115215812138-679881037.png

2) 陣列長度保持2的次冪,length-1的低位都為1,會使得獲得的陣列索引index更加均勻。hash函式採用各種位運算也是為了使得低位更加雜湊,如果低位全部為1,那麼對於h低位部分來說,任何一位的變化都會對結果產生影響,可以儘可能的使元素分佈比較均勻。

1024555-20161116001404732-625340289.png

  1. HashMap,HashTable比較
  • HashMap允許將 null 作為一個 entry 的 key 或者 value,而 Hashtable 不允許。
  • HashTable 繼承自 Dictionary 類,而 HashMap 是 Java1.2 引進的 Map interface 的一個實現。
  • HashTable 的方法是 Synchronized 的,而 HashMap 不是,在多個執行緒訪問 Hashtable 時,不需要自己為它的方法實現同步,而 HashMap 就必須為之提供外同步。
  1. 極高併發下HashTable和ConcurrentHashMap哪個效能更好,為什麼,如何實現的。

ConcurrentHashMap。後者鎖粒度更低,前者直接對put、get方法加鎖。

  1. HashMap和HashSet的實現原理

HashSet的實現很簡單,內部有一個HashMap的成員變數,所有的Set相關的操作都轉換為了對HashMap的操作。

1.2 集合相關問題

  1. LinkedHashMap、ArrayList、LinkedList、Vector的底層實現。

LinkedHashMap:

public class LinkedHashMap<K,V>
    extends HashMap<K,V>
    implements Map<K,V>
{
    ...
}

可以看到,LinkedHashMap是HashMap的子類,但和HashMap的無序性不一樣,LinkedHashMap透過維護一個執行於所有條目的雙向連結串列,保證了元素迭代的順序。該迭代順序可以是插入順序或者是訪問順序,這個可以在初始化的時候確定,預設採用插入順序來維持取出鍵值對的次序。

在成員變數上,與HashMap不同的是,引入了before和after兩個變數來記錄前後的元素。

test.png

在建構函式中,需要指定accessOrder,有兩種情況:

false,所有的Entry按照插入的順序排列
true,所有的Entry按照訪問的順序排列

ArrayList、LinkedList、Vector、stack的底層實現:

從圖中我們可以看出:

  1. List是一個介面,它繼承與Collection介面,代表有序的佇列。

  2. AbstractList是一個抽象類,它繼承與AbstractCollection。AbstractList實現了List介面中除了size()、get(int location)之外的方法。

  3. AbstractSequentialList是一個抽象類,它繼承與AbstrctList。AbstractSequentialList實現了“連結串列中,根據index索引值操作連結串列的全部方法”。

  4. ArrayList、LinkedList、Vector和Stack是List的四個實現類,其中Vector是基於JDK1.0,雖然實現了同步(大部分方法),但是效率低,已經不用了,Stack繼承於Vector。

  5. LinkedList是個雙向連結串列,它同樣可以被當作棧、佇列或雙端佇列來使用。

  1. TreeMap以及查詢複雜度

TreeMap繼承於AbstractMap,實現了Map, Cloneable, NavigableMap, Serializable介面。

Screen Shot 2019-12-08 at 3.56.55 PM.png@w=400

TreeMap 是一個有序的key-value集合,它是透過紅黑樹實現的。該對映根據其鍵的自然順序進行排序,或者根據建立對映時提供的Comparator進行排序,具體取決於使用的構造方法。
TreeMap的基本操作 containsKey、get、put 和 remove 的時間複雜度是 log(n) 。

對於SortedMap來說,該類是TreeMap體系中的父介面,也是區別於HashMap體系最關鍵的一個介面。SortedMap介面中定義的第一個方法Comparator<? super K> comparator();該方法決定了TreeMap體系的走向,有了比較器,就可以對插入的元素進行排序了。

TreeMap的查詢、插入、更新元素等操作,主要是對紅黑樹的節點進行相應的更新,和資料結構中類似。

1.3 Java 泛型的理解

Java 泛型(generics)是 JDK 5 中引入的一個新特性, 泛型提供了編譯時型別安全檢測機制,該機制允許程式設計師在編譯時檢測到非法的型別。

泛型的本質是引數化型別,也就是說所操作的資料型別被指定為一個引數。

泛型的好處:

  1. 使程式的通用性更好,支援不同的型別;
  2. 編譯器無法進行型別檢查,可以向集合中新增任意型別的物件。取值時型別轉換失敗導致程式執行失敗。泛型的好處在於提高了程式的可讀性和安全性,這也是程式設計的宗旨之一。

1.4 跳錶(ConcurrentSkipListMap)的查詢過程是怎麼樣的,查詢和插入的時間複雜度?

ConcurrentSkipListMap是一個併發安全, 基於skiplist實現有序儲存的Map。可以看成是TreeMap的併發版本。

下面的圖示使用紫色的箭頭畫出了在一個SkipList中查詢key值50的過程。簡述如下:

  1. 從head出發,因為head指向最頂層(top level)連結串列的開始節點,相當於從頂層開始查詢;

  2. 移動到當前節點的右指標(right)指向的節點,直到右節點的key值大於要查詢的key值時停止;

  3. 如果還有更低層次的連結串列,則移動到當前節點的下一層節點(down),如果已經處於最底層,則退出;

  4. 重複第2步 和 第3步,直到查詢到key值所在的節點,或者不存在而退出查詢;

查詢複雜度:O(logN)

  • 為什麼Redis選擇使用跳錶而不是紅黑樹來實現有序集合?

Redis 中的有序集合(zset) 支援的操作:

插入一個元素
刪除一個元素
查詢一個元素
有序輸出所有元素
按照範圍區間查詢元素(比如查詢值在 [100, 356] 之間的資料)

其中,前四個操作紅黑樹也可以完成,且時間複雜度跟跳錶是一樣的。但是,按照區間來查詢資料這個操作,紅黑樹的效率沒有跳錶高。按照區間查詢資料時,跳錶可以做到 O(logn) 的時間複雜度定位區間的起點,然後在原始連結串列中順序往後遍歷就可以了,非常高效。

1.5 java 位元組流 字元流

Java中的流是對位元組序列的抽象,我們可以想象有一個水管,只不過現在流動在水管中的不再是水,而是位元組序列。和水流一樣,Java中的流也具有一個“流動的方向”,通常可以從中讀入一個位元組序列的物件被稱為輸入流;能夠向其寫入一個位元組序列的物件被稱為輸出流。

Java中的位元組流(Byte)處理的最基本單位為單個位元組,它通常用來處理二進位制資料。Java中最基本的兩個位元組流類是InputStream和OutputStream,它們分別代表了組基本的輸入位元組流和輸出位元組流。InputStream類與OutputStream類均為抽象類,我們在實際使用中通常使用Java類庫中提供的它們的一系列子類。

public abstract int read() throws IOException;

Java中的字元流(Char)處理的最基本的單元是Unicode碼元(大小2位元組),它通常用來處理文字資料。所謂Unicode碼元,也就是一個Unicode程式碼單元,範圍是0x0000~0xFFFF。在以上範圍內的每個數字都與一個字元相對應,Java中的String型別預設就把字元以Unicode規則編碼而後儲存在記憶體中。然而與儲存在記憶體中不同,儲存在磁碟上的資料通常有著各種各樣的編碼方式。使用不同的編碼方式,相同的字元會有不同的二進位制表示。實際上字元流是這樣工作的:

  • 輸出字元流:把要寫入檔案的字元序列(實際上是Unicode碼元序列)轉為指定編碼方式下的位元組序列,然後再寫入到檔案中;
  • 輸入字元流:把要讀取的位元組序列按指定編碼方式解碼為相應字元序列(實際上是Unicode碼元序列從)從而可以存在記憶體中。

字元流與位元組流的區別

  • 位元組流操作的基本單元為位元組;字元流操作的基本單元為Unicode碼元。
  • 位元組流預設不使用緩衝區;字元流使用緩衝區。
  • 位元組流通常用於處理二進位制資料,實際上它可以處理任意型別的資料,但它不支援直接寫入或讀取Unicode碼元;字元流通常處理文字資料,它支援寫入及讀取Unicode碼元。

1.8 包裝型別和基本型別比較問題

例如,Integer型別的變數能否==int型別變數,能否作比較

可以。 但是不同包裝型別直接進行比較,不會發生自動拆箱,比較的是兩個物件是否為同一個。

自動裝箱和拆箱實現了基本資料型別與物件資料型別之間的隱式轉換。

自動裝箱就是Java自動將原始型別值轉換成對應的物件,比如將int的變數轉換成Integer物件,這個過程叫做裝箱,反之將Integer物件轉換成int型別值,這個過程叫做拆箱。因為這裡的裝箱和拆箱是自動進行的非人為轉換,所以就稱作為自動裝箱和拆箱。原始型別byte,short,char,int,long,float,double和boolean對應的封裝類為Byte,Short,Character,Integer,Long,Float,Double,Boolean。

何時發生自動裝箱和拆箱,

  1. 賦值:
Integer iObject = 3; //autobxing - primitive to wrapper conversion
int iPrimitive = iObject; //unboxing - object to primitive conversion
  1. 方法呼叫:當我們在方法呼叫時,我們可以傳入原始資料值或者物件,同樣編譯器會幫我們進行轉換。
public static Integer show(Integer iParam){
   System.out.println("autoboxing example - method invocation i: " + iParam);
   return iParam;
}

//autoboxing and unboxing in method invocation
show(3); //autoboxing
int result = show(3); //unboxing because return type of method is Integer

自動裝箱的弊端,

自動裝箱有一個問題,那就是在一個迴圈中進行自動裝箱操作的時候,如下面的例子就會建立多餘的物件,影響程式的效能。

Integer sum = 0;
 for(int i=1000; i<5000; i++){
   sum+=i;
}

自動裝箱與比較:

下面程式的輸出結果是什麼?

public class Main {
    public static void main(String[] args) {
         
        Integer a = 1;
        Integer b = 2;
        Integer c = 3;
        Integer d = 3;
        Integer e = 321;
        Integer f = 321;
        Long g = 3L;
        Long h = 2L;
         
        System.out.println(c==d);
        System.out.println(e==f);
        System.out.println(c==(a+b));
        System.out.println(c.equals(a+b));
        System.out.println(g==(a+b));
        System.out.println(g.equals(a+b));
        System.out.println(g.equals(a+h));
    }
}

在解釋具體的結果時,首先必須明白如下兩點:

  • 當"=="運算子的兩個運算元都是 包裝器型別的引用,則是比較指向的是否是同一個物件,而如果其中有一個運算元是表示式(即包含算術運算)則比較的是數值(即會觸發自動拆箱的過程)。
  • 對於包裝器型別,equals方法並不會進行型別轉換。

下面是程式的具體輸出結果:

true
false
true
true
true
false
true

注意到對於Integer和Long,Java中,會對-128到127的物件進行快取,當建立新的物件時,如果符合這個這個範圍,並且已有存在的相同值的物件,則返回這個物件,否則建立新的Integer物件。

對於上面的結果:

cd:指向相同的快取物件,所以返回true;
e
f:不存在快取,是不同的物件,所以返回false;
c(a+b):比較數值,因此為true;
c.equals(a+b):比較的物件,由於存在快取,所以兩個物件一樣,返回true;
g
(a+b):直接比較數值,因此為true;
g.equals(a+b):比較物件,由於equals也不會進行型別轉換,a+b為Integer,g為Long,因此為false;
g.equals(a+h):和上面不一樣,a+h時,a會進行型別轉換,轉成Long,接著比較兩個物件,由於Long存在快取,所以兩個物件一致,返回true。

關於equals和==:

  • .equals(...) will only compare what it is written to compare, no more, no less.
  • If a class does not override the equals method, then it defaults to the equals(Object o) method of the closest parent class that has overridden this method.
  • If no parent classes have provided an override, then it defaults to the method from the ultimate parent class, Object, and so you're left with the Object#equals(Object o) method. Per the Object API this is the same as ==; that is, it returns true if and only if both variables refer to the same object, if their references are one and the same. Thus you will be testing for object equality and not functional equality.
  • Always remember to override hashCode if you override equals so as not to "break the contract". As per the API, the result returned from the hashCode() method for two objects must be the same if their equals methods show that they are equivalent. The converse is not necessarily true.

1.9 為什麼重寫equals和hashcode

以下是Effective Java的建議:

You must override hashCode() in every class that overrides equals(). Failure to do so will result in a violation of the general contract for Object.hashCode(), which will prevent your class from functioning properly in conjunction with all hash-based collections, including HashMap, HashSet, and Hashtable.

主要是考慮到在map等需要hashcode的場合,保證程式執行正常。

由於Object是所有類的基類,預設的equals函式如下,直接比較兩個物件是否為同一個。

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

預設的hashcode方法是一個native函式,

public native int hashCode();

It just means that the method is implemented in the native aka C/C++ parts of the JVM. This means you can't find Java source code for that method. But there is still some code somewhere within the JVM that gets invoked whenever you call hashCode() on some Object.

從Object中關於hashCode方法的描述,對於不同的Object,hashCode會返回不同的值;但是實現可能與物件的地址有關,也有可能無關,具體看JVM實現。

1.10 stringBuilder和stringBuffer的區別

String 是 Java 語言非常基礎和重要的類,提供了構造和管理字串的各種基本邏輯。它是典型的 Immutable 類,被宣告成為 final class,所有屬性也都是 final 的。也由於它的不可變性,類似拼接、裁剪字串等動作,都會產生新的 String 物件。由於字串操作的普遍性,所以相關操作的效率往往對應用效能有明顯影響。

StringBuffer 是為解決上面提到拼接產生太多中間物件的問題而提供的一個類,我們可以用 append 或者 add 方法,把字串新增到已有序列的末尾或者指定位置。StringBuffer 本質是一個執行緒安全的可修改字元序列,它保證了執行緒安全,也隨之帶來了額外的效能開銷,所以除非有執行緒安全的需要,不然還是推薦使用它的後繼者,也就是 StringBuilder。

StringBuilder 是 Java 1.5 中新增的,在能力上和 StringBuffer 沒有本質區別,但是它去掉了執行緒安全的部分,有效減小了開銷,是絕大部分情況下進行字串拼接的首選。

1.11 Java序列化的原理

序列化: 物件序列化的最主要的用處就是在傳遞和儲存物件的時候,保證物件的完整性和可傳遞性。序列化是把物件轉換成有序位元組流,以便在網路上傳輸或者儲存在本地檔案中。序列化後的位元組流儲存了Java物件的狀態以及相關的描述資訊。序列化機制的核心作用就是物件狀態的儲存與重建。

反序列化: 客戶端從檔案中或網路上獲得序列化後的物件位元組流後,根據位元組流中所儲存的物件狀態及描述資訊,透過反序列化重建物件。

序列化演算法一般會按步驟做如下事情:

  1. 將物件例項相關的類後設資料輸出。
  2. 遞迴地輸出類的超類描述直到不再有超類。
  3. 類後設資料完了以後,開始從最頂層的超類開始輸出物件例項的實際資料值。
  4. 從上至下遞迴輸出例項的資料

JDK中序列化一個物件:

  1. 建立某些OutputStream物件
  2. 將其封裝在一個ObjectOutputStream物件內
  3. 只需呼叫writeObject()即可將物件序列化

反序列化
將一個InputStream封裝在ObjectInputStream內,然後呼叫readObject()。最後獲得的是一個引用,它指向一個向上轉型的Object,所以必須向下轉型才能直接設定它們。

序列化的控制——透過實現Externalizable介面——代替實現Serializable介面——來對序列化過程進行控制。

  1. Externalizable介面繼承了Serializable介面,增加了兩個方法,writeExternal()和readExternal(),這兩個方法會在序列化和反序列化還原的過程中被自動呼叫。
  2. Externalizable物件,在還原的時候所有普通的預設構造器都會被呼叫(包括在欄位定義時的初始化)(只有這樣才能使Externalizable物件產生正確的行為),然後呼叫readExternal().
  3. 如果我們從一個Externalizable物件繼承,通常需要呼叫基類版本的writeExternal()和readExternal()來為基類元件提供恰當的儲存和恢復功能。
  4. 為了正常執行,我們不僅需要在writeExternal()方法中將來自物件的重要資訊寫入,還必須在readExternal()中恢復資料

防止物件的敏感部分被序列化,兩種方式:

  1. 將類實現Externalizable,在writeExternal()內部只對所需部分進行顯示的序列化
  2. 實現Serializable,用transient(瞬時)關鍵字(只能和Serializable一起使用)逐個欄位的關閉序列化,他的意思:不用麻煩你儲存或恢復資料——我自己會處理。

Externalizable的替代方法

  1. 實現Serializable介面,並新增名為writeObject()和readObject()的方法,這樣一旦物件被序列化或者被反序列化還原,就會自動的分別呼叫writeObject()和readObject()的方法(它們不是介面的一部分,介面的所有東西都是public的)。只要提供這兩個方法,就會使用它們而不是預設的序列化機制。
  2. 這兩個方法必須具有準確的方法特徵簽名,但是這兩個方法並不在這個類中的其他方法中呼叫,而是在ObjectOutputStream和ObjectInputStream物件的writeObject()和readObject()方法

1.11 Java8、9、10、11的一些新特性介紹

java8

  1. lambada表示式(Lambda Expressions)。Lambda允許把函式作為一個方法的引數(函式作為引數傳遞進方法中)。
  2. 方法引用(Method references)。方法引用提供了非常有用的語法,可以直接引用已有Java類或物件(例項)的方法或構造器。與lambda聯合使用,可以使語言的構造更緊湊簡潔,減少冗餘程式碼。
  3. 預設方法(Default methods)。預設方法允許將新功能新增到庫的介面中,並確保相容實現老版本介面的舊有程式碼。
  4. 重複註解(Repeating Annotations)。重複註解提供了在同一宣告或型別中多次應用相同註解型別的能力。
  5. 型別註解(Type Annotation)。在任何地方都能使用註解,而不是在宣告的地方。
  6. 型別推斷增強。
  7. 方法引數反射(Method Parameter Reflection)。
  8. Stream API 。新新增的Stream API(java.util.stream) 把真正的函數語言程式設計風格引入到Java中。Stream API整合到了Collections API裡。
  9. HashMap改進,在鍵值雜湊衝突時能有更好表現。
  10. Date Time API。加強對日期和時間的處理。
  11. java.util 包下的改進,提供了幾個實用的工具類。
  • 並行陣列排序。
  • 標準的Base64編解碼。
  • 支援無符號運算。
  1. java.util.concurrent 包下增加了新的類和方法。
  • java.util.concurrent.ConcurrentHashMap 類新增了新的方法以支援新的StreamApi和lambada表示式。
  • java.util.concurrent.atomic 包下新增了類以支援可伸縮可更新的變數。
  • java.util.concurrent.ForkJoinPool類新增了方法以支援 common pool。
  • 新增了java.util.concurrent.locks.StampedLock類,為控制讀/寫訪問提供了一個基於效能的鎖,且有三種模式可供選擇。
  1. HotSpot
  • 刪除了 永久代(PermGen).
  • 方法呼叫的位元組碼指令支援預設方法。

java9

  1. java模組系統 (Java Platform Module System)。
  2. 新的版本號格式。$MAJOR.$MINOR.$SECURITY.$PATCH
  3. java shell,互動式命令列控制檯。
  4. 在private instance methods方法上可以使用@SafeVarargs註解。
  5. diamond語法與匿名內部類結合使用。
  6. 下劃線_不能單獨作為變數名使用。
  7. 支援私有介面方法(您可以使用diamond語法與匿名內部類結合使用)。
  8. Javadoc
  • 簡化Doclet API。
  • 支援生成HTML5格式。
  • 加入了搜尋框,使用這個搜尋框可以查詢程式元素、標記的單詞和文件中的短語。
  • 支援新的模組系統。
  1. JVM
  • 增強了Garbage-First(G1) 並用它替代Parallel GC成為預設的垃圾收集器。
  • 統一了JVM 日誌,為所有元件引入了同一個日誌系統。
  • 刪除了JDK 8中棄用的GC組合。(DefNew + CMS,ParNew + SerialOld,Incremental CMS)。
  1. properties檔案支援UTF-8編碼,之前只支援ISO-8859-1。
  2. 支援Unicode 8.0,在JDK8中是Unicode 6.2。

java10

  1. 區域性變數型別推斷(Local-Variable Type Inference)
//之前的程式碼格式
URL url = new URL("http://www.oracle.com/"); 
URLConnection conn = url.openConnection(); 
Reader reader = new BufferedReader(
    new InputStreamReader(conn.getInputStream()))
//java10中用var來宣告變數
var url = new URL("http://www.oracle.com/"); 
var conn = url.openConnection(); 
var reader = new BufferedReader(
    new InputStreamReader(conn.getInputStream()));

var是一個保留型別名稱,而不是關鍵字。所以之前使用var作為變數、方法名、包名的都沒問題,但是如果作為類或介面名,那麼這個類和介面就必須重新命名了。

var的使用場景主要有以下四種:

  • 本地變數初始化。
  • 增強for迴圈中。
  • 傳統for迴圈中宣告的索引變數。
  • Try-with-resources 變數。
  1. Optional類新增了新的方法orElseThrow(無引數版)。相比於已經存在的get方法,這個方法更推薦使用。

java11

  1. 支援Unicode 10.0,在jdk10中是8.0。
  2. 標準化HTTP Client
  3. 編譯器執行緒的延遲分配。新增了新的命令-XX:+UseDynamicNumberOfCompilerThreads動態控制編譯器執行緒的數量。
  4. 新的垃圾收集器—ZGC。一種可伸縮的低延遲垃圾收集器(實驗性)。
  5. Epsilon。一款新的實驗性無操作垃圾收集器。Epsilon GC 只負責記憶體分配,不實現任何記憶體回收機制。這對於效能測試非常有用,可用於與其他GC對比成本和收益。
  6. Lambda引數的區域性變數語法。java10中引入的var欄位得到了增強,現在可以用在lambda表示式的宣告中。如果lambda表示式的其中一個形式引數使用了var,那所有的引數都必須使用var。

參考:https://www.jianshu.com/p/38985b61ea83

1.12 java中四種修飾符的限制範圍。

1.13 Object類中的方法。

  1. Object():預設構造方法
  2. clone():建立並返回此物件的一個副本,這是淺複製。
  3. equals():指示某個其他物件是否與此物件相等
  4. finalize():當垃圾回收器確定不存在對該物件的更多引用時,由物件的垃圾回收器呼叫此方法。JVM準備對此對物件所佔用的記憶體空間進行垃圾回收前,將被呼叫。
  5. getClass():返回一個物件的執行時類物件。

首先解釋下"類物件"的概念:在Java中,類是是對具有一組相同特徵或行為的例項的抽象並進行描述,物件則是此類所描述的特徵或行為的具體例項。作為概念層次的類,其本身也具有某些共同的特性,如都具有類名稱、由類載入器去載入,都具有包,具有父類,屬性和方法等。於是,Java中有專門定義了一個類,Class,去描述其他類所具有的這些特性,因此,從此角度去看,類本身也都是屬於Class類的物件。為與經常意義上的物件相區分,在此稱之為"類物件"。

  1. hashCode():返回該物件的雜湊值
  2. notify():喚醒此物件監視器上等待的單個執行緒
  3. notifyAll():喚醒此物件監視器上等待的所有執行緒
  4. toString():返回該物件的字串表示
  5. wait():導致當前的執行緒等待,直到其它執行緒呼叫此物件的notify()或notifyAll()
  6. wait(long timeout):導致當前的執行緒等待呼叫此物件的notify()或notifyAll()
  7. wait(long timeout, int nanos):導致當前的執行緒等待,直到其他執行緒呼叫此物件的notify()或notifyAll(),或其他某個執行緒中斷當前執行緒,或者已超過某個實際時間量。
  8. registerNatives():對本地方法進行註冊

1.14 淺複製 深複製

在 Java 中,除了基本資料型別(元型別)之外,還存在 類的例項物件 這個引用資料型別。而一般使用 『 = 』號做賦值操作的時候。對於基本資料型別,實際上是複製的它的值,但是對於物件而言,其實賦值的只是這個物件的引用,將原物件的引用傳遞過去,他們實際上還是指向的同一個物件。

而淺複製和深複製就是在這個基礎之上做的區分,如果在複製這個物件的時候,只對基本資料型別進行了複製,而對引用資料型別只是進行了引用的傳遞,而沒有真實的建立一個新的物件,則認為是淺複製。反之,在對引用資料型別進行複製的時候,建立了一個新的物件,並且複製其內的成員變數,則認為是深複製。

1、淺複製:對基本資料型別進行值傳遞,對引用資料型別進行引用傳遞般的複製,此為淺複製。(預設的clone函式)

2、深複製:對基本資料型別進行值傳遞,對引用資料型別,建立一個新的物件,並複製其內容,此為深複製。(序列化物件或者重寫clone函式)

1.15 介面和抽象類的區別,注意JDK8的介面可以有實現。

介面和抽象類是 Java 物件導向設計的兩個基礎機制。

介面是對行為的抽象,它是抽象方法的集合,利用介面可以達到 API 定義和實現分離的目的。介面,不能例項化;不能包含任何非常量成員,任何 field 都是隱含著 public static final 的意義 Java 標準類庫中,定義了非常多的介面,比如 java.util.List。

Java 8 以後,介面也是可以有方法實現的。 從 Java 8 開始,interface 增加了對 default method 的支援。Java 9 以後,甚至可以定義 private default method。Default method 提供了一種二進位制相容的擴充套件已有介面的辦法。在 Java 8 中新增了一系列 default method,主要是增加 Lambda(forEach)、Stream 相關的功能。

抽象類是不能例項化的類,用 abstract 關鍵字修飾 class,其目的主要是程式碼重用。除了不能例項化,形式上和一般的 Java 類並沒有太大區別,可以有一個或者多個抽象方法,也可以沒有抽象方法。抽象類大多用於抽取相關 Java 類的共用方法實現或者是共同成員變數,然後透過繼承的方式達到程式碼複用的目的。 Java 標準庫中,比如 collection 框架,很多通用部分就被抽取成為抽象類,例如 java.util.AbstractList。

Java 類實現 interface 使用 implements 關鍵詞,繼承 abstract class 則是使用 extends 關鍵詞,可以參考 Java 標準庫中的 ArrayList。

1.16 動態代理的兩種方式,以及區別。

1. JDK動態代理

1、因為利用JDKProxy生成的代理類實現了介面,所以目標類中所有的方法在代理類中都有。
2、生成的代理類的所有的方法都攔截了目標類的所有的方法。而攔截器中invoke方法的內容正好就是代理類的各個方法的組成體。
3、利用JDKProxy方式必須有介面的存在。
4、invoke方法中的三個引數可以訪問目標類的被呼叫方法的API、被呼叫方法的引數、被呼叫方法的返回型別。

2. cglib動態代理

1、 CGlib是一個強大的,高效能,高質量的Code生成類庫。它可以在執行期擴充套件Java類與實現Java介面。
2、 用CGlib生成代理類是目標類的子類。
3、 用CGlib生成 代理類不需要介面
4、 用CGLib生成的代理類重寫了父類的各個方法。
5、 攔截器中的intercept方法內容正好就是代理類中的方法體

JDK動態代理和cglib動態代理有什麼區別?

  • JDK動態代理只能對實現了介面的類生成代理物件;
  • cglib可以對任意類生成代理物件,它的原理是對目標物件進行繼承代理,如果目標物件被final修飾,那麼該類無法被cglib代理。

Spring框架的一大特點就是AOP,SpringAOP的本質就是動態代理,那麼Spring到底使用的是JDK代理,還是cglib代理呢?

答:混合使用。如果被代理物件實現了介面,就優先使用JDK代理,如果沒有實現介面,就用用cglib代理。

1.16 傳值和傳引用的區別,Java是怎麼樣的,有沒有傳值引用。

在Java程式中,不區分傳值呼叫和傳引用,總是採用傳值呼叫

注意以下幾種情況:

  • 一個方法不能修改一個基本資料型別的引數(即數值型和布林型);
  • 一個方法可以改變一個物件引數的狀態;
  • 一個方法不能讓物件引數引用一個新的物件。

值傳遞與引用傳遞的區別:一個方法可以修改傳遞引用所對應的變數值,而不能修改傳遞值呼叫所對應的變數值,這句話相當重要,這是按值呼叫與引用呼叫的根本區別。

就算是包裝型別也不行,修改之後生成新的變數,改變了形參的值,但是實參的值不會改變。

由於String類和包裝類都被設定成不可變的,沒有提供value對應的setter方法,而且很多都是final的,我們無法改變其內容,所以導致我們看起來好像是值傳遞。

在Java下實現swap函式可以透過反射實現,或者使用陣列。

1.17 一個ArrayList在迴圈過程中刪除,會不會出問題,為什麼。

會有問題,不過需要分情況討論。

所有可能的刪除方法如下,

public class ArrayListTest {
    public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<String>();
        list.add("aa");
        list.add("bb");
        list.add("bb");
        list.add("aa");
        list.add("cc");
        // 刪除元素 bb
        remove(list, "bb");
        for (String str : list) {
            System.out.println(str);
        }
    }
    public static void remove(ArrayList<String> list, String elem) {
        // 五種不同的迴圈及刪除方法
        // 方法一:普通for迴圈正序刪除,刪除過程中元素向左移動,不能刪除重複的元素
//        for (int i = 0; i < list.size(); i++) {
//            if (list.get(i).equals(elem)) {
//                list.remove(list.get(i));
//            }
//        }
        // 方法二:普通for迴圈倒序刪除,刪除過程中元素向左移動,可以刪除重複的元素
//        for (int i = list.size() - 1; i >= 0; i--) {
//            if (list.get(i).equals(elem)) {
//                list.remove(list.get(i));
//            }
//        }
        // 方法三:增強for迴圈刪除,使用ArrayList的remove()方法刪除,產生併發修改異常 ConcurrentModificationException
//        for (String str : list) {
//            if (str.equals(elem)) {
//                list.remove(str);
//            }
//        }
        // 方法四:迭代器,使用ArrayList的remove()方法刪除,產生併發修改異常 ConcurrentModificationException
//        Iterator iterator = list.iterator();
//        while (iterator.hasNext()) {
//            if(iterator.next().equals(elem)) {
//                list.remove(iterator.next());
//            }
//        }

        // 方法五:迭代器,使用迭代器的remove()方法刪除,可以刪除重複的元素,但不推薦
//        Iterator iterator = list.iterator();
//        while (iterator.hasNext()) {
//            if(iterator.next().equals(elem)) {
//                iterator.remove();
//            }
//        }
    }
}

針對上述結果總結如下:

  1. 普通for迴圈刪除,無論正向或者反向,不會丟擲異常。但是由於刪除過程中,list整體左移,所以正向刪除無法刪除連續的重複元素。
    public E remove(int index) {
        rangeCheck(index);

        modCount++;
        E oldValue = elementData(index);

        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

    public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }

    /*
     * Private remove method that skips bounds checking and does not
     * return the value removed.
     */
    private void fastRemove(int index) {
        modCount++;
        int numMoved = size - index - 1;
        if (numMoved > 0)
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work
    }
  1. 使用增強的for迴圈或者迭代器,只要是呼叫list本身的remove函式,由於在remove中會修改list內部的modCount,導致expectedModCount!=modCount,當呼叫迭代器的next函式時,首先會檢查兩個計數是否相等,由於不相等,因此發生異常。
        public E next() {
            checkForComodification();
            int i = cursor;
            if (i >= size)
                throw new NoSuchElementException();
            Object[] elementData = ArrayList.this.elementData;
            if (i >= elementData.length)
                throw new ConcurrentModificationException();
            cursor = i + 1;
            return (E) elementData[lastRet = i];
        }
        
        final void checkForComodification() {
            if (modCount != expectedModCount)
                throw new ConcurrentModificationException();
        }
  1. 如果使用迭代器並呼叫迭代器的remove方法來刪除元素,由於迭代器的remove函式中對兩個計數進行了同步,所以不會出現異常。
        public void remove() {
            if (lastRet < 0)
                throw new IllegalStateException();
            checkForComodification();

            try {
                ArrayList.this.remove(lastRet);
                cursor = lastRet;
                lastRet = -1;
                expectedModCount = modCount;
            } catch (IndexOutOfBoundsException ex) {
                throw new ConcurrentModificationException();
            }
        }

1.18 Exception和Error區別

Exception 和 Error 都是繼承了 Throwable 類,在 Java 中只有 Throwable 型別的例項才可以被丟擲(throw)或者捕獲(catch),它是異常處理機制的基本組成型別。Exception 和 Error 體現了 Java 平臺設計者對不同異常情況的分類。

Exception 是程式正常執行中,可以預料的意外情況,可能並且應該被捕獲,進行相應處理。Exception 又分為可檢查(checked)異常和不檢查(unchecked)異常,可檢查異常在原始碼裡必須顯式地進行捕獲處理,這是編譯期檢查的一部分。不檢查異常就是所謂的執行時異常,類似 NullPointerException、ArrayIndexOutOfBoundsException 之類,通常是可以編碼避免的邏輯錯誤,具體根據需要來判斷是否需要捕獲,並不會在編譯期強制要求。

Error 是指在正常情況下,不大可能出現的情況,絕大部分的 Error 都會導致程式(比如 JVM 自身)處於非正常的、不可恢復狀態。既然是非正常情況,所以不便於也不需要捕獲,常見的比如 OutOfMemoryError 之類,都是 Error 的子類。

1.19 new關鍵字和newinstance()方法

它們的區別在於建立物件的方式不一樣,前者newInstance()是使用類載入機制,後者new關鍵字是建立一個新類。那麼為什麼會有兩種建立物件方式?這主要考慮到軟體的可伸縮、可擴充套件和可重用等軟體設計思想。

1、類的載入方式不同

在執行Class.forName("a.class.Name")時,JVM會在classapth中去找對應的類並載入,這時JVM會執行該類的靜態程式碼段。在使用newInstance()方法的時候,必須保證這個類已經載入並且已經連結了,而這可以透過Class的靜態方法forName()來完成的。

使用關鍵字new建立一個類的時候,這個類可以沒有被載入,一般也不需要該類在classpath中設定,但可能需要透過classlaoder來載入。

2、所呼叫的構造方法不盡相同

new關鍵字能呼叫任何構造方法。
newInstance()只能呼叫無參構造方法。

3、執行效率不同

new關鍵字是強型別的,效率相對較高。
newInstance()是弱型別的,效率相對較低。

既然使用newInstance()構造物件的地方透過new關鍵字也可以建立物件,為什麼又會使用newInstance()來建立物件呢?

假設定義了一個介面Door,開始的時候是用木門的,定義為一個類WoodenDoor,在程式裡就要這樣寫 Door door = new WoodenDoor() 。假設後來生活條件提高,換為自動門了,定義一個類AutoDoor,這時程式就要改寫為 Door door = new AutoDoor() 。雖然只是改個識別符號,如果這樣的語句特別多,改動還是挺大的。於是出現了工廠模式,所有Door的例項都由DoorFactory提供,這時換一種門的時候,只需要把工廠的生產模式改一下,還是要改一點程式碼。

而如果使用newInstance(),則可以在不改變程式碼的情況下,換為另外一種Door。具體方法是把Door的具體實現類的類名放到配置檔案中,透過newInstance()生成例項。這樣,改變另外一種Door的時候,只改配置檔案就可以了。示例程式碼如下:
String className = 從配置檔案讀取Door的具體實現類的類名;
Door door = (Door) Class.forName(className).newInstance();

再配合依賴注入的方法,就提高了軟體的可伸縮性、可擴充套件性。

https://blog.csdn.net/luckykapok918/article/details/50186797

1.20 Map、List、Set 分別說下你知道的執行緒安全類和執行緒不安全的類

MAP:

不安全:hashmap、treeMap、LinkedHashMap
安全:concurrentHashMap、ConcurrentSkipListMap、或者說hashtable

List:

不安全:ArrayList、linkedlist
安全:Vector、SynchronizedList(將list轉為執行緒安全,全部上鎖)、CopyOnWriteArrayList(讀讀不上鎖、寫上鎖ReentrantLock、寫完直接替換)

Set:

不安全:hashset、treeSet、LinkedHashSet
安全:ConcurrentSkipListSet、CopyOnWriteArraySet、synchronizedSet

1.21 Java防止SQL隱碼攻擊

  1. PreparedStatement:採用預編譯語句集,它內建了處理SQL隱碼攻擊的能力,只要使用它的setXXX方法傳值即可。PreparedStatement已經準備好了,執行階段只是把輸入串作為資料處理,
    而不再對sql語句進行解析,準備,因此也就避免了sql注入問題。
  2. 使用正規表示式過濾傳入的引數
  3. 字串過濾

1.22 反射原理及使用場景

反射 (Reflection) 是 Java 的特徵之一,它允許執行中的 Java 程式獲取自身的資訊,並且可以操作類或物件的內部屬性。

簡而言之,透過反射,我們可以在執行時獲得程式或程式集中每一個型別的成員和成員的資訊。程式中一般的物件的型別都是在編譯期就確定下來的,而 Java 反射機制可以動態地建立物件並呼叫其屬性,這樣的物件的型別在編譯期是未知的。所以我們可以透過反射機制直接建立物件,即使這個物件的型別在編譯期是未知的。

反射的核心是 JVM 在執行時才動態載入類或呼叫方法/訪問屬性,它不需要事先(寫程式碼的時候或編譯期)知道執行物件是誰。

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

  • 在執行時判斷任意一個物件所屬的類;
  • 在執行時構造任意一個類的物件;
  • 在執行時判斷任意一個類所具有的成員變數和方法(透過反射甚至可以呼叫private方法);
  • 在執行時呼叫任意一個物件的方法。

重點:是執行時而不是編譯時。

1 主要用途

反射最重要的用途就是開發各種通用框架。 很多框架(比如 Spring)都是配置化的(比如透過 XML 檔案配置 Bean),為了保證框架的通用性,它們可能需要根據配置檔案載入不同的物件或類,呼叫不同的方法,這個時候就必須用到反射,執行時動態載入需要載入的物件。

2 基本使用

1. 獲得Class物件

方法有三種:

(1) 使用 Class 類的 forName 靜態方法:

public static Class<?> forName(String className)

比如在 JDBC 開發中常用此方法載入資料庫驅動,Class.forName(driver);

(2) 直接獲取某一個物件的 class,比如:

Class<?> klass = int.class;
Class<?> classInt = Integer.TYPE;

(3) 呼叫某個物件的 getClass() 方法,比如:

StringBuilder str = new StringBuilder("123");
Class<?> klass = str.getClass();

2. 判斷是否為某個類的例項

一般地,我們用 instanceof 關鍵字來判斷是否為某個類的例項。同時我們也可以藉助反射中 Class 物件的 isInstance() 方法來判斷是否為某個類的例項,它是一個 native 方法:

public native boolean isInstance(Object obj);

3. 建立例項

透過反射來生成物件主要有兩種方式。

  • 使用Class物件的newInstance()方法來建立Class物件對應類的例項。
Class<?> c = String.class;
Object str = c.newInstance();
  • 先透過Class物件獲取指定的Constructor物件,再呼叫Constructor物件的newInstance()方法來建立例項。這種方法可以用指定的構造器構造類的例項。
//獲取String所對應的Class物件
Class<?> c = String.class;
//獲取String類帶一個String引數的構造器
Constructor constructor = c.getConstructor(String.class);
//根據構造器建立例項
Object obj = constructor.newInstance("23333");
System.out.println(obj);

4. 獲取方法

獲取某個Class物件的方法集合,主要有以下幾個方法:

  • getDeclaredMethods 方法返回類或介面宣告的所有方法,包括公共、保護、預設(包)訪問和私有方法,但不包括繼承的方法。
public Method[] getDeclaredMethods() throws SecurityException
  • getMethods 方法返回某個類的所有公用(public)方法,包括其繼承類的公用方法。
public Method[] getMethods() throws SecurityException
  • getMethod 方法返回一個特定的方法,其中第一個引數為方法名稱,後面的引數為方法的引數對應Class的物件。
public Method getMethod(String name, Class<?>... parameterTypes)

5. 獲取構造器資訊

獲取類構造器的用法與上述獲取方法的用法類似。主要是透過Class類的getConstructor方法得到Constructor類的一個例項,而Constructor類有一個newInstance方法可以建立一個物件例項:

public T newInstance(Object ... initargs)

此方法可以根據傳入的引數來呼叫對應的Constructor建立物件例項。

6、獲取類的成員變數(欄位)資訊

主要是這幾個方法,在此不再贅述:

  • getFiled:訪問公有的成員變數
  • getDeclaredField:所有已宣告的成員變數,但不能得到其父類的成員變數

注:可以透過method.setAccessible(true)和field.setAccessible(true)來關閉安全檢查來提升反射速度。

7. 呼叫方法

當我們從類中獲取了一個方法後,我們就可以用 invoke() 方法來呼叫這個方法。invoke 方法的原型為:

public Object invoke(Object obj, Object... args)
        throws IllegalAccessException, IllegalArgumentException,
           InvocationTargetException

下面是例子:

public class test1 {
    public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        Class<?> klass = methodClass.class;
        //建立methodClass的例項
        Object obj = klass.newInstance();
        //獲取methodClass類的add方法
        Method method = klass.getMethod("add",int.class,int.class);
        //呼叫method對應的方法 => add(1,4)
        Object result = method.invoke(obj,1,4);
        System.out.println(result);
    }
}
class methodClass {
    public final int fuck = 3;
    public int add(int a,int b) {
        return a+b;
    }
    public int sub(int a,int b) {
        return a+b;
    }
}

8. 利用反射建立陣列

陣列在Java裡是比較特殊的一種型別,它可以賦值給一個Object Reference。下面我們看一看利用反射建立陣列的例子:

public static void testArray() throws ClassNotFoundException {
        Class<?> cls = Class.forName("java.lang.String");
        Object array = Array.newInstance(cls,25);
        //往陣列裡新增內容
        Array.set(array,0,"hello");
        Array.set(array,1,"Java");
        Array.set(array,2,"fuck");
        Array.set(array,3,"Scala");
        Array.set(array,4,"Clojure");
        //獲取某一項的內容
        System.out.println(Array.get(array,3));
    }

其中的Array類為java.lang.reflect.Array類。我們透過Array.newInstance()建立陣列物件,它的原型是:

public static Object newInstance(Class<?> componentType, int length)
        throws NegativeArraySizeException {
        return newArray(componentType, length);
    }

3 注意事項

由於反射會額外消耗一定的系統資源,因此如果不需要動態地建立一個物件,那麼就不需要用反射。

另外,反射呼叫方法時可以忽略許可權檢查,因此可能會破壞封裝性而導致安全問題。

1.23 static Vs Final ? 如何讓類不能被繼承

Static :被static修飾的成員變數屬於類,不屬於這個類的某個物件。

final意味著”不可改變的”,一般應用於資料、方法和類。

  • final資料:當資料是基本型別時,意味著這是一個永不改變的編譯時常量。

  • final方法:一般我們使用final方法的目的就是防止子類對該類方法的覆蓋或修改。

  • final類:一般我們使用final類的目的就是說明我們不打算用任何類繼承該類,即不希望該類有子類。

如何讓類不被繼承:用final修飾這個類,或者將建構函式宣告為私有。

1.24 記憶體洩露?記憶體溢位?

記憶體溢位:是指程式在申請記憶體時,沒有足夠的記憶體空間供其使用,出現OutOfMemoryError。

產生該錯誤的原因主要包括:

  • JVM記憶體過小。
  • 程式不嚴密,產生了過多的垃圾。

記憶體洩露:Memory Leak,是指程式在申請記憶體後,無法釋放已申請的記憶體空間,一次記憶體洩露危害可以忽略,但記憶體洩露堆積後果很嚴重,無論多少記憶體,遲早會被佔光。

在Java中,記憶體洩漏就是存在一些被分配的物件,這些物件有下面兩個特點:
1)首先,這些物件是可達的,即在有向圖中,存在通路可以與其相連;
2)其次,這些物件是無用的,即程式以後不會再使用這些物件。

兩者的聯絡:

記憶體洩露會最終會導致記憶體溢位。

相同點:都會導致應用程式執行出現問題,效能下降或掛起。
不同點:

  1. 記憶體洩露是導致記憶體溢位的原因之一,記憶體洩露積累起來將導致記憶體溢位。
  2. 記憶體洩露可以透過完善程式碼來避免,記憶體溢位可以透過調整配置來減少發生頻率,但無法徹底避免。

1.25 重寫Vs過載

  • 重寫是子類對父類的允許訪問的方法的實現過程進行重新編寫, 返回值和形參都不能改變。即外殼不變,核心重寫!

  • 過載(overloading) 是在一個類裡面,方法名字相同,而引數不同。返回型別可以相同也可以不同。每個過載的方法(或者建構函式)都必須有一個獨一無二的引數型別列表。

在 Java 中過載是由靜態型別確定的,在類載入的時候即可確定,屬於靜態分派;而重寫是由動態型別確定,是在執行時確定的,屬於動態分派,動態分派是由虛方法表實現的,虛方法表中存在著各個方法的實際入口地址,如若父類中某個方法子類沒有重寫,則父類與子類的方法表中的方法地址相同,如若重寫了,則子類方法表的地址指向重寫後的地址。

1.26 Lambda表示式實現

這樣就完成的實現了Lambda表示式,使用invokedynamic指令,執行時呼叫LambdaMetafactory.metafactory動態的生成內部類,實現了介面,內部類裡的呼叫方法塊並不是動態生成的,只是在原class裡已經編譯生成了一個靜態的方法,內部類只需要呼叫該靜態方法。

1.27 ClassNotFoundException和NoClassDefFoundError的區別

  1. ClassNotFoundException是一個檢查異常。從類繼承層次上來看,ClassNotFoundException是從Exception繼承的,所以ClassNotFoundException是一個檢查異常。

當應用程式執行的過程中嘗試使用類載入器去載入Class檔案的時候,如果沒有在classpath中查詢到指定的類,就會丟擲ClassNotFoundException。一般情況下,當我們使用Class.forName()或者ClassLoader.loadClass以及使用ClassLoader.findSystemClass()在執行時載入類的時候,如果類沒有被找到,那麼就會導致JVM丟擲ClassNotFoundException。

  1. NoClassDefFoundError異常,看命名字尾是一個Error。從類繼承層次上看,NoClassDefFoundError是從Error繼承的。和ClassNotFoundException相比,明顯的一個區別是,NoClassDefFoundError並不需要應用程式去關心catch的問題。

當JVM在載入一個類的時候,如果這個類在編譯時是可用的,但是在執行時找不到這個類的定義的時候,JVM就會丟擲一個NoClassDefFoundError錯誤。 比如當我們在new一個類的例項的時候,如果在執行是類找不到,則會丟擲一個NoClassDefFoundError的錯誤。

歡迎關注【後端精進之路】,硬貨文章一手掌握~

相關文章