一、常見概念(可跳過)
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)為什么需要 hashCode() ?以 HashSet 檢查重復元素為例。
當加入一個 對象 到 HashSet 中時,HashSet 首先會計算出 對象的 HashCode 值,並根據該值定位到 對象加入的位置,若不存在相同的 hashCode 值,直接添加對象即可。若此時 存在相同 的 hashCode,則通過 equals() 去比較兩個 對象是否 真的相同,如果相同,那么 HashSet 此次加入操作失敗,如果不同,則會將該數據散列到 其他位置。
引入了 hashCode(),可以直接定位到 需要比較的位置,從而減少了 沒必要的 equals 比較次數,提高執行速度。
(3)hashCode() 與 equals() 關系
equals() 相同的兩個對象,hashCode() 必定相同。
hashCode() 相同的兩個對象,equals() 不一定相同。
equals() 重寫時,hashCode() 一般也要重寫。
注:
hashCode() 默認行為是對 堆中對象 產生獨特值,如果沒有重寫 hashCode(),那么即使這兩個對象指向相同的數據,這兩個對象也不會相等。
5、String、StringBuilder、StringBuffer 的區別?
常用方法詳見:https://www.cnblogs.com/l-y-h/p/10910596.html
通過 JVM 研究一下 Sring:https://www.cnblogs.com/l-y-h/p/13554451.html#_label1
(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; } } }
9、Java 面向對象編程三大特性
(1)三大特性:
封裝、繼承、多態。
(2)封裝:
封裝 指的是 將客觀的事物 包裝成抽象的類,並封裝其代碼邏輯、通過訪問權限控制符 來控制訪問方式,進而保護程序。
比如:將類 的成員變量 私有化,並提供給 外界 一個 訪問、修改 該變量的方法。如果 不想該成員變量 被外界訪問,那么可以 不對外提供 訪問、修改 自身的方法。
注:
如果一個類 沒有提供給外界 訪問自身的方法,那么這個類一般沒什么實際意義。
使用反射時,可以獲取到 對象 未對外提供的 私有方法以及成員,此時會破壞封裝性。
(3)繼承:
繼承 指的是 以某個類為基礎 創建新的類(代碼復用)。基礎類 稱為 父類,新的類 稱為 子類。
子類 通過 繼承父類的方式,可以獲取到 父類所有的屬性、方法(包括私有屬性、方法,但是子類只是擁有,無法修改)。子類可以 編寫自己的方法、擴展程序。
注:
對於 父類 非 private 修飾的 方法、屬性,子類可以直接訪問、修改。
對於 父類 private 修飾的 方法、屬性,子類只是擁有、不能直接修改,可以通過 父類對外暴露 的 方法進行 訪問、修改。
(4)多態:
多態 指的是 一個類的實例 的相同方法 在不同情形下有不同的結果,一般在 程序運行期 才能確定。也即 一個 引用變量 其具體的 引用類型 以及 通過 該引用變量 真實調用的 方法 只有在 程序運行期 才能確定。
多態常見形式:
繼承。多個子類 繼承 一個父類,並對 父類中的 同一個方法 進行方法重寫。此時使用 父類 去聲明一個子類時,需要在程序運行期 才能知道具體是哪個子類。
接口。實現接口 並 覆蓋接口中的 抽象方法,那么也需要在 程序運行期 才能確定 真實調用的方法。
10、接口 與 抽象類的區別?
(1)接口:
Java 8 之前,接口中 方法默認修飾符為 public abstract(可省略),且沒有方法體(即方法不能有默認實現)。Java8 開始后,接口中 方法可以有默認實現(使用 default 去修飾方法)。
接口中成員變量 默認修飾符為 public final(即 常量)。
一個類可以實現多個接口,但是只能實現一個抽象類。
一個類實現 接口 時,必須重寫該接口的所有抽象方法(default 方法可以不重寫)。
接口不能直接通過 new 去實例化,但是可以通過 new 實例化 實現該接口 的子類。
(2)抽象類:
抽象類中可以存在 非抽象方法,並定義方法體(默認實現)。
抽象類中可以存在 非 final 修飾的成員變量。
一個類繼承 抽象類 時,必須重寫該抽象類中所有抽象方法(非抽象方法可以不重寫)。
(3)使用場景:
抽象是對 類 的抽象,一般存儲 某類事物共有的屬性、方法。屬於 模板設計。
接口是對 行為 的抽象,一般存儲 某類事物 特有的方法。屬於 行為規范。
11、常用獲取鍵盤輸入的方式
【方式一:通過 Scanner】 Scanner scanner = new Scanner(System.in); // 獲取一行數據 String str = scanner.nextLine(); // 獲取下一個整型數據 int age = scanner.nextInt(); System.out.println(str); System.out.println(age); scanner.close(); 【方式二:通過 BufferedReader】 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(System.in)); try { // 獲取一行數據 String str = bufferedReader.readLine(); System.out.println(str); bufferedReader.close(); } catch (IOException e) { e.printStackTrace(); }
二、易混概念(可以瞅兩眼)
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; // 調用靜態內部類的靜態屬性 } }