備戰-Java 基礎

濤姐濤哥發表於2021-07-15

備戰-Java 基礎

 

        仰天大笑出門去,我輩豈是蓬蒿人。

 

簡介:備戰-Java 基礎。

一、基本資料型別

1、Java基本資料型別

基本資料型別有8種:byte、short、int、long、float、double、boolean、char

分為4類:整數型、浮點型、布林型、字元型。

整數型:byte、short、int、long

浮點型:float、double

布林型:boolean

字元型:char

2、各資料型別所佔位元組大小

計算機的基本單位:bit .  一個bit代表一個0或1

  byte:1byte = 8bit     1個位元組是8個bit

  short:2byte

  int:4byte

  long:8byte

  float:4byte

  double:8byte

  boolean:1byte

  char:2byte

boolean 只有兩個值:true、false,可以使用 1 bit 來儲存,但是具體大小沒有明確規定。JVM 會在編譯時期將 boolean 型別的資料轉換為 int,使用 1 來表示 true,0 表示 false。JVM 支援 boolean 陣列,但是是通過讀寫 byte 陣列來實現的。

3、包裝型別

基本型別都有對應的包裝型別,基本型別與其對應的包裝型別之間的賦值使用自動裝箱與拆箱完成。

Integer x = 2;     // 裝箱 呼叫了 Integer.valueOf(2)
int y = x;         // 拆箱 呼叫了 X.intValue()

4、緩衝池

new Integer(123) 與 Integer.valueOf(123) 的區別在於:

  • new Integer(123) 每次都會新建一個物件;
  • Integer.valueOf(123) 會使用快取池中的物件,多次呼叫會取得同一個物件的引用。
備戰-Java 基礎
Integer x = new Integer(123);
Integer y = new Integer(123);
System.out.println(x == y);    // false
Integer z = Integer.valueOf(123);
Integer k = Integer.valueOf(123);
System.out.println(z == k);   // true
View Code

valueOf() 方法的實現是先判斷值是否在快取池中,如果在的話就直接返回快取池的內容。

備戰-Java 基礎
1 public static Integer valueOf(int i) {
2     if (i >= IntegerCache.low && i <= IntegerCache.high)
3         return IntegerCache.cache[i + (-IntegerCache.low)];
4     return new Integer(i);
5 }
View Code

在 Java 8 中,Integer 快取池的大小預設為 -128~127。

備戰-Java 基礎
 1 static final int low = -128;
 2 static final int high;
 3 static final Integer cache[];
 4 
 5 static {
 6     // high value may be configured by property
 7     int h = 127;
 8     String integerCacheHighPropValue =
 9         sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
10     if (integerCacheHighPropValue != null) {
11         try {
12             int i = parseInt(integerCacheHighPropValue);
13             i = Math.max(i, 127);
14             // Maximum array size is Integer.MAX_VALUE
15             h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
16         } catch( NumberFormatException nfe) {
17             // If the property cannot be parsed into an int, ignore it.
18         }
19     }
20     high = h;
21 
22     cache = new Integer[(high - low) + 1];
23     int j = low;
24     for(int k = 0; k < cache.length; k++)
25         cache[k] = new Integer(j++);
26 
27     // range [-128, 127] must be interned (JLS7 5.1.7)
28     assert IntegerCache.high >= 127;
29 }
View Code

編譯器會在自動裝箱過程呼叫 valueOf() 方法,因此多個值相同且值在快取池範圍內的 Integer 例項使用自動裝箱來建立,那麼就會引用相同的物件。

1 Integer m = 123;
2 Integer n = 123;
3 System.out.println(m == n); // true

基本型別對應的緩衝池如下:

  • boolean values true and false
  • all byte values
  • short values between -128 and 127
  • int values between -128 and 127
  • char in the range \u0000 to \u007F

在使用這些基本型別對應的包裝型別時,如果該數值範圍在緩衝池範圍內,就可以直接使用緩衝池中的物件。

在 jdk 1.8 所有的數值類緩衝池中,Integer 的緩衝池 IntegerCache 很特殊,這個緩衝池的下界是 - 128,上界預設是 127,但是這個上界是可調的,在啟動 jvm 的時候,通過 -XX:AutoBoxCacheMax=<size> 來指定這個緩衝池的大小,該選項在 JVM 初始化的時候會設定一個名為 java.lang.IntegerCache.high 系統屬性,然後 IntegerCache 初始化的時候就會讀取該系統屬性來決定上界。

二、String

String 被宣告為 final,因此它不可被繼承。(Integer 等包裝類也不能被繼承)

在 Java 8 中,String 內部使用 char 陣列儲存資料。

1 public final class String
2     implements java.io.Serializable, Comparable<String>, CharSequence {
3     /** The value is used for character storage. */
4     private final char value[];
5 }

在 Java 9 之後,String 類的實現改用 byte 陣列儲存字串,同時使用 coder 來標識使用了哪種編碼。

備戰-Java 基礎
1 public final class String
2     implements java.io.Serializable, Comparable<String>, CharSequence {
3     /** The value is used for character storage. */
4     private final byte[] value;
5 
6     /** The identifier of the encoding used to encode the bytes in {@code value}. */
7     private final byte coder;
8 }
View Code

value 陣列被宣告為 final,這意味著 value 陣列初始化之後就不能再引用其它陣列。並且 String 內部沒有改變 value 陣列的方法,因此可以保證 String 不可變。

1、String 不可變的好處

1. 可以快取 hash 值

因為 String 的 hash 值經常被使用,例如 String 用做 HashMap 的 key。不可變的特性可以使得 hash 值也不可變,因此只需要進行一次計算。

2. String Pool 的需要

如果一個 String 物件已經被建立過了,那麼就會從 String Pool 中取得引用。只有 String 是不可變的,才可能使用 String Pool。

3. 安全性

String 經常作為引數,String 不可變性可以保證引數不可變。例如在作為網路連線引數的情況下如果 String 是可變的,那麼在網路連線過程中,String 被改變,改變 String 的那一方以為現在連線的是其它主機,而實際情況卻不一定是。

4. 執行緒安全

String 不可變性天生具備執行緒安全,可以在多個執行緒中安全地使用。

2、String、StringBuffer、StringBuilder

1. 可變性

  • String 不可變
  • StringBuffer 和 StringBuilder 可變

2. 執行緒安全

  • String 不可變,因此是執行緒安全的
  • StringBuilder 不是執行緒安全的
  • StringBuffer 是執行緒安全的,內部使用 synchronized 進行同步

3、String Pool

字串常量池(String Pool)儲存著所有字串字面量(literal strings),這些字面量在編譯時期就確定。不僅如此,還可以使用 String 的 intern() 方法在執行過程將字串新增到 String Pool 中。

當一個字串呼叫 intern() 方法時,如果 String Pool 中已經存在一個字串和該字串值相等(使用 equals() 方法進行確定),那麼就會返回 String Pool 中字串的引用;否則,就會在 String Pool 中新增一個新的字串,並返回這個新字串的引用。

下面示例中,s1 和 s2 採用 new String() 的方式新建了兩個不同字串,而 s3 和 s4 是通過 s1.intern() 和 s2.intern() 方法取得同一個字串引用。intern() 首先把 "aaa" 放到 String Pool 中,然後返回這個字串引用,因此 s3 和 s4 引用的是同一個字串。

備戰-Java 基礎
1 String s1 = new String("aaa");
2 String s2 = new String("aaa");
3 System.out.println(s1 == s2);           // false
4 String s3 = s1.intern();
5 String s4 = s2.intern();
6 System.out.println(s3 == s4);           // true
View Code

如果是採用 "bbb" 這種字面量的形式建立字串,會自動地將字串放入 String Pool 中。

1 String s5 = "bbb";
2 String s6 = "bbb";
3 System.out.println(s5 == s6);  // true

在 Java 7 之前,String Pool 被放在執行時常量池中,它屬於永久代。而在 Java 7,String Pool 被移到堆中。這是因為永久代的空間有限,在大量使用字串的場景下會導致 OutOfMemoryError 錯誤

4、new String("abc")

使用這種方式一共會建立兩個字串物件(前提是 String Pool 中還沒有 "abc" 字串物件)。

  • "abc" 屬於字串字面量,因此編譯時期會在 String Pool 中建立一個字串物件,指向這個 "abc" 字串字面量;
  • 而使用 new 的方式會在堆中建立一個字串物件。

建立一個測試類,其 main 方法中使用這種方式來建立字串物件。

1 public class NewStringTest {
2     public static void main(String[] args) {
3         String tjt = new String("2021-07-14");
4     }
5 }

使用 javap -verbose 進行反編譯,得到以下內容:

在 Constant Pool 中,#17 儲存這字串字面量 "2021-07-14",#3 是 String Pool 的字串物件,它指向 #17 這個字串字面量。在 main 方法中,0: 行使用 new #2 在堆中建立一個字串物件,並且使用 ldc #3 將 String Pool 中的字串物件作為 String 建構函式的引數。

以下是 String 建構函式的原始碼,可以看到,在將一個字串物件作為另一個字串物件的建構函式引數時,並不會完全複製 value 陣列內容,而是都會指向同一個 value 陣列。

1 public String(String original) {
2     this.value = original.value;
3     this.hash = original.hash;
4 }

三、運算

1、引數傳遞

Java 的引數是以值傳遞的形式傳入方法中,而不是引用傳遞。

以下程式碼中 Dog dog 的 dog 是一個指標,儲存的是物件的地址。在將一個引數傳入一個方法時,本質上是將物件的地址以值的方式傳遞到形參中。

備戰-Java 基礎
 1 public class Dog {
 2 
 3     String name;
 4 
 5     Dog(String name) {
 6         this.name = name;
 7     }
 8 
 9     String getName() {
10         return this.name;
11     }
12 
13     void setName(String name) {
14         this.name = name;
15     }
16 
17     String getObjectAddress() {
18         return super.toString();
19     }
20 }
View Code

在方法中改變物件的欄位值會改變原物件該欄位值,因為引用的是同一個物件。

備戰-Java 基礎
 1 class PassByValueExample {
 2     public static void main(String[] args) {
 3         Dog dog = new Dog("A");
 4         func(dog);
 5         System.out.println(dog.getName());          // B
 6     }
 7 
 8     private static void func(Dog dog) {
 9         dog.setName("B");
10     }
11 }
View Code

但是在方法中將指標引用了其它物件,那麼此時方法裡和方法外的兩個指標指向了不同的物件,在一個指標改變其所指向物件的內容對另一個指標所指向的物件沒有影響。

備戰-Java 基礎
 1 public class PassByValueExample {
 2     public static void main(String[] args) {
 3         Dog dog = new Dog("A");
 4         System.out.println(dog.getObjectAddress()); // Dog@4554617c
 5         func(dog);
 6         System.out.println(dog.getObjectAddress()); // Dog@4554617c
 7         System.out.println(dog.getName());          // A
 8     }
 9 
10     private static void func(Dog dog) {
11         System.out.println(dog.getObjectAddress()); // Dog@4554617c
12         dog = new Dog("B");
13         System.out.println(dog.getObjectAddress()); // Dog@74a14482
14         System.out.println(dog.getName());          // B
15     }
16 }
View Code

2、float 與 double

Java 不能隱式執行向下轉型,因為這會使得精度降低。

1.1 字面量屬於 double 型別,不能直接將 1.1 直接賦值給 float 變數,因為這是向下轉型。

// float f = 1.1;

1.1f 字面量才是 float 型別。

float f = 1.1f;

3、隱式型別轉換

因為字面量 1 是 int 型別,它比 short 型別精度要高,因此不能隱式地將 int 型別向下轉型為 short 型別。

1 short s1 = 1;
2 // s1 = s1 + 1;   // Incompatible types

但是使用 += 或者 ++ 運算子會執行隱式型別轉換。

1 s1 += 1;
2 s1++;

上面的語句相當於將 s1 + 1 的計算結果進行了向下轉型:

s1 = (short) (s1 + 1);

4、switch

從 Java 7 開始,可以在 switch 條件判斷語句中使用 String 物件。

備戰-Java 基礎
1 String s = "a";
2 switch (s) {
3     case "a":
4         System.out.println("aaa");
5         break;
6     case "b":
7         System.out.println("bbb");
8         break;
9 }
View Code

switch 不支援 long、float、double,是因為 switch 的設計初衷是對那些只有少數幾個值的型別進行等值判斷,如果值過於複雜,那麼還是用 if 比較合適。

備戰-Java 基礎
1 // long x = 111;
2 // switch (x) { // Incompatible types. Found: 'long', required: 'char, byte, short, int, Character, Byte, Short, Integer, String, or an enum'
3 //     case 111:
4 //         System.out.println(111);
5 //         break;
6 //     case 222:
7 //         System.out.println(222);
8 //         break;
9 // }
View Code

四、關鍵字

1、final

1. 資料

final 宣告資料為常量,可以是編譯時常量,也可以是在執行時被初始化後不能被改變的常量。

  • 對於基本型別,final 使數值不變;
  • 對於引用型別,final 使引用不變,也就不能引用其它物件,但是被引用的物件本身是可以修改的。
1 final int x = 1;
2 // x = 2;  // cannot assign value to final variable 'x'
3 final A y = new A();
4 y.a = 1;

2. 方法

final 宣告方的法不能被子類重寫。

private 方法隱式地被指定為 final,如果在子類中定義的方法和基類中的一個 private 方法簽名相同,此時子類的方法不是重寫基類方法,而是在子類中定義了一個新的方法。

3. 類

final 宣告的類不允許被繼承。

2、static

1. 靜態變數

  • 靜態變數:又稱為類變數,也就是說這個變數屬於類的,類所有的例項都共享靜態變數,可以直接通過類名來訪問它。靜態變數在記憶體中只存在一份。
  • 例項變數:每建立一個例項就會產生一個例項變數,它與該例項同生共死。
備戰-Java 基礎
 1 public class A {
 2 
 3     private int x;         // 例項變數
 4     private static int y;  // 靜態變數
 5 
 6     public static void main(String[] args) {
 7         // int x = A.x;  // Non-static field 'x' cannot be referenced from a static context
 8         A a = new A();
 9         int x = a.x;
10         int y = A.y;
11     }
12 }
View Code

2. 靜態方法

靜態方法在類載入的時候就存在了,它不依賴於任何例項。所以靜態方法必須有實現,也就是說它不能是抽象方法。

1 public abstract class A {
2     public static void func1(){
3     }
4     // public abstract static void func2();  // Illegal combination of modifiers: 'abstract' and 'static'
5 }

只能訪問所屬類的靜態欄位和靜態方法,方法中不能有 this 和 super 關鍵字,因為這兩個關鍵字與具體物件關聯。

備戰-Java 基礎
 1 public class A {
 2 
 3     private static int x;
 4     private int y;
 5 
 6     public static void func1(){
 7         int a = x;
 8         // int b = y;  // Non-static field 'y' cannot be referenced from a static context
 9         // int b = this.y;     // 'A.this' cannot be referenced from a static context
10     }
11 }
View Code

3. 靜態語句塊

靜態語句塊在類初始化時執行一次。

備戰-Java 基礎
 1 public class A {
 2     static {
 3         System.out.println("123");
 4     }
 5 
 6     public static void main(String[] args) {
 7         A a1 = new A();
 8         A a2 = new A();
 9     }
10 }
View Code

4. 靜態內部類

非靜態內部類依賴於外部類的例項,也就是說需要先建立外部類例項,才能用這個例項去建立非靜態內部類。而靜態內部類不需要。

備戰-Java 基礎
 1 public class OuterClass {
 2 
 3     class InnerClass {
 4     // 非靜態內部類
 5     }
 6 
 7     static class StaticInnerClass {
 8      // 靜態內部類
 9     }
10 
11     public static void main(String[] args) {
12         // InnerClass innerClass = new InnerClass(); // 'OuterClass.this' cannot be referenced from a static context
13         OuterClass outerClass = new OuterClass();
14         InnerClass innerClass = outerClass.new InnerClass();
15         StaticInnerClass staticInnerClass = new StaticInnerClass();
16     }
17 }
View Code

同時,靜態內部類不能訪問外部類的非靜態的變數和方法。

5. 靜態導包

在使用靜態變數和方法時不用再指明 ClassName,從而簡化程式碼,但可讀性大大降低。

import static com.xxx.ClassName.*

6. 初始化順序

靜態變數和靜態語句塊優先於例項變數和普通語句塊,靜態變數和靜態語句塊的初始化順序取決於它們在程式碼中的順序。

五、Object 通用方法

備戰-Java 基礎
 1 public native int hashCode()
 2 
 3 public boolean equals(Object obj)
 4 
 5 protected native Object clone() throws CloneNotSupportedException
 6 
 7 public String toString()
 8 
 9 public final native Class<?> getClass()
10 
11 protected void finalize() throws Throwable {}
12 
13 public final native void notify()
14 
15 public final native void notifyAll()
16 
17 public final native void wait(long timeout) throws InterruptedException
18 
19 public final void wait(long timeout, int nanos) throws InterruptedException
20 
21 public final void wait() throws InterruptedException
View Code

1、equals()

1. 等價關係

兩個物件具有等價關係,需要滿足以下五個條件:

Ⅰ 自反性

x.equals(x); // true

Ⅱ 對稱性

x.equals(y) == y.equals(x); // true

Ⅲ 傳遞性

1 if (x.equals(y) && y.equals(z))
2     x.equals(z); // true;

Ⅳ 一致性

多次呼叫 equals() 方法結果不變

x.equals(y) == x.equals(y); // true

Ⅴ 與 null 的比較

對任何不是 null 的物件 x 呼叫 x.equals(null) 結果都為 false

x.equals(null); // false;

2. 等價與相等

  • 對於基本型別,== 判斷兩個值是否相等,基本型別沒有 equals() 方法。
  • 對於引用型別,== 判斷兩個變數是否引用同一個物件,而 equals() 判斷引用的物件是否等價。
備戰-Java 基礎
1 Integer x = new Integer(1);
2 Integer y = new Integer(1);
3 System.out.println(x.equals(y)); // true
4 System.out.println(x == y);      // false
View Code

3. 實現

  • 檢查是否為同一個物件的引用,如果是直接返回 true;
  • 檢查是否是同一個型別,如果不是,直接返回 false;
  • 將 Object 物件進行轉型;
  • 判斷每個關鍵域是否相等。
備戰-Java 基礎
 1 public class EqualExample {
 2 
 3     private int x;
 4     private int y;
 5     private int z;
 6 
 7     public EqualExample(int x, int y, int z) {
 8         this.x = x;
 9         this.y = y;
10         this.z = z;
11     }
12 
13     @Override
14     public boolean equals(Object o) {
15         if (this == o) return true;
16         if (o == null || getClass() != o.getClass()) return false;
17 
18         EqualExample that = (EqualExample) o;
19 
20         if (x != that.x) return false;
21         if (y != that.y) return false;
22         return z == that.z;
23     }
24 }
View Code

2、hashCode()

hashCode() 返回雜湊值,而 equals() 是用來判斷兩個物件是否等價。等價的兩個物件雜湊值一定相同,但是雜湊值相同的兩個物件不一定等價,這是因為計算雜湊值具有隨機性,兩個值不同的物件可能計算出相同的雜湊值。

在覆蓋 equals() 方法時應當總是覆蓋 hashCode() 方法,保證等價的兩個物件雜湊值也相等。

HashSet 和 HashMap 等集合類使用了 hashCode() 方法來計算物件應該儲存的位置,因此要將物件新增到這些集合類中,需要讓對應的類實現 hashCode() 方法。

下面的程式碼中,新建了兩個等價的物件,並將它們新增到 HashSet 中。我們希望將這兩個物件當成一樣的,只在集合中新增一個物件。但是 EqualExample 沒有實現 hashCode() 方法,因此這兩個物件的雜湊值是不同的,最終導致集合新增了兩個等價的物件。

備戰-Java 基礎
1 EqualExample e1 = new EqualExample(1, 1, 1);
2 EqualExample e2 = new EqualExample(1, 1, 1);
3 System.out.println(e1.equals(e2)); // true
4 HashSet<EqualExample> set = new HashSet<>();
5 set.add(e1);
6 set.add(e2);
7 System.out.println(set.size());   // 2
View Code

理想的雜湊函式應當具有均勻性,即不相等的物件應當均勻分佈到所有可能的雜湊值上。這就要求了雜湊函式要把所有域的值都考慮進來。可以將每個域都當成 R 進位制的某一位,然後組成一個 R 進位制的整數。

R 一般取 31,因為它是一個奇素數,如果是偶數的話,當出現乘法溢位,資訊就會丟失,因為與 2 相乘相當於向左移一位,最左邊的位丟失。並且一個數與 31 相乘可以轉換成移位和減法:31*x == (x<<5)-x,編譯器會自動進行這個優化。

備戰-Java 基礎
1 @Override
2 public int hashCode() {
3     int result = 17;
4     result = 31 * result + x;
5     result = 31 * result + y;
6     result = 31 * result + z;
7     return result;
8 }
View Code

3、toString()

預設返回 ToStringExample@4554617c 這種形式,其中 @ 後面的數值為雜湊碼的無符號十六進位制表示。

備戰-Java 基礎
1 public class ToStringExample {
2 
3     private int number;
4 
5     public ToStringExample(int number) {
6         this.number = number;
7     }
8 }
View Code
1 ToStringExample example = new ToStringExample(123);
2 System.out.println(example.toString());  // ToStringExample@4554617c

4、clone()

1. cloneable

clone() 是 Object 的 protected 方法,它不是 public,一個類不顯示去重寫 clone(),其它類就不能直接去呼叫該類例項的 clone() 方法。

1 public class CloneExample {
2     private int a;
3     private int b;
4 }
1 CloneExample e1 = new CloneExample();
2 // CloneExample e2 = e1.clone(); // 'clone()' has protected access in 'java.lang.Object'

重寫 clone() 得到以下實現:

備戰-Java 基礎
1 public class CloneExample {
2     private int a;
3     private int b;
4 
5     @Override
6     public CloneExample clone() throws CloneNotSupportedException {
7         return (CloneExample)super.clone();
8     }
9 }
View Code
1 CloneExample e1 = new CloneExample();
2 try {
3     CloneExample e2 = e1.clone();
4 } catch (CloneNotSupportedException e) {
5     e.printStackTrace();  // java.lang.CloneNotSupportedException: CloneExample
6 
7 }

以上丟擲了 CloneNotSupportedException,這是因為 CloneExample 沒有實現 Cloneable 介面。

應該注意的是,clone() 方法並不是 Cloneable 介面的方法,而是 Object 的一個 protected 方法。Cloneable 介面只是規定,如果一個類沒有實現 Cloneable 介面又呼叫了 clone() 方法,就會丟擲 CloneNotSupportedException。

備戰-Java 基礎
1 public class CloneExample implements Cloneable {
2     private int a;
3     private int b;
4 
5     @Override
6     public Object clone() throws CloneNotSupportedException {
7         return super.clone();
8     }
9 }
View Code

2. 淺拷貝

拷貝物件和原始物件的引用型別引用同一個物件。

備戰-Java 基礎
 1 public class ShallowCloneExample implements Cloneable {
 2 
 3     private int[] arr;
 4 
 5     public ShallowCloneExample() {
 6         arr = new int[10];
 7         for (int i = 0; i < arr.length; i++) {
 8             arr[i] = i;
 9         }
10     }
11 
12     public void set(int index, int value) {
13         arr[index] = value;
14     }
15 
16     public int get(int index) {
17         return arr[index];
18     }
19 
20     @Override
21     protected ShallowCloneExample clone() throws CloneNotSupportedException {
22         return (ShallowCloneExample) super.clone();
23     }
24 }
View Code
備戰-Java 基礎
1 ShallowCloneExample e1 = new ShallowCloneExample();
2 ShallowCloneExample e2 = null;
3 try {
4     e2 = e1.clone();
5 } catch (CloneNotSupportedException e) {
6     e.printStackTrace();
7 }
8 e1.set(2, 222);
9 System.out.println(e2.get(2)); // 222
View Code

3. 深拷貝

拷貝物件和原始物件的引用型別引用不同物件。

備戰-Java 基礎
 1 public class DeepCloneExample implements Cloneable {
 2 
 3     private int[] arr;
 4 
 5     public DeepCloneExample() {
 6         arr = new int[10];
 7         for (int i = 0; i < arr.length; i++) {
 8             arr[i] = i;
 9         }
10     }
11 
12     public void set(int index, int value) {
13         arr[index] = value;
14     }
15 
16     public int get(int index) {
17         return arr[index];
18     }
19 
20     @Override
21     protected DeepCloneExample clone() throws CloneNotSupportedException {
22         DeepCloneExample result = (DeepCloneExample) super.clone();
23         result.arr = new int[arr.length];
24         for (int i = 0; i < arr.length; i++) {
25             result.arr[i] = arr[i];
26         }
27         return result;
28     }
29 }
View Code
備戰-Java 基礎
1 DeepCloneExample e1 = new DeepCloneExample();
2 DeepCloneExample e2 = null;
3 try {
4     e2 = e1.clone();
5 } catch (CloneNotSupportedException e) {
6     e.printStackTrace();
7 }
8 e1.set(2, 222);
9 System.out.println(e2.get(2)); // 2
View Code

4. clone() 的替代方案

使用 clone() 方法來拷貝一個物件即複雜又有風險,它會丟擲異常,並且還需要型別轉換。Effective Java 書上講到,最好不要去使用 clone(),可以使用拷貝建構函式或者拷貝工廠來拷貝一個物件。

備戰-Java 基礎
 1 public class CloneConstructorExample {
 2 
 3     private int[] arr;
 4 
 5     public CloneConstructorExample() {
 6         arr = new int[10];
 7         for (int i = 0; i < arr.length; i++) {
 8             arr[i] = i;
 9         }
10     }
11 
12     public CloneConstructorExample(CloneConstructorExample original) {
13         arr = new int[original.arr.length];
14         for (int i = 0; i < original.arr.length; i++) {
15             arr[i] = original.arr[i];
16         }
17     }
18 
19     public void set(int index, int value) {
20         arr[index] = value;
21     }
22 
23     public int get(int index) {
24         return arr[index];
25     }
26 }
View Code
1 CloneConstructorExample e1 = new CloneConstructorExample();
2 CloneConstructorExample e2 = new CloneConstructorExample(e1);
3 e1.set(2, 222);
4 System.out.println(e2.get(2)); // 2

六、繼承

1、訪問許可權

Java 中有三個訪問許可權修飾符:private、protected 以及 public,如果不加訪問修飾符,表示包級可見。

可以對類或類中的成員(欄位和方法)加上訪問修飾符。

  • 類可見表示其它類可以用這個類建立例項物件。
  • 成員可見表示其它類可以用這個類的例項物件訪問到該成員;

protected 用於修飾成員,表示在繼承體系中成員對於子類可見,但是這個訪問修飾符對於類沒有意義。

設計良好的模組會隱藏所有的實現細節,把它的 API 與它的實現清晰地隔離開來。模組之間只通過它們的 API 進行通訊,一個模組不需要知道其他模組的內部工作情況,這個概念被稱為資訊隱藏或封裝。因此訪問許可權應當儘可能地使每個類或者成員不被外界訪問。

如果子類的方法重寫了父類的方法,那麼子類中該方法的訪問級別不允許低於父類的訪問級別。這是為了確保可以使用父類例項的地方都可以使用子類例項去代替,也就是確保滿足里氏替換原則。

欄位決不能是公有的,因為這麼做的話就失去了對這個欄位修改行為的控制,客戶端可以對其隨意修改。例如下面的例子中,AccessExample 擁有 id 公有欄位,如果在某個時刻,我們想要使用 int 儲存 id 欄位,那麼就需要修改所有的客戶端程式碼。

1 public class AccessExample {
2     public String id;
3 }

可以使用公有的 getter 和 setter 方法來替換公有欄位,這樣的話就可以控制對欄位的修改行為。

備戰-Java 基礎
 1 public class AccessExample {
 2 
 3     private int id;
 4 
 5     public String getId() {
 6         return id + "";
 7     }
 8 
 9     public void setId(String id) {
10         this.id = Integer.valueOf(id);
11     }
12 }
View Code

但是也有例外,如果是包級私有的類或者私有的巢狀類,那麼直接暴露成員不會有特別大的影響。

備戰-Java 基礎
 1 public class AccessWithInnerClassExample {
 2 
 3     private class InnerClass {
 4         int x;
 5     }
 6 
 7     private InnerClass innerClass;
 8 
 9     public AccessWithInnerClassExample() {
10         innerClass = new InnerClass();
11     }
12 
13     public int getValue() {
14         return innerClass.x;  // 直接訪問
15     }
16 }
View Code

2、抽象類與介面

1. 抽象類

抽象類和抽象方法都使用 abstract 關鍵字進行宣告。如果一個類中包含抽象方法,那麼這個類必須宣告為抽象類。

抽象類和普通類最大的區別是,抽象類不能被例項化,只能被繼承。

備戰-Java 基礎
 1 // 抽象類
 2 public abstract class AbstractClassExample {
 3 
 4     protected int x;
 5     private int y;
 6 
 7     public abstract void func1();
 8 
 9     public void func2() {
10         System.out.println("func2");
11     }
12 }
View Code
備戰-Java 基礎
1 // 普通類
2 public class AbstractExtendClassExample extends AbstractClassExample {
3     @Override
4     public void func1() {
5         System.out.println("func1");
6     }
7 }
View Code
1 // AbstractClassExample ac1 = new AbstractClassExample(); // 'AbstractClassExample' is abstract; cannot be instantiated
2 AbstractClassExample ac2 = new AbstractExtendClassExample();
3 ac2.func1();

2. 介面

介面是抽象類的延伸,在 Java 8 之前,它可以看成是一個完全抽象的類,也就是說它不能有任何的方法實現。

從 Java 8 開始,介面也可以擁有預設的方法實現,這是因為不支援預設方法的介面的維護成本太高了。在 Java 8 之前,如果一個介面想要新增新的方法,那麼要修改所有實現了該介面的類,讓它們都實現新增的方法。

介面的成員(欄位 + 方法)預設都是 public 的,並且不允許定義為 private 或者 protected。從 Java 9 開始,允許將方法定義為 private,這樣就能定義某些複用的程式碼又不會把方法暴露出去。

介面的欄位預設都是 static 和 final 的。

備戰-Java 基礎
 1 public interface InterfaceExample {
 2 
 3     void func1();
 4 
 5     default void func2(){
 6         System.out.println("func2");
 7     }
 8 
 9     int x = 123;
10     // int y;               // Variable 'y' might not have been initialized
11     public int z = 0;       // Modifier 'public' is redundant for interface fields
12     // private int k = 0;   // Modifier 'private' not allowed here
13     // protected int l = 0; // Modifier 'protected' not allowed here
14     // private void fun3(); // Modifier 'private' not allowed here
15 }
View Code
備戰-Java 基礎
1 public class InterfaceImplementExample implements InterfaceExample {
2     @Override
3     public void func1() {
4         System.out.println("func1");
5     }
6 }
View Code
1 // InterfaceExample ie1 = new InterfaceExample(); // 'InterfaceExample' is abstract; cannot be instantiated
2 InterfaceExample ie2 = new InterfaceImplementExample();
3 ie2.func1();
4 System.out.println(InterfaceExample.x);

3. 比較

  • 從設計層面上看,抽象類提供了一種 IS-A 關係,需要滿足裡式替換原則,即子類物件必須能夠替換掉所有父類物件。而介面更像是一種 LIKE-A 關係,它只是提供一種方法實現契約,並不要求介面和實現介面的類具有 IS-A 關係。
  • 從使用上來看,一個類可以實現多個介面,但是不能繼承多個抽象類。
  • 介面的欄位只能是 static 和 final 型別的,而抽象類的欄位沒有這種限制。
  • 介面的成員只能是 public 的,而抽象類的成員可以有多種訪問許可權。
  • Java 類是單繼承的,介面是多繼承;因為,如果一個類繼承了兩個類,但是這兩個類中有相同的方法,那麼子類呼叫方法時,無法確定應該呼叫哪個父類的方法。而,介面可以寫預設方法,多繼承時發現有同名的預設方法,編譯器會要求重寫預設方法。

4. 使用選擇

使用介面:

  • 需要讓不相關的類都實現一個方法,例如不相關的類都可以實現 Comparable 介面中的 compareTo() 方法;
  • 需要使用多重繼承。

使用抽象類:

  • 需要在幾個相關的類中共享程式碼。
  • 需要能控制繼承來的成員的訪問許可權,而不是都為 public。
  • 需要繼承非靜態和非常量欄位。

在很多情況下,介面優先於抽象類。因為介面沒有抽象類嚴格的類層次結構要求,可以靈活地為一個類新增行為。並且從 Java 8 開始,介面也可以有預設的方法實現,使得修改介面的成本也變的很低。

3、super

  • 訪問父類的建構函式:可以使用 super() 函式訪問父類的建構函式,從而委託父類完成一些初始化的工作。應該注意到,子類一定會呼叫父類的建構函式來完成初始化工作,一般是呼叫父類的預設建構函式,如果子類需要呼叫父類其它建構函式,那麼就可以使用 super() 函式。
  • 訪問父類的成員:如果子類重寫了父類的某個方法,可以通過使用 super 關鍵字來引用父類的方法實現。
備戰-Java 基礎
 1 // 父類
 2 public class SuperExample {
 3 
 4     protected int x;
 5     protected int y;
 6 
 7     public SuperExample(int x, int y) {
 8         this.x = x;
 9         this.y = y;
10     }
11 
12     public void func() {
13         System.out.println("SuperExample.func()");
14     }
15 }
View Code
備戰-Java 基礎
 1 // 子類繼承父類
 2 public class SuperExtendExample extends SuperExample {
 3 
 4     private int z;
 5 
 6     public SuperExtendExample(int x, int y, int z) {
 7         super(x, y);
 8         this.z = z;
 9     }
10 
11     @Override
12     public void func() {
13         super.func();
14         System.out.println("SuperExtendExample.func()");
15     }
16 }
View Code

4、重寫與過載

1. 重寫(Override)

存在於繼承體系中,指子類實現了一個與父類在方法宣告上完全相同的一個方法。

為了滿足裡式替換原則,重寫有以下三個限制:

  • 子類方法的訪問許可權必須大於等於父類方法;
  • 子類方法的返回型別必須是父類方法返回型別或為其子型別。
  • 子類方法丟擲的異常型別必須是父類丟擲異常型別或為其子型別。

使用 @Override 註解,可以讓編譯器幫忙檢查是否滿足上面的三個限制條件。

下面的示例中,SubClass 為 SuperClass 的子類,SubClass 重寫了 SuperClass 的 func() 方法。其中:

  • 子類方法訪問許可權為 public,大於父類的 protected。
  • 子類的返回型別為 ArrayList<Integer>,是父類返回型別 List<Integer> 的子類。
  • 子類丟擲的異常型別為 Exception,是父類丟擲異常 Throwable 的子類。
  • 子類重寫方法使用 @Override 註解,從而讓編譯器自動檢查是否滿足限制條件。
備戰-Java 基礎
 1 class SuperClass {
 2     protected List<Integer> func() throws Throwable {
 3         return new ArrayList<>();
 4     }
 5 }
 6 
 7 class SubClass extends SuperClass {
 8     @Override
 9     public ArrayList<Integer> func() throws Exception {
10         return new ArrayList<>();
11     }
12 }
View Code

在呼叫一個方法時,先從本類中查詢看是否有對應的方法,如果沒有再到父類中檢視,看是否從父類繼承來。否則就要對引數進行轉型,轉成父類之後看是否有對應的方法。總的來說,方法呼叫的優先順序為:

  • this.func(this)
  • super.func(this)
  • this.func(super)
  • super.func(super)
備戰-Java 基礎
 1 /*
 2     A
 3     |
 4     B
 5     |
 6     C
 7     |
 8     D
 9  */
10 
11 
12 class A {
13 
14     public void show(A obj) {
15         System.out.println("A.show(A)");
16     }
17 
18     public void show(C obj) {
19         System.out.println("A.show(C)");
20     }
21 }
22 
23 class B extends A {
24 
25     @Override
26     public void show(A obj) {
27         System.out.println("B.show(A)");
28     }
29 }
30 
31 class C extends B {
32 }
33 
34 class D extends C {
35 }
View Code
備戰-Java 基礎
 1 public static void main(String[] args) {
 2 
 3     A a = new A();
 4     B b = new B();
 5     C c = new C();
 6     D d = new D();
 7 
 8     // 在 A 中存在 show(A obj),直接呼叫
 9     a.show(a); // A.show(A)
10     // 在 A 中不存在 show(B obj),將 B 轉型成其父類 A
11     a.show(b); // A.show(A)
12     // 在 B 中存在從 A 繼承來的 show(C obj),直接呼叫
13     b.show(c); // A.show(C)
14     // 在 B 中不存在 show(D obj),但是存在從 A 繼承來的 show(C obj),將 D 轉型成其父類 C
15     b.show(d); // A.show(C)
16 
17     // 引用的還是 B 物件,所以 ba 和 b 的呼叫結果一樣
18     A ba = new B();
19     ba.show(c); // A.show(C)
20     ba.show(d); // A.show(C)
21 }
View Code

2. 過載(Overload)

存在於同一個類中,指一個方法與已經存在的方法名稱上相同,但是引數型別、個數、順序至少有一個不同。

應該注意的是,返回值不同,其它都相同不算是過載。

備戰-Java 基礎
1 class OverloadingExample {
2     public void show(int x) {
3         System.out.println(x);
4     }
5 
6     public void show(int x, String y) {
7         System.out.println(x + " " + y);
8     }
9 }
View Code
備戰-Java 基礎
1 public static void main(String[] args) {
2     OverloadingExample example = new OverloadingExample();
3     example.show(1);
4     example.show(1, "2");
5 }
View Code

七、反射

每個類都有一個 Class 物件,包含了與類有關的資訊。當編譯一個新類時,會產生一個同名的 .class 檔案,該檔案內容儲存著 Class 物件。

類載入相當於 Class 物件的載入,類在第一次使用時才動態載入到 JVM 中。也可以使用 Class.forName("com.mysql.jdbc.Driver") 這種方式來控制類的載入,該方法會返回一個 Class 物件。

反射可以提供執行時的類資訊,並且這個類可以在執行時才載入進來,甚至在編譯時期該類的 .class 不存在也可以載入進來。

Class 和 java.lang.reflect 一起對反射提供了支援,java.lang.reflect 類庫主要包含了以下三個類:

  • Field :可以使用 get() 和 set() 方法讀取和修改 Field 物件關聯的欄位;
  • Method :可以使用 invoke() 方法呼叫與 Method 物件關聯的方法;
  • Constructor :可以用 Constructor 的 newInstance() 建立新的物件。

反射的優點:

  • 可擴充套件性 :應用程式可以利用全限定名建立可擴充套件物件的例項,來使用來自外部的使用者自定義類。
  • 類瀏覽器和視覺化開發環境 :一個類瀏覽器需要可以列舉類的成員。視覺化開發環境(如 IDE)可以從利用反射中可用的型別資訊中受益,以幫助程式設計師編寫正確的程式碼。
  • 偵錯程式和測試工具 : 偵錯程式需要能夠檢查一個類裡的私有成員。測試工具可以利用反射來自動地呼叫類裡定義的可被發現的 API 定義,以確保一組測試中有較高的程式碼覆蓋率。

反射的缺點:

儘管反射非常強大,但也不能濫用。如果一個功能可以不用反射完成,那麼最好就不用。在我們使用反射技術時,下面幾條內容應該牢記於心。

  • 效能開銷 :反射涉及了動態型別的解析,所以 JVM 無法對這些程式碼進行優化。因此,反射操作的效率要比那些非反射操作低得多。我們應該避免在經常被執行的程式碼或對效能要求很高的程式中使用反射。

  • 安全限制 :使用反射技術要求程式必須在一個沒有安全限制的環境中執行。如果一個程式必須在有安全限制的環境中執行,如 Applet,那麼這就是個問題了。

  • 內部暴露 :由於反射允許程式碼執行一些在正常情況下不被允許的操作(比如訪問私有的屬性和方法),所以使用反射可能會導致意料之外的副作用,這可能導致程式碼功能失調並破壞可移植性。反射程式碼破壞了抽象性,因此當平臺發生改變的時候,程式碼的行為就有可能也隨著變化。

八、異常

Throwable 可以用來表示任何可以作為異常丟擲的類,分為兩種: Error 和 Exception。其中 Error 用來表示 JVM 無法處理的錯誤,Exception 分為兩種:

  • 受檢異常 :需要用 try...catch... 語句捕獲並進行處理,並且可以從異常中恢復;
  • 非受檢異常 :是程式執行時錯誤,例如除 0 會引發 Arithmetic Exception,此時程式崩潰並且無法恢復。

九、泛型

泛型,即“引數化型別”。一提到引數,最熟悉的就是定義方法時有形參,然後呼叫此方法時傳遞實參。那麼引數化型別怎麼理解呢?顧名思義,就是將型別由原來的具體的型別引數化,類似於方法中的變數引數,此時型別也定義成引數形式(可以稱之為型別形參),然後在使用/呼叫時傳入具體的型別(型別實參)。

泛型的本質是為了引數化型別(在不建立新的型別的情況下,通過泛型指定的不同型別來控制形參具體限制的型別)。也就是說在泛型使用過程中,操作的資料型別被指定為一個引數,這種引數型別可以用在類、介面和方法中,分別被稱為泛型類、泛型介面、泛型方法。

1     // T stands for "Type"
2     private T t;
3     public void set(T t) { this.t = t; }
4     public T get() { return t; }
5 }

十、註解

Java 註解是附加在程式碼中的一些元資訊,用於一些工具在編譯、執行時進行解析和使用,起到說明、配置的功能。註解不會也不能影響程式碼的實際邏輯,僅僅起到輔助性的作用。

備戰-Java 基礎
 1 package com.tjt.springmvc;
 2 
 3 
 4 import java.lang.annotation.*;
 5 
 6 
 7 /**
 8  * @MyController 自定義註解類
 9  *
10  * @@Target(ElementType.TYPE)
11  * 表示該註解可以作用在類上;
12  *
13  * @Retention(RetentionPolicy.RUNTIME)
14  * 表示該註解會在class 位元組碼檔案中存在,在執行時可以通過反射獲取到
15  *
16  * @Documented
17  * 標記註解,表示可以生成文件
18  */
19 @Target(ElementType.TYPE)
20 @Retention(RetentionPolicy.RUNTIME)
21 @Documented
22 public @interface MyController {
23 
24     /**
25      * public class MyController
26      * 把 class 替換成 @interface 該類即成為註解類
27      */
28 
29     /**
30      * 為Controller 註冊別名
31      * @return
32      */
33     String value() default "";
34 
35 }
View Code

十一、特性

1、Java 與 C++ 的區別

  • Java 是純粹的面嚮物件語言,所有的物件都繼承自 java.lang.Object,C++ 為了相容 C 即支援物件導向也支援程式導向。
  • Java 通過虛擬機器從而實現跨平臺特性,但是 C++ 依賴於特定的平臺。
  • Java 沒有指標,它的引用可以理解為安全指標,而 C++ 具有和 C 一樣的指標。
  • Java 支援自動垃圾回收,而 C++ 需要手動回收。
  • Java 不支援多重繼承,只能通過實現多個介面來達到相同目的,而 C++ 支援多重繼承。
  • Java 不支援操作符過載,雖然可以對兩個 String 物件執行加法運算,但是這是語言內建支援的操作,不屬於操作符過載,而 C++ 可以。
  • Java 的 goto 是保留字,但是不可用,C++ 可以使用 goto。

2、JRE & JDK

  • JRE:Java Runtime Environment,Java 執行環境的簡稱,為 Java 的執行提供了所需的環境。它是一個 JVM 程式,主要包括了 JVM 的標準實現和一些 Java 基本類庫。
  • JDK:Java Development Kit,Java 開發工具包,提供了 Java 的開發及執行環境。JDK 是 Java 開發的核心,整合了 JRE 以及一些其它的工具,比如編譯 Java 原始碼的編譯器 javac 等。

 

 

 

        仰天大笑出門去

我輩豈是蓬蒿人