日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

《Java 后端面试经》Java 基础篇

發布時間:2024/3/7 java 27 豆豆
生活随笔 收集整理的這篇文章主要介紹了 《Java 后端面试经》Java 基础篇 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

《Java 后端面試經》專欄文章索引:
《Java 后端面試經》Java 基礎篇
《Java 后端面試經》Java EE 篇
《Java 后端面試經》數據庫篇
《Java 后端面試經》多線程與并發編程篇
《Java 后端面試經》JVM 篇
《Java 后端面試經》操作系統篇
《Java 后端面試經》Linux 篇
《Java 后端面試經》設計模式篇
《Java 后端面試經》計算機網絡篇
《Java 后端面試經》微服務篇

《Java 后端面試經》 Java 基礎篇

  • 面向對象
  • JDK、JRE 和 JVM 三者之間的區別
  • Java 創建對象有哪幾種方式?
  • 獲取一個類對象的幾種方式?
  • Java 中的自動類型轉換
  • final
    • 為什么局部內部類和匿名內部類只能訪問局部 final 變量?
  • String 相關
    • String、StringBuffer 和 StringBuilder 區別及使用場景
    • String str = new String("abc") 創建了幾個對象?
    • 為什么 String 類不可變?
    • String 源碼中如何計算 hashCode 值的?
      • 為什么選擇 31*h?
  • 重載和重寫的區別
  • 單例與 static 的區別
  • 談談接口和抽象類
  • hashCode() 與 equals()
    • hashCode() 介紹
    • 為什么要有 hashcode
    • hashCode() 和 equals() 的關系
    • 為什么要重寫 hashCode() 和 equals()
    • == 和 equals 的區別
    • 重寫 equals() 方法的原則
  • 集合
    • Collection 和 Collections 區別?
    • Java 中的集合框架有哪些?
    • Map 家族繼承實現關系
    • List 和 Set 的區別
    • ArrayList 和 LinkedList 區別
      • 說說 ArrayList 的擴容機制?
    • HashMap 的底層實現?擴容?是否線程安全?
      • HashMap 的擴容機制是怎樣的?
      • 線程安全性:
      • HashMap 擴容的時候為什么是 2 的 n 次冪?
      • HashMap 中的循環鏈表是如何產生的?
      • HashMap 為什么用紅黑樹而不用 B 樹?
      • HashMap 的 put 方法說一下
      • 追問3:HashMap源碼中在計算 hash 值的時候為什么要右移 16 位?
    • Java 中線程安全的集合有哪些?
    • ConcurrentHashMap 的底層實現,它為什么是線程安全的?
    • ConcurrentHashMap 是如何分段分組的?
    • HashMap 和 HashTable 的區別?
    • HashMap 和 TreeMap 的區別?
    • HashSet 的底層數據結構?
  • int 和 Integer 哪個會占用更多的內存?
  • Java 中 ++ 操作符是線程安全的嗎?
  • Java 中如何實現序列化,有什么意義?
    • 追問:Serializable 接口為什么需要定義 serialVersionUID 常量?
  • 什么是字節碼?采用字節碼的好處是什么?
  • 異常
    • Java 中的異常體系
      • 追問1:異常的處理方式?
    • throws 和 throw
  • Java 中的深拷貝和淺拷貝說一下?
    • 追問1:淺拷貝與深拷貝的特點是什么?
    • 值傳遞和引用傳遞的區別?
  • Java 中的基本數據類型有哪些?
  • 談談全局變量和局部變量的區別?
  • Java 中抽象類和接口中方法的默認訪問權限?
  • Object 類中的方法有哪些
  • IO
    • 字節流和字符流
    • 怎么使用流打開一個大文件?
    • BIO 和 NIO 的區別
    • 談談 NIO 的實現原理
    • Java 反射機制
    • Java 反射在實際項目中有哪些應用場景?
  • Java 的關鍵字、保留字
  • 闡述成員變量和局部變量的區別?
  • Java 對象初始化順序
  • Java 內部類
    • 為什么使用內部類?
    • 內部類分類
      • 成員內部類
      • 靜態內部類(static 修飾的內部類)
      • 局部內部類(其作用域僅限于方法內,方法外部無法訪問該內部類)
      • 匿名內部類
    • 思維導圖總結
  • 關于接口中的屬性和方法
  • Java 的體系結構
  • Java 中的訪問修飾符
    • 怎么獲取 private 修飾的變量
  • 談談對泛型的理解?
  • 介紹一下泛型擦除?

面向對象

面向對象編程與之相對應的是面向過程編程。

面向過程(Procedure Oriented 簡稱 PO):把事情拆分成一個個的方法和數據,然后按照一定的順序,執行完這些方法,等方法執行完了,事情就搞定了。(因為每個方法都可以看作一個過程,所以叫面向過程)。

面向對象(Object Oriented 簡稱 OO):面向對象會把事物抽象成對象的概念,先抽象出對象,然后給對象賦一些屬性和方法,然后讓每個對象去執行自己的方法,問題得到解決。

舉例:用洗衣機洗衣服,來看一下兩者的差別。

面向過程:

放衣服(方法)-> 加洗衣粉(方法)-> 加水(方法)-> 漂洗(方法)-> 清洗(方法)-> 甩干(方法).

面向對象:

new 出兩個對象 ”人“ 和 ”洗衣機“

”人“ 加入屬性和方法:放衣服(方法)、加洗衣粉(方法)、加水(方法)

”洗衣機“ 加入屬性和方法:漂洗(方法)、清洗(方法)、甩干(方法)

然后執行:
人.放衣服(方法)-> 人.加洗衣粉(方法)-> 人.加水(方法)-> 洗衣機.漂洗(方法)-> 洗衣機.清洗(方法)-> 洗衣機.甩干(方法)

優缺點對比

面向過程:
優點:性能比面向對象高,因為不需要實例化對象。
缺點:可維護性差(例如:洗衣服我不喜歡甩干、我洗衣服更喜歡用洗衣液)

面向對象:
優點:易維護、易復用、易擴展,由于面向對象有封裝、繼承、多態性的特點,可以設計出低耦合的系統,使系統更加靈活、更加易于維護。
缺點:性能比面向過程低。

面向對象編程的特性:封裝、繼承、多態。

封裝: 把客觀事物封裝成抽象的類,并且類可以把自己的數據和方法只讓可信的類或者對象操作,對不可信的信息隱藏。

繼承: 繼承可以使用現有類的所有功能,并在無需重新編寫原來的類的情況下對這些功能進行擴展。

多態: 一個類實例的相同方法在不同情形有不同的表現形式。

JDK、JRE 和 JVM 三者之間的區別

JDK: Java Development Kit,java 開發工具,提供給開發人員使用

JRE: Java Runtime Environment,java 運行時環境,提供給運行 java 程序的用戶使用

JVM: Java Virtual Machine,java 虛擬機,解釋 class 文件

Java 創建對象有哪幾種方式?

  • 使用 new 關鍵字調用對象的構造器。
  • 使用 Java 反射的 newInstance() 方法。User user = Class.forName("com.hzz.User").newInstance();
  • 使用 Object 類的 clone() 方法。
  • 使用對象流 ObjectInputStream 的 readObject() 方法讀取序列化對象。

獲取一個類對象的幾種方式?

  • 通過類對象的 getClass() 方法獲取,即 A.getClass()
  • 通過類的靜態成員表示,每個類都有隱含的靜態成員 class,即 A.class
  • 通過 Class 類的靜態方法 forName() 方法獲取,即 Class.forName("com.hzz.A")
  • Java 中的自動類型轉換

    自動類型轉換遵循下面的規則

    • 若參與運算的數據類型不同,則先轉換成同一類型,然后進行運算
    • 轉換按數據長度增加的方向進行,以保證精度不降低。例如 int 型和 long 型運算時,先把 int 型轉成 long 型后再進行運算
    • 所有的浮點運算都是以雙精度進行的,即使僅含 float 單精度量運算的表達式,也要先轉換成 double 型,再做運算
    • char 型和 short 型參與運算時,必須先轉換成 int 型

    final

    • 修飾類:表示類不可被繼承。final 類中的所有成員方法都會被隱式地指定為 final 方法。
    • 修飾方法:表示方法不可被子類重寫,但是可以重載。
    • 修飾變量:表示變量一旦被賦值就不可以更改它的值。如果是基本數據類型的變量,則其數值一旦在初始化之后便不能更改;如果是引用類型的變量,則在對其初始化之后便不能再讓其指向另一個對象。

    (1)修飾成員變量

    • 如果 final 修飾的是類變量,只能在靜態初始化塊中指定初始值或者聲明該類變量時指定初始值。
    • 如果 final 修飾的是成員變量,可以在非靜態初始化塊聲明該變量時或者構造器中指定初始值

    (2)修飾局部變量

    系統不會為局部變量進行初始化,局部變量必須由程序員顯式初始化。因此使用 final 修飾局部變量時,既可以在定義時指定默認值(后面的代碼不能對變量再賦值),也可以不指定默認值,而在后面的代碼中對 final 變量賦初始值(僅一次)。

    public class FinalVar {final static int a = 0; // 在聲明的時候需要賦值或者靜態代碼塊賦值/**static {a = 0;}*/final int b = 0; // 在聲明的時候需要賦值或者代碼塊中賦值或者構造器賦值/**{b = 0;}*/public static void main(String[] args) {final int localA; // 局部變量只聲明沒有初始化,不會報錯,與 final 無關localA = 0; // 在使用之前一定要賦值// localA = 1; 但是不允許第二次賦值} }

    (3)修飾基本類型數據和引用類型數據

    • 如果是基本數據類型的變量,則其數值一旦在初始化之后便不能修改
    • 如果是引用類型的變量,則在其初始化之后便不能再讓其指向另一個對象。但是引用的值是可變的。
    public class FinalReferenceTest {public static void main() {final int[] iArr = {1,2,3,4};iArr[2] = -3; // 合法iArr = null;final Person p = new Person(25);p.setAge(24); // 合法p = null; // 非法} }

    為什么局部內部類和匿名內部類只能訪問局部 final 變量?

    編譯之后會生成兩個 class 文件,Test.class 和 Test1.class

    public class Test {public static void main(String[] args) {}// 局部 final 變量 a, bpublic void test(final int b) {final int a = 10;// 匿名內部類new Thread() {public void run() {System.out.println(a);System.out.println(b);}}.start();}class OutClass {private int age = 12;public void outPrint(final int x) {class InClass {public void InPrint() {System.out.println(x);System.out.println(age);}}new InClass().InPrint();}} }

    首先需要知道一點的是:內部類和外部類是處于同一個級別的,內部類不會因為定義在方法中就會隨著方法的執行完畢就被銷毀。

    這里就會產生問題:當外部類的方法結束時,局部變量就會被銷毀了,但是內部類對象可能還存在(只有沒有人再引用它時,才會死亡)。這里就出現了一個矛盾:內部類對象訪問了一個不存在的變量。為了解決這個問題,就將局部變量復制了一份作為內部類的成員變量,這樣當局部變量死亡后,內部類仍然可以訪問它,實際訪問的是局部變量的 “copy”。這樣就好像延長了局部變量的生命周期。

    將局部變量復制為內部類的成員變量時,必須保證這兩個變量是一樣的,也就是如果我們在內部類中修改了成員變量,方法中的局部變量也得跟著改變,怎么解決問題?

    就將局部變量設置為 final, 對它初始化后,我就不讓你再去修改這個變量,就保證了內部類的成員變量和方法的局部變量的一致性。這實際上也是一種妥協,使得局部變量與內部類建立的拷貝保持一致。

    String 相關

    String、StringBuffer 和 StringBuilder 區別及使用場景

    • String 類底層使用 final 關鍵字修飾的字符數組來保存字符串,private final char value[],所以 String 對象是不可變的。
    • StringBuilder 與 StringBuffer 都繼承自 AbstractStringBuilder 類,在 AbstractStringBuilder 中也是使用字符數組保存字符串 char[] value 但是沒有用 final 關鍵字修飾,所以這兩種對象都是可變的。
    • StringBuffer 對方法加了同步鎖(synchronized)或者對調用的方法加了同步鎖,所以是線程安全。 StringBuilder 并沒有對方法進行加同步鎖,所以是非線程安全的。

    性能:StringBuilder > StringBuffer > String.

    場景:經常需要改變字符串內容時使用前面兩個。

  • 操作少量的數據: 適用 String.
  • 單線程操作字符串緩沖區下操作大量數據: 適用 StringBuilder.
  • 多線程操作字符串緩沖區下操作大量數據: 適用 StringBuffer.
  • 想要效率就優先使用 StringBuilder,多線程使用共享變量時使用 StringBuffer.

    String str = new String(“abc”) 創建了幾個對象?

    一個或者兩個對象。

    如果常量池中沒有 “abc”,則會在常量池中創建一個 String 對象 ”abc“,再在堆內存中創建一個對象。如果常量池中已經存在對象 “abc”,則不會重復創建,而是只在堆內存中創建一個對象。

    為什么 String 類不可變?

    之所以要把 String 類設計為不可變類,主要是出于安全和性能的考慮,可歸納為如下三點:

    • 字符串通常會用來存儲敏感信息(如賬號,密碼等),保證字符串 String 類的安全性就尤為重要了,如果字符串是可變的,容易被篡改,那我們就無法保證使用字符串進行操作時,它是安全的,很有可能出現 SQL 注入,訪問危險文件等操作。
    • 在多線程中,只有不變的對象和值是線程安全的,可以在多個線程中共享數據。由于 String 天然的不可變,當一個線程”修改“了字符串的值,只會產生一個新的字符串對象,不會對其他線程的訪問產生副作用,訪問的都是同樣的字符串數據,不需要任何同步操作。
    • 當字符串不可變時,字符串常量池才有意義。字符串常量池的出現,可以減少創建相同字面量的字符串,讓不同的引用指向池中同一個字符串,為運行時節約很多的堆內存。若字符串可變,字符串常量池失去意義,基于常量池的 String.intern() 方法也失效,每次創建新的字符串將在堆內開辟出新的空間,占據更多的內存。

    String 源碼中如何計算 hashCode 值的?

    public int hashCode() {int h = hash;if (h == 0 && value.length > 0) {char val[] = value;for (int i = 0; i < value.length; i++) {h = 31 * h + val[i];}hash = h;}return h; }

    其計算過程主要是對每一位進行遍歷,用當前位數的 ascll 碼值加上前面每位的累加和。

    為什么選擇 31*h?

    • 乘法運算可以被移位和減法運算取代,來獲取更好的性能:31 * i == (i << 5) - i,現代的 Java 虛擬機可以自動的完成這個優化。
    • 盡量減少 hash 沖突。

    選擇數字31是因為它是一個奇質數,如果選擇一個偶數會在乘法運算中產生溢出,導致數值信息丟失,因為乘二相當于移位運算。選擇質數的優勢并不是特別的明顯,但這是一個傳統。同時,數字31有一個很好的特性,即

    重載和重寫的區別

    重載和重寫都是多態的一種表現形式。

    重載:

    • 重載是在編譯期通過方法中形參的靜態類型確定調用方法版本的過程
    • 重載是多態在編譯期的表現形式
    • 重載的判定只有兩個條件(其他的條件都不能作為判定):1. 方法名相同。2. 形參列表不同。

    重寫:

    • 重寫在方法運行時,通過調用者的實際類型來確定調用的方法版本。(具體細說,就是子父類中的重寫方法在對應的 class 文件常量池的位置相同,一旦子類沒有重寫,那么子類的實例就會沿著這個位置往上找,直到找到父類的同名方法)。
    • 重寫只發生在可見的實例方法中
      1. 靜態方法不存在重寫,形式上的重寫只能說是隱藏。
      2. 私有方法也不存在重寫,父類中 private 的方法,子類中就算定義了,就是相當于一個新的方法。
      3. 靜態方法和實例方法不存在相互重寫。
    • 重寫滿足一個規則:兩同兩小一大
      1. 兩同:方法名和形參列表相同
      2. 兩小:重寫方法的返回值(引用類型)和拋出異常,要和被重寫方法的返回值(引用類型)和拋出異常相同或者是其子類。注意,一旦返回值是基本數據類型,那么重寫方法和被重寫方法必須相同,且不存在自動拆裝箱的問題。
      3. 一大:重寫方法的訪問修飾符大于等于被重寫方法的訪問修飾符

    單例與 static 的區別

  • static 在類加載時執行一次,其生命周期是隨著類方法的執行完成而結束,其不需要再 new 對象,因為在類加載時就 new 對象了,所以 static 有更好的性能。

  • 工具類適用于 static,因為有更好的訪問效率(和狀態有關的用單例模式,如游戲中全局的一些狀態和變量)。

  • 單例模式的靈活性更高,方法可以被重寫,因為靜態類都是靜態方法,所以不能被重寫。

  • 如果是一個非常重的對象,單例模式可以懶加載,靜態類就無法做到。

  • 談談接口和抽象類

    接口和抽象類是支持抽象類定義的兩種機制。

    相同點:

    • 都不能被實例化。
    • 接口的實現類或抽象類的子類都只有實現了接口或抽象類中的方法后才能實例化。

    不同點:

    • 接口只有定義,不能有方法的實現,但 java 1.8 中可以定義 default 方法體,而抽象類可以有定義與實現,方法可在抽象類中實現。
    • 接口強調特定功能的實現,而抽象類強調所屬關系。
    • 一個類可以實現多個接口,但一個類只能繼承一個抽象類。所以,使用接口可以間接地實現多重繼承。
    • 接口方法默認修飾符是 public,抽象方法可以有 public、protected 和 default 這些修
      飾符(抽象方法就是為了被重寫所以不能使用 private`關鍵字修飾!)。
    • 接口被用于常用的功能,便于日后維護和添加刪除,而抽象類更傾向于充當公共類的角色,不適用于日后重新對立面的代碼修改。從設計層面來說,抽象是對類的抽象,是一種模板設計,而接口是對行為的抽象,是一種行為的規范。

    hashCode() 與 equals()

    hashCode() 介紹

    • hashCode() 的作用是獲取哈希碼,它實際上是返回一個 int 整數,這個哈希碼的作用是確定該對象在哈希表中的索引位置。
    • hashCode() 定義在 JDK 的 Object.java 中,Java 中的任何類都包含有 hashCode() 函數。
    • 散列表存儲的是鍵值對(key-value),它的特點是:能根據 key 快速的檢索出對應的 value.

    為什么要有 hashcode

    以 ”HashSet“ 如何檢查重復” 為例子來說明為什么要有 hashcode

    • 對象加入 HashSet 時,HashSet 會先計算對象的 hashcode 值來判斷對象加入的位置,看該位置是否有值:

    • 如果沒有,HashSet 會假設對象沒有重復出現,但是如果發現有值,這時就會調用 equals() 方法來檢查兩個對象是否真的相同:

    • 如果兩者相同,HashSet 就不會讓其加入,操作失敗。如果不同的話,就會重新散列到其他位置,這樣就會大大減少了 equals 的次數,相應就大大提高了執行速度。

    • 兩個對象相等,則 hashcode 一定也是相同的。

    • 兩個對象相等,對兩個對象分別調用 equals 方法都返回 true.

    • 兩個對象的 hashcode 值相同,它們不一定是相等的。

    • 因此,如果 equals() 方法被覆蓋過,則 hashCode() 方法也必須被覆蓋。

    • hashCode() 的默認行為是對堆上的對象產生獨特值,如果沒有重寫 hashCode(),則該類的兩個對象無論如何都不會相等(即使這兩個對象指向相同的數據)

    hashCode() 和 equals() 的關系

    hashCode() 用于獲取哈希碼,eauqls() 用于比較兩個對象是否相等,它們應遵守如下規定:

    • 如果兩個對象相等,則它們必須有相同的哈希碼
    • 如果兩個對象有相同的哈希碼,則它們未必相等

    為什么要重寫 hashCode() 和 equals()

    • Object 類提供的 equals() 方法默認是用 == 來進行比較的,也就是說只有兩個對象是同一個對象時,才能返回相等的結果。
    • 而實際的業務中的需求通常是,若兩個不同的對象它們的內容是相同的,就認為它們相等。鑒于這種情況,Object 類中 equals() 方法的默認實現是沒有實用價值的,所以通常都要重寫。
    • 由于 hashCode() 與 equals() 具有聯動關系,所以 equals() 方法重寫時,通常也要將 hashCode() 進行重寫,使得這兩個方法始終滿足相關的約定。

    == 和 equals 的區別

    == 運算符:

    • 作用于基本數據類型時,是比較兩個數值是否相等
    • 作用于引用數據類型時,是比較兩個對象的內存地址是否相同,即判斷它們是否為同一個對象

    equals() 方法:

    • equals() 方法不能作用于基本數據類型的變量
    • 沒有重寫時,Object 默認以 == 來實現,即比較兩個對象的內存地址是否相同
    • 進行重寫后,一般會按照對象的內容來進行比較,若兩個對象內容相同則認為對象相等,否則認為對象不等

    例如,String 類中對 equals() 方法進行了重寫:

    Object

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

    String

    public boolean equals(Object object) {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; }

    上述代碼可以看出,String 類中被復寫的 equals() 方法其實是比較兩個字符串的內容。

    public class StringDemo {public static void main(String args[]) {String str1 = "Hello";String str2 = new String("hello");String str3 = str2;System.out.println(str1==str2); // falseSystem.out.println(str1==str3); // faslseSystem.out.println(str2==str3); // trueSystem.out.println(str1.equals(str2)); // trueSystem.out.println(str1.equals(str3)); // trueSystem.out.println(str2.equals(str3)); // true} }

    重寫 equals() 方法的原則

    • 自反性:對于任何非空參考值 x,x.equals(x) 應該返回 true
    • 對稱性:對于任何非空參考值 x 和 y,當且僅當 y.equals(x) 返回 true 時,x.equals(y) 才應返回 true
    • 傳遞性:對于 x,y 和 z 的任何非空引用值,如果 x.equals(x) 返回 true,而 y.equals(z) 返回 true,則 x.equals(z) 應該返回 true.
    • 一致性:對于任何非空引用值 x 和 y,只要未修改對象的 equals 比較中使用的信息,對x.equals(y) 的多次調用將始終返回 true 或始終返回 false.
    • 對于任何非 null 參考值 x,x.equals(null) 應該返回 false

    集合

    Collection 和 Collections 區別?

    Collection 是一個接口,它是 Set、List 等容器的父接口。

    public interface Collection<E> extends Iterable<E> {... } public interface Set<E> extends Collection<E> {... } public interface List<E> extends Collection<E> {.... }

    Collections 是一個工具類,提供了一系列的靜態方法來輔助容器操作,這些方法包括對容器的搜索、排序、線程安全化等等。

    Java 中的集合框架有哪些?

    Java 集合框架主要包括兩種類型的容器,一種是集合(Collection),存儲一個元素集合,另一種是圖(Map),存儲鍵/值對映射。


    Map 家族繼承實現關系

    Map 家族的繼承實現關系如下,注意一點就是頂層的 Map 接口與 Collection 接口是依賴關系:



    關于 key 和 value 能否為 null 的問題:

    Map 集合類KeyValue
    HashMap允許為 null允許為 null
    TreeMap不允許為 null允許為 null
    ConcurrentHashMap不允許為 null不允許為 null

    List 和 Set 的區別

    • List: 有序,按對象進入的順序保存對象,可重復,允許多個 null 元素對象,可以使用 iterator 取出所有元素,再逐一遍歷,還可以使用 get(int index) 方法獲取指定下標的元素
    • Set: 無序,不可重復,最多有一個 null 元素對象,取元素時只能用 iterator 接口取得所有元素,再逐一遍歷各個元素,并沒有提供下標訪問的方法

    ArrayList 和 LinkedList 區別

    理解版:

    • ArrayList: 基于Object數組實現的,連續內存存儲,適合隨機訪問。擴容機制:因為數組長度固定,超過長度存數據需要新建數組,然后將舊數組的數據拷貝到新數組,如果不是尾部插入數據還會涉及到元素的移動(往后復制一份,插入新元素),使用尾插法并指定初始容量可以極大提高性能,甚至超過 LinkedList (需要創建大量的 node 對象)
    • LinkedList: 基于鏈表,可以存儲在分散的內存中,適合做數據插入及刪除操作,不適合查詢,需要逐一遍歷。值得注意的是,LinkedList 沒有初始化大小,也沒有擴容機制,就是直接在前面或者后面添加就可以了。
    • 遍歷 LinkedList 必須使用 迭代器(iterator) 不能使用 for 循環,因為每次 for 循環體內部通過 get(i) 取得某一元素時都需要對 list 重新進行遍歷,性能消耗極大。
    • 另外不要試圖使用 indexOf 等返回元素索引,并利用其進行遍歷,使用 indexOf 對 list 進行了遍歷,當結果為空時會遍歷整個列表。

    速記版:

  • ArrayList 底層使用的是 Object[] 數組;LinkedList 底層使用的是雙向鏈表數據結構。

  • ArrayList 增刪慢、查詢快,線程不安全,對元素必須連續存儲。

  • LinkedList 增刪快,查詢慢,線程不安全,元素可以在內存中分散存儲。

  • 說說 ArrayList 的擴容機制?

    通過閱讀 ArrayList 的源碼我們可以發現當以無參數構造方法創建 ArrayList 時,實際上初始化賦值的是一個空數組。當真正對數組進行添加元素操作時,才真正分配容量。即向數組中添加第一個元素時,數組容量擴為 10. 當插入的元素個數大于當前容量時,就需要進行擴容了,**ArrayList 每次擴容之后容量都會變為原來的 1.5 倍。

    HashMap 的底層實現?擴容?是否線程安全?

    (1)HashMap 的數據結構:

    JDK 1.7 及之前的 HashMap 底層是數組和鏈表,采用頭插法。這種實現方案有一個缺點就是,當 hash 沖突嚴重時,在桶上形成的鏈表會變得越來越長,這樣在查詢時的效率就會越來越低,其時間復雜度為O(N).

    JDK 1.8 及以后 HashMap 底層是數組和鏈表(或紅黑樹),采用尾插法。當鏈表的存儲的數據個數大于等于 8 的時候,不再采用鏈表存儲,而采用了紅黑樹存儲結構。這么做主要是在查詢的時間復雜度上進行優化,鏈表的時間復雜度為 O(N),而紅黑樹一直是 O(logN),可以大大的提升查找性能。

    HashMap 的擴容機制是怎樣的?

  • 數組的初始容量為 16,而容量是以 2 的次方擴充的,一是為了提升性能使用足夠大的數組,二是為了能使用位運算代替取模運算,提高了運算效率
  • 數組是否需要擴充是通過負載因子判斷的,如果當前元素個數為數組容量的 0.75 時,就會擴充數組。這個 0.75 就是默認的負載因子,可由構造器傳入。我們也可以設置大于 1 的負載因子,這樣數組就不會擴充,犧牲性能,節省內存。
  • 為了解決碰撞問題,數組中的元素是單向鏈表類型。當鏈表長度到達一個閾值(8),會將鏈表轉換成紅黑樹提升性能。而當鏈表長度縮小到另一個閾值(7),又會將紅黑樹轉換回單向鏈表提高性能。
  • 檢查鏈表長度轉換成紅黑樹之前,還會先檢測當前數組長度是否到達一個閾值(64),如果沒有到達這個容量,會放棄轉換,先去擴充數組。
  • 線程安全性:

    HashMap 是線程不安全的,其主要體現:

  • 在 jdk1.7 中,在多線程環境下,HashMap 執行 put 操作時會引起死循環,因為多線程會導致 HashMap 的 Entry 鏈表形成環形數據結構,一旦形成環形數據結構, Entry 的 next 節點永遠不為空,就會產生死循環獲取 Entry.
  • 在 jdk1.8 中,在多線程環境下,會發生數據覆蓋的情況。
  • HashMap 擴容的時候為什么是 2 的 n 次冪?

    數組下標的計算方法是 (n - 1) & hash,取余(%)操作中如果除數是 2 的冪次,則等價于與其除數減一的與(&)操作(也就是說 hash%length==hash&(length-1) 的前提是 length 是 2 的 n 次方)。” 并且采用二進制位操作 &,相對于 % 能夠提高運算效率,這就解釋了 HashMap 的長度為什么是 2 的冪次方。

    HashMap 中的循環鏈表是如何產生的?

    • 在多線程的情況下,當重新調整 HashMap 大小的時候,就會存在條件競爭,因為如果兩個線程都發現 HashMap 需要重新調整大小了,它們會同時試著調整大小。
    • 在調整大小的過程中,存儲在鏈表中的元素的次序會反過來,因為移動到新的 bucket 位置的時候,HashMap 并不會將元素放在鏈表的尾部,而是放在頭部,這是為了避免尾部遍歷。如果條件競爭發生了,那么就會產生死循環了。

    HashMap 為什么用紅黑樹而不用 B 樹?

    • B/B+ 樹多用于外存上時,因此 B/B+ 也被稱為一個磁盤友好的數據結構。
    • HashMap 本來是數組+鏈表的形式,鏈表由于其查找慢的特點,所以需要被查找效率更高的樹結構來替換。如果用 B/B+ 樹的話,在數據量不是很多的情況下,數據都會“擠在”一個結點里面,這個時候遍歷效率就退化成了鏈表。

    HashMap 的 put 方法說一下

  • 根據 key 計算出來 hash 值,然后 hash&length-1 運算得出數組下標。

  • 如果數組下標元素為空,則將 key 和 value 封裝為 Entry 對象(JDK1 .7 是 Entry 對象,JDK 1.8 是 Node 對象)并放入該位置。

  • 如果數組下標位置元素不為空,則要分情況:

  • 如果是在 JDK 1.7,則首先會判斷是否需要擴容,如果要擴容就進行擴容,如果不需要擴容就生成 Entry 對象,并使用頭插法添加到當前鏈表中。

  • 如果是在 JDK 1.8 中,則會先判斷當前位置上的節點的類型,看是紅黑樹還是鏈表 Node:
    (a) 如果是紅黑樹 TreeNode,則將 key 和 value 封裝為一個紅黑樹節點并添加到紅黑樹中去,在這個過程中會判斷紅黑樹中是否存在當前 key,如果存在則更新 value.

    (b) 如果此位置上的 Node 對象是鏈表節點,則將 key 和 value 封裝為一個 Node 并通過尾插法插入到鏈表的最后位置去,因為是尾插法,所以需要遍歷鏈表,在遍歷過程中會判斷是否存在當前 key,如果存在則更新其 value,當遍歷完鏈表后,將新的 Node 插入到鏈表中,插入到鏈表后,會看當前鏈表的節點個數,如果大于等于 8,則會將鏈表轉為紅黑樹。

  • 將 key 和 value 封裝為 Node 插入到鏈表或紅黑樹后,再判斷是否需要擴容,如果需要擴容,就結束 put 方法。

  • 下圖是 HashMap 的 put 和 get 方法流程:


    源碼(JDK 1.8 根據 key 值計算 hash 值的方法)

    static final int hash(Object key) {int h;return (key == null) ? 0:(h = key.hashCode()) ^ (h >> 16); }

    源碼(JDK 1.8)根據 hash 值計算出對應數組的下標的代碼:

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {Node<K,V>[] tab;Node<K,V> p;int n, i;if ((tab = table) == null || (n = tab.length) == 0)n = (tab = resize()).length;if ((p = tab[i = (n - 1) & hash]) == null)tab[i] = newNode(hash, key, value, null);else {Node<K,V> e; K k;if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))e = p;else if (p instanceof TreeNode)e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);else {for (int binCount = 0; ; ++binCount) {if ((e = p.next) == null) {p.next = newNode(hash, key, value, null);if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1sttreeifyBin(tab, hash);break;}if (e.hash == hash &&((k = e.key) == key|| (key != null && key.equals(k))))break;p = e;}}if (e != null) { // existing mapping for keyV oldValue = e.value;if (!onlyIfAbsent || oldValue == null)e.value = value;afterNodeAccess(e);return oldValue;}}++modCount;if (++size > threshold)resize();afterNodeInsertion(evict);return null; }

    追問3:HashMap源碼中在計算 hash 值的時候為什么要右移 16 位?

    讓元素在 HashMap 中更加均勻的分布。


    Java 中線程安全的集合有哪些?

    Vector:相比 ArrayList 多了個同步化機制(線程安全)。
    Stack:棧,也是線程安全的,繼承于 Vector.
    HashTable:相比 HashMap 多了個線程安全。
    Enumeration:枚舉,相當于迭代器。
    ConcurrentHashMap:是一種高效但是線程安全的集合。

    ConcurrentHashMap 的底層實現,它為什么是線程安全的?

    • 首先將數據分為一段一段的存儲,然后給每一段數據配一把鎖,當一個線程占用鎖訪問其中一個段數據時,其他段的數據也能被其他線程訪問。
    • ConcurrentHashMap 是由 Segment 數組結構和 HashEntry 數組結構組成。Segment 實現了 ReentrantLock,所以 Segment 是一種可重入鎖,扮演鎖的角色,HashEntry 用于存儲鍵值對數據。
    static class Segment<K,V> extends ReentrantLock implements Serializable {}

    一個 ConcurrentHashMap 里包含一個 Segment 數組。Segment 的結構和 HashMap 類似,是一種數組和鏈表結構,一個 Segment 包含一個 HashEntry 數組,每個 HashEntry 是一個鏈表結構的元素,每個 Segment 守護著一個 HashEntry 數組里的元素,當對 HashEntry 數組的數據進行修改時,必須首先獲得對應的 Segment 的鎖。

    在 JDK 1.8 中摒棄了 Segment 的概念,而是直接用 Node 數組+鏈表+紅黑樹的數據結構來實現,并發控制使用 synchronized 和 CAS 來操作,整個看起來就像是優化過且線程安全的 HashMap,雖然在 JDK1.8 中還能看到 Segment 的數據結構,但是已經簡化了屬性,只是為了兼容舊版本。

    ConcurrentHashMap 是如何分段分組的?

    Segment 的 get 操作實現非常簡單和高效,先經過一次散列,然后使用這個散列值通過散列運算定位到 Segment,再通過散列算法定位到元素。get 操作的高效之處在于整個 get 過程都不需要加鎖,除非讀到空的值才會加鎖重讀,原因就是將使用的共享變量定義成 volatile 類型。

    當執行 put 操作時,會經歷兩個步驟:

    • 判斷是否需要擴容
    • 定位到添加元素的位置,將其放入 HashEntry 數組中

    插入過程會進行第一次 key 的 hash 來定位 Segment 的位置,如果該 Segment 還沒有初始化,即通過 CAS 操作進行賦值,然后進行第二次 hash 操作,找到相應的 HashEntry 的位置,這里會利用繼承過來的鎖的特性,在將數據插入指定的 HashEntry 位置時(尾插法),會通過繼承 ReentrantLock 的 tryLock() 方法嘗試去獲取鎖,如果獲取成功就直接插入相應的位置,如果已經有線程獲取該 Segment 的鎖,那當前線程會以自旋的方式去繼續的調用 tryLock() 方法去獲取鎖,超過指定次數就掛起,等待喚醒。

    HashMap 和 HashTable 的區別?

    1、 線程是否安全: HashMap 是非線程安全的,HashTable 是線程安全的,因為 HashTable 內部的方法基本都經過 synchronized 修飾。(如果你要保證線程安全的話就使用ConcurrentHashMap)。

    2、 對 Null key 和 Null value 的支持HashMap 可以存儲 null 的 key 和 value,但 null 作為鍵只能有一個,null 作為值可以有多個。HashTable 不允許有 null 鍵和 null 值,否則會拋出 NullPointerException.

    static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }

    可以看到,當 key 為 null 時,它的 hashcode 返回值為 0,并不報錯。

    HashTable 源碼:

    public synchronized V put(K key, V value) {// Make sure the value is not nullif (value == null) {throw new NullPointerException();}// Makes sure the key is not already in the hashtable.Entry<?,?> tab[] = table;int hash = key.hashCode();int index = (hash & 0x7FFFFFFF) % tab.length;@SuppressWarnings("unchecked")Entry<K,V> entry = (Entry<K,V>)tab[index];for(; entry != null ; entry = entry.next) {if ((entry.hash == hash) && entry.key.equals(key)) {V old = entry.value;entry.value = value;return old;}}addEntry(hash, key, value, index);return null; }

    可以看到,當 value 為 null 時,拋出 NullPointerException;當 key 為 null 時,在調用 hashCode() 方法時,也會拋出 NullPointerException.

    3、初始容量大小和每次擴充容量大小的不同

    • 創建時如果不指定容量初始值,HashTable 默認的初始大小為 11,之后每次擴充,容量變為原來的 2n+1. HashMap 默認的初始化大小為 16,之后每次擴充,容量變為原來的 2 倍。
    • 創建時如果給定了容量初始值,那么 HashTable 會直接使用你給定的大小,而 HashMap 會將其擴充為 2 的冪次方大小(HashMap 中的 tableSizeFor() 方法保證,下面給出了源代碼)。例如,指定初始容量為20,實際容量會變成32

    HashMap 帶有初始容量的構造函數源碼如下:

    public HashMap(int initialCapacity, float loadFactor) {if (initialCapacity < 0)throw new IllegalArgumentException("Illegal initial capacity: " +initialCapacity);if (initialCapacity > MAXIMUM_CAPACITY)initialCapacity = MAXIMUM_CAPACITY;if (loadFactor <= 0 || Float.isNaN(loadFactor))throw new IllegalArgumentException("Illegal load factor: " +loadFactor);this.loadFactor = loadFactor;this.threshold = tableSizeFor(initialCapacity); }

    tableSizeFor 方法源碼:

    /*** Returns a power of two size for the given target capacity.*/ static final int tableSizeFor(int cap) {int n = cap - 1;n |= n >>> 1;n |= n >>> 2;n |= n >>> 4;n |= n >>> 8;n |= n >>> 16;return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }

    4、 底層數據結構: JDK 1.8 以后的 HashMap 在解決哈希沖突時有了較大的變化,當鏈表長度大于閾值(默認為 8)(將鏈表轉換成紅黑樹前會判斷,如果當前數組的長度小于 64,那么會選擇先進行數組擴容,而不是轉換為紅黑樹)時,將鏈表轉化為紅黑樹,以減少搜索時間。HashTable 沒有這樣的機制。

    5、效率: 因為線程安全的問題,HashMap 要比 HashTable 效率高一點。另外,HashTable基本被淘汰,不要在代碼中使用它。

    HashMap 和 TreeMap 的區別?

  • HashMap 是通過 hash 值進行快速查找的,HashMap 中的元素是沒有順序的。TreeMap 中所有的元素都是有某一固定順序的,如果需要得到一個有序的結果,就應該使用 TreeMap.
  • HashMap 和 TreeMap 都是線程不安全的。
  • HashMap 繼承 AbstractMap 類,覆蓋了 hashcode() 和 equals() 方法,以確保兩個相等的映射返回相同的哈希值。TreeMap 繼承 SortedMap 類,它保持鍵的有序順序。
  • HashMap 基于 hash 表實現的,使用 HashMap 要求添加的鍵類明確定義了 hashcode() 和 equals() (可以重寫該方法),為了優化 HashMap 的空間使用,可以調優初始容量和負載因子。TreeMap 基于紅黑樹實現的,TreeMap 就沒有調優選項,因為紅黑樹總是處于平衡的狀態。
  • HashMap 適用于 Map 插入,刪除,定位元素。TreeMap 適用于按自然順序或自定義順序遍歷鍵(key).
  • HashSet 的底層數據結構?

    • HashSet 是基于 HashMap 實現的,默認構造函數是構建一個初始容量為 16,負載因子為 0.75 的 HashMap.
    • 它封裝了一個 HashMap 對象來存儲所有的集合元素,所有放入 HashSet 中的集合元素實際上由 HashMap 的 key 來保存,而 HashMap 的 value 則存儲了一個 PRESENT,它是一個靜態的 Object 對象。
    private transient HashMap<E,Object> map;// Dummy value to associate with an Object in the backing Map private static final Object PRESENT = new Object();public boolean add(E e) {return map.put(e, PRESENT)==null; }

    int 和 Integer 哪個會占用更多的內存?

    Integer 對象會占用更多的內存,Integer 是一個對象,需要存儲對象的元數據,但是 int 是一個基本數據類型的數據,所以占用的空間更少。

    int 本身沒有空值,定義出來時候初始值為 0,但是在數據庫操作的時候,有可能數據的值是空的,因此封裝為 Integer,它允許有 null 值。

    Java 中 ++ 操作符是線程安全的嗎?

    不是線程安全的操作,它涉及多個指令,如讀取變量值,增加,然后存儲回內存,這個過程可能出現多線程交錯從而導致值的不正確。

    Java 中如何實現序列化,有什么意義?

  • 為什么要序列化
    網絡傳輸的數據都必須是二進制數據,但是在 Java 中都是對象,是沒有辦法在網絡中進行傳輸的,所以就需要對 Java 對象進行序列化,而且這個要求這個轉換算法是可逆的,不然要是不可逆那就不知道傳輸過來的是什么。
  • Java 原生序列化
    只要讓類實現 Serializable 接口就行,序列化的具體實現是由 ObjectOutputStream 和 ObjectInputStream 實現的。
  • Java 序列化的缺點:1、序列化碼流太大。2、序列化效率低。一般建議使用第三方的 JSON、Hessian、ProtoBuf 等效率高的方式。

    追問:Serializable 接口為什么需要定義 serialVersionUID 常量?

    serialVersionUID 代表序列化的版本,通過定義類的序列化版本,在反序列化時,只要對象中所存的版本和當前類的版本一致,就允許做恢復數據的操作,否則將會拋出序列化版本不一致的錯誤。

    如果不定義序列化版本,在反序列化時可能出現沖突的情況,如:

  • 創建該類的實例,并將這個實例序列化,保存在磁盤上。
  • 升級這個類,例如增加、刪除、修改這個類的成員變量;
  • 反序列化該類的實例,即從磁盤上恢復修改之前保存的數據。
  • 在第 3 步恢復數據的時候,當前的類已經和序列化的數據的格式產生了沖突,可能會發生各種意想不到的問題。增加了序列化版本之后,在這種情況下則可以拋出異常,以提示這種矛盾的存在,提高數據的安全性。

    什么是字節碼?采用字節碼的好處是什么?

    Java 中的編譯器和解釋器:

    • Java 中引入了虛擬機的概念,即在機器和編譯程序之間加入了一層抽象的虛擬的機器。這臺虛擬的機器在任何平臺上都提供給編譯程序一個共同的接口。
    • 編譯程序只需要面向虛擬機,生成虛擬機能夠理解的代碼,然后由解釋器來將虛擬機代碼轉換為特定系統的機器碼執行。在 Java 中,這種虛擬機理解的代碼叫做字節碼(即擴展名為 .class 的文件),它不面向任何特定的處理器,只面向虛擬機。
    • 每一種平臺的解釋器是不同的,但是實現的虛擬機是相同的,Java 源程序經過編譯器編譯后變成字節碼,字節碼由虛擬機解釋執行,虛擬機將每一條要執行的字節碼送給解釋器,解釋器將其翻譯成特定機器上的機器碼,然后在特定的機器上運行。這也就解釋了 Java 的編譯與解釋并存的特點。

    Java 源代碼 ----> 編譯器 -----> jvm 可執行的 java 字節碼(即虛擬指令)-----> jvm -----> jvm 中的解釋器 -----> 機器可執行的二進制機器碼 -----> 程序運行

    采用字節碼的好處:

    • Java 語言通過字節碼的方式,在一定程度上解決了傳統解釋型語言執行效率的問題,同時又保留了解釋型語言可移植的特點。
    • Java 程序運行時比較高效,由于字節碼并不針對一種特定的機器,Java 程序無須重新編譯便可在多種不同的計算機上運行。

    異常

    Java 中的異常體系

    Java 中的所有異常都來自頂級父類 Throwable. Throwable 下有兩個子類 Error 和 Exception:

    • Error 是錯誤,一般是指與虛擬機相關的問題,如系統崩潰、虛擬機錯誤、動態鏈接失敗( OutOfMemoryError、StackOverFlowError、VirtualMachineError、AssertionError.)等,這種錯誤無法恢復或不可能捕獲,將導致應用程序中斷
    • Exception 不會導致程序停止,又分為兩個部分 RunTimeException 運行時異常和 CheckedException 檢查異常。
    • RunTimeException 常常發生在程序運行過程中,會導致程序當前線程執行失敗。CheckedException 常常發生在程序編譯過程中,會導致程序編譯不通過。

  • 粉紅色的是受檢查的異常(checked exceptions),其必須被 try{} catch 語句塊所捕獲,或者在方法簽名里通過 throws 子句聲明。受檢查的異常必須在編譯時被捕捉處理,命名為 CheckedException 是因為Java 編譯器要進行檢查,Java 虛擬機也要進行檢查,以確保這個規則得到遵守。
  • 綠色的異常是運行時異常 (Runtime Exceptions),需要程序員自己分析代碼決定是否捕獲和處理,比如空指針,被 0 除…
  • 而聲明為 Error 的,則屬于嚴重錯誤,如系統崩潰、虛擬機錯誤、動態鏈接失敗等,這些錯誤無法恢復或者不可能捕捉,將導致應用程序中斷,Error 不需要捕捉。
  • 追問1:異常的處理方式?

    異常處理方式有拋出異常和使用 try catch 語句塊捕獲異常兩種方式。

  • 拋出異常:遇到異常時不進行具體的處理,直接將異常拋給調用者,讓調用者自己根據情況處理。拋出異常的三種形式:throws、throw 和系統自動拋出異常。其中 throws 作用在方法上,用于定義方法可能拋出的異常;throw 作用在方法內,表示明確拋出一個異常。
  • 使用 try catch 捕獲并處理異常:使用 try catch 捕獲異常能夠有針對性的處理每種可能出現的異常,并在捕獲到異常后根據不同的情況做不同的處理。其使用過程比較簡單:用 try catch 語句塊將可能出現異常的代碼抱起來即可。
  • throws 和 throw

  • throws 出現在方法頭,throw 出現在方法體。
  • throws 表示出現異常的一種可能性,并不一定會發生異常;throw 則是拋出了異常,執行throw 則一定拋出了某種異常。
  • 兩者都是消極的異常處理方式,只是拋出或者可能拋出異常,是不會由函數處理,真正的處理異常由它的上層調用處理。
  • Java 中的深拷貝和淺拷貝說一下?

    深拷貝和淺拷貝都是對象拷貝。

    淺拷貝:按位拷貝對象,它會創建一個新對象,這個對象有著原始對象屬性值的一份精確拷貝。如果屬性是基本類型,拷貝的就是基本類型的值。如果屬性是內存地址(引用類型),拷貝的就是內存地址 ,因此如果其中一個對象改變了這個地址,就會影響到另一個對象。(淺拷貝僅僅復制所考慮的對象,而不復制它所引用的對象)。


    上圖: 兩個引用 student1 和 student2 指向不同的兩個對象,但是兩個引用 student1 和student2 中的兩個 teacher 引用指向的是同一個對象,所以說明是淺拷貝。

    深拷貝:在拷貝引用類型成員變量時,為引用類型的數據成員另辟了一個獨立的內存空間,實現真正內容上的拷貝。(深拷貝把要復制的對象所引用的對象都復制了一遍)


    上圖:兩個引用 student1 和 student2 指向不同的兩個對象,兩個引用 student1 和 student2 中的兩個 teacher 引用指向的是兩個對象,但對 teacher 對象的修改只能影響 student1 對象,所以說是深拷貝。

    淺拷貝:對基本數據類型進行值傳遞,對引用數據類型進行引用傳遞般的拷貝,此為淺拷貝。

    深拷貝:對基本數據類型進行值傳遞,對引用數據類型,創建一個新的對象,并復制其內容,此為深拷貝。

    追問1:淺拷貝與深拷貝的特點是什么?

    淺拷貝特點

  • 對于基本數據類型的成員對象,因為基礎數據類型是值傳遞的,所以是直接將屬性值賦值給新的對象。基礎類型的拷貝,其中一個對象修改該值,不會影響另外一個。
  • 對于引用類型,比如數組或者類對象,因為引用類型是引用傳遞,所以淺拷貝只是把內存地址賦值給了成員變量,它們指向了同一內存空間。改變其中一個,會對另外一個也產生影響。
  • 深拷貝特點

  • 對于基本數據類型的成員對象,因為基礎數據類型是值傳遞的,所以是直接將屬性值賦值給新的對象。基礎類型的拷貝,其中一個對象修改該值,不會影響另外一個(和淺拷貝一樣)。
  • 對于引用類型,比如數組或者類對象,深拷貝會新建一個對象空間,然后拷貝里面的內容,所以它們指向了不同的內存空間。改變其中一個,不會對另外一個也產生影響。
  • 對于有多層對象的,每個對象都需要實現 Cloneable 并重寫 clone() 方法,進而實現了對象的串行層層拷貝。
  • 深拷貝相比于淺拷貝速度較慢并且花銷較大。
  • 值傳遞和引用傳遞的區別?

    • 值傳遞:是指在調用函數時將實際參數復制一份傳遞到函數中,這樣在函數中如果對參數進行修改,將不會影響到實際參數。
    • 引用傳遞:是指在調用函數時將實際參數的地址傳遞到函數中,那么在函數中對參數所進行的修改,將影響到實際參數。

    Java 中傳遞引用數據類型的時候也是值傳遞,復制的是參數的引用(地址值),并不是引用指向的存在于堆內存中的實際對象。

    Java 中的基本數據類型有哪些?

    Java 中的四類八種基本數據類型:

    第一類:整數類型 byte(1 byte) short(2 byte) int(4 byte) long(8 byte)
    第二類:浮點型 float(4 byte) double(8 byte)
    第三類:布爾型 boolean(1 bit)
    第四類:字符型 char(2 byte)

    談談全局變量和局部變量的區別?

    Java 中沒有全局變量的說法,只有成員變量和局部變量,這里的成員變量就相當于 C 語言中的全局變量。

    成員變量和局部變量的區別

    • 成員變量是在類的范圍里定義的變量,局部變量是在方法中定義的變量。
    • 成員變量有默認初始值,局部變量沒有默認初始值。
    • 未被 static 修飾的成員變量叫實例變量,它存儲于對象所在的堆內存中,生命周期與對象相同;被 static 修飾的成員變量叫類變量,它存儲于方法區中,生命周期與當前類相同。局部變量存儲于棧內存中,作用的范圍結束,變量空間會自動的釋放

    Java 中抽象類和接口中方法的默認訪問權限?

    關于抽象類

    JDK 1.8 以前,抽象類的方法默認訪問權限為 protected.
    JDK 1.8 及以后,抽象類的方法默認訪問權限變為 default.

    關于接口

    JDK 1.8 以前,接口中的方法必須是 public 的。
    JDK 1.8 時,接口中的方法可以是 public 的,也可以是 default 的。
    JDK 1.9 時,接口中的方法可以是 private 的。

    Object 類中的方法有哪些

    Object 類中方法:

    • protected Object clone() 創建并返回此對象的一個副本。
    • boolean equals(Object obj) 指示其他某個對象是否與此對象“相等”。
    • protected void finalize() 當垃圾回收器確定不存在對該對象的更多引用時,由對象的垃圾回收器調用此方法。
    • class getClass() 返回此 Object 的運行時類。
    • int hashCode() 返回該對象的哈希碼值。
    • void notify() 喚醒在此對象監視器上等待的單個線程。
    • void notifyAll() 喚醒在此對象監視器上等待的所有線程。
    • String toString() 返回該對象的字符串表示。
    • void wait() 在其他線程調用此對象的 notify() 方法或 notifyAll() 方法前,導致當前線程等待。
    • void wait(long timeout) 在其他線程調用此對象的 notify() 方法或 notifyAll() 方法,或者超過指定的時間量前,導致當前線程等待。
    • void wait(long timeout, int nanos) 在其他線程調用此對象的 notify() 方法或 notifyAll() 方法,或者其他某個線程中斷當前線程,或者已超過某個實際時間量前,導致當前線程等待。

    IO

    字節流和字符流

    Java 的流操作分為字節流字符流兩種:

    字節流與字符流主要的區別是他們的處理方式:

    • 字節流是最基本的,所有的 InputStream 和 OutputStream 的子類都是,主要用在處理二進制數據,它是按字節來處理的。但實際中很多的數據是文本,又提出了字符流的概念,它是按虛擬機的編碼來處理,也就是要進行字符集的轉化。
    • 這兩個之間通過 InputStreamReader,OutputStreamWriter 來關聯,實際上是通過 byte[] 和String 來關聯。

    在實際開發中出現的漢字問題實際上都是在字符流和字節流之間轉化不統一而造成的:

    • 字節流---->字符流實際上就是 byte[] 轉化為 String 時,public String(byte bytes[], String charsetName) 有一個關鍵的參數字符集編碼,通常我們都省略了,那系統就用操作系統的 lang.
    • 字符流---->字節流實際上是 String 轉化為 byte[] 時,byte[] String.getBytes(String charsetName) 也是一樣的道理。至于 java.io 中還出現了許多其他的流,按主要是為了提高性能和使用方便,如 BufferedInputStream,PipedInputStream 等。

    需要知道的知識點:

    • 對于 GBK 編碼標準,英文占用 1 個字節,中文占用 2 個字節。
    • 對于 UTF-8 編碼標準,英文占用 1 個字節,中文占用 3 個字節。
    • 對于 Unicode 編碼標準,英文中文都是 2 個字節。這也是為什么叫做統一編碼(Unicode).

    怎么使用流打開一個大文件?

    打開大文件,應避免直接將文件中的數據全部讀取到內存中,可以采用分次讀取的方式:

  • 使用緩沖流。緩沖流內部維護了一個緩沖區,通過與緩沖區的交互,減少與設備的交互次數。使用緩沖輸入流時,它每次會讀取一批數據將緩沖區填滿,每次調用讀取方法并不是直接從設備取值,而是從緩沖區取值,當緩沖區為空時,它會再一次讀取數據,將緩沖區填滿。 使用緩沖輸出流時,每次調用寫入方法并不是直接寫入到設備,而是寫入緩沖區,當緩沖區填滿時它會自動刷入設備。
  • 使用 NIO. NIO 采用內存映射文件的方式來處理輸入/輸出,NIO 將文件或文件的一段區域映射到內存中,這樣就可以像訪問內存一樣來訪問文件了(這種方式模擬了操作系統上的虛擬內存的概念),通過這種方式來進行輸入/輸出比傳統的輸入/輸出要快得多。
  • BIO 和 NIO 的區別

    • BIO 以流的方式處理數據,而 NIO 以塊的方式處理數據,IO 塊比 IO 流效率更高
    • BIO 是阻塞的,NIO 是非阻塞的
    • BIO 基于字節流和字符流進行操作,NIO 基于通道(Channel)和緩存區(Buffer)進行操作,數據總是從通道讀取到緩存區中,或者從緩存區寫入到通道中

    談談 NIO 的實現原理

    Java 的 NIO 主要由三個核心部分組成:Channel、Buffer、Selector.

    • 數據可以從 Channel 讀到 Buffer 中,也可以從 Buffer 寫到 Channel 中。
    • Buffer 本質上是一塊可以寫入數據,然后可以從中讀取數據的內存。這塊內存被包裝成 NIO Buffer 對象,并提供了一組方法,用來方便的訪問該塊內存。Buffer 對象包含三個重要的屬性,分別是 capacity、position、limit,其中 position 和 limit 的含義取決于 Buffer 處在讀模式還是寫模式。但不管 Buffer 處在什么模式,capacity 的含義總是一樣的。
    • Selector 允許單線程處理多個 Channel,如果你的應用打開了多個連接(通道),但每個連接的流量都很低,使用 Selector 就會很方便。要使用 Selector,得向 Selector 注冊 Channel,然后調用它的 select() 方法。這個方法會一直阻塞到某個注冊的通道有事件就緒。一旦這個方法返回,線程就可以處理這些事件,事件例如有新連接進來,數據接收等。

    JDK 在 Linux 已經默認使用 epoll 方式,但是 JDK 的 epoll 采用的是水平觸發,所以 Netty 自4.0.16 起, Netty 為 Linux 通過 JNI 的方式提供了 native socket transport. Netty 重新實現了 epoll 機制:

    • 采用邊緣觸發方式
    • netty epoll transport 暴露了更多的 nio 沒有的配置參數,如 TCP_CORK, SO_REUSEADDR等等
    • C 代碼,更少 GC,更少 synchronized.

    Java 反射機制

    定義:所謂反射機制是指在程序運行的過程中,對任意一個類都能獲取其屬性和方法,并且對任意一個對象都能調用其任意的一個方法。

    反射的 API

    Java 中有些反射的 API,比較常用的是獲取屬性和方法。主要是在程序運行過程中動態的生成類、接口或對象等信息。

    • Class 類:用于獲取類的屬性、方法等信息。
    • Field 類:表示類的成員變量,用于獲取和設置類中的屬性值。
    • Method 類:表示類的方法,用于獲取方法的描述信息或者執行某個方法。
    • Constructor 類:表示類的構造方法。

    反射的步驟

  • 獲取類的 Class 對象,這是反射的核心!因為通過它可以獲取類的屬性和方法。

  • 調用 Class 對象所對應的類中定義的方法,這是反射的使用階段。

  • 使用反射 API 來獲取并調用類的屬性和方法等信息。

  • 普通的 Java 對象是通過 new 關鍵字把對應類的字節碼文件加載到內存,然后創建該對象的。反射是通過一個名為 Class 的特殊類,用 Class.forName("className"); 得到類的字節碼對象,然后用 newInstance() 方法在虛擬機內部構造這個對象(針對無參構造函數)。也就是說反射機制讓我們可以先拿到 Java 類對應的字節碼對象,然后動態的進行任何可能的操作,包括:

    • 在運行時判斷任意一個對象所屬的類
    • 在運行時構造任意一個類的對象
    • 在運行時判斷任意一個類所具有的成員變量和方法
    • 在運行時調用任意一個對象的方法

    這些都是反射的功能。使用反射的主要作用是方便程序的擴展。

    Java 反射在實際項目中有哪些應用場景?

    • 使用 JDBC 時,如果要創建數據庫的連接,則需要先通過反射機制加載數據庫的驅動程序
    • 多數框架都支持注解/XML 配置,從配置中解析出來的類是字符串,需要利用反射機制實例化
    • 面向切面編程 (AOP) 的實現方案,是在程序運行時創建目標對象的代理類,這必須由反射機制來實現

    Java 的關鍵字、保留字

    Java 中有 48 個關鍵字:


    2 個保留字:goto、const
    3 個直接量false、true、null 都不是關鍵字,叫做直接量!

    闡述成員變量和局部變量的區別?

    • 成員變量是在類的范圍里定義的變量,局部變量是在方法里定義的變量
    • 成員變量有默認初始值,局部變量沒有默認的初始值
    • 未被 static 修飾的成員變量也叫實例變量,它存儲于對象所在的堆內存中,生命周期與對象相同,被 static 修飾的成員變量叫類變量,存儲在方法區中,生命周期與當前類相同。局部變量存儲于棧內存中,作用的范圍結束,變量空間會自動的釋放。

    Java 對象初始化順序

    Java 對象初始化順序:

    父類靜態代碼塊,父類靜態成員變量(同級,按代碼順序執行)
    子類靜態代碼塊,子類靜態成員變量(同級,按代碼順序執行)
    父類普通代碼塊,父類普通成員變量(同級,按代碼順序執行)
    父類構造方法
    子類普通代碼塊,子類普通成員變量(同級,按代碼順序執行)
    子類構造方法

    注意點:

  • 靜態內容只在類加載時執行一次,之后不再執行。
  • 默認調用父類的無參構造方法,可以在子類構造方法中利用 super 指定調用父類的哪個構造方法。
  • Java 內部類

    為什么使用內部類?

    使用內部類最吸引人的原因是:每個內部類都能獨立地繼承一個接口的實現,所以無論外圍類是否已經繼承了某個接口的實現,對于內部類都沒有影響。

    使用內部類最大的優點就在于它能夠非常好的解決多重繼承的問題,使用內部類還能夠為我們帶來如下特性:

  • 內部類可以用多個實例,每個實例都有自己的狀態信息,并且與其他外圍對象的信息相互獨立。
  • 在單個外圍類中,可以讓多個內部類以不同的方式實現同一個接口,或者繼承同一個類。
  • 創建內部類對象的時刻并不依賴于外圍類對象的創建。
  • 內部類并沒有令人迷惑的 “is-a” 關系,它就是一個獨立的實體。
  • 內部類提供了更好的封裝,除了該外圍類,其他類都不能訪問。
  • 內部類分類

    成員內部類

    public class Outer{private int age = 99;String name = "Coco";public class Inner{String name = "Jayden";public void show(){System.out.println(Outer.this.name);System.out.println(name);System.out.println(age);}}public Inner getInnerClass(){return new Inner();}public static void main(String[] args){Outer o = new Outer();Inner in = o.new Inner();in.show();}}

    (1)Inner 類定義在 Outer 類的內部,相當于 Outer 類的一個成員變量的位置,Inner 類可以使用任意訪問控制符,如 public 、 protected 、 private 等。

    (2)Inner 類中定義的 show() 方法可以直接訪問 Outer 類中的數據而不受訪問控制符的影響,如直接訪問 Outer 類中的私有屬性 age.

    (3)定義了成員內部類后,必須使用外部類對象來創建內部類對象,而不能直接去 new 一個內部類對象, 即:內部類 對象名 = 外部類對象.new 內部類( );。

    (4)編譯上面的程序后,會發現產生了兩個 .class 文件: Outer.class, Outer$Inner.class{}

    (5)成員內部類中不能存在任何 static 的變量和方法,可以定義常量。

    • 因為非靜態內部類是要依賴于外部類的實例,而靜態變量和方法是不依賴于對象的,僅與類相關。即在加載靜態域時,根本沒有外部類,所在在非靜態內部類中不能定義靜態域或方法,編譯不通過,非靜態內部類的作用域是實例級別。
    • 常量是在編譯器就確定的,放到所謂的常量池了。

    注意:

  • 外部類是不能直接使用內部類的成員和方法的,可先創建內部類的對象,然后通過內部類的對象來訪問其成員變量和方法。
  • 如果外部類和內部類具有相同的成員變量或方法,內部類默認訪問自己的成員變量或方法,如果要訪問外部類的成員變量,可以使用 this 關鍵字,如: Outer.this.name.
  • 靜態內部類(static 修飾的內部類)

  • 靜態內部類不能直接訪問外部類的非靜態成員,但可以通過 new 外部類().成員的方式訪問。
  • 如果外部類的靜態成員與內部類的成員名稱相同,可通過“類名.靜態成員”訪問外部類的靜態成員;如果外部類的靜態成員與內部類的成員名稱不相同,則可通過“成員名”直接調用外部類的靜態成員。
  • 創建靜態內部類的對象時,不需要外部類的對象,可以直接創建 內部類 對象名 = new 內部類();
  • public class Outer{private int age = 99;static String name = "Coco";public static class Inner {String name = "Jayden";public void show(){System.out.println(Outer.name);System.out.println(name); }}public static void main(String[] args) {Inner i = new Inner();i.show();} }

    局部內部類(其作用域僅限于方法內,方法外部無法訪問該內部類)

    (1) 局部內部類就像是方法里面的一個局部變量一樣,是不能有 public、protected、private 以及 static 修飾符的。
    (2) 只能訪問方法中定義的 final 類型的局部變量,因為當方法被調用運行完畢之后,局部變量就已消亡了。但內部類對象可能還存在,直到沒有被引用時才會消亡。此時就會出現一種情況,就是內部類要訪問一個不存在的局部變量。
    ==> 使用 final 修飾符不僅會保持對象的引用不會改變,而且編譯器還會持續維護這個對象在回調方法中的生命周期。
    局部內部類并不是直接調用方法傳進來的參數,而是內部類將傳進來的參數通過自己的構造器備份到了自己的內部,自己內部的方法調用的實際是自己的屬性而不是外部類方法的參數。防止被篡改數據,而導致內部類得到的值不一致

    /*使用的形參為何要為 final在內部類中的屬性和外部方法的參數兩者從外表上看是同一個東西,但實際上卻不是,所以他們兩者是可以任意變化的,也就是說在內部類中我對屬性的改變并不會影響到外部的形參,而然這從程序員的角度來看這是不可行的,畢竟站在程序的角度來看這兩個根本就是同一個,如果內部類該變了,而外部方法的形參卻沒有改變這是難以理解和不可接受的,所以為了保持參數的一致性,就規定使用 final 來避免形參的不改變 */ public class Outer{public void Show(){final int a = 25;int b = 13;class Inner{int c = 2;public void print(){System.out.println("訪問外部類:" + a);System.out.println("訪問內部類:" + c);}}Inner i = new Inner();i.print();}public static void main(String[] args){Outer o = new Outer();o.show();} }

    (3) 在 JDK 1.8 版本之中,方法內部類中調用方法中的局部變量,可以不需要修飾為 final,匿名內部類也是一樣的,主要是 JDK8 之后增加了 Effectively final 功能。
    反編譯 jdk 1.8 編譯之后的 class 文件,發現內部類引用外部的局部變量都是 final 修飾的。

    匿名內部類

    (1) 匿名內部類是直接使用 new 來生成一個對象的引用。

    (2) 對于匿名內部類的使用它是存在一個缺陷的,就是它僅能被使用一次,創建匿名內部類時它會立即創建一個該類的實例,該類的定義會立即消失,所以匿名內部類是不能夠被重復使用。

    (3) 使用匿名內部類時,我們必須是繼承一個類或者實現一個接口,但是兩者不可兼得,同時也只能繼承一個類或者實現一個接口。

    (4) 匿名內部類中是不能定義構造函數的,匿名內部類中不能存在任何的靜態成員變量和靜態方法。

    (5) 匿名內部類中不能存在任何的靜態成員變量和靜態方法,匿名內部類不能是抽象的,它必須要實現繼承的類或者實現的接口的所有抽象方法。

    (6) 匿名內部類初始化:使用構造代碼塊!利用構造代碼塊能夠達到為匿名內部類創建一個構造器的效果。

    public class OuterClass {public InnerClass getInnerClass(final int num,String str2){return new InnerClass(){int number = num + 3;public int getNumber(){return number;}}; /* 注意:分號不能省 */}public static void main(String[] args) {OuterClass out = new OuterClass();InnerClass inner = out.getInnerClass(2, "chenssy");System.out.println(inner.getNumber());}}interface InnerClass {int getNumber();}

    思維導圖總結

    關于接口中的屬性和方法

    接口中的屬性在不提供修飾符修飾的情況下,會自動加上 public static final

    注意(在 jdk 1.8 的編譯器下可試):

  • 屬性不能用 private,protected,default 修飾,因為默認是 public
  • 如果屬性是基本數據類型,需要賦初始值,若是引用類型,也需要初始化,因為默認有 final 修飾,必須賦初始值
  • 接口中常規的來說不能夠定義方法體,所以無法通過 get 和 set 方法獲取屬性值,所以屬性不屬于對象,屬于類(接口),因為默認使用 static 修飾
  • Java 的體系結構

    Java 體系結構包括四個獨立但相關的技術:

    • Java 程序設計語言
    • Java.class 文件格式
    • Java 應用編程接口(API)
    • Java 虛擬機

    四者之間的關系:

    當我們編寫并運行一個 Java 程序時,就同時運用了這四種技術,用 Java 程序設計語言編寫源代碼,把它編譯成 Java.class 文件格式,然后再在 Java 虛擬機中運行 class 文件。當程序運行的時候,它通過調用 class 文件實現了 Java API 的方法來滿足程序的 Java API 調用。

    Java 中的訪問修飾符

    public:可以被所有其他類所訪問。

    private:只能被自己訪問和修改。

    protected:自身、子類及同一個包中類可以訪問。

    default(包訪問權限):同一包中的類可以訪問,聲明時沒有加修飾符,認為是 friendly.

    訪問權限控制從大到小依次為:public > protected > default(包訪問權限)>private

    怎么獲取 private 修飾的變量

    通過調用對象的 get() 方法。

    談談對泛型的理解?

    Java集合有個缺點把一個對象“丟進”集合里之后,集合就會“忘記”這個對象的數據類型,當再次取出該對象時,該對象的編譯類型就變成了 Object 類型(其運行時類型沒變)。Java 集合之所以被設計成這樣,是因為集合的設計者不知道我們會用集合來保存什么類型的對象,所以他們把集合設計成能保存任何類型的對象,只要求具有很好的通用性,但這樣做帶來如下兩個問題:

    • 集合對元素類型沒有任何限制,這樣可能引發一些問題。例如,想創建一個只能保存 Dog 對象的集合,但程序也可以輕易地將 Cat 對象“丟”進去,所以可能引發異常。
    • 由于把對象“丟進”集合時,集合丟失了對象的狀態信息,只知道它盛裝的是 Object,因此取出集合元素后通常還需要進行強制類型轉換。這種強制類型轉換既增加了編程的復雜度,也可能引發 ClassCastException 異常。

    從 Java 5 開始,Java 引入了“參數化類型”的概念,允許程序在創建集合時指定集合元素的類型,Java 的參數化類型被稱為泛型(Generic)。例如, List<String>,表明該 List 只能保存字符串類型的對象。

    有了泛型以后,程序再也不能“不小心”地把其他對象“丟進”集合中。而且程序更加簡潔,集合自動記住所有集合元素的數據類型,從而無須對集合元素進行強制類型轉換。

    介紹一下泛型擦除?

    在嚴格的泛型代碼里,帶泛型聲明的類總應該帶著類型參數。但為了與老的 Java 代碼保持一致,也允許在使用帶泛型聲明的類時不指定實際的類型。如果沒有為這個泛型類指定實際的類型,此時被稱作 raw type(原始類型),默認是聲明該泛型形參時指定的第一個上限類型。

    當把一個具有泛型信息的對象賦給另一個沒有泛型信息的變量時,所有在尖括號之間的類型信息都將被扔掉。比如一個 List<String> 類型被轉換為 List,則該 List 對集合元素的類型檢查變成了泛型參數的上限(即 Object)。

    上述規則即為泛型擦除,可以通過下面代碼進一步理解泛型擦除:

    List<String> list1 = ...; List list2 = list1; // list2將元素當做Object處理

    總結

    以上是生活随笔為你收集整理的《Java 后端面试经》Java 基础篇的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。

    主站蜘蛛池模板: 国产高潮自拍 | 91精品国产aⅴ一区 黄色a网 | 精品无码久久久久久久久成人 | 女人舌吻男人茎视频 | 自慰无码一区二区三区 | 1024久久| 国产一区二区三区四区五区六区 | 91免费观看网站 | 永久免费未满 | 红桃一区二区三区 | 久久潮 | 星铁乱淫h侵犯h文 | 熟妇五十路六十路息与子 | 好吊色在线观看 | 涩涩视频在线观看免费 | 欧美激情国产在线 | 美女亚洲一区 | 国产丰满大乳奶水在线视频 | 成人黄色免费观看 | 国模在线| 日韩成人在线播放 | 欧美一级网址 | 狠狠婷| 手机成人免费视频 | 日本在线黄色 | av第一页| 亚洲自拍偷拍综合 | 少妇25p | 麻豆国产91在线播放 | 精品毛片一区二区三区 | 在线爱情大片免费观看大全 | 成人在线观看h | 99黄色片| 欧日韩在线 | 国产成人a∨| 亚洲伦理中文字幕 | 青青青免费视频观看在线 | 黄色在线视频网址 | 成人免费在线网址 | 色哟哟一区二区三区 | 国产精品无码一区 | 神马久久精品 | 精品欧美国产 | 国产又爽又黄又嫩又猛又粗 | 国产五月天婷婷 | 91在线免费视频 | 91精品福利在线 | 艳母动漫在线播放 | 国产 中文 字幕 日韩 在线 | 久久久久久久9 | 国内爆初菊对白视频 | 国产91在线播放 | 免费看的毛片 | 娇妻高潮浓精白浆xxⅹ | 欧美18—19性高清hd4k | 久久视频在线看 | 日韩不卡在线播放 | 国产精品99久久久久 | 黄色男同视频 | 2021毛片| 天天艹夜夜 | 成人片黄网站久久久免费 | 一区二区三区四区中文字幕 | xxxxⅹxxxhd日本8hd| 中文字幕网站在线观看 | 无码国产色欲xxxx视频 | 欧美xxxx少妇 | 国产强被迫伦姧在线观看无码 | 成人免费观看网站 | 国产乱国产乱300精品 | china国产乱xxxxx绿帽 | 91超碰国产在线 | 91精品国产99久久久久久红楼 | 精品人妻久久久久久888不卡 | 少妇高潮一区二区三区99小说 | 日韩福利一区二区三区 | 天堂资源在线 | 欧美激情影院 | 三区在线 | 色香五月 | 樱花影院最新免费观看攻略 | 老女人一毛片 | 中国一级特黄毛片 | 日韩av色| 成人日批 | 色综合久久天天综合网 | av网站免费在线观看 | 成人欧美一区二区三区黑人动态图 | 玉女心经 在线 | 男人疯狂高潮呻吟视频 | 中文永久免费观看 | 无码人妻丰满熟妇区五十路百度 | 亚洲精品一区二区三区蜜臀 | 久操中文 | 人妻体体内射精一区二区 | 女人被狂躁c到高潮喷水电影 | 在线成年人视频 | 中国免费黄色 | 看黄色大片 |