java學習總結之基礎

前言

萬事開頭難,準備從零把java相關知識點撿起來,把自己所學的Java知識點歸納,下面是關於java的一些基本知識點。

java代碼的運行過程

  • 創建java源程序,擴展名爲.java
  • 使用javac命令編譯源程序爲字節碼文件,擴展名爲.class
  • 使用java命令運行字節碼文件,在不同平臺執行

數據類型

下面用一張表概括:

數據類型 類型說明符 位數 字節
整形 int 32 4
短整型 short 16 2
長整形 long 64 8
字節型 byte 8 1
單精度浮點型 float 32 4
雙精度浮點型 double 64 8
字符類型 char 16 2
布爾類型 boolean - -
字符串類型 String - -
自定義類型 public class … - -

其中java的數據類型又分爲:

  • 基本類型/原始類型(primitive type):用來保存簡單的單個數據,如:int、short、long、byte、float、double、boolean、char共8種。

  • 類類型/引用類型(class type or reference types):用來保存複雜的組合數據,如String和自定義類型。

在java中,char類型實際是一個16位的無符號整數(<=65535),可以保存中文和轉義字符(\b,\t,\n等)。

而在java中並沒有明確的表示boolean類型應該佔多少位,其大小與JVM實現相關,JVM的建議如下:

1、boolean 類型應該被編譯成 int 類型來使用,佔 4 個字節;

2、boolean 數組應該被編譯成 byte 數組類型,每個 boolean 數組成員佔 1 個 字節.

可以肯定的是,boolean 類型不會只佔 1 個位,boolean 類型的大小與JVM實現相關。

成員變量和局部變量

public class 類名{

    數據類型 變量1//成員變量
    
    返回值類型 方法名(){
    
        數據類型 變量2//局部變量
        
    }
    
}

1、成員變量的作用域在整個類都是可見的。
2、局部變量的作用域僅在定義它們的方法中可見。
3、成員變量有默認的初始值(數字爲0,對象爲null)。
4、局部變量沒有默認的初始值,需要賦初值再使用。
5、成員變量和局部變量重名時,局部變量的優先級更高。

字符串

1、String類型

在java中String是引用類型,它的構造器如下:
了用以上new的方法創建一個字符串,還可以用以下方式:

String name1 = "rain";
String name2 = "rain";

那麼用new和用=有什麼不同的呢? new出來字符串是一個String對象,它被放在堆中,地址不一樣。用=賦值的字符串是從字符串池(String Pool,保存着所有字符串字面量,這些字面量在編譯時期就確定)中拿的,如果這個字符串在池中沒有,就會先放進池,所以上面兩個name1和name2是同一個字符串。

String中常用的方法:

String的比較:

獲取String的字串的方法:

在 Java 8 中,String 內部使用 char 數組存儲數據,在 Java 9 之後,String 類的實現改用 byte 數組存儲字符串,這些數組在都用final修飾,所以才保證String是不可變的,不可變就是每次意圖修改String,都會產生一個新的String對象。

1、在字符串的比較中==是用來比較地址的,String的equals方法纔是用來比較兩個字符串是否相等 ;
2、在 Java 7 之前,String Pool 被放在運行時常量池中,它屬於永久代,而在 Java 7,String Pool 被移到堆中。這是因爲永久代的空間有限,在大量使用字符串的場景下會導致 OutOfMemoryError 錯誤。

2、StringBuffer和StringBuilder

與String類不同的是,StringBuffer和StringBuilder是可變的,它們的對象可以被多次修改,因爲它裏面的數組並沒有使用final修飾,所以每次意圖修改StringBuffer或StringBuilder對象時,都會在原始對象上進行修改,不會產生新的對象,StringBuilder是在JDK5中被提出來的,它和StringBuffer之間最大的不同是StringBuilder的方法都不是線程安全的,而StringBuffer的方法都是線程安全的。

StringBuffer的構造方法(StringBuilder類似):

StringBuffer的常用方法:
在這裏插入圖片描述

3、字符串的拼接

在java中,可以通過+、String的concat方法、StringBuilder的append方法和StringBuffer的append方法來拼接字符串,如下:

使用+拼接:

public static void main(String[] args) {
    String str1 = "Hello";
    String str2 = "World!";
    String str = str1 + " " + str2;
}

反編譯後,如下:

public static void main(String[] args) {
    String str1 = "Hello";
    String str2 = "World!";
    String str = (new StringBuilder()).append(str1).append(" ").append(str2).toString();
}

可以看到使用**+**拼接字符串時,底層會new一個StringBuilder對象,調用StringBuilder的append方法來拼接。

使用String的concat方法拼接:

public static void main(String[] args) {
    String str1 = "Hello";
    String str2 = "World!";
    String str = str1.concat(" ").concat(str2);
}

查看String的concat方法的源碼,如下:

public String concat(String str) {
    int olen = str.length();
    if (olen == 0) {
        return this;
    }
  	//...
    int len = length();
    //創建一個新的長度的字節數組,新長度 = 原始字符串長度len + 要拼接字符串長度olen
    byte[] buf = StringUTF16.newBytesFor(len + olen);
    //把原始字符串的字節數組複製到buf中
    getBytes(buf, 0, UTF16);
    //把要拼接的字符串的字節數組複製到buf中
    str.getBytes(buf, len, UTF16);
    //通過buf創建一個String返回
    return new String(buf, UTF16);
}

可以看到concat方法底層是新創建一個字節數組,長度爲原始字符串長度和要拼接字符串長度之和,然後把原始字符串和要拼接字符串的字節數組複製到新的字節數組中,最後通過新的字節數組創建一個新的String對象返回,所以String的concat方法最終會返回一個新的String對象,這也說明了String的不可變性,不會修改原始字符串的字節數組到達拼接字符串的目的。

使用StringBuilder和StringBuffer的append方法拼接:

//以StringBuilder舉例,StringBuffer類似
public static void main(String[] args) {
    String str1 = "Hello";
    String str2 = "World!";
    StringBuilder str = new StringBuilder();
    str.append(str1).append(" ").append(str2).toString();
}

查看StringBuilder的append方法的源碼,如下:

public StringBuilder append(String str) {
    super.append(str);
    return this;
}

//StringBuilder的父類AbstractStringBuilder中的append方法
public AbstractStringBuilder append(String str) {
    if (str == null) {
        return appendNull();
    }
    int len = str.length();
    //擴容內部的字節數組,擴容後長度 = 原始字符串長度count + 要拼接的字符串長度len
    ensureCapacityInternal(count + len);
    //把要拼接的字符串追加到內部的字節數組
    putStringAt(count, str);
    count += len;
    return this;
}

可以看到,StringBuilder的append方法會直接拷貝待拼接的字符串字節數組到內部的字節數組中,如果內部的字節數組長度不夠,就會先擴容後再拷貝,所以append方法並不會產生新的StringBuilder對象,對於StringBuffer的append方法,它和StringBuilder的append方法的邏輯一樣,只是多了一個synchronized關鍵字。

效率比較:

分別使用+、concat方法、StringBuffer和StringBuilder的append方法來拼接字符串,各自的效率怎麼樣?在單線程環境下的一個for循環中拼接大量字符串,經過測試,它們的效率高低如下:

StringBuilder > StringBuffer > concat > +

+效率最低,這是因爲每次拼接字符串時,都會new一個StringBuilder對象來拼接,頻繁的新建對象是很耗時的,而StringBuffer每次append都需要進行同步,所以它的效率比StringBuilder低。

阿里巴巴Java開發手冊建議:在循環體內,使用 StringBuilderappend 方法進行字符串的拼接,而不要使用+,因爲+會頻繁的創建新的StringBuilder對象導致耗費更多的時間和造成內存資源的浪費。

包裝類

包裝類就是將java的基本數據類型打包成對象處理,包裝類都在java.lang包中,下面用一個表顯示:

基本數據類型 包裝類
int Interger
short Short
long Long
char Character
byte Byte
float Float
double Double
boolean Boolean

它涉及到以下兩種操作:

1、裝箱(boxing)

以double裝箱爲例:

double num = 3.14;
Double dnum1 = new Double(num);//1
Double dnum2 = Double.valueOf(num);//2
Double dnum3 = num;//3

註釋1、2都是手動裝箱,註釋3是自動裝箱。

2、拆箱(unboxing)

同樣上述double拆箱爲例:

num = dnum1;//1
num = dnum2.doubleValue;//2

註釋1是自動拆箱,註釋2是手動拆箱。

除了Double和Float,每個包裝類都會有一個默認大小的緩存池,例如Integer,緩存池默認大小是-128-127,緩存池中都是緩存了一些經常使用的值,而對於Double和Float,都是浮點型,它們沒有經常使用的值,編譯器會在自動裝箱過程中會調用 valueOf() 方法,因此多個值相同,且值在緩存池範圍內的包裝類實例使用自動裝箱來創建,那麼就會引用相同的對象。

ps:
1、包裝類沒有無參構造,所有包裝類的實例都是不可變的,一旦創建對象後,它們的內部值就不能再改變。
2、每個基本類型包裝類都有常量MAX_VALUE和MIN_VALUE。

關鍵字

1、final

防止擴展和重寫。

  • 修飾成員變量:常量(可以是編譯時常量,也可以是在運行時被初始化後不能被改變的常量),不可更改(對於基本數據類型,final使數值不能改變,對於引用類型,final使引用不能改變,即不能引用其他對象,但引用本身可以更改)
  • 修飾方法:不可被重寫
  • 修飾類:不可被繼承

2、static

可以通過類名直接訪問它修飾的屬性,靜態屬性和方法都是優先於類的實例存在。

  • 修飾變量:稱爲靜態變量(區別於實例變量)、類變量,類的所有實例都共享靜態變量,靜態變量在內存中只存在一份
  • 修飾方法:稱爲靜態方法,靜態方法必須有實現,它不依賴於任何實例,靜態方法中只能調用類的靜態屬性和靜態方法,方法中不能有 this 和 super 關鍵字
  • 修飾語句塊:稱爲靜態語句塊,在類初始化時運行一次
  • 修飾內部類:稱爲靜態內部類,非靜態內部類依賴於外部類的實例,而靜態內部類不需要

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

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

關於==、equal()和hashCode()

1、==

==是一個關係操作符,所以:

  • 如果左右兩邊的操作數是基本數據類型,那麼X==Y,就是判斷左右兩邊的操作數的值是否相等.
  • 如果左右兩邊的操作數是引用數據類型,那麼X==Y,就是判斷左右兩邊的操作數的內存地址是否相等.

2、equal()

equal()是用來判斷兩個對象是否等價,即兩個對象是否相等,所以如果要重寫一個equal方法,需要做到以下3步:

  • 先使用**==**判斷兩個對象的引用是否相等.(地址相同)
  • 然後使用**instanceof **判斷兩個對象是否是同一個類型.(類型相同)
  • 最後比較兩個對象的內容是否一致.(內容相同)

按照上面3步重寫的equal方法,滿足自反性、對稱性、傳遞性,如下:

  • 自反性:對於非null的x,x.equal(x)返回true;
  • 對稱性:對於非null的x,y,x.equal(y)返回true當且僅當y.equal(x)返回true;
  • 傳遞性:對於非null的x,y,z,如果x.equal(y)返回true,並且y.equal(z)返回true,那麼x.equal(z)返回true。

3、hashCode()

hashCode()用來返回一個對象的hash值,它是一個native方法,它主要使用於哈希表中的hash算法,用於定位一個元素的位置,所以當你的對象要作爲哈希表中的元素時,你要保證以下幾個原則:

  • 要比較兩個對象是否相等,必須使用equal方法,如果相等,那麼調用兩個對象的 hashCode 方法必須返回相同的結果,即相等的兩個對象返回的hashCode必須是相等的.
  • 如果兩個對象根據 equals方法比較是不相等的,則 hashCode 方法不一定得返回不同的整數.
  • 對同一個對象調用多次hashcode方法必須返回相同的hash值.
  • 兩個不同對象的hashcode可能相等.
  • 兩個不同hashcode的對象一定不相等.

在使用hashXX集合添加對象時,集合先調用該對象的hashCode方法,根據哈希函數得到對象在哈希表中的位置,如果該位置沒有元素,就直接把它存儲在該位置上;如果該位置已經有元素了,就調用對象的equal方法與該位置的每個元素逐個比較,如果相等,就更新該元素,如果都不相等,就把該對象的映射添加到這個位置對應的鏈表中。

因此在覆蓋 equals() 方法時應當總是覆蓋 hashCode() 方法,保證等價的兩個對象hash值也相等,不然會導致集合中出現重複的元素,一個好的習慣是equals方法中用到的成員變量也必定會在hashcode方法中用到,這樣就能保證兩個相等的對象hash值也相等

當你沒有重寫hashCode方法時,它可能返回以下的值:
1、隨機生成數字;
2、對象的內存地址,強制轉換爲int;
3、根據對象的內存地址生成;
4、1硬編碼(用於敏感性測試);
5、一個序列;
6、使用線程狀態結合xorshift算法生成。
具體返回什麼需要看不同JDK版本的實現。

異常

異常就是一種對象(Exception), 表示阻止程序正常執行的錯誤。異常類的層次結構如下:

  • 1、RuntimeException和Error以及它們的子類都稱爲免檢異常, 這種異常一般是由程序邏輯錯誤引起的,對於這種異常,可以選擇捕獲處理,也可以不處理。
  • 2、除了免檢異常,其他異常都稱爲必檢異常,這種異常跟程序運行的上下文環境有關,即使程序設計無誤,仍然可能因使用的問題而引發,所以Java強制要求我們必須對這些異常進行處理。

由於免檢異常可能在程序的任何一個地方出現,爲了避免過多的使用try-catch塊,java語言不強制要求編寫代碼捕獲免檢異常,也不要求在方法頭顯示聲明免檢異常。

1、常見的異常類型

2、java中的異常處理機制

異常處理機制就是可以使程序處理非預期的情景,並繼續正常執行,異常處理機制的主要組成如下:

  • try:監控有可能產生異常的語句塊
  • catch:以合理的方式捕獲異常
  • finally:不管有沒有異常,一定會執行的語句塊(一般用來釋放資源),除了遇到System.exit(0)語句
  • throw:手動引發異常
  • throws: 指定由方法引發的異常

所以一個異常捕獲處理語句可以如下形式:

try{
    //監控可能產生異常的語句塊
}catch(Exception1 e){
    //捕獲異常,處理異常,如打印異常信息,日誌紀錄
}catch(Exception2 e){
    //JDK7後簡化寫法catch(Exception1|Exception2|Exception3|... e)
}finally{
    //不管有無異常,一定會執行的語句,用來釋放資源
}

try塊中的代碼可能會引發多種類型的異常,當引發異常時,會按照catch的順序進行匹配異常類型,並執行第一個匹配的catch語句。

泛型

1、爲什麼使用泛型

在java5之前,任何類型的元素都可以“丟進“集合中,元素一旦進入集合中,元素的類型就會被集合忘記,導致從集合中取出的元素都是Object類型,需要進行強制類型轉換後才能變成我們”丟進“集合前的元素類型,這樣就導致了以下兩個缺點:

  • 1、編程的複雜度增加:任何從集合中取出的元素都要進行強制類型轉換,增加編程的工作量;
  • 2、運行時容易引發ClassCastException:由於任何類型的元素都可以放進集合中,導致集合中的元素的類型不一致,在取出元素強制類型轉換時,就有可能人爲地轉換錯誤,引發ClassCastException異常,導致程序崩潰.

所以爲了解決集合編譯時不檢查類型的問題,就出現了泛型,泛型(GenericType)是從java5開始支持的新語法,它又被稱爲參數化類型ParameterizedType,ParameterizedType是java5新增的Type,泛型它表示廣泛通用的類型,可以在代碼編譯期就進行類型檢查,在創建集合的時候可以動態地指明元素的類型是什麼,從而約束所有放進集合的元素類型一致,這樣從集合中取出元素時就不需要進行強制類型轉換,從而避免了在運行時出現ClassCastException異常,如果把錯誤類型元素放入集合,編譯器就會提出錯誤,所以在java中使用泛型時它保證只要程序在編譯期沒有提示“UnChecked”未經檢查警告,那麼運行時就不會產生ClassCastException

所以從java5之後,集合框架中的全部類和接口都增加了泛型支持,從而在創建集合時可以動態地指明元素的類型是什麼,如:List<String> list = new ArrayList<String>(),其中List<String>、ArrayList<String>就統稱爲泛型,而<>括號中的類型就稱爲類型形參

java7的時候出現了菱形語法簡化了泛型的寫法,java7開始允許使用泛型時構造器後面的<>括號內不帶類型形參,只需要給出<>括號就行,如:List<String> list = new ArrayList<>()。

泛型可以分爲泛型類(包括類和接口)和泛型方法

2、泛型類

泛型類就是直接在類或接口上定義的泛型,上面所講的集合也是屬於泛型類,如下創建一個泛型類:

public class Bean<T> {
    private T t;
    
    public void set(T t) { 
        this.t = t; 
    }
    
    public T get() {
        return t; 
    }
}

如果有多個類型形參,在<>括號中就用 , 隔開,在創建Bean實例的時候就可以動態的指明T(類型形參)的類型,如下:

Bean<String> bean = new Bean<String>();

泛型類中的T(類型形參)不存在繼承的關係,如下是錯誤的:

Bean<Object> bean = new Bean<String>();//錯誤,Bean<String>並不是Bean<Object>的子類。

同時需要注意靜態變量不能使用類型形參修飾,還有靜態方法不能使用泛型類聲明的類型形參,如下是錯誤的:

public class Bean<T> {
    private T t;
    private static T t2; //錯誤,類型形參在編譯後會被擦除
    public static void doWork(Bean<T> bean){}//錯誤,類型形參在編譯後會被擦除,如果靜態方法需要使用泛型,只能使用泛型方法
}

還有instanceof運算符後面不能使用泛型類,如下是錯誤的:

if(XX instanceof Bean<String>){//錯誤,不存在Bean<String>對應的Class<String>對象
    //...
}

以上錯誤的原因都可以歸結爲泛型擦除,不管傳入的類型形參是什麼類型,在運行時它們總是具有相同的類(Class)。

如果從泛型類派生子類時,必須爲作爲父類的泛型類的類型形參指明類型或者不寫<>括號,不寫<>括號時泛型類的類型形參默認爲上限類型,如果沒有上限,默認爲Object類型。

3、泛型方法

泛型方法就是直接在方法上定義的泛型,如下創建一個泛型方法:

public static <T> T doWork(Bean<T> bean){
    return bean.get();
}

如果有多個類型形參,在<>括號中就用 , 隔開,在調用doWork方法時,java會自動推斷方法形參的類型,從而推斷出類型形參的類型,如下:

Bean<String> bean = new Bean<String>();
doWork(bean);

上面doWork方法傳入形參爲bean實例,它的類型形參爲String類型,從而推斷出doWork方法的類型形參T爲String類型。

泛型方法允許類型形參被用來表示方法的一個或多個參數之間的類型依賴關係,或者返回值與參數之間的類型依賴關係,如果沒有這樣的類型依賴關係,就不應該使用泛型方法,可以考慮使用類型通配符。

4、類型通配符、上限和下限

類型通配符使用 ? 表示,它表示未知的元素類型,當不知道使用什麼類型接收時,此時可以使用 ?,如下:

//此時可以往doWork傳入List<String>、List<Integer>等實例
public static void doWork(List<?> list){
    for(Object o : list){
        System.out.println(o);
    }
}

不管往doWork方法中傳入任何類型的List<XX>實例,當用 ? 接收後,此時List集合中的所有元素類型都是Object,不能往元素類型是 ? 的集合中增加、修改元素,只能查詢、刪除。

類型通配符 ? 一般代表所有類型的父類,即Object,可以爲 ? 添加上限或下限,如下:

//上限:此時的類型形參必須爲Number或Number的子類
public static void doWork(List<? extends Number> list){
    for(Number o : list){
        System.out.println(o);
    }
}
//下限:此時的類型形參必須爲Number或Number的父類
public static void doWork(List<? super Number> list){
    for(Object o : list){
        System.out.println(o);
    }
}

在<>中加入extends,就是 <= 的關係,在<>中加入super,就是 >= 的關係。

extends和super也可以用於限制泛型的類型形參的上限和下限。

5、泛型擦除

泛型其實是一個語法糖,系統並不會爲每個泛型生成一個Class對象,它們在運行時始終具有相同的Class對象,例如List<String>、List<Integer>等泛型類在編譯之後,<>括號內的類型信息都會被擦除,在運行時都是使用List的Class對象,並且在反編譯後,還是使用強制類型來獲取元素,如下:

public static void main(String[] args){
    //類型形參爲Integer的泛型
    List<Integer> list = new ArrayList<>();
    list.add(1);
    Integer num = list.get(0);

     //類型形參爲String的泛型
    List<String> list2 = new ArrayList<>();
    list2.add("1");
    String str = list2.get(0);
}

//反編譯後
public static void main(String args[]) {
    //Integer類型被擦除
    List list = new ArrayList();
    list.add(Integer.valueOf(1));
    Integer num = (Integer)list.get(0);

	//String類型被擦除
    List list2 = new ArrayList();
    list2.add("1");
    String str = (String)list2.get(0);
}

還有當把一個具有泛型信息的變量賦值給另一個沒有泛型信息的變量時,所有<>括號之間的類型信息都會被擦除,如下:

//類型形參爲Integer的泛型
List<Integer> list = new ArrayList<>();
list.add(1);

//把具有Integer類型信息的泛型list賦值給沒有泛型信息的list2
List list2 = list;
//此時list的所有類型信息被擦除,變成了Object類型,這是可以往裏面添加任何類型的元素
//添加元素時會提示"UnChecked"警告,所以在訪問list2中的元素時,如果訪問不當就會在運行時引發ClassCastException
list2.add("123");

//這裏嘗試把"123"轉化爲Integer,將會引發ClassCastException
Integer num = (Integer) list2.get(1);
//這裏訪問正確
Object o = list2.get(1);//或者String num = (String)list2.get(1)

同時系統支持把沒有泛型信息的變量賦值給具有泛型信息的變量,而不會提示任何警告或錯誤,如下:

//類型形參爲Integer的泛型
List<Integer> list = new ArrayList<>();
list.add(1);

//list的Integer類型信息被擦除
List list2 = list;

//把擦除後的list賦值給類型形參爲String的泛型list3
List<String> list3 = list2;

//下面的訪問將會引起ClassCastException
//等價於String num = 1;
String num = list3.get(0);

上述代碼將會引發ClassCastException,因爲list3的類型信息是String,編譯器會把list3中的元素類型當作是String,而此時list3實際引用的變量泛型擦除後的list,泛型擦除後的list中的元素類型在編譯時是Object,但在運行時卻是Integer,所以在運行時,從list3中取出的元素是Integer類型,Integer是不可以強轉成String的,從而引起ClassCastException。

所以總的來說,泛型擦除主要在以下兩個方面:

  • 1、編譯之後,泛型會被擦除;(自動擦除)
  • 2、把泛型變量賦值給原始變量時,泛型會被擦除。(手動擦除)

結語

本文簡單介紹了java語言的基本知識點,希望大家有所收穫。

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