一、常見概念(可跳過)
1、JVM、JRE、JDK 關系與區別?
(1)JVM:
指的是 Java 虛擬機,加載編譯好的 位元組碼(.class)檔案,將其轉為機器語言執行,
對于不同的系統,有不同的 JVM,Java 代碼編譯一次后,可以在不同系統上的 JVM 上執行,故 Java 代碼可以一次編譯、到處運行,
(2)JRE:
指的是 Java 最小的運行環境,包括 JVM 以及 Java 的系統類別庫,
(3)JDK:
指的是 Java 最小的開發環境,包括 JRE 以及 編譯、運行等開發工具,
2、String 賦值問題?
(1)直接賦值,即 String str = "hello";
對于直接賦值,值存在于 常量池中,若常量池存在 “hello”,則 str 指向這個字串,若不存在,則創建一個 “hello” 置于常量池中,并將其參考回傳,
(2)通過 new 關鍵字賦值,即 String str = new String("hello");
對于 new 實體化,如果常量池不存在 “hello”,會創建一個 “hello” 置于常量池中,然后 new 關鍵字會在 堆 中創建一個 String 物件,并將堆中的參考回傳,
即 new 實體化一個 String 物件時,會創建 一個 或 兩個 物件,
3、== 和 equals 的區別?
(1)== 用于比較兩個數的值是否相同,
對于 基本型別(char、boolean、byte、short、int、long、float、double), == 比較的是 值是否相同,
對于 參考型別(除基本型別外的型別),比較的是參考是否相同,即比較 指向參考變數的地址是否相同,
(2)equals 未重寫時,等價于 ==,
一般重寫后用于比較兩個物件的值是否相同(比如 String),
【舉例:】 public class Main { public static void main(String[] args) { String str1 = "hello"; String str2 = "hello"; String str3 = new String("hello"); String str4 = new String("hello"); System.out.println(1 == 1); // true,基本型別 直接 比較值,值相同,故為 true System.out.println(str1 == str2); // true,String 直接賦值,str1、str2指向常量池同一值,即參考地址相同,故為 true System.out.println(str1 == str3); // false,str3 為 new 實體化,在堆中創建,參考地址與 str1 不同,故為 false System.out.println(str1.equals(str3)); // true,String 重寫 equals 方法,比較的是值,值相同,故為 true System.out.println(str3 == str4); // false,str3、str4 均為 new 實體化,兩者參考地址不同,故為 false System.out.println(str3.equals(str4)); // true,比較的是值,值相同,故為 true } }

4、hashCode() 與 equals() 的關系?
(1) 作用:
hashCode() 方法用于獲取 哈希碼(散列值),其回傳一個 int 值,用于確定物件在哈希表中的索引位置,
equals() 方法用于判斷兩個值(物件)是否相同,
(2) 關系:
equals() 相同的兩個物件,hashCode() 必定相同,
hashCode() 相同的兩個物件,equals() 不一定相同,
equals() 重寫時,hashCode() 一般也要重寫,
5、String、StringBuilder、StringBuffer 的區別?
常用方法詳見:https://www.cnblogs.com/l-y-h/p/10910596.html
(1)String 是 final 修改的不可變類,
內部采用 final 修飾的 char 陣列來保存字串,故String 物件不可變,
是執行緒安全的,頻繁修改字串時,使用 String 會頻繁的創建物件,會影響效率,故適合操作少量資料,
(2)StringBuilder 與 StringBuffer 內部直接采用 char 陣列來保存字串,
適用于頻繁修改字串,且不會產生新的物件,
StringBuilder 沒有使用同步鎖,故執行緒不安全,適用于單執行緒下操作大量資料,
StringBuffer 使用同步鎖,故執行緒安全,適合于多執行緒下操作大量資料,
6、throw、throws 的區別?
(1)throw 用于方法內部,后跟例外物件,只能指明一個例外物件,
(2)throws 用于方法宣告上,后跟例外型別,可以一次指明多個例外型別,
【舉例:】 public class Main { public static void main(String[] args) throws Exception { try { System.out.println(10 / 0); } catch (ArithmeticException e) { throw new Exception(); } } }
7、final、finally、finalize 的區別?
(1)final 是修飾符,如果修飾類,則此類不能被繼承,修飾方法、變數,則表示此方法、變數不能被修改,
(2)finally 是 try - catch - finally 代碼塊的最后一部分,可以省略,如果存在,則表示一定會執行(除了特殊原因退出外,比如 System.exit(0)),
(3)finalize 是 Object 的一個方法,GC 執行時會自動呼叫被回收物件的該方法(執行時機不可控),
8、try - catch - finally 的使用?
(1)try - catch - finally 塊中 catch、finally 均可以省略,但是必須存在一個,即 try - catch 或者 try - finally 必須存在一個,
(2)對于 try - catch - finally 中出現 return 的情況時,若 finally 中無 return,則回傳 try 或 catch 中return 的結果,若 finally 中有 return,則回傳 finally 中 return 的結果,
public class Main { public static void main(String[] args) { System.out.println(test()); // 輸出 1,finally 中無 return,回傳 try 中 return 的結果 System.out.println(test2()); // 輸出 4,finally 中有 return,回傳 finally 中 return 的結果 } public static int test() { int a = 1; try { return a; }finally { a = 4; } } public static int test2() { int a = 1; try { return a; }finally { a = 4; return a; } } }

二、易混概念(可以瞅兩眼)
1、自增變數賦值問題
(1)問題:
下面代碼輸出的值為多少?(主要涉及 變數如何變化、賦值等問題)
public class Main { public static void main(String[] args) { int i = 1; i = i++; int j = i++; int k = i + ++i * i++; System.out.println("i = " + i); System.out.println("j = " + j); System.out.println("k = " + k); } }
(2)回答:
輸出如下:
i = 4 j = 1 k = 11
分析:
可以結合著下面的位元組碼檔案一起看,主要就是 區域變數表 以及 運算元堆疊 的值的變化,
此處簡單分析一下程序,
【int i = 1;】 此時 i = 1, 區域變數保存一個 i = 1 【i = i++;】 此時 i = 1, 對于 i++ ,會先把 i 的值壓入運算元堆疊,即運算元堆疊保存 i = 1,i++ 使 區域變數中為 i = 2, 然后 賦值操作(=)會將 運算元堆疊的值賦給區域變數 i,即最后區域變數中保存的為 i = 1, 注: i++ 可理解為先賦值(壓入運算元堆疊),再 ++(區域變數++) ++i 可理解為先 ++,再賦值 【int j = i++;】 此時 j = 1, i = 2, 與 i = i++ 類似,只是此時,賦值操作將值壓入 區域變數 j 中,所以 j = 1, i = 2, 【int k = i + ++i * i++;】 此時 i = 4, k = 11, 按順序壓入運算元堆疊,可以分為 i、++i、i++ 三步(運算元堆疊有三個值), 首先區域變數 i 為 2,運算元堆疊為 2, ++i 使區域變數 i = 3, 運算元堆疊為 3(先 ++,再壓堆疊), i++ 使區域變數 i = 4, 運算元堆疊為 3(先壓堆疊,再 ++). 由于 * 優先級高于 +,所以 ++i * i++ 會先執行,即 3 * 3 = 9. 最后執行 +,即 9 + 2 = 11,然后將該值賦給 k 變數, 所以,最后區域變數 i = 4,k = 11
(3)查看位元組碼
idea 中使用 bytecode viewer 插件可用于查看位元組碼檔案,

通過 選單欄的 view 中的 show bytecode,可以查看位元組碼檔案,

上例代碼轉為位元組碼檔案如下:(看不懂的,就直接跳過吧)
// class version 52.0 (52) // access flags 0x21 public class Main { // compiled from: Main.java // access flags 0x1 public <init>()V L0 LINENUMBER 1 L0 ALOAD 0 INVOKESPECIAL java/lang/Object.<init> ()V RETURN L1 LOCALVARIABLE this LMain; L0 L1 0 MAXSTACK = 1 MAXLOCALS = 1 // access flags 0x9 public static main([Ljava/lang/String;)V L0 LINENUMBER 3 L0 ICONST_1 ISTORE 1 L1 LINENUMBER 4 L1 ILOAD 1 IINC 1 1 ISTORE 1 L2 LINENUMBER 5 L2 ILOAD 1 IINC 1 1 ISTORE 2 L3 LINENUMBER 6 L3 ILOAD 1 IINC 1 1 ILOAD 1 ILOAD 1 IINC 1 1 IMUL IADD ISTORE 3 L4 LINENUMBER 7 L4 GETSTATIC java/lang/System.out : Ljava/io/PrintStream; NEW java/lang/StringBuilder DUP INVOKESPECIAL java/lang/StringBuilder.<init> ()V LDC "i = " INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; ILOAD 1 INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder; INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V L5 LINENUMBER 8 L5 GETSTATIC java/lang/System.out : Ljava/io/PrintStream; NEW java/lang/StringBuilder DUP INVOKESPECIAL java/lang/StringBuilder.<init> ()V LDC "j = " INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; ILOAD 2 INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder; INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V L6 LINENUMBER 9 L6 GETSTATIC java/lang/System.out : Ljava/io/PrintStream; NEW java/lang/StringBuilder DUP INVOKESPECIAL java/lang/StringBuilder.<init> ()V LDC "k = " INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder; ILOAD 3 INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder; INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String; INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V L7 LINENUMBER 10 L7 RETURN L8 LOCALVARIABLE args [Ljava/lang/String; L0 L8 0 LOCALVARIABLE i I L1 L8 1 LOCALVARIABLE j I L3 L8 2 LOCALVARIABLE k I L4 L8 3 MAXSTACK = 3 MAXLOCALS = 4 }
(4)總結
對于賦值操作(值 = 運算式),基本順序如下:
對于 = 右邊運算式,從左到右按順序執行,依次壓入運算元堆疊,然后根據運算式的優先級進行相關計算,
運算式中若出現自增、自減操作(++、--),會直接操作區域變數,不經過運算元堆疊,
賦值操作(=),是最后執行的,其會將運算元堆疊的最終的值回傳,
2、類初始化、實體化程序 以及 方法重寫等問題
(1)問題:
下面代碼輸出值為多少?(主要涉及 類初始化、實體化程序 以及 方法重寫等問題)
public class Main { public static void main(String[] args) { Son son1 = new Son(); System.out.println(); Son son2 = new Son(); System.out.println(); Son son3 = new Son(1); } } class Father { private int i = test(); private static int j = test2(); private final int K = test3(); Father() { System.out.print("(1)"); } static { System.out.print("(2)"); } { System.out.print("(3)"); } public int test() { System.out.print("(4)"); return 1; } public static int test2() { System.out.print("(5)"); return 1; } private int test3() { System.out.print("(6)"); return 1; } } class Son extends Father{ private int i = test(); private static int j = test2(); private final int K = test3(); Son() { System.out.print("(7)"); } Son(int i) { System.out.print("(8)"); } static { System.out.print("(9)"); } { System.out.print("(10)"); } public int test() { System.out.print("(11)"); return 1; } public static int test2() { System.out.print("(12)"); return 1; } private int test3() { System.out.print("(13)"); return 1; } }
(2)回答:
輸出如下:
(5)(2)(12)(9)(11)(6)(3)(1)(11)(13)(10)(7) (11)(6)(3)(1)(11)(13)(10)(7) (11)(6)(3)(1)(11)(13)(10)(8)
首先得對類初始化、實體化、方法重寫有個基本概念,
類初始化程序:
首先會找到 main 方法所在的類,然后加載、初始化,
加載時,若該類存在父類,則會先加載、初始化父類,
通過位元組碼檔案(使用 bytecode viewer 插件可以查看,此處不再重復截圖)可以看到 類初始化時,會執行 <clinit>() 相關操作,
clinit 主要加載的為 靜態代碼塊 和 靜態代碼變數,且執行順序為從上到下執行,只會執行一次(即只加載一次靜態變數、靜態代碼塊),
實體化程序:
通過位元組碼檔案,可以看到實體化程序,會執行 <init>() 相關操作,有幾個構造器,就有幾個 <init>(),當然,執行哪個構造器,才會執行那個 init 操作,
init 主要加載的為 非靜態代碼塊、非靜態變數、構造器,且執行順序為從上到下執行,但構造器最后執行,當然,若有父類,則會先加載父類(執行父類的 init 操作),
方法重寫(Override):
使用 final、static 關鍵字修飾的方法不可被重寫,
使用 private 修飾的方法,子類也不可重寫該方法,
非靜態方法默認呼叫物件為 this,即當前正在創建的物件,所以如果子類重寫了父類的方法,由于正在創建子類,執行的是子類的方法,而非父類的方法,
(3)執行結果分析
通過上面的基本概念,應該不難得到最后的輸出結果,
對于 Son son1 = new Son();
首先,執行類加載程序,
加載 main 方法所在的類 Son,因為其繼承 Father,所以先加載 Father、再加載 Son 的靜態變數、靜態代碼塊,即輸出:(5)(2)(12)(9),
然后,執行實體化程序,
先加載 Father,再加載 Son,由于 父類的 test 被 子類的 test 方法覆寫(重寫),而 test3 被 final 修飾,不會被子類的 test3 覆寫,構造器最后加載,所以輸出結果為:(11)(6)(3)(1)(11)(13)(10)(7),
對于 Son son2 = new Son();
由于之前已經執行過一次 類加載程序,所以此處不會再執行,即輸出結果為 (11)(6)(3)(1)(11)(13)(10)(7),
對于 Son son3 = new Son(1);
由于指定了不同的構造器,所以會執行不同的 init 操作,即輸出結果為 (11)(6)(3)(1)(11)(13)(10)(8),
(4)總結
一般加載程序:
父類的靜態變數 -》 父類的靜態代碼塊 -》 子類的靜態變數 -》 子類的靜態代碼塊 -》父類的非靜態變數 -》 父類非靜態代碼塊 -》 父類的構造方法 -》 子類的非靜態變數 -》 子類的非靜態代碼塊 -》 子類的構造方法,
對于重寫方法(非 final、static、private 修飾的父類方法),會執行子類的方法,
3、方法引數傳遞問題
(1)問題:
下面代碼輸出值為多少?(主要涉及 引數傳參的值傳遞、參考傳遞問題)
import java.util.Arrays; public class Main { public static void main(String[] args) { int i = 1; Integer num = 2; Integer num2 = 200; String string = "hello"; int[] arrays = new int[]{1, 2, 3, 4, 5}; int[] arrays2 = new int[]{1, 2, 3, 4, 5}; Test test = new Test(); changeValue(i, num, num2, string, arrays, arrays2, test); System.out.println("i = " + i); System.out.println("num = " + num); System.out.println("num2 = " + num2); System.out.println("string = " + string); System.out.println("arrays = " + Arrays.toString(arrays)); System.out.println("arrays2 = " + Arrays.toString(arrays2)); System.out.println("test = " + test.a); } public static void changeValue(int i, Integer num, Integer num2, String string, int[] arrays, int[] arrays2, Test test) { i++; num++; num2++; string += " world"; arrays[2]++; arrays2 = new int[]{1, 2}; test.a++; } } class Test { public int a = 10; }
(2)回答;
輸出值如下:
i = 1 num = 2 num2 = 200 string = hello arrays = [1, 2, 4, 4, 5] arrays2 = [1, 2, 3, 4, 5] test = 11
首先對值傳遞、參考傳遞有個基本認識:
值傳遞:傳遞資料值,
參考傳遞:傳遞地址值(資料所在的地址),
形參、實參:
形參就是 方法中的 引數,
實參就是 實際要傳遞給 方法的 引數,
值傳遞時,形參的變化不會影響實參中的資料,
參考傳遞時,由于傳遞的是地址,若修改地址,則不會影響實參的資料,但若是通過地址修改資料,則會影響實參的資料,
在 Java 中,
對于基本資料型別,其使用 值傳遞,即實參向形參中傳遞的為 資料值,
對于參考資料型別,其使用 參考傳遞,即實參向形參中傳遞的為 資料所在的地址值,
當然,對于 String、以及 Integer 等包裝類,由于其值的不可變性,其值變化時,會重新指向另一個變數(即地址發生改變),
(3)分析:
【int i = 1;】 由于 int 屬于基本資料型別,其傳遞的是資料值,形參的變化不影響, 即 i++ 不會影響原資料,輸出 i = 1. 【Integer num = 2;】 Integer 屬于包裝類,但由于其不可變性,其值變化時,會重新指向另一個物件, 也即地址改變,所以不會影響原資料,輸出 num = 2. 注: Integer 內部 快取了 -128 ~ 127 的值, 即使用 Integer 保存了 -128 ~ 127 的值時,其不會重新 new 個物件,為同一個物件, Integer test1 = 1; Integer test2 = 1; System.out.println(test1 == test2); // 輸出結果為 true 【Integer num2 = 200;】 同上處理,輸出 num2 = 200, 【String string = "hello";】 String 型別也存在不可變性,與包裝類類似,修改值修改的是地址,不會影響原資料, 所以輸出值為 string = hello 【int[] arrays = new int[]{1, 2, 3, 4, 5};】 對于陣列,其傳遞的也是 地址, 由于此處 arrays[2]++; 是通過地址修改資料,所以原資料會被修改, 即輸出 arrays = [1, 2, 4, 4, 5] 【int[] arrays2 = new int[]{1, 2, 3, 4, 5};】 同上,但是 arrays2 = new int[]{1, 2}; 修改的是地址,所以不會影響原資料, 即輸出 arrays2 = [1, 2, 3, 4, 5], 【Test test = new Test();】 對于自定義型別,同樣傳遞的是 地址, test.a++; 通過地址修改值,會影響到原資料, 所以輸出 test = 11,
(4)總結
對于基本型別,引數傳遞為值傳遞,傳遞的為資料值,其值修改不會影響到原資料,
對于參考型別,引數傳遞為參考傳遞,傳遞的為資料的地址,若直接修改此值(即修改的是地址),不會影響到原資料,但若通過地址去修改資料,則會影響到原資料,
對于 String、Integer 型別,由于其不可變性,其值修改相當于修改了地址,所以不會影響到原資料,
4、變數、作用域問題
(1)問題:
下面代碼輸出值為多少?(主要涉及 成員變數、區域變數、以及變數作用域問題)
public class Main { public static void main(String[] args) { Test test1 = new Test(); Test test2 = new Test(); test1.test(10); test1.test(20); test2.test(30); System.out.println("test1: i = " + test1.i + " , j = " + test1.j + " , s = " + test1.s); System.out.println("test2: i = " + test2.i + " , j = " + test2.j + " , s = " + test2.s); } } class Test { static int s; int i; int j; { int i = 1; i++; j++; s++; } public void test(int j) { i++; j++; s++; } }
(2)回答:
輸出如下:
test1: i = 2 , j = 1 , s = 5
test2: i = 1 , j = 1 , s = 5
首先得了解一下 變數、作用域 相關知識,
變數分類:
成員變數,又可分為 類變數、實體變數,
區域變數,
成員變數、區域變數的區別:
diff1:宣告位置,
區域變數出現在 方法體、代碼塊、以及形參 中,
成員變數出現在 類中,方法體或代碼塊 外,
其中:
使用 static 修飾的成員變數稱為類變數,可以使用 類名.變數名 或者 物件.變數名 獲取值,
沒有 static 修飾的成員變數為實體變數,只能通過 物件.變數名 獲取值,
diff2:修飾符使用,
區域變數最多只能被 final 關鍵字修飾,
成員變數可被 static、final、public、protected、private、volatile、transient 等修飾,
diff3:值存盤的位置,
區域變數存盤在 堆疊中(存放基本型別資料以及 物件參考資料(物件所在堆記憶體的首地址)),
實體變數存盤在 堆中(存放物件實體資料),
類變數存盤在 方法區中(存放類資訊,比如常量、靜態變數等),
diff4:生命周期、作用域
區域變數作用域為當前代碼塊,每次執行時都是一個新的生命周期,
實體變數作用域為當前物件,隨物件的創建而初始化、銷毀而消失,
類變數作用域為當前類,隨類的初始化而初始化、銷毀而消失,且類變數在所有物件中共用,
注:
實體變數在當前類中使用 "this.變數名" 呼叫,可以預設 this,在 其他類中使用 "物件名.變數名" 呼叫,
類變數在當前類中使用 "類名.變數名" 呼叫,可以預設 類名,在 其他類中 可以使用 "類名.變數名" 或者 "物件名.變數名" 呼叫,
(3)分析
【Test test1 = new Test();】 執行時,會先加載 代碼塊, 由于 s 為類變數(所有物件共用),i 為區域變數(離開代碼塊失效),j 為實體變數, 所以執行后 i = 0, j = 1, s = 1. 【Test test2 = new Test();】 同理,會再次加載代碼塊,但是屬于兩個物件,所以實體變數不會相互影響, 所以執行后 i = 0, j = 1, s = 2. 【test1.test(10);】 執行 test 方法,此時 i 為實體變數, j 為區域變數(形參), s 為類變數, 所以執行后 i = 1, j = 1, s = 3. 【test1.test(20);】 同理,執行 test 方法, 執行后 i = 2, j = 1, s = 4. 【test2.test(30);】 執行后 i = 1, j = 1, s = 5. 所以最后輸出: test1: i = 2 , j = 1 , s = 5 test2: i = 1 , j = 1 , s = 5
(4)總結
類變數被所有實體物件所共享,
在代碼塊中,若區域變數與 實體變數 重名,則需要以 this.變數名來指定 實體變數,否則會默認為區域變數,
若區域變數 與 類變數重名,則需要以 類名.變數名 指定類變數,否則會默認為 區域變數,
5、遞回、迭代問題
(1)問題:
使用 遞回、迭代 兩種方式實作 斐波那契數列,
斐波那契數列:
【數列形如:】 1, 1, 2, 3, 5, 8, 13, 21, 34,... 【數學公式:】 f(1) = 1 f(2) = 1 f(n) = f(n - 1) + f(n - 2)
當然,換一種方式描述這個問題(本質依舊是斐波那契數列):
有一對兔子,從出生后第3個月起每個月都生一對兔子,小兔子長到第三個月后每個月又生一對兔子,假如兔子都不死,問每個月的兔子總共有多少對?
(2)遞回方式實作
方法呼叫自身并解決問題的程序叫遞回,
優點:
大問題轉換為小問題,精簡代碼、減少代碼量、可讀性較好,
缺點:
遞回浪費了大量空間,遞回層數太深容易導致堆疊溢位,
分析:
簡單將兔子按月數分為三組:1 月兔、2 月兔、3 月兔,
【第 1 個月】 1 月兔不能繁殖, 兔子對數: 1 月兔 1 2 月兔 0 3 月兔 0 所以 f(1) = 1 = 1 + 0 + 0 【第 2 個月】 1 月兔變 2 月兔, 2 月兔不能繁殖, 兔子對數: 1 月兔 0 2 月兔 1 3 月兔 0 所以 f(2) = 1 = 0 + 1 + 0 【第 3 個月】 2 月兔變 3 月兔,3 月兔可以繁殖,生出 1 對 1 月兔, 兔子對數: 1 月兔 1 2 月兔 0 3 月兔 1 所以 f(3) = 2 = 1 + 1 + 0 從兔子對數的變化不難看出: f(3) = f(2) + f(1) 1 + 0 + 0 + 0 + 1 + 0 = 1 + 1 + 0 【第 4 個月】 1 月兔變 2 月兔,2 月兔變 3 月兔, 3 月兔可以繁殖,再生出 1 對 1 月兔, 兔子對數: 1 月兔 1 2 月兔 1 3 月兔 1 所以 f(4) = 3 從兔子對數的變化不難看出: f(4) = f(3) + f(2) 【第五個月】 同理, 兔子對數: 1 月兔 2 2 月兔 1 3 月兔 2 所以 f(5) = 5 從兔子對數的變化不難看出: f(5) = f(4) + f(3), 從而推出公式: f(1) = 1, n = 1 f(2) = 2, n = 2 f(n) = f(n-1) + f(n-2), n >= 3
代碼實作:
public class Main { public static void main(String[] args) { for (int i = 1; i < 10; i++) { System.out.println(test(i)); } } public static int test(int n) { if (n == 1 || n == 2) { return 1; } else { return test(n - 1) + test(n - 2); } } }

(3)迭代方法實作
利用變數原值計算出新值并解決問題的程序叫迭代,
優點:
空間開銷小,運行效率比遞回高,
缺點:
代碼不夠簡潔、可讀性較差,
分析:
同樣將兔子按月數分為三組:1 月兔、2 月兔、3 月兔,
使用 one 保存最后 1 月兔的對數,初始值為 1,
使用 two 保存最后 2 月兔、3 月兔的對數,初始值為 1,
使用 sum 保存最終數量,初始值為 0,
可以發現一個有意思的現象,
1 月兔在下個月會轉為 2 月兔,
2 月兔在下個月會轉為 3 月兔,3 月兔會生 1 月兔,
3 月兔在每個月均會生 1 月兔,
也即下一次 1 月兔的數量,在于 2 月兔、3 月兔總數量,
下一次 2 月兔、3月兔的數量,在于兔子總數量,
所以:
sum = one + two
one = two
two = sum
代碼實作:
public class Main { public static void main(String[] args) { for (int i = 1; i < 10; i++) { System.out.println(test(i)); } } public static int test(int n) { int one = 1, two = 1, sum = 0; if (n == 1 || n == 2) { return 1; } for (int i = 3; i <= n; i++) { sum = one + two; one = two; two = sum; } return sum; } }

6、單例設計模式問題
之前曾總結過一次單例設計模式,詳見:https://www.cnblogs.com/l-y-h/p/11290728.html
(1)問題:
什么是單例設計模式?
單例設計模式注意點、實作?
單例設計模式的幾種實作方式?
(2)回答:
單例設計模式(Singleton):
指的是某個類在整個系統中只有一個實體物件能夠被獲取和使用的一種代碼模式(比如 JVM 中的 Runtime 類),
單例設計模式注意點:
某個類只能有一個實體物件,
這個類必須自行創建其實體物件,
這個類必須向外暴露出這個實體物件,
單例設計模式實作:
構造器私有化,只能在該類內部呼叫 new 關鍵字,防止外部類通過 new 關鍵字創建實體物件,
使用靜態變數保存這個實體物件,
使用靜態方法向外暴露出這個實體物件(靜態變數私有化時必須存在,要不然無法獲取到實體物件),
單例設計模式的幾種方式:
餓漢式:直接創建物件,不存在執行緒安全問題,
懶漢式:延遲創建物件,使用時再創建,可能會有執行緒安全問題,
餓漢式分類:
靜態常量版,宣告變數時,直接實體化一個物件,
靜態代碼塊,在靜態代碼塊中實體化物件,
列舉型,通過列舉的方式指定,可以防止反序列化、反射問題(推薦使用),
懶漢式分類:
執行緒不安全(直接通過靜態方法回傳實體物件,多執行緒下,可能會同時執行回傳多個實體),
執行緒安全(使用同步關鍵字實作同步代碼塊、同步方法),
執行緒安全(雙重檢查,提高同步執行的效率),
執行緒安全(靜態內部類,推薦使用),
(3)代碼實作
具體代碼可以參考:https://www.cnblogs.com/l-y-h/p/11290728.html
此處僅舉兩個例子:
對于餓漢式來說,列舉實作最簡單,
對于懶漢式來說,靜態內部類實作最簡單,
列舉形式:
public class Main { public static void main(String[] args) { SingleTon singleTon1 = SingleTon.INSTANCE; SingleTon singleTon2 = SingleTon.INSTANCE; System.out.println(singleTon1 == singleTon2); } } enum SingleTon { INSTANCE; }

靜態內部類:
public class Main { public static void main(String[] args) { SingleTon singleTon1 = SingleTon.getSingleTon(); SingleTon singleTon2 = SingleTon.getSingleTon(); System.out.println(singleTon1 == singleTon2); } } class SingleTon { /** * 構造器私有化(防止通過new創建實體物件) */ private SingleTon() { } /** * 靜態內部類,在被呼叫的時候才會被加載,實作懶加載, 且內部使用靜態常量實體化一個物件,保證了執行緒安全問題, */ private static class SingleTonInstance { private static final SingleTon INSTANCE = new SingleTon(); } /** * 向外暴露一個靜態的公共方法用于獲取實體物件. */ public static SingleTon getSingleTon() { return SingleTonInstance.INSTANCE; // 呼叫靜態內部類的靜態屬性 } }

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/169421.html
標籤:Java
上一篇:Java 必須掌握的 12 種 Spring 常用注解!
下一篇:如何寫好一個 Spring 組件
