主頁 > 後端開發 > Java學習筆記

Java學習筆記

2022-10-04 07:01:40 後端開發

編碼

ASCII:用八位二進制的低七位,一共規定了128個字符的編碼,一個位元組表示一個字符,
擴展ASCII:第八位為1,規定了以1開頭的128個字符
Unicode:固定大小的編碼,通常兩個位元組表示一個字符,字母和漢字統一用兩個位元組,浪費空間
UTF-8:是一種變長的編碼方式,字母用一個位元組,漢字用三個位元組,是在互聯網上使用最廣的一中Unicode的實作方式
gbk:可以表示漢字,范圍廣,字母用一個位元組,漢字用兩個位元組 ANSI編碼 -- 不同地區采用的編碼的統稱
UTF-8編碼規則:
1)對于單位元組的符號,位元組的第一位設為0,后面7位為這個符號的 Unicode 碼,因此對于英語字母,UTF-8 編碼和 ASCII 碼是相同的,
2)對于n位元組的符號(n>1),第一個位元組的前n位都設為1,第n+1位設為0,后面位元組的前兩位一律設為10,剩下的沒有提及的二進制位,全部為這個符號的 Unicode 碼,
編碼 大小 支持語言
ASCII 1個位元組 英文
Unicode 2個位元組(生僻字4個) 所有語言
UTF-8 1-6個位元組,英文字母1個位元組,漢字3個位元組,生僻字4-6個位元組 所有語言

基本語法

撰寫 Java 程式時,應注意以下幾點:

  • 大小寫敏感:Java 是大小寫敏感的,這就意味著識別符號 Hello 與 hello 是不同的,
  • 類名:對于所有的類來說,類名的首字母應該大寫,如果類名由若干單詞組成,那么每個單詞的首字母應該大寫,例如 MyFirstJavaClass
  • 方法名:所有的方法名都應該以小寫字母開頭,如果方法名含有若干單詞,則后面的每個單詞首字母大寫,
  • 源檔案名:源檔案名必須和類名相同,當保存檔案的時候,你應該使用類名作為檔案名保存(切記 Java 是大小寫敏感的),檔案名的后綴為 .java,(如果檔案名和類名不相同則會導致編譯錯誤),
  • 主方法入口:所有的 Java 程式由 public static void main(String[] args) 方法開始執行,

識別符號

Java 所有的組成部分都需要名字,類名、變數名以及方法名都被稱為識別符號,

關于 Java 識別符號,有以下幾點需要注意:

  • 所有的識別符號都應該以字母(A-Z 或者 a-z),美元符($)、或者下劃線(_)開始
  • 首字符之后可以是字母(A-Z 或者 a-z),美元符($)、下劃線(_)或數字的任何字符組合
  • 關鍵字和保留字不能用作識別符號
  • 識別符號是大小寫敏感的
  • 識別符號不能包含空格
  • 合法識別符號舉例:age、$salary、_value、__1_value
  • 非法識別符號舉例:123abc、-salary

內置資料型別

Java語言提供了八種基本型別,六種數字型別(四個整數型,兩個浮點型),一種字符型別,還有一種布爾型,

byte:

  • byte 資料型別是8位、有符號的,以二進制補碼表示的整數;
  • 最小值是 -128(-2^7)
  • 最大值是 127(2^7-1)
  • 默認值是 0
  • byte 型別用在大型陣列中節約空間,主要代替整數,因為 byte 變數占用的空間只有 int 型別的四分之一;
  • 例子:byte a = 100,byte b = -50,

short:

  • short 資料型別是 16 位、有符號的以二進制補碼表示的整數
  • 最小值是 -32768(-2^15)
  • 最大值是 32767(2^15 - 1)
  • Short 資料型別也可以像 byte 那樣節省空間,一個short變數是int型變數所占空間的二分之一;
  • 默認值是 0
  • 例子:short s = 1000,short r = -20000,

int:

  • int 資料型別是32位、有符號的以二進制補碼表示的整數;
  • 最小值是 -2,147,483,648(-2^31)
  • 最大值是 2,147,483,647(2^31 - 1)
  • 一般地整型變數默認為 int 型別;
  • 默認值是 0
  • 例子:int a = 100000, int b = -200000,

long:

  • long 資料型別是 64 位、有符號的以二進制補碼表示的整數;

  • 最小值是 -9,223,372,036,854,775,808(-2^63)

  • 最大值是 9,223,372,036,854,775,807(2^63 -1)

  • 這種型別主要使用在需要比較大整數的系統上;

  • 默認值是 0L

  • 例子: long a = 100000L,Long b = -200000L,
    "L"理論上不分大小寫,但是若寫成"l"容易與數字"1"混淆,不容易分辯,所以最好大寫,

    //注意:java對于整型的默認型別為int,數的末尾要加上L
    long n1 = 100L;  
    

float:

  • float 資料型別是單精度、32位、符合IEEE 754標準的浮點數;

  • float 在儲存大型浮點陣列的時候可節省記憶體空間;

  • 默認值是 0.0f

  • 浮點數不能用來表示精確的值,如貨幣;

  • 例子:float f1 = 234.5f,

    //Java對于浮點數的默認型別為double
    float f1 = 1.1f;  //末尾的f不可省,否則相當于八個位元組的資料放到四個位元組,編譯會報錯
    

double:

  • double 資料型別是雙精度、64 位、符合IEEE 754標準的浮點數;

  • 浮點數的默認型別為double型別;

  • double型別同樣不能表示精確的值,如貨幣;

  • 默認值是 0.0d

  • 例子:double d1 = 123.4,

    System.out.println(8.1/3);     //結果為2.6999999999999997
    //盡量不要直接判斷兩個浮點數是否相等,應該以兩個數的差值的絕對值是否在某個精度范圍內來判斷兩個浮點數是否相等
    System.out.println(5.2e1);        //結果為52.0  注意末尾的0不可以省略
    

boolean:

  • boolean資料型別表示一位的資訊;

  • 只有兩個取值:true 和 false;

  • 這種型別只作為一種標志來記錄 true/false 情況;

  • 默認值是 false

  • 例子:boolean one = true,

    不可以用0/1代替false/true,只有兩個值

char:

  • char型別是一個單一的 16 位 Unicode 字符 兩個位元組;
  • 最小值是 \u0000(即為0);
  • 最大值是 \uffff(即為65,535);
  • char 資料型別可以儲存任何字符;
  • 例子:char letter = 'A';,

注:字符型的本質,其實是字符對應的ASCII編碼

自動型別轉換

  • 有多種型別的資料混合運算時,系統首先自動將所有資料轉換成容量最大的那種資料型別,然后再進行計算

    int n1 = 10;
    float t1 = n1 + 1.1;   //錯誤,系統會將n1 + 1.1的結果轉為默認的double型別,將8位元組的double賦給4位元組的float會報錯
    float t2 = n1 + 1.1F;  //正確
    
  • 當把精度大的資料型別賦給精度小的資料型別時,就會報錯,反之就會進行自動型別轉換

  • (byte , short ) 和 char 之間不會相互自動型別轉換

    byte b1 = 10;   //當把數賦給byte時,先判斷該數是否在byte范圍內[-128,127],在就沒問題
    int n2 = 1;
    byte b2 = n2;   //錯誤,只有賦的是具體的數時,才會先判斷該數是否在byte范圍內
    char c1 = b1;   //char與byte之間不能相互自動型別轉換
    short s1 = b1;  //short與byte之間同樣也不能相互自動型別轉換
    
  • byte , short , char 他們三者之間可以計算,在計算時首先轉換為int型別

    byte b1 = 1;
    byte b2 = 2;
    short s1 = b1 + b2;  //錯誤,計算后的結果為int
    char c1 = b1 + b2;   //錯誤,原因同上
    int s2 = b1 + b2;    //correct
    
  • Boolean型別不參與自動型別轉換

  • 自動提升原則,運算式結果的型別自動提升為運算元中精度最高的型別

強制型別轉換

當進行資料精度大小 由大到小的轉換時,就需要用到強制型別轉換

int n1 = (int)1.9;
System.out.println("n1="+n1);   //n1=1
int n2 = 2000;
byte b1 = (byte)n2;
System.out.println("b1="+b1);    //b1=-48        發生溢位

強制型別強轉只針對最近的運算元有效,往往會用小括號提高優先級

int x = (int)10*3.5+2.4;      //錯誤,不能將double轉換為int
int x = (int)(10*3.5+2.4);    //正確 37.4->37

char型別可以保存int的常量值,但不能保存int的變數值,需要強轉

byte ,short 和 char 型別在進行運算時,當做int型別處理

基本型別與字串的轉換??

//基本資料型別轉字串
int n1 = 100;
String s1 = n1 + "";  //+"" 即可將任意基本資料型別轉為字串
//字串轉基本型別  使用基本型別對應的包裝類的相應方法,得到基本資料型別
String s2 = "123";
int n2 = Integer.parseInt(s2);  //使用基本資料型別對應的包裝類,將String轉為int,利用Integer.parseInt
boolean b1 = Boolean.parseBoolean("true"); //將String轉為Boolean型別
//注意,如果s2="hello",將其轉換為int型別,編譯時不會出錯,但是執行時會拋出例外Exception導致程式終止

//字串加一個整形資料
String s1 = "abc";
System.out.println(s1 + 1);   //  結果為abc1

字串的內容比較使用字串的equal方法

==判斷是兩個字串是否在同一塊記憶體空間,而equal判斷的是內容是否相等

String name = scan.next();                              
System.out.println("SVicen",equals(name));   //比較傳入的字串name是否等于SVicen

string[] names = {"金毛獅王",“白眉鷹王","紫衫龍王“};
    for (int i = 0; i <names.length; i++) {
    if(name.equals(names[i])) {
          System.out.println("找到了");
    }
}

參考型別

  • 在Java中,參考型別的變數非常類似于C/C++的指標,參考型別指向一個物件,指向物件的變數是參考變數,這些變數在宣告時被指定為一個特定的型別,比如 Employee、Puppy 等,變數一旦宣告后,型別就不能被改變了,
  • 物件、陣列都是參考資料型別,
  • 所有參考型別的默認值都是null,
  • 一個參考變數可以用來參考任何與之兼容的型別,
  • 例子:Site site = new Site("Nowcoder"),

常量

常量在程式運行時是不能被修改的,

在 Java 中使用 final 關鍵字來修飾常量,宣告方式和變數類似:

final double PI = 3.1415927;

雖然常量名也可以用小寫,但為了便于識別,通常使用大寫字母表示常量,

字面量可以賦給任何內置型別的變數,例如:

byte a = 68;
char a = 'A'

byte、int、long、和short都可以用十進制、16進制以及8進制的方式來表示,

當使用字面量的時候,前綴 0 表示 8 進制,而前綴 0x 代表 16 進制, 例如:

int decimal = 100;
int octal = 0144;
int hexa =  0x64;

和其他語言一樣,Java的字串常量也是包含在兩個引號之間的字符序列,下面是字串型字面量的例子:

"Hello World"
"two\nlines"
"\"This is in quotes\""    // \"將"轉義為",若不加",會將字串內容認為前兩個雙引號的內容,后面的內容報錯

字串常量和字符變數都可以包含任何 Unicode 字符,例如:

char a = '\u0001';
String a = "\u0001";

轉義字符

符號 字符含義
\n 換行 (0x0a)
\r 回車 (0x0d)
\f 換頁符(0x0c)
\b 退格 (0x08)
\0 空字符 (0x20)
\s 字串
\t 制表符
" 雙引號
' 單引號
\ 反斜杠
\ddd 八進制字符 (ddd)
\uxxxx 16進制Unicode字符 (xx

注:\r 回車符的意思并不是換行,而是將游標移動到當前行的開始位置 詳見下列

System.out.println("helloworld\r北京");
//輸出結果為北京oworld    因為一個漢字占兩個位元組

變數型別

Java語言支持的變數型別有:

  • 類變數:獨立于方法之外的變數,用 static 修飾,
  • 實體變數:獨立于方法之外的變數,不過沒有 static 修飾,
  • 區域變數:類的方法中的變數,
public class Variable{
    static int allClicks=0;    // 類變數
    String str="hello world";  // 實體變數
    public void method(){
        int i =0;  // 區域變數
    }
}

接收用戶輸入

import java.util.Scanner; 
//1.引入Scanner類(簡單文本掃描器)所在的包
//2.創建Scanner物件
Scanner myScanner = new Scanner(System.in);
System.out.println("請輸入名字");
String name = myScanner.next();  //接收用戶輸入
System.out.println("請輸入年齡");
int age = myScanner.nextInt();
System.out.println("名字:" + name + "\n年齡:" + age);
myScanner.close();   //使用完后應該close掉
import java.util.Scanner; 
public class ScannerDemo {
    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        // 從鍵盤接收資料
        // next方式接收字串
        System.out.println("next方式接收:");
        // 判斷是否還有輸入
        if (scan.hasNext()) {
            String str1 = scan.next();  
            System.out.println("輸入的資料為:" + str1);
        }
        scan.close();
    }
}

讀入char型資料char ch = scan.next().charAt(0); --回傳指定索引處的 char 值,這里的索引就是0

char ch = scan.next().charAt(0); --如果輸入的是"你好",輸出結果為''你''

next() 與 nextLine() 區別

next():

  • 1、一定要讀取到有效字符后才可以結束輸入,
  • 2、對輸入有效字符之前遇到的空白,next() 方法會自動將其去掉,
  • 3、只有輸入有效字符后才將其后面輸入的空白作為分隔符或者結束符,
  • next() 不能得到帶有空格的字串,

nextLine():

  • 1、以Enter為結束符,也就是說 nextLine()方法回傳的是輸入回車之前的所有字符,
  • 2、可以獲得空白,

如果要輸入 int 或 float 型別的資料,在 Scanner 類中也有支持,但是在輸入之前最好先使用 hasNextXxx() 方法進行驗證,再使用 nextXxx() 來讀取:

流程控制

分支控制

if...else if...else 陳述句

if 陳述句后面可以跟 else if…else 陳述句,這種陳述句可以檢測到多種可能的情況,

使用 if,else if,else 陳述句的時候,需要注意下面幾點:

  • if 陳述句至多有 1 個 else 陳述句,else 陳述句在所有的 else if 陳述句之后,
  • if 陳述句可以有若干個 else if 陳述句,它們必須在 else 陳述句之前,
  • 一旦其中一個 else if 陳述句檢測為 true,其他的 else if 以及 else 陳述句都將跳過執行,
boolean b = true;
if (b = false) {         //注意這里將b賦值為false  此時第一個if的判斷陳述句為false,不會執行
    System.out.println("a");
} else if(b) {
    System.out.println("b");
} else if(!b) {            //這里判斷為true,會執行,最終輸出結果為 c
    System.out.println("c");
} else {
    System.out.println("d");
}

switch case 陳述句

switch(expression){
    case value :
       //陳述句
       break; //可選,一般加上
    case value :
       //陳述句
       break; 
    default : //可選
       //陳述句
}

switch case 陳述句有如下規則:

  • switch 陳述句中的變數型別可以是: byte、short、int 、enum或者 char,(注意不可以使float或double),從 Java SE 7 開始,switch 支持字串 String 型別了,同時 case 標簽必須為字串常量或字面量,
  • switch 陳述句可以擁有多個 case 陳述句,每個 case 后面跟一個要比較的值和冒號,
  • case 陳述句中的值的資料型別必須與變數的資料型別相同,而且只能是常量或者字面常量,
  • 當變數的值與 case 陳述句的值相等時,那么 case 陳述句之后的陳述句開始執行,直到 break 陳述句出現才會跳出 switch 陳述句,
  • 當遇到 break 陳述句時,switch 陳述句終止,程式跳轉到 switch 陳述句后面的陳述句執行,case 陳述句不必須要包含 break 陳述句,如果沒有 break 陳述句出現,程式會繼續執行下一條 case 陳述句,直到出現 break 陳述句,
  • switch 陳述句可以包含一個 default 分支,該分支一般是 switch 陳述句的最后一個分支(可以在任何位置,但建議在最后一個),default 在沒有 case 陳述句的值和變數值相等的時候執行,default 分支不需要 break 陳述句,

switch case 執行時,一定會先進行匹配,匹配成功回傳當前 case 的值,再根據是否有 break,判斷是否繼續輸出,或是跳出判斷,

char c = 'a';
switch(c){
    case 'a' :
       System.out.println("ok1");
       break;
    case 65 :    //這樣是可以的,字符型實質上就是int
       System.out.println("ok2");
       break; 
    default :
       System.out.println("ok3");
}

回圈結構

for回圈

關于 for 回圈有以下幾點說明:

  • 最先執行初始化步驟,可以宣告一種型別,但可初始化一個或多個回圈控制變數,也可以是空陳述句,
  • 然后,檢測布爾運算式的值,如果為 true,回圈體被執行,如果為false,回圈終止,開始執行回圈體后面的陳述句,
  • 執行一次回圈后,更新回圈控制變數,
  • 再次檢測布爾運算式,回圈執行上面的程序,

增強 for 回圈--Java5中引入

for(宣告陳述句 : 運算式) {
   //代碼句子
}

宣告陳述句:宣告新的區域變數,該變數的型別必須和陣列元素的型別匹配,其作用域限定在回圈陳述句塊,其值與此時陣列元素的值相等,

運算式:運算式是要訪問的陣列名,或者是回傳值為陣列的方法,

public class Test {
   public static void main(String args[]){
      int [] numbers = {10, 20, 30, 40, 50};

      for(int x : numbers ){
         System.out.print( x );
         System.out.print(",");
      }
      System.out.print("\n");
      String [] names ={"James", "Larry", "Tom", "Lacy"};
      for( String name : names ) {
         System.out.print( name );
         System.out.print(",");
      }
   }
}
//運行結果為
//10,20,30,40,50,
//James,Larry,Tom,Lacy,

break 關鍵字

break 主要用在回圈陳述句或者 switch 陳述句中,用來跳出整個陳述句塊,

break 跳出最里層的回圈,并且繼續執行該回圈下面的陳述句,

可以配合label使用, break label1; //跳出label層回圈

陣列

  • 陣列中的元素可以使任意資料型別,包括基本型別和參考型別,但是不能混用

  • 陣列創建后,如果沒有賦值,有默認值int,short,byte,long->0, float,double->0.0, char->\u0000, boolean->false,String->null

  • 陣列屬于參考型別,陣列型資料是物件

  • 陣列的賦值為參考傳遞,傳遞的是一個地址

    int[] arr1 = {1,2,3,4};
    int[] arr2 = arr1;
    arr2[0] = 5;  //修改arr2的值,arr1的值也會發生改變
    

宣告陣列變數

dataType[] arrayRefVar;   // 首選的方法
dataType arrayRefVar[];  // 效果相同,但不是首選方法

創建陣列

Java語言使用new運算子來創建陣列,語法如下:

arrayRefVar = new dataType[arraySize];

上面的語法陳述句做了兩件事:

  1. 使用 dataType[arraySize] 創建了一個陣列,
  2. 把新創建的陣列的參考賦值給變數 arrayRefVar,

陣列變數的宣告,和創建陣列可以用一條陳述句完成,如下所示:

dataType[] arrayRefVar = new dataType[arraySize];

還可以使用如下的方式創建陣列(靜態初始化):

dataType[] arrayRefVar = {value0, value1, ..., valuek};

陣列擴容

int[] arr1 = {1,2,3};
//要在arr1后加一個元素,注意不可以直接arr[3] = 4;  --發生陣列下標越界
int[] newArr = new int[arr1.length + 1];
for (int i = 0; i < arr1.length; i++){
    newArr[i] = arr1[i];
}
newArr[newArr.length - 1] = 4;
arr1 = newArr;   //將擴容后的陣列賦給原來的陣列

For-Each 回圈

又叫加強型回圈,能在不使用下標的情況下遍歷陣列,

public class TestArray {
   public static void main(String[] args) {
      double[] myList = {1.9, 2.9, 3.4, 3.5};
      // 列印所有陣列元素
      for (double element: myList) {
         System.out.println(element);
      }
   }
}

多維陣列

多維陣列可以看成是陣列的陣列,比如二維陣列就是一個特殊的一維陣列,其每一個元素都是一個一維陣列,例如:

String str[][] = new String[3][4];
int[][] y 或者 int y[][] 或者 int []y[];
int[][] a= {{1, 2, 3}, {4, 5, 6},{7, 8, 9}};
for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 3; j++) {
        System.out.print(a[i][j] + " ");
    }
    System.out.println("");
}
System.out.println(a[0][2]);   //注意java語言 這里不可以輸出 a[0][3] 而c++可以,輸出的為第二行第一列的4
//由此可見c++的多維陣列記憶體存盤空間是連續的,而java的多維陣列記憶體空間是不連續的

Java的多維陣列的每個陣列的資料個數可以不同,因為每個一維的陣列可以單獨new一定的空間

多維陣列的動態初始化(以二維陣列為例)

1.直接為每一維分配空間,格式如下:

type[][] typeName = new type[typeLength1][typeLength2];
例如
int a[][] = new int[2][3];
int[][] arr = {{1},{1,2},{1,2,3}};

2.從最高維開始,分別為每一維分配空間,例如:

String[] strs = new String[] { "a", "b", "c" };  //是正確的  strs其實是一維的,只不過直接靜態賦值了
//注意這里String[] strs = new String[3] { "a", "b", "c" }  就是錯誤的
String s[][] = new String[2][];
s[0] = new String[2];   //每一個一維的陣列都需要 使用new運算子,否則其為空-無法存放資料
s[1] = new String[3];
//注意這里 s[0]中兩個元素,而s[1]中有三個元素
s[0][0] = new String("Good");
s[0][1] = new String("Luck");
s[1][0] = new String("to");
s[1][1] = new String("you");
s[1][2] = new String("!");

s[0]=new String[2]s[1]=new String[3] 是為最高維分配參考空間,也就是為最高維限制其能保存資料的最長的長度,然后再為其每個陣列元素單獨分配空間 s0=new String("Good") 等操作,

Arrays 類

ava.util.Arrays 類能方便地操作陣列,它提供的所有方法都是靜態的,

具有以下功能:

  • 給陣列賦值:通過 fill 方法,
  • 對陣列排序:通過 sort 方法,按升序,
  • 比較陣列:通過 equals 方法比較陣列中元素值是否相等,
  • 查找陣列元素:通過 binarySearch 方法能對排序好的陣列進行二分查找法操作,
序號 方法和說明
1 public static int binarySearch(Object[] a, Object key) 用二分查找演算法在給定陣列中搜索給定值的物件(Byte,Int,double等),陣列在呼叫前必須排序好的,如果查找值包含在陣列中,則回傳搜索鍵的索引;否則回傳 (-(插入點) - 1),
2 public static boolean equals(long[] a, long[] a2) 如果兩個指定的 long 型陣列彼此相等,則回傳 true,如果兩個陣列包含相同數量的元素,并且兩個陣列中的所有相應元素對都是相等的,則認為這兩個陣列是相等的,換句話說,如果兩個陣列以相同順序包含相同的元素,則兩個陣列是相等的,同樣的方法適用于所有的其他基本資料型別(Byte,short,Int等),
3 public static void fill(int[] a, int val) 將指定的 int 值分配給指定 int 型陣列指定范圍中的每個元素,同樣的方法適用于所有的其他基本資料型別(Byte,short,Int等),
4 public static void sort(Object[] a) 對指定物件陣列根據其元素的自然順序進行升序排列,同樣的方法適用于所有的其他基本資料型別(Byte,short,Int等),

JAVA記憶體結構

  • 堆疊:一般存放基本資料型別(區域變數)和類的方法
  • 堆:存放參考型資料,物件(物件的各個屬性的地址)、陣列(通過new創建的資料型別)
  • 方法區:常量池(存放常量比如字串),類加載資訊(具體物件的各個屬性的值)

類與物件

一個類可以包含以下型別變數:

  • 區域變數:在方法、構造方法或者陳述句塊中定義的變數被稱為區域變數,變數宣告和初始化都是在方法中,方法結束后,變數就會自動銷毀,
  • 成員變數:又叫全域變數,全域變數又分為類變數(靜態變數)、實體變數兩種,成員變數是定義在類中,方法體之外的變數,這種變數在創建物件的時候實體化,成員變數可以被類中方法、構造方法和特定類的陳述句塊訪問,
  • 類變數:也稱為靜態變數,類變數也宣告在類中,方法體之外,但必須宣告為 static 型別

區域變數

  • 區域變數宣告在方法、構造方法或者陳述句塊中;
  • 區域變數在方法、構造方法、或者陳述句塊被執行的時候創建,當它們執行完成后,變數將會被銷毀;
  • 訪問修飾符不能用于區域變數
  • 區域變數只在宣告它的方法、構造方法或者陳述句塊中可見;
  • 區域變數是在堆疊上分配的
  • 區域變數沒有默認值,所以區域變數被宣告后,必須經過初始化,才可以使用,

實體變數??

  • 實體變數宣告在一個類中,但在方法、構造方法和陳述句塊之外,沒有 static 修飾;
  • 當一個物件被實體化之后,每個實體變數的值就跟著確定;
  • 實體變數在物件創建的時候創建,在物件被銷毀的時候銷毀;
  • 實體變數的值應該至少被一個方法、構造方法或者陳述句塊參考,使得外部能夠通過這些方式獲取實體變數資訊;
  • 實體變數可以宣告在使用前或者使用后;
  • 訪問修飾符可以修飾實體變數;
  • 實體變數對于類中的方法、構造方法或者陳述句塊是可見的,一般情況下應該把實體變數設為私有,通過使用訪問修飾符可以使實體變數對子類可見;
  • 實體變數具有默認值,數值型變數的默認值是0,布爾型變數的默認值是false,參考型別變數的默認值是null,變數的值可以在宣告時指定,也可以在構造方法中指定;
  • 實體變數可以直接通過變數名訪問,但在靜態方法以及其他類中,就應該使用完全限定名:ObejectReference.VariableName

類變數(靜態變數)

  • 類變數也稱為靜態變數,在類中以 static 關鍵字宣告,但必須在方法之外,
  • 無論一個類創建了多少個物件,類只擁有類變數的一份拷貝,
  • 靜態變數除了被宣告為常量外很少使用,常量是指宣告為public/private,final和static型別的變數,常量初始化后不可改變,
  • 靜態變數儲存在靜態存盤區,經常被宣告為常量,很少單獨使用static宣告變數,
  • 隨著類的加載而存在,隨著類的消失而消失,靜態變數在第一次被訪問時創建,在程式結束時銷毀,
  • 與實體變數具有相似的可見性,但為了對類的使用者可見,大多數靜態變數宣告為public型別,
  • 默認值和實體變數相似,數值型變數默認值是0,布爾型默認值是false,參考型別默認值是null,變數的值可以在宣告的時候指定,也可以在構造方法中指定,此外,靜態變數還可以在靜態陳述句塊中初始化,
  • 靜態變數可以通過:ClassName.VariableName的方式訪問,
  • 類變數被宣告為public static final型別時,類變數名稱一般建議使用大寫字母,如果靜態變數不是public和final型別,其命名方式與實體變數以及區域變數的命名方式一致,

??屬性和區域變數可以重名,訪問時遵循就近原則

??屬性隨著物件的創建而創建,隨著物件的銷毀而銷毀,其實非靜態屬性就是實體變數

??全域變數/屬性:可以被本類使用,也可以被其他類使用,創建物件的時候實體化

??全域變數/屬性可以加修飾符,但區域變數不可以加修飾符

構造方法/構造器

構造器是對物件進行初始化的而不是創建物件的

每個類都有構造方法,如果沒有顯式地為類定義構造方法,Java 編譯器將會為該類提供一個默認構造方法進行物件的初始化,

在創建一個物件的時候,至少要呼叫一個構造方法,構造方法的名稱必須與類同名,且沒有回傳值,也不寫void,

一旦定義了自己的構造器,系統默認的構造器就被覆寫了,不能使用,除非顯示的再定義一下默認無參構造器

下面是一個構造方法示例:

public class Puppy{
    public Puppy(){
    }
    public Puppy(String name){
        // 這個構造器僅有一個引數:name
    }
}

反匯編指令javap 類名(.class后綴) 可以加 -c -v 選項

image-20220412213329202

this

-- 與物件關聯,實際上每個物件在堆區分配空間的時候就隱式分配了一個this指向它自己

使用this輸出的值一定是屬性值,但是如果不加this,如果有區域變數與屬性同名,則會就近輸出區域變數

public static void main(String[] args) {
    T t1 = new T();
}
class T {
    public T() {
        //對this的呼叫必須在構造器的第一條陳述句
        //訪問構造器語法:this(引數串列) 注意只能在一個構造器中訪問另一個構造器  且this陳述句必須放在第一條
        this("SVicen", 20);   //在一個構造器內呼叫其他的構造器
        System.out.println("T()構造器呼叫");
    }

    public T(String name, int age) {
        System.out.println("T(String name, int age)構造器呼叫");
    }
}
//輸出結果如下
T(String name, int age)構造器呼叫
T()構造器呼叫

IDEA自定義模板 settings-> editor -> live templates

創建物件

物件是根據類創建的,在Java中,使用關鍵字 new 來創建一個新的物件,創建物件需要以下三步:

  • 宣告:宣告一個物件,包括物件名稱和物件型別,
  • 實體化:使用關鍵字 new 來創建一個物件,
  • 初始化:使用 new 創建物件時,會呼叫構造方法初始化物件,
public class Puppy{
   int age = 10;
   String name;
   public Puppy(String n,int a){  //建構式
      name = n;
      age = a;
      //這個構造器僅有一個引數:name
      System.out.println("小狗的名字是 : " + name ); 
   }
   public static void main(String[] args){ 
      // 下面的陳述句將創建一個Puppy物件
      Puppy myPuppy = new Puppy( "tommy",18 );
   }
}
//結果為:小狗的名字是 : tommy

匿名物件

  • 只可以使用一次
public class Circle {
    double radius; //半徑
    public Circle(double r) {
        radius = r;
    }
    public double area() {
        return Math.PI * radius * radius;
    }
    public double len() {
        return 2 * Math.PI * radius;
    }
    public double test() {
        double a = 1.0;
        System.out.println(2 * a);
    }
    public static void main(String[] args) {
        new Circle.test();
    }
}

物件創建流程詳解(以上代碼為例)??

  • Puppy myPuppy = new Puppy( "tommy" )后 先在方法區加載類的屬性和方法資訊 ---只會加載一次
  • 在堆開辟空間(根據物件的屬性個數)
  • 對物件進行默認初始化(Sting型為null,int型為0,boolean為false,char型為空),age初始化為0,name初始化為null
  • 進行類的定義中對屬性的顯式初始化,age = 10;name = null
  • 進行構造器的初始化,將傳入的引數賦值給創建的物件的屬性對應的堆空間
  • 將創建的物件的地址回傳給創建的物件myPuppy (物件的參考)

物件的賦值

public class Object {
    public static void main(String[] args) {
        //創建Person物件
        Person p1 = new Person(20,'男',"SVicen");
        System.out.println("姓名為:" + p1.name + "   性別為:" + p1.gender + "   年齡為:" + p1.age);
        Person p2 = p1; //注意這里傳值,傳的其實是參考,修改p1,p2任意一個的值都會導致兩個都發生變化
        p2.age = 18;             //但是如果令p2=null,只是將p2指向的地址改為了null,并不影響p1的值
        p1.gender = '女';   
        System.out.println("=====第一個人的屬性如下=====");
        System.out.println("姓名為:" + p1.name + "   性別為:" + p1.gender + "   年齡為:" + p1.age);
        System.out.println("=====第二個人的屬性如下=====");
        System.out.println("姓名為:" + p2.name + "   性別為:" + p2.gender + "   年齡為:" + p2.age);
    }
}
class Person {
    int age;
    char gender;
    String name;

    Person(int a, char g, String s) {
        this.age = a;
        this.gender = g;
        this.name = s;
    }
}
//輸出結果如下
姓名為:SVicen   性別為:男   年齡為:20
=====第一個人的屬性如下=====
姓名為:SVicen   性別為:女   年齡為:18
=====第二個人的屬性如下=====
姓名為:SVicen   性別為:女   年齡為:18

源檔案宣告規則

在本節的最后部分,我們將學習源檔案的宣告規則,當在一個源檔案中定義多個類,并且還有import陳述句和package陳述句時,要特別注意這些規則,

  • 一個源檔案中只能有一個public類
  • 一個源檔案可以有多個非public類
  • 源檔案的名稱應該和public類的類名保持一致,例如:源檔案中public類的類名是Employee,那么源檔案應該命名為Employee.java,
  • 如果一個類定義在某個包中,那么package陳述句應該在源檔案的首行,
  • 如果源檔案包含import陳述句,那么應該放在package陳述句和類定義之間,如果沒有package陳述句,那么import陳述句應該在源檔案中最前面,
  • import陳述句和package陳述句對源檔案中定義的所有類都有效,在同一源檔案中,不能給不同的類不同的包宣告,

類有若干種訪問級別,并且類也分不同的型別:抽象類和final類等,這些將在訪問控制章節介紹,

除了上面提到的幾種型別,Java還有一些特殊的類,如:內部類、匿名類,

Java包

包主要用來對類和介面進行分類,當開發Java程式時,可能撰寫成百上千的類,因此很有必要對類和介面進行分類,

IDEA,新建packet,名字為com.xiaoming -- 會創建二級目錄xiaoming,以及目錄com

包的命名一般為:com.公司名.專案名.業務模塊名

包的作用

  • 1、把功能相似或相關的類或介面組織在同一個包中,方便類的查找和使用,
  • 2、區分相同名字的類,同一個包中的類名字是不同的,不同的包中的類的名字是可以相同的,當同時呼叫兩個不同包中相同類名的類時,應該加上包名加以區別,因此,包可以避免名字沖突
  • 3、包也限定了訪問權限,擁有包訪問權限的類才能訪問某個包中的類

Java 使用包(package)這種機制是為了防止命名沖突,訪問控制,提供搜索和定位類(class)、介面、列舉(enumerations)和注釋(annotation)等,

包陳述句的語法格式為:

//例如 它的路徑應該是 net/java/util/Something.java 這樣保存的
package net.java.util;   // 一個類中只能有一個packeg
public class Something{
   ...
}

一個包(package)可以定義為一組相互聯系的型別(類、介面、列舉和注釋),為這些型別提供訪問保護和命名空間管理的功能,

開發者可以自己把一組類和介面等打包,并定義自己的包,而且在實際開發中這樣做是值得提倡的,當你自己完成類的實作之后,將相關的類分組,可以讓其他的編程者更容易地確定哪些類、介面、列舉和注釋等是相關的,

由于包創建了新的命名空間(namespace),所以不會跟其他包中的任何名字產生命名沖突,使用包這種機制,更容易實作訪問控制,并且讓定位相關類更加簡單,參考時只可以引入一個包,否則編譯器無法區分,

以下是一些 Java 中的包:

  • java.lang-----打包基礎的類,默認引入
  • java.io-----包含輸入輸出功能的函式
  • java.util.*-----util包,系統提供的工具包,工具類,使用Scanner
  • java.net.*-----網路包,網路開發
  • java.awt.*----做java的界面開發,GUI
創建包

創建包的時候,你需要為這個包取一個合適的名字,之后,如果其他的一個源檔案包含了這個包提供的類、介面、列舉或者注釋型別的時候,都必須將這個包的宣告放在這個源檔案的開頭,

包宣告應該在源檔案的第一行,每個源檔案只能有一個包宣告,這個檔案中的每個型別都應用于它,

如果一個源檔案中沒有使用包宣告,那么其中的類,函式,列舉,注釋等將被放在一個無名的包(unnamed package)中,

在 animals 包中加入一個介面(interface):

/* 檔案名: Animal.java */
package animals;

interface Animal {
   public void eat();
   public void travel();
}

接下來,在同一個包中加入該介面的實作:

package animals;
/* 檔案名 : MammalInt.java */
public class MammalInt implements Animal{

   public void eat(){
      System.out.println("Mammal eats");
   }

   public void travel(){
      System.out.println("Mammal travels");
   } 

   public int noOfLegs(){
      return 0;
   }

   public static void main(String args[]){
      MammalInt m = new MammalInt();
      m.eat();
      m.travel();
   }
}

可變引數??

注:當有多個引數時,可變引數需在最后一個引數的位置,否則無法確定個數,同時一個函式無法有多個型別的可變引數

class Method{
    public int sum(int... nums) {
        int res = 0;
        for (int i = 0; i < nums.length; i++) {
            res += nums[i];
        }
        return res;
    }
    public void f1(double... nums,double d1) {
        //這樣寫是錯誤的
    }
    //使用可變引數時,可以當做陣列使用,即nums當做陣列
    Method m = new Method();
    System.out.println(m.sum(5,10,100));
    System.out.println(m.sum(5,10,20,50,100));  //可以輸入任意引數個數
}

Java 修飾符

Java語言提供了很多修飾符,主要分為以下兩類:

  • 訪問修飾符
  • 非訪問修飾符

修飾符用來定義類、方法或者變數,通常放在陳述句的最前端,

訪問控制修飾符??

Java中,可以使用訪問控制符來保護對類、變數、方法和構造方法的訪問,Java 支持 4 種不同的訪問權限,

  • default (即默認,什么也不寫): 在同一包內可見,不使用任何修飾符,使用物件:類、介面、變數、方法,
  • private : 在同一類內可見,使用物件:變數、方法, 注意:不能修飾類(外部類)
  • public : 對所有類可見,使用物件:類、介面、變數、方法
  • protected : 對同一包內的類和所有子類可見,使用物件:變數、方法, 注意:不能修飾類(外部類)
修飾符 當前類 同一包內 子孫類(同一包) 子孫類(不同包) 其他包
public Y Y Y Y Y
protected Y Y Y Y/N N
default Y Y Y N N
private Y N N N N

封裝

在面向物件程式設計方法中,封裝(Encapsulation)是指一種將抽象性函式介面的實作細節部份包裝、隱藏起來的方法,
封裝可以被認為是一個保護屏障,防止該類的代碼和資料被外部類定義的代碼隨機訪問,
要訪問該類的代碼和資料,必須通過嚴格的介面控制,
封裝最主要的功能在于我們能修改自己的實作代碼,而不用修改那些呼叫我們代碼的程式片段,
適當的封裝可以讓程式碼更容易理解與維護,也加強了程式碼的安全性,

封裝的優點
  • 良好的封裝能夠減少耦合, 高內聚,低耦合
  • 類內部的結構可以自由修改,
  • 可以對成員變數進行更精確的控制,保證資料安全合理
  • 隱藏實作細節
封裝實作步驟
  • 屬性私有化
  • 提供一個公共的public set方法,用于對屬性判斷并賦值
  • 提供一個公共的public get方法,用于獲取屬性的值

繼承

繼承的概念

繼承是java面向物件編程技術的一塊基石,因為它允許創建分等級層次的類,

繼承就是子類繼承父類的特征和行為,使得子類物件(實體)具有父類的實體域和方法,或子類從父類繼承方法,使得子類具有父類相同的行為,

類的繼承格式

在 Java 中通過 extends 關鍵字可以申明一個類是從另外一個類繼承而來的,一般形式如下:

class 父類 {
}

class 子類 extends 父類 {
}
繼承的構造器呼叫??

當創建子類物件時,不管是用子類的哪個構造器,默認情況下都會去呼叫父類的無參構造器(通過一個super()函式 ),如果父類沒有提供無參構造器(定義了有參構造器而沒有對無參構造器進行顯示的宣告),則必須**在子類的構造器中顯示用super(構造器形參串列) 來指明呼叫父類的有參構造器來完成父類的初始化, **

super()與this()類似,都是只能應用在構造器里,且必須放在構造器的第一行,但可以在普通非靜態方法中通過super.eat()呼叫父類的非靜態方法,

繼承型別

Java 不支持多繼承,但支持多重繼承,

圖片說明

繼承的特性
  • 子類擁有父類非 private 的屬性、方法,
  • 子類可以擁有自己的屬性和方法,即子類可以對父類進行擴展,
  • 子類可以用自己的方式實作父類的方法,
  • Java 的繼承是單繼承,但是可以多重繼承,單繼承就是一個子類只能繼承一個父類,多重繼承就是,例如 A 類繼承 B 類,B 類繼承 C 類,所以按照關系就是 C 類是 B 類的父類,B 類是 A 類的父類,這是 Java 繼承區別于 C++ 繼承的一個特性,
  • 提高了類之間的耦合性(繼承的缺點,耦合度高就會造成代碼之間的聯系越緊密,代碼獨立性越差),
繼承關鍵字

繼承可以使用 extendsimplements 這兩個關鍵字來實作繼承,而且所有的類都是繼承于 java.lang.Object,當一個類沒有繼承的兩個關鍵字,則默認繼承object(這個類在 java.lang 包中,所以不需要 import)祖先類,

extends關鍵字

在 Java 中,類的繼承是單一繼承,也就是說,一個子類只能擁有一個父類,所以 extends 只能繼承一個類,

public class Animal { 
    private String name;   
    private int id; 
    public Animal(String myName, String myid) { 
        //初始化屬性值
    } 
    public void eat() {  //吃東西方法的具體實作  } 
    public void sleep() { //睡覺方法的具體實作  } 
} 

public class Penguin  extends  Animal{ 
}
implements關鍵字

使用 implements 關鍵字可以變相的使java具有多繼承的特性,使用范圍為類繼承介面的情況,可以同時繼承多個介面(介面跟介面之間采用逗號分隔),

public interface A {
    public void eat();
    public void sleep();
}

public interface B {
    public void show();
}

public class C implements A,B {
}
super關鍵字??

super關鍵字:我們可以通過super關鍵字來實作對父類成員的訪問,用來參考當前物件的父類,

super的訪問不限于直接父類,如果爺爺類和本類中有同名的成員,也可以使用super取訪問爺爺類的成員(前提是直接父類中沒有同名成員),如果多個上級類中都有同名的成員,使用super訪問遵循就近原則,

class Animal {
  void eat() {
    System.out.println("animal : eat");
  }
}

class Dog extends Animal {
  void eat() {
    System.out.println("dog : eat");
  }
  void eatTest() {
    this.eat();   // this 呼叫自己的方法
    super.eat();  // super 呼叫父類方法
  }
}
final關鍵字

final 關鍵字宣告類可以把類定義為不能繼承的,即最終類;或者用于修飾方法,該方法不能被子類重寫:

繼承的記憶體布局??

創建son物件時,father和grandpa物件都會進行構造且放在一塊記憶體,對于father或grandpa的private屬性son是不可以訪問的

son s = new son();
//通過son訪問名字時,如果子類有該屬性,則訪問子類的,否則訪問父類的,知道Object類(最終父類)
s.name;     //大頭兒子
s.age;        //爸爸的年齡 39
s.hobby;    //爺爺的愛好 旅游

image-20220413184449013

public class ExtendExercise {
    public static void main(String[] args) {
        B b = new B();  //創建子類物件
    }
}
class A{
    A(){
        System.out.println("a");
    }
    A(String name) {
        System.out.println("a name");
    }
}
class B extends A{
    B(){
        //呼叫自己的有參構造R
        this("abc");
        System.out.println("b");
    }
    B(String name) {
        //默認有一個 super
        System.out.println("b name");
    }
}
//輸出結果為
a
b name
b

重寫(Override)

重寫是子類對父類的允許訪問的方法的實作程序進行重新撰寫, 回傳值和形參都不能改變,即外殼不變,核心重寫!

重寫的好處在于子類可以根據需要,定義特定于自己的行為, 也就是說子類能夠根據需要實作父類的方法,

重寫方法不能拋出新的檢查例外或者比被重寫方法申明更加寬泛的例外,例如: 父類的一個方法申明了一個檢查例外 IOException,但是在重寫這個方法的時候不能拋出 Exception 例外,因為 Exception 是 IOException 的父類,只能拋出 IOException 的子類例外,

class Animal{
   public void move(){
      System.out.println("動物可以移動");
   }
}

class Dog extends Animal{
   public void move(){
      System.out.println("狗可以跑和走");
   }
}

public class TestDog{
   public static void main(String args[]){
      Animal a = new Animal(); // Animal 物件
      Animal b = new Dog(); // Dog 物件
      a.move();// 執行 Animal 類的方法        動物可以移動
      b.move();//執行 Dog 類的方法            狗可以跑和走
   }
}

在上面的例子中可以看到,盡管b屬于Animal型別,但是它運行的是Dog類的move方法,

這是由于在編譯階段,只是檢查引數的參考型別(最前面宣告的型別),

然而在運行時,Java虛擬機(JVM)指定物件的型別并且運行該物件的方法,

因此在上面的例子中,之所以能編譯成功,是因為Animal類中存在move方法,然而運行時,運行的是特定物件的方法,

class Animal{
   public void move(){
      System.out.println("動物可以移動");
   }
}

class Dog extends Animal{
   public void move(){
      System.out.println("狗可以跑和走");
   }
   public void bark(){
      System.out.println("狗可以吠叫");
   }
}

public class TestDog{
   public static void main(String args[]){
      Animal a = new Animal(); // Animal 物件
      Animal b = new Dog(); // Dog 物件

      a.move();// 執行 Animal 類的方法
      b.move();//執行 Dog 類的方法
      b.bark();
   }
}

這里,該程式將拋出一個編譯錯誤,因為b的參考型別Animal(最左邊的宣告型別) 沒有bark方法,編譯不成功,

方法的重寫規則??
  • 引數串列必須完全與被重寫方法的相同,
  • 回傳型別與被重寫方法的回傳型別可以不相同,但是必須是父類回傳值的派生類(java5 及更早版本回傳型別要一樣,java7 及更高版本可以不同), 比如說子類回傳String(參考型),父類回傳Object
  • 訪問權限不能比父類中被重寫的方法的訪問權限更低,例如:如果父類的一個方法被宣告為 public,那么在子類中重寫該方法就不能宣告為 protected, 默認的權限比protected低,比private高,
  • 父類的成員方法只能被它的子類重寫,
  • 宣告為 final 的方法不能被重寫,
  • 宣告為 static 的方法不能被重寫,但是能夠被再次宣告,
  • 子類和父類在同一個包中,那么子類可以重寫父類所有方法,除了宣告為 private 和 final 的方法,
  • 子類和父類不在同一個包中,那么子類只能夠重寫父類的宣告為 public 和 protected 的非 final 方法,
  • 重寫的方法能夠拋出任何非強制例外,無論被重寫的方法是否拋出例外,但是,重寫的方法不能拋出新的強制性例外,或者比被重寫方法宣告的更廣泛的強制性例外,反之則可以,
  • 構造方法不能被重寫
  • 如果不能繼承一個方法,則不能重寫這個方法,
重寫與多載之間的區別
區別點 多載方法 重寫方法
引數串列 必須修改 一定不能修改
回傳型別 可以修改 一定不能修改
發生范圍 本類 父子類
例外 可以修改 可以減少或洗掉,一定不能拋出新的或者更廣的例外
訪問修飾符 可以修改 一定不能做更嚴格的限制(可以降低限制)

多型??

多型:方法或物件具有多種形態,是OOP的第三大特征,建立在封裝和繼承基礎之上,

多型的優點
  • 消除型別之間的耦合關系
  • 可替換性
  • 可擴充性
  • 介面性
  • 靈活性
  • 簡化性
  • 可以使程式有良好的擴展,并可以對所有類的物件進行通用處理,
多型存在的三個必要條件
  • 繼承
  • 重寫
  • 父類參考指向子類物件
物件的多型
  • 一個物件的編譯型別和運行型別可以不一致
  • 編譯型別在定義多型時就確定了,不能改變,而運行型別是可以改變的(類似于c++的父類指標指向子類物件)
  • 編譯型別看定義時 =號 的左邊,運行型別看 =號 的右邊,可以通過Object的getClass方法查看運行型別
多型的向上轉型??

父類的參考指向了子類物件(子類的物件向上轉型)

??當使用多型方式呼叫方法時,首先檢查父類中是否有該方法,如果沒有,則編譯錯誤;如果有,再去呼叫子類的同名方法,

寫代碼時看的是編譯型別,可以呼叫編譯型別的所有可訪問的成員,不能呼叫子類特有的成員

??寫代碼時能呼叫的方法和屬性都是編譯型別的,但運行時呼叫的方法時從子類開始找的

呼叫方法時,從子類(運行型別)向上查找方法呼叫,(有可能父類定義的函式子類未定義 --但要注意這就不是多型了)

??呼叫方法時,體現多型--其實就是子類對父類的方法做了具體的實作,所以應該從子類開始找方法,

修改后的父類可以呼叫父類的所有成員(訪問權限滿足),但是不能呼叫子類特有的成員(父類中必須有對應的介面)??

Animal ani = new Animal();//             編譯型別為Animal  運行型別為Animal
ani = new Dog();    //父類參考指向Dog子類    編譯型別為Animal  運行型別為 Dog
ani = new Cat();    //父類參考指向Cat子類   編譯型別為Animal  運行型別為 Cat
ani.eat();    //注意這里先看子類的方法,運行時關注運行型別,輸出的是貓吃魚,如果子類沒有eat方法再去呼叫父類的
public class Test {
    public static void main(String[] args) {
      show(new Cat());  // 以 Cat 物件呼叫 show 方法
      show(new Dog());  // 以 Dog 物件呼叫 show 方法

      Animal a = new Cat();  // 向上轉型  
      a.eat();               // 呼叫的是 Cat 的 eat
      Cat c = (Cat)a;        // 向下轉型  
      c.work();        // 呼叫的是 Cat 的 work
  }  

    public static void show(Animal a)  {
      a.eat();  
        // 型別判斷
        if (a instanceof Cat)  {  // 貓做的事情  instanceof用于判斷物件的運行型別是否為Cat型別或Cat型別的子型別
            Cat c = (Cat)a;  
            c.work();  
        } else if (a instanceof Dog) { //狗做的事情 instanceof用于判斷物件的運行型別是否為Dog型別或Dog型別的子型別
            Dog c = (Dog)a;  
            c.work();  
        }  
    }  
}

abstract class Animal {  
    abstract void eat();    //abstract  抽象類
}  

class Cat extends Animal {  
    public void eat() {  
        System.out.println("吃魚");  
    }  
    public void work() {  
        System.out.println("抓老鼠");  
    }  
}  

class Dog extends Animal {  
    public void eat() {  
        System.out.println("吃骨頭");  
    }  
    public void work() {  
        System.out.println("看家");  
    }  
}
//輸出結果
吃魚
抓老鼠
吃骨頭
看家
吃魚
抓老鼠
多型的向下轉型

其實就是把執行子類物件的父類參考,轉為執行子類物件的子類參考

  • 語法:子型別別 參考名 = (子型別別) 父類參考 Cat c = (Cat) animal
  • 只能強轉父類的參考,不能強轉父類物件 ??
  • 要求父類的參考必須指向當前要轉型后的型別的物件,即上面的animal必須指向cat子類物件 ??
  • 當向下轉型后,就可以呼叫子型別中的所有成員

屬性沒有重寫之說, --看編譯型別

public class Test {
    public static void main(String[] args) {
        Base base = new Sub(); //向上轉型
        System.out.println(base.count);  //看編譯型別,為Base,結果為10
        Sub sub = new Sub();
        System.out.println(sub.count);    //編譯型別為Sub,  結果為20
    }
}
class Base{
    int count = 10;
}
class Sub{
    int count = 20;
}

instanceOf 比較運算子,用于判斷物件的型別是否為XX型別或XX型別的子型別,

動態系結機制DynamicBinding
  • 當呼叫物件方法時,該方法會和該物件的記憶體地址(運行型別)系結
  • 當呼叫物件屬性時,沒有動態系結機制,哪里宣告,哪里使用

??詳情見com.poly.dynamic 例子以及多型陣列的實作

多型陣列內會用到 instanceof 運算子

多型引數

方法定義的形參型別為父型別別,實參型別允許為子型別別 詳見com\poly\polyexercise\TestPolyParameter.java

Object類(頂級父類)詳解??

是類層次結構的根類,在java.lang.Object包里,每個類都是要它作為超類,所有物件(包括陣列)都實作這個類的方法

  • Object類的equals()方法:指示其他某個物件是否與此物件“相等,只能比較參考型別,

    public boolean equals(Object obj) {
            return (this == obj);
    }
    //可見默認的Object的equals方法直接 用==判斷,判斷的是地址是否相等,子類往往需要重寫
    

    可以判斷基本資料型別,判斷的是值是否相等,也可以判斷參考資料型別,判斷的是地址是否相等,即是否是一個物件

    子類沒有重寫 Object 類中的 equals 方法,equals方法和==號比較參考資料型別無區別,重寫后的equals方法比較的是物件中的屬性,

    String類的equals方法,重寫了Object的equals方法,判斷的是兩個字串的內容是否相等,每個字符逐一比較

    public boolean equals(Object anObject) {
            if (this == anObject) {
                return true;
            }
            if (anObject instanceof String) {
                String aString = (String)anObject;
                if (coder() == aString.coder()) {
                    return isLatin1() ? StringLatin1.equals(value, aString.value)   //拉丁語和UTF16分開處理
                                      : StringUTF16.equals(value, aString.value);
                }
            }
            return false;
    }
    
  • Object類的hashCode()方法:回傳該物件的哈希碼值,默認情況下,該方法會根據物件的地址來計算,(java是在虛擬機jvm跑的,所以一般沒有運行地址一說,這里的地址只是參考概念)

    不同物件的 hashCode() 的值一般是不相同,但是,同一個物件的hashCode() 值肯定相同,兩個參考指向一個物件值也相同,

  • Object類的toString()方法:回傳該物件的字串表示,包名.類名@hashCode 一般會重寫為輸出內容,可以使用快捷鍵創建重寫,

    public String toString() {
            return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }
    //getClass().getName() 類的包名+類名 
    //toHexString(hashCode())  將物件的hashcode值轉換成16進制字串
    
    //重寫toString方法
        @Override
        public String toString() {
            return "Person{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    ", gender=" + gender +
                    '}';
        }
    

    當輸出一個物件時 System.out.println(person);會默認呼叫 person.toString 方法

  • Object類的finalize()方法,finalize 是一個 protected 的方法,雖然不是 public 的,但是這個方法也同樣能夠被所有子類繼承

    當物件被回收時,系統會自動呼叫該物件的finalize方法,子類可以重寫該方法,做一些釋放資源的操作

    什么時候被回收:當某個物件沒有任何參考時,則jvm就會認為這個物件是個垃圾,垃圾回收器就會回收(銷毀)物件,在銷毀物件前,會呼叫該物件的finalize方法,程式員可以再該方法中寫業務邏輯代碼,比如釋放資源,資料庫連接,檔案資源釋放等,

    其基本原理是:如果垃圾回收器準備對某物件占用的記憶體資源進行回收,會先將該物件放入回收佇列,處于回收佇列中的物件會執行其finalize()方法,做一些清除前的作業,例如資源釋放等;直至下一次垃圾回收動作發生時才會真正回收物件占用的記憶體空間
    但這并不意味著我們在編程時把應該finalize()方法作為類似于“解構式”使用,因為該方法只會在垃圾回收時才會執行,而物件可能是不被垃圾回收的,

    jdk9之后以棄用該方法,因為finalize()方法不能保證執行,實際開發中也不會使用,轉而使用System.gc()顯式呼叫垃圾收集器,呼叫這個方法,就相當于通知 JVM,程式員希望能夠進行垃圾回收,

斷點除錯

F7 --跳入方法內 F8 --逐行執行代碼 F9 --resume執行到下一個斷點 shift + F8 --跳出方法

設定填入源代碼 settings->Build,Execution->Debugger->Data Views->Stepping->將java.*和javax.*前的對號取消掉

類與物件進階

類變數(靜態變數)

Static修飾的成員變數,被同一個類的所有物件共享,在類加載的時候就生成了

  • 記憶體分布:JDK8之前,類變數主要存盤在方法區中,JDK8以后,在加載類資訊的時候通過反饋機制會在堆中生成相應的class物件

  • 定義語法

    • 訪問修飾符 static 資料型別 變數名[推薦]
    • static 訪問修飾符 資料型別 變數名
  • 訪問類變數

    • 類名.類變數名 [推薦]
    • 物件.類變數名 [不推薦],IDEA中無語法提示

    類變數的訪問也需要遵守相應的訪問權限,定義為private則不能訪問

  • 類變數的生命周期隨著類的創建而創建,隨著類的消亡而銷毀,與具體的物件無關,

類方法(靜態方法)

  • 定義語法

    • 訪問修飾符 static 回傳資料型別 方法名(){} [推薦]
    • static 訪問修飾符 回傳資料型別 方法名(){}
  • 呼叫方法 --也需要滿足相應的訪問權限

    • 類名.類方法名 [推薦]
    • 物件名.類方法名
  • 什么時候需要用到類方法

    當方法內不涉及到任何與物件相關的成員時,可以把方法設定為類方法(靜態方法),可以不創建實體呼叫方法,提高開發效率

    比如JAVA的Math類中很多方法都是靜態方法,所以可以直接Math.sqrt(9)

  • 類方法只能訪問類成員,不能使用this和super

  • 普通成員方法既可以訪問非靜態成員也可以訪問靜態成員

main方法

  • main方法形式 public static void main(String[] args) {}

  • main方法是JAVA虛擬機呼叫的,所以該方法的訪問權限必須是public

  • JAVA虛擬機在呼叫方法的時候不需要創建物件,所以必須加上關鍵字 static

  • main方法接收String型別的陣列引數,該陣列中保存執行 java 命令時傳遞給所運行的類的引數

  • 如下代碼編譯時 java hello vicen 引數名2 引數名3 結果會輸出 第1各引數=vicen, ... ...

    public class hello(){
        public static void main(String[] args) {
            for (int i = 0; i < args.length; i++) {
                System.out.println("第" + (i+1) "個引數=" + args[i]);
            }
        }
    }
    
  • 由于main方法也是靜態方法,也不可以呼叫本類的非靜態成員,要訪問本類的非靜態成員需要先創建物件再呼叫,??

代碼塊??

又稱為初始化塊,屬于類中的成員,類似于方法,用{}包起來但與方法不同,沒有方法名、引數、回傳值,不用通過物件顯示呼叫,而是加載類時或創建物件時隱式呼叫, 修飾符可選,要寫的話也只能寫static {}; --分號可以寫也可以不寫,

  • 好處

    • 相當于另一種形式的構造器(對構造器的補充),可以在呼叫任何形式的構造器時都執行代碼塊內的初始化操作,
    • 應用場景:如果多個構造器中都有重復的陳述句,可以抽取到初始化塊中,提高代碼重用性
  • 類什么時候被加載??

    • 創建物件實體時(new)
    • 創建子類物件實體,父類也會被加載
    • 使用類的靜態成員時(靜態方法,靜態屬性),使用子類的靜態成員父類也會被加載
    public class CodeBlockDetails {
        public static void main(String[] args) {
    
            System.out.println(B.bb);  //呼叫子類的靜態成員屬性時,父類的靜態代碼塊也會執行
            System.out.println(A.aa);  //由于上面呼叫子類的時候已經呼叫了父類的靜態代碼塊,這里A的靜態代碼塊不會被呼叫
            A a = new A();
            B b = new B();
        }
    }
    class A{
        public static int aa = 10;
        //沒有實體化物件,呼叫類的靜態成員時以下非靜態代碼塊不會被加載    只有使用new創建實體物件,以下代碼塊才會執行
        {
            System.out.println("A的非靜態代碼塊呼叫--");
        }
        static {
            System.out.println("A的靜態代碼塊被呼叫~~");
        }
    }
    class B extends A{
        public static int bb = 20;
        {
            System.out.println("B的非靜態代碼塊呼叫--");
        }
        static {
            System.out.println("B的靜態代碼塊被呼叫~~");
        }
    }
    
  • static 代碼塊只會被呼叫一次

  • 普通代碼塊與類的加載無關系,只有在用new實體化物件時普通代碼塊才會被呼叫,并且可以被呼叫多次(與構造器相似)

    public class CodeBlockDetails {
        public static void main(String[] args) {
    
            //System.out.println(B.bb);  //呼叫子類的靜態成員屬性時,父類的靜態代碼塊也會執行
            //System.out.println(A.aa);
            A a = new A();
            B b = new B();
        }
    }
    class A{
        public static int aa = 10;
        //沒有實體化物件,呼叫類的靜態成員時一下非靜態代碼塊不會被加載
        {
            System.out.println("A的非靜態代碼塊呼叫--");
        }
        static {
            System.out.println("A的靜態代碼塊被呼叫~~");
        }
    }
    class B extends A{
        public static int bb = 20;
        {
            System.out.println("B的非靜態代碼塊呼叫--");
        }
        static {
            System.out.println("B的靜態代碼塊被呼叫~~");
        }
    }
    //運行結果
    A的靜態代碼塊被呼叫~~        //先加載類資訊,后創建實體化物件  所以先呼叫靜態代碼塊
    A的非靜態代碼塊呼叫--
    B的靜態代碼塊被呼叫~~      
    A的非靜態代碼塊呼叫--        //實體化B的物件時,會加載A的類資訊,但A的靜態代碼塊已經加載過了,所以只會呼叫A的非靜態代碼塊
    B的非靜態代碼塊呼叫--
    
    //如果把上面的實體化物件的兩條陳述句交換位置  
    B b = new B();
    A a = new A();
    //輸出結果為
    A的靜態代碼塊被呼叫~~        //加載B的類資訊時先加載了A的類資訊
    B的靜態代碼塊被呼叫~~
    A的非靜態代碼塊呼叫--        //創建B的實體化物件時右先加載了B的類資訊,由于static代碼塊已呼叫,所以只呼叫A的非靜態代碼塊
    B的非靜態代碼塊呼叫--
    A的非靜態代碼塊呼叫--
    
  • 創建一個物件時,在一個類呼叫順序是

    • 呼叫靜態代碼塊和靜態屬性初始化(靜態代碼塊和靜態屬性初始化呼叫的優先級一樣,如果有多個按定義的先后順序呼叫)
    • 呼叫普通代碼塊和普通屬性的初始化(普通代碼塊和普通屬性優先級一樣,如果有多個同樣按定義的先后順序呼叫)
    • 最后呼叫構造器(不準確,其實是執行構造器的輸出陳述句,上面的第二步其實是在構造器內隱式呼叫的)
  • 構造器呼叫時隱藏了默認呼叫了自己的普通代碼塊和普通屬性初始化 (public int n = getVal(); 會去呼叫getVal()方法)

    public class CodeBlockDetails02 {
        public static void main(String[] args) {
            BB bb = new BB();
        }
    }
    class AA{
        {
            System.out.println("AA的普通代碼塊呼叫");
        }
        public AA(){
            System.out.println("AA()的構造器呼叫");
        }
    }
    class BB extends AA{
        {
            System.out.println("BB的普通代碼塊呼叫");
        }
        public BB(){
            //這里隱含呼叫了BB的普通代碼塊,注意與非靜態代碼塊不同,普通代碼塊在加載類資訊時并不會被呼叫
            System.out.println("BB()的構造器呼叫");
        }
    }
    //結果
    AA的普通代碼塊呼叫            //實體化BB物件時先呼叫父類的構造器,父類的構造器又隱式呼叫了父類自己的普通代碼塊
    AA()的構造器呼叫             //呼叫完父類的普通代碼塊后才執行父類的構造器
    BB的普通代碼塊呼叫            //執行完父類的所有要執行的后再回到子類,執行順序與父類相同
    BB()的構造器呼叫
    

單例設計模式

設計模式:

  • 靜態方法和屬性的經典使用
  • 是在大量的實踐中總結和理論化后的優選的代碼結構、編程風格以及解決問題的思考方式,就像是經典的棋譜,針對不同的棋局可以使用不同的棋譜

單例設計模式:

  • 采取一定的方法保證在整個的軟體系統中對某個類只能存在一個一個物件實體,并且該類只提供一個取得該物件實體的方法

  • 單例模式有兩種方式

    • 餓漢式:還沒有用到物件的時候在類內已經把物件創建好了,可能造成資源的浪費
    • 懶漢式:在使用物件時才會創建實體,存在執行緒安全的問題
  • 餓漢式實作步驟

    • 構造器私有化 --防止用戶直接new
    • 類的內部創建一個靜態物件并初始化(為了在靜態的公共方法中可以放回該物件,故將其創建為靜態物件)
    • 向外暴露一個靜態的公共方法, getInstance (為了在不創建時就可以呼叫方法)
  • 懶漢式實作步驟

    • 構造器私有化 --防止用戶直接new

    • 定義一個static靜態屬性物件,但不直接利用new對它進行初始化

    • 提供一個public的static方法,判斷如果當前類內物件為null則回傳一個new的物件

    • 注意:懶漢式會存在執行緒安全的問題,多個執行緒同時呼叫getInstance方法,有可能創建出多個物件

      class Cat{
          private String name;
          private static Cat cat;
          private Cat(String name) {
              this.name = name;
          }
          public static Cat getInstance() {
              if(cat == null) {  //如果還沒有創建Cat物件再創建
                  cat = new Cat("小貓");
              }
              return cat;
          }
      

Final關鍵字

final 關鍵字宣告類可以把類定義為不能繼承的,即最終類;或者用于修飾方法,該方法不能被子類重寫:

不希望類的某個屬性被修改,也可以用final修飾該屬性 public final double TAX_RATE = 0.1;

不希望某個區域變數被修改,也可以用final修飾,final int NUM = 1.0; (這時NUM可以成為區域常量)

  • 被final修飾的基本型別變數(四類八種) 不可變!

  • 被final修飾的 參考型別變數 地址不可變!,內容可變!

    final char[] c = {'j','a','v','a'};
    System.out.println(c);
    c[0] = 'h';    //這里是可以修改的
    System.out.println(c);
    
  • final修飾的屬性又叫常量,一般用XX_XX命名,注意要大寫

  • final修飾的屬性在初始化時必須賦初值,可以在定義屬性時,或者在代碼塊中,或者在構造器中

    class A{
        public final double TAX_RATE = 0.1;  //在定義時直接賦初值
        public final double TAX_RATE2;
        public final double TAX_RATE3;
    
        public A() {   //構造器中對常量賦初值
            TAX_RATE2 = 0.2;
        }
        {   //代碼塊中對常量賦初值
            TAX_RATE3 = 0.3;
        }
    }
    
  • final也可以修飾形參,被修飾后該形參不能被改變(相當于const)

  • 一般一個類如果已經是final類了,就不需要對它下面的方法定義為final方法了

  • final不能修飾構造器

  • final和static往往搭配使用,效率更高,用他們修飾的屬性時不會導致類加載,底層編譯器做了優化,順序可以顛倒,

    public class FlnalExer {
        public static void main(String[] args) {
            System.out.println(B.num);
        }
    }
    class B{
        //final 和 static 往往搭配使用 效率更高,不會導致類加載,底層編譯器做了優化,
        public final static int num = 1000;
        static {
            System.out.println("B的靜態代碼塊被呼叫");
        }
    }
    //加上final后最終輸出結果只有 1000
    //去掉final 輸出會有 B的靜態代碼塊被呼叫   1000
    
  • 包裝類String,Double,Integer,Float,Boolean,Byte,Short,Character都是final型別的,不可以繼承

抽象類

如果一個類中沒有包含足夠的資訊來描繪一個具體的物件,這樣的類就是抽象類,
抽象類除了不能實體化物件之外,類的其它功能依然存在,成員變數、成員方法和構造方法的訪問方式和普通類一樣,
由于抽象類不能實體化物件,所以抽象類必須被繼承,才能被使用,也是因為這個原因,通常在設計階段決定要不要設計抽象類,

  • 當一個類中含有抽象(abstract)方法時,需要將這個類宣告為抽象(abstract)類,類內方法不能有 {}
  • 抽象類中不一定包含抽象方法,但是有抽象方法的類必定是抽象類,
  • 構造方法,類方法(用 static 修飾的方法)不能宣告為抽象方法,
  • 抽象類的子類必須給出抽象類中的抽象方法的具體實作,除非該子類也是抽象類,
  • abstract只能修飾類和方法,不能修飾屬性和其他的東西
  • 抽象方法不能使用private,final和static來修飾,因為這些與重寫相違背,static關鍵字和重寫無關
  • 抽象類配合模板使用實作模板設計模式,見實體com.svicen.abstract_

介面

介面(英文:Interface),在JAVA編程語言中是一個抽象型別,是抽象方法的集合,介面通常以interface來宣告,一個類通過繼承介面的方式,從而來繼承介面的抽象方法,

介面并不是類,撰寫介面的方式和類很相似,但是它們屬于不同的概念,類描述物件的屬性和方法,介面則包含類要實作的方法,

除非實作介面的類是抽象類,否則該類要定義介面中的所有方法,

介面無法被實體化,但是可以被實作,一個實作介面的類,必須實作介面內所描述的所有方法,否則就必須宣告為抽象類,另外,在 Java 中,介面型別可用來宣告一個變數,他們可以成為一個空指標,或是被系結在一個以此介面實作的物件,

介面與類相似點:
  • 介面和類的修飾符都只能是 public 或者 默認
  • 一個介面可以有多個方法,

  • 介面檔案保存在 .java 結尾的檔案中,檔案名使用介面名,

  • 介面的位元組碼檔案保存在 .class 結尾的檔案中,

  • 介面相應的位元組碼檔案必須在與包名稱相匹配的目錄結構中,

介面與類的區別:
  • 介面不能用于實體化物件,
  • 介面沒有構造方法,
  • 介面中所有的方法必須是抽象方法,所有方法都是默認public修飾的
  • 介面不能包含成員變數,除了 static 和 final 變數,
  • 介面不是被類繼承了,而是要被類實作,
  • 介面支持多繼承,使用 implements 關鍵字
介面特性
  • 介面中每一個方法也是隱式抽象的,介面中的方法會被隱式的指定為 public abstract(只能是 public abstract,其他修飾符都會報錯),
  • 介面中可以含有變數,但是介面中的變數會被隱式的指定為 public static final 變數(并且只能是 public,用 private 修飾會報編譯錯誤),
  • 介面中的方法是不能在介面中實作的,只能由實作介面的類來實作介面中的方法,
抽象類和介面的區別??
  • 抽象類中的方法可以有方法體,就是能實作方法的具體功能,但是介面中的方法不行,
  • 抽象類中的成員變數可以是各種型別的,而介面中的成員變數只能是 public static final 型別的,
  • 介面中不能含有靜態代碼塊以及靜態方法(用 static 修飾的方法),而抽象類是可以有靜態代碼塊和靜態方法,
  • 一個類只能繼承一個抽象類,而一個類卻可以實作多個介面,

:JDK 1.8 以后,介面里可以有靜態方法和方法體了,??

介面使用
  • 介面內的方法(抽象方法,默認實作方法,靜態方法)
public interface AInterface {
    //寫屬性
    public int n1 = 10;
    //寫方法(抽象方法,默認實作方法,靜態方法)
    //在介面中,抽象方法可以省略abstract關鍵字
    public void hi();
    //jdk8及之后版本,可以有默認實作方法,但需要用default關鍵字修飾
    default public void ok(){
        System.out.println("ok");
    }
    //jdk8之后,可以有靜態方法,不需要使用default
    public static void run(){
        System.out.println("run");
    }
}
  • 示例
public class InterFaceDetail {
}
interface IA{
    void say();   //默認public
    void hi();
}
class Cat implements IA{   //  Ctrl + i 快速實作介面的方法
    @Override
    public void say() {

    }
    @Override
    public void hi() {
    }
}
  • 抽象類實作介面時,可以不實作介面的方法

  • 一個類可以實作多個介面,

    interface IB{}
    interface IC{}
    class Dog implements IB,IC{
    
    }
    
  • 介面不能繼承其它的類,但是可以繼承多個別的介面,使用extends而不是implements??

    interface ID extends IB,IC {}
    
  • 介面VS繼承

    繼承的價值主要在于:解決代碼的復用性和可維護性

    介面的價值注意在于:設計好各種規范(方法),讓其他類取實作這些方法,更加的靈活,可以視為對單繼承的補充

    繼承是滿足 is a 的關系(貓是動物),而介面只需滿足 like a 的關系 (貓像魚一樣游泳)??

    public class InterFaceDetail02 {
        public static void main(String[] args) {
            littlemonkey littlemonkey = new littlemonkey("悟空");
            littlemonkey.climbing();
            littlemonkey.swiming();
            littlemonkey.flying();
        }
    }
    class monkey{
        private String name;
        public monkey(String name) {
            this.name = name;
        }
        public String getName() {
            return name;
        }
        public void climbing(){
            System.out.println("猴子" + name + "會爬樹");
        }
    }
    interface Swimming{
        void swiming();
    }
    interface Flying{
        void flying();
    }
    class littlemonkey extends monkey implements Flying,Swimming{  //可以繼承類的同時,實作多個介面的方法
        @Override
        public void swiming() {
            System.out.println("通過學習," + getName() + "可以像魚兒一樣游泳");
        }
        @Override
        public void flying() {
            System.out.println("通過學習," + getName() + "可以像鳥兒一樣飛翔");
        }
        public littlemonkey(String name) {
            super(name);
        }
    }
    
介面多型特性

如下例,只要是實作了USBInterface介面方法的類都可以作為引數傳遞給USBinterface

public class InterfacePoly {
    public static void main(String[] args) {
        Camera camera = new Camera();
        Phone phone = new Phone();
        Computer computer = new Computer();
        computer.work(phone);    //這里由于work的引數型別為USBInterface,所以引數可以傳進它的子類
        computer.work(camera);
    }
}
public class Computer {
    public void work(USBInterface usbInterface) {
        //通過介面,呼叫方法  體現多型性      固定介面的方法格式
        usbInterface.start();
        usbInterface.stop();
    }
}
public class Phone implements USBInterface{
    @Override
    public void start() {
        System.out.println("手機開始作業");     //對介面做具體的實作
    }
    @Override
    public void stop() {
        System.out.println("手機停止作業");
    }
}

再比如,介面型別可以指向所有實作了介面內方法的類的物件

interface IF{}
class Monster implements IF{}
class Car implements IF{}
public static void main(String[] args) {
    IF if01 = new Monster();  //創建介面型別的變數,指向Monster型別  相當于向上轉型
    if01 = new Car();         //介面型別可以 指向實作了介面的類的物件實體

}

可以創建介面型別的陣列,對于不同位置的元素放不同的實作介面的類

USBInterface usb[] = new USBInterface[2];
//多型陣列
usb[0] = new Phone();
usb[1] = new Camera();
for (int i = 0; i < usb.length; i++) {
    usb[i].start();
    usb[i].stop();
    if (usb[i] instanceof Phone) {
        ((Phone) usb[i]).call();
    }
}
介面多型的傳遞??

如果IG繼承了IH介面,而Cat類實作了IG介面,那么實際上相當于Cat類也實作了IH介面

內部類

  • 可以直接訪問外部類的所有成員,包括私有成員
  • 外部類只能先創建內部類物件,然后通過該物件呼叫內部類方法
  • 外部其他類不能創建內部類物件
  • 如果外部類和區域內部類的成員重名,默認遵循就近原則,如果想訪問外部類的成員,使用外部類名.this.成員名
區域內部類
  • 區域內部類是定義在外部類的區域位置,通常是在外部類的方法中,

  • 區域內部類不能加訪問修飾符,因為他的地位就相當于區域變數,但可以加final,不被別的類繼承

  • 區域內部類的作用域僅僅在定義它的方法或代碼塊中,

  • 外部類只能定義內部類的方法中創建內部類物件,然后通過該物件呼叫內部類方法

    public class LocalInnerClass {
        public static void main(String[] args) {
            Outer01 outer01 = new Outer01();
            outer01.out2();
        }
    }
    class Outer01{
        private int n1 = 100;
        private void out1(){}
        public void out2(){
            //內部類只可以加 final 修飾符
            final class Inner01{
                //內部類可以直接訪問外部類的所有成員,包括私有的
                public void in1(){
                    System.out.println("內部類呼叫  n1=" + n1);
                }
            }
            Inner01 inn01 = new Inner01();
            inn01.in1();
        }
    }
    
  • 外部其他類不能創建內部類物件

  • 如果外部類和區域內部類的成員重名,默認遵循就近原則,如果想訪問外部類的成員,使用外部類名.this.成員名

    public void out2(){
            //內部類只可以加 final 修飾符
            final class Inner01{
                private int n1 = 200;
                //內部類可以直接訪問外部類的所有成員,包括私有的
                public void in1(){
                    System.out.println("內部類呼叫  n1=" + this.n1);
    //Outer01.this本質就是外部類的一個物件,誰呼叫了out2方法,Outer01.this就指向哪個物件
                    System.out.println("外部類呼叫  n1=" + Outer01.this.n1);
                }
            }
    }
    
匿名內部類??
  • 本質是內部類,類的名字由系統分配,名字是外部類的名字 + $1

  • 同時還是一個物件,系統實作類的定義和對介面的實作后,new了一個物件并把它的地址回傳給了我們要接收的變數

  • 由于匿名類沒有類名,那么除了定義它的地方,其他地方無法呼叫,匿名內部類使用一次后就不可以再使用了

  • 基于介面的匿名內部類如下,匿名內部類的宣告是一個運算式,所以要以 ; 結尾

  • 直接 new IA{ 在這里實作介面里的方法 };

    public class AnnoymousInnerClass {
        public static void main(String[] args) {
            Outer02 outer02 = new Outer02();
            outer02.method();
        }
    }
    class Outer02{
        private int num = 10;
        public void method(){
            //基于介面的匿名內部類
            //1.傳統方式,是創建一個類Tiger,實作該介面  并創建物件
            //2.需求是 Tiger 只呼叫一次,后面不再使用  如果每次都定義一個類會造成資源浪費
            //3.使用匿名內部類簡化開發
            //本質是繼承了IA介面,然后對它要使用的方法進行了具體的實作
            IA tiger = new IA(){
                @Override
                public void cry() {
                    System.out.println("老虎叫喚");
                }
            };   //注意這里要有分號
            //編譯型別為 IA  運行型別為 匿名內部類,底層定義了這個類并直接創建了物件,把物件的this賦給了tiger
            /*底層
                class Outer02$1 implements IA{
                    public void cry() {
                    System.out.println("老虎叫喚");
                    }
                }
               */
            System.out.println("tiger的運行型別為" + tiger.getClass()); //運行型別為 Outer02$1
            tiger.cry();
        }
    }
    interface IA{
        public void cry();
    }
    
  • 基于類的匿名內部類 本質是繼承了Father類,然后對它要使用的方法進行了重寫

    class Outer02{
        private int num = 10;
        public void method(){
            //基于介面的匿名內部類
            //1.傳統方式,是創建一個類Tiger,實作該介面  并創建物件
            //2.需求是 Tiger 只呼叫一次,后面不再使用  如果每次都定義一個類會造成資源浪費
            //3.使用匿名內部類簡化開發
            IA tiger = new IA(){
                @Override
                public void cry() {
                    System.out.println("老虎叫喚");
                }
            };
            System.out.println("tiger的運行型別為" + tiger.getClass());
            tiger.cry();
            //基于類的匿名內部類,與介面的區別在于()內可以有引數
            //father的編譯型別 Father   運行型別 為匿名內部類 Outer02$2
            Father father = new Father("jack") {
            };
            System.out.println("father的運行型別為" + father.getClass());
        }
    }
    class Father{
        public Father(String name) {  //構造器
        }
        public void test() {
        }
    }
    
  • 基于類的匿名內部類與普通的定義類區別

    //匿名內部類的實作關鍵在于 后面的 {};    本質是繼承了Father類,然后對它要使用的方法進行了重寫
    Father father1 = new Father("jack") {};  //這里可以什么都不做,即沒有對Father類的方法進行重寫
    System.out.println("father的運行型別為" + father1.getClass());  //運行型別為 匿名內部類  Outer02$2  按順序編號
    
    //普通的new一個物件實體并賦地址給 father2  
    Father father2 = new Father("jack");
    System.out.println("father的運行型別為" + father2.getClass()); //運行型別為Father
    class Father{
        public Father(String name) {  //構造器
        }
        public void test() {
        }
    }
    
  • 基于抽象類的匿名內部類,與上類似,只不過{}內必須把抽象類的方法做出具體實作,

匿名內部類存在的前提是要有繼承或者實作關系的,但是并沒有看到extends和implements關鍵字,由底層JVM實作??

匿名內部類其實還是一個物件,系統實作類的定義和對介面的實作后,new了一個物件并把它的地址回傳給了我們要接收的變數

如上基于介面的匿名內部類

new IA(){
    @Override
    public void cry() {
        System.out.println("老虎叫喚");
    }
}.cry();      //當做物件直接呼叫方法
  • 可以直接訪問外部類的所有成員,包括私有成員

  • 不能添加訪問修飾符,因為它的地位就是一個區域變數

  • 作用域僅僅在定義它的方法或者代碼塊中

  • 外部其他類不可以訪問匿名內部類

  • 如果外部類和匿名內部類的成員重名,默認遵循就近原則,如果想訪問外部類的成員,使用外部類名.this.成員名

    如果匿名內部類重新定義了它所繼承的父類的屬性(前提是父類的屬性不為private)那么呼叫

class Outer02{
    private int num = 10;
    public void method(){  
        new Father("jack") {
            //private int num = 20;
            @Override
            public void test() {
                System.out.println("匿名內部類呼叫的num=" + num);
//首先看內部類有沒有重新定義num,沒有則取繼承的父類即Father找是否有num且可以訪問,還沒有則找外部類的num
                //System.out.println("Father類的 num=" + Outer02.this.num); 這是呼叫外部類的num值
                //   Outer02.this 就是呼叫了method方法的物件
            }
        }.test();  //當成物件直接呼叫方法 
    }
}
class Father{
    protected int num = 30;
    public Father(String name) {  //構造器
    }
    public void test() {
    }
}
  • 最佳實踐,匿名內部類可以直接當成引數使用
public static void main(String[] args) {
        //匿名內部類可以當做實參直接傳遞
        f1(new IG() {
            @Override
            public void show() {
                System.out.println("這是一幅名畫");
            }
        });
    }
    //靜態方法  為了直接在main里呼叫
    public static void f1(IG ig){
        ig.show();
    }
}
interface IG{
    void show();
}

實體二

public class InnerClassExercise02 {
    public static void main(String[] args) {
        //測驗手機的鬧鐘功能,通過匿名內部類(物件)作為引數,列印懶豬起床了 和 小伙伴上課了
        CellPhone cellPhone = new CellPhone();
        //這里的函式引數是一個實作了Bell介面的匿名內部類  重寫了ring方法
        cellPhone.alarmclock(new Bell() {
            @Override
            public void ring() {
                System.out.println("懶豬起床了");  //運行型別為 InnerClassExercise02$1
            }

        });
        cellPhone.alarmclock(new Bell() {
            @Override
            public void ring() {
                System.out.println("小伙伴上課了"); //運行型別為 InnerClassExercise02$2
            }
        });
    }
}
//鈴聲介面
interface Bell{
    void ring();
}
class CellPhone{
    //鬧鐘功能
    public void alarmclock(Bell bell) {
        //這里傳入的bell 編譯型別為Bell   運行型別為呼叫時創建的匿名內部類(是個物件)
        bell.ring();  //呼叫ring方法,首先去自己的運行型別里找,運行型別里重寫了ring方法,所以呼叫自己重寫后的ring
        System.out.println("運行型別為" + bell.getClass());  
    }
}
成員內部類

沒有定義在外部類的方法中,而是定義在外部類的成員的位置上

public class MemberInnerClass {
    public static void main(String[] args) {
        Outer03 outer03 = new Outer03();
        outer03.T();
    }
}
class Outer03{
    private int n1 =10;
    public String name = "張三";
    //成員內部類,沒有定義在外部類的方法中
    class Inner03{
        public void say(){
            //可以直接訪問外部類的所有成員,包括私有成員
            System.out.println("name=" + name + "  n1=" + n1);
        }
    }
    //使用成員內部類
    public void T(){
        Inner03 inner03 = new Inner03();
        inner03.say();
    }
}
  • 可以直接訪問外部類的所有成員,包括私有成員

  • 可以添加任意訪問修飾符(public,private,protected,默認),因為它的地位是一個成員(這一點與區域內部類和匿名內部類不同)

  • 作用域為整個外部類(因為成員內部類就是外部類的一個成員),這一點與區域內部類和匿名內部類也不同

  • 外部類要訪問成員內部類,必須先創建物件,再通過物件訪問方法

  • 外部其他類也可以使用成員內部類(與上面兩個也不同)

    • 外部其它類使用成員內部類的三種方式
    • 第一種
    Outer03 outer03 = new Outer03();
    Outer03.Inner03 inner031 = outer03.new Inner03()  //把內部類當成成員
    
    • 第二種

    在外部類中撰寫一個方法,回傳創建好的成員內部類物件實體

    public class MemberInnerClass {
        public static void main(String[] args) {
            Outer03 outer03 = new Outer03();
            //Outer03.Inner03 inner031 = outer03.new Inner03();  方式1
            Outer03.Inner03 inner032 = outer03.getInner03Instance;  //方式2
        }
    }
    class Outer03{
        class Inner03{
            public void say(){
                //可以直接訪問外部類的所有成員,包括私有成員
            }
        }
        public Inner03 getInner03Instance(){
            return new Inner03();
        }
    }
    
    • 第三種
    Outer03.Inner03 inner033 = new outer03().new Inner03()  //其實就是第一種兩句合成了一句
    
  • 如果外部類和成員內部類的成員重名,默認遵循就近原則,如果想訪問外部類的成員,使用 外部類名.this.成員名

靜態內部類
public class StaticInnerClass {
    public static void main(String[] args) {
        Outer04 outer04 = new Outer04();
        outer04.m();
    }
}
class Outer04 {
    private int n1 = 10;
    public static String name = "張三";
    //同成員內部類,靜態內部類的地位也是外部類的一個成員
    static class Inner04 {
        public void say(){
            System.out.println(name);
        }
    }
    //定義方法去創建內部類物件并呼叫內部類的方法
    public void m(){
        Inner04 inner04 = new Inner04();
        inner04.say();
    }
}
  • 放在外部類的成員的位置,但添加了static修飾符

  • 可以直接訪問外部類的所有靜態成員,包括私有的,但不能直接訪問非靜態成員

  • 可以添加任意的訪問修飾符,因為它的地位就是一個成員,

  • 外部類要訪問成員內部類,必須先創建物件,再通過物件訪問方法

  • 作用域:為整個外部類體

  • 如果外部類和區域內部類的成員重名,默認遵循就近原則,如果想訪問外部類的成員,使用外部類名.this.成員名

  • 外部其他類也可以使用成員內部類(直接通過內部類名 前提訪問控制不為private)

    • 方式1,直接通過外部類名訪問到內部類,前提要有訪問權限
    //這里解釋一下  首先是 Outer04.Inner04()作為一個整體   相當于new了一個Outer04.Inner04() 創建了一個內部類物件
    Outer04.Inner04 inner04 = new Outer04.Inner04();  //之前成員內部類是 先new了外部類在new了內部類
    inner04.say();
    
    • 方式2,撰寫一個方法,回傳靜態內部類的物件實體
    Outer04 outer04 = new Outer04();   //創建外部類物件實體
    Outer04.Inner04 inner041 = outer04.getInner04();  //利用外部類物件呼叫它的回傳靜態內部類的物件的方法
    inner041.say();
    public Inner04 getInner04(){
        return new Inner04();
    }
    
    • 方式3,撰寫一個靜態方法(可以直接用外部類名 . 靜態方法名呼叫),回傳靜態內部類的物件實體
    //以下方法更簡潔,充分利用了靜態內部類的靜態特性,
    Outer04.Inner04 inner04_ = Outer04.getInner04_();
    inner04_.say();
    public static Inner04 getInner04_(){
        return new Inner04();
    }
    

列舉類

  • 列舉類的物件是確定的,只有有限個,例如,如果把季節定義成類,那么這個類只有四個物件:春夏秋冬,此時就能把季節定義為一個列舉類,這個列舉類的物件是確定的并且只有有限個,
  • 需要定義一組常量時,推薦使用列舉類,
  • 如果列舉類只有一個物件,則可以作為一種單例模式的實作方式,

自定義列舉類

  • 第一步,構造器私有化,防止直接new

  • 第二步,去掉set方法,防止屬性被修改

  • 在類內部,直接創建固定物件

    public static final Season SPRINT = new Season("春天","溫暖");
    public static final Season SUMMER = new Season("夏天","炎熱");
    public static final Season AUTUMN = new Season("秋天","涼爽");
    public static final Season WINTER = new Season("冬天","寒冷");
    

使用enum關鍵字實作列舉類

  • 將類的定義 class 改為 enum
enum Season2{
    //使用enum關鍵字
    SPRINT("春天","溫暖"),
    SUMMER("夏天","炎熱");      //這里的兩個引數對應構造器的兩個引數
    private String name;
    private String desc; //描述
    private Season2(String name, String desc) {
        this.name = name;
        this.desc = desc;
    }
    @Override
    public String toString() {
        return "Season2{" +
                "name='" + name + '\'' +
                ", desc='" + desc + '\'' +
                '}';
    }
}
  • 如果有多個常量(物件),使用,間隔
  • 如果用enum實作列舉,要求將定義的常量物件寫在最前面,位置固定,不然報錯
  • 本質:定義 enum Season2 實際上是 Season2 繼承了 java 的java.lang.Enum類,而且是一個final類,然后定義了對應的public static final常量
  • 由于enum已繼承了Enum類,所以不能加extends關鍵字了,但可以繼承介面,加implements

javap反編譯

//命令列界面,使用javap 命令對生成的.class檔案進行反編譯
D:\JetBrains\IDEA\enum_annotation\out\production\enum_annotation\com\svicen\enum_>javap Season2.class
Compiled from "EnumExercise02.java"
final class com.svicen.enum_.Season2 extends java.lang.Enum<com.svicen.enum_.Season2> {
  public static final com.svicen.enum_.Season2 SPRINT;
  public static final com.svicen.enum_.Season2 SUMMER;
  public static com.svicen.enum_.Season2[] values();
  public static com.svicen.enum_.Season2 valueOf(java.lang.String);
  public java.lang.String getName();
  public java.lang.String getDesc();
  public java.lang.String toString();
  static {};
}
  • 如果使用無參構造器創建列舉物件,小括號也可以省略,WINTER即可

  • 如果enum 列舉類內沒有重寫 toString方法,則回去呼叫父類的toString方法,父類即Enum類,直接訪問它定義的名字

    //Enum類的屬性由 name  和 ordinal  構造器如下
    protected Enum(String name, int ordinal) {
            this.name = name;         //這里的name即為定義時的SPRING  SUMMER 等  連同索引作為引數傳給了父類的構造器
            this.ordinal = ordinal;
    }
    
    public String toString() {
        return name;     //上面的Season如果將toString注釋,輸出Season2.SPRINT  即為 SPRING
    }
    

    Enum類的常用方法

方法名稱 描述
values() 以陣列形式回傳列舉型別的所有成員 呼叫時用列舉類名.values()
valueOf() 將普通字串轉換為列舉實體,要求字串為已有的常量
compareTo() 比較兩個列舉成員在定義時的順序(索引),大于回傳1,等于回傳0,小于回傳-1
ordinal() 獲取列舉物件的索引位置(從0開始)
name() 獲取列舉物件的名字
toString() Enum類已經重寫過了,回傳的是當前物件的物件名,子類可以重寫該方法
System.out.println("name=" + Season2.SPRINT.name());
System.out.println("ordinal=" + Season2.AUTYMN.ordinal());
Season2[] values = Season2.values();
for (Season2 season:values) {
    System.out.println(season);
}
Season2 summer = Season2.valueOf("SUMMER");
System.out.println("summer.ordinal=" + summer.ordinal());
System.out.println(summer.compareTo(Season2.SUMMER));  //結果為0  比較的就是ordinal

注解

Java 注解(Annotation)又稱 Java 標注,Java 語言中的類、方法、變數、引數和包等都可以被標注,和 Javadoc 不同,Java 標注可以通過反射獲取標注內容,在編譯器生成類檔案時,標注可以被嵌入到位元組碼中,Java 虛擬機可以保留標注內容,在運行時可以獲取到標注內容 , 當然它也支持自定義 Java 標注,

內置的注解

Java 定義了一套注解,共有 7 個,3 個在 java.lang 中,剩下 4 個在 java.lang.annotation 中,

作用在代碼的注解是

  • @Override - 檢查該方法是否是重寫方法,如果發現其父類,或者是參考的介面中并沒有該方法時,會報編譯錯誤,

    @Target(ElementType.METHOD)                        //標記這個注解應該是哪種 Java 成員
    //標識這個注解怎么保存,是只在代碼中,還是編入class檔案中,或者是在運行時可以通過反射訪問,
    @Retention(RetentionPolicy.SOURCE)
    public @interface Override {
    }
    
  • @Deprecated - 標記過時方法,如果使用該方法,會報編譯警告,

  • @SuppressWarnings - 指示編譯器去忽略注解中宣告的警告,一直警告(黃色提示)

    @SuppressWarnings({"all"})       //注意它有作用范圍,方法或類上面作用于方法或類內部
    //該注解類內部有一個屬性String[] value(); 所以傳的時候可以傳多種警告,比如@SuppressWarnings({"all","unused"})
    //作用于@SuppressWarnings的注解
    @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, MODULE})
    

作用在其他注解的注解(或者說元注解)是:

  • @Retention-標識這個注解怎么保存,在編譯代碼中(RetentionPolicy.SOURCE),還是編入class檔案中(RetentionPolicy.CLASS)(默認是這個),或者是在運行時可以通過反射訪問(RetentionPolicy.RUNTIME)(運行時還保存,保留時間最長),
  • @Documented - 標記這些注解是否包含在用戶檔案中,生成javaDOC時該注解會被保留在檔案上
  • @Target - 標記這個注解可以修飾的 Java 成員
  • @Inherited - 標記這個注解是繼承于哪個注解類(默認 注解并沒有繼承于任何子類)

從 Java 7 開始,額外添加了 3 個注解:

  • @SafeVarargs - Java 7 開始支持,忽略任何使用引數為泛型變數的方法或建構式呼叫產生的警告,
  • @FunctionalInterface - Java 8 開始支持,標識一個匿名函式或函式式介面,
  • @Repeatable - Java 8 開始支持,標識某注解可以在同一個宣告上使用多次,

例外

執行程序發生的例外有兩大類

  • Error,Java虛擬機無法解決的嚴重問題,如JVM系統內部錯誤,資源耗盡等嚴重情況,比如StackOverflowError和OOM(out of memeory),會導致程式的崩潰
  • Exception:其它因為編譯錯誤或偶然的外在因素導致的一般性問題,可以使用針對性的代碼對其進行處理,例如空指標訪問,識圖讀取不存在的檔案,網路連接中斷等等,Execption分為兩大類,運行時例外和編譯時例外,
    • 運行時例外:RunTimeException:有NullPointerException,ArithmeticException(分母為0),有默認處理機制 ArrayIndexOutOfBoundsException,ClassCastException(型別轉換),NumberFormatException(數字格式例外)等
    • 編譯時例外:FileNotException:ClassNotFoundException(加載不存在的類),必須處理

進入Throwable后查看原始碼,右鍵Diagrams -> show Diagrams

image-20220421104141869

例外處理方式

  • try-catch-finally Ctrl+alt +t 選擇例外

    int num1 = 1,num2 = 0;
    try {
        int num3 = num1 / num2;  //可能有例外的代碼塊
    } catch (Exception e) {
        //當例外發生時,系統將例外封裝到Exception物件e,傳遞給catch,程式員可以自己處理
        //沒有發生例外,catch代碼塊是不會執行的
        //throw new RuntimeException(e);
        System.out.println("例外為" + e.getMessage());
    } finally {
        //不管try代碼塊有沒有發生例外,始終要執行finally代碼塊,
        //通常將釋放資源的代碼放在這里
    }
    
  • throws

    將發生的例外拋出,交由呼叫者處理,最終JVM呼叫了main函式,每一步都有兩種處理方式,默認throws(二選一即可)

    image-20220421110240880

try-catch細節

  • 如果發生例外,則例外發生后面的代碼不會執行,直接進入到catch代碼塊
try {
    String str = "例外";
    int a = Integer.parseInt(str);
    System.out.println("轉換后:" + a);  //不會執行
} catch(NumberFormatException e) {
    System.out.println("例外資訊:" + e.getMessage());
} finally{
    System.out.println("finally代碼塊執行");
}
System.out.println("程式繼續執行");  //會執行
  • 如果例外沒有發生,則順序執行try的代碼塊,不會進入到catch

  • 如果希望無論是否發生例外都執行某段代碼(比如斷開連接、釋放資源),可以加上finally

  • try代碼塊可以有多個例外,可以有多個catch陳述句,分別捕獲不同例外,要求子類例外要寫在前面,父類例外(Exception e)寫在后面

  • 可以進行try-finally配合使用,應用于:執行一段代碼,不管是否發生例外,都必須執行某個業務邏輯

    相當于沒有捕獲例外,執行完finally的陳述句后程式會直接崩掉/退出

    try {
        int n1 = 10;
        int n2 = 0;
        System.out.println(n1 / n2);
    }finally {
        System.out.println("finally陳述句塊執行...");
    }
    System.out.println("程式繼續執行");  //這條陳述句不會執行
    //如下回傳結果為4,即使捕獲到了NullPointerException例外,但是catch下的return陳述句還是沒有執行,執行了finally陳述句
    //注意 catch下的return不會執行,但是 相關的陳述句還是會執行的 比如 return ++i;  i還是會完成自增,但不會回傳
    

    image-20220421112403640

注意:下面的catch陳述句塊中會將++i后的值作為臨時變數先保存起來再去執行finally陳述句,最后回傳這個臨時變數

image-20220421112848951

throws細節

  • 對于編譯例外,程式中必須處理, 快捷鍵 : alt + 回車
  • 對于運行時例外,程式中如果沒有處理,默認是throws
  • 如果一個方法可能生成某種例外,但是并不能確定如何處理這些例外,則此方法應顯示地宣告拋出例外,表名該方法將不對例外進行處理,而由該方法的呼叫者負責處理,
  • 在方法宣告中用throws陳述句可以宣告拋出例外的串列,throws后面的例外型別可以是方法中產生的具體例外型別,也可以是它的父類,也可以是一個例外串列,即可以拋出多個例外,如下所示
public class Throws01 {
    public static void main(String[] args) {

    }
    public void f1() throws FileNotFoundException,NullPointerException,ArithmeticException  {
        //創建了一個檔案流物件,
        //這里的例外是一個FileNotFoundException,編譯時例外
        //可以使用try-catch-finally
        //使用throws,拋出例外  throws FileNotFoundException  也可以是 throws Exception
        FileInputStream fileInputStream = new FileInputStream("d//aa.txt"); //編譯例外
    }
}
  • 子類重寫父類方法時,對拋出的例外的規定:子類重寫的方法所拋出的例外要么和父類拋出的例外一致,要么為父類拋出例外的子型別,范圍不能大于父類的例外范圍,比如說父類拋出RunTimeException,子類拋出Exception是不允許的,
  • 如果f3呼叫f1,f1里有編譯例外并拋出給呼叫者,f3里必須對例外進行處理,或者try-catch或者throws,但是如果f1有運行例外則f3可以不處理,默認是throws

自定義例外

一般是繼承RunTimeException

public class Throws01 {
    public static void main(String[] args) {
        int age = 200;
        if(!(age >=18 && age <=120)) {
            throw new AgeException("年齡需在18-120");  //拋出例外,輸出的即為我們這里傳的引數
        }
        System.out.println("你的年齡范圍正確");
    }
}
class AgeException extends RuntimeException{
    public AgeException(String message) {  //構造器
        super(message);
    }
}
意義 位置 后面跟的東西
throw 手動生成例外物件的關鍵字,與new并用 方法體中 例外物件
throws 例外處理的一種方式 方法宣告處 例外型別

包裝類

所有的包裝類(Integer、Long、Byte、Double、Float、Short)都是抽象類 Number 的子類,

這種由編譯器特別支持的包裝稱為裝箱,所以當內置資料型別被當作物件使用的時候,編譯器會把內置型別裝箱為包裝類,相似的,編譯器也可以把一個物件拆箱為內置型別,Number 類屬于 java.lang 包,

//手動裝箱   jdk5之前手動裝箱和拆箱
int n1 = 10;
Integer integer = new Integer(n1);   // jdk9以后 @Deprecated方法過時
//手動拆箱
int i = integer.intValue();
//自動裝箱  jdk5之后可以
int n2 = 20;
Integer integer2 = n2;
System.out.println(n2);
//自動拆箱
int n3 = integer2;
System.out.println(n3);

//注意看以下代碼  三元運算子是一個整體,雖然執行new Integer(1)但整體精度為double,輸出1.0
Object obj1 = true ? new Integer(1):new Double(2.0);
System.out.println(obj1);  // 1.0

包裝類方法

  • 包裝類轉字串

    Integer i = 100;
    //方式1
    String str1 = i + "";
    //方式2
    String str2 = i.toString();
    //方式3
    String str3 = String.valueOf(i);
    
  • 字串轉包裝類

    //String->Wrapper
    String str4 = "123";
    Integer i2 = Integer.parseInt(str4);
    //方式2
    Integer integer = new Integer(str4);
    

經典題目

Integer m = 1;
Integer n = 1;
System.out.println(m == n);   //true  這里沒有new物件
Integer x = 128;
Integer y = 128;
System.out.println(x == y);      //false 這里回傳的是new的物件
int n = 10;
Integer nn = 10;
System.out.println(n == nn);  //false 只要有基本資料型別判斷的就是值是否相等

//Integer m = 1; 底層是呼叫了Integer的 valueOf方法,
public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}
public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)  //low為-128   high為127 Integer的快取機制
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

String類

image-20220422142032445

  • String類實作了Comparable(物件可以比較)和Serializable介面(說明String可以串行化,可以在網路傳輸)

String的字符使用Unicode字符編碼,一個字符(不區分字母還是漢字)占兩個位元組

String類有很多構造器的多載,常用的有

String s1 = new String();
String s2 = new String(String original);
String s3 = new String(char[] a);
String s4 = new String(char[] a,int startIndex,int count);
String s5 = new String(byte[] b);
  • String是final類,不能被其他的類繼承,String有屬性,private final byte[] value; 用于存放字串內容,value陣列也有final關鍵字,不可以修改(指的是value[]不可以指向新的地址,但具體的字符是可以改的),

物件的創建

//1、直接在常量區找是否有"svicen"的資料空間,有則將s1指向該空間,沒有則重新創建,然后指向,s1最終指向的常量池的空間地址
String s1 = "svicen";
//2、現在堆中創建空間,里面維護了value屬性指向常量池的"svicen"的資料空間,如果常量池沒有則創建后指向,s2指向的是堆的空間地址
String s2 = new String("svicen");

String的intern方法,如果池已經包含一個等于此String物件的字串(用equals(Object)判斷),則回傳池中的字串,否則,將此String物件添加到池中,并回傳此物件的參考, 最侄訓傳的是常量池的地址

經典題目

Person p1 = new Person();   //在堆區創建了兩個Person物件,他的name屬性時字串,字串的value指向常量區的"svicen"
p1.name = "svicen";
Person p2 = new Person();
p2.name = "svicen";
System.out.println(p1.name.equals(p2.name)); //true
System.out.println(p1.name == p2.name);     //true  只要常量區里有"svicen"就直接指向,所以p1 p2的value指向的地址相同
System.out.println(p1.name == "svicen");    //true  p1.name的value[]指向的就是常量區的"svicen"的地址
String s1 = new String("abc");
String s2 = new String("abc");
System.out.println(s1 == s2);               //false  這里新創建了兩個物件,物件的地址一定是不一樣的
System.out.println(s1.intern() == s2.intern()); //true intern方法回傳了兩個物件的value指向的常量區的地址
//關于字串物件創建的問題
String s1 = "hello";
s1 = "haha";              //創建了兩個物件  因為字串的值不能改變,修改后相當于新創建了一個物件讓s1指向它

String a = "hello" + "abc";  //創建一個物件,編譯器會做優化,判斷創建的常量區物件是否有參考指向,
//等價于 String a = "helloabc"

String a = "hello";
String b = "abc";
String c = a + b;       //創建三個物件                             // d = "helloabc"
//1.先創建一個StringBuilder sb = StringBuilder()
//2.執行sb.append("hello");
//3.執行sb.append("abc");
//4.執行sb.toString()方法,return 上面的字串  回傳給c
//最后實際上是 c 指向堆中的物件(String) value[] value陣列再指向 常量池的"helloabc"  所以 c==d 回傳false
// 字串常量相加結果直接指向常量池   字串變數相加結果指向堆的記憶體

String常用方法

int length():回傳字串的長度:return value.length

char charAt(int index):回傳某索引處的字符  return value[index]   不要使用str[0]取第一個字符

boolean isEmpty():判斷是否是空字串:return value.length == 07

String toLowerCase():使用默認語言環境,將String 中的所有字符轉為小寫

String toUpperCase():使用默認語言環境,將String 中的所有字符轉為大寫

char[] toCharArray():將String轉換成一個char陣列 

boolean equals(Object obj):比較字串的內容是否相同

boolean equalsIgnoreCase(String str) 功能與equals相似,忽略大小寫

String concat(String str):將指定字串連接到此字串的結尾,等價于”+“

int compareTo(String str):比較兩個字串的大小,前者大回傳正數,后者大回傳負數,長度和內容都相同回傳0
String a = "jacks";
String b = "jaaks";
System.out.println(a.compareTo(b));  //每一位依次比較,最侄訓傳 'c' - 'a' = 2
a = "jaak";
System.out.println(a.compareTo(b)); //如果二者已比較的的都相等,但長度不等,最后回傳的是二者的長度的差值

String substring(int beginIndex):回傳一個新的字串,它是此字串的從beginIndex位置開始截取

String substring(int beginIndex,int endIndex):回傳一個新的字串,它是此字串的從beginIndex位置開始截取到endIndex位置,

boolean endsWidth(String str):測驗此字串是否以指定的后綴結束

boolean startsWidth(String str):測驗此字串是否以指定的前綴開始

boolean startsWidth(String str, int toffset):測驗此字串是否在指定索引開始的位置以指定的前綴開始  

boolean contains(CharSequence s):當且僅當此字串包含指定的char值序列時回傳true

int indexOf(String str):回傳指定子字串在此字串中第一次出現的索引,找不到回傳-1

int indexOf(String str,int index):回傳指定子字串在此字串中第一次出現的索引,從指定索引處開始查找

int lastIndexOf(String str):回傳指定子字串在此字串中最右邊出現的索引,找不到回傳-1

int lastIndexOf(String str):回傳指定子字串在此字串中最右邊出現的索引,從指定索引處開始查找

String replace(char oldChar,char newChar):回傳一個新的字串,它是通過newChar替換此字串中出現的所有oldChar得到的
s2 = s1.replace("a","b");  //對于s1本身沒有影響
String replace(CharSequence target,CharSequence replacement):使用指定的字面值替換序列替換此字串所有匹配字面值目標序列的子字串

String replaceAll(String regex,String replacement):使用指定的replacement代替正則運算式查詢出來的值

String replaceFirst(String regex,String replacement):使用指定的replacement代替正則運算式查詢出來的第一個值

boolean matches(String regex):告知此字串是否匹配給定的正則運算式

String[] split(String regex):根據給定正則運算式的匹配拆分此字串,s = s.split(",");以逗號為分割點分割

String[] split(String regex,int limit):根據匹配的正則運算式來拆分此字串,最多不超過limit個,如果超過,剩下的全部放到最后一個元素中,

String format(String format, Object args):就是利用%S %d %x占位符   

JAVA的格式化字串 format

String formatStr = "我的名字是%s,年齡是%d,成績是%.2f,性別是%c.";
String info = String.format(formatStr,a,age,score,sex);
System.out.println(info);

由于String每次更新物件都需要重新開辟空間,效率較低,因此有StringBuilder和StringBuffer來增強String的功能

StringBuffer

  • StringBuffer的直接父類是AbstractStringBuilder,StringBuffer也實作了Serializable介面,即它創建的物件也可以串行化

  • 在父類AbstractStringBuilder中有屬性char[] value,不是final修飾的,所以該value陣列存放在堆里,不再是常量池

  • StringBuffer也是一個final類,不可以被繼承

  • StringBuffer保存的是字串變數,里面的值可以修改,每次StringBuffer的更新實際上可以更新內容,不用每次更新地址

    即創建新物件),所以效率要比String高

StringBuffer方法

  • 構造器

    //創建一個大小為16的char陣列
    StringBuffer stringBuffer = new StringBuffer();
    //通過構造器指定char value陣列的大小
    StringBuffer stringBuffer1 = new StringBuffer(100);
    //通過構造器直接賦值一個字串,char陣列大小為16+length(傳入的字串)
    StringBuffer hello = new StringBuffer("hello");
    
  • String與StringBuffer的轉換

    //String->StringBuffer
    //方法一:使用構造器,對str本身沒有影響,只是回傳的是一個StringBuffer物件
    String str = "svicen";
    StringBuffer stringBuffer2 = new StringBuffer(str);
    //方法二:使用append方法,同樣對str本身沒有影響,只是拷貝了一份str追加到了當前StringBuffer物件的末尾
    StringBuffer stringBuffer = new StringBuffer();
    stringBuffer.append(str);
    
    //StringBuffer->String
    //方法一:使用StringBuffer的toString方法
    StringBuffer hello = new StringBuffer("hello");
    String s = hello.toString();
    //方法二:使用構造器
    String s1 = new String(stringBuffer2);
    
  • 1、append方法

    public StringBuffer append(boolean b)

    該方法的作用是追加內容到當前StringBuffer物件的末尾,類似于字串的連接,呼叫該方法以后,StringBuffer物件的內容也發生 改變,

  • 2、insert方法

    public StringBuffer insert(int offset, boolean b)

    作用是在StringBuffer物件中插入內容,然后形成新的字串,

  • 3、delete()方法

    s.delete(1,2); 洗掉1-2的字符,左閉右開

  • 4、replace()方法

    s.replace(1,2,'a'); 把索引為1的字符替換為 'a'

  • 5、deleteCharAt方法

    public StringBuffer deleteCharAt(int index)

    該方法的作用是洗掉指定位置的字符,然后將剩余的內容形成新的字串,

  • 6、reverse方法

    public StringBuffer reverse()

    作用是反轉StringBuffer物件中的內容,然后形成新的字串,

  • 7、trimToSize方法

    public void trimToSize()

    該方法的作用是將StringBuffer物件的中存盤空間縮小到和字串長度一樣的長度,減少空間的浪費,

  • 8、setCharAt方法

    public void setCharAt(int index, char ch)

    作用是修改物件中索引值為index位置的字符為新的字符ch,

題目實體,對金額形式的轉換,每三位添加一個,

Scanner scanner = new Scanner(System.in);
String str = scanner.next();
StringBuffer stringBuffer = new StringBuffer(str);
for (int i = stringBuffer.lastIndexOf(".") - 3; i > 0; i -= 3) {
    //123456.59
    //先找到小數點位置
    stringBuffer.insert(i,",");
}
System.out.println(stringBuffer);

StringBuilder

一個可變的字符序列,此類提供一個與StringBuffer兼容的API,但不保證同步(會有線程安全問題),此類被設計用作StringBuffer的一個簡易替換,用在字串緩沖區被單個執行緒使用的時候,如果可能,建議優先采用該類,因為大多數實作中,它比StringBuffer更快

StringBuilder上的主要操作是append和insert方法,可多載這些方法,以接受任意型別的資料,

  • StringBuilder的直接父類是AbstractStringBuilder,StringBuilder也實作了Serializable介面,即它創建的物件也可以串行化
  • 仍是final類,物件字符序列仍是存放在父類AbstractStringBuilder的屬性char[] value中,
  • StringBuilder的方法,沒有做互斥的處理,即沒有synchronized關鍵字,因此在單執行緒的情況下使用

Math類

  • Math.sqrt() : 計算平方根

  • Math.cbrt() : 計算立方根

  • Math.pow(a, b) : 計算a的b次方

  • Math.max( , ) : 計算最大值

  • Math.min( , ) : 計算最小值

  • Math.abs() : 取絕對值

  • Math.ceil(): 天花板的意思,就是逢余進一

  • Math.floor() : 地板的意思,就是逢余舍一

  • Math.rint(): 四舍五入,回傳double值,注意.5的時候會取偶數

  • Math.round(): 四舍五入,float時回傳int值,double時回傳long值

  • Math.random(): 取得一個[0, 1)范圍內的亂數

Arrays類

  • Arrays.fill(Object[ ] array, Object obj):用指定元素填充整個陣列(替換陣列原元素)

  • Arrays.sort(Object [ ]arr, new Comparator() ):對傳入陣列進行遞增排序,字符則按照ASCII進行排序(不區分大小寫),可以自己指定排序方法,傳入一個介面Comparator實作定制排序(重寫compare方法),sort底層呼叫了匿名內部類Comparator的compare方法

    Integer[] arr = {1,5,9,3,6};
    Arrays.sort(arr);   //默認升序 
    System.out.println(Arrays.toString(arr));  //1,3,5,6,9
    Arrays.sort(arr, new Comparator<Integer>() {    //介面編程
        @Override
        public int compare(Integer o1, Integer o2) {
            return o2 - o1;            //降序排列
        }
    });
    System.out.println(Arrays.toString(arr)); //9,6,5,3,1
    
    //原始碼閱讀
    public static <T> void sort(T[] a, Comparator<? super T> c) {
        if (c == null) {  //首先判斷匿名內部類是否為空
            sort(a);
        } else {
            if (LegacyMergeSort.userRequested)
                legacyMergeSort(a, c);
            else
                TimSort.sort(a, 0, a.length, c, null, 0, 0); //底層呼叫的Timsort的sort方法,底層二分排序
        }
    }
    //binarySort的部分代碼如下              
    while (left < right) {
        int mid = (left + right) >>> 1;  //無符號右移
        //體現動態系結
        if (c.compare(pivot, a[mid]) < 0)//呼叫了我們實作的匿名內部類的compare方法  因此可以通過多載compare實作定制排序
            right = mid;
        else
            left = mid + 1;
    }
    
  • Arrays.equal(Object []arr,Object []nums):判斷兩個陣列是否相等,實際上比較的是兩個陣列的哈希值,

  • Arrays.equals(Object []arr,Object []nums):判斷兩個陣列的元素是否完全相同

  • Array.hashCode(Object []arr):回傳陣列的哈希值

  • Arrays.copyOf(Object [], int length):拷貝陣列,其內部呼叫了 System.arraycopy() 方法,從下標0開始,拷貝指定長度的元素(length可選),如果超過原陣列長度,會用null進行填充,

  • Arrays.copyOfRange(T[] original, int from, int to):拷貝陣列,指定起始位置和結束位置,如果超過原陣列長度,會用null進行填充

  • Arrays.toString(Object []arr):將陣列中的內容全部列印出來

    Integer[] integer = {1, 30, 15};
    System.out.println(Arrays.toString(integer));
    //結果
    [1,30,15]
    
  • Arrays.binarySearch(Object []arr,T ans)二分查找法找指定元素的索引值(陣列一定是排好序的,否則會出錯,找到元素,只會回傳最后一個位置) 如果不存在, - (low + 1) low 為按升序排該數字應該在該陣列的位置,

  • Arrays.asList(arr):將arr里的資料轉為List集合再回傳,

System類

  • exit 退出當前程式,exit(0)表示例外退出
  • arrcopy(arr,0,dest,0,len):復制陣列元素 第二個引數表示從原陣列的哪個位置開始拷貝,第四個引數為目標陣列的開始索引,第四個引數為拷貝長度
  • currentTimeMillis(),回傳當前時間距離1970.1.1的毫秒數

大數處理

BigInteger

BigInteger bigInteger = new BigInteger("22222222222222222222222222222");
BigInteger bigInteger1 = new BigInteger("22222222222222222222222222222");
System.out.println(bigInteger);
//在對BigInteger進行加減乘除時需要使用對應的方法   將數字當成字串,處理完后再轉為數字
BigInteger sum = bigInteger.add(bigInteger1);
System.out.println(sum);
BigInteger sub = bigInteger.subtract(bigInteger1);
System.out.println(sub);
BigInteger multy = bigInteger.multiply(bigInteger1);
System.out.println(multy);
BigInteger div = bigInteger.divide(bigInteger1);
System.out.println(div);

BigDecimal

BigDecimal bigDecimal1 = new BigDecimal("1.11111111111111111111111111111111111111");
BigDecimal bigDecimal2 = new BigDecimal("1.11111111111111111111111111111111111111");
//也需要使用對應的方法對BigDecimal進行加減乘除
BigDecimal sum = bigDecimal1.add(bigDecimal2);
System.out.println(sum);
//對于除法  如果除不盡,有可能會拋出例外  -- 可以在divide方法后指定精度
BigDecimal div = bigDecimal1.divide(bigDecimal2);
BigDecimal div = bigDecimal1.divide(bigDecimal2,BigDecimal.ROUND_CEILING); 
//BigDecimal.ROUND_CEILING 在JDK9后已過時
System.out.println(div);

日期類

第一代日期類Date

  • Date:精確到毫秒,代表特定的時間 System.out.println(new Date());

    Date(123),傳入的引數為毫秒,把毫秒轉換為對應的時間

  • SimpleDateFormat:格式化和決議日期的類,允許進行格式化(日期->文本),決議(文本->日期)和規范化

    Date date = new Date();
    System.out.println(date);
    //Mon Apr 25 18:57:05 CST 2022
    //這里注意月份的占位符是MM,E是周幾
    SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy年MM月dd日 hh:mm:ss E");
    String format = simpleDateFormat.format(date);
    System.out.println(format);
    //2022年04月25日 06:57:05 周一
    

第二代日期類Calendar

Calendar是抽象類,所以不可以實體化物件

缺點:可變性(日期和時間這樣的類應該是不可變的),偏移性(月份從0開始),不能格式化,不是執行緒安全的,不能處理閏秒

Calendar c = Calendar.getInstance();
System.out.println("c=" + c);   //會將整個日歷的欄位全部列印出來
//如果要分別列印欄位  需要自己組合,沒有提供格式化字串的方法
System.out.println("年:" + c.get(Calendar.YEAR));
System.out.println("月:" + (c.get(Calendar.MONTH) + 1));  //月份按照0開始編號的,所以需要+1
System.out.println("日:" + c.get(Calendar.DAY_OF_MONTH));
System.out.println("時:" + c.get(Calendar.HOUR));
System.out.println("時:" + c.get(Calendar.HOUR_OF_DAY)); //以24小時的方式獲取 時
System.out.println("分:" + c.get(Calendar.MINUTE));
System.out.println("秒:" + c.get(Calendar.SECOND));

第三代日期類LocalDateTime

  • LocalDate(日期,年/月/日)
  • LocalDate(時間,時/分/秒)
  • LocalDateTime(年/月/日/時/分/秒) JDK8 加入
LocalDateTime ldt = LocalDateTime.now();
System.out.println(ldt);
//2022-04-25T19:34:02.573627400
System.out.println("年:" + ldt.getYear());
System.out.println("月:" + ldt.getMonth());             //APRIL
System.out.println("月:" + ldt.getMonthValue());        //4
System.out.println("日:" + ldt.getDayOfMonth());

//使用DateTimeFormatter 物件對日期進行格式化
DateTimeFormatter date = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");//注意小時使用 HH 按24小時來算
String format = date.format(ldt);  
System.out.println("格式化日期:" + format);  //格式化日期:2022年04月25日 19:42:23
  • Instant時間戳:提供了一系列與Date類相互轉換的方法
//通過靜態方法,now 獲取當前時間戳的物件
Instant now = Instant.now();
System.out.println(now);
//通過 Date類的from 方法可以把Instant轉為Date
Date date = Date.from(now);
System.out.println(date);
//通過 date物件的toInstant方法可以把一個Date轉換為Instant
Instant instant = date.toInstant();
System.out.println(instant);
//最終輸出結果為
2022-04-25T11:47:50.124618200Z
Mon Apr 25 19:47:50 CST 2022
2022-04-25T11:47:50.124Z      可知再轉換為Instannt物件后時間戳的精度比now出來的小
  • 對日期的加減
//對日期的加減
LocalDateTime ldt = LocalDateTime.now();
LocalDateTime localDateTime = ldt.plusDays(23);
System.out.println("23天后:" + localDateTime);
//也可以格式化輸出
DateTimeFormatter date = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss");
System.out.println("23天后:" + date.format(localDateTime)); //23天后:2022年05月18日 19:55:16
//對日期的減
LocalDateTime localDateTime1 = ldt.minusHours(24);
System.out.println(date.format(localDateTime1));     //    減去24小時后:2022年04月24日 19:55:16

集合

ctrl + alt +b 在類圖中查看子介面 alt + insert 快速插入方法

image-20220427154620682 image-20220427155432910

集合主要分為兩類,單列集合和雙列集合List和Set都是單列集合,map是雙列集合,

List

ArrayList

底層維護了一個Object型別的陣列transient Object[] elementData,transient 表示瞬間的,短暫的,表示該屬性不會被序列化

List list = new ArrayList();
//add方法插入
list.add("svicen");
list.add(10);
list.add(true);
System.out.println(list);
//remove方法洗掉元素
list.remove("svicen");
System.out.println(list);
//contains方法查看是否有指定元素
System.out.println(list.contains(true));
//isEmpty方法判斷是否為空
System.out.println(list.isEmpty());
//clear方法清空
list.clear();
//addAll方法,添加一個集合
ArrayList list1 = new ArrayList();
list1.add("西游記");
list1.add("紅樓夢");
list.addAll(list1);
System.out.println(list);
//containsAll方法查找集合是否存在
System.out.println(list.containsAll(list1));
//removeAll方法洗掉集合
System.out.println(list.removeAll(list1));
  • 迭代器Iterator遍歷
ArrayList col = new ArrayList();
col.add(new Book("三國演義","羅貫中",50));
col.add(new Book("西游記","吳承恩",40));
col.add(new Book("紅樓夢","曹雪芹",60));
//使用迭代器遍歷
//首先需要獲得迭代器
Iterator iterator = col.iterator();
//快捷鍵 itit    查看所有快捷鍵 ctrl + j
while (iterator.hasNext()) {
    Object next =  iterator.next();
    System.out.println(next);
}
//退出回圈后,iterator指向最后的元素
//如果需要再次遍歷,需要重置迭代器
iterator = col.iterator();
  • 增強for回圈遍歷
//快捷鍵 I
for (Object book : col) {
    System.out.println(book);
}
  • list記憶體的元素是有序的(添加順序和取出順序一致)

  • list內可以有重復的元素

  • list的每個元素都有對應的順序索引

    list.add("張三");
    list.add("李四");
    //在指定位置查出元素
    //在index=1的位置插入一個物件
    list.add(1,"劉備");
    System.out.println(list);
    ArrayList list2 = new ArrayList();
    list2.add("sv");
    list2.add("sva");
    //在index=1的位置插入一個集合
    list.addAll(1,list2);
    //洗掉index=1的元素
    list.remove(1);
    System.out.println(list);
    //回傳元素"劉備"最后出現位置的索引
    System.out.println(list.lastIndexOf("劉備"));
    //在指定位置為元素賦值,相當于替換
    list.set(1,"svicen");
    //回傳指定索引范圍的集合 [1,3)
    List res = list.subList(1,3);
    
  • ArrayList的元素的值可以為null,ArrayList底層是用陣列來實作資料存盤的,它基本等同于Vector,除了ArrayList是線程不安全的(效率高),在多執行緒情況下,不建議使用ArrayLIst

  • ArrayList底層維護了一個Object型別的陣列elementData[],如果使用無參構造器創建ArrayList物件,初始elementData容量為0,第一次添加,其容量擴充為10,如需要再次擴容,則擴容為原來的1.5倍

  • 如果使用有參構造器,則初始容量為指定大小,此后再次擴容則擴大為1.5倍

  • 原始碼剖析

    //使用無參構造器創建物件時,DEFAULTCAPACITY_EMPTY_ELEMENTDATA為一個空陣列,并不給定初始大小
    public ArrayList() {
        this.elementData = https://www.cnblogs.com/SVicen/archive/2022/10/03/DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
    //我們在代碼中呼叫ArrayList的add方法,其實呼叫的是這個,然后這個add方法又呼叫了另一個多載后的add方法
    public boolean add(E e) {
        modCount++;     //modCount記錄了陣列的修改次數
        add(e, elementData, size); 
        return true;
    }
    //核心邏輯,判斷是否需要擴容的邏輯,不需要便直接賦值并讓size++
    private void add(E e, Object[] elementData, int s) {
        //如果s(即傳進來的size)與當前陣列的length相等,則說明再添加的話需要擴容
        if (s == elementData.length)
            elementData = grow();
        elementData[s] = e;
        size = s + 1;
    }
    //這里的無參函式grow又呼叫了多載后的grow函式
    private Object[] grow() {
        return grow(size + 1);
    }
    //陣列擴容,其實還是呼叫了陣列的copyof方法
    private Object[] grow(int minCapacity) {
        //這里的newCapacity決定了擴容后的大小
        return elementData = Arrays.copyOf(elementData,newCapacity(minCapacity)); 
    }
    //決定擴容是1.5倍還是10    比如說剛開始傳入的minCapacity=1,然后下一次擴容傳入11
    private int newCapacity(int minCapacity) { 
        // overflow-conscious code
        int oldCapacity = elementData.length; //第一次:0         第二次:10
        int newCapacity = oldCapacity + (oldCapacity >> 1); //第一次:0     第二次:15
        if (newCapacity - minCapacity <= 0) {  //第一次:0-1<0       第二次:15-11>0
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) //此時elementData還等于默認的空集合
                return Math.max(DEFAULT_CAPACITY, minCapacity);//DEFAULT_CAPACITY為10
            if (minCapacity < 0) // overflow
                throw new OutOfMemoryError();
            return minCapacity;
        }
        //MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8    
        //如果要擴容的大小超過了最大限制,呼叫hugeCapacity進行擴容,否則直接回傳newCapacity即可
        return (newCapacity - MAX_ARRAY_SIZE <= 0)
            ? newCapacity
            : hugeCapacity(minCapacity);
    }
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
    }
    //這里newLength即為10
    public static  T[] copyOf(T[] original, int newLength) { 
        return (T[]) copyOf(original, newLength, original.getClass());
    }
    //
    public static  T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        @SuppressWarnings("unchecked")
        //這里已經將copy陣列的長度設定為了10
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]  
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        //關鍵一步, 引數依次為原陣列,srcPos,拷貝后的陣列,srcPos,拷貝的長度
        //拷貝長度Math.min(original.length, newLength),為了防止原陣列很長,每次只拷貝一部分
        System.arraycopy(original, 0, copy, 0,Math.min(original.length, newLength));
        return copy;
    }
    
Vector
  • Vector底層也是一個物件陣列,protected Object[] elementData

  • Vector是執行緒同步的,即執行緒安全的,Vector類的方法帶有synchronized,但效率不如ArrayList

  • 使用無參構造器創建Vector物件時,直接會給定初始陣列大小為10,這里與ArrayList不同,之后每次擴容2倍

  • 原始碼剖析

    //自己寫的代碼
    Vector vector = new Vector();
    for (int i = 0; i < 10; i++) {
        vector.add(i);
    }
    //追進去的原始碼
    //呼叫無參構造器時,直接給了初始容量10
    public Vector() {
        this(10);
    }
    //之后進行add方法的操作,與上面幾乎完全一樣,唯一不同的函式為newCapacity,即確定擴容后大小的函式
    private int newCapacity(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;  //10
        //capacityIncrement為vector方法的屬性,為了提供一個api讓用戶自己指定擴容的增量
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ? //0>0不成立,newCapacity = 2*oldCapacity
                                         capacityIncrement : oldCapacity);
        if (newCapacity - minCapacity <= 0) {
            if (minCapacity < 0) // overflow
                throw new OutOfMemoryError();
            return minCapacity;
        }
        return (newCapacity - MAX_ARRAY_SIZE <= 0)
            ? newCapacity
            : hugeCapacity(minCapacity);
    }
    
    private static int hugeCapacity(int minCapacity) {
        if (minCapacity < 0) // overflow
            throw new OutOfMemoryError();
        return (minCapacity > MAX_ARRAY_SIZE) ?
            Integer.MAX_VALUE :
        MAX_ARRAY_SIZE;
    }
    
LinkedList
  • 雙向鏈表,存放了first和last兩個結點,每個節點有prev,next,item三個屬性,也是執行緒不安全的,
  • 原始碼剖析
//自己寫的代碼
LinkedList linkedList = new LinkedList();
linkedList.add(1);
linkedList.add(2);
//默認洗掉第一個結點
linkedList.remove();
//跟進去的原始碼
public boolean add(E e) {
    linkLast(e);
    return true;
}
//默認向鏈表的最后添加結點
void linkLast(E e) {
    final Node<E> l = last;
    //新結點的prev指向l,值item為e,next指向null
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    //如果l為空,說明原來鏈表里沒有元素,直接讓first也指向新添加的結點即可
    if (l == null)
        first = newNode;
    else//l不為空,則讓l.next指向新添加的結點
        l.next = newNode;
    size++;
    modCount++;
}

public E remove() {
    return removeFirst();
}
public E removeFirst() {
    final Node<E> f = first;
    //如果洗掉的是空結點,則會報例外
    if (f == null)
        throw new NoSuchElementException();
    return unlinkFirst(f);
}
//核心邏輯,將雙向鏈表的第一個結點洗掉
private E unlinkFirst(Node<E> f) {
    // assert f == first && f != null;
    final E element = f.item;  //取出內容
    final Node<E> next = f.next; //取出要刪去的結點的下一個結點
    f.item = null;
    f.next = null; // help GC  GC垃圾處理機制處理
    first = next;  //把first指向后移
    if (next == null)  //如果洗掉后鏈表為空,則讓last也為空
        last = null;
    else   //first指向的不為空,last不用修改,只需要則將next的prev指向設定為空
        next.prev = null;
    size--; //元素個數--
    modCount++; //修改次數++
    return element;
}
資料結構 底層結構 啟用版本 執行緒安全 擴容倍數
ArrayList 可變陣列 jdk1.2 不安全,增刪效率低,改查效率高 有參,按照1.5倍擴容;無參,第一次擴容10,之后按照1.5倍擴容
Vector 可變陣列 jdk1.0 安全,效率不如ArrayList 無參,默認10,然后按照2倍擴容,有參,按照2倍擴容
LinkedList 雙向鏈表 不安全,增刪效率高,改查效率低

Set??

Set介面物件 -- 即實作了set介面的類的物件

  • Set 不可以放重復的元素,且存放資料是無序的,取出的資料的順序不是添加時的順序,但也只會是這個順序

    HashSet set = new HashSet();
    set.add("vicen");
    set.add("jack");
    set.add(null);
    System.out.println(set);  // [null, vicen, jack]
    
  • 遍歷方式有兩種,增強for回圈和迭代器遍歷,不能用索引(因為set是無需的)

HashSet

  • 執行add方法時,添加成功則回傳true,失敗回傳false (null也不可以重復)

  • HashSet的底層就是HashMap 陣列+鏈表+紅黑樹

    public static void main(String[] args) {
        //模擬HashSet的底層
        //創建一個Node陣列
        Node[] table = new Node[16];
        System.out.println("table=" + table);
        //創建結點
        Node node = new Node("john",null);
        table[2] = node;
        Node node1 = new Node("svicen",null);
        node.next = node1;
    }
    class Node{
        Object item;
        Node next;
    
        public Node(Object item, Node next) {
            this.item = item;
            this.next = next;
        }
    }
    
  • HashSet的add方法的底層原始碼剖析

    • HashSet的底層就是HashMap,添加一個元素時,先得到hash值,再將hash值轉為索引值
    • 找到存盤資料表table,看這個索引位置是否已經存放元素 如果hash值相同就比較內容是否相同,內容不同就加到最后
    • 如果沒有則直接加入
    • 如果有,呼叫equals方法比較,如果相同就放棄添加,equals比較的內容可以自己定義,如果不相同就添加到最后一個Node的next
    • 在Java8中,如果一條鏈表的元素個數超過 TREEIFY_THRESHOLD(8) 并且table表的大小大于MIN_TREEIFY_CAPACITY(64) 就會進行優化(優化為紅黑樹)
    //自己寫的代碼
    public static void main(String[] args) {
        HashSet set = new HashSet();    //斷點
        set.add("java");
        set.add("php");
        set.add("java");
    }
    public HashSet() {
        map = new HashMap<>();
    }
    public boolean add(E e) {
        return map.put(e, PRESENT)==null; //第一次插入回傳true  因為map有key-value  而set只用了key
        //PRESENT為HashSet的一個私有屬性,起到占位作用,private static final Object PRESENT = new Object();
    }
    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true); //第一次add,回傳null
    }
    static final int hash(Object key) {  
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);  //得到key對應的hash值,并不是hashcode
    }
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;  // 這里resize回傳后  tab的長度就是16了
        //計算根據key值得到的hash去計算一個存放到table表的哪一個位置,并把這個位置的物件賦給p
        //判斷p是否為空
        //如果p為null,表示還沒有存放元素,直接創建一個Node放在tab[i]
        if ((p = tab[i = (n - 1) & hash]) == null) //i = (n - 1) & hash 求索引  n就是陣列容量 16
            //(n - 1) & hash  =  hash % n 位與運算可以實作和取模運算相同  而位運算效率更高
            tab[i] = newNode(hash, key, value, null);  //key就是傳進來的"java"   value永遠都是屬性  PRESENT
        //當添加的位置已經有元素時 走這里
        else {
            Node<K,V> e; K k;
            //關鍵代碼
            //要add的元素與 當前索引位置對應的鏈表的第一個物件p的hash相同 
    //并且滿足   加入的key和p指向的node的key是同一物件 或者 不是一個物件但是內容相同(由重寫的equals決定) key.equals(k)
            //就不能加入
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;  // 不能加入 把當前節點賦給e
            //判斷 p 是否是一棵紅黑樹   是的話呼叫putTreeVal進行添加
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); 
            //p指向的是一個鏈表  但還沒有優化為紅黑樹
            else {
                //依次對鏈表的元素進行比較  如果都不相同,就添加到最后
                for (int binCount = 0; ; ++binCount) {
                    //走到鏈表末尾
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null); //插入到最后 
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st  鏈表數量大于默認值后優化為紅黑樹
                            treeifyBin(tab, hash); // TREEIFY_THRESHOLD - 1 即 8 - 1
                        //在treeifyBin函式內部還有判斷   當tab.length >= MIN_TREEIFY_CAPACITY(64)時再優化 <則 resize
                        break;   //添加成功 退出回圈
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;  //有一個相同直接 break; 無法加入
                    p = e;   //指標后移 
                }
            }
            //這里表示add不成功,回傳oldValue
            if (e != null) { // existing mapping for key
                V oldValue = https://www.cnblogs.com/SVicen/archive/2022/10/03/e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;  //修改次數++
        if (++size > threshold)  //threshold為12,加載因子,防止到達容量上限再擴容
            resize();
        afterNodeInsertion(evict); //給HashMap的子類提供的方法
        return null;        //add成功,回傳null  
    }
    final Node[] resize() {
        Node[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;
        if (oldCap > 0) {
            if (oldCap >= MAXIMUM_CAPACITY) {
                threshold = Integer.MAX_VALUE;
                return oldTab;
            }
            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                     oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1; // double threshold
        }
        else if (oldThr > 0) // initial capacity was placed in threshold
            newCap = oldThr;
        else {               // zero initial threshold signifies using defaults
            newCap = DEFAULT_INITIAL_CAPACITY;  //默認的table表的大小16  1 << 4 之所以取16是為了降低hash碰撞的幾率
            // 0.75 * 16 加載因子0.75 用到12后開始準備擴容
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
        }
        if (newThr == 0) {
            float ft = (float)newCap * loadFactor;
            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                      (int)ft : Integer.MAX_VALUE);
        }
        threshold = newThr;
        @SuppressWarnings({"rawtypes","unchecked"})
        Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
        table = newTab;
        if (oldTab != null) {
            for (int j = 0; j < oldCap; ++j) {
                Node<K,V> e;
                if ((e = oldTab[j]) != null) {
                    oldTab[j] = null;
                    if (e.next == null)
                        newTab[e.hash & (newCap - 1)] = e;
                    else if (e instanceof TreeNode)
                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                    else { // preserve order
                        Node<K,V> loHead = null, loTail = null;
                        Node<K,V> hiHead = null, hiTail = null;
                        Node<K,V> next;
                        do {
                            next = e.next;
                            if ((e.hash & oldCap) == 0) {
                                if (loTail == null)
                                    loHead = e;
                                else
                                    loTail.next = e;
                                loTail = e;
                            }
                            else {
                                if (hiTail == null)
                                    hiHead = e;
                                else
                                    hiTail.next = e;
                                hiTail = e;
                            }
                        } while ((e = next) != null);
                        if (loTail != null) {
                            loTail.next = null;
                            newTab[j] = loHead;
                        }
                        if (hiTail != null) {
                            hiTail.next = null;
                            newTab[j + oldCap] = hiHead;
                        }
                    }
                }
            }
        }
        return newTab;
    }
    
    • HashSet底層是HashMap,第一次添加時,table陣列擴容為16,臨界值(threshold) 為16 * 加載因子(loadFactor 0.75) = 12
    • 如果table陣列大于臨界值12就會擴容到16*2=32,陣列大小包括(陣列元素和鏈表內元素)新的臨界值就是32 *0.75=24 依次類推
    • 在Java8中,如果一條鏈表的元素個數超過TREEIFY_THRESHOLD(8) 并且table表的大小大于MIN_TREEIFY_CAPACITY(64) 就會進行優化(優化為紅黑樹)
    • 如果已經一條鏈表的元素個數超過8,但table表大小還沒有超過64,再加入元素時會對table進行resize,擴容為2倍,直到table表大小超過64后優化為紅黑樹,將Node優化為TreeNode
作業例題??
public class Homework04 {
    public static void main(String[] args) {
        Person p1 = new Person(1001, "AA");
        Person p2 = new Person(1002, "BB");
        HashSet hashSet = new HashSet();
        hashSet.add(p1);
        hashSet.add(p2);
        System.out.println("洗掉前: " + hashSet);
        p1.setName("CC"); //這里修改了p1的屬性
        //因為上面修改了p1的name,所以洗掉時根據Person的id和name查找的索引不再是原來的p1的位置,無法將p1洗掉
        hashSet.remove(p1);
        System.out.println("洗掉后: " + hashSet); // 2個物件,沒有洗掉
        hashSet.add(new Person(1001,"CC"));
        System.out.println(hashSet);  // 3個物件
        hashSet.add(new Person(1001,"AA"));
        System.out.println(hashSet); //4個物件,因為上面p1的name已經修改,加入時比較時equals方法判斷不相等,可以加入
    }
}
/*
洗掉前: [Person{id=1002, name='BB'}, Person{id=1001, name='AA'}]
洗掉后: [Person{id=1002, name='BB'}, Person{id=1001, name='CC'}]
[Person{id=1002, name='BB'}, Person{id=1001, name='CC'}, Person{id=1001, name='CC'}]
[Person{id=1002, name='BB'}, Person{id=1001, name='CC'}, Person{id=1001, name='CC'}, Person{id=1001, name='AA'}]
*/
class Person{
    private int id;
    private String name;
    //注意這里需要重寫Person 的equals和hashCode方法
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return id == person.id && Objects.equals(name, person.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, name);
    }
}
LinkedHashSet
  • 實作了HashSet的子類,底層是LinkedHashMap,底層維護了一個陣列+雙寫鏈表

  • LinkedHashSet根據元素的hashcode值來決定元素的存盤位置,同時用鏈表維護元素的次序,使得元素看起來是以插入順序排序的

  • 每插入一個元素會將該元素的pre指向前一個結點,前一個結點的next指向插入的結點

  • 效率不入HashSet,但取資料時是有序的

    LinkedHashSet linkedHashSet = new LinkedHashSet();
    linkedHashSet.add(123);
    linkedHashSet.add("abc");
    linkedHashSet.add(456);
    linkedHashSet.add(123);
    System.out.println("linkedhashset =" + linkedHashSet);  // linkedhashset =[123, abc, 456]
    
  • 第一次添加時,直接將table擴容到16,存放的結點型別是LinkedHashMap$Entry,陣列型別是HashMap$Node[]

     //LinkedHashMap的靜態內部類Entry    繼承了HashMap的一個靜態內部類Node
    static class Entry<K,V> extends HashMap.Node<K,V> { 
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }
    

TreeSet

  • TreeSet底層還是TreeMap

  • TreeSet可以排序,利用自己定義的匿名內部類compartor,在add元素時,在TreeSet底層會呼叫自己定義的compartor方法

    TreeSet treeSet = new TreeSet(new Comparator() {
        @Override
        public int compare(Object o1, Object o2) {
            //下面呼叫String 的compareTo方法進行字串大小比較
            return ((String) o1).compareTo((String) o2);
        }
    });
    treeSet.add("tom");
    treeSet.add("jack");
    treeSet.add("abc");
    System.out.println(treeSet);
    
    //建構式內有匿名內部類時 
    //TreeSet接收了實作了comparator介面的物件  然后將其傳給了TreeMap
    public TreeSet(Comparator<? super E> comparator) {
        this(new TreeMap<>(comparator));
    }
    public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;  // 實作了comparator介面物件 賦給了TreeMap的一個屬性comparator
    }
    // put函式內的關鍵代碼   cpr其實就是我們實作的匿名內部類的compare方法
    //這里的put方法其實就是HashMap的put方法,此時的value值為TreeSet的靜態物件屬性  PRESENT
    public V put(K key, V value) {
        Entry<K,V> t = root;
        //第一次加入時 t為null
        if (t == null) {
            compare(key, key); // type (and possibly null) check
    
            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);  //根據compare方法決定添加元素的順序
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);  //當兩個元素cmp后為0時,即相等直接回傳0,key無法加入
            } while (t != null);
        }
        else {
            if (key == null)
                throw new NullPointerException();
            @SuppressWarnings("unchecked")
            Comparable<? super K> k = (Comparable<? super K>) key;
            do {
                parent = t;
                cmp = k.compareTo(t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    return t.setValue(value);
            } while (t != null);
        }
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (cmp < 0)
            parent.left = e;
        else
            parent.right = e;
        fixAfterInsertion(e);
        size++;
        modCount++;
        return null;
    }
    

TreeSet 與 HashSet比較

二者去重比較:

  • HashSet:hashCode()+equals(),當add元素時,通過運算得到一個hash值,然后根據hash值得到一個索引,如果該索引位置為空,則直接加入,如果有資料,則進行equals比較(遍歷比較),如果比較后結果為0表示相同,則不加入(equals方法具體比較什么由程式員自己決定)

  • TreeSet:如果傳入了一個Comparator匿名物件,就使用該匿名物件實作的compare方法去重,如果結果回傳0,就認為是相同的元素,就不添加,如果沒有傳入一個Comparator匿名物件,就以添加的物件實作的Comparable介面的compareTo方法去重

    如果添加的物件沒有實作Comparable介面,Comparable<? super K> k = (Comparable<? super K>) key 向上轉型時會報錯

HashSet LinkedHashSet TreeSet
按hash值定索引,取出與插入順序往往不同 有pre和next屬性,使得元素看起來是以插入順序排序的 可以自己指定排序的方法
底層其實就是HashMap 繼承了HashSet 底層其實就是TreeMap

Map

  • Map與Collection并列存在,用于保存具有映射關系的雙列資料型別:key-value
  • Map的key和value可以是任何參考型別的資料,會封裝到 HashMap$Node物件中
  • Map的key值不允許重復,放入已有key值的物件時會覆寫原來的,但是Map的value值可以重復
  • Map的key值可以為null,但也只能有一個為null,value可以有多個null

HashMap

  • HashMap沒有實作同步,因此是執行緒不安全的

    HashMap hashMap = new HashMap();
    hashMap.put("no1","張無忌");
    hashMap.put("no2","張三豐");
    hashMap.put(null,"123");
    hashMap.put(1,"數字");
    hashMap.put(new Object(),1);  //key可以為任何Object型別
    //get方法取出key為指定值的元素  回傳值是物件
    
    System.out.println(hashMap.get(1));
    
  • 一對key-value是放在一個HashMap$Node中的,因為Node實作了Entry介面,有些也說一對key-value就是一個Entry

    static class Node<K,V> implements Map.Entry<K,V>{
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
    }
    
  • k-v為了方便程式員的遍歷,還會創建一個entrySet集合,存放的型別為Entry,Set<Map.Entry<K,V>> entrySet

    注意這里的K和V并沒有重新創建,只是指向了HashMap$Node的K和V,定義的雖然是entry,但實際上型別還是Node

    Set set = hashMap.entrySet();
    System.out.println(set.getClass());   //HashMap$EntrySet
    for (Object obj : set) {
        System.out.println(obj.getClass()); //HashMap$Node
        Map.Entry entry = (Map.Entry) obj;  //向下轉型,呼叫子類方法
        System.out.println(entry.getKey()+ "-" + entry.getValue());  // 方便遍歷
    }
    //為了方便遍歷,Entry有以下方法
    interface Entry<K, V> {
        K getKey();
        V getValue();
        V setValue(V value);
    }
    

    Entry中存放的只是指向hashMap的table表的元素,再把Entry放在EntrySet集合里,

    //自動補全后如下,可知keySet的編譯型別為Set,而hashMap.values()的型別為Collection
    //Entry內的key放在keySet集合里,而value放在Collection里  也是指向HashMap$Node的元素
    Set set1 = hashMap.keySet();  //可以取出Enrty的所有的key,賦給Set集合物件
    Collection values = hashMap.values();
    

    image-20220501133231573

  • 方法

    //判斷鍵是否存在  containsKey
    System.out.println(hashMap.containsKey("no1"));
    //移除鍵值對關系,以key為索引
    hashMap.remove(1);
    //鍵值對個數  size
    System.out.println(hashMap.size());
    
    //三種遍歷方法
    //第一種,通過keySet
    Set keyset = hashMap.keySet();
    //增強for
    for (Object key :keyset) {
        System.out.println(key + "--" + hashMap.get(key));
    }
    //迭代器  只要繼承了Collection介面的都有iterator迭代器
    Iterator iterator = keyset.iterator();
    while (iterator.hasNext()) {
        Object key =  iterator.next();
        System.out.println(key + "--" + hashMap.get(key));
    }
    //第二種,通過values
    System.out.println("-----第二種------");
    Collection values = hashMap.values();
    //增強for
    for (Object value : values) {
        System.out.println(value);
    }
    //迭代器  只要繼承了Collection介面的都有iterator迭代器
    Iterator iterator1 = values.iterator();
    while (iterator1.hasNext()) {
        Object value = https://www.cnblogs.com/SVicen/archive/2022/10/03/iterator1.next();
        System.out.println(value);
    }
    //第三種,通過entrySet
    System.out.println("-----第三種-----");
    Set set1 = hashMap.entrySet();
    //增強for
    for (Object entry : set1) {
        Map.Entry me =  (Map.Entry) entry;
        System.out.println(me.getKey() + "--" + me.getValue());
    }
    //迭代器  只要繼承了Collection介面的都有iterator迭代器
    Iterator iterator2 = set1.iterator();
    while (iterator2.hasNext()) {
        Object m = iterator2.next();
        Map.Entry me =  (Map.Entry) m;
        System.out.println(me.getKey() + "--" + me.getValue());
    }
    

Hashtable

  • Hashtable的key和value都不可以為空

  • Hashtable是執行緒安全的,put方法有synchronize關鍵字,但效率沒有HashMap高

  • 底層有一個陣列Hashtable$Entry[] , Hashtable的內部類Entry,實作了Map.Entry介面,初始容量為11 臨界值11*0.75=8

  • 擴容:當陣列大小等于臨界值11*0.75 = 8時會擴容,if (count >= threshold) newCapacity = (Oldcapacity << 1) + 1

    擴容的大小為 兩倍+1

??以上所有擴容機制都是先檢查是否達到threshold然后再將要加入的元素加進table表,然后count++

由于每次判斷的都是count >= threshold 所以在加入第八個元素時count還為7,不會擴容

Properties

  • 繼承了HashTable類,并且實作了Map介面,也是使用鍵值對形式來保存資料
  • Properties還可以從xxx.properties檔案中,加載資料到Properties物件并進行讀取和修改
  • 一般情況下,xxx.properties檔案往往為組態檔,

TreeMap

  • TreeMap內有屬性compartor,可以按照自己指定的規則而進行排序

    TreeMap treeMap = new TreeMap(new Comparator() {
        @Override
        public int compare(Object o1, Object o2) {
            //按照傳入的key的大小進行排序
            return ((String) o2).compareTo((String) o1);
        }
    });
    treeMap.put("jack","杰克");
    treeMap.put("smith","史密斯");
    treeMap.put("tom","湯姆");
    System.out.println(treeMap);
    }
    
    //構造器處下斷點,原始碼如下
    public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }
    public V put(K key, V value) {
        Entry<K,V> t = root;
        //第一次添加元素時下面這個判斷為true  最侄訓傳null   第二次添加時root即為第一次添加的元素
        if (t == null) {
            compare(key, key); // type (and possibly null) check
    
            root = new Entry<>(key, value, null);
            size = 1;
            modCount++;
            return null;
        }
        int cmp;
        Entry<K,V> parent;
        // split comparator and comparable paths
        Comparator<? super K> cpr = comparator;
        if (cpr != null) {
            do {  //遍歷所有的key,找當前的key應該添加的位置
                parent = t;
                //動態系結到我們的匿名內部類的compare
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else
                    //如果找到相等的,則把當前Entry的value設定為新添加的元素的value值,但key值不會改變
                    return t.setValue(value);
            } while (t != null);
        }
    

Collection工具類

  • 是一個操作集合型別的工具類,內置了很多對List,Set,Map的操作的靜態方法,reverse,shuffle(隨機排序),sort,swap,max
ArrayList arrayList = new ArrayList();
arrayList.add("tom");
arrayList.add("smith");
arrayList.add("king");
arrayList.add("milan");
System.out.println(arrayList);
Collections.reverse(arrayList);
System.out.println(arrayList);
Collections.sort(arrayList);
System.out.println(arrayList);
//自己指定排序方式
Collections.sort(arrayList, new Comparator() {
    @Override
    public int compare(Object o1, Object o2) {
        //校驗代碼
        if (o1 instanceof String && o2 instanceof String) {
            return ((String) o1).length() - ((String) o2).length();
        }
        return 0;
    }
});
System.out.println(arrayList);
// 交換指定位置元素
Collections.swap(arrayList,1,2);
//求指定排序方式的最大元素
System.out.println(Collections.max(arrayList));
System.out.println(Collections.max(arrayList, new Comparator() {
    @Override
    public int compare(Object o1, Object o2) {
        return ((String)o1).length() - ((String)o2).length();
    }
}));
//查找出現次數
System.out.println("tom的次數" + Collections.frequency(arrayList,"tom"));
//拷貝
ArrayList dest = new ArrayList();
for (int i = 0; i < arrayList.size(); i++) {
    dest.add(i);
}
Collections.copy(dest,arrayList);  //這里需要先把dest的陣列大小設定為與原來的arrayList大小相同,不然會拋出陣列越界例外
System.out.println(dest);
//替換
Collections.replaceAll(arrayList,"tom","湯姆");

泛型

  • 泛型又稱引數化型別,是jdk5后出現的新特性,用于解決資料型別的安全性問題
  • 在類宣告或實體化時只要指定好需要的具體的型別即可,如果沒有指定具體的資料型別默認是Object
  • 泛型的作用是:可以在類宣告時通過一個標識表示類中某個屬性的型別,或者是某個方法的回傳值的型別,或者是引數型別
  • 泛型可以保證如果在編譯時沒有錯誤,那么在運行時就不會出現型別轉換例外(ClassCastException)
//泛型入門,指定泛型的具體資料型別時 只能用參考型別,不能用基本型別
Person<String, Integer> hs = new Person<>("hs", 123);   //建議這樣寫
//Person<String, Integer> hs = new Person<String, Integer>("hs", 123);  后邊的<>里型別可以省略,且推薦省略
System.out.println(hs);
HashMap<String, Person> stringPersonHashMap = new HashMap<>();
//這樣也可以 HashMap<String, Person<String,Integer>> stringPersonHashMap = new HashMap<>();
stringPersonHashMap.put("jack",new Person("jak",123));
System.out.println(stringPersonHashMap);
Person person = new Person("123", 123);  //創建Person實體化物件時也可以不顯示給出變數型別宣告,直接用特定變數填充E、T即可
class Person<E,T>{
    private E name;
    private T age;
}
@SuppressWarnings({"all"})
public class GenericExercise01 {
    public static void main(String[] args) {
        ArrayList<Employee> employees = new ArrayList<>();
        employees.add(new Employee("jack",30000,new MyDate(2000,6,19)));
        employees.add(new Employee("tom",10000,new MyDate(2001,6,15)));
        employees.add(new Employee("tom",20000,new MyDate(2001,5,17)));
        System.out.println(employees);
        //下面是對birthday的比較,最好將其放在MyDate類完成
        employees.sort(new Comparator<Employee>() {
            @Override
            public int compare(Employee emp1, Employee emp2) {
                //先對傳入的引數進行驗證
                if (!(emp1 instanceof Employee && emp2 instanceof Employee)) {
                    System.out.println("型別不正確");
                    return 0;
                }
                //先按名字排序,如果名字相同就比較出生日期
                int i = emp1.getName().compareTo(emp2.getName());
                if (i != 0){
                    return i;
                }
                //為了提高程式的可讀性和封裝性,比較的具體操作再MyDate類中實作
                return emp1.getBirthday().compareTo(emp2.getBirthday());
            }
        });
        System.out.println("=====排序后======");
        System.out.println(employees);
    }
}
//繼承Comparable介面,傳入引數型別為MyDate
class MyDate implements Comparable<MyDate>{
    private int year;
    private int month;
    private int day;

    @Override
    public int compareTo(MyDate o) {
        //如果 name 相同,比較birthday,依次比較年、月、日
        int yearMinus = year - o.getYear();
        if (yearMinus != 0){
            return yearMinus;
        }
        int monthMinus = month - o.getMonth();
        if (monthMinus != 0){
            return monthMinus;
        }
        return day - o.getDay();
    }
}
class Employee{
    private String name;
    private double sal;
    private MyDate birthday;
}

自定義泛型

  • 普通成員可以使用泛型(屬性,方法)
  • 使用泛型的陣列不能初始化,在泛型類內不可以初始化
  • 靜態方法中不能使用類的泛型,有可能加載類時物件仍未創建,但具體的型別在創建物件時才會指定
  • 泛型類的型別,是在創建物件的時候確定的(創建物件時,需指定型別,如果沒有默認是Object)
  • 泛型識別符號:T、R、M、E ...

自定義介面

  • 在介面中,靜態成員也不能使用泛型,與自定義泛型一樣
  • 泛型介面的型別,在繼承介面或實作介面的時候確定
  • 沒有指定型別,默認是Object
interface IA extends IUsb<String,Double>{

}
//這里繼承了IA,也就相當于指定了IUsb的泛型的型別,在實作IUsb方法時,使用String替換U,Double替換R
//如果直接繼承了IUsb,則需要自己在繼承的時候或者實作介面方法的時候指定型別
class AA implements IA{
    @Override
    public Double get(String s) {
        return null;
    }
    @Override
    public void hi(Double aDouble) {

    }
    @Override
    public void run(Double r1, Double r2, String u1, String u2) {

    }
}
interface IUsb<U,R>{
    //U u;  錯誤,介面的變數隱式的指定為 public static final   靜態成員不能使用泛型
    int n = 10;  //這是正確的
    //普通方法中,可以使用介面泛型
    R get(U u);
    void hi(R r);
    void run(R r1,R r2,U u1, U u2);
    //可以在介面中使用默認方法,默認方法也是可以使用泛型的
    default R method(U u) {
        return null;
    }
}

泛型方法

  • 泛型方法可以定義在泛型類里,也可以定義在普通類里

    public class CustomMethodGeneric {
        public static void main(String[] args) {
            Car car = new Car();
            car.fly("寶馬",123);
            Fish<String, ArrayList> stringArrayListFish = new Fish<>();
            stringArrayListFish.hello("jack",1.23f); //使用泛型方法時第一個引數的具體型別已經確定了,自己指定第二個型別
        }
    }
    class Car{
        //普通方法
        public void run(){
        }
        //泛型方法
        public <T,R> void fly(T t,R r) {
        }
    }
    class Fish<T,R>{
        //普通方法
        public void run(){
        }
        //泛型方法,泛型識別符號最好與類的泛型識別符號區分開
        public <M,U> void eat(M m, U u){ 
        }
        //這個方法并不是泛型方法,定義的前面并沒有<>指定泛型識別符號,而是方法使用了泛型
        public void hi(T t){
        }
         //泛型方法可以使用類宣告的泛型,也可使用自己宣告的泛型
        public <K> void hello(T t, K K){
        }
    }
    

泛型繼承與通配符

  • 泛型沒有繼承性

    List <Object> list = new ArrayList<String> ();  //錯誤,泛型不具有繼承性
    
  • 通配符

    • :支持任意泛型型別
    • :支持A類以及A類的子類,規定了泛型的上限A
    • :支持A類以及A類的父類,不限于直接父類,規定了泛型的下限A
//可以接收任意泛型型別
public static void printCollection(List<?> c){
    for (Object o :c) {
        System.out.println(o);
    }
}

JUnit使用

  • 一個類中有很多功能代碼需要測驗,為了測驗就需要寫到main方法中
  • 如果有多個功能代碼測驗,就需要來回注銷,切換很麻煩
  • 如果可以直接運行一個方法,就方便很多,并且可以給出相關資訊,
  • java提供了一個類JUnit,是一個Java語言的單元測驗框架
public class Junit_ {
    public static void main(String[] args) {
    }
    @Test  //輸入@Test后 alt + enter 添加 JUnit5.8  之后會引入包  之后就可以直接運行該方法了,也可以使用快捷鍵
    public void m1(){ 
        System.out.println("m1被呼叫");
    }
    @Test
    public void m2(){
        System.out.println("m2被呼叫");
    }
}
  • 作業題
@SuppressWarnings({"all"})
public class Homework01 {
    public static void main(String[] args) {

    }
    @Test
    public void testList(){
        //這里給T的指定型別為User
        DAO<User> dao = new DAO<>();
        dao.save("001",new User(1,18,"jack"));
        dao.save("002",new User(2,28,"rom"));
        dao.save("003",new User(3,38,"smith"));
        //拿到回傳的所有物件
        List<User> list = dao.list();
        System.out.println("list=" + list);
        //修改物件
        dao.update("003",new User(15,55,"milan"));
        //獲取修改后的所有物件
        list = dao.list();
        System.out.println(list);
        System.out.println(dao.get("003"));
    }
}
@SuppressWarnings({"all"})
class DAO<T>{
    private Map<String,T> map = new HashMap<>();
    //存放元素
    public void save(String id, T entity) {
        map.put(id,entity);
    }
    public T get(String id){
        return map.get(id);
    }
    //更改物件value
    public void update(String id,T entity) {
        map.put(id,entity);
    }
    //回傳map里的物件
    public List<T> list(){
        //遍歷map的key,找到所有的value,然后封裝到ArrayList回傳即可
        List<T> list = new ArrayList<>();
        //遍歷map
        Set<String> keySet = map.keySet();
        for (String key : keySet) {
            list.add(get(key));
            //list.add(map.get(key)); 這樣也可以
        }
        return list;
    }
}
class User{
    private int id;
    private int age;
    private String name;
}

繪圖

package com.svicen.draw_;
import javax.swing.*;
import java.awt.*;
//要想繪圖,需要繼承JFrame  在視窗內嵌入面板
public class DrawCircle extends JFrame{
    //定義一個面板
    private MyPanel mp = null;
    public static void main(String[] args) {
        new DrawCircle();
    }
    public DrawCircle(){
        //初始化面板
        mp = new MyPanel();
        //面板放入到視窗(就是畫框)
        this.add(mp);
        this.setSize(400,300);  //400*300 像素
        //當關閉彈出的組件后 jvm并不會釋放這個JFrame物件  在這里設定當點擊組件的關閉x后程式完全退出
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.setVisible(true);  //設定可以顯示
    }
}
//先定義一個MyPanel,繼承java的JPanel類  在畫板上畫圖形
class MyPanel extends JPanel {
    //Graphics 可以認為是一個畫筆  Panel是一個面板
    @Override
    /*   paint方法呼叫有四種情況
    1.當組件第一次在螢屏顯示時,系統自動呼叫  
    2.視窗最小化后再最大化
    3.視窗的大小發生變化
    4.repaint函式被呼叫
    */
    public void paint(Graphics g) {
        super.paint(g); //呼叫父類的方法完成初始化
        //畫一個圓形 四個引數 x,y,width,height
        g.drawOval(10,10,100,100); 
    }
}
  • 繪圖方法
//畫一個圓形 四個引數 x,y,width,height  x y為圓的邊界的坐標
g.drawOval(10,10,100,100);
//畫直線  四個引數 x1 x2 y1 y2
g.drawLine(10,10,100,100);
//繪制矩形邊框  四個引數 x y width height
g.drawRect(10,10,100,100);
//繪制填充矩形,先指定顏色
g.setColor(Color.blue);
g.fillRect(10,10,100,100);
//繪制填充圓形,可以使橢圓
g.fillOval(10,10,100,100);
//獲取圖片資源  這里getResource在jdk9之后做了修改
Image image = Toolkit.getDefaultToolkit().getImage(MyPanel.class.getResource("/1.png"));
g.drawImage(image,10,10,28,28,this);
//繪制字串,即寫字
g.setColor(Color.red);
g.setFont(new Font("隸書",Font.BOLD,50)); //字體,是否加粗,字體大小
g.drawString("你好",100,100); //字體的左下角坐標
  • Java事件處理

    java事件處理是采取"委派事件模型",當事件發生時,會把此資訊傳遞給事件的監聽者處理,這里所說的資訊實際就是java.awt.event事件類別庫的某個類所創建的物件,把它稱為事件的物件

public class BallMove extends JFrame {
    MyPanel mp = null;

    public static void main(String[] args) {
        new BallMove();
    }
    public BallMove(){
        mp = new MyPanel();
        this.add(mp);
        this.setSize(400,300);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        //使視窗的的JFrame物件可以監聽到面板上發生的鍵盤時間   這里必須要add進去
        this.addKeyListener(mp);
        this.setVisible(true);
    }
}
//implements KeyListener  實作監聽鍵盤的介面
class MyPanel extends JPanel implements KeyListener {
    //為了讓小球可以移動,我們需要把小球左上角的坐標設定為變數
    int x = 10;
    int y = 10;
    @Override
    public void paint(Graphics g) {
        super.paint(g);
        g.fillOval(x,y,20,20);
    }
    //當某個鍵按下時該方法觸發
    @Override
    public void keyPressed(KeyEvent e) {
        //System.out.println((char) e.getKeyCode() + "被按下");
        if(e.getKeyCode() == KeyEvent.VK_DOWN) {
            //向下的鍵被按下
            y++;
        } else if (e.getKeyCode() == KeyEvent.VK_UP) {
            y--;
        } else if (e.getKeyCode() == KeyEvent.VK_LEFT) {
            x--;
        } else if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
            x++;
        }
        //改變后需要讓面板重繪
        this.repaint();
    }
}

詳情見坦克大戰代碼

執行緒

image-20220505100344887

執行緒使用

繼承Thread類
  • 執行緒類需要重寫run方法,Thread類并沒有run方法,Thread類實作了Runnable的run介面

    //Thread類的原始碼
    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }
    //自己寫的代碼
    public class Thread01 {
        public static void main(String[] args) throws InterruptedException {
            Cat cat = new Cat();
            cat.start();    //呼叫cat執行緒類的start方法后,它會自動呼叫自己的run方法
    //如果直接呼叫cat的run方法,則不會有多執行緒,相當于用main執行緒把run方法執行完后再接著執行main函式的內容,會阻塞
            for (int i = 0; i < 10; i++) {
                System.out.println("main " + i);
                //讓主執行緒休眠
                Thread.sleep(1000);
            }
        }
    }
    //當一個類繼承了Thread類,該類就可以當成執行緒類使用
    class Cat extends Thread{
        int times = 0;
        @Override
        public void run() {  //重寫run方法,寫自己的業務邏輯
            while(true) {
                System.out.println("喵喵" + (++times));
                try {
                    //執行緒休眠1秒
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                if(times == 5) {
                    break;
                }
            }
        }
    }
    
    • 行程啟動后首先會開啟一個main執行緒,main執行緒又 start 一個子執行緒Thread-0,主執行緒并不會阻塞,多CPU并行,單CPU并行

    • 可以使用JConsole把執行緒執行情況圖形化展示出來,終端輸入jconsole

      image-20220505124804687

    • 主執行緒main結束并不代表程式已經結束了,如果子執行緒比主執行緒執行的時間長也照樣可以執行

    • 同樣,子執行緒也可以再開子執行緒,只有所有的子執行緒全部執行完成后,行程才會結束

    //start底層原始碼
    public synchronized void start() {
            /**
             * This method is not invoked for the main method thread or "system"
             * group threads created/set up by the VM. Any new functionality added
             * to this method in the future may have to also be added to the VM.
             *
             * A zero status value corresponds to state "NEW".
             */
        if (threadStatus != 0)
            throw new IllegalThreadStateException();
    
        /* Notify the group that this thread is about to be started
             * so that it can be added to the group's list of threads
             * and the group's unstarted count can be decremented. */
        group.add(this);
    
        boolean started = false;
        try {
            start0();  //start0由JVM機呼叫,由c/c++撰寫, 關鍵,執行緒同步都是靠這個方法
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
                /* do nothing. If start0 threw a Throwable then
                      it will be passed up the call stack */
            }
        }
    
    • 底層的start0()方法才是真正實作多執行緒的函式,所以要通過呼叫start方法而不是直接呼叫run方法
實作介面Runnable
  • 主要用于對繼承了其他類的類實作執行緒,因為Java只能單繼承
  • 實作介面的執行緒類沒有start方法,只能通過創建執行緒物件來呼叫start
public class Thread02 {
    public static void main(String[] args) {
        Dog dog = new Dog();
        //dog.start();   由于實作介面的執行緒類沒有start方法
        Thread thread = new Thread(dog);
        thread.start();
    }
}
class Dog implements Runnable{
    int count = 0;
    @Override
    public void run() {
        while(true){
            System.out.println("小狗汪汪叫" + (++count) + Thread.currentThread().getName());
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            if(count == 10){
                break;
            }
        }
    }
}
  • 為什么呼叫Thread類的物件的start方法可以呼叫dog類的run方法呢,底層使用了一種設計模式:代理模式(靜態代理)

  • 代碼模擬實作Thread類的多執行緒實作程序

    public class Thread02 {
        public static void main(String[] args) {
            Tiger tiger = new Tiger();
            Thread thread = new Thread(tiger);
            thread.start();
        }
    }
    class Animal{}
    class Tiger extends Animal implements Runnable{
        int count = 0;
        @Override
        public void run() {
            while (true){
                System.out.println("老虎嗷嗷叫" + (++count) + Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                if(count == 10){
                    break;
                }
            }
        }
    }
    
  • 實作Runnable介面的方式更加適合多個執行緒共享一個資源的情況,并且避免了單繼承的限制,建議使用

通知執行緒退出

public class ThreadExit {
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T();
        t1.start();
        //如果希望main執行緒去控制t1執行緒的終止,可以修改loop為false 讓t1退出run方法,從而終止t1執行緒
        //主執行緒休眠5秒,退出t1執行緒
        Thread.sleep(5000);
        t1.setLoop(false);
    }
}
class T extends Thread{
    private int count = 0;
    private boolean loop = true;

    @Override
    public void run() {
        while(loop){
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("T 運行中" + (++count));
        }
    }

    public void setLoop(boolean loop) {
        this.loop = loop;
    }
}

執行緒方法

setName 設定執行緒名稱,使之與引數name相同

getName 回傳該執行緒的名稱

start 使該執行緒開始執行;Java虛擬機底層呼叫該執行緒的start0方法

run 呼叫執行緒物件run方法;

setPriority 更改執行緒的優先級

getPriority 獲取執行緒的優先級

sleep 在指定的毫秒數內讓當前正在執行的執行緒休眠(暫停執行)

interrupt 中斷執行緒,而不是終止執行緒,如果正在休眠則提前終止休眠

public static void static yield() 暫停當前正在執行的執行緒物件,并執行其他執行緒,禮讓行程,但禮讓的時間不確定,不一定禮讓成功

public final void setDaemon(boolean on) 將該執行緒標記為守護執行緒或用戶執行緒,

public final void join(long millisec) 等待該執行緒終止的時間最長為 millis 毫秒,插入的執行緒一旦插隊成功,則肯定先執行完插入的執行緒所有的任務

public static void sleep(long millisec) 在指定的毫秒數內讓當前正在執行的執行緒休眠(暫停執行),此操作受到系統計時器和調度程式精度和準確性的影響,

public static Thread currentThread() 回傳對當前正在執行的執行緒物件的參考,

public class ThreadMethod01 {
    public static void main(String[] args) throws InterruptedException {
        //測驗相關的方法
        T t = new T();
        t.setName("jack");
        t.setPriority(Thread.MIN_PRIORITY);//設定優先級為 1 最小優先級
        t.start();//啟動子執行緒
        //主執行緒列印5次hi ,然后就中斷子執行緒的休眠
        for(int i = 0; i < 5; i++) {
            Thread.sleep(1000);
            System.out.println("hi " + i);
        }
        System.out.println(t.getName() + " 執行緒的優先級 =" + t.getPriority());//1
        t.interrupt();//當執行到這里,就會中斷 t執行緒的休眠.  提前結束休眠
    }
}

class T extends Thread { //自定義的執行緒類
    @Override
    public void run() {
        while (true) {
            for (int i = 0; i < 10; i++) {
                //Thread.currentThread().getName() 獲取當前執行緒的名稱
                System.out.println(Thread.currentThread().getName() + " 吃東西~~~" + i);
            }
            try {
                System.out.println(Thread.currentThread().getName() + " 休眠中~~~");
                Thread.sleep(20000);    //20秒
            } catch (InterruptedException e) {
                //當該執行緒執行到一個interrupt 方法時,就會catch 一個 例外, 可以在這里加入自己的業務代碼
                //InterruptedException 是捕獲到一個中斷例外.
                System.out.println(Thread.currentThread().getName() + "被 interrupt了");
            }
        }
    }
}
  • 執行緒插隊與禮讓
public class ThreadMethod02 {
    public static void main(String[] args) throws InterruptedException {
        T3 t3 = new T3();
        t3.start();
        for(int i = 1; i <= 20; i++) {
            Thread.sleep(1000);
            System.out.println("主執行緒(小弟) 吃了 " + i  + " 包子");
            if(i == 5) {
                System.out.println("主執行緒(小弟) 讓 子執行緒(老大) 先吃");
                //join, 執行緒插隊
                t3.join();// 這里相當于讓t2 執行緒先執行完畢  呼叫的是對方的join方法,join到自己的執行緒里
               //Thread.yield();//禮讓,不一定成功 如果內核資源不緊張,可以滿足兩個執行緒  那就不會禮讓
                System.out.println("子執行緒(老大) 吃完了 主執行緒(小弟) 接著吃..");
            }
        }
    }
}
class T3 extends Thread {
    @Override
    public void run() {
        for (int i = 1; i <= 20; i++) {
            try {
                Thread.sleep(1000);//休眠1秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("子執行緒(老大) 吃了 " + i +  " 包子");
        }
    }
}

用戶執行緒:也叫作業執行緒,當執行緒的任務執行完畢或以通知方式結束(即上面的退出方式)

守護執行緒:一般是為作業執行緒服務的,當所有的用戶執行緒結束,守護執行緒自動結束,一般用于監視某執行緒

常見的守護執行緒:垃圾回識訓制,只要還有執行緒在作業,垃圾回識訓制就一直會守護,

執行緒七大狀態

JDK 中用 Thread.State 列舉表示了執行緒的六種狀態

在這里插入圖片描述

執行緒狀態轉換圖(RUNNABLE又可以細分為兩種狀態(Ready和Running))

Thread_state

使用程式查看執行緒狀態
創建 T 執行緒,然后輸出此時的狀態,再啟動執行緒,利用回圈,查看執行緒狀態,只要執行緒沒終止,就會不停的輸出狀態

public class Thread_State {
    public static void main(String[] args) throws InterruptedException {
        T4 t4 = new T4();
        System.out.println(t4.getName() + " 狀態 " + t4.getState()) ;
        t4.start();
        while(t4.getState() != Thread.State.TERMINATED) {
            System.out.println(t4.getName() + " 狀態 " + t4.getState());
            Thread.sleep(1000);
        }
        System.out.println(t4.getName() + " 狀態 " + t4.getState());
    }
}
class T4 extends Thread{
    @Override
    public void run() {
        while(true){
            for (int i = 0; i < 10; i++) {
                System.out.println("hi  " + i);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            break;
        }
    }
}

執行緒同步

同步具體方法:Synchronized

public class SellTickets {
    public static void main(String[] args) {
        SellTicket02 sellTicket02 = new SellTicket02();
        //這里會出現票數超賣現象,需要進行執行緒互斥的限制
        //同一個物件開啟三個執行緒
        new Thread(sellTicket02).start();
        new Thread(sellTicket02).start();
        new Thread(sellTicket02).start();
    }
}
//使用同步方法Synchronized實作執行緒同步
class SellTicket02 implements Runnable{
    private static int ticketNum = 100;
    private boolean loop = true;
    //這里就是一個同步方法,鎖的物件為  this物件 ,也可以用同步代碼塊實作
    public synchronized void sell(){
        if(ticketNum <= 0) {
            System.out.println("票已售空");
            loop = false;
            return;
        }
        try {
            Thread.sleep(50);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        System.out.println("視窗" + Thread.currentThread().getName() + "售出一張票" +
                "  剩余票數" + (--ticketNum));
    }
    public /*synchronized*/ void sell(){
        synchronized (this) {
            if (ticketNum <= 0) {
                System.out.println("票已售空");
                loop = false;
                return;
            }
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            System.out.println("視窗" + Thread.currentThread().getName() + "售出一張票" +
                    "  剩余票數" + (--ticketNum));
        }
    }
    //靜態方法實作同步代碼塊,不能用于鎖this,只能鎖  類名.class
    public static void m(){
        synchronized (/*this*/SellTicket02.class){
            System.out.println("靜態方法鎖");
        }
    }
    @Override
    public void run() {
        while (loop) {
            sell();
        }
    }
}
互斥鎖
  • 一般選擇同步代碼塊(上鎖的代碼量盡量小)
  • 要求多個執行緒的鎖物件必須為同一個,如果用extends Thread實作的執行緒類,需要創建多個物件,不滿足互斥鎖
  • Java語言中,引入了物件互斥鎖的概念,來保證共享資料操作的完整性
  • 每個物件都對應一個可稱為互斥鎖的標記,這個標記用來保證在任意時刻,只能有一個執行緒訪問該物件
  • 關鍵字synchronized來與物件的互斥鎖聯系,當某個物件用synchronized修飾時,表明該物件在任一時刻只能由一個執行緒訪問
  • 同步的局限性:導致程式的執行效率降低
  • 同步方法(非靜態)的鎖可以是this,也可以是其他物件(要求是同一個物件):比如說Object等父類
  • 同步方法(靜態)的鎖為當前類本身,(因為靜態方法可以通過類直接呼叫)
  • this物件鎖是非公平鎖

執行緒死鎖

多個執行緒都占用了對方的鎖資源,但不肯相認,導致了死鎖

釋放鎖

以下情況會釋放鎖

  • 同步代碼塊執行結束
  • 當前執行緒在同步代碼塊、同步方法中遇到break,return
  • 當前執行緒在同步代碼塊、同步方法中出現了未處理的Error或Exception導致例外結束
  • 當前執行緒同步代碼塊、同步方法中執行了執行緒的wait()方法,當前執行緒暫停,并釋放鎖

以下情況不會釋放鎖

  • 當前執行緒在同步代碼塊、同步方法中呼叫sleep方法和yield方法不會釋放鎖
  • 當前執行緒執行同步代碼塊、同步方法時,其他執行緒呼叫了該執行緒的suspend方法將該執行緒掛起(由Running到Ready狀態),該執行緒不會釋放鎖, suspend和resume兩個方法已經不再使用

IO流

檔案的創建

  • createNewFile()方法來創建一個新檔案
//方式一  直接寫明檔案路徑 public File(String pathname)
@Test
public void create01(){
    String filePath = "D:\\JetBrains\\hello.txt";
    File file = new File(filePath); //只是創建了一個file物件但是還沒有創建具體的檔案
    try {
        file.createNewFile();
        System.out.println("hello1檔案創建成功");
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}
@Test
//方式二  通過父目錄檔案 + 子路徑 public File(File parent, String child)
public void create02(){
    File parentfile = new File("D:\\JetBrains\\");
    String fileName = "hello2.txt";
    File file = new File(parentfile, fileName);
    try {
        file.createNewFile();
        System.out.println("hello2創建成功");
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}
@Test
//方式三 通過父目錄 + 子檔案路徑   public File(String parent, String child) 用于在一個父目錄下創建多個子檔案
public void create03(){
    String parentPath = "D:\\JetBrains\\";
    String fileName = "hello3.txt";
    File file = new File(parentPath, fileName);
    try {
        file.createNewFile();
        System.out.println("hello3創建成功");
    } catch (IOException e) {
        throw new RuntimeException(e);
    }
}

常用的檔案操作

方法名稱 說明
public String getName() 回傳由此抽象路徑名表示的檔案或檔案夾的名稱
public booleean isFile() 測驗此抽象路徑名表示的File是否為檔案
public boolean isDirectory() 測驗此抽象路徑名表示的File是否為檔案夾
public String getAbsolutePath() 回傳此抽象路徑名的絕對路徑名字串
public String getPath() 將此抽象路徑名轉換為路徑名字串
public boolean exists() 測驗此抽象路徑名表示的File是否存在
public long lastModified() 回傳檔案最后修改的時間毫秒值
public long length() 回傳檔案大小,utf-8中英文字母1個位元組,漢字3個位元組
public File[] listFiles()(常用) 獲取當前目錄下所有的"一級檔案物件"到一個檔案物件陣列中去回傳
public boolean mkdir() 只能創建一級檔案夾
public boolean mkdirs() 可以創建多級檔案夾
public boolean delete() 洗掉由此抽象路徑名表示的檔案或空檔案夾,默認不能洗掉非空檔案夾
//判斷檔案是否存在,存在則洗掉
@Test
public void m(){
    String filePath = "D:\\JetBrains\\hello2.txt";
    File file = new File(filePath);
    if (file.exists()){
        if (file.delete()){  //洗掉成功回傳true
            System.out.println(filePath + "洗掉成功");
        } else {
            System.out.println(filePath + "洗掉失敗");
        }
    } else {
        System.out.println("該檔案不存在");
    }
}
//判斷目錄是否存在,存在則洗掉
@Test
public void m2(){
    String filePath = "D:\\JetBrains\\test";
    File file = new File(filePath);
    if (file.exists()){
        if (file.delete()){
            System.out.println(filePath + "洗掉成功");
        } else {
            System.out.println(filePath + "洗掉失敗");
        }
    } else {
        System.out.println("該目錄不存在...");
    }
}
//判斷目錄是否存在,不存在就創建該目錄
@Test
public void m3(){
    String dirPath = "D:\\JetBrains\\test";
    File file = new File(dirPath);
    if (file.exists()){
        System.out.println("該目錄存在");
    } else {
        if (file.mkdirs()) {  //創建多級目錄使用mkdirs,創建一級目錄使用mkdir
            System.out.println("目錄創建成功");
        } else {
            System.out.println("目錄創建失敗");
        }
    }
}

I/O流原理和分類

流與檔案:檔案通過輸入流讀入到程式(記憶體)中,程式輸出的結果通過輸出流寫入到檔案里??

  • 按操作資料單位不同分為:位元組流(8 bit),字符流(16 bit)
  • 按資料流的流向不同分為:輸入流,輸出流
  • 按流的角色的不同分為:節點流,處理流

圖片說明

image-20220531102006140

位元組流

FileInputStream

image-20220530174632645

該流用于從檔案讀取資料,它的物件可以用關鍵字 new 來創建,

可以使用字串型別的檔案名來創建一個輸入流物件來讀取檔案:

InputStream f = new FileInputStream("C:/java/hello");

也可以使用一個檔案物件來創建一個輸入流物件來讀取檔案,我們首先得使用 File() 方法來創建一個檔案物件:

File f = new File("C:/java/hello");
InputStream out = new FileInputStream(f);

創建了InputStream物件,就可以使用下面的方法來讀取流或者進行其他的流操作,

序號 方法及描述
1 public void close() throws IOException{} 關閉此檔案輸入流并釋放與此流有關的所有系統資源,拋出IOException例外,
2 protected void finalize()throws IOException {} 這個方法清除與該檔案的連接,確保在不再參考檔案輸入流時呼叫其 close 方法,拋出IOException例外,
3 public int read(int r)throws IOException{} 這個方法從 InputStream 物件讀取指定位元組的資料,回傳為整數值,回傳下一位元組資料,如果已經到結尾則回傳-1,
4 public int read(byte[] r) throws IOException{} 這個方法從輸入流讀取r.length長度的位元組,回傳讀取的位元組數,如果是檔案結尾則回傳-1,
5 public int available() throws IOException{} 回傳下一次對此輸入流呼叫的方法可以不受阻塞地從此輸入流讀取的位元組數,回傳一個整數值,
@Test
public void readFile02() {
    String filePath = "D:\\JetBrains\\hello.txt";
    int readData = https://www.cnblogs.com/SVicen/archive/2022/10/03/0;
    int readLen = 0;
    FileInputStream fileInputStream = null;
    //使用read(byte [] b)讀取
    byte[] buffer = new byte[8];  //一次讀取八個位元組  回圈讀入,超過八個會從頭覆寫之前的資料
    try {
        //創建FileInputStream物件,用于讀取檔案
        fileInputStream = new FileInputStream(filePath);
        //read()讀取一個位元組的資料,如果沒有輸入則不讀,讀到最后回傳-1  效率較低
        /*while ((readData = fileInputStream.read()) != -1) {
                System.out.print((char) readData);
            }*/
        //使用read(byte [] b)讀取提高效率,一次讀取八個位元組  回傳實際讀取的位元組數
        //底層維護一個byte型陣列的緩沖區
        while ((readLen = fileInputStream.read(buffer)) != -1) {
            System.out.print(new String(buffer,0,readLen));
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //這里要關閉流物件,防止其占用資源
        try {
            fileInputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
FileOutputStream

該類用來創建一個檔案并向檔案中寫資料,

如果該流在打開檔案進行輸出前,目標檔案不存在,那么該流會創建該檔案,

有兩個構造方法可以用來創建 FileOutputStream 物件,

使用字串型別的檔案名來創建一個輸出流物件:

OutputStream f = new FileOutputStream("C:/java/hello")

也可以使用一個檔案物件來創建一個輸出流來寫檔案,我們首先得使用File()方法來創建一個檔案物件:

File f = new File("C:/java/hello");
OutputStream f = new FileOutputStream(f);

創建OutputStream 物件完成后,就可以使用下面的方法來寫入流或者進行其他的流操作,

序號 方法及描述
1 public void close() throws IOException{} 關閉此檔案輸入流并釋放與此流有關的所有系統資源,拋出IOException例外,
2 protected void finalize()throws IOException {} 這個方法清除與該檔案的連接,確保在不再參考檔案輸入流時呼叫其 close 方法,拋出IOException例外,
3 public void write(int w)throws IOException{} 這個方法把指定的位元組寫到輸出流中,
4 public void write(byte[] w) 把指定陣列中w.length長度的位元組寫到OutputStream中,
@Test
public void writeFile(){
    //創建FileOutputStream物件
    FileOutputStream fileOutputStream = null;
    String outputPath = "D:\\JetBrains\\output.txt";
    try {
        //public FileOutputStream(String name, boolean append) 如果append設為true則在上次寫后追加而非覆寫
        fileOutputStream = new FileOutputStream(outputPath);
        //得到FileOutputStream物件后可以呼叫write方法  可以寫入一個位元組或多個位元組
        try {
            String str = "hello123,world!";
            fileOutputStream.write(str.getBytes());
            //繼續寫的話會從上次寫的游標后開始寫
            fileOutputStream.write(str.getBytes(),5,3);  //引數依次為byte[]  off  len 
            //最終寫入結果為 hello123,world!123  off為5 從索引為5的字符開始寫入三個字符長度
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    } catch (FileNotFoundException e) {
        throw new RuntimeException(e);
    }finally {
        try {
            fileOutputStream.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
//例題 拷貝檔案(圖片)
public class FileCopy {
    public static void main(String[] args) {
        //完成檔案拷貝,將F://演算法代碼截圖//1.png拷貝到D://JetBrains//1.png
        //思路分析
        //1.創建檔案輸入流,將檔案讀入到程式
        //2.創建檔案輸出流,將讀取到的檔案寫入指定位置
        //注意讀取部分資料就寫入,利用回圈寫入全部
        FileInputStream fileInputStream = null;
        FileOutputStream fileOutputStream = null;
        String SourcePath = "F://演算法代碼截圖//1.png";
        String destPath = "D://JetBrains//1.png";
        try {
            fileInputStream = new FileInputStream(SourcePath);
            fileOutputStream = new FileOutputStream(destPath);
            //利用字符陣列一次讀入多個字符
            byte[] buf = new byte[1024];
            int readLen = 0;
            while ((readLen = fileInputStream.read(buf)) != -1) {
                //讀取到后寫入到目標檔案
                fileOutputStream.write(buf,0,readLen);  //必須使用這個write方法  不能直接傳入buf 防止最后寫入的字符長度大于檔案還剩的字符長度
            }
            System.out.println("copy finished");
        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            try {
                if (fileInputStream != null) {
                    fileInputStream.close();
                }
                if (fileOutputStream != null) {
                    fileOutputStream.close();
                }
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

字符流

FileReader

FileReader類從InputStreamReader類繼承而來,該類按字符讀取流中資料,可以通過以下幾種構造方法創建需要的物件,

在給定從中讀取資料的 File 的情況下創建一個新 FileReader,

FileReader(File file)

在給定從中讀取資料的 FileDescriptor 的情況下創建一個新 FileReader,

FileReader(FileDescriptor fd)

在給定從中讀取資料的檔案名的情況下創建一個新 FileReader,

FileReader(String fileName)

創建FIleReader物件成功后,可以參照以下串列里的方法操作檔案,

序號 檔案描述
1 public int read() throws IOException 讀取單個字符,回傳一個int型變數代表讀取到的字符
2 public int read(char [] c, int offset, int len) 讀取字符到c陣列,回傳讀取到字符的個數
public class fileReader {
    public static void main(String[] args) {
        String path = "D://JetBrains//hello.txt";
        FileReader fileReader = null;
        char[] buf = new char[50];
        int readLen = 0;
        int data = https://www.cnblogs.com/SVicen/archive/2022/10/03/0;
        try {
            //創建FileReader物件
            fileReader = new FileReader(path);
            //讀取檔案內容
            //1.單個字符讀取
            while ((data = fileReader.read()) != -1) {  //把例外改為IOException
                System.out.print((char) data);
            }
            //2.字符陣列讀取
            while ((readLen = fileReader.read(buf)) != -1) {  //把例外改為IOException
                System.out.print(new String(buf,0,readLen));
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            if(fileReader != null){
                try {
                    fileReader.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}
FileWriter

寫完后必須關閉流或者flush才能真正寫入檔案,否則只有檔案里面沒有內容,close相當于flush + 關閉檔案

FileWriter 類從 OutputStreamWriter 類繼承而來,該類按字符向流中寫入資料,可以通過以下幾種構造方法創建需要的物件,

在給出 File 物件的情況下構造一個 FileWriter 物件,

FileWriter(File file)

在給出 File 物件的情況下構造一個 FileWriter 物件,

FileWriter(File file, boolean append)

引數:

  • file:要寫入資料的 File 物件,
  • append:如果 append 引數為 true,則將位元組寫入檔案末尾處,相當于追加資訊,如果 append 引數為 false, 則寫入檔案開始處,
  • 構造與某個檔案描述符相關聯的 FileWriter 物件,
FileWriter(FileDescriptor fd)

在給出檔案名的情況下構造 FileWriter 物件,它具有指示是否掛起寫入資料的 boolean 值,

FileWriter(String fileName, boolean append)

創建FileWriter物件成功后,可以參照以下串列里的方法操作檔案,

序號 方法描述
1 public void write(int c) throws IOException 寫入單個字符c,
2 public void write(char [] c, int offset, int len) 寫入字符陣列中開始為offset長度為len的某一部分,
3 public void write(String s, int offset, int len) 寫入字串中開始為offset長度為len的某一部分,
public class fileWriter {
    public static void main(String[] args) {
        String path = "D://JetBrains//fileWriter.txt";
        FileWriter fileWriter = null;
        char[] ch = {'a','b','c'};
        try {
            fileWriter = new FileWriter(path);
            //寫入單個字符
            fileWriter.write('a');  //其實write(int b) 但是char和int是可以隱式轉換的
            //寫入字符陣列 默認是覆寫寫入
            fileWriter.write(ch);
            //寫入指定陣列的指定部分
            fileWriter.write("svicen".toCharArray(),0,6);
            //寫入字串
            fileWriter.write("心之所向");
            //寫入字串的指定部分
            fileWriter.write("北京上海",2,2);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                fileWriter.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
        System.out.println("程式結束");
    }
}

節點流和處理流

  • 節點流:直接操縱資料源,底層流(低級流)從一個特定的資料源讀寫資料,如FileWriter和FileReader
  • 處理流:也叫包裝流,是連接在已存在的流之上,為程式提供更強大的讀寫功能,比如BufferedReader,BufferedWriter

image-20220531090741349

public class BufferedReader extends Reader {
    private Reader in;
    private char cb[];
    private int nChars, nextChar;
}
//BufferedReader封裝了一個屬性Reader,該屬性可以是任何節點流,可以放FileReader或者CharArrayReader或者StringReader等,   只要是Reader的子類即可,使用更加靈活          ----對應設計模式 修飾器模式

??修飾器模式模擬見 package com.svicen.reader_

BufferedReader
public class BufferedReader_ {
    public static void main(String[] args) throws Exception{
        String path = "D://JetBrains//hello.txt";
        //創建bufferedReader
        BufferedReader bufferedReader = new BufferedReader(new FileReader(path));
        //按行讀取,性能較高   當回傳null時讀取完畢
        String line;
        while ((line = bufferedReader.readLine()) != null){
            System.out.println(line);
        }
        //關閉流,只需要關閉BufferedReader流即可,底層會自動關閉節點流
        bufferedReader.close();
    }
}
//close底層原始碼
public void close() throws IOException {
    synchronized (lock) {
        if (in == null)
            return;
        try {
            in.close(); //這里的in就是我們傳入的FileReader
        } finally {
            in = null;
            cb = null;
        }
    }
}
BufferedWriter
public class BufferedWriter_ {
    public static void main(String[] args) throws IOException {
        String path = "D://JetBrains//bufWriter.txt";
        BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(path,true));
        bufferedWriter.write("hello,svicen\n");
        bufferedWriter.write("123456",2,3); //寫入456
        //關閉外部流(包裝流)即可
        bufferedWriter.close();
    }
}
//拷貝檔案示例
public class BufferedCopy_ {
    public static void main(String[] args) throws IOException {
        String srcPath = "D://JetBrains//hello.txt";
        String destPath = "D://JetBrains//hello2.txt";
        BufferedReader bufferedReader = null;
        BufferedWriter bufferedWriter = null;
        String line;
        bufferedReader = new BufferedReader(new FileReader(srcPath));
        bufferedWriter = new BufferedWriter(new FileWriter(destPath));
        while((line = bufferedReader.readLine()) != null){
            //每讀取一行就寫入到目標檔案 readline讀取一行的內容,不讀取換行符
            bufferedWriter.write(line);
            //插入換行符
            bufferedWriter.newLine();
        }
        if(bufferedReader != null){
            bufferedReader.close();
        }
        if(bufferedWriter != null){
            bufferedWriter.close();
        }
    }
}

??注意,BufferedReader和BufferedWriter用于處理字符流,不能操作二進制檔案(聲音,視頻,docx,pdf)

BufferedInputStream
  • 位元組處理流

image-20220601130052160

  • 繼承父類FilterInputStream的型別為InPutStream(抽象類)的in屬性,而BufferedReader本身有私有的型別為Reader的in屬性
BufferedOutputStream
  • 同樣也是結點處理流
  • 繼承父類FilterOutputStream的型別為OutputStream(抽象類)的out屬性,BufferedWriter本身有型別為Writer的out屬性

image-20220601130651594

//拷貝圖片/視頻,位元組流用于拷貝二進制檔案,也可以用來拷貝字符流檔案,但是字符處理流不能控制二進制檔案
public class BufferedIOStream_ {
    public static void main(String[] args) throws IOException{
        BufferedInputStream bufferedInputStream = null;
        BufferedOutputStream bufferedOutputStream = null;
        String srcPath = "D://JetBrains//1.png";
        String destPath = "D://JetBrains//1_bak.png";
        try {
            //這里是因為FileInputStream是InputStream的子類
            bufferedInputStream = new BufferedInputStream(new FileInputStream(srcPath));
            bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(destPath));
            //回圈讀取檔案 寫入destPath
            byte[] buf = new byte[1024];
            int readLen = 0;
            //讀到-1代表讀取結束
            while((readLen = bufferedInputStream.read(buf)) != -1){
                bufferedOutputStream.write(buf,0,readLen);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }finally {
            if (bufferedInputStream != null){
                bufferedInputStream.close();
            }
            if (bufferedOutputStream != null){
                bufferedOutputStream.close();
            }
        }
    }
}

物件處理流

ObjectIputStream
  • 位元組流

  • 保存檔案時可能需要保存 int 10(同時保存資料型別和值),保存Dog ketty 就需要用到物件的處理流

  • 序列化:在保存資料時保存為資料型別+值的形式,要保存的物件必須可序列化

  • 反序列化:恢復資料的值和資料型別

  • 要實作序列化,必須實作以下兩個介面其中一個,Serializable和Externalizable,推薦使用Serializable,這是一個標記介面,內部沒有任何方法,Externalizable其實也是實作了Serializable介面,實作該介面需要實作它的這兩個方法

    public interface Externalizable extends java.io.Serializable {
        void writeExternal(ObjectOutput out) throws IOException;
        void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
    }
    

image-20220601182328442

//讀取序列化資料
public class ObjectInputStream_ {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        String srcPath = "D://JetBrains//Object.txt";
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(srcPath));
        //讀取(反序列化)的順序必須和保存資料(序列化)的順序一致,否則會出現例外 先讀Int再讀Boolean
        System.out.println(ois.readInt());
        System.out.println(ois.readBoolean());
        System.out.println(ois.readChar());
        System.out.println(ois.readDouble());
        System.out.println(ois.readUTF());
        Object dog = ois.readObject();
        System.out.println("運行型別:" + dog.getClass());  //會拋出型別轉換例外 運行型別為Dog 這里用Object接收
        System.out.println("dog資訊" + dog);  //底層Object->Dog
//如果想呼叫Dog類的方法需要向下轉型,而且需要將Dog類的定義放在此類可以參考的地方(單獨定義Dog類匯入或者放在同一個包內)
        Dog dog2 = (Dog) dog;
        System.out.println(dog2.getName());
        ois.close();
    }
}
ObjectOutputStream

image-20220601182524166

//保存序列化資料
public class ObjectOutputStream_ {
    public static void main(String[] args) throws Exception{
        String filePath = "D://JetBrains//Object.txt";  //保存到txt檔案中為亂碼
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath));
        //序列化資料到目標檔案
        oos.writeInt(100); // int 底層 呼叫包裝類Integer 而Integer是實作了Serializable介面的
        oos.writeBoolean(true);//boolean -> Boolead 實作了Serializable介面
        oos.writeChar('a'); //char -> Character 實作了Serializable介面
        //oos.writeChars("svicen");//String 實作了Serializable介面  沒有與其對應的readChars方法
        oos.writeDouble(3.6); //double -> Double 實作了Serializable介面
        oos.writeUTF("蘇文成"); //String 實作了Serializable介面
        oos.writeObject(new Dog("旺財",10));//由于Dog沒有實作Serializable介面  運行會拋出例外
        oos.close();
        System.out.println("序列化資料保存完畢");
    }
}
class Dog implements Serializable {  //Dog必須實作Serializable介面  否則運行會拋出例外
    private String name;
    private int age;
   //序列化的類中最好加上serialVersionUID的屬性,為了提高兼容性,比如 類中加入屬性后會認為是增加而不是一個新的類
    private static final long serialVersionUID = 1L;
    public Dog(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

注意事項

  • 注意序列化和反序列化的順序要一致
  • 序列化物件時,物件必須實作了Serializable介面
  • 序列化的類中建議加上serialVersionUID的屬性,為了提高兼容性,
  • 序列化物件時,默認將里面所有的屬性進行序列化,除了static和transient(暫時的,不可序列化)修飾的屬性
  • 序列化物件時,要求里面屬性的型別也需要實作序列化介面,一個物件的屬性的型別為其他物件
  • 序列化具有可繼承性,如果某類已經實作了序列化,則它的所有子類默認也已經實作了序列化介面

標準輸入輸出流

image-20220601215750615

public static final InputStream in = null; //System.in  可以看出其編譯型別為InputStream  標準輸入 --> 鍵盤
//而System.in運行型別為BufferedInputStream --位元組流,包裝流(處理流)
public static final PrintStream out = null;//System.out 其編譯型別為PrintStream          標準輸出 --> 顯示幕
//System.in運行型別也為PrintStream,是FilterOutStream的子類,也是位元組流 BufferedOutputStream也是FilterOutStream的子類

image-20220601220330387

轉換流

轉換流:將位元組流轉換為字符流,(需要指定編碼方式)transformation

InputStreamReader
  • 是處理流,也字符輸入流
  • InputStreamReader構造器中可以傳入繼承了抽象父類InputStream的任意字類,Charset指定編碼方式,
  • InputStream為位元組流輸入流的頂級抽象父類,Reader為字符流的頂級抽象父類

image-20220602132307421

//利用InputStreamReader解決讀取到亂碼問題(其實要做的就是指定編碼方式)
public class InputStreamReader_ {
    public static void main(String[] args) throws IOException {
        //將位元組流 FileInPutStream 轉為字符流  指定編碼 gbk/utf-8
        String path = "D:\\JetBrains\\hello2.txt";
        //1.將 FileInPutStream 轉為 InputStreamReader 并指定編碼UTF-8  StandardCharsets是一個類,內有各種編碼方式
        InputStreamReader isr = new InputStreamReader(new FileInputStream(path), StandardCharsets.UTF_8);
        //2.把 InputStreamReader 傳入字符處理流 BufferedReader
        BufferedReader bufferedReader = new BufferedReader(isr);
        //3.讀取
        String data = https://www.cnblogs.com/SVicen/archive/2022/10/03/bufferedReader.readLine();
        System.out.println("讀取到內容" + data);
        //4.關閉外層流即可(處理流)
        isr.close();
    }
}
OutputStreamWriter
  • 是處理流,也字符輸出流
  • OutputStream為位元組流輸出流的頂級抽象父類,Writer為字符流輸出流的頂級抽象父類

image-20220602132557184

public class OutputStreamWriter_ {
    public static void main(String[] args) throws IOException {
        String filePath = "D:\\JetBrains\\swc.txt";
        String charSet = "gbk";
        //利用OutputStreamWriter把位元組流FileOutputStream轉為了字符流
        OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(filePath),charSet);
        osw.write("hello,蘇文成");
        osw.close();
        System.out.println("按照" + charSet + "保存檔案成功");
    }
}

列印流

  • 只有輸出流,沒有輸入流
PrintStream
  • 位元組流,是FileOutputStream的子類,不關閉流也可以寫入檔案
  • 可以列印到顯示幕,也可以列印到檔案,默認列印到標準輸出即顯示幕,
  • PrintStream的構造器,可以傳入檔案路徑實作列印到檔案 image-20220603095224356
public static final PrintStream out = null;//System.out 其編譯型別為PrintStream
public class PrintStream_ {
    public static void main(String[] args) throws IOException {
        PrintStream out = System.out;
        out.println("1322");
        out.write("hello,svicen".getBytes());
        out.close();
        //修改列印流列印的位置
        /* System類中有一個設定out的方法
        public static void setOut(PrintStream out) {   是一個native方法,就是java呼叫非java實作的介面,用c++實作的
            checkIO();
            setOut0(out);
        }
        */
        System.setOut(new PrintStream("D:\\JetBrains\\printStream.txt"));
        System.out.println("修改setOut后   System.out.println列印到檔案");
    }
}
//println追進去原始碼,呼叫print,print又使用了write方法,所以可以直接使用write方法列印
private void write(String s) {
        try {
            synchronized (this) {
                ensureOpen();
                textOut.write(s);
                textOut.flushBuffer();
                charOut.flushBuffer();
                if (autoFlush && (s.indexOf('\n') >= 0))
                    out.flush();
            }
        }
        catch (InterruptedIOException x) {
            Thread.currentThread().interrupt();
        }
        catch (IOException x) {
            trouble = true;
        }
    }
PrintWriter
  • 字符流,不關閉流就無法寫入檔案,不關閉流也無法列印到顯示幕

    public class PrintWriter_ {
        public static void main(String[] args) throws IOException {
            PrintWriter printWriter1 = new PrintWriter(System.out);
            printWriter1.println("123");
            printWriter1.close();  //必須關閉流,否則顯示幕上也無法顯示123
            PrintWriter printWriter = new PrintWriter(new FileWriter("D:\\JetBrains\\printWriter.txt"));
            printWriter.println(123);
            printWriter.close();  //同樣,不關閉流的話檔案可以創建,但內容為空  close里面呼叫了implclose方法writerbytes
        }
    }
    void implClose() throws IOException {
            flushLeftoverChar(null, true);
            try {
                for (;;) {
                    CoderResult cr = encoder.flush(bb);
                    if (cr.isUnderflow())
                        break;
                    if (cr.isOverflow()) {
                        assert bb.position() > 0;
                        writeBytes();  //這個方法真正寫入資料,
                        continue;
                    }
                    cr.throwException();
                }
    
                if (bb.position() > 0)
                    writeBytes();
                if (ch != null)
                    ch.close();
                else
                    out.close();
            } catch (IOException x) {
                encoder.reset();
                throw x;
            }
        }
    

Properties

  • 繼承了HashTable類,并且實作了Map介面,使用鍵值對形式來保存資料

  • Properties還可以從xxx.properties檔案中,加載資料到Properties物件并進行讀取和修改

  • 一般情況下,xxx.properties檔案往往為組態檔,

  • 常用方法

    1. setProperty(String key, String value)
      呼叫 Hashtable 的方法 put,
    2. getProperty(String key)
      用指定的鍵在此屬性串列中搜索屬性
    3. getProperty(String key, String defaultValue)
      用指定的鍵在屬性串列中搜索屬性,
    4. load(InputStream inStream)
      從輸入流中讀取屬性串列(鍵和元素對),
    5. load(Reader reader)
      按簡單的面向行的格式從輸入字符流中讀取屬性串列(鍵和元素對),
    6. loadFromXML(InputStream in)
      將指定輸入流中由 XML 檔案所表示的所有屬性加載到此屬性表中,
    7. store(OutputStream out, String comments)
      以適合使用 load(InputStream) 方法加載到 Properties 表中的格式,將此 Properties 表中的屬性串列(鍵和元素對)寫入輸出流,
    8. store(Writer writer, String comments)
      以適合使用 load(Reader) 方法的格式,將此 Properties 表中的屬性串列(鍵和元素對)寫入輸出字符,
    9. storeToXML(OutputStream os, String comment)
      發出一個表示此表中包含的所有屬性的 XML 檔案,
    10. storeToXML(OutputStream os, String comment, String encoding)
      使用指定的編碼發出一個表示此表中包含的所有屬性的 XML 檔案,
    //讀取組態檔內容,利用properties.getProperty(key)
    public class Properties02 {
      public static void main(String[] args) throws IOException {
          // 1.創建Properties物件
          Properties properties = new Properties();
          // 2.加載指定組態檔
          properties.load(new FileReader("src\\mysql.properties"));
          // 3.把K-V顯示到控制臺(System.out)
          properties.list(System.out);
          // 4.根據鍵獲取對應的值
          String user = properties.getProperty("user");
          String pwd = properties.getProperty("pwd");
          System.out.println("用戶名為:" + user);
          System.out.println("密碼為:" + pwd);
      }
    }
    
    //創建組態檔,利用properties.setProperty(key,value),然后利用store寫入檔案,如果沒有key就是創建,沒有就是修改
    public class Properties03 {
      public static void main(String[] args) throws IOException {
          //利用Properties類來創建組態檔,修改組態檔內容
          Properties properties = new Properties();
          //創建三對鍵值對
          properties.setProperty("charset","utf8");
          properties.setProperty("name","svicen");
          properties.setProperty("pwd","111");
          //將 K-V存盤到檔案中
          properties.store(new FileOutputStream("src\\mysql2.properties"),null);
          System.out.println("保存組態檔成功");
      }
    }
    

網路編程

InetAddress

public static void main(String[] args) throws UnknownHostException {
    //1.獲取本機的InetAddress物件
    InetAddress localHost = InetAddress.getLocalHost();
    System.out.println(localHost); //LAPTOP-V7FVCK3U/192.168.192.1
    InetAddress host1 = InetAddress.getByName("LAPTOP-V7FVCK3U");
    //2.根據指定主機名獲取InetAddress物件
    System.out.println("host1 = " + host1);//host1 = LAPTOP-V7FVCK3U/192.168.192.1
    //3.根據域名回傳InetAddress物件,比如www.baidu.com
    InetAddress host2 = InetAddress.getByName("www.baidu.com");
    System.out.println("host2 = " + host2); //host2 = www.baidu.com/36.152.44.95
    //4.通過InetAddress物件 獲取對應的主機地址
    String hostAddress = host2.getHostAddress();
    System.out.println("host2地址為 " + hostAddress); //host2地址為 36.152.44.95
    //5.通過InetAddress物件 獲取對應的主機名/域名
    String hostName = host2.getHostName();
    System.out.println("host2主機名為 " + hostName); //host2主機名為 www.baidu.com
}

Socket

  • Socket意思為插口,通常成為套接字,是客戶端與服務端之間進行資料通信的介面

image-20220607215017189

  • 基于Socket編程又分為TCP編程和UDP編程

TCP位元組流編程

服務器端:

1.監聽本機的 某個 埠

2.當沒有客戶端連接該埠時,程式會阻塞,等待連接

3.通過socket.getInputStream()獲取客戶端寫入到資料通道的資料,顯示

客戶端:

1.連接服務端(IP,埠)

2.連接后,通過socket.getOutputStream(),向資料通道寫入資料

//服務器端
public class SockcetTCP01Server {
    public static void main(String[] args) throws IOException {
        //1.服務器監聽本機9999埠
        ServerSocket serverSocket = new ServerSocket(9999);
        //2.當有客戶端連接時,則回傳一個socket物件,程式繼續執行
        System.out.println("服務端監聽9999埠,等待連接...");
        //注意:serverSocket可以通過accept方法,回傳多個socket物件(多個客戶機連接服務器的多并發)
        Socket socket = serverSocket.accept();

        System.out.println("成功獲取socket物件,服務器socket = " + socket);
        //3.通過socket.getInputStream()讀取客戶端寫入到資料通道的資料
        InputStream inputStream = socket.getInputStream();
        //4.IO讀取 位元組流
        byte[] buf = new byte[1024];
        int readlen = 0;
        while((readlen = inputStream.read(buf)) != -1){
            System.out.println(new String(buf,0,readlen)); //根據讀取到的實際長度顯示資料
            //public String(byte bytes[], int offset, int length)
        }
        //5.關閉流和socket
        inputStream.close();
        socket.close();
        System.out.println("服務器端退出");
    }
}
//客戶端
public class SockcetTCP01Client {
    public static void main(String[] args) throws IOException {
        //1.連接服務器(ip + 埠)  由于服務器,客戶機都是本機  連接成功的話回傳socket物件
        Socket socket = new Socket(InetAddress.getLocalHost(),9999);
        System.out.println("客戶端  socket = " + socket);
        //2.連接后,通過socket.getOutputStream() 獲取和socket物件相關聯的輸出流物件
        OutputStream outputStream = socket.getOutputStream();
        //3.通過輸出流物件,向資料通道寫入資料
        outputStream.write("hello,server".getBytes(StandardCharsets.UTF_8));
        //4.關閉socket和流物件,必須關閉,否則占用資源
        outputStream.close();
        socket.close();
        System.out.println("客戶端斷開連接");
    }
}
//例題:客戶端向服務器端發送圖片,客戶端需要IO讀磁盤,然后在寫入資料通道,服務端需要讀資料通道,然后IO寫磁盤
//工具類
public class StreamUtils {
    //功能:把輸入流轉換成byte[]
    public static byte[] streamToByteArray(InputStream is) throws IOException {
        //創建輸出流物件
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] bytes = new byte[1024];
        int len;
        while((len = is.read(bytes)) != -1) {
            baos.write(bytes,0,len);
        }
        byte[] arr = baos.toByteArray(); //將輸出流讀入的內容轉為byte陣列
        baos.close();
        return arr;
    }
    //功能:將InputStream轉換成String
    public static String streamToString(InputStream is) throws IOException {
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is));
        //字串處理流
        StringBuilder stringBuilder = new StringBuilder();
        String line;
        while((line = bufferedReader.readLine()) != null) {
            stringBuilder.append(line + "\r\n");
        }
        return stringBuilder.toString();
    }
}
public class TCPFileUploadClient {
    public static void main(String[] args) throws IOException {
        //客戶端向服務器端上傳一張圖片,并接收服務器端回復的 訊息
        //客戶端連接服務器端
        Socket socket = new Socket(InetAddress.getLocalHost(), 8888);
        //從磁盤上讀取檔案 ,利用位元組流
        String path = "D:\\JetBrains\\1.png";
        BufferedInputStream bis = new BufferedInputStream(new FileInputStream(path));
        //bytes就是這個檔案對應的位元組陣列   BufferedInputStream是InputStream的子類,所以
        byte[] bytes = StreamUtils.streamToByteArray(bis);
        //獲取輸出流,將bytes資料發送到資料通道
        BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
        bos.write(bytes);
        //設定結束標志
        socket.shutdownOutput();
        //獲取輸入流,讀取服務器回送訊息 可位元組流,也可字符流,這里用了位元組流
        InputStream inputStream = socket.getInputStream();
        //使用StreamUtils工具類的方法將輸入流轉為字串
        String s = StreamUtils.streamToString(inputStream);
        System.out.println("收到服務器端回送訊息: " + s);
        //關閉流
        bis.close();
        bos.close();
        socket.close();
    }
}
public class TCPFileUploadServer {
    public static void main(String[] args) throws IOException {
        //服務器端在本機監聽8888埠
        ServerSocket serverSocket = new ServerSocket(8888);
        System.out.println("服務器端監聽8888埠 ");
        //等待客戶端連接 ,連接成功后回傳一個socket物件
        Socket socket = serverSocket.accept();
        //讀取資料通道的內容 先創建輸入流物件,然后再讀取
        BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
        byte[] bytes = StreamUtils.streamToByteArray(bis);
        //將得到的bytes陣列寫入指定路徑
        String destPath = "src\\code.png";
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destPath));
        bos.write(bytes);
        bos.close();
        //服務器端向客戶端回送訊息, 說明收到照片 ,這次用字符流(利用轉換流將位元組流轉換為字符流)
        BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
        bw.write("收到圖片");
        //需要重繪才可寫入到資料通道
        bw.flush();
        socket.shutdownOutput();
        //關閉其它資源
        bis.close();
        socket.close();
        serverSocket.close();
    }
}

UDP位元組流編程

類DatagramSocket和DatagramPacket(資料包/資料報)實作了基于UDP的協議網路程式

UDP資料報通過資料報套接字DatagramSocket 發送和接收,發送資料前先要建立資料報DatagramPacket物件,系統不保證UDP資料報一定可以安全送到目的地,也不確定什么時候送達

DatagramPacket物件封裝了UDP資料報,在資料報中包含了發送端的IP地址和埠號以及接收端的IP地址和埠號

UDP協議中每個資料報都給出了完整的地址資訊,因此無需建立發送方和接收方的連接,

UDP中沒有客戶端和服務器端的說法,只區分發送端和接收端

image-20220609095833873

//接收端和發送端是相對的,發送端也可以接收資料  兩方進行udp通信,只需創建一個datagramSocket物件,可能創建多個packet物件
public class UDPSenderB {
    public static void main(String[] args) throws IOException {
        //1.創建 DatagramSocket 物件  埠最好不與接收端的一樣,是發送端要接收資料的埠(不需要連接到接收端)
        DatagramSocket datagramSocket = new DatagramSocket(9999);
        //2.創建一個位元組陣列, DatagramPacket 物件,需要知道目的主機IP,目的主機埠,用于在網路上傳輸資料
        byte[] bytes = "hello udp".getBytes(StandardCharsets.UTF_8);  //要發送的資料
        DatagramPacket datagramPacket = new DatagramPacket(bytes, bytes.length, InetAddress.getByName("192.168.192.1"),8888);
        //3.發送資料
        datagramSocket.send(datagramPacket);
        //4.接收資料
        byte[] data = https://www.cnblogs.com/SVicen/archive/2022/10/03/new byte[64 * 1024];
        //發送和接收時datagramSocket是一樣的,但是發送時datagramPacket需指明目標IP和埠
        DatagramPacket datagramPacket1 = new DatagramPacket(data, data.length);
        System.out.println("等待接受A回送的訊息");
        datagramSocket.receive(datagramPacket1);
        int length = datagramPacket1.getLength();
        byte[] data1 = datagramPacket1.getData();
        String s = new String(data1, 0, length);
        System.out.println(s);
        //5.關閉資源
        datagramSocket.close();
        System.out.println("B端退出");
    }
}
public class UDPReceiverA {
    public static void main(String[] args) throws IOException {
        //1.創建一個 DatagramSocket 物件,埠8888,準備接收資料
        DatagramSocket datagramSocket = new DatagramSocket(8888);
        //2.創建一個 DatagramPacket 物件,傳入一個byte陣列,準備接收資料
        byte[] buf = new byte[64 * 1024];  //UDP資料包最大 64k
        DatagramPacket datagramPacket = new DatagramPacket(buf,buf.length);
        //3.準備接收資料,呼叫 datagramSocket 的方法,將通過網路傳輸的 datagramPacket 填充到 datagramSocket中
        // 有資料包發送給本機的8888埠就會接收到資料,否則阻塞等待
        System.out.println("接收端A,等待接收資料");
        datagramSocket.receive(datagramPacket);
        //4.把 datagramPacket 進行拆包,取出資料并顯示
        int length = datagramPacket.getLength();  //實際接收到的資料位元組長度
        byte[] data = https://www.cnblogs.com/SVicen/archive/2022/10/03/datagramPacket.getData();
        String s = new String(data, 0, length);
        //5.發送資料,這里實作接收端向發送端發送資料(發送端和接收端都是相對的)
        byte[] sendData ="hello,see you later".getBytes(StandardCharsets.UTF_8);
        //發送資料時 DatagramPacket 的構造器有四個引數,要發送的資料的位元組陣列,長度,目標IP和埠
        DatagramPacket datagramPacket1 = new DatagramPacket(sendData, sendData.length, InetAddress.getByName("192.168.192.1"),9999);
        datagramSocket.send(datagramPacket1);
        System.out.println(s);
        //6.關閉資源
        datagramSocket.close();
        System.out.println("A端退出");
    }
}
//TCP編程作業
public class Homework01Client {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket(InetAddress.getLocalHost(), 9999);
        //獲取輸出流物件
        OutputStream outputStream = socket.getOutputStream();
        //使用字符流處理流寫入資料通道
        BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
        //從鍵盤讀取用戶輸入
        Scanner scanner = new Scanner(System.in);
        System.out.println("請輸入你的問題");
        String question = scanner.next();
        //將問題寫入資料通道
        bufferedWriter.write(question);
        //設定讀入結束符  這里要求讀取的時候必須使用 readLine()
        bufferedWriter.newLine();
        bufferedWriter.flush();  //字符流寫入,需要重繪
        InputStream inputStream = socket.getInputStream();
        //直接利用位元組流讀入
        int readLen = 0;
        byte[] buf = new byte[1024];
        while((readLen = inputStream.read(buf)) != -1){
            //顯示從服務器端讀取的資料
            System.out.println(new String(buf,0,readLen));
        }
        /*BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        String s = bufferedReader.readLine();
        System.out.println(s);*/         //也可以這樣讀入  利用轉換流將位元組流轉為字符流
        //bufferedReader.close();
        inputStream.close();
        outputStream.close();
        socket.close();
    }
}
public class Homework01Server {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(9999);
        System.out.println("服務端等待連接");
        Socket socket = serverSocket.accept();
        //連接后成功獲取到socket物件
        //獲取寫入資料通道的輸入流
        InputStream inputStream = socket.getInputStream();
        // 讀取資料,使用字符流,利用轉換流將位元組流轉換為字符流
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
        String s = bufferedReader.readLine();
        String answer;
        if("name".equals(s)) {
            answer = "svicen";
        } else if ("hobby".equals(s)) {
            answer = "programing";
        }else {
            answer = "what?";
        }
        //向資料通道寫入資料  這次以位元組流的形式
        OutputStream outputStream = socket.getOutputStream();
        outputStream.write(answer.getBytes(StandardCharsets.UTF_8));
        socket.shutdownOutput();  //設定結束標志
        /*BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
        bufferedWriter.write(answer);
        bufferedWriter.newLine();
        bufferedWriter.flush();*/  // 利用轉換流將位元組流轉換為字符流再寫入,注意字符流寫入需要flush
        //關閉流
        outputStream.close();
        //bufferedWriter.close();
        bufferedReader.close();
        socket.close();
    }
}

反射

應用場景:學習框架時用到的較多

通過外部組態檔,在不修改原始碼的情況下來控制程式,符合設計模式的ocp原則(開閉原則)

反射其實就是 new 的第二種和形態

package com.svicen.reflection;

import com.svicen.Cat;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Properties;

/**
 * @author svicen
 * @version 1.0
 */
public class ReflectionQuestion {
    // 使用發射類機制 解決通過組態檔加載類
    public static void main(String[] args) throws IOException, ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        // 1.使用 Properties 類可以讀寫組態檔
        Properties properties = new Properties();
        properties.load(new FileInputStream("src\\re.properties"));
        String classfullpath = properties.get("classfullpath").toString();
        String methodName = properties.get("method").toString();
        System.out.println("classfullpath = " + classfullpath);
        System.out.println("method = " + methodName);
        // 但是此時的 classfullpath 是一個字串型別 無法直接創建 Cat物件
        // 2.使用反射機制解決上述問題  可以不修改原始碼只修改組態檔實作功能的切換
        // (1) 加載類,回傳一個 Class 物件,這個類名就叫 Class
        Class<?> cls = Class.forName(classfullpath);
        // (2) 通過 cls 可以得到加載的類的實體物件
        Object o = cls.newInstance();
        System.out.println(o.getClass()); // class com.svicen.Cat
        // (3) 通過 cls 得到加載的類 com.svicen.Cat 的 methodName 的方法物件 "hi"
        Method method1 = cls.getMethod(methodName);
        // (4) 通過 method1 呼叫方法:即通過方法物件實作呼叫方法  -- 萬物皆物件
        System.out.println("=======反射======");
        method1.invoke(o);  // 反射機制: 方法.invoke(物件)
        // 通過 Field 物件獲取某個類的成員變數
        Field nameField = cls.getField("name");  // getField 不能得到私有的屬性
        System.out.println(nameField.get(o));    //輸出:招財貓   反射的 括號里的引數 一般都是物件

    }
}

反射的優點和缺點:

優點:可以動態的創建和使用物件(是框架的底層核心),使用靈活

缺點:使用反射機制基本是解釋執行,會降低執行速度,可以關閉反射呼叫方法時的訪問檢測來優化反射,但效果有限

Class類

  • Class類其實也就是一個普通的類,

  • Class類不可以通過new來實體化物件,而是系統通過類加載器來創建的

            // (1) 傳統new物件
            //  底層呼叫了 loadClass 方法來實體化物件
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                return loadClass(name, false);
            }
    
            Cat cat = new Cat();  // 通過 ClassLoader類 實作類的加載
            // (2)反射方式  注意要將上面的注釋掉  否則Cat類已經加載完成了
            Class<?> cls1 = Class.forName("com.svicen.Cat");
            //  同樣呼叫了 loadClass 方法來實體化物件
            public Class<?> loadClass(String name) throws ClassNotFoundException {
                return loadClass(name, false);
            }
            // 由于類只加載一次  所以下面輸出的 hashCode 是一樣的  
            Class<?> cls2 = Class.forName("com.svicen.Cat");
            System.out.println(cls1.hashCode());
            System.out.println(cls2.hashCode());
    
  • 對于某個類的Class物件,在記憶體中只有一份,因為類只加載一次 多次加載所得的物件的hashCode值一樣

  • 每個類的實體都會記得自己是由哪個Class實體所生成,

  • 通過Class物件可以完整地得到一個類的完整結構,通過一系列API

    
    

  • Class物件是存放在堆中的

  • 類的位元組碼二進制資料是存放在方法區的,有的地方稱為類的原資料(包括方法代碼、變數名、方法名、訪問權限等)

Java 程式的執行階段

  • 獲取Class類物件的方式

    • 編譯階段通過 Class.forname()使用,多用于讀取組態檔中已知的一個類的全類名
    • 加載階段通過 類.class ,一般用于引數傳遞
    • 運行階段通過 物件.getClass() , 前提是已經有了某類的物件實體
    // 1.Class.forName()
            String classAllPath = "com.svicen.Car";  //一般是讀取的組態檔的資訊
            Class<?> cls1 = Class.forName(classAllPath);
            System.out.println(cls1);
            // 2.類名.class 一般用于引數傳遞
            Class cls2 = Car.class;
            System.out.println(cls2);
            // 3.物件.getClass()  前提是已知某類的物件實體
            Car car = new Car();
            Class<? extends Car> cls3 = car.getClass();
            System.out.println(cls3);
            // 4.通過類加載器【4種】
            // (1)先得到類加載器
            ClassLoader classLoader = car.getClass().getClassLoader();
            // (2)通過類加載器獲取物件
            Class<?> cls4 = classLoader.loadClass(classAllPath);
            System.out.println(cls4);
            // cls1 cls2 cls3 cls4 實際上是一個物件,hashCode值都是一樣的
            // 5.基本資料型別.class
            Class<Integer> integerClass = int.class;
            Class<Character> characterClass = char.class;
            // 6.基本資料型別對應的包裝類.TYPE
            Class<Integer> type = Integer.TYPE;
            System.out.println(type);
    
  • 哪些型別有class物件

     Class<String> cls1 = String.class;  // 外部類
     Class<Serializable> cls2 = Serializable.class; // 介面
     Class<int[]> cls3 = int[].class;   // 陣列
     Class<Deprecated> cls4 = Deprecated.class;  // 注解
     Class<Thread.State> cls5 = Thread.State.class;  // 列舉
     Class<Long> cls6 = long.class;   // 基本資料型別
     Class<Integer> cls7 = Integer.class;  // 包裝類
     Class<Void> cls8 = void.class;  // void 也有
     Class<Class> cls9 = Class.class;  // Class 本事也是一種資料型別  也是外部類
    

類加載程序

靜態加載

在編譯時進行類的加載,依賴性較強,找不到類編譯時就會報錯

import java.lang.reflect.Method;
import java.util.Scanner;

public class ClassLoad_ {
    public static void main(String[] args) throws Exception {
        Scanner scanner = new Scanner(System.in);
        System.out.println("請輸入key");
        String key = scanner.next();
        switch (key) {
            case "1":
                //Dog dog = new Dog();  // 靜態加載類  依賴性較強
                //dog.cry();
                break;
            case "2":
                // 動態加載類  使用時再加載 用到的類
                Class cls = Class.forName("Person");
                Object o = cls.newInstance();
                Method m = cls.getMethod("hi");
                m.invoke(o);
                System.out.println("ok");
            default:
                System.out.println("nothing!");

        }
    }
}

class Dog {
    public void cry() {
        System.out.println("小狗汪汪叫");
    }
}
動態加載

執行到具體的代碼時才會加載指定類

加載階段
  • JVM在該階段的主要目的是將位元組碼從不同的資料源(可能是Class檔案,jar包,網路)轉化為二進制位元組流加載到記憶體中,并生成一個代表該類的java.lang.Class物件
連接階段
驗證
  • 目的是為了確保Class檔案的位元組流包含的資訊符合當前虛擬機的要求并且不會危害虛擬機的自身安全
  • 驗證包括:檔案格式(是否以魔數0xcafebabe開頭)、元資料驗證、位元組碼驗證、符號參考驗證
  • 可以考慮使用 -Xverify:none 引數來關閉大部分的類驗證措施,縮短虛擬機類加載的時間
準備
  • 對于實體屬性變數,在準備階段不會分配記憶體(類內非靜態變數)
  • 對于靜態變數,在準備階段默認初始化為0,在初始化階段才會初始化為設定的值
  • 對于 static final修飾的靜態常量,它一旦賦值后就不再變化,在準備階段初始化的值即為設定的值
決議
  • JVM虛擬機將常量池的符號參考替換為直接參考(記憶體地址)的程序
初始化
  • 初始化階段才真正執行類中定義的 Java程式代碼
  • 此階段是執行<clinit>()方法的程序,該方法是由編譯器按陳述句在源檔案中出現的順序,依次自動收集類中的所有靜態變數的賦值動作和靜態代碼塊中的陳述句,并進行合并(合并為一個代碼塊)
  • 虛擬機會保證一個類的 <clinit>()方法在多執行緒環境中被正確地加鎖、同步,如果多個執行緒同時去初始化一個類,那么只會有一個執行緒去執行這個類的<clinit>()方法,其他執行緒都需要阻塞等待,直到活動執行緒執行<clinit>()方法完畢

獲取類結構資訊的API

package com.svicen.reflection;

import org.junit.jupiter.api.Test;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Arrays;

/**
 * @author svicen
 * @version 1.0
 * 演示如何通過反射獲取類的結構資訊
 */
public class ReflectionUtils {
    public static void main(String[] args) {

    }
    // 第一組 API
    @Test
    public void api_01() throws ClassNotFoundException {
        // 獲取 Class 物件
        Class<?> personCls = Class.forName("com.svicen.reflection.Person");
        // getName 獲取全類名
        System.out.println(personCls.getName());  // com.svicen.reflection.Person
        // getSimpleName 獲取簡單類名
        System.out.println(personCls.getSimpleName()); //Person
        // getFields 獲取所有public修飾的屬性,包括本類以及父類
        Field[] fields = personCls.getFields();
        for(Field filed : fields){
            System.out.println("本類及父類的public屬性:" + filed.getName()); // name  hobby
        }
        // getDeclaredFields 獲取本類中所有屬性R
        Field[] declaredFields = personCls.getDeclaredFields();
        for (Field declaredField : declaredFields) {
            System.out.println("本類中所有屬性:" + declaredField.getName());
        }
        // getMethods 獲取本類及父類(超類)中的public修飾的方法
        Method[] methods = personCls.getMethods();
        for (Method method : methods) {
            System.out.println("本類及父類的public方法:" + method.getName()); // 還有超類的方法
        }
        // getDeclaredMethods 獲取本類所有方法
        Method[] declaredMethods = personCls.getDeclaredMethods();
        for (Method declaredMethod : declaredMethods) {
            System.out.println("本類中所有方法:" + declaredMethod.getName());
        }
        // getConstructors 獲取本類所有public修飾的構造器  沒有父類
        Constructor<?>[] constructors = personCls.getConstructors();
        for (Constructor<?> constructor : constructors) {
            System.out.println("本類所有public修飾的構造器:" + constructor.getName());
        }
        // getDeclaredConstructors 獲取本類中所有構造器
        Constructor<?>[] declaredConstructors = personCls.getDeclaredConstructors();
        for (Constructor<?> declaredConstructor : declaredConstructors) {
            System.out.println("本類中所有構造器" + declaredConstructor.getName());
        }
        // getPackage 以 Package 形式回傳包資訊
        Package aPackage = personCls.getPackage();
        System.out.println(aPackage);
        // getSuperclass 以Class 形式回傳父類資訊
        Class<?> superclass = personCls.getSuperclass();
        System.out.println("父類類名:" + superclass.getSimpleName()); // A
        // getInterfaces 以 Class[]形式回傳介面資訊
        Class<?>[] interfaces = personCls.getInterfaces();
        for (Class<?> anInterface : interfaces) {
            System.out.println("介面資訊:" + anInterface.getName());
        }
        // getAnnotations 以 Annotation[] 的形式回傳注解資訊
        Annotation[] annotations = personCls.getAnnotations();
        for (Annotation annotation : annotations) {
            System.out.println("注解資訊:" + annotation);
        }
    }
}
class  A{
    public String hobby;
    public A(){}
    public void hi(){}
    private void hello(){}
}
interface IA{
}
interface IB{
}
@Deprecated
class Person extends A implements IA, IB{
    // 屬性
    public String name;
    protected int age;
    String job;
    private double sal;
    // 構造器
    public Person(){}
    public Person(String name){}
    private Person(String name, int age){}
    // 方法
    public void m1(){}
    protected void m2(){}
    void m3(){}
    private void m4(){}
}
  • getModifiers以int形式回傳邢師傅,修飾符對應的值: 0位默認,1為public,2為private,4為protected,8為static,16為final
// 第二組API
    @Test
    public void api_02() throws ClassNotFoundException {
        // 獲取 Class 物件
        Class<?> personCls = Class.forName("com.svicen.reflection.Person");
        // getDeclaredFields 獲取本類中所有屬性
        Field[] declaredFields = personCls.getDeclaredFields();
        for (Field declaredField  declaredFields) {
            System.out.println("本類中所有屬性:" + declaredField.getName() +
                    "  該屬性的修飾符值為:" + declaredField.getModifiers() +
                    "  該屬性的型別為:" + declaredField.getType()); // 回傳屬性對應的類的 Class 物件
        // 修飾符對應的值: 0位默認,1為public,2為private,4為protected,8為static,16為final
        }
        // getDeclaredMethods 獲取本類所有方法
        Method[] declaredMethods = personCls.getDeclaredMethods();
        for (Method declaredMethod : declaredMethods) {
            System.out.println("本類中所有方法:" + declaredMethod.getName() +
                    "  該方法的訪問修飾符的值:" + declaredMethod.getModifiers() +
                    "  該方法回傳型別:" + declaredMethod.getReturnType());  // 回傳回傳型別對應的Class物件
            // 輸出方法的形參型別
            Class<?>[] parameterTypes = declaredMethod.getParameterTypes();
            for (Class<?> parameterType : parameterTypes) {
                System.out.println("該方法的形參型別:" + parameterType);
            }
        }
        // getDeclaredConstructors 獲取本類中所有構造器
        Constructor<?>[] declaredConstructors = personCls.getDeclaredConstructors();
        for (Constructor<?> declaredConstructor : declaredConstructors) {
            System.out.println("本類中所有構造器" + declaredConstructor.getName());
            // 獲取構造器的形參型別
            Class<?>[] parameterTypes = declaredConstructor.getParameterTypes();
            for (Class<?> parameterType : parameterTypes) {
                System.out.println("該構造器的形參有:" + parameterType);
            }
            System.out.println("============================");
        }
    }    

反射爆破創建實體

  • 反射爆破,可以使類的訪問修飾符無效,通過設定setAccessible為true
public class ReflecCreateInstance {
    public static void main(String[] args) throws Exception {
        //1.先獲取到User類的Class物件
        Class<?> userClass = Class.forName("com.svicen.reflection.User");
        //2.通過public的無參構造器創建實體
        Object o = userClass.newInstance();
        System.out.println(o);
        //3.通過public的有參構造器創建實體
        // 首先獲取有參構造器
        Constructor<?> constructor = userClass.getConstructor(String.class);
        Object swc = constructor.newInstance("蘇文成");//體現反射 方法(物件)
        System.out.println(swc);
        //4.通過非public的有參構造器創建實體
        Constructor<?> declaredConstructor = userClass.getDeclaredConstructor(int.class, String.class);
        // 爆破:使用反射可以利用私有的構造器構造實體
        declaredConstructor.setAccessible(true);  // 設定爆破,否則對于非public的構造器創建實體會報錯
        Object swc1 = declaredConstructor.newInstance(21, "swc"); // 由于構造器是私有的  會報錯
        System.out.println(swc1);
    }
}
class User{
    private int age = 20;
    private String name = "svicen";
    public User(){}

    public User(String name) {
        this.name = name;
    }
    private User(int age, String name) {
        this.age = age;
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}

反射爆破操作屬性

  • 對于static屬性,set方法第一個引數可以設定為null,也可以設定為具體的物件
public class ReflecAccessProperty {
    public static void main(String[] args) throws Exception{
        //1. 獲取Student類對應的Class物件
        Class<?> stuClass = Class.forName("com.svicen.reflection.Student");
        //2. 利用無參構造器創建物件
        Object o = stuClass.newInstance();
        System.out.println(o);
        //3. 使用反射得到 age 屬性物件
        Field age = stuClass.getField("age");
        age.set(o,22);  //通過反射對指定物件設定屬性值
        System.out.println(o);
        System.out.println(age.get(o));  // age.get() 括號內傳入物件
        //4. 使用反射操作 私有的、靜態的 name物件
        Field name = stuClass.getDeclaredField("name");
        name.setAccessible(true);
        name.set(null, "svicen");
        System.out.println(o);
        System.out.println(name.get(null));
    }
}
class Student{
    public int age;
    private static String name;
    public Student(){}

    @Override
    public String toString() {
        return "Student{" +
                "age=" + age +
                ", name=" + name + '}';
    }
}

反射爆破操作方法

public class ReflecAccessMethod {
    public static void main(String[] args) throws Exception {
        // 1.得到 Boss 類對應的Class物件
        Class<?> bossCls = Class.forName("com.svicen.reflection.Boss");
        // 2.創建一個 物件
        Object o = bossCls.newInstance();
        // 3.得到public的hi方法物件  注意需要指明引數的型別對應的class物件
        Method hi = bossCls.getMethod("hi", String.class);
        // 4.通過反射呼叫該方法
        hi.invoke(o, "svicen");
        // 5.private且static的say方法物件
        Method say = bossCls.getDeclaredMethod("say", int.class, String.class, char.class);
        // 6.通過反射呼叫方法  注意invoke的第一個引數可以為null,因為是靜態方法
        say.setAccessible(true);  // 因為方法時私有的,所以需要爆破
        System.out.println(say.invoke(null,22,"svicen",'男'));
        // 如果呼叫的方法有回傳值,對于反射而言,回傳的型別一定為 Object
        // 但是運行型別和方法中定義的方法型別一致
        Object returnVal = say.invoke(null, 33, "sv", '女');
        System.out.println(returnVal.getClass());
    }
}
class Boss{
    public int age;
    private static String name;
    public Boss(){}
    private static String say(int n, String s, char c) {
        return n + " " + s + " " + c;
    }
    public void hi(String s){
        System.out.println("hi " + s);
    }
}

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/510897.html

標籤:其他

上一篇:bbs專案前期準備和表設計

下一篇:設計模式---配接器模式

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more