Java修煉之道--基礎

原做地址:https://github.com/frank-lam/2019_campus_applyhtml

前言

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

1、基本概念

1. Java程序初始化的順序是怎麼樣的

  在 Java 語言中,當實例化對象時,對象所在類的全部成員變量首先要進行初始化,只有當全部類成員完成初始化後,纔會調用對象所在類的構造函數建立象。mysql

初始化通常遵循3個原則:c++

  • 靜態對象(變量)優先於非靜態對象(變量)初始化,靜態對象(變量)只初始化一次,而非靜態對象(變量)可能會初始化屢次;
  • 父類優先於子類進行初始化;
  • 按照成員變量的定義順序進行初始化。 即便變量定義散佈於方法定義之中,它們依然在任何方法(包括構造函數)被調用以前先初始化;

加載順序git

  • 父類(靜態變量、靜態語句塊)
  • 子類(靜態變量、靜態語句塊)
  • 父類(實例變量、普通語句塊)
  • 父類(構造函數)
  • 子類(實例變量、普通語句塊)
  • 子類(構造函數)

實例程序員

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();
    }
}

結果是:github

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++ 須要手動回收。(C++11 中引入智能指針,使用引用計數法垃圾回收)
  • Java 不支持多重繼承,只能經過實現多個接口來達到相同目的,而 C++ 支持多重繼承
  • Java 不支持操做符重載,雖然能夠對兩個 String 對象支持加法運算,可是這是語言內置支持的操做,不屬於操做符重載,而 C++ 能夠。
  • Java 內置了線程的支持,而 C++ 須要依靠第三方庫。
  • Java 的 goto 是保留字,可是不可用,C++ 能夠使用 goto。
  • Java 不支持條件編譯,C++ 經過 #ifdef #ifndef 等預處理命令從而實現條件編譯

參考資料:web

3. 反射

先看一個知乎回答

  首先看一個在知乎上的優秀回答吧:面試

  反射是什麼呢?當咱們的程序在運行時,須要動態的加載一些類這些類可能以前用不到因此不用加載到 JVM,而是在運行時根據須要才加載,這樣的好處對於服務器來講不言而喻。算法

  舉個例子咱們的項目底層有時是用 mysql,有時用 oracle,須要動態地根據實際狀況加載驅動類,這個時候反射就有用了,假設 com.java.dbtest.myqlConnection,com.java.dbtest.oracleConnection 這兩個類咱們要用,這時候咱們的程序就寫得比較動態化,經過Class tc = Class.forName("com.java.dbtest.TestConnection"); 經過類的全類名讓 JVM 在服務器中找到並加載這個類,而若是是 oracle 則傳入的參數就變成另外一個了。這時候就能夠看到反射的好處了,這個動態性就體現出 Java 的特性了!

  舉多個例子,你們若是接觸過spring,會發現當你配置各類各樣的bean時,是以配置文件的形式配置的,你須要用到哪些bean就配哪些,spring容器就會根據你的需求去動態加載,你的程序就能健壯地運行。

什麼是反射

  反射 (Reflection) 是 Java 程序開發語言的特徵之一,它容許運行中的 Java 程序獲取自身的信息,而且能夠操做類或對象的內部屬性。經過 Class 獲取 class 信息稱之爲反射(Reflection)

  簡而言之,經過反射,咱們能夠在運行時得到程序或程序集中每個類型的成員和成員的信息。

  程序中通常的對象的類型都是在編譯期就肯定下來的,而 Java 反射機制能夠動態地建立對象並調用其屬性,這樣的對象的類型在編譯期是未知的。因此咱們能夠經過反射機制直接建立對象,即便這個對象的類型在編譯期是未知的。

  反射的核心是 JVM 在運行時才動態加載類或調用方法/訪問屬性,它不須要事先(寫代碼的時候或編譯期)知道運行對象是誰。

Java 反射框架主要提供如下功能:

  1. 在運行時判斷任意一個對象所屬的類;
  2. 在運行時構造任意一個類的對象;
  3. 在運行時判斷任意一個類所具備的成員變量和方法(經過反射甚至能夠調用private方法);
  4. 在運行時調用任意一個對象的方法

重點:是運行時而不是編譯時

主要用途

  不少人都認爲反射在實際的Java開發應用中並不普遍,其實否則。

當咱們在使用IDE(如Eclipse,IDEA)時,當咱們輸入一個對象或類並想調用它的屬性或方法時,一按點號,編譯器就會自動列出它的屬性或方法,這裏就會用到反射。

  反射最重要的用途就是開發各類通用框架

  不少框架(好比Spring)都是配置化的(好比經過XML文件配置JavaBean,Action之類的),爲了保證框架的通用性,它們可能須要根據配置文件加載不一樣的對象或類,調用不一樣的方法,這個時候就必須用到反射——運行時動態加載須要加載的對象。

  對與框架開發人員來講,反射雖小但做用很是大,它是各類容器實現的核心。而對於通常的開發者來講,不深刻框架開發則用反射用的就會少一點,不過了解一下框架的底層機制有助於豐富本身的編程思想,也是頗有益的。

得到Class對象

  1. 調用運行時類自己的 .class 屬性
Class clazz1 = Person.class;
System.out.println(clazz1.getName());
  1. 經過運行時類的對象獲取 getClass();
Person p = new Person();
Class clazz3 = p.getClass();
System.out.println(clazz3.getName());
  1. 使用 Class 類的 forName 靜態方法
public static Class<?> forName(String className)
// 在JDBC開發中經常使用此方法加載數據庫驅動:
Class.forName(driver);
  1. (瞭解)經過類的加載器 ClassLoader
ClassLoader classLoader = this.getClass().getClassLoader();
Class clazz5 = classLoader.loadClass(className);
System.out.println(clazz5.getName());

參考資料:

4. 註解

什麼是註解

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

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

  簡單來講:註解其實就是代碼中的特殊標記,這些標記能夠在編譯、類加載、運行時被讀取,並執行相對應的處理

爲何要用註解

傳統的方式,咱們是經過配置文件 .xml 來告訴類是如何運行的。

有了註解技術之後,咱們就能夠經過註解告訴類如何運行

例如:咱們之前編寫 Servlet 的時候,須要在 web.xml 文件配置具體的信息。咱們使用了註解之後,能夠直接在 Servlet 源代碼上,增長註解...Servlet 就被配置到 Tomcat 上了。也就是說,註解能夠給類、方法上注入信息。

明顯地能夠看出,這樣是很是直觀的,而且 Servlet 規範是推崇這種配置方式的。

基本Annotation

在 java.lang 包下存在着5個基本的 Annotation,重點掌握前三個。

  1. @Override 重寫註解
    • 若是咱們使用IDE重寫父類的方法,咱們就能夠看見它了。
    • @Override是告訴編譯器要檢查該方法是實現父類的,能夠幫咱們避免一些低級的錯誤。
    • 好比,咱們在實現 equals() 方法的時候,把 euqals() 打錯了,那麼編譯器就會發現該方法並非實現父類的,與註解 @Override 衝突,因而就會給予錯誤。
  2. @Deprecated 過期註解
    • 該註解也很是常見,Java 在設計的時候,可能以爲某些方法設計得很差,爲了兼容之前的程序,是不能直接把它拋棄的,因而就設置它爲過期。
    • Date對象中的 toLocalString() 就被設置成過期了
    • 當咱們在程序中調用它的時候,在 IDE 上會出現一條橫槓,說明該方法是過期的。
@Deprecated
public String toLocaleString() {
    DateFormat formatter = DateFormat.getDateTimeInstance();
    return formatter.format(this);
}
  1. @SuppressWarnings 抑制編譯器警告註解
    • 該註解在咱們寫程序的時候並非很常見,咱們能夠用它來讓編譯器不給予咱們警告
    • 當咱們在使用集合的時候,若是沒有指定泛型,那麼會提示安全檢查的警告
    • 若是咱們在類上添加了@SuppressWarnings這個註解,那麼編譯器就不會給予咱們警告了
  2. @SafeVarargs Java 7「堆污染」警告
    • 什麼是堆污染呢??當把一個不是泛型的集合賦值給一個帶泛型的集合的時候,這種狀況就很容易發生堆污染。
    • 這個註解也是用來抑制編譯器警告的註解,用的地方並很少。
  3. @FunctionalInterface 用來指定該接口是函數式接口
    • 用該註解顯示指定該接口是一個函數式接口。

自定義註解類編寫規則

  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實現原理與自定義註解例子

5. 泛型

通俗解釋

  通俗的講,泛型就是操做類型的 佔位符,即:假設佔位符爲 T,那麼這次聲明的數據結構操做的數據類型爲T類型。

  假定咱們有這樣一個需求:寫一個排序方法,可以對整型數組、字符串數組甚至其餘任何類型的數組進行排序,該如何實現?答案是能夠使用 Java 泛型

  使用 Java 泛型的概念,咱們能夠寫一個泛型方法來對一個對象數組排序。而後,調用該泛型方法來對整型數組、浮點數數組、字符串數組等進行排序。

泛型方法

  你能夠寫一個泛型方法,該方法在調用時能夠接收不一樣類型的參數。根據傳遞給泛型方法的參數類型,編譯器適當地處理每個方法調用。

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

  • 全部泛型方法聲明都有一個類型參數聲明部分(由尖括號分隔),該類型參數聲明部分在方法返回類型以前(在下面例子中的 <E>)。
  • 每個類型參數聲明部分包含一個或多個類型參數,參數間用逗號隔開。一個泛型參數,也被稱爲一個類型變量,是用於指定一個泛型類型名稱的標識符。
  • 類型參數能被用來聲明返回值類型,而且能做爲泛型方法獲得的實際參數類型的佔位符。
  • 泛型方法體的聲明和其餘方法同樣。注意類型參數 只能表明引用型類型,不能是原始類型 (像 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<?> 在邏輯上是 List<String>List<Integer> 等全部 List <具體類型實參> 的父類。
  2. 類型通配符上限經過形如 List 來定義,如此定義就是通配符泛型值接受 Number 及其下層子類類型。
  3. 類型通配符下限經過形如 List<? super Number> 來定義,表示類型只能接受 Number 及其三層父類類型,如 Objec 類型的實例。

參考資料:

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. 深拷貝與淺拷貝

  • 淺拷貝:被複制對象的全部變量都含有與原來的對象相同的值,而全部的對其餘對象的引用仍然指向原來的對象。換言之,淺拷貝僅僅複製所拷貝的對象,而不復制它所引用的對象。
  • 深拷貝:對基本數據類型進行值傳遞,對引用數據類型,建立一個新的對象,並複製其內容,此爲深拷貝。

參考資料:

9. Lamda表達式

跟上Java8 - 瞭解lambda
https://zhuanlan.zhihu.com/p/28093333

10. 字符串常量池

Java 中字符串對象建立有兩種形式,一種爲字面量形式,如 String str = "droid";,另外一種就是使用 new 這種標準的構造對象的方法,如 String str = new String("droid");,這兩種方式咱們在代碼編寫時都常用,尤爲是字面量的方式。然而這兩種實現其實存在着一些性能和內存佔用的差異。這一切都是源於 JVM 爲了減小字符串對象的重複建立,其維護了一個特殊的內存,這段內存被成爲字符串常量池或者字符串字面量池

工做原理

當代碼中出現字面量形式建立字符串對象時,JVM首先會對這個字面量進行檢查,若是字符串常量池中存在相同內容的字符串對象的引用,則將這個引用返回,不然新的字符串對象被建立,而後將這個引用放入字符串常量池,並返回該引用。

public class Test {
    public static void main(String[] args) {

        String s1 = "abc";
        String s2 = "abc";

        // 以上兩個局部變量都存在了常量池中
        System.out.println(s1 == s2); // true


        // new出來的對象不會放到常量池中,內存地址是不一樣的
        String s3 = new String();
        String s4 = new String();

        /**
        * 字符串的比較不能夠使用雙等號,這樣會比較內存地址
        * 字符串比較應當用equals,可見String重寫了equals
        */
        System.out.println(s3 == s4); // false
        System.out.println(s3.equals(s4)); // true
    }
}

2、面向對象

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();
    }
}

(三)匿名內部類

匿名內部類也就是沒有名字的內部類。正由於沒有名字,因此匿名內部類只能使用一次,它一般用來簡化代碼編寫。但使用匿名內部類還有個前提條件:必須繼承一個父類或實現一個接口

實例1:不使用匿名內部類來實現抽象方法

abstract class Person {
    public abstract void eat();
}
 
class Child extends Person {
    public void eat() {
        System.out.println("eat something");
    }
}
 
public class Demo {
    public static void main(String[] args) {
        Person p = new Child();
        p.eat();
    }
}

運行結果:eat something

能夠看到,咱們用 Child 繼承了 Person 類,而後實現了 Child 的一個實例,將其向上轉型爲 Person 類的引用

可是,若是此處的 Child 類只使用一次,那麼將其編寫爲獨立的一個類豈不是很麻煩?

這個時候就引入了匿名內部類

實例2:匿名內部類的基本實現

abstract class Person {
    public abstract void eat();
}
 
public class Demo {
    public static void main(String[] args) {
        Person p = new Person() {
            public void eat() {
                System.out.println("eat something");
            }
        };
        p.eat();
    }
}

運行結果:eat something

能夠看到,咱們直接將抽象類 Person 中的方法在大括號中實現了,這樣即可以省略一個類的書寫,而且,匿名內部類還能用於接口上。

實例3:在接口上使用匿名內部類

interface Person {
    public void eat();
}
 
public class Demo {
    public static void main(String[] args) {
        Person p = new Person() {
            public void eat() {
                System.out.println("eat something");
            }
        };
        p.eat();
    }
}

運行結果:eat something

由上面的例子能夠看出,只要一個類是抽象的或是一個接口,那麼其子類中的方法均可以使用匿名內部類來實現

最經常使用的狀況就是在多線程的實現上,由於要實現多線程必須繼承 Thread 類或是繼承 Runnable 接口

實例4:Thread類的匿名內部類實現

public class Demo {
    public static void main(String[] args) {
        Thread t = new Thread() {
            public void run() {
                for (int i = 1; i <= 5; i++) {
                    System.out.print(i + " ");
                }
            }
        };
        t.start();
    }
}

運行結果:1 2 3 4 5

實例5:Runnable接口的匿名內部類實現

public class Demo {
    public static void main(String[] args) {
        Runnable r = new Runnable() {
            public void run() {
                for (int i = 1; i <= 5; i++) {
                    System.out.print(i + " ");
                }
            }
        };
        Thread t = new Thread(r);
        t.start();
    }
}

運行結果:1 2 3 4 5

(四)靜態內部類

關鍵字 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 另一個類的對象,以添加該對象的特性。
  • 繼承:從基類繼承獲得子類,得到父類的特性。
  • 代理:在代理類中建立某功能的類,調用類的一些方法以得到該類的部分特性。

使用場合

  • 組合:各部件之間沒什麼關係,只須要組合便可。例如組裝電腦,須要 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{    }
  • 繼承:子類須要具備父類的功能,各子類之間有所差別。例如 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;
}

3、關鍵字

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 5 ;6之後的都不會輸出

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區別

final

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

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

finally

  在異常處理的時候,提供 finally 塊來執行任何的清除操做。若是拋出一個異常,那麼相匹配的 catch 字句就會執行,而後控制就會進入 finally 塊,前提是有 finally 塊。例如:數據庫鏈接關閉操做上

  finally 做爲異常處理的一部分,它只能用在 try/catch 語句中,而且附帶一個語句塊,表示這段語句最終必定會被執行(無論有沒有拋出異常),常常被用在須要釋放資源的狀況下。(×)(這句話其實存在必定的問題,尚未深刻了解,歡迎你們在 issue 中提出本身的看法)

  • 異常狀況說明:
    • 在執行 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

  若是 爲 true,則程序繼續執行。

  若是爲 false,則程序拋出 AssertionError,並終止執行。

  assert : <錯誤信息表達式>

  若是 爲 true,則程序繼續執行。

  若是爲 false,則程序拋出 java.lang.AssertionError,並輸入 <錯誤信息表達式> 。

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,序列化對象的時候,這個屬性就不會序列化到指定的目的地中。

參考資料:

9. native

native(即 JNI,Java Native Interface),凡是一種語言,都但願是純。好比解決某一個方案都喜歡就單單這個語言來寫便可。Java 平臺有個用戶和本地 C 代碼進行互操做的 API,稱爲 Java Native Interface (Java本地接口)。


參考資料:

4、基本數據類型與運算

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],
[-32768,32767],±3萬
(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 / flase 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() 方法的實現比較簡單,就是先判斷值是否在緩存池中,若是在的話就直接使用緩存池的內容。

// 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

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

參考資料:

3. i++和++i有什麼區別

i++

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

i++ 的操做分三步

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

三個階段:內存到寄存器,寄存器自增,寫回內存(這三個階段中間均可以被中斷分離開)

因此 i++ 不是原子操做,上面的三個步驟中任何一個步驟同時操做,均可能致使 i 的值不正確自增

++i

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

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

原子性:指的是一個操做是不可中斷的。即便是在多個線程一塊兒執行的時候,一個操做一旦開始,就不會被其餘線程打斷。

JMM 三大特性:原子性,可見性,有序性。詳情請閱讀 Github 倉庫:Java 併發編程 一文。

4. 位運算符

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

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

A(60):0011 1100

B(13):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] = [0000 0001] = [0000 0001] = [0000 0001]

[-1] = [1000 0001] = [1111 1110] = [1111 1111]

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

參考資料:

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,a^b = 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; 進位
  • 只要進位不爲零,則迭代;不然返回
#include <stdio.h>

int add(int a, int b)
{
    int c = a & b;
    int r = a ^ b;
    if(c == 0){
        return r;
    }
    else{
        return add(r, c << 1);
    }
}

int main(int argn, char *argv[])
{
    printf("sum = %d\n", add(-10000, 56789));
    return 0;
}

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,後面的條件就沒有進行判斷就執行了括號中的代碼
    • 而 | 要對全部的條件進行判斷,因此會報錯

5、字符串與數組

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
         */
    }
}

6、異常處理

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
  • 常見的異常

7、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。

參考資料:

相關文章
相關標籤/搜索