校招面試——Java 基礎知識

前言

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

from 2018/7/11

關於更詳細的校招面試指南,我已在Github上託管,請轉向
https://github.com/frank-lam/2019_campus_apply
持續更新中

一、基本概念

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 "";
}

參考資料:註解Annotation實現原理與自定義註解例子

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()的作用往往被認爲是用來做最後的資源回收。
基於在自我救贖中的表現來看,此方法有很大的不確定性(不保證方法中的任務執行完)而且運行代價較高。所以用來回收資源也不會有什麼好的表現。

綜上:finalize()方法並沒有什麼鳥用。

至於爲什麼會存在一個雞肋的方法:書中說“它不是C/C++中的析構函數,而是Java剛誕生時爲了使C/C++程序員更容易接受它所做出的一個妥協”。

參考資料:

4. assert有什麼作用?

斷言(assert)作爲一種軟件調試的方法,提供了一種在代碼中進行正確性檢查的機制,目前很多開發語言都支持這種機制。

在實現中,assertion就是在程序中的一條語句,它對一個boolean表達式進行檢查,一個正確程序必須保證這個boolean表達式的值爲true;如果該值爲false,說明程序已經處於不正確的狀態下,系統將給出警告並且退出。一般來說,assertion用於保證程序最基本、關鍵的正確性。assertion檢查通常在開發和測試時開啓。爲了提高性能,在軟件發佈後,assertion檢查通常是關閉的。下面簡單介紹一下Java中assertion的實現。

在語法上,爲了支持assertion,Java增加了一個關鍵字assert。它包括兩種表達式,分別如下:

assert

public static void main(String[] args) {
    System.out.println("123");

    int a = 0;
    int b = 1;
    assert a == b; //需顯示開啓,默認爲不開啓狀態 
    assert a == b : "執行失敗!";

    System.out.println("1234");
}

assert的應用範圍很多,主要包括:

  • 檢查控制流
  • 檢查輸入參數是否有效
  • 檢查函數結果是否有效
  • 檢查程序不變

5. volatile

每次都讀錯,美式發音:volatile /’vɑlətl/ adj. [化學] 揮發性的;不穩定的;爆炸性的;反覆無常的

​ volatile是一個類型修飾符(type specifier),它是被設計用來修飾被不同線程訪問和修改的變量。在使用volatile修飾成員變量後,所有線程在任何時間所看到變量的值都是相同的。此外,使用volatile會組織編譯器對代碼的優化,因此會降低程序的執行效率。所以,除非迫不得已,否則,能不使用volatile就儘量不要使用volatile

  • 每次訪問變量時,總是獲取主內存的最新值
  • 每次修改變量後,立刻寫回到主內存中


參考資料:

6. instanceof

instanceof 是 Java 的一個二元操作符,類似於 ==,>,< 等操作符。

instanceof 是 Java 的保留關鍵字。它的作用是測試它左邊的對象是否是它右邊的類的實例,返回 boolean 的數據類型。

public class Main {

public static void main(String[] args) {
   Object testObject = new ArrayList();
      displayObjectClass(testObject);
   }
   public static void displayObjectClass(Object o) {
      if (o instanceof Vector)
      System.out.println("對象是 java.util.Vector 類的實例");
      else if (o instanceof ArrayList)
      System.out.println("對象是 java.util.ArrayList 類的實例");
      else
      System.out.println("對象是 " + o.getClass() + " 類的實例");
   }
}

7. strictfp

strictfp, 即 strict float point (精確浮點)。

strictfp 關鍵字可應用於類、接口或方法。使用 strictfp 關鍵字聲明一個方法時,該方法中所有的float和double表達式都嚴格遵守FP-strict的限制,符合IEEE-754規範。當對一個類或接口使用 strictfp 關鍵字時,該類中的所有代碼,包括嵌套類型中的初始設定值和代碼,都將嚴格地進行計算。嚴格約束意味着所有表達式的結果都必須是 IEEE 754 算法對操作數預期的結果,以單精度和雙精度格式表示。

如果你想讓你的浮點運算更加精確,而且不會因爲不同的硬件平臺所執行的結果不一致的話,可以用關鍵字strictfp.

8. transient

transient 英 /’trænzɪənt/ adj. 短暫的;路過的 n. 瞬變現象;過往旅客;候鳥

我們都知道一個對象只要實現了Serilizable接口,這個對象就可以被序列化,Java的這種序列化模式爲開發者提供了很多便利,我們可以不必關係具體序列化的過程,只要這個類實現了Serilizable接口,這個類的所有屬性和方法都會自動序列化。

然而在實際開發過程中,我們常常會遇到這樣的問題,這個類的有些屬性需要序列化,而其他屬性不需要被序列化,打個比方,如果一個用戶有一些敏感信息(如密碼,銀行卡號等),爲了安全起見,不希望在網絡操作(主要涉及到序列化操作,本地序列化緩存也適用)中被傳輸,這些信息對應的變量就可以加上transient關鍵字。換句話說,這個字段的生命週期僅存於調用者的內存中而不會寫到磁盤裏持久化。

總之,Java 的transient關鍵字爲我們提供了便利,你只需要實現Serilizable接口,將不需要序列化的屬性前添加關鍵字transient,序列化對象的時候,這個屬性就不會序列化到指定的目的地中

參考資料:

四、基本數據類型與運算

1. Java的基本數據類型/引用類型有哪些?知道自動裝箱和拆箱嗎?

  • 4類8種基本數據類型。4整數型,2浮點型,1布爾型,1字符型
類型 存儲 取值範圍 默認值 包裝類
byte 8 最大存儲數據量是255,最小-27,最大27-1,即:[-128~127] (byte) 0 Byte
short 16 最大數據存儲量是65536,[-215,215-1],±3萬 [-32768,32767] (short) 0 Short
int 32 最大數據存儲容量是231-1,範圍:[-231,231-1],±21億[ -2147483648, 2147483647] 0 Integer
long 64 最大數據存儲容量是264-1,範圍:[-263,263-1], ±922億億(±(922+16個零)) 0L Long
float 32 數據範圍在3.4e-45~1.4e38,直接賦值時必須在數字後加上f或F 0.0f Float
double 64 數據範圍在4.9e-324~1.8e308,賦值時可以加d或D也可以不加 0.0d Double
boolean 1 只有true和false兩個取值 false Boolean
char 16 存儲Unicode碼,用單引號賦值 ‘\u0000’ (null) Character

- 引用數據類型
- 類(class)、接口(interface)、數組
- 自動裝箱和拆箱
- 基本數據類型和它對應的封裝類型之間可以相互轉換。自動拆裝箱是jdk5.0提供的新特特性,它可以自動實現類型的轉換
- 裝箱:從基本數據類型封裝類型叫做裝箱
- 拆箱:從封裝類型基本數據類型叫拆箱

jdk 1.5
public class TestDemo {
    public static void main(String[] args) {
        Integer m =10;
        int i=m;
    }
}

​ 上面的代碼在jdk1.4以後的版本都不會報錯,它實現了自動拆裝箱的功能,如果是jdk1.4,就得這樣寫了

jdk 1.4
public class TestDemo {
    public static void main(String[] args) {
        Integer b = new Integer(210);
        int c = b.intValue();
    }
}

2. ValueOf緩存池

new Integer(123)Integer.valueOf(123)的區別在於,new Integer(123) 每次都會新建一個對象,而 Integer.valueOf(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() 方法,因此多個 Integer 實例使用自動裝箱來創建並且值相同,那麼就會引用相同的對象。

Integer m = 123;
Integer n = 123;
System.out.println(m == n); // 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。

static final int low = -128;
static final int high;
static final Integer cache[];

static {
    // high value may be configured by property
    int h = 127;
    String integerCacheHighPropValue =
        sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
    if (integerCacheHighPropValue != null) {
        try {
            int i = parseInt(integerCacheHighPropValue);
            i = Math.max(i, 127);
            // Maximum array size is Integer.MAX_VALUE
            h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
        } catch( NumberFormatException nfe) {
            // If the property cannot be parsed into an int, ignore it.
        }
    }
    high = h;

    cache = new Integer[(high - low) + 1];
    int j = low;
    for(int k = 0; k < cache.length; k++)
        cache[k] = new Integer(j++);

    // range [-128, 127] must be interned (JLS7 5.1.7)
    assert IntegerCache.high >= 127;
}

Java 還將一些其它基本類型的值放在緩衝池中,包含以下這些:

  • 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

因此在使用這些基本類型對應的包裝類型時,就可以直接使用緩衝池中的對象。

StackOverflow : Differences between new Integer(123), Integer.valueOf(123) and just 123

3. i++和++i有什麼區別【B97】

i++是在程序執行完畢後進行自增,而++i是在程序開始執行前進行自增。

i++的操作分三步

  1. 棧中取出i
  2. i自增1
  3. 將i存到棧

所以i++不是原子操作,上面的三個步驟中任何一個步驟同時操作,都可能導致i的值不正確自增

++i

在多核的機器上,cpu在讀取內存i時也會可能發生同時讀取到同一值,這就導致兩次自增,實際只增加了一次。

i++和++i都不是原子操作

原子性:指的是一個操作是不可中斷的。即使是在多個線程一起執行的時候,一個操作一旦開始,就不會被其他線程打斷。

4. 位運算符

Java定義了位運算符,應用於整數類型(int),長整型(long),短整型(short),字符型(char),和字節型(byte)等類型。

下表列出了位運算符的基本運算,假設整數變量A的值爲60和變量B的值爲13

A:0011 1100

B:0000 1101

操作符 描述 例子
如果相對應位都是1,則結果爲1,否則爲0 (A&B)得到12,即0000 1100
| 如果相對應位都是0,則結果爲0,否則爲1 (A|B)得到61,即 0011 1101
^ 如果相對應位值相同,則結果爲0,否則爲1 (A^B)得到49,即 0011 0001
按位取反運算符翻轉操作數的每一位,即0變成1,1變成0 (〜A)得到-61,即1100 0011
<< (左移一位乘2)按位左移運算符。左操作數按位左移右操作數指定的位數。左移n位表示原來的值乘2n A << 2得到240,即 1111 0000
>
(右移一位除2)有符號右移,按位右移運算符。左操作數按位右移右操作數指定的位數 A >> 2得到15即 1111
>
無符號右移,按位右移補零操作符。左操作數的值按右操作數指定的位數右移,移動得到的空位以零填充 A>>>2得到15即0000 1111

5. 原碼、補碼、反碼是什麼

機器數

一個數在計算機中的二進制表示形式,叫做這個數的機器數。機器數是帶符號的,在計算機用一個數的最高位存放符號,正數爲0,負數爲1。

比如,十進制中的數 +3 ,計算機字長爲8位,轉換成二進制就是00000011。如果是 -3 ,就是 10000011 。那麼,這裏的 00000011 和 10000011 就是機器數。

真值

因爲第一位是符號位,所以機器數的形式值就不等於真正的數值。例如上面的有符號數 10000011,其最高位1代表負,其真正數值是 -3 而不是形式值131(10000011轉換成十進制等於131)。所以,爲區別起見,將帶符號位的機器數對應的真正數值稱爲機器數的真值。

例:0000 0001的真值 = +000 0001 = +1,1000 0001的真值 = –000 0001 = –1

原碼

原碼就是符號位加上真值的絕對值, 即用第一位表示符號, 其餘位表示值. 比如如果是8位二進制:

[+1]原 = 0000 0001

[-1]原 = 1000 0001

第一位是符號位. 因爲第一位是符號位, 所以8位二進制數的取值範圍就是:

[1111 1111 , 0111 1111],即:

[-127 , 127]

原碼是人腦最容易理解和計算的表示方式.

反碼

反碼的表示方法是:

  • 正數的反碼是其本身;
  • 負數的反碼是在其原碼的基礎上, 符號位不變,其餘各個位取反

[+1] = [00000001]原 = [00000001]反

[-1] = [10000001]原 = [11111110]反

可見如果一個反碼錶示的是負數, 人腦無法直觀的看出來它的數值. 通常要將其轉換成原碼再計算。

補碼

補碼的表示方法是:

  • 正數的補碼就是其本身;
  • 負數的補碼是在其原碼的基礎上, 符號位不變, 其餘各位取反, 最後+1. (即在反碼的基礎上+1)

[+1] = [00000001]原 = [00000001]反 = [00000001]補

[-1] = [10000001]原 = [11111110]反 = [11111111]補

對於負數, 補碼錶示方式也是人腦無法直觀看出其數值的。 通常也需要轉換成原碼在計算其數值。

參考資料:

6. 不用額外變量交換兩個整數的值

如果給定整數a和b,用以下三行代碼即可交換a和b的值

a = a ^ b;
b = a ^ b;
a = a ^ b;
  • 假設a異或b的結果記爲c,c就是a整數位信息和b整數位信息的所有不同信息。比如:a=4=100,b=3=011,ab=c=111
  • a異或c的結果就是b,比如:a=4=100,c=111,a^c=011=3=b
  • b異或c的結果就是a,比如:b=3=011,c=111,b^c=100=4=a

說明:位運算的題目基本上都帶有靠經驗積累纔會做的特徵,也就是準備階段需要做足夠多的題,面試時纔會有良好的感覺。

7. 不使用運算符進行a+b操作

  • a^b; 得到不含進位之和
  • (a&b)<<1; 進位
  • 只要進位不爲零,則迭代;否則返回
public class Solution {
    /*
     * @param : An integer
     * @param : An integer
     * @return: The sum of a and b
     */
    public int aplusb(int a, int b) {

        int sum_without_carry, carry;
        sum_without_carry = a^b; //沒有進位的和
        carry = (a&b)<<1; //進位
        if(carry==0)
            return sum_without_carry;
        else 
            return aplusb(sum_without_carry, carry);   
    }
};

8. &和&& 、|和||的區別?【阿里實習生面試】

​ (1)&&和&都是表示與,區別是&&只要第一個條件不滿足,後面條件就不再判斷。而&要對所有的條件都進行判斷。

// 例如:
public static void main(String[] args) {  
    if((23!=23)&&(100/0==0)){  
        System.out.println("運算沒有問題。");  
    }else{  
        System.out.println("沒有報錯");  
    }  
}  
// 輸出的是“沒有報錯”。而將&&改爲&就會如下錯誤:
// Exception in thread "main" java.lang.ArithmeticException: / by zero
  • 原因:

    • &&時判斷第一個條件爲false,後面的100/0==0這個條件就沒有進行判斷。

    • &時要對所有的條件進行判斷,所以會對後面的條件進行判斷,所以會報錯。

    (2)||和|都是表示“或”,區別是||只要滿足第一個條件,後面的條件就不再判斷,而|要對所有的條件進行判斷。 看下面的程序:

public static void main(String[] args) {  
    if((23==23)||(100/0==0)){  
        System.out.println("運算沒有問題。");  
    }else{  
        System.out.println("沒有報錯");  
    }  
}
// 此時輸出“運算沒有問題”。若將||改爲|則會報錯。
  • 原因
    • ||判斷第一個條件爲true,後面的條件就沒有進行判斷就執行了括號中的代碼
    • 而|要對所有的條件進行判斷, 所以會報錯

五、字符串與數組

1. String、StringBuffer、StringBuilder以及對String不變性的理解

  • String、StringBuffer、StringBuilder
    • 都是 final 類, 都不允許被繼承;
    • String 長度是不可變的, StringBuffer、StringBuilder 長度是可變的;
    • StringBuffer 是線程安全的, StringBuilder 不是線程安全的,但它們兩個中的所有方法都是相同的,StringBuffer在StringBuilder的方法之上添加了synchronized修飾,保證線程安全。
    • StringBuilder比StringBuffer擁有更好的性能。
    • 如果一個String類型的字符串,在編譯時就可以確定是一個字符串常量,則編譯完成之後,字符串會自動拼接成一個常量。此時String的速度比StringBuffer和StringBuilder的性能好的多。
  • String不變性的理解
    • String 類是被final進行修飾的,不能被繼承。
    • 在用+號鏈接字符串的時候會創建新的字符串。
    • String s = new String(“Hello world”); 可能創建兩個對象也可能創建一個對象。如果靜態區中有“Hello world”字符串常量對象的話,則僅僅在堆中創建一個對象。如果靜態區中沒有“Hello world”對象,則堆上和靜態區中都需要創建對象。
    • 在 java 中, 通過使用 “+” 符號來串聯字符串的時候, 實際上底層會轉成通過 StringBuilder 實例的 append() 方法來實現。

2. String有重寫Object的hashcode和toString嗎?如果重寫equals不重寫hashcode會出現什麼問題?

  • String有重寫Object的hashcode和toString嗎?

    • String重寫了Object類的hashcode和toString方法。
  • 當equals方法被重寫時,通常有必要重寫hashCode方法,以維護hashCode方法的常規協定,該協定聲明相對等的兩個對象必須有相同的hashCode

    • object1.euqal(object2)時爲true, object1.hashCode() == object2.hashCode() 爲true
    • object1.hashCode() == object2.hashCode() 爲false時,object1.euqal(object2)必定爲false
    • object1.hashCode() == object2.hashCode() 爲true時,但object1.euqal(object2)不一定定爲true
  • 重寫equals不重寫hashcode會出現什麼問題

    • 在存儲散列集合時(如Set類),如果原對象.equals(新對象),但沒有對hashCode重寫,即兩個對象擁有不同的hashCode,則在集合中將會存儲兩個值相同的對象,從而導致混淆。因此在重寫equals方法時,必須重寫hashCode方法。

3. 如果你定義一個類,包括學號,姓名,分數,如何把這個對象作爲key?要重寫equals和hashcode嗎

  • 需要重寫equals方法和hashcode,必須保證對象的屬性改變時,其hashcode不能改變。

4. 字面量

在編程語言中,字面量(literal)指的是在源代碼中直接表示的一個固定的值。

八進制是用在整數字面量之前添加“0”來表示的。

十六進制用在整數字面量之前添加“0x”或者“0X”來表示的

Java 7中新增了二進制:用在整數字面量之前添加“0b”或者“0B”來表示的。

在數值字面量中使用下劃線
在Java7中,數值字面量,不管是證書還是浮點數都允許在數字之間插入任意多個下劃線。並且不會對數值產生影響,目的是方便閱讀,規則只能在數字之間使用。

public class BinaryIntegralLiteral {
    public static void main(String[] args) {
        System.out.println(0b010101);
        System.out.println(0B010101);
        System.out.println(0x15A);
        System.out.println(0X15A);
        System.out.println(077);
        System.out.println(5_000);
        /**
         * 輸出結果
         * 21 
         * 21 
         * 346 
         * 346 
         * 63
         * 5000
         */
    }
}

六、異常處理

1. 常見異常分爲那兩種(Exception,Error),常見異常的基類以及常見的異常

  • Throwable是java語言中所有錯誤和異常的超類(萬物即可拋)。它有兩個子類:Error、Exception。

  • 異常種類

    • Error:Error爲錯誤,是程序無法處理的,如OutOfMemoryError、ThreadDeath等,出現這種情況你唯一能做的就是聽之任之,交由JVM來處理,不過JVM在大多數情況下會選擇終止線程。
    • Exception:Exception是程序可以處理的異常。它又分爲兩種CheckedException(受撿異常),一種是UncheckedException(不受檢異常)。
    • 受檢異常(CheckException):發生在編譯階段,必須要使用try…catch(或者throws)否則編譯不通過。
    • 非受檢異常 (UncheckedException):是程序運行時錯誤,例如除 0 會引發 Arithmetic Exception,此時程序奔潰並且無法恢復。 (發生在運行期,具有不確定性,主要是由於程序的邏輯問題所引起的,難以排查,我們一般都需要縱觀全局才能夠發現這類的異常錯誤,所以在程序設計中我們需要認真考慮,好好寫代碼,儘量處理異常,即使產生了異常,也能儘量保證程序朝着有利方向發展。 )
  • 常見異常的基類(Exception)

    • IOException
    • RuntimeException
  • 常見的異常

    圖像

七、Object 通用方法

以下爲Object中的通用方法

public final native Class<?> getClass()

public native int hashCode()

public boolean equals(Object obj)

protected native Object clone() throws CloneNotSupportedException

public String toString()

public final native void notify()

public final native void notifyAll()

public final native void wait(long timeout) throws InterruptedException

public final void wait(long timeout, int nanos) throws InterruptedException

public final void wait() throws InterruptedException

protected void finalize() throws Throwable {} // JVM內存回收之finalize()方法

equals()

1. equals() 與 == 的區別

  • 對於基本類型,== 判斷兩個值是否相等,基本類型沒有 equals() 方法。
  • 對於引用類型,== 判斷兩個實例是否引用同一個對象,而 equals() 判斷引用的對象是否等價。
Integer x = new Integer(1);
Integer y = new Integer(1);
System.out.println(x.equals(y)); // true
System.out.println(x == y);      // false

2. 等價關係

(一)自反性

x.equals(x); // true

(二)對稱性

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

(三)傳遞性

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

(四)一致性

多次調用 equals() 方法結果不變

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

(五)與 null 的比較

對任何不是 null 的對象 x 調用 x.equals(null) 結果都爲 false

x.euqals(null); // false;

3. 實現

  • 檢查是否爲同一個對象的引用,如果是直接返回 true;
  • 檢查是否是同一個類型,如果不是,直接返回 false;
  • 將 Object 實例進行轉型;
  • 判斷每個關鍵域是否相等。
public class EqualExample {
    private int x;
    private int y;
    private int z;

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

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        EqualExample that = (EqualExample) o;

        if (x != that.x) return false;
        if (y != that.y) return false;
        return z == that.z;
    }
}

hashCode()

hasCode() 返回散列值,而 equals() 是用來判斷兩個實例是否等價。等價的兩個實例散列值一定要相同,但是散列值相同的兩個實例不一定等價。

在覆蓋 equals() 方法時應當總是覆蓋 hashCode() 方法,保證等價的兩個實例散列值也相等。

下面的代碼中,新建了兩個等價的實例,並將它們添加到 HashSet 中。我們希望將這兩個實例當成一樣的,只在集合中添加一個實例,但是因爲 EqualExample 沒有實現 hasCode() 方法,因此這兩個實例的散列值是不同的,最終導致集合添加了兩個等價的實例。

EqualExample e1 = new EqualExample(1, 1, 1);
EqualExample e2 = new EqualExample(1, 1, 1);
System.out.println(e1.equals(e2)); // true
HashSet<EqualExample> set = new HashSet<>();
set.add(e1);
set.add(e2);
System.out.println(set.size());   // 2

理想的散列函數應當具有均勻性,即不相等的實例應當均勻分佈到所有可能的散列值上。這就要求了散列函數要把所有域的值都考慮進來,可以將每個域都當成 R 進制的某一位,然後組成一個 R 進制的整數。R 一般取 31,因爲它是一個奇素數,如果是偶數的話,當出現乘法溢出,信息就會丟失,因爲與 2 相乘相當於向左移一位。

一個數與 31 相乘可以轉換成移位和減法:31\*x == (x<<5)-x,編譯器會自動進行這個優化。

@Override
public int hashCode() {
    int result = 17;
    result = 31 * result + x;
    result = 31 * result + y;
    result = 31 * result + z;
    return result;
}

toString()

默認返回 ToStringExample@4554617c 這種形式,其中 @ 後面的數值爲散列碼的無符號十六進制表示。

public class ToStringExample {
    private int number;

    public ToStringExample(int number) {
        this.number = number;
    }
}
ToStringExample example = new ToStringExample(123);
System.out.println(example.toString());
ToStringExample@4554617c

clone()

1. cloneable

clone() 是 Object 的 protect 方法,它不是 public,一個類不顯式去重寫 clone(),其它類就不能直接去調用該類實例的 clone() 方法。

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

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

public class CloneExample {
    private int a;
    private int b;

    @Override
    protected CloneExample clone() throws CloneNotSupportedException {
        return (CloneExample)super.clone();
    }
}
CloneExample e1 = new CloneExample();
try {
    CloneExample e2 = e1.clone();
} catch (CloneNotSupportedException e) {
    e.printStackTrace();
}
java.lang.CloneNotSupportedException: CloneTest

以上拋出了 CloneNotSupportedException,這是因爲 CloneTest 沒有實現 Cloneable 接口。

public class CloneExample implements Cloneable {
    private int a;
    private int b;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

應該注意的是,clone() 方法並不是 Cloneable 接口的方法,而是 Object 的一個 protected 方法。Cloneable 接口只是規定,如果一個類沒有實現 Cloneable 接口又調用了 clone() 方法,就會拋出 CloneNotSupportedException。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章