每日十道面試題(十一)

1.請問 ArrayList、HashSet、HashMap 是線程安全的嗎?如果不是怎麼獲取線程安全的集合?

通過以上類的源碼進行分析,每個方法都沒有加鎖,顯然都是非線程安全的。在集合中Vector 和HashTable是線程安全的。打開源碼會發現其實就是把各自核心方法添加上了synchronized 關鍵字。Collections工具類提供了相關的 API,可以讓上面那3個不安全的集合變爲安全的。

Collections.synchronizedCollection(c);
Collections.synchronizedList(list);
Collections.synchronizedMap(m);
Collections.synchronizedSet(s);

list還有CopyOnArrayList,hashmap還有ConcurrentHashMap

2.ArrayList內部用什麼實現的?

回答這樣的問題,不要只回答個皮毛,可以再介紹一下ArrayList內部是如何實現數組的增加和刪除的,因爲數組在創建的時候長度是固定的,那麼就有個問題我們往ArrayList中不斷的添加對象,它是如何管理這些數組呢?通過源碼可以看到ArrayList內部是用Object[]實現的。接下來我們分別分析ArrayList的構造以及add()、remove()、clear()方法的實現原理。

public ArrayList(){
    array=EmptyArray.OBJECT;
}

array 是一個 Object[]類型。當我們 new 一個空參構造時系統調用了 EmptyArray.OBJECT 屬性,EmptyArray 僅僅是一個系統的類庫,該類源碼如下:

public final class EmptyArray {
    private EmptyArray() {
    }
    public static final boolean[] BOOLEAN = new boolean[0];
    public static final byte[] BYTE = new byte[0];
    public static final char[] CHAR = new char[0];
    public static final double[] DOUBLE = new double[0];
    public static final int[] INT = new int[0];
    public static final Class<?>[] CLASS = new Class[0];
    public static final Object[] OBJECT = new Object[0];
    public static final String[] STRING = new String[0];
    public static final Throwable[] THROWABLE = new Throwable[0];
    public static final StackTraceElement[] STACK_TRACE_ELEMENT = new StackTraceElement[0];
}

也就是說當我們 new 一個空參 ArrayList 的時候,系統內部使用了一個 new Object[0]數組。

  • 帶容量參數的構造器
public ArrayList(int capacity) {
    if (capacity < 0) {
        throw new IllegalArgumentException("capacity < 0: " + capacity);
    }
    array = (capacity == 0 ? EmptyArray.OBJECT : new Object[capacity]);
}

該構造函數傳入一個 int 值,該值作爲數組的長度值。如果該值小於 0,則拋出一個運行時異常。如果等於 0,則使用一個空數組,如果大於 0,則創建一個長度爲該值的新數組。

  • 帶集合參數的構造器
public ArrayList(Collection<? extends E> collection) {
    if (collection == null) {
        throw new NullPointerException("collection == null");
    }

    Object[] a = collection.toArray();
    if (a.getClass() != Object[].class) {
        Object[] newArray = new Object[a.length];
        System.arraycopy(a, 0, newArray, 0, a.length);
        a = newArray;
    }
    array = a;
    size = a.length;
}

如果調用構造函數的時候傳入了一個 Collection 的子類,那麼先判斷該集合是否爲 null,爲 null 則拋出空指針異常。如果不是則將該集合轉換爲數組 a,然後將該數組賦值爲成員變量 array,將該數組的長度作爲成員變量 size。

-● add方法

@Override
public boolean add(E object) {
    Object[] a = array;
    int s = size;
    if (s == a.length) {
        Object[] newArray = new Object[s +
                (s < (MIN_CAPACITY_INCREMENT / 2) ? MIN_CAPACITY_INCREMENT : s >> 1)];
        System.arraycopy(a, 0, newArray, 0, s);
        array = a = newArray;
    }
    a[s] = object;
    size = s + 1;
    modCount++;
    return true;
}

● 第一:首先將成員變量 array 賦值給局部變量 a,將成員變量 size 賦值給局部變量 s。

● 第二:判斷集合的長度 s 是否等於數組的長度(如果集合的長度已經等於數組的長度了,說明數組已經滿了,該重新分 配 新 數 組 了 ) , 重 新 分 配 數 組 的 時 候 需 要 計 算 新 分 配 內 存 的 空 間 大 小 , 如 果 當 前 的 長 度 小 於MIN_CAPACITY_INCREMENT/2(這個常量值是 12,除以 2 就是 6,也就是如果當前集合長度小於 6)則分配 12 個長度,如果集合長度大於 6 則分配當前長度 s 的一半長度。這裏面用到了三元運算符和位運算,s >> 1,意思就是將s 往右移 1 位,相當於 s=s/2,只不過位運算是效率最高的運算。

● 第三:將新添加的 object 對象作爲數組的 a[s]個元素。

● 第四:修 改 集 合 長 度size爲s+1。

● 第五:modCount++,該變量是父類中聲明的,用於記錄集合修改的次數,記錄集合修改的次數是爲了防止在用迭代器迭代集合時避免併發修改異常,或者說用於判斷是否出現併發修改異常的。

● 第六:return true,這個返回值意義不大,因爲一直返回 true,除非報了一個運行時異常。

● remove方法

@Override
public E remove(int index) {
    Object[] a = array;
    int s = size;
    if (index >= s) {
        throwIndexOutOfBoundsException(index, s);
    }
    @SuppressWarnings("unchecked") E result = (E) a[index];
    System.arraycopy(a, index + 1, a, index, --s - index);
    a[s] = null; // Prevent memory leak
    size = s;
    modCount++;
    return result;
}

● 第一:先將成員變量 array 和 size 賦值給局部變量 a 和 s。

● 第二:判斷形參 index 是否大於等於集合的長度,如果成了則拋出運行時異常

● 第三:獲取數組中腳標爲 index 的對象 result,該對象作爲方法的返回值

● 第四:調用 System 的 arraycopy 函數完成數組拷貝

● 第五:接下來就是很重要的一個工作,因爲刪除了一個元素,而且集合整體向前移動了一位,因此需要將集合最後一個元素設置爲 null,否則就可能內存泄露。

● 第六:重新給成員變量 array 和 size 賦值。

● 第七:記錄修改次數。

● 第八:返回刪除的元素

● clear方法

@Override
public void clear() {
    if (size != 0) {
        Arrays.fill(array, 0, size, null);
        size = 0;
        modCount++;
    }
}

如果集合長度不等於 0,則將所有數組的值都設置爲 null,然後將成員變量 size 設置爲 0 即可,最後讓修改記錄加 1。

3.數組和鏈表分別比較適合用於什麼場景,爲什麼?

● 數組和鏈表的區別

數組是將元素在內存中連續存儲的;它的優點:因爲數據是連續存儲的,內存地址連續,所以在查找數據的時候效率比較高;它的缺點:在存儲之前,我們需要申請一塊連續的內存空間,並且在編譯的時候就必須確定好它的空間的大小。在運行的時候空間的大小是無法隨着你的需要進行增加和減少而改變的,當數據兩比較大的時候,有可能會出現越界的情況,數據比較小的時候,又有可能會浪費掉內存空間。在改變數據個數時,增加、插入、刪除數據效率比較低。鏈表是動態申請內存空間,不需要像數組需要提前申請好內存的大小,鏈表只需在用的時候申請就可以,根據需要來動態申請或者刪除內存空間,對於數據增加和刪除以及插入比數組靈活。還有就是鏈表中數據在內存中可以在任意的位置,通過應用來關聯數據(就是通過存在元素的地址來聯繫)

● 鏈表和數組使用場景

數組應用場景:數據比較少;經常做的運算是按序號訪問數據元素;數組更容易實現,任何高級語言都支持;構建的線性表較穩定。

鏈表應用場景:對線性表的長度或者規模難以估計;頻繁做插入刪除操作;構建動態性比較強的線性表。

ps:1.7map是數組加鏈表,1.8多了紅黑樹

4.請用兩個隊列模擬堆棧結構?

兩個隊列模擬一個堆棧,隊列是先進先出,而堆棧是先進後出。模擬如下隊列 a 和 b:

● 入棧:a 隊列爲空,b 爲空。例:則將”a,b,c,d,e”需要入棧的元素先放 a 中,a 進棧爲”a,b,c,d,e”出棧:a 隊列目前的元素爲”a,b,c,d,e”。將 a 隊列依次加入 Arraylist 集合 a 中。以倒序的方法,將 a 中的集合取出,放入 b 隊列中,再將 b 隊列出列。代碼如下:


public static void main(String[] args) {
    Queue<String> queue = new LinkedList<String>(); //a 隊 列
    Queue<String> queue2 = new LinkedList<String>();    //b 隊列
    ArrayList<String> a = new ArrayList<String>();    //arrylist 集合是中間參數
    //往 a 隊列添加元素
    queue.offer("a");
    queue.offer("b");
    queue.offer("c");
    queue.offer("d");
    queue.offer("e");
    System.out.print("進棧:");    //a 隊列依次加入 list 集合之中
    for (String q : queue) {
        a.add(q);
        System.out.print(q);
    }
    //以倒序的方法取出(a 隊列依次加入 list 集合)之中的值,加入 b 對列
    for (int i = a.size() - 1; i >= 0; i--) {
        queue2.offer(a.get(i));
    }
    //打印出棧隊列
    System.out.println("");
    System.out.print("出棧:");
    for (String q : queue2) {
        System.out.print(q);
    }
}

運行結果爲(遵循棧模式先進後出):

進棧:a b c d e

出棧:e d c b a

5.爲什麼redis需要把所有數據放到內存中?

Redis爲了達到最快的讀寫速度將數據都讀到內存中,並通過異步的方式將數據寫入磁盤。所以redis具有快速和數據持久化的特徵。如果不將數據放在內存中,磁盤I/O速度爲嚴重影響redis的性能。在內存越來越便宜的今天,redis將會越來越受歡迎。如果設置了最大使用的內存,則數據已有記錄數達到內存限值後不能繼續插入新值。

6.Redis有哪幾種數據結構?

● String——字符串

String數據結構是簡單的key-value類型,value不僅可以是String,也可以是數字(當數字類型用Long可以表示的時候encoding就是整型,其他都存儲在sdshdr當做字符串)。

● Hash——字典

在Memcached中,我們經常將一些結構化的信息打包成hashmap,在客戶端序列化後存儲爲一個字符串的值(一般是JSON格式),比如用戶的暱稱、年齡、性別、積分等。

● List——列表

List說白了就是鏈表(redis使用雙端鏈表實現的List)

● Set——集合

Set就是一個集合,集合的概念就是一堆不重複值的組合。利用Redis提供的Set數據結構,可以存儲一些集合性的數據。

● Sorted Set——有序集合

和Set相比,Sorted Set是將Set中的元素增加了一個權重參數score,使得集合中的元素能夠按score進行有序排列,

● 帶有權重的元素,比如一個遊戲的用戶得分排行榜

● 比較複雜的數據結構,一般用到的場景不算太多

7.BeanFactory常用的實現類有哪些?

Bean工廠是工廠模式的一個實現,提供了控制反轉功能,用來把應用的配置和依賴從正真的應用代碼中分離。常用的BeanFactory實現有DefaultListableBeanFactory、XmlBeanFactory、ApplicationContext等。XMLBeanFactory,最常用的就是org.springframework.beans.factory.xml.XmlBeanFactory,它根據XML文件中的定義加載beans。該容器從XML文件讀取配置元數據並用它去創建一個完全配置的系統或應用。

8.BeanFactory與AppliacationContext有什麼區別?

● BeanFactory

基礎類型的IOC容器,提供完成的IOC服務支持。如果沒有特殊指定,默認採用延遲初始化策略。相對來說,容器啓動初期速度較快,所需資源有限。

● ApplicationContext

ApplicationContext是在BeanFactory的基礎上構建,是相對比較高級的容器實現,除了BeanFactory的所有支持外,ApplicationContext還提供了事件發佈、國際化支持等功能。ApplicationContext管理的對象,在容器啓動後默認全部初始化並且綁定完成。

9.簡單解釋一下Spring的AOP?

AOP(Aspect Oriented Programming),即面向切面編程,可以說是OOP(Object Oriented Programming,面向對象編程)的補充和完善。OOP引入封裝、繼承、多態等概念來建立一種對象層次結構,用於模擬公共行爲的一個集合。不過OOP允許開發者定義縱向的關係,但並不適合定義橫向的關係,例如日誌功能。日誌代碼往往橫向地散佈在所有對象層次中,而與它對應的對象的核心功能毫無關係。對於其他類型的代碼,如安全性、異常處理和透明的持續性也都是如此。這種散佈在各處的無關的代碼被稱爲橫切(cross cutting),在OOP設計中,它導致了大量代碼的重複,而不利於各個模塊的重用。

AOP技術恰恰相反,它利用一種稱爲“橫切”的技術,剖解開封裝的對象內部,並將那些影響了多個類的公共行爲封裝到一個可重用模塊,並將其命名爲“Aspect”,即切面。所謂“切面”,簡單說就是那些與業務無關,卻爲業務模塊所共同調用的邏輯或責任封裝起來,便於減少系統的重複代碼,降低模塊之間的耦合度,並有利於未來的可操作性和可維護性。使用“橫切”技術,AOP把軟件系統分爲兩個部分:核心關注點和橫切關注點。業務處理的主要流程是核心關注點,與之關係不大的部分是橫切關注點。橫切關注點的一個特點是,他們經常發生在覈心關注點的多處,而各處基本相似,比如權限認證、日誌、事物。AOP的作用在於分離系統中的各種關注點,將核心關注點和橫切關注點分離開來。AOP核心就是切面,它將多個類的通用行爲封裝成可重用的模塊,該模塊含有一組API提供橫切功能。比如,一個日誌模塊可以被稱作日誌的AOP切面。根據需求的不同,一個應用程序可以有若干切面。在Spring AOP中,切面通過帶有@Aspect註解的類實現

10.在Spring AOP中,關注點和橫切關注的區別是什麼?

關注點是應用中一個模塊的行爲,一個關注點可能會被定義成一個我們想實現的一個功能。橫切關注點是一個關注點,此關注點是整個應用都會使用的功能,並影響整個應用,比如日誌,安全和數據傳輸,幾乎應用的每個模塊都需要的功能。因此這些都屬於橫切關注點。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章