應(yīng)屆生/社招面試最愛問的幾道Java基礎(chǔ)問題

本文已經(jīng)收錄自筆者開源的 JavaGuide: https://github.com/Snailclimb (【Java學(xué)習(xí)+面試指南】 一份涵蓋大部分Java程序員所需要掌握的核心知識)如果覺得不錯的還瓷马,不妨去點個Star馋记,鼓勵一下!

一 為什么 Java 中只有值傳遞灯荧?

首先回顧一下在程序設(shè)計語言中有關(guān)將參數(shù)傳遞給方法(或函數(shù))的一些專業(yè)術(shù)語粱甫。按值調(diào)用(call by value)表示方法接收的是調(diào)用者提供的值费韭,而按引用調(diào)用(call by reference)表示方法接收的是調(diào)用者提供的變量地址闪唆。一個方法可以修改傳遞引用所對應(yīng)的變量值,而不能修改傳遞值調(diào)用所對應(yīng)的變量值。 它用來描述各種程序設(shè)計語言(不只是 Java)中方法參數(shù)傳遞方式。

Java 程序設(shè)計語言總是采用按值調(diào)用。也就是說吃挑,方法得到的是所有參數(shù)值的一個拷貝钝荡,也就是說,方法不能修改傳遞給它的任何參數(shù)變量的內(nèi)容儒鹿。

下面通過 3 個例子來給大家說明

example 1

public static void main(String[] args) {
    int num1 = 10;
    int num2 = 20;

    swap(num1, num2);

    System.out.println("num1 = " + num1);
    System.out.println("num2 = " + num2);
}

public static void swap(int a, int b) {
    int temp = a;
    a = b;
    b = temp;

    System.out.println("a = " + a);
    System.out.println("b = " + b);
}

結(jié)果:

a = 20
b = 10
num1 = 10
num2 = 20

解析:

example 1

在 swap 方法中化撕,a、b 的值進行交換约炎,并不會影響到 num1植阴、num2。因為圾浅,a掠手、b 中的值,只是從 num1狸捕、num2 的復(fù)制過來的喷鸽。也就是說,a灸拍、b 相當于 num1做祝、num2 的副本,副本的內(nèi)容無論怎么修改鸡岗,都不會影響到原件本身混槐。

通過上面例子,我們已經(jīng)知道了一個方法不能修改一個基本數(shù)據(jù)類型的參數(shù)轩性,而對象引用作為參數(shù)就不一樣声登,請看 example2.

example 2

    public static void main(String[] args) {
        int[] arr = { 1, 2, 3, 4, 5 };
        System.out.println(arr[0]);
        change(arr);
        System.out.println(arr[0]);
    }

    public static void change(int[] array) {
        // 將數(shù)組的第一個元素變?yōu)?
        array[0] = 0;
    }

結(jié)果:

1
0

解析:

example 2

array 被初始化 arr 的拷貝也就是一個對象的引用,也就是說 array 和 arr 指向的是同一個數(shù)組對象揣苏。 因此悯嗓,外部對引用對象的改變會反映到所對應(yīng)的對象上。

通過 example2 我們已經(jīng)看到卸察,實現(xiàn)一個改變對象參數(shù)狀態(tài)的方法并不是一件難事脯厨。理由很簡單,方法得到的是對象引用的拷貝坑质,對象引用及其他的拷貝同時引用同一個對象俄认。

很多程序設(shè)計語言(特別是,C++和 Pascal)提供了兩種參數(shù)傳遞的方式:值調(diào)用和引用調(diào)用洪乍。有些程序員(甚至本書的作者)認為 Java 程序設(shè)計語言對對象采用的是引用調(diào)用,實際上夜焦,這種理解是不對的壳澳。由于這種誤解具有一定的普遍性,所以下面給出一個反例來詳細地闡述一下這個問題茫经。

example 3

public class Test {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Student s1 = new Student("小張");
        Student s2 = new Student("小李");
        Test.swap(s1, s2);
        System.out.println("s1:" + s1.getName());
        System.out.println("s2:" + s2.getName());
    }

    public static void swap(Student x, Student y) {
        Student temp = x;
        x = y;
        y = temp;
        System.out.println("x:" + x.getName());
        System.out.println("y:" + y.getName());
    }
}

結(jié)果:

x:小李
y:小張
s1:小張
s2:小李

解析:

交換之前:

交換之后:

通過上面兩張圖可以很清晰的看出: 方法并沒有改變存儲在變量 s1 和 s2 中的對象引用巷波。swap 方法的參數(shù) x 和 y 被初始化為兩個對象引用的拷貝萎津,這個方法交換的是這兩個拷貝

總結(jié)

Java 程序設(shè)計語言對對象采用的不是引用調(diào)用,實際上抹镊,對象引用是按
值傳遞的锉屈。

下面再總結(jié)一下 Java 中方法參數(shù)的使用情況:

  • 一個方法不能修改一個基本數(shù)據(jù)類型的參數(shù)(即數(shù)值型或布爾型)。
  • 一個方法可以改變一個對象參數(shù)的狀態(tài)垮耳。
  • 一個方法不能讓對象參數(shù)引用一個新的對象颈渊。

參考:

《Java 核心技術(shù)卷 Ⅰ》基礎(chǔ)知識第十版第四章 4.5 小節(jié)

二 ==與 equals(重要)

== : 它的作用是判斷兩個對象的地址是不是相等。即终佛,判斷兩個對象是不是同一個對象俊嗽。(基本數(shù)據(jù)類型==比較的是值,引用數(shù)據(jù)類型==比較的是內(nèi)存地址)

equals() : 它的作用也是判斷兩個對象是否相等铃彰。但它一般有兩種使用情況:

  • 情況 1:類沒有覆蓋 equals()方法绍豁。則通過 equals()比較該類的兩個對象時,等價于通過“==”比較這兩個對象牙捉。
  • 情況 2:類覆蓋了 equals()方法竹揍。一般,我們都覆蓋 equals()方法來兩個對象的內(nèi)容相等邪铲;若它們的內(nèi)容相等芬位,則返回 true(即,認為這兩個對象相等)霜浴。

舉個例子:

public class test1 {
    public static void main(String[] args) {
        String a = new String("ab"); // a 為一個引用
        String b = new String("ab"); // b為另一個引用,對象的內(nèi)容一樣
        String aa = "ab"; // 放在常量池中
        String bb = "ab"; // 從常量池中查找
        if (aa == bb) // true
            System.out.println("aa==bb");
        if (a == b) // false晶衷,非同一對象
            System.out.println("a==b");
        if (a.equals(b)) // true
            System.out.println("aEQb");
        if (42 == 42.0) { // true
            System.out.println("true");
        }
    }
}

說明:

  • String 中的 equals 方法是被重寫過的,因為 object 的 equals 方法是比較的對象的內(nèi)存地址阴孟,而 String 的 equals 方法比較的是對象的值晌纫。
  • 當創(chuàng)建 String 類型的對象時,虛擬機會在常量池中查找有沒有已經(jīng)存在的值和要創(chuàng)建的值相同的對象永丝,如果有就把它賦給當前引用锹漱。如果沒有就在常量池中重新創(chuàng)建一個 String 對象。

三 hashCode 與 equals(重要)

面試官可能會問你:“你重寫過 hashcode 和 equals 么慕嚷,為什么重寫 equals 時必須重寫 hashCode 方法哥牍?”

3.1 hashCode()介紹

hashCode() 的作用是獲取哈希碼,也稱為散列碼喝检;它實際上是返回一個 int 整數(shù)嗅辣。這個哈希碼的作用是確定該對象在哈希表中的索引位置。hashCode() 定義在 JDK 的 Object.java 中挠说,這就意味著 Java 中的任何類都包含有 hashCode() 函數(shù)澡谭。另外需要注意的是: Object 的 hashcode 方法是本地方法,也就是用 c 語言或 c++ 實現(xiàn)的损俭,該方法通常用來將對象的 內(nèi)存地址 轉(zhuǎn)換為整數(shù)之后返回蛙奖。

    /**
     * Returns a hash code value for the object. This method is
     * supported for the benefit of hash tables such as those provided by
     * {@link java.util.HashMap}.
     * <p>
     * As much as is reasonably practical, the hashCode method defined by
     * class {@code Object} does return distinct integers for distinct
     * objects. (This is typically implemented by converting the internal
     * address of the object into an integer, but this implementation
     * technique is not required by the
     * Java&trade; programming language.)
     *
     * @return  a hash code value for this object.
     * @see     java.lang.Object#equals(java.lang.Object)
     * @see     java.lang.System#identityHashCode
     */
    public native int hashCode();

散列表存儲的是鍵值對(key-value)潘酗,它的特點是:能根據(jù)“鍵”快速的檢索出對應(yīng)的“值”。這其中就利用到了散列碼Q阒佟(可以快速找到所需要的對象)

3.2 為什么要有 hashCode

我們以“HashSet 如何檢查重復(fù)”為例子來說明為什么要有 hashCode:

當你把對象加入 HashSet 時仔夺,HashSet 會先計算對象的 hashcode 值來判斷對象加入的位置,同時也會與其他已經(jīng)加入的對象的 hashcode 值作比較攒砖,如果沒有相符的 hashcode缸兔,HashSet 會假設(shè)對象沒有重復(fù)出現(xiàn)。但是如果發(fā)現(xiàn)有相同 hashcode 值的對象祭衩,這時會調(diào)用 equals()方法來檢查 hashcode 相等的對象是否真的相同灶体。如果兩者相同,HashSet 就不會讓其加入操作成功掐暮。如果不同的話蝎抽,就會重新散列到其他位置。(摘自我的 Java 啟蒙書《Head fist java》第二版)路克。這樣我們就大大減少了 equals 的次數(shù)樟结,相應(yīng)就大大提高了執(zhí)行速度。

3.3 hashCode()與 equals()的相關(guān)規(guī)定

  1. 如果兩個對象相等精算,則 hashcode 一定也是相同的
  2. 兩個對象相等,對兩個對象分別調(diào)用 equals 方法都返回 true
  3. 兩個對象有相同的 hashcode 值瓢宦,它們也不一定是相等的
  4. 因此,equals 方法被覆蓋過灰羽,則 hashCode 方法也必須被覆蓋
  5. hashCode()的默認行為是對堆上的對象產(chǎn)生獨特值驮履。如果沒有重寫 hashCode(),則該 class 的兩個對象無論如何都不會相等(即使這兩個對象指向相同的數(shù)據(jù))

3.4 為什么兩個對象有相同的 hashcode 值廉嚼,它們也不一定是相等的玫镐?

在這里解釋一位小伙伴的問題。以下內(nèi)容摘自《Head Fisrt Java》怠噪。

因為 hashCode() 所使用的雜湊算法也許剛好會讓多個對象傳回相同的雜湊值恐似。越糟糕的雜湊算法越容易碰撞,但這也與數(shù)據(jù)值域分布的特性有關(guān)(所謂碰撞也就是指的是不同的對象得到相同的 hashCode)傍念。

我們剛剛也提到了 HashSet,如果 HashSet 在對比的時候矫夷,同樣的 hashcode 有多個對象,它會使用 equals() 來判斷是否真的相同憋槐。也就是說 hashcode 只是用來縮小查找成本双藕。

四 String 和 StringBuffer、StringBuilder 的區(qū)別是什么阳仔?String 為什么是不可變的蔓彩?

可變性

簡單的來說:String 類中使用 final 關(guān)鍵字修飾字符數(shù)組來保存字符串,private final char value[],所以 String 對象是不可變的赤嚼。而 StringBuilder 與 StringBuffer 都繼承自 AbstractStringBuilder 類,在 AbstractStringBuilder 中也是使用字符數(shù)組保存字符串char[]value 但是沒有用 final 關(guān)鍵字修飾顺又,所以這兩種對象都是可變的更卒。

StringBuilder 與 StringBuffer 的構(gòu)造方法都是調(diào)用父類構(gòu)造方法也就是 AbstractStringBuilder 實現(xiàn)的,大家可以自行查閱源碼稚照。

AbstractStringBuilder.java

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    char[] value;
    int count;
    AbstractStringBuilder() {
    }
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }

線程安全性

String 中的對象是不可變的蹂空,也就可以理解為常量,線程安全果录。AbstractStringBuilder 是 StringBuilder 與 StringBuffer 的公共父類上枕,定義了一些字符串的基本操作,如 expandCapacity弱恒、append辨萍、insert、indexOf 等公共方法返弹。StringBuffer 對方法加了同步鎖或者對調(diào)用的方法加了同步鎖锈玉,所以是線程安全的。StringBuilder 并沒有對方法進行加同步鎖义起,所以是非線程安全的拉背。

性能

每次對 String 類型進行改變的時候,都會生成一個新的 String 對象默终,然后將指針指向新的 String 對象椅棺。StringBuffer 每次都會對 StringBuffer 對象本身進行操作,而不是生成新的對象并改變對象引用齐蔽。相同情況下使用 StringBuilder 相比使用 StringBuffer 僅能獲得 10%~15% 左右的性能提升两疚,但卻要冒多線程不安全的風(fēng)險。

對于三者使用的總結(jié):

  1. 操作少量的數(shù)據(jù): 適用 String
  2. 單線程操作字符串緩沖區(qū)下操作大量數(shù)據(jù): 適用 StringBuilder
  3. 多線程操作字符串緩沖區(qū)下操作大量數(shù)據(jù): 適用 StringBuffer

String 為什么是不可變的嗎肴熏?

簡單來說就是 String 類利用了 final 修飾的 char 類型數(shù)組存儲字符鬼雀,源碼如下圖所以:

    /** The value is used for character storage. */
    private final char value[];

String 真的是不可變的嗎?

我覺得如果別人問這個問題的話蛙吏,回答不可變就可以了源哩。
下面只是給大家看兩個有代表性的例子:

1) String 不可變但不代表引用不可以變

        String str = "Hello";
        str = str + " World";
        System.out.println("str=" + str);

結(jié)果:

str=Hello World

解析:

實際上,原來 String 的內(nèi)容是不變的鸦做,只是 str 由原來指向"Hello"的內(nèi)存地址轉(zhuǎn)為指向"Hello World"的內(nèi)存地址而已励烦,也就是說多開辟了一塊內(nèi)存區(qū)域給"Hello World"字符串。

2) 通過反射是可以修改所謂的“不可變”對象

        // 創(chuàng)建字符串"Hello World"泼诱, 并賦給引用s
        String s = "Hello World";

        System.out.println("s = " + s); // Hello World

        // 獲取String類中的value字段
        Field valueFieldOfString = String.class.getDeclaredField("value");

        // 改變value屬性的訪問權(quán)限
        valueFieldOfString.setAccessible(true);

        // 獲取s對象上的value屬性的值
        char[] value = (char[]) valueFieldOfString.get(s);

        // 改變value所引用的數(shù)組中的第5個字符
        value[5] = '_';

        System.out.println("s = " + s); // Hello_World

結(jié)果:

s = Hello World
s = Hello_World

解析:

用反射可以訪問私有成員坛掠, 然后反射出 String 對象中的 value 屬性, 進而改變通過獲得的 value 引用改變數(shù)組的結(jié)構(gòu)。但是一般我們不會這么做屉栓,這里只是簡單提一下有這個東西舷蒲。

五 什么是反射機制?反射機制的應(yīng)用場景有哪些友多?

5.1 反射機制介紹

JAVA 反射機制是在運行狀態(tài)中牲平,對于任意一個類,都能夠知道這個類的所有屬性和方法域滥;對于任意一個對象纵柿,都能夠調(diào)用它的任意一個方法和屬性;這種動態(tài)獲取的信息以及動態(tài)調(diào)用對象的方法的功能稱為 java 語言的反射機制启绰。

5.2 靜態(tài)編譯和動態(tài)編譯

  • 靜態(tài)編譯:在編譯時確定類型昂儒,綁定對象
  • 動態(tài)編譯:運行時確定類型,綁定對象

5.3 反射機制優(yōu)缺點

  • 優(yōu)點: 運行期類型的判斷委可,動態(tài)加載類渊跋,提高代碼靈活度。
  • 缺點: 性能瓶頸:反射相當于一系列解釋操作撤缴,通知 JVM 要做的事情刹枉,性能比直接的 java 代碼要慢很多。

5.4 反射的應(yīng)用場景

反射是框架設(shè)計的靈魂屈呕。

在我們平時的項目開發(fā)過程中微宝,基本上很少會直接使用到反射機制,但這不能說明反射機制沒有用虎眨,實際上有很多設(shè)計蟋软、開發(fā)都與反射機制有關(guān),例如模塊化的開發(fā)嗽桩,通過反射去調(diào)用對應(yīng)的字節(jié)碼岳守;動態(tài)代理設(shè)計模式也采用了反射機制,還有我們?nèi)粘J褂玫?Spring/Hibernate 等框架也大量使用到了反射機制碌冶。

舉例:① 我們在使用 JDBC 連接數(shù)據(jù)庫時使用 Class.forName()通過反射加載數(shù)據(jù)庫的驅(qū)動程序湿痢;②Spring 框架也用到很多反射機制,最經(jīng)典的就是 xml 的配置模式扑庞。Spring 通過 XML 配置模式裝載 Bean 的過程:1) 將程序內(nèi)所有 XML 或 Properties 配置文件加載入內(nèi)存中;
2)Java 類里面解析 xml 或 properties 里面的內(nèi)容譬重,得到對應(yīng)實體類的字節(jié)碼字符串以及相關(guān)的屬性信息; 3)使用反射機制,根據(jù)這個字符串獲得某個類的 Class 實例; 4)動態(tài)配置實例的屬性

推薦閱讀:

六 什么是 JDK?什么是 JRE罐氨?什么是 JVM臀规?三者之間的聯(lián)系與區(qū)別

6.1 JVM

Java 虛擬機(JVM)是運行 Java 字節(jié)碼的虛擬機。JVM 有針對不同系統(tǒng)的特定實現(xiàn)(Windows栅隐,Linux塔嬉,macOS)玩徊,目的是使用相同的字節(jié)碼,它們都會給出相同的結(jié)果谨究。

什么是字節(jié)碼?采用字節(jié)碼的好處是什么?

在 Java 中恩袱,JVM 可以理解的代碼就叫做字節(jié)碼(即擴展名為 .class 的文件),它不面向任何特定的處理器记盒,只面向虛擬機憎蛤。Java 語言通過字節(jié)碼的方式,在一定程度上解決了傳統(tǒng)解釋型語言執(zhí)行效率低的問題纪吮,同時又保留了解釋型語言可移植的特點。所以 Java 程序運行時比較高效萎胰,而且碾盟,由于字節(jié)碼并不針對一種特定的機器,因此技竟,Java 程序無須重新編譯便可在多種不同操作系統(tǒng)的計算機上運行冰肴。

Java 程序從源代碼到運行一般有下面 3 步:

Java程序運行過程

我們需要格外注意的是 .class->機器碼 這一步。在這一步 JVM 類加載器首先加載字節(jié)碼文件榔组,然后通過解釋器逐行解釋執(zhí)行熙尉,這種方式的執(zhí)行速度會相對比較慢。而且搓扯,有些方法和代碼塊是經(jīng)常需要被調(diào)用的(也就是所謂的熱點代碼)检痰,所以后面引進了 JIT 編譯器,而 JIT 屬于運行時編譯锨推。當 JIT 編譯器完成第一次編譯后铅歼,其會將字節(jié)碼對應(yīng)的機器碼保存下來,下次可以直接使用换可。而我們知道椎椰,機器碼的運行效率肯定是高于 Java 解釋器的。這也解釋了我們?yōu)槭裁唇?jīng)常會說 Java 是編譯與解釋共存的語言沾鳄。

HotSpot 采用了惰性評估(Lazy Evaluation)的做法慨飘,根據(jù)二八定律,消耗大部分系統(tǒng)資源的只有那一小部分的代碼(熱點代碼)译荞,而這也就是 JIT 所需要編譯的部分瓤的。JVM 會根據(jù)代碼每次被執(zhí)行的情況收集信息并相應(yīng)地做出一些優(yōu)化,因此執(zhí)行的次數(shù)越多磁椒,它的速度就越快堤瘤。JDK 9 引入了一種新的編譯模式 AOT(Ahead of Time Compilation),它是直接將字節(jié)碼編譯成機器碼浆熔,這樣就避免了 JIT 預(yù)熱等各方面的開銷本辐。JDK 支持分層編譯和 AOT 協(xié)作使用桥帆。但是 ,AOT 編譯器的編譯質(zhì)量是肯定比不上 JIT 編譯器的慎皱。

總結(jié):

Java 虛擬機(JVM)是運行 Java 字節(jié)碼的虛擬機老虫。JVM 有針對不同系統(tǒng)的特定實現(xiàn)(Windows,Linux茫多,macOS)祈匙,目的是使用相同的字節(jié)碼,它們都會給出相同的結(jié)果天揖。字節(jié)碼和不同系統(tǒng)的 JVM 實現(xiàn)是 Java 語言“一次編譯夺欲,隨處可以運行”的關(guān)鍵所在。

6.2 JDK 和 JRE

JDK 是 Java Development Kit今膊,它是功能齊全的 Java SDK些阅。它擁有 JRE 所擁有的一切,還有編譯器(javac)和工具(如 javadoc 和 jdb)斑唬。它能夠創(chuàng)建和編譯程序市埋。

JRE 是 Java 運行時環(huán)境。它是運行已編譯 Java 程序所需的所有內(nèi)容的集合恕刘,包括 Java 虛擬機(JVM)缤谎,Java 類庫,java 命令和其他的一些基礎(chǔ)構(gòu)件褐着。但是坷澡,它不能用于創(chuàng)建新程序。

如果你只是為了運行一下 Java 程序的話献起,那么你只需要安裝 JRE 就可以了洋访。如果你需要進行一些 Java 編程方面的工作,那么你就需要安裝 JDK 了谴餐。但是姻政,這不是絕對的。有時岂嗓,即使您不打算在計算機上進行任何 Java 開發(fā)汁展,仍然需要安裝 JDK。例如厌殉,如果要使用 JSP 部署 Web 應(yīng)用程序食绿,那么從技術(shù)上講,您只是在應(yīng)用程序服務(wù)器中運行 Java 程序公罕。那你為什么需要 JDK 呢器紧?因為應(yīng)用程序服務(wù)器會將 JSP 轉(zhuǎn)換為 Java servlet,并且需要使用 JDK 來編譯 servlet楼眷。

七 什么是字節(jié)碼铲汪?采用字節(jié)碼的最大好處是什么熊尉?

先看下 java 中的編譯器和解釋器:

Java 中引入了虛擬機的概念,即在機器和編譯程序之間加入了一層抽象的虛擬的機器掌腰。這臺虛擬的機器在任何平臺上都提供給編譯程序一個的共同的接口狰住。編譯程序只需要面向虛擬機,生成虛擬機能夠理解的代碼齿梁,然后由解釋器來將虛擬機代碼轉(zhuǎn)換為特定系統(tǒng)的機器碼執(zhí)行催植。在 Java 中,這種供虛擬機理解的代碼叫做字節(jié)碼(即擴展名為.class的文件)勺择,它不面向任何特定的處理器创南,只面向虛擬機。每一種平臺的解釋器是不同的省核,但是實現(xiàn)的虛擬機是相同的扰藕。Java 源程序經(jīng)過編譯器編譯后變成字節(jié)碼,字節(jié)碼由虛擬機解釋執(zhí)行芳撒,虛擬機將每一條要執(zhí)行的字節(jié)碼送給解釋器,解釋器將其翻譯成特定機器上的機器碼未桥,然后在特定的機器上運行笔刹。這也就是解釋了 Java 的編譯與解釋并存的特點。

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

采用字節(jié)碼的好處:

Java 語言通過字節(jié)碼的方式舌菜,在一定程度上解決了傳統(tǒng)解釋型語言執(zhí)行效率低的問題,同時又保留了解釋型語言可移植的特點亦镶。所以 Java 程序運行時比較高效日月,而且,由于字節(jié)碼并不專對一種特定的機器缤骨,因此爱咬,Java 程序無須重新編譯便可在多種不同的計算機上運行。

八 接口和抽象類的區(qū)別是什么?

  1. 接口的方法默認是 public绊起,所有方法在接口中不能有實現(xiàn)精拟,抽象類可以有非抽象的方法
  2. 接口中的實例變量默認是 final 類型的,而抽象類中則不一定
  3. 一個類可以實現(xiàn)多個接口虱歪,但最多只能實現(xiàn)一個抽象類
  4. 一個類實現(xiàn)接口的話要實現(xiàn)接口的所有方法蜂绎,而抽象類不一定
  5. 接口不能用 new 實例化,但可以聲明笋鄙,但是必須引用一個實現(xiàn)該接口的對象 從設(shè)計層面來說师枣,抽象是對類的抽象,是一種模板設(shè)計萧落,接口是行為的抽象践美,是一種行為的規(guī)范洗贰。

注意:Java8 后接口可以有默認實現(xiàn)( default )。

九 重載和重寫的區(qū)別

重載

發(fā)生在同一個類中拨脉,方法名必須相同哆姻,參數(shù)類型不同、個數(shù)不同玫膀、順序不同矛缨,方法返回值和訪問修飾符可以不同。

下面是《Java 核心技術(shù)》對重載這個概念的介紹:

重寫

重寫是子類對父類的允許訪問的方法的實現(xiàn)過程進行重新編寫,發(fā)生在子類中帖旨,方法名箕昭、參數(shù)列表必須相同,返回值范圍小于等于父類解阅,拋出的異常范圍小于等于父類落竹,訪問修飾符范圍大于等于父類。另外货抄,如果父類方法訪問修飾符為 private 則子類就不能重寫該方法述召。也就是說方法提供的行為改變,而方法的外貌并沒有改變蟹地。

十. Java 面向?qū)ο缶幊倘筇匦? 封裝 繼承 多態(tài)

封裝

封裝把一個對象的屬性私有化积暖,同時提供一些可以被外界訪問的屬性的方法,如果屬性不想被外界訪問怪与,我們大可不必提供方法給外界訪問夺刑。但是如果一個類沒有提供給外界訪問的方法,那么這個類也沒有什么意義了分别。

繼承

繼承是使用已存在的類的定義作為基礎(chǔ)建立新類的技術(shù)遍愿,新類的定義可以增加新的數(shù)據(jù)或新的功能,也可以用父類的功能耘斩,但不能選擇性地繼承父類沼填。通過使用繼承我們能夠非常方便地復(fù)用以前的代碼。

關(guān)于繼承如下 3 點請記谆屯:

  1. 子類擁有父類對象所有的屬性和方法(包括私有屬性和私有方法)倾哺,但是父類中的私有屬性和方法子類是無法訪問,只是擁有刽脖。
  2. 子類可以擁有自己屬性和方法羞海,即子類可以對父類進行擴展。
  3. 子類可以用自己的方式實現(xiàn)父類的方法曲管。(以后介紹)却邓。

多態(tài)

所謂多態(tài)就是指程序中定義的引用變量所指向的具體類型和通過該引用變量發(fā)出的方法調(diào)用在編程時并不確定,而是在程序運行期間才確定院水,即一個引用變量到底會指向哪個類的實例對象腊徙,該引用變量發(fā)出的方法調(diào)用到底是哪個類中實現(xiàn)的方法简十,必須在由程序運行期間才能決定。

在 Java 中有兩種形式可以實現(xiàn)多態(tài):繼承(多個子類對同一方法的重寫)和接口(實現(xiàn)接口并覆蓋接口中同一方法)撬腾。

十一. 什么是線程和進程?

11.1 何為進程?

進程是程序的一次執(zhí)行過程螟蝙,是系統(tǒng)運行程序的基本單位,因此進程是動態(tài)的民傻。系統(tǒng)運行一個程序即是一個進程從創(chuàng)建胰默,運行到消亡的過程。

在 Java 中漓踢,當我們啟動 main 函數(shù)時其實就是啟動了一個 JVM 的進程牵署,而 main 函數(shù)所在的線程就是這個進程中的一個線程,也稱主線程喧半。

如下圖所示奴迅,在 windows 中通過查看任務(wù)管理器的方式,我們就可以清楚看到 window 當前運行的進程(.exe 文件的運行)挺据。

進程示例圖片-Windows

11.2 何為線程?

線程與進程相似取具,但線程是一個比進程更小的執(zhí)行單位。一個進程在其執(zhí)行的過程中可以產(chǎn)生多個線程扁耐。與進程不同的是同類的多個線程共享進程的方法區(qū)資源者填,但每個線程有自己的程序計數(shù)器虛擬機棧本地方法棧做葵,所以系統(tǒng)在產(chǎn)生一個線程,或是在各個線程之間作切換工作時心墅,負擔(dān)要比進程小得多酿矢,也正因為如此,線程也被稱為輕量級進程怎燥。

Java 程序天生就是多線程程序瘫筐,我們可以通過 JMX 來看一下一個普通的 Java 程序有哪些線程,代碼如下铐姚。

public class MultiThread {
    public static void main(String[] args) {
        // 獲取 Java 線程管理 MXBean
    ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        // 不需要獲取同步的 monitor 和 synchronizer 信息策肝,僅獲取線程和線程堆棧信息
        ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false);
        // 遍歷線程信息,僅打印線程 ID 和線程名稱信息
        for (ThreadInfo threadInfo : threadInfos) {
            System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName());
        }
    }
}

上述程序輸出如下(輸出內(nèi)容可能不同隐绵,不用太糾結(jié)下面每個線程的作用之众,只用知道 main 線程執(zhí)行 main 方法即可):

[5] Attach Listener //添加事件
[4] Signal Dispatcher // 分發(fā)處理給 JVM 信號的線程
[3] Finalizer //調(diào)用對象 finalize 方法的線程
[2] Reference Handler //清除 reference 線程
[1] main //main 線程,程序入口

從上面的輸出內(nèi)容可以看出:一個 Java 程序的運行是 main 線程和多個其他線程同時運行

十二. 請簡要描述線程與進程的關(guān)系,區(qū)別及優(yōu)缺點依许?

從 JVM 角度說進程和線程之間的關(guān)系

12.1 圖解進程和線程的關(guān)系

下圖是 Java 內(nèi)存區(qū)域棺禾,通過下圖我們從 JVM 的角度來說一下線程和進程之間的關(guān)系。如果你對 Java 內(nèi)存區(qū)域 (運行時數(shù)據(jù)區(qū)) 這部分知識不太了解的話可以閱讀一下這篇文章:《可能是把 Java 內(nèi)存區(qū)域講的最清楚的一篇文章》

<div align="center">
<img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3/JVM運行時數(shù)據(jù)區(qū)域.png" width="600px"/>
</div>

從上圖可以看出:一個進程中可以有多個線程峭跳,多個線程共享進程的方法區(qū) (JDK1.8 之后的元空間)資源膘婶,但是每個線程有自己的程序計數(shù)器缺前、虛擬機棧本地方法棧

總結(jié): 線程 是 進程 劃分成的更小的運行單位悬襟。線程和進程最大的不同在于基本上各進程是獨立的衅码,而各線程則不一定,因為同一進程中的線程極有可能會相互影響脊岳。線程執(zhí)行開銷小逝段,但不利于資源的管理和保護;而進程正相反

下面是該知識點的擴展內(nèi)容逸绎!

下面來思考這樣一個問題:為什么程序計數(shù)器惹恃、虛擬機棧本地方法棧是線程私有的呢?為什么堆和方法區(qū)是線程共享的呢棺牧?

12.2 程序計數(shù)器為什么是私有的?

程序計數(shù)器主要有下面兩個作用:

  1. 字節(jié)碼解釋器通過改變程序計數(shù)器來依次讀取指令巫糙,從而實現(xiàn)代碼的流程控制,如:順序執(zhí)行颊乘、選擇参淹、循環(huán)、異常處理乏悄。
  2. 在多線程的情況下浙值,程序計數(shù)器用于記錄當前線程執(zhí)行的位置,從而當線程被切換回來的時候能夠知道該線程上次運行到哪兒了檩小。

需要注意的是开呐,如果執(zhí)行的是 native 方法,那么程序計數(shù)器記錄的是 undefined 地址规求,只有執(zhí)行的是 Java 代碼時程序計數(shù)器記錄的才是下一條指令的地址筐付。

所以,程序計數(shù)器私有主要是為了線程切換后能恢復(fù)到正確的執(zhí)行位置阻肿。

12.3 虛擬機棧和本地方法棧為什么是私有的?

  • 虛擬機棧: 每個 Java 方法在執(zhí)行的同時會創(chuàng)建一個棧幀用于存儲局部變量表瓦戚、操作數(shù)棧、常量池引用等信息丛塌。從方法調(diào)用直至執(zhí)行完成的過程较解,就對應(yīng)著一個棧幀在 Java 虛擬機棧中入棧和出棧的過程。
  • 本地方法棧: 和虛擬機棧所發(fā)揮的作用非常相似赴邻,區(qū)別是: 虛擬機棧為虛擬機執(zhí)行 Java 方法 (也就是字節(jié)碼)服務(wù)印衔,而本地方法棧則為虛擬機使用到的 Native 方法服務(wù)。 在 HotSpot 虛擬機中和 Java 虛擬機棧合二為一姥敛。

所以当编,為了保證線程中的局部變量不被別的線程訪問到,虛擬機棧和本地方法棧是線程私有的。

12.4 一句話簡單了解堆和方法區(qū)

堆和方法區(qū)是所有線程共享的資源,其中堆是進程中最大的一塊內(nèi)存,主要用于存放新創(chuàng)建的對象 (所有對象都在這里分配內(nèi)存)圆到,方法區(qū)主要用于存放已被加載的類信息、常量揍拆、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)茶凳。

十三. 說說并發(fā)與并行的區(qū)別?

  • 并發(fā): 同一時間段嫂拴,多個任務(wù)都在執(zhí)行 (單位時間內(nèi)不一定同時執(zhí)行);
  • 并行: 單位時間內(nèi)贮喧,多個任務(wù)同時執(zhí)行筒狠。

十四. 什么是上下文切換?

多線程編程中一般線程的個數(shù)都大于 CPU 核心的個數(shù),而一個 CPU 核心在任意時刻只能被一個線程使用箱沦,為了讓這些線程都能得到有效執(zhí)行辩恼,CPU 采取的策略是為每個線程分配時間片并輪轉(zhuǎn)的形式。當一個線程的時間片用完的時候就會重新處于就緒狀態(tài)讓給其他線程使用谓形,這個過程就屬于一次上下文切換灶伊。

概括來說就是:當前任務(wù)在執(zhí)行完 CPU 時間片切換到另一個任務(wù)之前會先保存自己的狀態(tài),以便下次再切換回這個任務(wù)時寒跳,可以再加載這個任務(wù)的狀態(tài)聘萨。任務(wù)從保存到再加載的過程就是一次上下文切換

上下文切換通常是計算密集型的童太。也就是說米辐,它需要相當可觀的處理器時間,在每秒幾十上百次的切換中书释,每次切換都需要納秒量級的時間儡循。所以,上下文切換對系統(tǒng)來說意味著消耗大量的 CPU 時間征冷,事實上,可能是操作系統(tǒng)中時間消耗最大的操作誓琼。

Linux 相比與其他操作系統(tǒng)(包括其他類 Unix 系統(tǒng))有很多的優(yōu)點检激,其中有一項就是,其上下文切換和模式切換的時間消耗非常少腹侣。

十五. 什么是線程死鎖?如何避免死鎖?

15.1. 認識線程死鎖

多個線程同時被阻塞叔收,它們中的一個或者全部都在等待某個資源被釋放。由于線程被無限期地阻塞傲隶,因此程序不可能正常終止饺律。

如下圖所示,線程 A 持有資源 2跺株,線程 B 持有資源 1复濒,他們同時都想申請對方的資源脖卖,所以這兩個線程就會互相等待而進入死鎖狀態(tài)。

線程死鎖示意圖

下面通過一個例子來說明線程死鎖,代碼模擬了上圖的死鎖的情況 (代碼來源于《并發(fā)編程之美》):

public class DeadLockDemo {
    private static Object resource1 = new Object();//資源 1
    private static Object resource2 = new Object();//資源 2

    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "線程 1").start();

        new Thread(() -> {
            synchronized (resource2) {
                System.out.println(Thread.currentThread() + "get resource2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1");
                }
            }
        }, "線程 2").start();
    }
}

Output

Thread[線程 1,5,main]get resource1
Thread[線程 2,5,main]get resource2
Thread[線程 1,5,main]waiting get resource2
Thread[線程 2,5,main]waiting get resource1

線程 A 通過 synchronized (resource1) 獲得 resource1 的監(jiān)視器鎖巧颈,然后通過Thread.sleep(1000);讓線程 A 休眠 1s 為的是讓線程 B 得到執(zhí)行然后獲取到 resource2 的監(jiān)視器鎖畦木。線程 A 和線程 B 休眠結(jié)束了都開始企圖請求獲取對方的資源,然后這兩個線程就會陷入互相等待的狀態(tài)砸泛,這也就產(chǎn)生了死鎖十籍。上面的例子符合產(chǎn)生死鎖的四個必要條件。

學(xué)過操作系統(tǒng)的朋友都知道產(chǎn)生死鎖必須具備以下四個條件:

  1. 互斥條件:該資源任意一個時刻只由一個線程占用唇礁。
  2. 請求與保持條件:一個進程因請求資源而阻塞時勾栗,對已獲得的資源保持不放。
  3. 不剝奪條件:線程已獲得的資源在末使用完之前不能被其他線程強行剝奪盏筐,只有自己使用完畢后才釋放資源围俘。
  4. 循環(huán)等待條件:若干進程之間形成一種頭尾相接的循環(huán)等待資源關(guān)系。

15.2 如何避免線程死鎖?

我們只要破壞產(chǎn)生死鎖的四個條件中的其中一個就可以了机断。

破壞互斥條件

這個條件我們沒有辦法破壞楷拳,因為我們用鎖本來就是想讓他們互斥的(臨界資源需要互斥訪問)。

破壞請求與保持條件

一次性申請所有的資源吏奸。

破壞不剝奪條件

占用部分資源的線程進一步申請其他資源時欢揖,如果申請不到,可以主動釋放它占有的資源奋蔚。

破壞循環(huán)等待條件

靠按序申請資源來預(yù)防她混。按某一順序申請資源,釋放資源則反序釋放泊碑。破壞循環(huán)等待條件坤按。

我們對線程 2 的代碼修改成下面這樣就不會產(chǎn)生死鎖了。

        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "線程 2").start();

Output

Thread[線程 1,5,main]get resource1
Thread[線程 1,5,main]waiting get resource2
Thread[線程 1,5,main]get resource2
Thread[線程 2,5,main]get resource1
Thread[線程 2,5,main]waiting get resource2
Thread[線程 2,5,main]get resource2

Process finished with exit code 0

我們分析一下上面的代碼為什么避免了死鎖的發(fā)生?

線程 1 首先獲得到 resource1 的監(jiān)視器鎖,這時候線程 2 就獲取不到了馒过。然后線程 1 再去獲取 resource2 的監(jiān)視器鎖臭脓,可以獲取到。然后線程 1 釋放了對 resource1腹忽、resource2 的監(jiān)視器鎖的占用来累,線程 2 獲取到就可以執(zhí)行了。這樣就破壞了破壞循環(huán)等待條件窘奏,因此避免了死鎖嘹锁。

十六. 說說 sleep() 方法和 wait() 方法區(qū)別和共同點?

  • 兩者最主要的區(qū)別在于:sleep 方法沒有釋放鎖,而 wait 方法釋放了鎖 着裹。
  • 兩者都可以暫停線程的執(zhí)行领猾。
  • Wait 通常被用于線程間交互/通信,sleep 通常被用于暫停執(zhí)行。
  • wait() 方法被調(diào)用后摔竿,線程不會自動蘇醒面粮,需要別的線程調(diào)用同一個對象上的 notify() 或者 notifyAll() 方法。sleep() 方法執(zhí)行完成后拯坟,線程會自動蘇醒但金。或者可以使用 wait(long timeout)超時后線程會自動蘇醒郁季。

十七. 為什么我們調(diào)用 start() 方法時會執(zhí)行 run() 方法冷溃,為什么我們不能直接調(diào)用 run() 方法?

這是另一個非常經(jīng)典的 java 多線程面試問題梦裂,而且在面試中會經(jīng)常被問到似枕。很簡單,但是很多人都會答不上來年柠!

new 一個 Thread凿歼,線程進入了新建狀態(tài);調(diào)用 start() 方法,會啟動一個線程并使線程進入了就緒狀態(tài)冗恨,當分配到時間片后就可以開始運行了答憔。 start() 會執(zhí)行線程的相應(yīng)準備工作,然后自動執(zhí)行 run() 方法的內(nèi)容掀抹,這是真正的多線程工作虐拓。 而直接執(zhí)行 run() 方法,會把 run 方法當成一個 main 線程下的普通方法去執(zhí)行傲武,并不會在某個線程中執(zhí)行它蓉驹,所以這并不是多線程工作。

總結(jié): 調(diào)用 start 方法方可啟動線程并使線程進入就緒狀態(tài)揪利,而 run 方法只是 thread 的一個普通方法調(diào)用态兴,還是在主線程里執(zhí)行。

參考

公眾號

如果大家想要實時關(guān)注我更新的文章以及分享的干貨的話疟位,可以關(guān)注我的公眾號瞻润。

《Java面試突擊》: 由本文檔衍生的專為面試而生的《Java面試突擊》V2.0 PDF 版本公眾號后臺回復(fù) "Java面試突擊" 即可免費領(lǐng)取甜刻!

Java工程師必備學(xué)習(xí)資源: 一些Java工程師常用學(xué)習(xí)資源公眾號后臺回復(fù)關(guān)鍵字 “1” 即可免費無套路獲取绍撞。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市罢吃,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌昭齐,老刑警劉巖尿招,帶你破解...
    沈念sama閱讀 223,002評論 6 519
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡就谜,警方通過查閱死者的電腦和手機怪蔑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,357評論 3 400
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來丧荐,“玉大人缆瓣,你說我怎么就攤上這事『缤常” “怎么了弓坞?”我有些...
    開封第一講書人閱讀 169,787評論 0 365
  • 文/不壞的土叔 我叫張陵,是天一觀的道長车荔。 經(jīng)常有香客問我渡冻,道長,這世上最難降的妖魔是什么忧便? 我笑而不...
    開封第一講書人閱讀 60,237評論 1 300
  • 正文 為了忘掉前任族吻,我火速辦了婚禮,結(jié)果婚禮上珠增,老公的妹妹穿的比我還像新娘超歌。我一直安慰自己,他們只是感情好蒂教,可當我...
    茶點故事閱讀 69,237評論 6 398
  • 文/花漫 我一把揭開白布巍举。 她就那樣靜靜地躺著,像睡著了一般悴品。 火紅的嫁衣襯著肌膚如雪禀综。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,821評論 1 314
  • 那天苔严,我揣著相機與錄音定枷,去河邊找鬼。 笑死届氢,一個胖子當著我的面吹牛欠窒,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播退子,決...
    沈念sama閱讀 41,236評論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼岖妄,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了寂祥?” 一聲冷哼從身側(cè)響起荐虐,我...
    開封第一講書人閱讀 40,196評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎丸凭,沒想到半個月后福扬,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體腕铸,經(jīng)...
    沈念sama閱讀 46,716評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,794評論 3 343
  • 正文 我和宋清朗相戀三年铛碑,在試婚紗的時候發(fā)現(xiàn)自己被綠了狠裹。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,928評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡汽烦,死狀恐怖涛菠,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情撇吞,我是刑警寧澤俗冻,帶...
    沈念sama閱讀 36,583評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站梢夯,受9級特大地震影響言疗,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜颂砸,卻給世界環(huán)境...
    茶點故事閱讀 42,264評論 3 336
  • 文/蒙蒙 一噪奄、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧人乓,春花似錦勤篮、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,755評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至戳护,卻和暖如春金抡,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背腌且。 一陣腳步聲響...
    開封第一講書人閱讀 33,869評論 1 274
  • 我被黑心中介騙來泰國打工梗肝, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人铺董。 一個月前我還...
    沈念sama閱讀 49,378評論 3 379
  • 正文 我出身青樓巫击,卻偏偏與公主長得像,于是被迫代替她去往敵國和親精续。 傳聞我的和親對象是個殘疾皇子坝锰,可洞房花燭夜當晚...
    茶點故事閱讀 45,937評論 2 361

推薦閱讀更多精彩內(nèi)容

  • 一:java概述:1,JDK:Java Development Kit重付,java的開發(fā)和運行環(huán)境顷级,java的開發(fā)工...
    ZaneInTheSun閱讀 2,662評論 0 11
  • 九種基本數(shù)據(jù)類型的大小,以及他們的封裝類确垫。(1)九種基本數(shù)據(jù)類型和封裝類 (2)自動裝箱和自動拆箱 什么是自動裝箱...
    關(guān)瑋琳linSir閱讀 1,891評論 0 47
  • 相關(guān)概念 面向?qū)ο蟮娜齻€特征 封裝,繼承,多態(tài).這個應(yīng)該是人人皆知.有時候也會加上抽象. 多態(tài)的好處 允許不同類對...
    東經(jīng)315度閱讀 1,955評論 0 8
  • 1.Java中的原始數(shù)據(jù)類型都有哪些弓颈,它們的大小及對應(yīng)的封裝類是什么拣凹? 關(guān)于boolean的說明: boolean...
    大棄閱讀 1,032評論 0 12
  • 生活的小滿足,可能就因為100塊塞滿小冰箱的蔬菜水果恨豁; 生活的小滿足,可能就因為自己做的美味的鯽魚豆腐湯爬迟; 生活的...
    子_一閱讀 129評論 0 0