1. 程式人生 > >java面試基礎知識總結(一)

java面試基礎知識總結(一)

一、資料型別

包裝型別

八個基本型別:
boolean/1
byte/8
char/16
short/16
int/32
float/32
long/64
double/64
基本型別都有對應的包裝型別,基本型別與其對應的包裝型別之間的賦值使用自動裝箱與拆箱完成。

Integer x = 2;     // 裝箱
int y = x;         // 拆箱

快取池

new Integer(123) 與 Integer.valueOf(123) 的區別在於:
new Integer(123) 每次都會新建一個物件;
Integer.valueOf(123) 會使用快取池中的物件,多次呼叫會取得同一個物件的引用。

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

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

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

在 Java 8 中,Integer 快取池的大小預設為 -128~127
編譯器會在自動裝箱過程呼叫 valueOf() 方法,因此多個 Integer 例項使用自動裝箱來建立並且值相同,那麼就會引用相同的物件。

Integer m = 123;
Integer n = 123;
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
    在使用這些基本型別對應的包裝型別時,就可以直接使用緩衝池中的物件。

二、String

概覽

String 被宣告為 final,因此它不可被繼承。

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

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

不可變的好處

  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 不可變性天生具備執行緒安全,可以在多個執行緒中安全地使用。

String, StringBuffer and StringBuilder

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

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() 方法取得一個字串引用。intern() 首先把 s1 引用的字串放到 String Pool 中,然後返回這個字串引用。因此 s3 和 s4 引用的是同一個字串。

String s1 = new String("aaa");
String s2 = new String("aaa");
System.out.println(s1 == s2);           // false
String s3 = s1.intern();
String s4 = s1.intern();
System.out.println(s3 == s4);           // true

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

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

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

new String(“abc”)

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

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

三、運算

引數傳遞

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

以下程式碼中 Dog dog 的 dog 是一個指標,儲存的是物件的地址。在將一個引數傳入一個方法時,本質上是將物件的地址以值的方式傳遞到形參中。因此在方法中使指標引用其它物件,那麼這兩個指標此時指向的是完全不同的物件,在一方改變其所指向物件的內容時對另一方沒有影響。

public class Dog {
    String name;
    Dog(String name) {
        this.name = name;
    }
    String getName() {
        return this.name;
    }
    void setName(String name) {
        this.name = name;
    }
    String getObjectAddress() {
        return super.toString();
    }
}
public class PassByValueExample {
    public static void main(String[] args) {
        Dog dog = new Dog("A");
        System.out.println(dog.getObjectAddress()); // [email protected]
        func(dog);
        System.out.println(dog.getObjectAddress()); // [email protected]
        System.out.println(dog.getName());          // A
    }

    private static void func(Dog dog) {
        System.out.println(dog.getObjectAddress()); // [email protected]
        dog = new Dog("B");
        System.out.println(dog.getObjectAddress()); // [email protected]
        System.out.println(dog.getName());          // B
    }
}

如果在方法中改變物件的欄位值會改變原物件該欄位值,因為改變的是同一個地址指向的內容。

class PassByValueExample {
    public static void main(String[] args) {
        Dog dog = new Dog("A");
        func(dog);
        System.out.println(dog.getName());          // B
    }

    private static void func(Dog dog) {
        dog.setName("B");
    }
}

float 與 double

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

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

// float f = 1.1;

1.1f 字面量才是 float 型別。

float f = 1.1f;

隱式型別轉換

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

short s1 = 1;
// s1 = s1 + 1;

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

s1 += 1;
// s1++;

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

s1 = (short) (s1 + 1);

四、繼承

訪問許可權

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

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

  • 類可見表示其它類可以用這個類建立例項物件。
  • 成員可見表示其它類可以用這個類的例項物件訪問到該成員;
  • protected 用於修飾成員,表示在繼承體系中成員對於子類可見,但是這個訪問修飾符對於類沒有意義。

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

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

欄位決不能是公有的,因為這麼做的話就失去了對這個欄位修改行為的控制,客戶端可以對其隨意修改。

抽象類與介面

  1. 抽象類
    抽象類和抽象方法都使用 abstract 關鍵字進行宣告。抽象類一般會包含抽象方法,抽象方法一定位於抽象類中。
    抽象類和普通類最大的區別是,抽象類不能被例項化,需要繼承抽象類才能例項化其子類。
public abstract class AbstractClassExample {

    protected int x;
    private int y;

    public abstract void func1();

    public void func2() {
        System.out.println("func2");
    }
}
public class AbstractExtendClassExample extends AbstractClassExample {
    @Override
    public void func1() {
        System.out.println("func1");
    }
}
  1. 介面

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

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

介面的成員(欄位 + 方法)預設都是 public 的,並且不允許定義為 private 或者 protected。

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

public interface InterfaceExample {

    void func1();

    default void func2(){
        System.out.println("func2");
    }

    int x = 123;
    // int y;               // Variable 'y' might not have been initialized
    public int z = 0;       // Modifier 'public' is redundant for interface fields
    // private int k = 0;   // Modifier 'private' not allowed here
    // protected int l = 0; // Modifier 'protected' not allowed here
    // private void fun3(); // Modifier 'private' not allowed here
}
public class InterfaceImplementExample implements InterfaceExample {
    @Override
    public void func1() {
        System.out.println("func1");
    }
}
  1. 比較
  • 從設計層面上看,抽象類提供了一種 IS-A 關係,那麼就必須滿足裡式替換原則,即子類物件必須能夠替換掉所有父類物件。而介面更像是一種 LIKE-A 關係,它只是提供一種方法實現契約,並不要求介面和實現介面的類具有 IS-A 關係。
  • 從使用上來看,一個類可以實現多個介面,但是不能繼承多個抽象類。
  • 介面的欄位只能是 static 和 final 型別的,而抽象類的欄位沒有這種限制。
  • 介面的成員只能是 public 的,而抽象類的成員可以有多種訪問許可權。
  1. 使用選擇

使用介面:

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

使用抽象類:

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

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

super

  • 訪問父類的建構函式:可以使用 super() 函式訪問父類的建構函式,從而委託父類完成一些初始化的工作。
  • 訪問父類的成員:如果子類重寫了父類的某個方法,可以通過使用 super 關鍵字來引用父類的方法實現。
public class SuperExample {

    protected int x;
    protected int y;

    public SuperExample(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public void func() {
        System.out.println("SuperExample.func()");
    }
}
public class SuperExtendExample extends SuperExample {

    private int z;

    public SuperExtendExample(int x, int y, int z) {
        super(x, y);
        this.z = z;
    }

    @Override
    public void func() {
        super.func();
        System.out.println("SuperExtendExample.func()");
    }
}
SuperExample e = new SuperExtendExample(1, 2, 3);
e.func();
SuperExample.func()
SuperExtendExample.func()

重寫與過載

  1. 重寫(Override)

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

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

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

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

  1. 過載(Overload)

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

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

未完待續。。。

參考

https://github.com/CyC2018/CS-Notes/blob/master/notes/Java 基礎.md