java基本

1:反射

在運行時動態的獲取信息以及動態調用對象的方法的功能稱爲Java 的反射機制。

Class類對象和類的對象區別:

類對象應該指類Class對象,字節碼對象可以通Class.forName()/getclass()/.class來獲取,當jvm加載一個類時就會爲這個類創建一個Class對象。
類的對象,通常就是指我們通過new這個類或者反射得到Class對象再調newInstance()創建的對象,存在內存的堆中,也叫類的實例。

獲取Class字節碼對象後,調用newInstance()有兩種方式。

1:操作無參的構造函數實現方式

Class字節碼對象.newInstance();

2:操作有參的構造函數實現方式

Constructor constructor =Class字節碼對象.getConstructor(String.class);

constructor.newInstance("String類型");

Class.forName()和ClassLoader.loadClass的區別:

Class.forName(className)方法,內部實際調用的方法Class.forName(className,true,classloader);第2個boolean參數表示類是否需要初始化,Class.forName(className)默認是需要初始化。一旦初始化,就會觸發目標對象的 static塊代碼執行,static參數也也會被再次初始化。

ClassLoader.loadClass(className)方法,內部實際調用的方法是  ClassLoader.loadClass(className,false);第2個 boolean參數,表示目標對象是否進行鏈接,false表示不進行鏈接,由上面介紹可以知道不進行鏈接意味着不進行包括初始化等一些列步驟,那麼靜態塊和靜態對象就不會得到執行


2:String類

  • equals() 與 == 的區別是什麼?

對於==,如果作用於基本數據類型的變量,則直接比較其存儲的 “值”是否相等;

    如果作用於引用類型的變量,則比較的是所指向的對象的地址

對於equals方法,equals方法不能作用於基本數據類型的變量

    如果沒有對equals方法進行重寫,則比較的是引用類型的變量所指向的對象的地址;諸如String、Date等類對equals方法進行了重寫的話,比較的是所指向的對象的內容。

通過equals()比較兩個對象時返回true它們的hashCode()值一定相同。因此在重寫equals方法的時候,同時也需要重寫hashcode。以維持這一特性。

hashCode()值相同通過equals()比較兩個對象時返回不一定是true

Equals的特性:自反性,對稱性,傳遞性,一致性,非空性。

  • Java的String,StringBuffer,StringBuilder有什麼區別?

String是不可變類(immutable),每次在String對象上的操作都會生成一個新的對象;StringBuffer和StringBuilder則允許在原來對象上進行操作,而不用每次增加對象;StringBuffer是線程安全的,但效率較低,而StringBuilder則不是效率最高。

當字符串相加操作或者改動較少的情況下,建議使用 String str="hello"這種形式;
當字符串相加操作較多的情況下,建議使用StringBuilder,如果採用了多線程,則使用StringBuffer。

StringBuilder爲什麼線程不安全?

StringBuffer 每次獲取 toString 都會直接使用緩存區的 toStringCache 值來構造一個字符串。而 StringBuilder 則每次都需要複製一次字符數組,再構造一個字符串。所以,緩存衝這也是對 StringBuffer 的一個優化吧,不過 StringBuffer 的這個toString 方法仍然是同步的。

  • String與char[]數組之間的關係

兩者的轉化:

將字符串轉爲字符(char)數組 :String類的toCharArray()方法

將字符(char)數組轉換爲字符串 :String類的valueOf()方法

  • String爲什麼設計成不可變

字符串常量池的需要 當創建一個String對象時,假如此字符串值已經存在於常量池中,則不會創建一個新的對象,而是引用已經存在的對象。

允許String對象緩存HashCode字符串不變性保證了hash碼的唯一性,因此可以放心地進行緩存.這也是一種性能優化手段,意味着不必每次都去計算新的哈希碼.

安全性String被許多的Java類(庫)用來當做參數,例如 網絡連接地址URL,文件路徑path,還有反射機制所需要的String參數等, 假若String不是固定不變的,將會引起各種安全隱患。

線程同步由於 String 是不可變的,它可以安全地共享許多線程,這對於多線程編程非常重要. 並且避免了 Java 中的同步問題,不變性也使得String 實例在 Java 中是線程安全的,這意味着你不需要從外部同步 String 操作。關於 String 的另一個要點是由截取字符串 SubString 引起的內存泄漏,這不是與線程相關的問題,但也是需要注意的。

  • 當一個String實例調用intern()方法時,Java查找常量池中是否有相同Unicode的字符串常量,如果有,則返回其的引用,如果沒有,則在常量池中增加一個Unicode等於str的字符串並返回它的引用。

3:jdk1.7和1.8區別

Jvm運行時的數據區域劃分:

最大的差別就是:元數據區取代了永久代。元空間的本質和永久代(方法區)類似,都是對JVM規範中方法區的實現。不過元空間與永久代之間最大的區別在於:元數據空間並不在虛擬機中,而是使用本地內存。 

接口的默認和靜態方法

通過default關鍵字允許在接口中定義一個默認方法(普通方法)

使用方式:創建子類對象.方法名

若一個子類繼承一個父類並且實現了一個接口。這個父類和接口都同時定義了一個相同名稱的方法(接口中的默認方法,父類的普通方法)。則子類調用時以父類爲準,即類優先原則。

若一個接口定義了一個默認方法,另外一個接口也定義了一個同名的默認方法。當一個類同時實現這兩個接口的時候,會提示要自重寫默認方法,否則報錯。

CurrentHashMap:1.7:分段鎖,1.8:CAS;Hashmap:1.7:底層是數組+鏈表;鏈表上新增元素採用頭插法,1.8:底層是數組+鏈表+紅黑樹;鏈表上新增元素採用尾插法

Switch語句支持String類型

函數式接口:只有一個抽象方法的接口。當有兩個或兩個以上的抽象方法,或沒有抽象方法都不可以叫做函數式接口。但是函數式接口中可以含有默認方法,靜態方法、被顯示重寫的Object類的public方法,即需要加上@override(除了clone是protected外,其餘八個都是public),只要求抽象方法的個數爲1個一般帶有@FunctionalInterface標識。

1.8之前常用的函數式接口:runnablle,callable,comparable

1.8新增的函數式接口:

Lambda表達式 :本質上是一段匿名內部類使用lambda表達式的一個前提要求是,該變量必須實現某個函數式接口。有三種寫法。()可以大致理解爲就是函數式接口的唯一抽象方法,裏面是該抽象方法的參數變量(可以寫參數類型,也可以不寫參數類型)

(參數)->單行語句

(參數)->{多行語句}

(參數)->表達式

方法與構造函數引用使用 :: 關鍵字來傳遞方法或者構造函數引用若lambda體中的內容有其他方法已經實現了,那麼可以使用“方法引用”即直接調用這個方法使用就行 也可以理解爲方法引用是lambda表達式的另外一種表現形式並且其語法比lambda表達式更加簡單

java.util.Stream 表示能應用在一組元素上一次執行的操作序列。Stream 操作分爲中間操作或者最終操作兩種,最終操作返回一特定類型的計算結果,而中間操作返回 Stream 本身,這樣就可以將多個操作依次串起來。Stream的創建需要指定一個數據源,比如 java.util.Collection 的子類,List 或者 Set,但是Map不支持。Stream的操作可以串行執行stream()或者並行執行parallelStram()。

第一步:創建stream:

Collection提供了兩個方法.stream()(串行執行)與paralleStream()(並行執行,依賴fork-join框架,不需要自己創建多線程)

通過Arrays中的Stream()獲取一個數組流。

通過Stream類中靜態方法of()

第二步:多箇中間操作可以連接起來形成一個流水線,除非流水線上觸發終止操作,否則中間操作不會執行任何的處理,而在終止操作時一次性全部執行,稱爲“惰性求值

中間操作:

  *  filter-接收lambda,從流中排除某些元素

  *  limit-接收integer,截斷流,使其元素個數不超過給定的integer數量

  *  distinct-一個去除重複元素的方法

  *  map-接收lambda,將元素轉換爲其他形式或提取信息時,接收一個函數作爲參數,該函數被應用到每個元素上,並將其映射成一個新的元素

  *  sort-排序方法

第三步:終止操作:

*      查找和匹配

         *  allMatch-檢查是否匹配所有元素

         *  anyMatch-檢查是否至少匹配一個元素

         *  noneMatch-檢查是否沒有匹配所有元素

         *  findFirst-返回第一個元素

         *  findAny-返回當前流中的任意元素

         *  count-返回流中元素的總個數

         *  max-返回流中最大值

         *  min-返回流中最小值

 *  reduce歸約操作: reduce:(T identity,BinaryOperator)-可以將流中元素反覆結合起來,得到一個值

   *  collect收集操作:將流轉爲其他形式,用於給Stream中元素做彙總

//.collect(Collectors.toList()));//.collect(Collectors.toMap(item -> item.getPid(),


4:常見的集合

集合之間的關係

Iterator接口是迭代器接口,主要用於遍歷集合中的元素,但是不能遍歷Map集合,只能遍歷Collection接口的實現類。

再看Enumeration,它是JDK 1.0引入的抽象類。作用和Iterator一樣,也是遍歷集合;但是Enumeration的功能要比Iterator少,不能支持刪除操作,且不再fail-fast機制。在上面的框圖中,Enumeration只能在Hashtable, Vector, Stack中使用。

Arrays和Collections工具類,是用來操作數組、集合的兩個工具類。

FAIL-FAST和FAIL-SAFE:

FAIL-FAST原理:

迭代器在遍歷時直接訪問集合中的內容,並且在遍歷過程中使用modCount變量,

集合中在被遍歷期間如果內容發生變化,就會改變modCount的值,

每當迭代器使用 hashNext()/next()遍歷下一個元素之前,都會檢測modCount變量和expectedmodCount值是否相等,

如果相等就返回遍歷,否則拋出異常,終止遍歷.

FAIL-SAFE原理:

由於迭代時是對原集合的拷貝的值進行遍歷,所以在遍歷過程中對原集合所作的修改並不能被迭代器檢測到,所以不會出發ConcurrentModificationException

使用場景:

java.util包下的集合類都是快速失敗機制的, 不能在多線程下發生併發修改(迭代過程中被修改).

java.util.concurrent包下的容器都是安全失敗,可以在多線程下併發使用,併發修改.

快速失敗和安全失敗是對迭代器而言的。併發環境下建議使用 java.util.concurrent 包下的容器類,除非沒有修改操作

For循環刪除list中元素的注意事項:

普通for循環

 這種方式的問題在於,刪除某個元素後,list的大小發生了變化,而索引也在變化,所以會導致在遍歷的時候漏掉某些元素。比如當你刪除第1個元素後,繼續根據索引訪問第2個元素時,因爲刪除的關係後面的元素都往前移動了一位,所以實際訪問的是第3個元素。因此,這種方式可以用在刪除特定的一個元素時使用,但不適合循環刪除多個元素時使用。解決辦法第一種是手動將要訪問的元素也同時向前移動一位。第二種是逆序遍歷list然後再進行刪除

增強For循環

這種方式的問題在於,刪除元素後繼續循環會報錯誤信息ConcurrentModificationException,因爲元素在使用的時候發生了併發的修改,導致異常拋出。在用迭代器迭代集合的時候,迭代器一旦創建,就不允許更改集合,如果所迭代的集合(Set或者List)的有修改的話,就會拋出 ConcurrentModificationException異常, 用迭代器自身的remove方法除外。在 foreach 循環中執行 list.remove(item);,對 list 對象的 modCount 值進行了修改,而 list 對象的迭代器的 expectedModCount 值未進行修改,因此拋出了ConcurrentModificationException異常。

Iterator迭代器遍歷

這種方式可以正常的循環及刪除。但要注意的是,使用iterator的remove方法,如果用list的remove方法同樣會報上面提到的ConcurrentModificationException錯誤。

Hashmap:允許null值和null鍵,但是null鍵只允許存在一個,多個null鍵的時候會發生值覆蓋,,null值是允許多個。不保證順序,非線程安全

容量capacity默認16,容量capacity最大值爲2^30,加載因子loadfactor默認0.75,鏈表轉化爲紅黑樹的節點數treeify-threshold默認8,紅黑樹轉化爲鏈表的節點數untreeify-threshold默認6,樹的最小節點數量min-tree-capacity默認64(只要容量capacity大於64時才真正的進行鏈表向紅黑樹的轉變,否則和jdk1.7一樣只進行數組擴容),底層數據結構Node(含hash,key,value,下一個節點Node),threshold=capacity*loadfactor=12.

構造函數:若是用戶指定的初始容量大於capacity最大值,則將最大值修改爲用戶指定的值;根據用戶傳入的初始容量和加載因子計算threshold;

Hashmap線程不安全的原因(造成環):

當多個線程同時對這個HashMap進行put操作,而察覺到內存容量不夠,需要進行擴容時,多個線程會同時執行resize操作。具體見https://blog.csdn.net/hhx0626/article/details/54024222

Get:

1:先根據傳進來的key,得到該key在數組中的角標。這個過程主要分爲三步

在HashMap中,計算key的hash值,也就是在table數組中的下標位置算法很巧妙,用到了許多位運算。

主要分爲三步:

  1. 計算對象自己的hashCode()值
  2. 計算上步得到的值進行無符號右移16位,然後和上步計算的hashcode值進行異或這樣做的好處有2點:

第一:由於n比較小,導致hash只要低4位參與了運算,高位的計算可認爲是無效的,這樣就導致計算結果只和底位信息相關,高位數據沒有發揮作用。通過這種方式,讓高位數據和低位數據進行異或,以此增加低位信息的隨機性,變相的讓高位數據參與了運算

第二:增加hash的複雜度,避免因重寫的hashCode導致分佈不佳。

3.定位操作,對上步計算得到hash值進行取模操作(這裏用的是位運算)

   

 

2:若角標爲空直接返回null;

3:若角標不爲空,檢查該角標對應的第一個節點,若hash值不同直接返回null,若相同則進行key的equals的判斷兩個鍵是否相同(這也是爲什麼重寫equals要求重寫hashcode的原因:因爲重寫的equals一般比較的比較全面和複雜,這樣效率會很低。而利用hashcode()進行比對時,只需要生成一個hash值,首先通過hash進行判等操作,若hash值不等直接返回,這樣大大的加快了判斷速度;而不同的鍵可能產生相同的hash,因此僅僅憑藉hash是不夠完全準確,所有hash值相同的情況下還要進行一次equals操作。這種比較機制常用在hash容器,比如hashmap,hashtable,hashset等)。若通過equals判斷後仍然相等的話,則說明第一個節點就是要查找的Node,返回該Node。若equals不同則對第一個節點的Next進行判斷;

4:若第一個節點的Next存在則要分兩種情況,因爲在1.8下,同一角標上的多個Node有鏈表和紅黑樹兩種解決hash衝突的方法。用instanceof判斷第一個節點的next節點是不是TreeNode類型,若是按照紅黑樹的方法查找,若不是則進行單向鏈表的遍歷(do-while實現)。兩種情況下要是找到對應的Node返回該Node。否則返回null。

Put:(對於增,刪,改在操作完成都會++modCount操作,該字段主要用於Fail-Fast ,在這些線程不安全的集合中,初始化迭代器時會給這個modCount賦值,如果在遍歷的過程中,一旦發現這個對象的modCount和迭代器存儲的modCount不一樣,就會報錯。因此推薦集合遍歷的時候採用迭代器)

1:首先當數組爲null或者數組長度爲0時,要進行一次擴容(即此次擴容是給數組賦初始長度和閾值。初始擴容和增大擴容是寫在一個resize()方法中,因此該方法前面的一系列判斷就是爲了區分是初始擴容還是增量擴容)。即將底層的數據結構延遲到插入鍵值對時再進行初始化

2: 然後根據傳進來的key,調用重寫的靜態hash方法得到哈希值,將哈希值與數組長度減1進行按位與(這裏就要求數組table的值是2的整數冪,目的是儘量散列均勻,減少哈希碰撞)得到該key在數組中的角標。

3:若角標爲null,說明數組此角標位置上現在沒有Node,則此時直接建立一個新的節點,將該角標指向它

4:若角標不爲空,則先判斷第一個Node節點,若hash和equals()都通過的話則讓e指向和它相等的節點。若第一個節點和現有Node不等,則需要判斷是用紅黑樹增加節點還是單鏈法增加節點。

5:用instanceof判斷第一個節點是不是TreeNode類型,若是按照紅黑樹的方法增加,若不是TreeNode,則開始對這個位置上的鏈表進行遍歷。在遍歷的過程中通過binCount記錄鍵值對個數,當遍歷到鏈表的末尾的時候生成新的節點插入,但是要根據binCount判斷此時是否需要進行紅黑樹的轉化,同時若在遍歷的過程中發現和插入節點相等的節點則直接跳出循環,讓e指向和他相等的節點

6:判斷e執行的節點是否爲null。不爲null的時候表示集合中存在和插入元素相等的鍵值對,則用新值覆蓋舊值,並將舊值返回。

7:最後還需要進行一次判斷。整個操作完成後,++size,size代表整個map中鍵值對的數量。當size>threshold的時候要進行一次擴容操作。size指的是整個集合中鍵值對的數量,也就意味着長度爲16的數組當存儲的鍵值對個數大於12個的時候就要進行第一次擴容。要想達到紅黑樹的轉變至少需要擴容2次才能滿足數組的大小不小於64.

Resize:

1:計算新桶的容量newCap和新閾值newThr

2:根據計算出的newCap創建新的桶數組,上面的put過程中桶數組也是在這裏進行初始化的

3:將鍵值對節點重新映射到新的桶數組中。對於樹形節點,需要先拆分成紅黑樹再映射,對於鏈表節點類型,則需要先對鏈表進行分組,然後再映射,且分組後,組內節點相對位置保持不變。依次遍歷鏈表,並計算hash&oldCap的值,如果值爲0,將 loHead 和 loTail 指向這個節點。如果後面還有節點 hash & oldCap 爲0的話,則將節點鏈入 loHead 指向的鏈表中,並將 loTail 指向該節點。如果值爲非0的話,則讓 hiHead 和 hiTail 指向該點。完成遍歷後,可能會得到兩條鏈表(最壞的時候一個桶節點對應的鏈表上hash & oldCap都爲0或者非0),此時就完成了鏈表分組。

 

 

 

Remove

1:首先也是根據鍵定義到桶的位置(三步定hash),若該角標處的第一個Node與其相等(包括hash和key),則就取該Node

2:若第一個不滿足,則按照紅黑樹或者鏈表的查找方法找到符合條件的Node

3:按照紅黑樹或者鏈表的方式將該節點刪除。同時要進行-鏈表或者紅黑樹修復操作。

TreeifyBin(將普通節點鏈表轉化爲樹形節點鏈表)

1:樹化的前提條件是:桶節點中的鏈表長度大於等於8,桶數組容量大於等於64.當桶數組容量比較小時,鍵值對節點 hash 的碰撞率可能會比較高,進而導致鏈表長度較長。這個時候應該優先擴容,而不是立馬樹化。畢竟高碰撞率是因爲桶數組容量較小引起的,這個是主因。容量小時,優先擴容可以避免一些不必要的樹化過程。同時,桶容量較小時,擴容會比較頻繁,擴容時需要拆分紅黑樹並重新映射。所以在桶容量比較小的情況下,將長鏈表轉成紅黑樹是一件喫力不討好的事。

2:首先將普通鏈表轉化爲TreeNode型節點組成的鏈表,然後調用treeify將該鏈表轉化爲紅黑樹。TreeNode繼承Node,因此TreeNode仍然包含next引用,原鏈表的節點順序最終通過next引用保存下來。

視圖:

支持三種視圖模式

父類AbstractMap:

    transient Set<K>         keySet; // key view

    transient Collection<V>   values; // value view

子類HashMap:

    transient Set<Map.Entry<K,V>>  entrySet;

加載因子設置成0.75的原因:

加載因子越大,填滿的元素越多,空間利用率越高,但衝突的機會加大了。
反之,加載因子越小,填滿的元素越少,衝突的機會減小,但空間浪費多了。

引入紅黑樹的原因:

當個數不多的時候,直接鏈表遍歷更方便,實現起來也簡單。而紅黑樹的實現要複雜的多,要進行左旋,右旋等操作以維持平衡。【參考知識點十六】

解決哈希衝突的方法:拉鍊法,再哈希法,開放定址法,建立公共溢出區。

底層數據結構table被定義爲transient的原因:

被transient修飾的變量是不會被默認的序列化機制序列化。

第一因爲hashmap存儲的是鍵值對,所以只要把鍵值對序列化,就可以根據鍵值對重構hashmap。

第二因爲table 多數情況下是無法被存滿的,序列化未使用的部分,浪費空間;

第三因爲同一個鍵值對在不同 JVM 下,所處的桶位置可能是不同的,在不同的 JVM 下反序列化 table 可能會發生錯誤。

Hashtable:線程安全(通過synchronized),不允許null鍵和null值;無序

視圖:支持Enumeration迭代器訪問

synchronized Set<K>               keySet()

synchronized Collection<V>       values()

synchronized Set<Entry<K, V>>    entrySet()

synchronized Enumeration<V>      elements()

synchronized Enumeration<K>      keys()

Hashmap和Hashtable的比較

1:hashTable是線程安全,不支持null鍵和null值,hashmap非線程安全,支持null鍵(只能有一個)和null值(可以有多個)

 HashMap計算key的hash值時調用單獨的方法,在該方法中會判斷key是否爲null,如果是則返回0;而Hashtable中則直接調用key的hashCode()方法,因此如果key爲null,則拋出空指針異常。

  HashMap將鍵值對添加進數組時,不會主動判斷value是否爲null;而Hashtable則首先判斷value是否爲null。

ConcurrentHashmap和Hashtable都是支持併發的,這樣會有一個問題,當你通過get(k)獲取對應的value時,如果獲取到的是null時,你無法判斷,它是put(k,v)的時候value爲null,還是這個key從來沒有做過映射。HashMap是非併發的,可以通過contains(key)來做這個判斷。而支持併發的Map在調用m.contains(key)和m.get(key),m可能已經不同了。

2:HashTable中hash數組默認大小是11,擴容大小是 old*2+1。HashMap中hash數組的默認大小是16,而且一定是2的指數,擴容大小是old*2。

3:hashtable支持Enumeration的遍歷,該遍歷方式是非fail-fast的

4:因爲hashmap非線程安全所有速度較快

5:hashtable和hashmap取hash和定義角標

Hashtable:

Hashmap:

 

Treemap:非線程安全,有序,基於紅黑樹(紅黑樹的參考知識點十六)

LinkedHashMap:有順序的存儲

插入有序:put的時候的順序是什麼,取出來的時候就是什麼樣子

訪問排序get的時候,會改變元素的順序,會把該元素移到數據的末尾

訪問排序的實現:創建集合的時候指定爲true

兩種比較實現方式的比較:

①、Comparator位於包java.util下,而Comparable位於包java.lang下。

②、Comparable接口將比較代碼嵌入需要進行比較的類的自身代碼中,而Comparator接口在一個獨立的類中實現比較。

③、如果前期類的設計沒有考慮到類的Compare問題而沒有實現Comparable接口,後期可以通過Comparator接口來實現比較算法進行排序,並且爲了使用不同的排序標準做準備,比如:升序、降序。

④、Comparable接口強制進行自然排序,而Comparator接口不強制進行自然排序,可以指定排序順序。

數組排序:Arrays.sort(array);//會檢查數組個數大於286且連續性好就使用歸併排序,若小於47使用插入排序其餘情況使用快速排序

集合排序:對於Integer、String等,這些類都已經重寫了Compare方法,都有默認排序規則,例如對於Integer類型會比較其包裝的值類型大小,對於String類型會以長度最小字符串爲基準,逐一比較相同位置字符的ASCII碼大小,如果都相同則比較字符串的長度。

集合排序:對於自定義類,有兩種實現方式

實現compare接口,重寫compareTo方法,讓該類的對象自身具有比較性

實現comarator接口,重寫compare方法,在調用sort方法時,傳入一個比較器

Arraylist::基於動態數組實現,非線程安全,默認10,允許null值;

Add:

1:首先將size+1,產生一個minCapacity的變量,調用ensureCapacity(minCapacity)方法保證數組在插入一個元素時有可用的容量,然後將元素e放到數組elementData的size位置,即新增的元素總是位於最後一個,最後將size+1

2:調用ensureCapacity(minCapacity)時進行判斷,必要時進行數組的擴容首先將modCount+1,modeCount表示修改數組結構的次數(維護在父類AbstractList中),如果入參minCapacit大於目前數組elementData的容量,則將容量擴展到 (oldCapacity * 3)/2 若此時容量仍然小於minCapacity,則直接將minCapacity設置爲最新的容量最後使用Arrays.copyof()方法將原來elementData中元素copy到新的數組中,並將新數組賦值給elementData.即當擴容後的容量任然不滿足的時候,就直接使用現在的長度作爲數組最新長度

3:若是插入到指定角標index的位置上的時候則在前兩步的基礎上做一定的補充。首先在進行第一步的時候會判斷角標是否越界(小於0或者大於size),然後執行第一步。在進行是否擴容的條件判斷之後,要將指定角標以後的元素(從index+1到size-index)全部向後移動,然後將新元素插入到指定角標位置index。

Remove:

若不指定角標,此時需要先判斷是否爲null。然後根據值找到對應的角標位置,然後進行數組的複製操作。若指定角標的時候則直接進行復制操作。即刪除的操作全部要轉化爲對角標的操作。

遍歷方式

增強for;迭代器;隨機訪問,通過索引值

Linkedlist::基於雙向鏈表,非線程安全,允許null

Get:效率較低,當目標index < 雙向鏈表長度的1/2,則從前先後查找 否則,從後向前查找。

Vector:矢量隊列等價於ArrayList,但它是線程安全的,默認10,但是不支持序列化

Stack:棧,線程安全

Arraylist和LinkedList區別

1.底層實現:ArrayList內部是數組實現,而LinkedList內部實現是雙向鏈表結構

2.接口實現:ArrayList實現了RandomAccess可以支持隨機元素訪問,而LinkedList實現了Deque可以當做隊列使用

3.性能:新增、刪除元素時ArrayList需要使用到拷貝原數組,而LinkedList只需移動指針,查找元素 ArrayList支持隨機元素訪問,而LinkedList只能一個節點的去遍歷

5:泛型:(參數化類型)

Java的泛型是如何工作的 ? 什麼是類型擦除 ?

泛型是通過類型擦除來實現的,泛型信息只存在於代碼編譯階段,在進入 JVM 之前,與泛型相關的信息會被擦除掉.

通配符有 3 種形式。

<?>被稱作無限定的通配符。

<? extends T>被稱作有上限的通配符。

<? super T>被稱作有下限的通配符。


6:線程的創建方式

繼承Thread;實現Runnable;實現collable:具有返回值;線程池創建:【具體見知識點十四中的Executor框架】

四種方式的區別和聯繫:

用接口實現去創建線程是多個線程共享一個target對象,適用於多線程處理同一個資源。因爲繼承創建線程是重寫run方法,而接口實現是實例化Thread類。

Runnable接口不能有返回值,不可以拋出異常,但Collable可以。

繼承Thread類的線程類不能再繼承其他父類(Java單繼承決定),但是卻可以實現多個接口,便於程序的擴展(一個類必須是先繼承,後實現)。

實現創建線程如果要訪問當前線程,則必須使用Thread.currentThread()方法。而繼承創建線程可以直接用this獲取當前線程。


7:接口和抽象類的區別

相同點:

兩者都不可直接被實例化

兩者都可以包含抽象方法(接口中的所有方法必須是抽象的,而抽象類中可以不包含抽象方法(但是包含抽象方法的類一定是抽象類))。實現接口或者是繼承抽象類,都要重寫他們的抽象方法(若繼承抽象類的子類還是抽象類,則可以不重寫抽象方法)

區別:

抽象類是對類的抽象,接口是對抽象類的抽象。抽象類主要用作代碼的複用,接口主要用作事物特性的抽象

抽象類除了不可以直接被實例化外,和一般普通的類沒有區別。可以有局部變量,普通方法,構造函數。而接口的成員變量只能是Public static final類型,接口中的方法只能是抽象方法。

抽象類的訪問權限可以是public,protected,default,private中的任意一種,而接口的訪問權限必須是public。

只可以是單繼承,但是可以多實現


8:Object類的方法

GetClass:是通過反射獲得Class(字節碼)對象的一種方式

Equals,Hashcode:返回的是對象的哈希碼值,int類型,爲了提高哈希表的性能。當重寫equals方法時建議重寫該方法。

Wait,Notify,notifyAll:這幾個方法主要用於java多線程之間的協作

toString:默認返回是:getClass().getName() + "@" + Integer.toHexString(hashCode());因此建議每一個對象都重寫改方法。

Clone:默認返回的是淺克隆。

深克隆:創建一個新對象,無論原型對象的成員變量是值類型還是引用類型,都將複製一份給克隆對象原對象的修改對現有對象不產生影響。(序列化實現(必須要實現serializable接口)或者覆蓋object類的clone方法)

淺克隆:創建一個新對象複製時只複製它本身和其中包含的值類型的成員變量對於非基本類型屬性,仍指向原有屬性所指向的對象的內存地址。原對象的修改對現有對象產生影響。

Finalize:主要與Java垃圾回收機制有關


9:多態的實現原理

JVM 的方法調用指令有個,分別是

  Invokestatic:調用static方法(類方法)

Invokespecial:調用實例構造器方法,私有方法,父類方法

Invokesvirtual:調用對象的實例方法,根據對象的實際類型進行分派

Invokeinterface:調用接口方法,運行時搜索一個實現了這個接口方法的對象,找出適合的方法進行調用。

Invokedynamic:運行時動態解析所引用的方法,然後再執行,支持動態類型語言

Invokestatic和invokespecial是靜態綁定,由這兩者調用的方法稱爲非虛方法invokevirtual和invokeinterface是動態綁定的,由這兩者調用的稱爲虛方法

在Class文件中的常量中存有大量的符號引用。字節碼中的方法調用指令就以常量池中指向方法的符號引用作爲參數。這些符號引用一部分在類的加載階段(解析)或第一次使用的時候就轉化爲了直接引用(指向數據所存地址的指針或句柄等),這種轉化稱爲靜態鏈接,要求這個方法(稱爲非虛方法)是編譯期可知,運行期不變。符合要求的就是靜態方法和私有方法和實例構造器和父類方法和final修飾的方法(它不是非虛方法)。而相反的,另一部分在運行期間轉化爲直接引用,就稱爲動態鏈接。

符號本質上就是對內存地址的標識。CPU指令執行和內存範文,都是基於地址,倘若沒有地址,CPU也就無法知道自己該執行哪條指令,該訪問哪裏的內存數據。可是,作爲人類我們卻很難直接使用二進制數,於是我們通過給地址打上標記的形式來簡化我們的使用,這裏給地址打上的標記也就是前面所說的符號。

重載是編譯時候的多態:使用哪個重載版本,完全取決於傳入參數的數量和數據類型。但編譯器在重載時是通過參數的靜態類型而不是實際類型作爲判定依據的。並且靜態類型是編譯期可知的,所以在編譯階段,javac 編譯器就根據參數的靜態類型決定使用哪個重載版本。覆蓋/重寫是運行時的多態:對於普通方法遵循的是編譯看左運行看右,對於靜態方法編譯和運行都看左。

重寫/覆蓋:方法名,返回值,參數列表保持相同,子類重寫時方法的訪問修飾符要大於父類,異常檢查類型要小於父類

重載:方法名相同,參數列表不同(參數的個數,參數的順序,參數的類型),與返回值的類型無關,不做要求。因爲函數返回值只是代表函數運行之後的一個狀態,它是調用者和被調用者之間進行交流的關鍵,不能作爲一個函數的標識。

多態允許具體訪問時實現方法的動態綁定。Java對於動態綁定的實現主要依賴於方法表,通過繼承和接口的多態實現有所不同。

繼承:在執行某個方法時,在方法區中找到該類的方法表,再確認該方法在方法表中的偏移量,找到該方法後如果被重寫則直接調用,否則認爲沒有重寫父類該方法,這時會按照繼承關係搜索父類的方法表中該偏移量對應的方法。 

接口:Java 允許一個類實現多個接口,從某種意義上來說相當於多繼承,這樣同一個接口的的方法在不同類方法表中的位置就可能不一樣了。所以不能通過偏移量的方法,而是通過搜索完整的方法表

重寫的原則:

這裏的final int prime = 31爲何要選這個值呢,有以下幾個原因:

1.首先31是個質數,只能被1和本身整除的數,乘上後不容易出現重複

2.其次31這個數不大也不小,不至於超出返回類型的int,也不容易在乘上後重復

3.最後就是31=32-1=2^5 -1,計算方便,向左移動5位,再減一就行了

步驟:

第一步:定義一個初始值,一般來說取17

int result = 17;

第二步:分別解析自定義類中與equals方法相關的所有字段(假如hashCode中考慮的字段在equals方法中沒有考慮,則兩個equals的對象就很可能具有不同的hashCode)

 

    情況一:字段a類型爲boolean 則[hashCode] = a ? 1 : 0;

    情況二:字段b類型爲byte/short/int/char, 則[hashCode] = (int)b;

    情況三:字段c類型爲long, 則[hashCode] = (int) (c ^ c>>>32);

   情況四:字段d類型爲float, 則[hashCode] = d.hashCode()(內部調用的是Float.hashCode(d), 而該靜態方法內部調用的另一個靜態方法是Float.floatToIntBits(d))

   情況五:字段e類型爲double, 則[hashCode] = e.hashCode()(內部調用的是Double.hashCode(e), 而該靜態方法內部調用的另一個靜態方法是Double.doubleToLongBits(e),得到一個long類型的值之後,跟情況三進行類似的操作,得到一個int類型的值)

   情況六:引用類型,若爲null則hashCode爲0,否則遞歸調用該引用類型的hashCode方法。

    情況七:數組類型。(要獲取數組類型的hashCode,可採用如下方法:s[0]*31 ^ (n-1) + s[1] * 31 ^ (n-2) + ..... + s[n-1], 該方法正是String類的hashCode實現所採用的算法)

第三步:對於涉及到的各個字段,採用第二步中的方式,將其依次應用於下式:

result = result * 31 + [hashCode];


10:instanceOf與isInstance

instanceof運算符 只被用於對象引用變量,檢查左邊的被測試對象 是不是 右邊類或接口的 實例化。如果被測對象是null值,則測試結果總是false。

形象地:自身實例或子類實例 instanceof 自身類   返回true

例: String s=new String("javaisland");

       System.out.println(s instanceof String); //true 

Class類的isInstance(Object obj)方法,obj是被測試的對象,如果obj是調用這個方法的class或接口 的實例,則返回true。這個方法是instanceof運算符的動態等價。

形象地:自身類.class.isInstance(自身實例或子類實例)  返回true

例:String s=new String("javaisland");

      System.out.println(String.class.isInstance(s)); //true


11:異常體系

1. runtimeException子類:

    1、 java.lang.ArrayIndexOutOfBoundsException, 數組索引越界異常。當對數組的索引值爲負數或大於等於數組大小時拋出。
       2、java.lang.ArithmeticException 算術條件異常。譬如:整數除零等。
        3、java.lang.NullPointerException 空指針異常。當應用試圖在要求使用對象的地方使用了null時,拋出該異常。譬如:調用null對象的實例方法、訪問null對象的屬性、計算null對象的長度、使用throw語句拋出null等等
        4、java.lang.ClassNotFoundException 找不到類異常。當應用試圖根據字符串形式的類名構造類,在遍歷CLASSPAH之後找不到對應名稱的class文件時,拋出該異常。

     5、java.lang.NegativeArraySizeException  數組長度爲負異常

     6、java.lang.ArrayStoreException 數組中包含不兼容的值拋出的異常

   7、java.lang.SecurityException 安全性異常

     8、java.lang.IllegalArgumentException 非法參數異常

2.IOException

IOException:操作輸入流和輸出流時可能出現的異常。

EOFException   文件已結束異常

FileNotFoundException   文件未找到異常

Try..catch..finally的執行

try 塊:用於捕獲異常。其後可接零個或多個catch塊,如果沒有catch塊,則必須跟一個finally塊。
catch 塊:用於處理try捕獲到的異常。一旦某個catch捕獲到匹配的異常類型,整個try..catch結束,其他catch不再執行。同時越基礎的異常類型應該越放在所有catch的最後。避免特定類型的異常執行不到。
finally 塊:無論是否捕獲或處理異常,finally塊裏的語句都會被執行。當在try塊或catch塊中遇到return語句時,finally語句塊將在方法返回之前被執行。在以下4種特殊情況下,finally塊不會被執行:
1)在finally語句塊中發生了異常。
2)在前面的代碼中用了System.exit()退出程序。
3)程序所在的線程死亡。
4)關閉CPU。

以下需要注意:

a.)當try沒有捕獲到異常時:try語句塊中的語句逐一被執行,程序將跳過catch語句塊,執行finally語句塊和其後的語句;

b.)當try捕獲到異常,catch語句塊裏沒有處理此異常的情況:當try語句塊裏的某條語句出現異常時,而沒有處理此異常的catch語句塊時,此異常將會拋給JVM處理,finally語句塊裏的語句還是會被執行,但finally語句塊後的語句不會被執行;

c.)當try捕獲到異常,catch語句塊裏有處理此異常的情況:在try語句塊中是按照順序來執行的,當執行到某一條語句出現異常時,程序將跳到catch語句塊,並與catch語句塊逐一匹配,找到與之對應的處理程序,其他的catch語句塊將不會被執行,而try語句塊中,出現異常之後的語句也不會被執行,catch語句塊執行完後,執行finally語句塊裏的語句,最後執行finally語句塊後的語句;


12:序列化

 序列化: 將數據結構或對象轉換成二進制串的過程

 反序列化:將在序列化過程中所生成的二進制串轉換成數據結構或者對象的過程

 當兩個進程進行遠程通信時,可以相互發送各種類型的數據,包括文本、圖片、音頻、視頻等,而這些數據都會以二進制序列的形式在網絡上傳送。序列化可以使發送方把這個Java對象轉換爲字節序列,然後在網絡上傳送;反序列化可以使接收方從字節序列中恢復出Java對象。注意:如果一個類能被序列化,那麼它的子類也能夠被序列化。

具體操作

第一是實現seriablize接口。這雖然是一個空接口,卻是可被序列化的標誌。

第二要指定一個private static final long serialVersionUID 。serialVersionUID 被稱爲序列化 ID,它是決定Java對象能否反序列化成功的重要因子。在反序列化時,Java虛擬機會把字節流中的 serialVersionUID 與被序列化類中的 serialVersionUID 進行比較,如果相同則可以進行反序列化,否則就會拋出序列化版本不一致的異常。

通常有三種方式:添加默認的版本序列ID;添加一個隨機生成的不重複的序列化ID;使用@SuppressWarnings註解。一般推薦第一種,註解的實質也是自動生成一個隨機序列化ID。當一個對象序列化完成,在反序列化之前修改了該對象的序列化ID,則無法正常反序列化。

常見問題:

  1. 如果類中的一個成員未實現可序列化接口, 會發生什麼情況?

如果嘗試序列化實現可序列化的類的對象,但該對象包含對不可序列化類的引用,則在運行時將引發不可序列化異常 NotSerializableException

要序列化的對象(普通屬性,靜態屬性和transit修飾屬性)

測試類:

輸出結果分析

對於普通屬性一旦序列化完成,在反序列化之前,對屬性再次修改。也不會產生影響(name);對於靜態屬性序列化完成,反序列化之前進行修改,會產生影響(noseri1),因爲序列化保存的是對象的狀態,而被static修飾是類的狀態,因此不會保存static修飾的屬性;被transit修飾的屬性不會進行序列化,意爲臨時的操作,反序列化後得到的是默認值。


13:java IO/NIO

所有的系統IO操作都分爲兩個階段:等待就緒和操作。具體來說,讀函數分爲等待系統可讀和真正的讀,寫函數分爲等待網卡可寫和真正的寫。其中等待就緒的阻塞是不使用cpu的,是在空等。而真正的讀操作是需要cpu的,但是這個過程非常快,可以理解爲不耗時。等待就緒劃分爲阻塞和非阻塞(等待就緒的時候是否可以提前返回),操作劃分爲同步和異步(到就緒後進行實際的操作,若必須是自己去讀則是同步,別人讀好送入內核再通知他是異步)。常見的IO模型對比

同步阻塞IO模型(BIO):服務器實現模式爲一個連接一個線程,即客戶端有連接請求時服務器端就需要啓動一個線程進行處理,如果這個連接不做任何事情會造成不必要的線程開銷,當然可以通過線程池機制改善。 在讀寫數據的過程中會發生阻塞現象。當用戶線程發出IO請求後,內核會檢查數據是否就緒,如果沒有就緒就等待數據就緒,而用戶線程就會處於阻塞狀態。當數據就緒後,內核會將數據拷貝到用戶線程,並返回結果給用戶線程,用戶線程纔會解除block狀態。典型的阻塞IO模型例子就是data=socket.read(),如果數據沒有就緒,則會一直阻塞在read方法。

同步非阻塞IO模型:當用戶線程發起有個read操作後,並不需要等待,而是馬上得到一個結果,如果結果是一個error時,它知道數據還沒有準備好,於是它可以再次發送read操作。一旦內核中的數據準備好了,並且又再次收到了用戶線程的請求,那麼它馬上將數據拷貝到用戶線程 ,然後返回。所以事實上在非阻塞IO模型中,用戶線程需要不斷的詢問內核數據是否準備就緒,也就是非阻塞IO不會交出cpu,而會一直佔用cpu

多路複用IO模型(NIO):服務器實現模式爲一個請求一個線程,即客戶端發送的連接請求都會註冊到多路複用器上,多路複用器輪詢到連接有I/O請求時才啓動一個線程進行處理。在多路複用IO模型中,會有一個線程不斷去輪詢多個socket的狀態,只有當socket真正有讀寫事件時,才真正的調用實際的IO讀寫操作。在該模型中,只需要使用一個線程就可以管理多個socket,系統不需要建立新的進程或者線程,也不必要去維護這些進程和線程,並且只有真正有socket讀寫事件時,纔會使用IO資源,所以大大減少了資源佔用。該模型比非阻塞模型高效的原因是在非阻塞IO中,不斷詢問socket狀態是通過用戶線程進行的,而在多路複用IO中,輪詢操作是在內核進行的,效率比用戶線程高很多。另外多路複用IO模型是採用輪詢的方式檢測是否有事件到達,並且對到達的事件逐一進行響應。因此該模型的一個缺點就是一旦事件響應體很大,就會導致後續的事件遲遲得不到處理。

NIO和BIO的區別:

NIO處理流程:面向緩衝區,讀取和寫入都必須先通過緩衝區,而原始IO是面向流

通道:對原始IO流的模擬,不過這是雙向的,可讀可寫可讀寫,而流是單向的

NioSocket的服務端具體處理過程:(客戶端則是SocketChannel)

1:創建ServerSocketChannel並設置相應的參數。

2創建Selector並調用ServerSocketChannel的registe方法將自己註冊到ServerSocketChannel。其中可以通過指定register方法的第二個參數選擇特定的操作(包括請求操作accept,連接操作connect,讀操作read和寫操作write)

3:調用Selector的select方法等待請求

4:Selector接收到請求後使用selectedKeys返回SelectionKey集合。這一步集合的返回內容,Selector可以根據第二步設置的特定操作進行過濾,只保留符合的

5:使用SelectionKey獲取channel,selector,和操作類型進行具體操作。

6:具體操作一般是由一個內部類Handler處理。Handler的處理過程用到了buffer,buffer是專門用於存儲數據,有4個屬性重要。Capacity:容量,limit:可以使用的上限,position:當前所操作元素所在的索引位置,mark:暫時保存position值

Select,poll,epoll的區別和聯繫:

select,poll,epoll都是IO多路複用的機制。I/O多路複用就通過一種機制,可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應的讀寫操作。但select,poll,epoll本質上都是同步I/O,因爲他們都需要在讀寫事件就緒後自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需自己負責進行讀寫,異步I/O的實現會負責把數據從內核拷貝到用戶空間。

select的缺點:

1:單個進程能夠監視的文件描述符的數量存在最大限制,通常是1024,當然可以更改數量,但由於select採用輪詢的方式掃描文件描述符,文件描述符數量越多,性能越差;(在linux內核頭文件中,有這樣的定義:#define __FD_SETSIZE    1024)

2:內核,用戶空間內存拷貝問題,select需要複製大量的句柄數據結構,產生巨大的開銷;即每次調用select的時候都會將fd文件從用戶態切換到核心態。

3:select返回的是含有整個句柄的數組,應用程序需要遍歷整個數組才能發現哪些句柄發生了事件;

4:select的觸發方式是水平觸發,應用程序如果沒有完成對一個已經就緒的文件描述符進行IO操作,那麼之後每次select調用還是會將這些文件描述符通知進程。

Poll的缺點:

相比select模型,poll使用鏈表保存文件描述符,因此沒有了監視文件數量的限制,但其他三個缺點依然存在。

Epoll:

epoll是在2.6內核中提出的,是之前的select和poll的增強版本。相對於select和poll來說,epoll更加靈活,沒有描述符限制。epoll使用一個文件描述符管理多個描述符,將用戶關係的文件描述符的事件存放到內核的一個事件表中,這樣在用戶空間和內核空間的copy只需一次。

Epoll工作模式

epoll對文件描述符的操作有兩種模式:LT(level trigger)和ET(edge trigger)。LT模式是默認模式,LT模式與ET模式的區別如下:

LT模式:當epoll_wait檢測到描述符事件發生並將此事件通知應用程序,應用程序可以不立即處理該事件。下次調用epoll_wait時,會再次響應應用程序並通知此事件。

ET模式:當epoll_wait檢測到描述符事件發生並將此事件通知應用程序,應用程序必須立即處理該事件。如果不處理,下次調用epoll_wait時,不會再次響應應用程序並通知此事件。

ET模式在很大程度上減少了epoll事件被重複觸發的次數,因此效率要比LT模式高。epoll工作在ET模式的時候,必須使用非阻塞套接口,以避免由於一個文件句柄的阻塞讀/阻塞寫操作把處理多個文件描述符的任務餓死。

Epoll實現流程:

第一步:epoll_create()系統調用,創建一個epoll句柄。此調用返回一個句柄,之後所有的使用都依靠這個句柄來標識。創建紅黑樹,用於存儲epoll_ctl傳遞來的socket,創建鏈表list,用於存儲準備就緒事件。

第二步:epoll_ctl()系統調用,註冊要監聽的事件類型。通過此調用向紅黑樹中添加、刪除、修改感興趣的事件,若紅黑樹中存在則直接返回,不存在添加到樹上,返回0標識成功,返回-1表示失敗註冊回調函數,當事件就緒時添加到list中。

第三步:epoll_wait()系統調用,等待事件的發生。通過此調用收集在epoll監控中已經發生的事件。若list中有數據直接返回,無數據則sleep,等到timeout之後即使鏈表中還是沒有元素,此時依舊返回。

異步非阻塞IO模型(AIO):服務器實現模式爲一個有效請求一個線程,客戶端的I/O請求都是由OS先完成了再通知服務器應用去啓動線程進行處理 。只需要先發起一個請求,當接收到內核返回的成功信號時表示IO操作已經完成,可以直接使用數據了。也就是說在這個模型中用戶發起read操作之後,立刻就可以開始做其他事,用戶線程完全不需要知道實際的IO操作是怎樣完成的。但是這個模型需要操作系統底層的支持。


14:零拷貝

定義:CPU不執行拷貝數據從一個存儲區域到另一個存儲區域的任務,通常用於網絡傳輸一個文件時候以減少CPU週期和內存帶寬

優點:減少不必要的cpu拷貝減少用戶空間和操作系統空間的上下文切換

實現:依賴於操作系統。和java本身沒有關係

傳統的I/O(4次複製和4次上下文的切換)

1:JVM發出read() 系統調用。

2:OS上下文切換到內核模式(第一次上下文切換)並將數據讀取到內核空間緩衝區。(第一次拷貝:hardware ----> kernel buffer)

3:OS內核然後將數據複製到用戶空間緩衝區(第二次拷貝: kernel buffer ——> user buffer),然後read系統調用返回。而系統調用的返回又會導致一次內核空間到用戶空間的上下文切換(第二次上下文切換)。

4:JVM處理代碼邏輯併發送write()系統調用。

5:OS上下文切換到內核模式(第三次上下文切換)並從用戶空間緩衝區複製數據到內核空間緩衝區(第三次拷貝: user buffer ——> kernel buffer)。

6:write系統調用返回,導致內核空間到用戶空間的再次上下文切換(第四次上下文切換)。將內核空間緩衝區中的數據寫到hardware(第四次拷貝: kernel buffer ——> hardware)。

通過sendfile實現零拷貝(3次複製和2次上下文的切換)

1:發出sendfile系統調用,導致用戶空間到內核空間的上下文切換(第一次上下文切換)。通過DMA將磁盤文件中的內容拷貝到內核空間緩衝區中(第一次拷貝: hard driver ——> kernel buffer)。

2:然後再將數據從內核空間緩衝區拷貝到內核中與socket相關的緩衝區中(第二次拷貝: kernel buffer ——> socket buffer)。

3:sendfile系統調用返回,導致內核空間到用戶空間的上下文切換(第二次上下文切換)。通過DMA引擎將內核空間socket緩衝區中的數據傳遞到協議引擎(第三次拷貝: socket buffer ——> protocol engine)。

帶有DMA收集功能的sendfile實現零拷貝(2次複製和2次上下文的切換)

1:發出sendfile系統調用,導致用戶空間到內核空間的上下文切換(第一次上下文切換)。通過DMA引擎將磁盤文件中的內容拷貝到內核空間緩衝區中(第一次拷貝: hard drive ——> kernel buffer)。

2:沒有數據拷貝到socket緩衝區。取而代之的是隻有相應的描述符信息會被拷貝到相應的socket緩衝區當中。該描述符包含了兩方面的信息:a)kernel buffer的內存地址;b)kernel buffer的偏移量。

3:sendfile系統調用返回,導致內核空間到用戶空間的上下文切換(第二次上下文切換)。DMA gather copy根據socket緩衝區中描述符提供的位置和偏移量信息直接將內核空間緩衝區中的數據拷貝到協議引擎上(第二次拷貝: kernel buffer ——> protocol engine),這樣就避免了最後一次CPU數據拷貝。

通過mmap實現零拷貝(3次複製和4次上下文的切換)

1:發出mmap系統調用,導致用戶空間到內核空間的上下文切換(第一次上下文切換)。通過DMA引擎將磁盤文件中的內容拷貝到內核空間緩衝區中(第一次拷貝: hard drive ——> kernel buffer)。

2:mmap系統調用返回,導致內核空間到用戶空間的上下文切換(第二次上下文切換)。接着用戶空間和內核空間共享這個緩衝區,而不需要將數據從內核空間拷貝到用戶空間。因爲用戶空間和內核空間共享了這個緩衝區數據,所以用戶空間就可以像在操作自己緩衝區中數據一般操作這個由內核空間共享的緩衝區數據。

3:發出write系統調用,導致用戶空間到內核空間的上下文切換(第三次上下文切換)。將數據從內核空間緩衝區拷貝到內核空間socket相關聯的緩衝區(第二次拷貝: kernel buffer ——> socket buffer)。

4:write系統調用返回,導致內核空間到用戶空間的上下文切換(第四次上下文切換)。通過DMA引擎將內核空間socket緩衝區中的數據傳遞到協議引擎(第三次拷貝: socket buffer ——> protocol engine)


15:自動拆箱和裝箱(享元模式)

Integer實際存儲的是對象的引用,執行new的integer對象,int是直接存儲數據值

1:兩個new出來的integer==比較永遠false:因爲new生成的是兩個對象,其內存地址不同

2:integer變量和int變量比較時,只要兩個變量的值是相等的,則結果爲true(因爲包裝類integer和基本數據類型int比較時,java會自動拆包爲int。實質就是兩個int變量比較)

3:非new出來的integer變量和new出來的integer變量==的結果是false。因爲非new出來的integer變量指向的是常量池中的地址,而new出來的變量指向的是堆中的地址

4:兩個非new出來的integer變量,只要範圍在-128-127之間,則==的結果是true。因爲指向的都是常量池中的地址,在第一次引用的時候會進行緩存,第二次再使用的時候指向的就是一個地址,而超過這個範圍,不會進行緩存,相當於是重新new。


16:常見問題

1:爲啥java不支持多繼承

1)第一個原因是圍繞鑽石形繼承問題產生的歧義,考慮一個類 A 有 foo() 方法, 然後 B 和 C 派生自 A, 並且有自己的 foo() 實現,現在 D 類使用多個繼承派生自 B 和C,如果我們只引用 foo(), 編譯器將無法決定它應該調用哪個 foo()。這也稱爲 Diamond 問題,因爲這個繼承方案的結構類似於菱形,見下圖:

  A foo()    
               / \    
             /     \    
 foo() B     C foo()    
             \     /    
               \ /    
               D  foo()

即使我們刪除鑽石的頂部 A 類並允許多重繼承,我們也將看到這個問題含糊性的一面。如果你把這個理由告訴面試官,他會問爲什麼 C++ 可以支持多重繼承而 Java不行。嗯,在這種情況下,我會試着向他解釋我下面給出的第二個原因,它不是因爲技術難度, 而是更多的可維護和更清晰的設計是驅動因素, 雖然這隻能由 Java 言語設計師確認,我們只是推測。維基百科鏈接有一些很好的解釋,說明在使用多重繼承時,由於鑽石問題,不同的語言地址問題是如何產生的。

2)對我來說第二個也是更有說服力的理由是,多重繼承確實使設計複雜化並在轉換、構造函數鏈接等過程中產生問題。假設你需要多重繼承的情況並不多,簡單起見,明智的決定是省略它。此外,Java 可以通過使用接口支持單繼承來避免這種歧義。由於接口只有方法聲明而且沒有提供任何實現,因此只有一個特定方法的實現,因此不會有任何歧義

2:爲啥等待通知定義在object中而不是thread中?

1)每個對象都可上鎖,這是在 Object 類而不是 Thread 類中聲明 wait 和 notify 的一個原因。

2) Java 是基於 Hoare 的監視器的思想。在Java中,所有對象都有一個監視器。

線程在監視器上等待,爲執行等待,需要2個參數:一個線程,一個監視器(任何對象)

在 Java 設計中,線程不能被指定,它總是運行當前代碼的線程。但是,我們可以指定監視器(這是我們稱之爲等待的對象)。這是一個很好的設計,因爲如果我們可以讓任何其他線程在所需的監視器上等待,這將導致“入侵”,導致在設計併發程序時會遇到困難。請記住,在 Java 中,所有在另一個線程的執行中侵入的操作都被棄用了(例如 stop 方法)。

3:爲什麼 char 數組比 Java 中的 String 更適合存儲密碼?

1)由於字符串在 Java 中是不可變的,如果你將密碼存儲爲純文本,它將在內存中可用,直到垃圾收集器清除它. 並且爲了可重用性,會存在 String 在字符串池中, 它很可能會保留在內存中持續很長時間,從而構成安全威脅。由於任何有權訪問內存轉儲的人都可以以明文形式找到密碼,這是另一個原因,你應該始終使用加密密碼而不是純文本。由於字符串是不可變的,所以不能更改字符串的內容,因爲任何更改都會產生新的字符串,而如果你使用char[],你就可以將所有元素設置爲空白或零。因此,在字符數組中存儲密碼可以明顯降低竊取密碼的安全風險。

2)使用 String 時,總是存在在日誌文件或控制檯中打印純文本的風險,但如果使用 Array,則不會打印數組的內容而是打印其內存位置。雖然不是一個真正的原因,但仍然有道理。

4:如何實現多繼承

  1. 多層繼承:class A,class B extends A,class C extends B
  2. 內部類:class A{class B{},class C{}}
  3. 接口:

具體見https://blog.csdn.net/weixin_42617262/article/details/85344819

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