1. 程式人生 > >校招面試——Java 基礎知識

校招面試——Java 基礎知識

前言

為了更好的總結Java面試中的系統知識結構,本文根據以下資料整理學習筆記。

from 2018/7/11

一、基本概念

1. Java程式初始化的順序是怎麼樣的(B50)

在Java語言中,當例項化物件時,物件所在類的所有成員變數首先要進行初始化,只有當所有類成員完成初始化後,才會呼叫物件所在類的建構函式建立象。

初始化一般遵循3個原則:

  • 靜態物件(變數)優先於非靜態物件(變數)初始化,靜態物件(變數)只初始化一次,而非靜態物件(變數)可能會初始化多次;
  • 父類優先於子類進行初始化;
  • 按照成員變數的定義順序進行初始化。 即使變數定義散佈於方法定義之中,它們依然在任何方法(包括建構函式)被呼叫之前先初始化;

載入順序

  • 父類(靜態變數、靜態語句塊)
  • 子類(靜態變數、靜態語句塊)
  • 父類(例項變數、普通語句塊)
  • 父類(建構函式)
  • 子類(例項變數、普通語句塊)
  • 子類(建構函式)

例項

class Base {
    // 1.父類靜態程式碼塊
    static {
        System.out.println("Base static block!");
    }
    // 3.父類非靜態程式碼塊
    {
        System.out.println("Base block");
    }
    // 4.父類構造器
    public Base() {
        System.out.println("Base constructor!"
); } } public class Derived extends Base { // 2.子類靜態程式碼塊 static{ System.out.println("Derived static block!"); } // 5.子類非靜態程式碼塊 { System.out.println("Derived block!"); } // 6.子類構造器 public Derived() { System.out.println("Derived constructor!"); } public
static void main(String[] args) { new Derived(); } }

結果是:

Base static block!
Derived static block!
Base block
Base constructor!
Derived block!
Derived constructor!

2. Java和C++的區別

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

2. 什麼是反射

通過Class獲取class資訊稱之為反射(Reflection)。反射機制是在執行狀態中,對於任意一個類,都能夠知道這個類的所有屬性和方法;對於任意一個物件,都能夠呼叫它的任意一個方法和屬性;這種動態獲取的資訊以及動態呼叫物件的方法的功能稱為Java語言的反射機制。

反射應用中獲取Class例項的四種方式

//1.呼叫執行時類本身的.class屬性
Class clazz1 = Person.class;
System.out.println(clazz1.getName());

Class clazz2 = String.class;
System.out.println(clazz2.getName());

//2.通過執行時類的物件獲取 getClass();
Person p = new Person();
Class clazz3 = p.getClass();
System.out.println(clazz3.getName());

//3.通過Class的靜態方法獲取.通過此方式,體會一下,反射的動態性。
String className = "com.atguigu.java.Person";
Class clazz4 = Class.forName(className);
// clazz4.newInstance();
System.out.println(clazz4.getName());

//4.(瞭解)通過類的載入器 ClassLoader
ClassLoader classLoader = this.getClass().getClassLoader();
Class clazz5 = classLoader.loadClass(className);
System.out.println(clazz5.getName());

3. 什麼是註解

  Annontation是Java5開始引入的新特徵,中文名稱叫註解。它提供了一種安全的類似註釋的機制,用來將任何的資訊或元資料(metadata)與程式元素(類、方法、成員變數等)進行關聯。為程式的元素(類、方法、成員變數)加上更直觀更明瞭的說明,這些說明資訊是與程式的業務邏輯無關,並且供指定的工具或框架使用。Annontation像一種修飾符一樣,應用於包、型別、構造方法、方法、成員變數、引數及本地變數的宣告語句中。

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

常見標準的Annotation:

1)Override

java.lang.Override是一個標記型別註解,它被用作標註方法。它說明了被標註的方法過載了父類的方法,起到了斷言的作用。讓編譯器檢查該方法是否正確地實現了複寫。

2)Deprecated

Deprecated也是一種標記型別註解。當一個型別或者型別成員使用@Deprecated修飾的話,編譯器將不鼓勵使用這個被標註的程式元素。告訴編譯器該方法已經被標記為“作廢”,在其他地方引用將會出現編譯警告。

3)SuppressWarnings

SuppressWarning不是一個標記型別註解。它有一個型別為String[]的成員,這個成員的值為被禁止的警告名。對於javac編譯器來講,被-Xlint選項有效的警告名也同樣對@SuppressWarings有效,同時編譯器忽略掉無法識別的警告名。

示例1——抑制單型別的警告:

@SuppressWarnings("unchecked")
public void addItems(String item){
  @SuppressWarnings("rawtypes")
   List items = new ArrayList();
   items.add(item);
}

示例2——抑制多型別的警告:

@SuppressWarnings(value={"unchecked", "rawtypes"})
public void addItems(String item){
   List items = new ArrayList();
   items.add(item);
}

示例3——抑制所有型別的警告:

@SuppressWarnings("all")
public void addItems(String item){
   List items = new ArrayList();
   items.add(item);
}

自定義註解類編寫的一些規則

  1. Annotation型定義為@interface, 所有的Annotation會自動繼承java.lang.Annotation這一介面,並且不能再去繼承別的類或是介面.
  2. 引數成員只能用public或預設(default)這兩個訪問權修飾
  3. 引數成員只能用基本型別byte,short,char,int,long,float,double,boolean八種基本資料型別和String、Enum、Class、annotations等資料型別,以及這一些型別的陣列.
  4. 要獲取類方法和欄位的註解資訊,必須通過Java的反射技術來獲取 Annotation物件,因為你除此之外沒有別的獲取註解物件的方法
  5. 註解也可以沒有定義成員, 不過這樣註解就沒啥用了
    PS:自定義註解需要使用到元註解

自定義註解例項

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

/**
 * 水果名稱註解
 */
@Target(FIELD)
@Retention(RUNTIME)
@Documented
public @interface FruitName {
    String value() default "";
}

4. 什麼是泛型

通俗解釋

通俗的講,泛型就是操作型別的 佔位符,即:假設佔位符為T,那麼此次宣告的資料結構操作的資料型別為T型別。

假定我們有這樣一個需求:寫一個排序方法,能夠對整型陣列、字串陣列甚至其他任何型別的陣列進行排序,該如何實現?

答案是可以使用 Java 泛型

使用 Java 泛型的概念,我們可以寫一個泛型方法來對一個物件陣列排序。然後,呼叫該泛型方法來對整型陣列、浮點數陣列、字串陣列等進行排序。

泛型方法

你可以寫一個泛型方法,該方法在呼叫時可以接收不同型別的引數。根據傳遞給泛型方法的引數型別,編譯器適當地處理每一個方法呼叫。

下面是定義泛型方法的規則:

  • 所有泛型方法宣告都有一個型別引數宣告部分(由尖括號分隔),該型別引數宣告部分在方法返回型別之前(在下面例子中的)。
  • 每一個型別引數宣告部分包含一個或多個型別引數,引數間用逗號隔開。一個泛型引數,也被稱為一個型別變數,是用於指定一個泛型型別名稱的識別符號。
  • 型別引數能被用來宣告返回值型別,並且能作為泛型方法得到的實際引數型別的佔位符。
  • 泛型方法體的宣告和其他方法一樣。注意型別引數 只能代表引用型型別,不能是原始型別 (像int,double,char的等)。
public class GenericMethodTest
{
   // 泛型方法 printArray                         
   public static < E > void printArray( E[] inputArray )
   {
      // 輸出陣列元素            
         for ( E element : inputArray ){        
            System.out.printf( "%s ", element );
         }
         System.out.println();
    }

    public static void main( String args[] )
    {
        // 建立不同型別陣列: Integer, Double 和 Character
        Integer[] intArray = { 1, 2, 3, 4, 5 };
        Double[] doubleArray = { 1.1, 2.2, 3.3, 4.4 };
        Character[] charArray = { 'H', 'E', 'L', 'L', 'O' };

        System.out.println( "整型陣列元素為:" );
        printArray( intArray  ); // 傳遞一個整型陣列

        System.out.println( "\n雙精度型陣列元素為:" );
        printArray( doubleArray ); // 傳遞一個雙精度型陣列

        System.out.println( "\n字元型陣列元素為:" );
        printArray( charArray ); // 傳遞一個字元型陣列
    } 
}

泛型類

泛型類的宣告和非泛型類的宣告類似,除了在類名後面添加了型別引數宣告部分。

和泛型方法一樣,泛型類的型別引數宣告部分也包含一個或多個型別引數,引數間用逗號隔開。一個泛型引數,也被稱為一個型別變數,是用於指定一個泛型型別名稱的識別符號。因為他們接受一個或多個引數,這些類被稱為引數化的類或引數化的型別。

public class Box<T> {
  private T t;
  public void add(T t) {
    this.t = t;
  }

  public T get() {
    return t;
  }

  public static void main(String[] args) {
    Box<Integer> integerBox = new Box<Integer>();
    Box<String> stringBox = new Box<String>();

    integerBox.add(new Integer(10));
    stringBox.add(new String("菜鳥教程"));

    System.out.printf("整型值為 :%d\n\n", integerBox.get());
    System.out.printf("字串為 :%s\n", stringBox.get());
  }
}

型別萬用字元

1、型別萬用字元一般是使用?代替具體的型別引數。例如 List

5. 為什麼要實現記憶體模型?

  • 記憶體模型的就是為了在現代計算機平臺中保證程式可以正確性的執行,但是不同的平臺實現是不同的。
  • 編譯器中生成的指令順序, 可以與原始碼中的順序不同;
  • 編譯器可能把變數儲存在暫存器而不是記憶體中;
  • 處理器可以採用亂序或並行等方式來執行指令;
  • 快取可能會改變將寫入變數提交到主記憶體的次序;
  • 儲存在處理器本地快取中的值,對其他處理器是不可見的;

6. 位元組與字元的區別 ?【螞蟻金服內推】

理解編碼的關鍵,是要把字元的概念和位元組的概念理解準確。這兩個概念容易混淆,我們在此做一下區分:

型別 概念描述 舉例
字元 人們使用的記號,抽象意義上的一個符號。 ‘1’, ‘中’, ‘a’, ‘$’, ‘¥’, ……
位元組 計算機中儲存資料的單元,一個8位的二進位制數,是一個很具體的儲存空間。 0x01, 0x45, 0xFA, ……
ANSI 字串 在記憶體中,如果“字元”是以 ANSI 編碼形式存在的,一個字元可能使用一個位元組或多個位元組來表示,那麼我們稱這種字串為 ANSI 字串或者多位元組字串 “中文123” (佔7位元組)
UNICODE 字串 在記憶體中,如果“字元”是以在 UNICODE 中的序號存在的,那麼我們稱這種字串為 UNICODE 字串或者寬位元組字串 L”中文123” (佔10位元組)

位元組與字元區別

它們完全不是一個位面的概念,所以兩者之間沒有“區別”這個說法。不同編碼裡,字元和位元組的對應關係不同:

型別 概念描述
ASCII 一個英文字母(不分大小寫)佔一個位元組的空間,一箇中文漢字佔兩個位元組的空間。一個二進位制數字序列,在計算機中作為一個數字單元,一般為8位二進位制數,換算為十進位制。最小值0,最大值255。
UTF-8 一個英文字元等於一個位元組,一箇中文(含繁體)等於三個位元組
Unicode 一個英文等於兩個位元組,一箇中文(含繁體)等於兩個位元組。符號:英文標點佔一個位元組,中文標點佔兩個位元組。舉例:英文句號“.”佔1個位元組的大小,中文句號“。”佔2個位元組的大小。
UTF-16 一個英文字母字元或一個漢字字元儲存都需要2個位元組(Unicode擴充套件區的一些漢字儲存需要4個位元組)
UTF-32 世界上任何字元的儲存都需要4個位元組

參考資料:

7. 有哪些訪問修飾符

Java面向物件的基本思想之一是封裝細節並且公開介面。Java語言採用訪問控制修飾符來控制類及類的方法和變數的訪問許可權,從而向使用者暴露介面,但隱藏實現細節。訪問控制分為四種級別:

修飾符 當前類 同 包 子 類 其他包
public
protected ×
default × ×
private × × ×

- 類的成員不寫訪問修飾時預設為default。預設對於同一個包中的其他類相當於公開(public),對於不是同一個包中的其他類相當於私有(private)。
- 受保護(protected)對子類相當於公開,對不是同一包中的沒有父子關係的類相當於私有。
- Java中,外部類的修飾符只能是public或預設,類的成員(包括內部類)的修飾符可以是以上四種。

8. 深拷貝與淺拷貝

  • 淺拷貝:對基本資料型別進行值傳遞,對引用資料型別進行引用傳遞般的拷貝,此為淺拷貝。

- 深拷貝:對基本資料型別進行值傳遞,對引用資料型別,建立一個新的物件,並複製其內容,此為深拷貝。
參考資料: - [細說 Java 的深拷貝和淺拷貝 - 承香墨影 - SegmentFault 思否](https://segmentfault.com/a/1190000010648514) - [(基礎系列)object clone 的用法、原理和用途 - 掘金](https://juejin.im/post/59bfc707f265da0646188bca)

二、面向物件

1. Java的四個基本特性(抽象、封裝、繼承,多型),對多型的理解(多型的實現方式)以及在專案中那些地方用到多型

  • Java的四個基本特性
    • 抽象:抽象是將一類物件的共同特徵總結出來構造類的過程,包括資料抽象行為抽象兩方面。抽象只關注物件有哪些屬性和行為,並不關注這些行為的細節是什麼。 
    • 封裝:通常認為封裝是把資料和操作資料的方法繫結起來,對資料的訪問只能通過已定義的介面。面向物件的本質就是將現實世界描繪成一系列完全自治、封閉的物件。我們在類中編寫的方法就是對實現細節的一種封裝;我們編寫一個類就是對資料和資料操作的封裝。可以說,封裝就是隱藏一切可隱藏的東西,只向外界提供最簡單的程式設計介面。
    • 繼承:繼承是從已有類得到繼承資訊建立新類的過程。提供繼承資訊的類被稱為父類(超類、基類);得到繼承資訊的類被稱為子類(派生類)。繼承讓變化中的軟體系統有了一定的延續性,同時繼承也是封裝程式中可變因素的重要手段。
    • 多型:多型性是指允許不同子型別的物件對同一訊息作出不同的響應。
  • 多型的理解(多型的實現方式)
    • 方法過載(overload):實現的是編譯時的多型性(也稱為前繫結)。
    • 方法重寫(override):實現的是執行時的多型性(也稱為後繫結)。執行時的多型是面向物件最精髓的東西。
    • 要實現多型需要做兩件事:
    • 1) 方法重寫(子類繼承父類並重寫父類中已有的或抽象的方法);
    • 2) 物件造型(用父型別引用引用子型別物件,這樣同樣的引用呼叫同樣的方法就會根據子類物件的不同而表現出不同的行為)。
  • 專案中對多型的應用
    • 舉一個簡單的例子,在物流資訊管理系統中,有兩種使用者:訂購客戶和賣房客戶,兩個客戶都可以登入系統,他們有相同的方法Login,但登陸之後他們會進入到不同的頁面,也就是在登入的時候會有不同的操作,兩種客戶都繼承父類的Login方法,但對於不同的物件,擁有不同的操作。
  • 面相物件開發方式優點(B65)
    • 較高的開發效率:可以把事物進行抽象,對映為開發的物件。
    • 保證軟體的魯棒性:高重用性,可以重用已有的而且在相關領域經過長期測試的程式碼。
    • 保證軟體的高可維護性:程式碼的可讀性非常好,設計模式也使得程式碼結構清晰,拓展性好。

2. 什麼是過載和重寫?

  • 過載:過載發生在同一個類中,同名的方法如果有不同的引數列表(引數型別不同、引數個數不同或者二者都不同)則視為過載。
  • 重寫:重寫發生在子類與父類之間,重寫要求子類被重寫方法與父類被重寫方法有相同的返回型別,比父類被重寫方法更好訪問,不能比父類被重寫方法宣告更多的異常(里氏代換原則)。根據不同的子類物件確定呼叫的那個方法。


3. 面向物件和麵向過程的區別?用面向過程可以實現面向物件嗎?那是不是不能面向物件?

  • 面向物件和麵向過程的區別
    • 面向過程就像是一個細心的管家,事無具細的都要考慮到。而面向物件就像是個家用電器,你只需要知道他的功能,不需要知道它的工作原理。
    • 面向過程是一種是“事件”為中心的程式設計思想。就是分析出解決問題所需的步驟,然後用函式把這些步驟實現,並按順序呼叫。面向物件是以“物件”為中心的程式設計思想。
    • 簡單的舉個例子:汽車發動、汽車到站
    • 這對於 面向過程 來說,是兩個事件,汽車啟動是一個事件,汽車到站是另一個事件,面向過程程式設計的過程中我們關心的是事件,而不是汽車本身。針對上述兩個事件,形成兩個函式,之 後依次呼叫。(事件驅動,動詞為主)
    • 然而這對於面向物件來說,我們關心的是汽車這類物件,兩個事件只是這類物件所具有的行為。而且對於這兩個行為的順序沒有強制要求。(物件驅動,名詞為主,將問題抽象出具體的物件,而這個物件有自己的屬性和方法,在解決問題的時候是將不同的物件組合在一起使用)
  • 用面向過程可以實現面向物件嗎 ?
    • 如果是c語言來展現出面向物件的思想,c語言中是不是有個叫結構體的東西,這個裡面有自己定義的變數 可以通過函式指標就可以實現物件
  • 那是不是不能面向物件 ?

4. 面向物件開發的六個基本原則(單一職責、開放封閉、里氏替換、依賴倒置、合成聚合複用、介面隔離),迪米特法則。在專案中用過哪些原則

  • 六個基本原則(參考《設計模式之禪》)

    • 單一職責(Single Responsibility Principle 簡稱 SRP):一個類應該僅有一個引起它變化的原因。在面向物件中,如果只讓一個類完成它該做的事,而不涉及與它無關的領域就是踐行了高內聚的原則,這個類就只有單一職責。

    • 里氏替換(Liskov Substitution Principle 簡稱 LSP):任何時候子型別能夠替換掉它們的父型別。子類一定是增加父類的能力而不是減少父類的能力,因為子類比父類的能力更多,把能力多的物件當成能力少的物件來用當然沒有任何問題。

    • 依賴倒置(Dependence Inversion Principle 簡稱 DIP):要依賴於抽象,不要依賴於具體類。要做到依賴倒置,應該做到:①高層模組不應該依賴底層模組,二者都應該依賴於抽象;②抽象不應該依賴於具體實現,具體實現應該依賴於抽象。

    • 介面隔離(Interface Segregation Principle 簡稱 ISP):不應該強迫客戶依賴於他們不用的方法 。介面要小而專,絕不能大而全。臃腫的介面是對介面的汙染,既然介面表示能力,那麼一個介面只應該描述一種能力,介面也應該是高度內聚的。

    • 最少知識原則(Least Knowledge Principle 簡稱 LKP):只和你的朋友談話。迪米特法則又叫最少知識原則,一個物件應當對其他物件有儘可能少的瞭解。

    • 開放封閉(Open Closed Principle 簡稱 OCP):軟體實體應當對擴充套件開放,對修改關閉。要做到開閉有兩個要點:①抽象是關鍵,一個系統中如果沒有抽象類或介面系統就沒有擴充套件點;②封裝可變性,將系統中的各種可變因素封裝到一個繼承結構中,如果多個可變因素混雜在一起,系統將變得複雜而換亂。

  • 其他原則

    • 合成聚和複用:優先使用聚合或合成關係複用程式碼
    • 面向介面程式設計
    • 優先使用組合,而非繼承
    • 一個類需要的資料應該隱藏在類的內部
    • 類之間應該零耦合,或者只有傳導耦合,換句話說,類之間要麼沒關係,要麼只使用另一個類的介面提供的操作
    • 在水平方向上儘可能統一地分佈系統功能
  • 專案中用到的原則

    • 單一職責、開放封閉、合成聚合複用(最簡單的例子就是String類)、介面隔離

5. 內部類有哪些

可以將一個類的定義放在另一個類的定義內部,這就是內部類。

在Java中內部類主要分為成員內部類、區域性內部類、匿名內部類、靜態內部類

(一)成員內部類

成員內部類也是最普通的內部類,它是外圍類的一個成員,所以他是可以無限制的訪問外圍類的所有成員屬性和方法,儘管是private的,但是外圍類要訪問內部類的成員屬性和方法則需要通過內部類例項來訪問。

public class OuterClass {
    private String str;

    public void outerDisplay(){
        System.out.println("outerClass...");
    }

    public class InnerClass{
        public void innerDisplay(){
            str = "chenssy..."; //使用外圍內的屬性
            System.out.println(str);
            outerDisplay();  //使用外圍內的方法
        }
    }

    // 推薦使用getxxx()來獲取成員內部類,尤其是該內部類的建構函式無引數時
    public InnerClass getInnerClass(){
        return new InnerClass();
    }

    public static void main(String[] args) {
        OuterClass outer = new OuterClass();
        OuterClass.InnerClass inner = outer.getInnerClass();
        inner.innerDisplay();
    }
}
--------------------
chenssy...
outerClass...

在成員內部類中要注意兩點:

  • 成員內部類中不能存在任何static的變數和方法;

  • 成員內部類是依附於外圍類的,所以只有先建立了外圍類才能夠建立內部類。

(二)區域性內部類

有這樣一種內部類,它是巢狀在方法和作用於內的,對於這個類的使用主要是應用與解決比較複雜的問題,想建立一個類來輔助我們的解決方案,到那時又不希望這個類是公共可用的,所以就產生了區域性內部類,區域性內部類和成員內部類一樣被編譯,只是它的作用域發生了改變,它只能在該方法和屬性中被使用,出了該方法和屬性就會失效。

//定義在方法裡:
public class Parcel5 {
    public Destionation destionation(String str){
        class PDestionation implements Destionation{
            private String label;
            private PDestionation(String whereTo){
                label = whereTo;
            }
            public String readLabel(){
                return label;
            }
        }
        return new PDestionation(str);
    }

    public static void main(String[] args) {
        Parcel5 parcel5 = new Parcel5();
        Destionation d = parcel5.destionation("chenssy");
    }
}

//定義在作用域內:
public class Parcel6 {
    private void internalTracking(boolean b){
        if(b){
            class TrackingSlip{
                private String id;
                TrackingSlip(String s) {
                    id = s;
                }
                String getSlip(){
                    return id;
                }
            }
            TrackingSlip ts = new TrackingSlip("chenssy");
            String string = ts.getSlip();
        }
    }

    public void track(){
        internalTracking(true);
    }

    public static void main(String[] args) {
        Parcel6 parcel6 = new Parcel6();
        parcel6.track();
    }
}

(三)匿名內部類

在做Swing程式設計中,我們經常使用這種方式來繫結事件

button2.addActionListener(  
                new ActionListener(){  
                    public void actionPerformed(ActionEvent e) {  
                        System.out.println("你按了按鈕二");  
                    }  
                });

我們咋一看可能覺得非常奇怪,因為這個內部類是沒有名字的,在看如下這個例子:

public class OuterClass {
    // ★當所在方法的形參需要被匿名內部類使用,那麼這個形參就必須為final!!!這裡要注意
    public InnerClass getInnerClass(final int num,String str2){ 
        return new InnerClass(){
            int number = num + 3;
            public int getNumber(){
                return number;
            }
        };        /* 注意:分號不能省 */
    }

    public static void main(String[] args) {
        OuterClass out = new OuterClass();
        InnerClass inner = out.getInnerClass(2, "chenssy");
        System.out.println(inner.getNumber());
    }
}

interface InnerClass {
    int getNumber();
}

----------------
Output:

這裡我們就需要看清幾個地方

  • 匿名內部類是沒有訪問修飾符的。

  • new 匿名內部類,這個類首先是要存在的。如果我們將那個InnerClass介面註釋掉,就會出現編譯出錯。

  • 注意getInnerClass()方法的形參,第一個形參是用final修飾的,而第二個卻沒有。同時我們也發現第二個形參在匿名內部類中沒有使用過,所以當所在方法的形參需要被匿名內部類使用,那麼這個形參就必須為final

  • 匿名內部類是沒有構造方法的。因為它連名字都沒有何來構造方法。

(四)靜態內部類

關鍵字static中提到Static可以修飾成員變數、方法、程式碼塊,其他它還可以修飾內部類,使用static修飾的內部類我們稱之為靜態內部類,不過我們更喜歡稱之為巢狀內部類。靜態內部類與非靜態內部類之間存在一個最大的區別,我們知道非靜態內部類在編譯完成之後會隱含地儲存著一個引用,該引用是指向建立它的外圍內,但是靜態內部類卻沒有。

  1. 它的建立是不需要依賴於外圍類的。

  2. 它不能使用任何外圍類的非static成員變數和方法。

public class OuterClass {
    private String sex;
    public static String name = "chenssy";

    // 靜態內部類 
    static class InnerClass1{
        // 在靜態內部類中可以存在靜態成員
        public static String _name1 = "chenssy_static";

        public void display(){ 
            // 靜態內部類只能訪問外圍類的靜態成員變數和方法
           // 不能訪問外圍類的非靜態成員變數和方法
            System.out.println("OutClass name :" + name);
        }
    }


    // 非靜態內部類
    class InnerClass2{
        // 非靜態內部類中不能存在靜態成員
        public String _name2 = "chenssy_inner";
        // 非靜態內部類中可以呼叫外圍類的任何成員,不管是靜態的還是非靜態的
        public void display(){
            System.out.println("OuterClass name:" + name);
        }
    }

    // 外圍類方法
    public void display(){
        // 外圍類訪問靜態內部類:內部類
        System.out.println(InnerClass1._name1);
        // 靜態內部類 可以直接建立例項不需要依賴於外圍類
        new InnerClass1().display();

        // 非靜態內部的建立需要依賴於外圍類
        OuterClass.InnerClass2 inner2 = new OuterClass().new InnerClass2();
        // 方位非靜態內部類的成員需要使用非靜態內部類的例項
        System.out.println(inner2._name2);
        inner2.display();
    }

    public static void main(String[] args) {
        OuterClass outer = new OuterClass();
        outer.display();
    }
}
----------------
Output:
chenssy_static
OutClass name :chenssy
chenssy_inner
OuterClass name:chenssy

6. 組合、繼承和代理的區別

定義

  • 組合:在新類中new 另外一個類的物件,以新增該物件的特性。
  • 繼承:從基類繼承得到子類,獲得基類的特性。
  • 代理:在代理類中建立某功能的類,呼叫類的一些方法以獲得該類的部分特性。

使用場合

  • 組合:各部件之間沒什麼關係,只需要組合即可。like組裝電腦,需要new CPU(),new RAM(),new Disk()……

    public class Computer {
      public Computer() {
          CPU cpu=new CPU();
          RAM ram=new RAM();
          Disk disk=new Disk();
      }
    }
    class CPU{    }
    class RAM{    }
    class Disk{    }
  • 繼承:子類需要具有父類的功能,各子類之間有所差異。like Shape類作為基類,子類有Rectangle,CirCle,Triangle……程式碼不寫了,大家都經常用。

  • 代理:飛機控制類,我不想暴露太多飛機控制的功能,只需部分前進左右轉的控制(而不需要暴露發射導彈功能)。通過在代理類中new一個飛機控制物件,然後在方法中新增飛機控制類的各個需要暴露的功能。

    public class PlaneDelegation{    
      private PlaneControl planeControl;    //private外部不可訪問
    
      // 飛行員許可權代理類,普通飛行員不可以開火
      PlaneDelegation(){
          planeControl=new PlaneControl();
      }
      public void speed(){
          planeControl.speed();
      }
      public void left(){
          planeControl.left();
      }
      public void right(){
          planeControl.right();
      }
    }
    
    final class PlaneControl {// final表示不可繼承,控制器都能繼承那還得了
      protected void speed() {}
      protected void fire() {}
      protected void left() {}
      protected void right() {}
    }

說明:

繼承:程式碼複用,引用不靈活; 組合:程式碼複用, 介面:引用靈活; 推薦組合+介面使用,看IO中包裝流FilterInputStream中的策略模式

7. 什麼是建構函式

建構函式是函式的一種特殊形式,特殊在哪裡?建構函式中不需要定義返回型別(void是無需返回值的意思,請注意區分兩者),且建構函式的名稱與所在的類名完全一致,其餘的與函式的特性相同,可以帶有引數列表,可以存在函式的過載現象。

一般用來初始化一些成員變數,當要生成一個類的物件(例項)的時候就會呼叫類的建構函式。如果不顯示宣告類的構造方法,會自動生成一個預設的不帶引數的空的建構函式。

public class Demo{
   private int num=0;

   //無參建構函式
   Demo()
  {
    System.out.println("constractor_run");
  }

   //有參建構函式
   Demo(int num)
  {
    System.out.println("constractor_args_run");
  }

   //普通成員函式
  public void demoFunction()
  {
    System.out.println("function_run");
  }
}

在這裡要說明一點,如果在類中我們不宣告建構函式,JVM會幫我們預設生成一個空引數的建構函式;如果在類中我們聲明瞭帶引數列表的建構函式,JVM就不會幫我們預設生成一個空引數的建構函式,我們想要使用空引數的建構函式就必須自己去顯式的宣告一個空參的建構函式。

建構函式的作用

  通過開頭的介紹,建構函式的輪廓已經漸漸清晰,那麼為什麼會有建構函式呢?建構函式有什麼作用?建構函式是面向物件程式設計思想所需求的,它的主要作用有以下兩個:

  • 建立物件。任何一個物件建立時,都需要初始化才能使用,所以任何類想要建立例項物件就必須具有建構函式。
  • 物件初始化。建構函式可以對物件進行初始化,並且是給與之格式(引數列表)相符合的物件初始化,是具有一定針對性的初始化函式。

8. 向上造型和向下造型

父類引用能指向子類物件,子類引用不能指向父類物件;

向上造型

​ 父類引用指向子類物件,例如:Father f1 = new Son();

向下造型

​ 把指向子類物件的父類引用賦給子類引用,需要強制轉換,例如:

Father f1 = new Son();
Son s1 = (Son)f1;

但有執行出錯的情況:

Father f2 = new Father();
Son s2 = (Son)f2;//編譯無錯但執行會出現錯誤

在不確定父類引用是否指向子類物件時,可以用instanceof來判斷:

if(f3 instanceof Son){
     Son s3 = (Son)f3;
}

三、關鍵字

1. final與static的區別

final

  • 1. 資料
    • 宣告資料為常量,可以是編譯時常量,也可以是在執行時被初始化後不能被改變的常量。
    • 對於基本型別,final 使數值不變;
    • 對於引用型別,final 使引用不變,也就不能引用其它物件,但是被引用的物件本身是可以修改的。
final int x = 1;
// x = 2;  // cannot assign value to final variable 'x'
final A y = new A();
y.a = 1;
  • 2. 方法

    ​ 宣告方法不能被子類覆蓋。

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

    • 宣告類不允許被繼承。

static

  • 1. 靜態變數

    ​ 靜態變數在記憶體中只存在一份,只在類初始化時賦值一次。

    • 靜態變數:類所有的例項都共享靜態變數,可以直接通過類名來訪問它;
    • 例項變數:每建立一個例項就會產生一個例項變數,它與該例項同生共死。
public class A {
    private int x;        // 例項變數
    public static int y;  // 靜態變數
}

​ 注意:不能再成員函式內部定義static變數。

  • 2. 靜態方法

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

  • 3. 靜態語句塊

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

  • 4. 靜態內部類

    內部類的一種,靜態內部類不依賴外部類,且不能訪問外部類的非靜態的變數和方法。

  • 5. 靜態導包

import static com.xxx.ClassName.*

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

  • 6. 變數賦值順序

    靜態變數的賦值和靜態語句塊的執行優先於例項變數的賦值和普通語句塊的執行,靜態變數的賦值和靜態語句塊的執行哪個先執行取決於它們在程式碼中的順序。

public static String staticField = "靜態變數";
static {
    System.out.println("靜態語句塊");
}
public String field = "例項變數";
{
    System.out.println("普通語句塊");
}

​ 最後才執行建構函式

public InitialOrderTest() {
    System.out.println("建構函式");
}

存在繼承的情況下,初始化順序為:

  • 父類(靜態變數、靜態語句塊)
  • 子類(靜態變數、靜態語句塊)
  • 父類(例項變數、普通語句塊)
  • 父類(建構函式)
  • 子類(例項變數、普通語句塊)
  • 子類(建構函式)

2. break、continue、return

break

跳出當前迴圈;但是如果是巢狀迴圈,則只能跳出當前的這一層迴圈,只有逐層break才能跳出所有迴圈。

for (int i = 0; i < 10; i++) {
    // 在執行i==6時強制終止迴圈,i==6不會被執行
    if (i == 6)
        break;
    System.out.println(i);  
}  

輸出結果為0 1 2 3 4 56以後的都不會輸出

continue

終止當前迴圈,但是不跳出迴圈(在迴圈中continue後面的語句是不會執行了),繼續往下根據迴圈條件執行迴圈。

for (int i = 0; i < 10; i++) {  
    // i==6不會被執行,而是被中斷了    
    if (i == 6)
        continue;
    System.out.println(i);  
}

輸出結果為0 1 2 3 4 5 7 8 9;只有6沒有輸出

return

  • return 從當前的方法中退出,返回到該呼叫的方法的語句處,繼續執行。
  • return 返回一個值給呼叫該方法的語句,返回值的資料型別必須與方法的宣告中的返回值的型別一致。
  • return後面也可以不帶引數,不帶引數就是返回空,其實主要目的就是用於想中斷函式執行,返回呼叫函式處。

特別注意:返回值為void的方法,從某個判斷中跳出,必須用return!

3. final、finally和finalize有什麼區別【B77】

final

final用於宣告屬性、方法和類,分別表示屬性不可變、方法不可覆蓋和類不可被繼承。

  • final屬性:被final修飾的變數不可變(引用不可變)
  • final方法:不允許任何子類重寫這個方法,但子類仍然可以使用這個方法
  • final引數:用來表示這個引數在這個函式內部不允許被修改
  • final類:此類不能被繼承,所有方法都不能被重寫

finally

在異常處理的時候,提供finally塊來執行任何的清除操作。如果丟擲一個異常,那麼相匹配的catch字句就會執行,然後控制就會進入finally塊,前提是有finally塊。

例如:資料庫連線關閉操作上

finally作為異常處理的一部分,它只能用在try/catch語句中,並且附帶一個語句塊,表示這段語句最終一定會被執行(不管有沒有丟擲異常),經常被用在需要釋放資源的情況下。(×)(這句話其實存在一定的問題)

  • 異常情況說明:
    • 在執行try語句塊之前已經返回或丟擲異常,所以try對應的finally語句並沒有執行。
    • 我們在 try 語句塊中執行了 System.exit (0) 語句,終止了 Java 虛擬機器的執行。那有人說了,在一般的 Java 應用中基本上是不會呼叫這個 System.exit(0) 方法的
    • 當一個執行緒在執行 try 語句塊或者 catch 語句塊時被打斷(interrupted)或者被終止(killed),與其相對應的 finally 語句塊可能不會執行
    • 還有更極端的情況,就是線上程執行 try 語句塊或者 catch 語句塊時,突然宕機或者斷電,finally 語句塊肯定不會執行了。可能有人認為宕機、斷電這些理由有些強詞奪理,沒有關係,我們只是為了說明這個問題。

finalize

finalize()是Object中的方法,當垃圾回收器將要回收物件所佔記憶體之前被呼叫,即當一個物件被虛擬機器宣告死亡時會先呼叫它finalize()方法,讓此物件處理它生前的最後事情(這個物件可以趁這個時機掙脫死亡的命運)。要明白這個問題,先看一下虛擬機器是如何判斷一個物件該死的。

可以覆蓋此方法來實現對其他資源的回收,例如關閉檔案。

判定死亡

Java採用可達性分析演算法來判定一個物件是否死期已到。Java中以一系列”GC Roots”物件作為起點,如果一個物件的引用鏈可以最終追溯到”GC Roots”物件,那就天下太平。

否則如果只是A物件引用B,B物件又引用A,A,B引用鏈均未能達到”GC Roots”的話,那它倆將會被虛擬機器宣判符合死亡條件,具有被垃圾回收器回收的資格。

最後的救贖

上面提到了判斷死亡的依據,但被判斷死亡後,還有生還的機會。

如何自我救贖:

1.物件覆寫了finalize()方法(這樣在被判死後才會呼叫此方法,才有機會做最後的救贖);

2.在finalize()方法中重新引用到”GC Roots”鏈上(如把當前物件的引用this賦值給某物件的類變數/成員變數,重新建立可達的引用).

需要注意:

finalize()只會在物件記憶體回收前被呼叫一次(The finalize method is never invoked more than once by a Java virtual machine for any given object. );

finalize()的呼叫具有不確定行,只保證方法會呼叫,但不保證方法裡的任務會被執行完(比如一個物件手腳不夠利索,磨磨嘰嘰,還在自救的過程中,被殺死回收了)。

finalize()的作用

雖然以上以物件救贖舉例,但finalize()的作用往往被認為是用來做最後的