一,java基本程序設計結構:
1,在網頁中運行的 Java 程序稱為 applet。 要使用 applet ,需要啟用 Java 的 Web 瀏覽器執行字節碼。
2,jdk安裝目錄下的 src.zip 文件中包含了所有公共類庫的源代碼。 要想獲得更多的源代碼 ( 例如 :編譯器 、 虛擬機 、 本地方法以及私有輔助類 ),請訪問網站 :http://jdk8.java.net。
3, 浮點數值不適用於無法接受舍入誤差的金融計算中。例如,命令System.out.println(2.0-1.1)將打印出0.8999999999999999,而不是人們想象的0.9。這種舍入誤差的主要原因是浮點數值采用二進制系統表示,而在二進制系統中無法精確地表示分數1/10。這就好像十進制無法精確地表示分數1/3—樣。
4, 在Java中,-共有8種基本類型(primitivetype),其中有4種整型【byte 1個字節,short 2個字節,int 4個字節,long 8個字節】、2種浮點類型【float 4個字節,double 8個字節】、1種用於表示Unicode編碼的字符單元的字符類型char和1種用於表示真值的boolean類型。基本類型和引用類型都保存在棧中,但是基本類型保存的是實際值,而引用類型保存的是一個對象的內存地址。基本類型是內建在Java語言中的特殊的數據類型,它們不是繼承自Object對象,所以int等基本類型不屬於Object 【參考1】【參考2:官方教程說明】。平常Object o = (int) 3;不會報錯,這是用了自動裝箱功能。但是泛型中類型參數不能為基本類型,因為編譯器類型擦除時會把泛型類型參數(假設此類型參數沒有邊界)設置為Object,而Object不能用於存儲基本類型的值(沒有用自動裝箱功能)。
4.1,float類型的有效位數(精度)為6~7位。double類型的有效位數為15位。
5,碼點(code point)表示 與一個編碼表(如Unicode編碼)中的某個字符對應的代碼值。在Unicode編碼表標准中,碼點采用十六進制書寫,並加上前綴U+,例如 U+0041 就是拉丁字母 A 的碼點。Unicode的碼點可以分為17個代碼平面(code plane)。第一個代碼平面,有時叫第零個代碼平面,叫做 基本多語言平面(basic multimultilingual plane),碼點從U+0000 到 U+FFFF。其余的16個平面從U+10000 到 U+10FFFF。 第一個平面里包含經典的Unicode代碼,其余16個包括一些輔助字符。 UTF-16是Unicode的一種使用方式,UTF即Unicode Transfer Format,即把Unicode轉換為某種格式的意思。UTF-16編碼采用不同長度的編碼來表示所有的Unicode碼點。在Unicode的基本多語言平面中,UTF-16將Unicode中的每個字符用2個字節16位來表示,通常被稱為 代碼單元(code unit,又稱碼元)。而對於其他16個平面中的輔助字符,UTF-16采用一對連續的代碼單元進行編碼,即用2個(2字節的)碼元表示。為了能夠區分出某個碼元是一個字符的編碼(基本多語言平面中的字符,即單16位)還是一個輔助字符(即雙16位)的第一或第二部分,UTF-16編碼規定以54開頭(110110)的一個碼元表示輔助字符的前16位即第一部分,以55開頭(110111)的一個碼元表示輔助字符的后16位,即第二部分。其他開頭的碼元則是單16位的表示字符的碼元。由於第零平面的字符有0x0000-0xffff共65536個字符,剛好可以用16位表示完,如此肯定也有以54開頭的單16位編碼。實際上,Unicode為了配合UTF-16規定了 以54開頭的區間(即110110 開頭的16位區間,范圍從D800-DBFF,共1024個字符位置),和以55開頭的區間(范圍從DC00~DFFF共1024個字符位置)不允許分配任何字符。所以實際上Unicode第零平面表示的字符共65536-2048 個。參考文章:https://blog.csdn.net/wusj3/article/details/88641084。 Java中的char類型描述了UTF-16編碼中的一個碼元,一個碼點可能包含一個碼元也可能包含2個碼元(例如: 𝕆 ,𠠦)。
5.1, Unicode字符編碼表其實和計算機沒有任何關系,它只是給每個字符一個數字編號。如何在計算機中存儲這些數字才是計算機的事情。有好多種實現方式,utf-8,utf-16等。其中,在Unicode的第零個平面中的字符(65536-2048個字符)其正常的二進制編碼 和 這些字符使用 utf-16 編碼后的結果是一樣的。
6,const是Java保留的關鍵字,但目前並沒有使用。在Java中,必須使用final定義常量。
7,整數被0除將會產生一個異常,而浮點數被0除將會得到無窮大或NaN結果。
8,在默認情況下,虛擬機設計者允許對中間計算結果采用擴展的精度。但是,對於使用 strictfp 關鍵字標記的方法必須使用嚴格的浮點計算(即中間結果要進行截斷)。
9,在Math類中,為了達到最快的性能,所有的方法都使用計算機浮點單元中的例程..如果得到一個完全可預測的結果比運行速度更重要的話,那么就應該使用StrictMath類,,它使用“自由發布的Math 庫”(fdlibm)實現算法,以確保在所有平台上得到相同的結果
10,基本類型之間的轉換:如圖,
一,
int n=123456789; float f = n; // f=1.23456792E8。 float類型的精度是6-7位。123456789包含的位數比float的精度要多,所以會損失一定的精度。 二, 兩個基本類型的數值進行二元運算時,java編譯器會先將兩個操作數轉換為同一種類型,然后再進行計算。 如果有一個操作數為double,另一個也會被轉換為double, 否則,如果有一個為float,另一個也會被轉換為float, 否則,如果有一個為long,另一個也會被轉換為long. 否則,兩個操作數都會被轉換為int。 精度小於int類型的數值運算會被自動轉換為int類型然后再運算。如, 兩個short類型的數值進行運算時,會首先將short類型轉換為int。所以,如下代碼編譯會報錯: short s1 = 1; short s2 = 1; s1 = s1 + s2;// 報錯:無法將int類型賦值給short類型! 必須使用強制類型轉換(cast): s1 = (short) (s1 + s2); 但是 s1 += s2;不會報錯,因為 += 運算符在運算后(s1+s2),如果得到的值的類型與左側操作數(s1)的類型不同,就會發生強制類型轉換:
即s1+=s2最終實際上是:s1 = (short) (s1+s2)。 三, 在必要的時候,int類型的值會自動的轉換為double類型。有時候需要將double類型轉為int類型(這種轉換會損失精度),在Java中這種操作不會自動進行,
需要通過強制類型轉換(cast)實現這個操作。如:double x = 9.997; int nx = (int) x;//nx = 9; int nx = (int) Math.round(x);// nx = 10;
11,Java沒有內置的字符串類型,而是在標准Java類庫中提供了一個預定義類,很自然地叫做String。每個用雙引號括起來的字符串都是String類的一個實例。由於不能修改Java字符串中的字符,所以在Java文檔中將String類對象稱為不可變字符串.不可變字符串卻有一個優點:編譯器可以讓字符串共享。為了弄清具體的工作方式,可以想象將各種字符串存放在公共的存儲池中。字符串變量指向存儲池中相應的位置。如果復制一個字符串變量,原始字符串與復制的字符串共享相同的字符。
12, Java中比較字符串是否相等不能使用 ==。因為這個運算符只能確定兩個字符串是否放在同一個位置(這句話的含義實際是 == 比較字符串不僅比較字面是否相同,還比較兩個字符串的內存地址是否相同!)。如果java虛擬機始終將相同的字符串共享放在同一個內存地址中,那么就可以使用 == 檢測字符串是否相等。但是實際上,只有字符串常量是共享的(即放在同一塊內存中),而使用 + 或 substring等操作產生的結果並不是共享的。所以千萬不能使用 == 比較字符串是否相等。例如,String s = "Hello"; s.substring(0,2) == "He" 是錯誤的,兩者並不 ==,但是卻是equals的。
12, String API
1,nothing to note
13,數組: 一旦創建了數組 ,就不能再改變它的大小( 盡管可以改變每一個數組元素 )。
一,
- int[] arr = new int[10] ; arr[0] = 3;arr[1]=4;
- int[] arr = {1,2,3,4};
- int[] arr = new int[] {1,2,3,4}
二,
- String arrStr = Arrays.toString(arr);
- int[] arr1 = arr; // 兩個變量引用同一個數組,一個改變會影響另一個
- int[] arrCopy = Arrays.copyOf(arr, arr.length * 2); // 只拷貝值。如果數組元素是數值型,那么多余的元素將被賦值為0;如果數組元素是布爾型,則將賦值為false。相反,如果長度小於原始數組的長度,則只拷貝最前面的數據元素。
- int[] arrCopy = Arrays.copyOfRange(arr, startIndex, endIndex); // [start, end)
- void Arrays.sort(arr);
- boolean Arrays.equals(arr1, arr2);
- //如果兩個數組長度相同,並且在對應的位置上數據元素也均相同,將返回true。數組的元素類型可以是Object、int、long、short、char、byte、boolean、float或double
- int[][] arrArr = new int[2][3];// {{1,2},{2,3}}
- String arrArrStr = Arrays.deepToString(arrArr);
三,
第四章,對象與類:
※,以下都以如下2個類為例子:Employee, Manager
class Employee
{
private String name; private double salary; private LocalDate hireDay; public Employee(String n, double s, int year, int month, int day) { name = n; salary = s; hireDay = LocalDate.of(year, month, day); } public String getName() { return name; } public double getSalary() { return salary; } public LocalDate getHireDay() { return hireDay; } public void raiseSalary(double byPercent) { double raise = salary * byPercent / 100; salary += raise; } }
class Manager extends Employee
{
private double bonus; public Manager(String name, double salary, int year, int month, int day) { super(name, salary, year, month, day); bonus = 0; } public double getSalary() { double baseSalary = super.getSalary(); return baseSalary + bonus; } public void setBonus(double b) { bonus = b; } }
14,可以顯式地將對象變量設置為 null, 表明這個對象變量目前沒有引用任何對象 。所有的Java對象都存儲在堆中。當一個對象包含另一個對象變量時,這個變量依然包含着指向另一個堆對象的指針。
15,注意,在這個示例程序中包含兩個類:Employee類和帶有public訪問修飾符的EmployeeTest類。EmployeeTest類包含了main方法。源文件名是EmployeeTest.java,這是因為文件名必須與public類的名字相匹配。在一個源文件中,只能有一個公有類,但可以有任意數目的非公有類。接下來,當編譯這段源代碼的時候,編譯器將在目錄下創建兩個類文件:EmployeeTest.class和Employee.class將程序中包含main方法的類名提供給字節碼解釋器,以便啟動這個程序:javaEmployeeTest字節碼解釋器開始運行EmployeeTest類的main方法中的代碼。
16,多個源文件的使用
一個源文件可以包含了兩個類。許多程序員習慣於將每一個類存在一個單獨的源文件中。例如,將Employee類存放在文件Employee.java中,將EmployeeTest類存放在文件EmployeeTest.java中。
如果喜歡這樣組織文件,將可以有兩種編譯源程序的方法。一種是使用通配符調用Java編譯器:
javac Employee*.java
於是,所有與通配符匹配的源文件都將被編譯成類文件。或者鍵人下列命令:
javac EmployeeTest.java
讀者可能會感到驚訝,使用第二種方式,並沒有顯式地編譯Employee.java,然而,當Java編譯器發現EmployeeTest.java使用了Employee類時會查找名為Employee.class的文件。如果沒有找到這個文件,就會自動地搜索Employee.java,然后,對它進行編譯。更重要的是:如果Employee.java版本較已有的Employee.class文件版本新,Java編譯器就會自動地重新編譯這個文件。
17,p127: getter訪問器方法注意不要返回 “可變對象”。因為對這個對象調用更改器方法會改變對象的私有狀態,這是我們不想要的。如果需要返回一個可變對象的引用,應該首先對它進行克隆,
18,p129: final 實例域。final關鍵字一般用於基本類型的域(即類的字段或稱屬性),或不可變類的域(如果類中的每個方法都不會改變其對象,這種類就是不可變的類。例如,String類就是一個不可變的類)。final一般不用於可變的類,容易引起讀者的理解混亂,例如:
private final StringBuilder evaluations ;
在 Employee 構造器中會初始化為
this.evaluations = new StringBuilder() ; final關鍵字只是表示存儲在 evaluations 變量中的對象引用不會再指示其他 StringBuilder對象。不過這個對象可以更改: public void giveGoldStar() { evaluations . append ( LocalDate . now ( ) + " : Gold star ! \ n " ) ; }
19,靜態域與靜態方法:
※,靜態域、靜態方法屬於類不屬於對象(或稱為實例),所以靜態方法中不可調用實例域,也不可調用實例方法。但是反過來,實例(或實例方法)可以調用靜態域,也可以調用靜態方法,但是不提倡,見下條。
※,可以使用對象調用靜態方法。例如,如果harry是一個Employee對象,可以用harry.getNextId()代替Employee.getNextId()。不過,這種方式很容易造成混淆,其原因是getNextld方法計算的結果與harry毫無關系。我們建議使用類名,而不是對象來調用靜態方法。
※,在下面兩種情況使用靜態方法:
- 一個方法不需要訪問對象狀態(訪問對象狀態意思即 實例/對象 作為方法的調用者,實例/對象 也稱為隱式參數),其所需參數都是通過顯式參數提供(例如:Math.pow)。相反的例子是:實例化一個日期對象LocalDate date, date.plusDays(100),這個方法依賴於對象的狀態(某個日期)。
- 一個方法只需要訪問類的靜態域。
※,如果查看一下System類,就會發現有一個setOut方法,它可以將System.out設置為不同的流。讀者可能會感到奇怪,為什么這個方法可以修改final變量的值。原因在於,setOut方法是一個本地方法,而不是用Java語言實現的。本地方法可以繞過Java語言的存取控制機制。這是一種特殊的方法,在自己編寫程序時,不應該這樣處理。
System.setOut(new PrintStream(new File("xxxx\\a.txt")));
System.out.println("hello out");//往文件中打印了hello out
※,術語“static”有一段不尋常的歷史。起初,C引入關鍵字static是為了表示退出一個塊后依然存在的局部變量在這種情況下,術語“static”是有意義的:變量一直存在,當再次進入該塊時仍然存在。隨后,static在C中有了第二種含義,表示不能被其他文件訪問的全局變量和函數。為了避免引入一個新的關鍵字,關鍵字static被重用了。最后,C++第三次重用了這個關鍵字,與前面賦予的含義完全不一樣,這里將其解釋為:屬於類且不屬於類對象的變量和函數。這個含義與Java相同。
※,工廠方法:
20,方法參數:按值調用 和 按引用調用。
※,Java 程序設計語言總是采用按值調用。有些程序員(甚至本書的作者)認為Java程序設計語言對對象采用的是引用調用,實際上,這種理解是不對的(P137)。Java方法可以改變對象參數的狀態,但這種改變的原理並不是引用傳遞,而是形參得到的是對象引用(即實參是對象的引用)的拷貝,對象引用及它的拷貝同時引用同一個對象。具體參考書中敘述圖解。
※,(C語言資料,這段話對理解Java對對象的按值調用很有幫助!)形參相當於是實參的“別名”,對形參的操作其實就是對實參的操作,在引用傳遞過程中,被調函數的形式參數雖然也作為局部變量在棧中開辟了內存空間,但是這時存放的是由主調函數(自己理解:main函數)放進來的實參變量的地址。被調函數對形參的任何操作都被處理成間接尋址,即通過棧中存放的地址訪問主調函數中的實參變量。正因為如此,被調函數對形參做的任何操作都影響了主調函數中的實參變量。
※,下面總結一下Java中方法參數的使用情況:
- 一個方法不能修改一個基本數據類型的參數(即數值型或布爾型)。
- 一個方法可以改變一個對象參數的狀態。
- 一個方法不能讓對象參數引用一個新的對象。
21,對象構造器
※,方法名和方法參數類型 在一起叫做方法簽名。方法返回類型不是方法簽名的一部分。方法重載(英文名實際上叫超載,是類的能力,超載的能力)需要方法的簽名不同。
※,域(即類的屬性)與局部變量的主要不同點:必須明確地初始化方法中的局部變量。但是,如果沒有初始化類中的域,將會被自動初始化為默認值(數值為0、布爾值為false、對象引用為null,如String類型默認為null)。
※,如果類中沒有任何一個構造器,那么系統會提供一個無參數構造器,這個構造器將所有的實例域設置為默認值。於是,實例域中的數值型數據設置為0、布爾型數據設置為false、所有對象變量將設置為null。注意只有類中沒有任何構造器時系統才會提供一個默認的無參數構造器,如果類中至少有一個構造器,但是沒有提供無參數構造器,則在構造對象時沒有提供參數會被視為不合法。
※,顯式域初始化:可以通過不同重載的構造器設置類的實例域的初始狀態。當一個類的所有構造器都希望把相同的值賦予某個特定的實例域時,可以直接在類聲明中將初始值賦給域。
域的初始值不一定是常量,例如:
class Employee {
private static int nextId = 1; private int id = assignId();// 初始化對象時執行 Employee e = new Employee() private String name; Employee (String name) { this.name = name; } public static int assignId() { int r = nextId; nextId++; return r; } }
※,調用另一個構造器:類中的this指代類方法的隱式參數,java類中,this可以省略,但最好帶上。this關鍵字還有另外一個含義,即調用另一個構造器。例如,
publicEmployee(doubles)
{
//calls Employee(String, double)
this("Employee#" + nextld, s);// 形如這樣,表示調用另一個構造器
nextld++; }
※,初始化塊 ☆
1, 前面講了java兩種初始化數據域的方法:①在構造器中設置值。②在聲明中賦值。實際上java還支持第三種機制:初始化塊。初始化塊中可以有多行代碼,只要構造類的對象,這些塊就會被執行。
2,初始化數據域的順序:
- 所有數據域被初始化為默認值(0、false或null)。
- 按照在類聲明中出現的次序,依次執行所有域初始化語句和初始化塊。
- 執行構造器方法。
3,如果靜態域的初始化代碼比較復雜也可以使用靜態初始化塊。只要在代碼放在一個塊中,並標記關鍵詞static即可。在類第一次加載時,所有的靜態初始化語句以及靜態初始化塊都將依照類定義的順序執行。
如將靜態域nextId起始值賦予一個10000以內的隨機整數:
static { Random generator = new Random(); nextId = generator.nextInt(10000); }
4,
※,對象析構與finalize方法: 由於Java有自動的垃圾回收器,不需要人工回收內存,所以Java不支持析構器。
22,包
※,java.lang包是被默認導入的。
※,所有標准的java包都處於java 和 javax 包層次中。
※,從編譯器的角度來看,嵌套的包之間沒有任何關系。例如,java.util包與java.util.jar包毫無關系。每一個都擁有獨立的類集合。
※,修飾符:public,package-private(即沒有任何修飾符時的默認值),protected,private
- 類的權限修飾符有兩個:public(對任何地方的類都是可見的),package-private(只對自己所在的包內的所有類可見,注意嵌套的包之間毫無關系)。
- 類中成員的修飾符有4個:public, package-private(只對自己包內的所有類可見), protected( 對自己包內的所有類以及其他包內本類的子類可見),private(只對本類可見)
- 注意 protected: 若子類與父類不在同一包中,那么在子類中,子類實例可以訪問其從父類繼承而來的protected方法,而不能訪問父類實例的protected方法,一個典型的例子就是Object類中的clone()方法,雖然是protected修飾符,但是不在java.lang包中的子類如果不重寫這個clone()方法是無法直接調用Object的clone()方法的。參見此篇文章。
- Java中的protected概念和C++稍有不同, 比C++中的 protected 安全性差。
修飾詞 | 本類 | 同一個包的類 | 繼承類 | 其他類 |
private | √ | × | × | × |
無(默認) | √ | √ | × | × |
protected | √ | √ | √ | × |
public | √ | √ | √ | √ |
※,靜態導入:import語句不僅可以導入類,還增加了導入靜態方法和靜態域的功能。
import static java.lang.System.*; out.println("fuck U");//
※,如果沒有在源文件中放置package語句,這個源文件中的類就被放置在一個默認包(default package中),默認包是一個沒有名字的包。類的路徑必須和包名一致。
javac ./com/learn/java/Test.java //編譯器命令可以在任何目錄下執行,只要能找到源代碼文件(編譯后的class文件叫類文件,java為后綴名的文件叫源文件)即可。
java com.learn.java.Test // 解釋器命令必須在基目錄下執行,即包含子目錄com的目錄。
※,如果沒有指定public或private, 這個部分(類、方法或變量(域))可以被同一個包中的所有方法訪問。
※,類路徑(class path): 類路徑就是所有包含類文件(即編譯后的class文件)的路徑的集合,即告訴編譯器和虛擬機去哪兒找類文件。
- 在Unix系統中,不同的類路徑之間用冒號分隔,
- Windows環境中,用分號分隔。如下類路徑:
- c:\classdir;.;c:\archives\archive.jar 包含3個部分,第二個是當前路徑,第三個是jar包,jar包是包含一系列class文件壓縮包。java虛擬機尋找類的時候可以在jar包里搜索class文件。
- 由於運行時庫文件(jre/lib/rt.jar和在jre/lib 與 jre/lib/ext 目錄下的一些其他的jar文件)會被自動地搜索,所以不必將它們顯式地列在類路徑中。
- java 虛擬機搜尋類文件過程。
- 編譯器搜尋定位源代碼文件的過程。
- 設置類路徑:
- 采用-classpath(或 -cp)選項指定類路徑,這是設置類路徑的首選方法: java -classpath 'c:\classdir;.;c:\archives\archive.jar' MyProg。整個指令必須書寫在一行,經測試Windows下類路徑要用引號引起來。
- 除首選方法外,也可以通過設置CLASSPATH環境變量來設置類路徑,直到退出shell為止,類路徑設置均有效。
- 在bash中,命令如下,export CLASSPATH=/home/user/classdir:.:/home/user/archives/archive.jar
- 在Windows shell中,命令如下, set CLASSPATH=c:\classdir;.;c:\archives\archive.jar。
- 有人建議將CLASSPATH環境變量設置為永久不變的值。總的來說這是一個很糟糕的主意。
- 有人建議繞開類路徑,將所有的文件放在jre/lib/ext路徑。這是一個極壞的主意。
※,JAR(Java Archive)文件:
- JAR文件可以將類文件(即class文件)打包(使用zip壓縮方式)成一個單個的文件,里面可以包含類文件(即class文件),也可以包含圖片或音頻等文件。
- 可以使用jar命令創建jar文件,jar.exe是JDK默認安裝的一部分,位於jdk/bin目錄下。使用方法和Linux下的tar命令很類似。
- jar -cvf jarFileName.jar(此處可以為絕對路徑或相對路徑,如:D:/test.jar) file1 file2 file3 ... //file 可以是任意的文件, 一般主要是class文件和資源文件
- 參數解釋:
- -c 創建一個新的jar文件,並將指定的文件添加其中。如果指定的文件是文件夾,jar程序將自動遞歸處理。
- -C 改變目錄。jar cvf jarFileName.jar -C ../ xx.class //將當前目錄的上一級目錄中的xx.class文件添加到jar文件中。
- -e 創建一個manifest條目
- MANIFEST.MF文件
- 每個jar文件里都有一個 指示 文件MANIFEST.MF。位於jar文件的META-INF子目錄下。
- MANIFEST.MF文件包含若干個節(section),第一節是主節,描述的是整個jar文件。不同的節之間使用空行分隔。主節之外的其他節可以描述單個的文件或包或URL等。這些副節中的條目必須有一個Name條目打頭。例如:
Manifest-Version: 1.0 Name: Woozle.class Name: com/mycompany/mypkg/
- 執行jar文件:
- 使用命令 jar -cvfe jarFileName.jar com.learn.java.Test xx.class yy.class 可以在MANIFEST.MF的主節中添加一個條目: Main-Class: com.learn.java.Test 或者手動在文件里添加也行。
- 然后就可以使用 java -jar jarFileName.jar 運行這個jar文件,從剛才設置的主類開始運行。
- Java 9之后支持多版本的jar包。在jar包里可以設置多個版本的類文件。具體待研究
※, 文檔注釋:JDK中包含一個很有用的工具叫javadoc。Java的API文檔就是通過對標准Java類庫的源代碼運行javadoc生成的。
- 注釋中如要添加等寬字體,不要使用<code>xxx</code> 而要使用{@code something} ,就不用擔心<字符的轉義了。
- 包注釋方法:
- 運行javadoc命令生成注釋文檔的方法:
- 切換到想要生成文檔的源文件目錄,如果有嵌套的包需要生成文檔,例如com.learn.java,就必須切換到基目錄,即包含子目錄com的目錄。
- javadoc - d docDirectory nameOfPackage // 一個包
- javadoc - d docDirectory nameOfPackage1 nameOfPackage2 . . .// 多個包
- javadoc -d docDirectory *.java // 默認包的文檔生成
※,類設計技巧
- 一定要保持數據的私有(實例域的私有性),不要破壞封裝性。
- 一定要對數據初始化。Java不對局部變量進行初始化(如果局部變量沒有賦初值就使用,編譯器會報錯:變量沒有初始化),但是會對對象的實例域進行初始化,但是最好不要依賴於系統的默認值,而是應該顯式地初始化所有的數據。
- 不要在類中使用過多的基礎數據域,最好將有關聯的數據域封裝在一個類中,然后引用這個類。比如:使用一個Address類封裝以下字段,然后在Customer類中引入Address類,而不是直接在Customer類中使用這些基礎數據域。
private String street;
private String city;
private String state; - 不是所有的數據域都需要獨立的域訪問器和域更改器。在構造類的對象后,常常有一些不希望別人獲取或設置的實例域,這些域就不需要設置任何訪問器或更改器。
- 將職責過多的類進行分解。這個需要經驗積累。書中有個例子: 一副牌 和 一張牌 各設計為一個類,而不是將兩個概念混在一個類中。
- 類名和方法名要能夠體現它們的職責。
- 優先使用不可變的類(immutable classes)。LocalDate類以及java.time包中的其他類是不可變的—沒有方法能修改對象的狀態。類似plusDays的方法並不是更改對象,而是返回狀態已修改的新對象。更改對象的問題在於,如果多個線程試圖同時更新一個對象,就會發生並發更改。其結果是不可預料的。如果類是不可變的,就可以安全地在多個線程間共享其對象。因此,要盡可能讓類是不可變的,當然,並不是所有類都應當是不可變的。如果員工加薪時讓raiseSalary方法返回一個新的Employee對象,這會很奇怪。
※,
※,
第五章,繼承(inheritance)
※,本章還闡述了反射(reflection)的概念。反射是指在程序運行期間發現更多的類及其屬性的能力。這是一個功能強大的特性,使用起來也比較復雜。由於主要是開發軟件工具的人員,而不是編寫應用程序 的人員對這項功能感興趣,因此對於這部分內容,可以先瀏覽一下,待日后再返回來學習。
※,關鍵字extends表明正在構造的新類派生於一個已存在的類。已存在的類稱為超類(super class)、基類(base class)或父類(parent class);新類稱為子類(sub class)、派生類(derived class)或孩子類(child class)。
※,Java是單一繼承,即只允許繼承一個類。
※,Java子類繼承父類時,並沒有繼承父類的私有方法。但是如果父類的公有方法或protected方法訪問了父類的私有屬性,那么子類對象也可以訪問到父類的這些私有屬性。官方文檔說明如下:A subclass does not inherit the private
members of its parent class. However, if the superclass has public or protected methods for accessing its private fields, these can also be used by the subclass.【點我Java官方文檔】。
※,方法重寫(Override) 【點我查看官方文檔】
- 注意:父類中的private 方法無法被子類繼承,因此就不存在方法重寫。就是說,父類有一個private方法,子類可以有一個同簽名但是返回值類型不同的方法。
class Super { private void get() {} } class Sub extends Super { /** * 子類沒有繼承父類的私有get()方法 * 因此可以簽名相同但是返回值類型不同 */ public String get() { return "到頭來都是為他人作嫁衣裳"; } }
- 方法重寫含義: 如果一個子類方法的 簽名(名稱和 參數個數、類型) 和 返回值 和父類的一個方法的簽名和返回值相同(子類返回值也可以是父類返回值的子類型),那么子類中的這個方法重寫了父類中的這個方法。只要滿足條件無論加不加@Override注解都是方法重寫。注意:重載(overload)要求是:方法名稱相同,但是參數個數或類型不同,重載並不檢查返回類型。
- 子類繼承父類時,如果子類和父類方法簽名相同(不考慮父類的private方法),那么返回值類型也要兼容。不允許子類方法的簽名和父類相同,但是返回值類型不同。原因是如果允許這樣,子類就會繼承父類的那個方法,那么子類方法中就有兩個方法簽名相同但是返回值類型不同的方法。這就相當於重載時只有返回值類型不同的兩個方法,是不被允許的。
- final 方法無法被子類重寫。但是子類可以重載一個同名方法。final類無法被繼承。
- static 方法無法被子類實例方法重寫。但是子類可以重載一個同名方法。
- 父類的static方法,子類也可以有一個方法簽名和返回值類型相同(兼容)的static 方法。此時叫做子類的靜態方法隱藏了父類的同名靜態方法。
- 子類靜態方法無法隱藏父類同簽名同返回值類型的實例方法(編譯錯誤)。
- 父類有一個static方法,子類也有一個同簽名的static方法,此時亦要求兩個方法的返回值類型兼容。
- 總結一下(不保證100%正確):
- 只要子類和父類的方法簽名相同(父類方法是private時,不在此規則內),那么兩個方法的返回值類型也要兼容。
※,子類中使用super關鍵詞調用超類的方法。有些人認為super和this引用是類似的概念,這是錯誤的。super不是一個對象的引用,不能將super賦給另一個對象變量(而this是對象引用,可以賦值給另一個變量)。super 只是一個指示編譯器調用超類方法的的特殊關鍵字。
※,子類構造器:
- 可以使用super(String name, int id) 實現對超類同樣函數簽名的構造器的調用。使用super調用超類構造器的語句必須是子類構造器的第一條語句。
- 如果子類構造器沒有顯式調用超類的構造器,則編譯器會自動調用超類默認(沒有參數)的構造器。如果超類中沒有不帶參數的構造器,並且在子類的構造器中沒有顯式的調用超類的其他構造器,那么Java編譯器將報錯!
※,多態(polymorphism):此書中多態是在繼承章節中的一小節,多態是繼承導致的,繼承是多態的前提。
0,兩個簡單例子:
1, Parent p = new Child(); /*當用父類引用來接收一個子類類型的對象時,對象變量p被編譯器視為是Parent類型的,但是調用父子類中都有的方法p.getName()時
(注意此時如果Parent類中如果沒有getName()方法,編譯器會報錯) ,實際調用的是子類Child中的getName方法。 p實際指向的是子類對象的引用。*/ 2, Parent[] parents = new Parent[3] Child c = new Child(); parents[0] = c; parent[1] = new Parent(); parent[2] = new Parent(); for (Parent e : parents) { System.out.println(e.getName()); } // 盡管這里將e聲明為Parent類型,但實際上e既可以引用Parent類型的對象,也可以引用Child類型的對象。 // 當e引用Parent對象時,e.getName()調用的是Parent類中的getName方法,當e引用Child對象時,e.getName()調用的是Child對象的方法。 像這種在運行時能夠自動地選擇調用哪個方法的現象稱為動態綁定(dynamic binding)1,一個對象變量可以指示多種實際類型的現象叫做多態。
2,多態存在的三個條件:
- 繼承
- 重寫(方法覆蓋)
- 父類引用指向子類對象
3,當使用多態方式調用方法時,首先檢查父類中是否含有該方法,如果沒有則編譯器報錯;如果有再去調用子類的同簽名方法(具體敘述見下)。
3.1, 注意:父類引用指向子類的對象時,父類引用只能調用父類已有的方法。如果子類沒有重寫父類的方法,就不存在多態,因為調用的還是父類的。所謂的多態就是需要對同一個方法的調用產生不同的狀態,不重寫也就沒有多態(但也不會報錯)。
4,靜態綁定和動態綁定:(Java編譯器將源碼編譯成class文件供Java虛擬機執行)
- 靜態綁定(前期綁定)是指在程序運行前就已經知道方法是屬於哪個類的,在編譯時就可以連接到類中,定位到這個方法。在Java中,final,static, private修飾的方法以及構造函數都是靜態綁定的,不需程序運行,不需具體的實例對象就可以知道這個方法的具體內容。
- 動態綁定(后期綁定)是指在程序運行過程中根據具體的實例對象才能具體確定是哪個方法。動態綁定是多態得以實現的重要因素。動態綁定通過方法表來實現:虛擬機預先為每個類創建一個方法表(method table),在真正調用方法的時候虛擬機僅查找這個表就行了。方法表中記錄了這個類中定義的方法的指針,每個表項指向一個具體的方法代碼。如果這個類重寫了父類中的某個方法,則對應的表項指向新的代碼實現處。從父類繼承來的方法位於子類定義的方法的前面。
5,向上轉型(upcasting) 和 向下轉型(downcasting)
向上轉型:通俗的講向上轉型就是將子類對象轉為父類對象,此處父類對象可以為接口。向上轉型不需要強制轉換。
- 向下轉型:將父類對象轉為子類對象叫做向下轉型。向下轉型需要強制轉換。且有可能出現編譯通過但運行時錯誤的向下轉型。
Parent p = new Child(); // 這個就叫做向上轉型。無需強制轉換。 Child c = (Child) p;// 這個就叫做向下轉型。需要強制轉換。此時編譯和運行都不會報錯,因為p實際指向是一個子類對象。
System.out.println(p instanceof Child);// true System.out.println(p instanceof Parent);// true Parent p1 = new Parent(); Child c1 = (Child) p1;// 這里編譯器不會報錯,但是運行時會報錯(ClassCastException),因為p1實際指向的是父類的對象。 System.out.println(p1 instanceof Child);// false System.out.println(p1 instanceof Parent);// true
應該養成這樣一個良好的程序設計習慣:在將超類轉換成子類之前,應該使用 instanceof 進行檢查是否能夠轉換成功。
null instanceof C ;// 始終為false。因為null沒有引用任何對象,當然也不會是引用C類型的對象。- 向上轉型的一個好處就是可以使代碼變得簡潔。比如:
Animal類有3個子類:Cat, Dog, Bird。每個子類都重寫了Animal類的 bark()方法,每種動物的叫聲都不一樣。
現有一個Test類,其中有個方法是getBark(Animal animal)。此時參數只要是父類的Animal即可,
Test的實例調用getBark()方法時可以傳入不同的動物,如getBark(cat)等等,此方法可以根據傳入的不同的動物類型發出正確的叫聲。
如果沒有向上轉型,那么getBark()這個方法就需要寫多個,有幾個子類動物就需要寫幾個。6,動態綁定的編譯、運行原理:
- 編譯階段:向上轉型時是用父類引用執行子類對象,並可以用父類引用調用子類中重寫了的同簽名方法。但是不能調用子類中有但父類中沒有的方法。原因在於在代碼的編譯階段,編譯器通過聲明的對象的類型(即引用本身的類型)在方法區該類型的方法表中查找匹配的方法(最佳匹配法:參數類型最接近的被調用,比如int可以轉成double),如果查找到了則編譯通過。向上轉型時,父類引用的類型是父類,所以編譯器在父類的方法表中查找匹配的方法,所以子類中新增的方法是查不到的,如果查不到編譯器就會報錯。
- 運行階段:當Parent p = new Child(); p.say();語句編譯通過后,進入Java虛擬機執行階段,執行Parent p = new Child()語句時,創建了一個Child實例對象,然后在p.say()調用方法時,JVM會把剛才創建的Child對象壓入操作數棧,用它來進行調用,這個過程就是動態綁定:即用實例對象所屬的類型去查找它的方法表,找到匹配的方法進行調用。子類的方法表包含從父類繼承來的方法以及自己新增的方法,如果p.say()在子類中被重寫了,那么JVM就會調用子類中的這個方法, 如果沒有被重寫,那么JVM就會調用父類的這個方法。以此類推。
7,子類覆蓋父類的方法(即方法重寫)時,子類方法不能降低父類方法的可見性。特別是,當父類方法是public時,子類方法一定要是public。
※,阻止繼承:final類和方法
- 可以將類中的某個方法聲明為final,表示這個類的這個方法不能被子類重寫。
- 也可以將類聲明為final,表示這個類不能被繼承。被聲明為 final 的類的所有方法會自動的成為 final 方法。注意,只是final 類的方法會自動成為 final 方法,final類中的域不會自動成為final域。
※,強制類型轉換:
※,抽象類:
- 可以用關鍵字 abstract 將一個方法定義為一個抽象方法,抽象方法只有簽名和返回類型,不需要實現。
- 類中只要含有抽象方法,那么這個類就必須被定義為抽象類。
- 抽象類中不必全是抽象方法,也可以含有具體實現的方法。
- 繼承抽象類時可以:①不實現抽象方法,那么這個子類也必須被定義為抽象類;②實現全部抽象方法,那么這個子類就可以不必為抽象類。
- 一個類中即使不含有任何抽象方法, 也可以將類聲明為抽象類。
- 抽象類不能被實例化,但是可以定義一個抽象類的對象變量,這個對象變量只能引用非抽象子類的對象。例子如下:
-
假設,Person是抽象類,Student和Employee是Person的非抽象子類。 1,Person p = new Student("晴雯"); 2, Person[] people = new Person[2]; people[0] = new Student("黛玉"); people[1] = new Employee("秦可卿");
※※※,Object 類: 所有類的超類
一,概述
- Object 類是Java中所有類的超類,在Java中每個類都是由它擴展而來的。所以熟悉這個類提供的所有服務十分重要。本章介紹一些基本的內容,沒有提到的部分可以參考后面的章節或在線文檔
- 在Java中,只有基本類型(8種)不是對象。
- Java中所有的數組類型,不管是對象數組還是基本類型數組都繼承了Object類。
二,equals()方法:
※,Object 類中的 equals()方法僅在兩個對象具有相同的引用(即兩個對象指向同一塊存儲區域)時才返回true,這是Object類的默認操作。但是對於多數類來講,這種判斷並沒有什么意義。實際上,經常需要檢測兩個對象狀態的相等性,如果兩個對象狀態相等(即某些域相等)就認為這兩個對象是相等的。所以經常需要重寫這個方法,以實現對象狀態相等(即某些域相等)就可以返回true。
※,假設兩個 Employee 對象,如果對象的姓名,薪水,和雇用日期都是相同的,就認為他們是相等的。以下為Employee類重寫的equals()方法:
public boolean equals(Object otherObject) {
// 兩個對象引用是否指向同一個對象,即完全相同
if (this == otherObject) return true; if (null == otherObject) return false; // this.getClass(): 獲取類的名稱。這里用getClass()判斷實際有點爭議,見下文 if (getClass() != otherObject.getClass()) return false; // 現在otherObject肯定是個非null的 Employee對象 Employee other = (Employee) otherObject; /* * 為防備name 或 hireDay可能為null的情況,這里使用了Objects.equals()方法而不是直接用name.equals(other.name). */ return Objects.equals(name, other.name)&& salary == other.salary && Objects.equals(hireDay, other.hireDay); }
※,在定義子類的equals()方法時,首先要調用超類的equals()方法,如果檢測失敗,對象就不能相等。如果超類中的域到相等,就需要比較子類中的實例域。
以下為Employee的子類Manager類重寫的equal()方法
public boolean equals(Object otherObject) {
if (!super.equals(otherObject)) return false; // 通過了超類的檢測,說明otherObject 和 this 屬於同一個類:Manager Manager other = (Manager) otherObject; return bonus == other.bonus; }
※,繼承中涉及到的equals()方法:
1,上面重寫的equals()方法使用了對象的getClass()方法比較兩個對象是否屬於同一個類來作為是否equals的一個條件,這個條件有些爭議。如下論述:
- 如果現在有需求:一個Manager對象的Id和一個Employee對象的Id相等,就認為這個Manager對象和這個Employee對象是相等的,那么此時getClass()方法便不再適用了。此時Employee類中的equals方法需要使用 instanceof 來進行檢測: if (!(otherObject instanceof Employee)) return false;
2,反過來講,如果使用 instanceof 作為判斷兩個對象equals的條件,也有不合適的地方。如下論述:
Java語言規范要求equals()方法必須具有如下特性:
- 自反性(reflexive): 對於任何非空引用x, x.equals(x)應該返回true。
- 對稱性(symmetric): 對於任何引用x和y, 當且僅當x.equasl(y)返回true,y.equals(x)也應該返回true。
- 傳遞性(transitive): 對於任何非空引用x,y和z,如果x.equals(y)返回true,y.equals(z)返回true,那么x.equals(z)也應該返回true。
- 一致性(consistent):如果x和y引用的對象沒有發生變化,反復調用x.equals(y)應該返回同樣的結果。
- 對於任意非空引用x, x.equals(null)應該返回false。
假設e為Employee的一個對象,m為Manager的一個對象,如果使用instanceof作為判斷兩個對象equals的條件,根據上面的對稱性規則,如果e.equasl(m)為true,那么m.equals(e)也必須返回true。這就使得Manager類受到了束縛:這個類的equals方法必須能夠用自己與任何一個Employee對象進行比較,這樣就會忽略Manager對象特有的信息。這樣就會導致兩個Manager對象無法比較是否是equals的, 只要m1和 m2 的name, salary, hireDay一致,那么兩個對象就是equals的, 無法比較兩個Manager對象的bonus是否相等。解決方法見下。
3,關於是使用getClass()方法還是使用 instanceof 操作符來作為判斷兩個對象是否equals的條件,可以從以下兩個角度看待:
- 如果子類能夠擁有自己的相等概念(比如,需要bonus也相等,兩個manager對象才相等),則由於equals()方法的對稱性,需要使用getClass()方法來判斷是否相等。
- 如果由超類決定相等的概念(比如,只要Employee或Manager的兩個對象的id相等,這兩個對象就是相等的),那么就可以使用instanceof進行檢測。這樣可以在不同子類的對象之間進行相等的比較。
※,關於equals()方法的一種常見的錯誤:
public boolean equals(Employee otherObject) {
// 兩個對象引用是否指向同一個對象,即完全相同
if (this == otherObject) return true; if (null == otherObject) return false; // this.getClass(): 獲取類的名稱 if (getClass() != otherObject.getClass()) return false; // 現在otherObject肯定是個非null的 Employee對象 Employee other = (Employee) otherObject; /* * 為防備name 或 hireDay可能為null的情況,這里應該使用Objects.equals()方法優化一下 * Objects.equals(name, other.name); Objects.equals(hireDay, other.hireDay); */ return name.equals(other.name) && salary == other.salary && hireDay.equals(other.hireDay); }
// 錯誤的地方在於equals()方法的參數類型是Employee。其結果是這個equals()方法並沒有覆蓋Object類的equals()方法,而是定義了一個完全無關的方法。為了避免發生類型錯誤,
可以使用@Override(比...更重要)對覆蓋超類的方法進行標記。如果出現了錯誤並且正在定義一個新的方法,編譯器就會給出錯誤報告。
※,java.util.Objects.equals(a, b)方法: 這個方法對null是安全的, 如果兩個參數都為null,Objects.equals(a,b)調用將返回true;如果其中一個參數為null,則返回false;否則,
如果兩個參數都不為null,則調用a.equals(b)
※,java.util.Arrays.equals(arr1, arr2): 對於兩個數組類型,可以使用Arrays.equals()檢測兩個數組是否相等。如果兩個數組以相同的順序包含相同的元素,則他們是相等的,否則就是不相等的。
三,hashCode()方法:
※,int java.lang.Object.hashCode(): 返回對象的散列碼。散列碼可以是任意的整數,可以整數也可以負數。兩個equals相等的對象要求返回相等的散列碼。
※,散列碼(hash code)是由對象導出的一個整形值。散列碼是沒有規律的,如果x和y是兩個不同的對象,那么x.hashCode()和y.hashCode()基本不會一樣。
※,hashCode()方法定義在Object類中,因此每個對象都有一個默認的散列碼,這個散列碼值是由對象的內存存儲地址推導出來的。
※,如果重新定義equals()方法,就必須重新定義hashCode()方法,以便用戶可以將對象插入到散列表中(散列表后面講述).
※,hashCode()方法的定義:
- hashCode()方法應該返回一個整形數值(可以是負數)。合理的組合實例域的散列碼以便能夠讓各個不同的對象產生的散列碼更加均勻。下面是Employee類的hashCode()方法的一個例子:
@Override
public int hashCode() { /** * static int java.util.Objects.hashCode()方法是null安全的方法,如果參數為null,返回0. * 否則對參數調用hashCode()方法。比如name.hashCode(); hireDay.hashCode(); * 使用靜態方法 static int java.lang.Double.hashCode()可以避免創造Double對象:new Double(salary).hashCode(); */ return 7 * Objects.hashCode(name) + 11 * Double.hashCode(salary) + 13 * Objects.hashCode(hireDay); }
- 定義Employee類的hashCode()方法還有一個更好的方法就是使用Objects.hash()並提供多個參數。這個方法會對各個參數調用Objects.hashCode()並組合這些散列值。即:
-
@Override public int hashCode() {
// static int java.util.Objects.hash(Object... objects) return Objects.hash(name, salary, hireDay); } - static int java.util.Arrays.hashCode(Type[] a): 計算數組a的散列碼。這個散列碼有數組元素的散列碼組成。
※,equals()方法與hashCode()方法的定義必須一致:如果x.equals(y)方法返回true,那么x.hashCode()就必須和y.hashCode()具有相同的值。否則就會出現問題(具體什么問題待研究)。也就是說,equals相等的兩個對象的hashCode也要保證相等。但是反過來兩個hashCode相等的對象不一定需要equals相等(這個好理解:假設兩個不同的類有相同的屬性和hashCode規則,那么hashCode相等而不equals相等)。比如,如果用定義的Employee.equals()比較雇員的ID,那么hashCode()方法就需要散列ID而不是雇員的姓名或繼承自Object的存儲地址推導出來。
四:toString()方法:
※,Object類中還有一個很重要的方法toString(),它用於返回表示對象值的字符串。Object類默認的toString()方法返回值是:對象所屬的類名和散列碼。如下例
- 首先:1,調用println(x)方法會直接調用x.toString()方法。2,只要一個對象與一個字符串通過操作符"+"連接起來,Java編譯器就會自動調用toString()方法,以便獲得這個對象的字符串描述。
- 例如調用System.out.println(System.out); 輸出以下內容:java.io.PrintStream@15db9742。因為PrintStream類的設計者沒有重寫覆蓋Object類的toString()方法,直接繼承了Object類的toString()方法。
- Java中的數組也沒有實現自己的toString()方法,也是繼承了Object類的toString方法。所以int[] arr = {1,2,3,4}; System.out.println(arr);//輸出結果是【 [I@6d06d69c】,【前綴[I表明是一個整形數組】。修正的方式是調用靜態方法 Arrays.toString(arr) ,打印多維數組就使用Arrays.deepToString(arr)方法。
※,絕大多數重寫了toString()方法的類都遵循這樣的規則:類的名字,隨后是一對方括號括起來的域值。
- 比如,Point p = new Point(10,29); System.out.println(p);// 輸出結果是: java.awt.Point[x=10,y=29]
- 下面是Employee類中toString()方法的實現
/** * 不直接寫Employee而是使用this.getClass().getName()獲取類名更具普適性。 * 比如繼承了此類的子類實現自己的toString()方法時可以直接調用super.toString()方法就可以得到子類自己的類名 */ @Override public String toString() { return this.getClass().getName() + "[name=" + this.name + ",salary=" + this.salary +",hireDay=" + this.hireDay +"]"; }
- 子類也應該有自己的toString()方法,以下是Manager類的toString()方法的實現:
@Override public String toString() { return super.toString() +"[bonus=" + this.bonus +"]"; }
※,在調用x.toString()方法的地方可以使用【""+x】替代,此時編譯器會自動調用x.toString()。這種寫法的好處是:如果x是基本類型,基本類型卻沒有toString()方法,這條語句照樣可以執行。
※,API
- java.lang.Object Class getClass();//返回包含對象信息的類對象
- java.lang.Class String getName();//返回類名
- java.lang.Class Class getSuperclass();//以Class對象的形式返回這個類的超類信息。
24,泛型數組列表(Generic Array List)
※,出現數組列表ArrayList的背景
- 在C++中,必須在編譯的時候就要確定整個數組的大小,這個很不方便。比如,有的員工部門100個員工,有的只有10個,願意為僅有10個員工的部門浪費90個員工占據的存儲空間嗎?
- Java中,情況好一些。它允許在運行時確定數組的大小: int actualSize = ...(動態確定大小的代碼); Employee[] staff = new Employee[actualSize]; 當然這段代碼並沒有完全解決運行時動態更改數組的問題。一旦確定了數組的大小,它就不可更改了。
- Java中解決這個問題最簡單的方法是使用Java中另外一個被稱為ArrayList的類。
※,使用ArrayList類: java.util.ArrayList<E> SE1.2
- ArrayList是一個采用類型參數(type parameter)的泛型類(generic class)。為了指定數組列表保存的元素對象類型,需要用一對尖括號將類名括起來加在后面,如ArrayList<Employee>。
- ArrayList<Employee> staff = new ArrayList<Employee>();// 在Java SE7之后,可以省去右邊的類型參數。即ArrayList<Employee> staff = new ArrayList<>();編譯器將檢查變量staff的泛型類型,然后將這個類型放入右邊的<>中
- 使用add()方法添加一個元素到數組列表中。staff.add(new Employee());
※,動態改變大小的原理
- 數組列表管理着對象引用(staff)的一個內部數組。如果調用add()方法時內部數組空間已經被用完了,數組列表就將自動創建一個更大的數組,並將所有的對象從較小的數組中拷貝到較大的數組中。
- 如果能夠估計出數組可能存儲的元素數量,可以在填充數組之前就確定數組列表的容量,有如下兩個方法:
- 調用ensureCapacity()方法,staff.ensureCapacity(100);
- 將初始容量傳遞給ArrayList構造器。ArrayList<Employee> staff = new ArrayList<>(100);
- 指定容量后,編譯器將分配一個包含100個對象的內部數組。然后調用100次add()方法而不用重新分配空間。
-
分配數組列表,如下所示 new ArrayList<Employee>(100) // capacity is 100,but size now is 0. 它與為新數組分配空間有所不同: new Employee[100] // size is 100 數組列表的容量與數組的大小有一個非常重要的區別。如果為數組分配100個元素的存儲空間,數組就有100個空位置可以使用。而容量為100個元素的數組列表只是 擁有保存100個元素的潛力(實際上重新分配空間的話,將會超過100),但是在最初,甚至是完成初始化構造之后,數組列表根本就不含有任何元素。
---------------------------------------------------------------------------------------------------------------------------
Employee[] staff = new Employee[2];
System.out.println(Arrays.toString(staff));// 打印:[null, null]
System.out.println(staff.length);// 打印:2
----------
ArrayList<Employee> staff = new ArrayList<>(2);
System.out.println(staff);// 打印:[]
System.out.println(staff.size());//打印:0 - 一旦能夠確認數組列表的大小不再發生變化,就可以調用trimToSize()方法。這個方法將存儲區域的大小調整為當前元素數量所需要的存儲空間數目。垃圾回收器將回收多余的存儲空間。注意一旦調用了trimToSize()方法,再添加新元素就需要花時間再次移動存儲塊。所以應該在確定不會再添加任何元素時再調用trimToSize()方法。
※,訪問ArrayList元素
- ArrayList類並不是Java程序設計語言的一部分,它只是一個由某些人編寫且被放在標准庫中的一個實用類(可以理解為ArrayList是Array的一個加強版)。訪問ArrayList的元素使用的語法是get()和set()方法,而不是Java中的[]語法格式。
- list.set(i, xxx);//用於設置數組列表list的第i個元素,將其設置為xxx。注意,這個方法只能替換數組中已經存在的元素內容,如果不存在,代碼運行時會報錯。
ArrayList<Employee> employees = new ArrayList<>(100);//容量100,但是大小此時還是0 employees.set(0, new Employee());// employees中尚未含有第0個元素,編譯通過但運行時報錯。
- 使用add()方法添加新元素而不要用set()方法。
- 可以使用toArray()方法將ArrayList類型轉換為Array類型
ArrayList<Employee> list = new ArrayList<>(); for (int i = 0; i < max; i++) { x = ... list.add(x) } Employee[] staff = new Employee[list.size()]; list.toArray(staff);//list中的元素從前往后依次添加到staff中。 /** * list.toArray()方法詳解: * 1,此方法始終有返回值:Object[],即將list中的元素從前往后依次添加至返回值arr中 * 2,如果數組參數staff的大小與list的大小相同,那么除了添加至返回值arr中之外,list中的元素也會依次復制到staff中 * 3,如果staff的大小大於list的大小,arr和staff一樣,除了list中的元素外,多余的元素用null填充 * 4,如果staff的大小小於list的大小,那么staff將保持不變,不會被填充。arr則會正常填充list中的所有元素。這種情況和 * 直接調用不帶參數的toArray()效果相同。 * Object[] orr = list.toArray();效果等同於Object[] orr2 = list.toArray(new X[0]) */ Object[] arr = list.toArray(type[]
- 1
※,插入或刪除ArrayList元素
- 使用帶索引的add()方法插入元素:java.util.ArrayList void add(int index, E obj); // 在index位置插入一個元素,index之后的所有元素后移一個位置,並將數組大小加1
- java.util.ArrayList E remove(int index);// 刪除一個元素,后面的元素前移一位。被刪除的元素由返回值返回。index只能是0~size-1之間。
- 對數組實施插入或刪除元素的操作效率比較低。對於小型數組不必擔心,但是如果數組存儲的元素比較多,有經常需要在中間位置插入、刪除元素,就應該考慮使用鏈表了(后面講鏈表)。
25,對象包裝器與自動裝箱(Object Wrappers and AutoBoxing)
※,有時需要將基本類型轉換為對象。所有的基本類型都有一個與之對應的類。這些類稱為包裝器(wrapper)。這些對象包裝器類擁有很明顯的名字:Byte, Short, Integer, Long, Float, Double(這六個類派生自公共的超類Number), Character, Void和Boolean。注意:對象包裝器類是不可變的,即一旦構造了包裝器,就不允許更改包裝在其中的值。同時對象包裝器類還是final的,因此不能定義他們的子類。
※,數組列表ArrayList的泛型參數是不允許為基本類型的,即ArrayList<int> al = new ArrayList<>();是不合法的。此時就需要用到包裝器類Integer。注意:由於每個值分別包裝在對象中,所以ArrayList<Integer>的效率遠低於int[]數組。因此應該用它構造小型集合,其原因是此時程序員操作的方便性要比執行效率更加重要
※,自動裝箱(autoboxing)與自動拆箱(unbox): ArrayList<Integer> list = new ArrayList<>();
- list.add(3);// 此時自動變換為: list.add(Integer.valueOf(3)); 這種變換稱為自動裝箱。autoboxing這個詞來源於C#,在Java中或許自動包裝(autowrapping)這個詞更合適.
- 相反地,如果將Integer對象賦值給int值時,將會自動拆箱。即:int n = list.get(i);//翻譯為 int n = list.get(i).intValue();
- Integer n = 3; n++;//編譯器將自動地插入一條對象拆箱指令,然后進行自增計算,最后再將結果裝箱。
※,關於包裝器類的幾個注意事項:
- 比較兩個包裝器類一般使用equals()方法,【==】比較兩個包裝器對象時,檢測的是兩個對象是否指向同一個存儲區域。
- 因此 Integer a = 128; Integer b = 128; a==b;// 返回false。
- 但是注意,Java中自動裝箱規范要求boolean, byte, char<=127, 介於[-128~127]之間的short和int被包裝到固定的對象中。所以如果 Integer a = 127; Integer b = 127; a==b;//此時返回true。
- 當Integer n = null 時, 3 * n; 會報空指針異常。
- 如果一個表達式中混合使用Integer 和Double類型,Integer值就會自動拆箱,提升為double,然后再裝箱為Double。Integer n = 1; Double x = 2.0; sout(true? n : x);//打印1.0。
-
// 注意調用triple(n)方法並不能將n變成3倍。因為包裝器類Integer是不可變的! public static void triple(Integer x) { x = x * 3; }
- org.omg.CORBA定義的IntHolder等類型可以用於編寫修改數值參數值的方法。public static void triple(IntHolder x) {x.value = 3 * x.value;}
※,裝箱和拆箱操作是編譯器的行為,而不是Javad虛擬機的行為。編譯器在生成類的字節碼時插入必要的方法調用。虛擬機只是執行這些字節碼。
※,數值對象包裝器的另一個好處是: Java設計者發現可以將某些基本方法放置在包裝器類中,如 int x = Integer.parseInt(string);// parseInt()是一個靜態方法,這與Integer對象毫無關系。但是Integer類是放置這個方法的好地方。
※,API---java.lang.Integer 1.0
- int intValue();//返回Integer對象的int值。覆蓋了Number類中的intValue()方法。
- static String toString(int i);//
- static String toString(int i, int radix);//radix指明了第一個參數的進制,即 i 是radix進制的數值。
- static int parseInt(String s);
- static int parseInt(Strin s, int radix);// radix作用同上。
- static Integer valueOf(String s);
- static Integer valueOf(String s, int radix);// radix 作用同上。
26,參數數量可變的方法
※,在JavaSE5.0之前,每個Java方法都是固定參數個數。之后有了變參方法。一個例子就是PrintStream System.out.printf()方法。這個方法可以傳入任意個數的參數。這個方法的實現如下:
// ...語法表示這個方法可以接收任意數量的對象。
public PrintStream printf(String format, Object ... args) {
return format(format, args); } 1, 實際上,printf()方法接收2個參數,一個是格式化字符串,另一個是Object[]數組,這個數組中保存着所有的參數(如果調用者提供的是整形數組或其他基本類型的值,自動裝箱功能將把它們轉換成對象)。 2,就是說,Object ... 和 Object[]完全一樣。
※,一個自定義的可變參數的方法:參數類型可以任意,甚至可以為基本類型。
//
public static double max(double... values) {
double largest = Double.NEGATIVE_INFINITY; for (double v : values) { f (v > largest) { largest = v; } } return largest; } 1, 調用方式: max(3.1, 4.5, -5); 2, 編譯器將new double[] {3.1, 4.5, -5}傳遞給max方法。也可以直接這么調用max方法(即傳入一個數組),但是注意要保持類型的一致。
比如:public static void f(Object...args){System.out.println(Arrays.toString(args));},如果以數組參數形式調用此方法,那么這個數組必須是Object[]才能保證一一對應。
假如傳入的是int[]{1,2,4},那么這個int[]數組整體將被看成是Object[]中的一項。即打印出來的是【[[I@15db9742]】,而如果傳入的是Object[]{1,2,4}那么打印出來的便是[1,2,4]。
※,在Java中已經存在且最后一個參數是數組的方法可以重定義為可變參數的方法而不會破壞任何已經存在的代碼。比如大名鼎鼎的main方法就可以寫為:
public static void main(String ... args) {...}
※,
※,
27,枚舉類(Enumeration Classes)
※,JDK1.5之后出現了enum類型。可以單獨成類,也可以定義在class或interface之中。
※,枚舉的用法:
/** * 1,枚舉enum是一個特殊的Java類。它繼承自java.lang.Enum。枚舉類是final類,不能被繼承。 * 2,通過 cfr jar包反編譯可以發現,其實enum就是一個語法糖。 * 3,RED, GREEN, BLUE(叫做枚舉常量)實際上就是枚舉類RGB的實例,外部無法再構造出RGB的實例。因此,再比較兩個 * 枚舉類型的值時,不需要調用equals()方法,直接使用“==”就可以了。 * 4,枚舉的構造器只是在構造枚舉常量的時候被調用,即RED,GREEN,BLUE各調用一次。所以,枚舉常量的 * 形式要和構造函數保持一致。比如RED(1, "紅色")需要對應RGB(int a, String b){}形式的構造函數。 * 可以存在多個構造函數,因此也可以存在多種形式的枚舉常量,比如:RED(1,"紅色"),GREEN, BLUE;枚舉的
* 構造器只能用private修飾,如果沒有修飾符,默認也是private。 * 5, 枚舉常量要放在最前面,枚舉的域和方法要放在枚舉常量的后面。這個不明白為啥..
* 6, 可以在枚舉類中覆蓋超類Enum的一些方法,比如@Override public String toString(){return ...;} * */ enum RGB { RED(), GREEN, BLUE; } // 反編譯后的代碼如下: java -jar ../cfr_0_132.jar RGB.class --sugarenums false /* * Decompiled with CFR 0_132. */ public final class RGB extends Enum<RGB> { public static final /* enum */ RGB RED = new RGB(); public static final /* enum */ RGB GREEN = new RGB(); public static final /* enum */ RGB BLUE = new RGB(); private static final /* synthetic */ RGB[] $VALUES; public static RGB[] values() { return (RGB[]) $VALUES.clone(); } public static RGB valueOf(String string) { return Enum.valueOf(RGB.class, string); } private RGB() { super(string, n); } static { $VALUES = new RGB[] { RED, GREEN, BLUE }; } }
※,枚舉類的一些方法:java.lang.Enum<E> 5.0 RGB r = RGB.RED;
- String toString();// 返回枚舉常量名。 r.toString(); // RED;
- int ordinal();// 返回枚舉常量在enum聲明中的位置,位置從0開始計數。 r.ordinal();//0
- int compareTo(E other);// 如果枚舉常量的ordinal在other之前,返回負值;如果this == other,返回0;否則返回負值。 r.compareTo(RGB.GREEN);// -1
Class<E>
getDeclaringClass();//返回枚舉常量所在枚舉類的類對象。r.getDeclaringClass();// class com.learn.java.RGB
- String name();// 枚舉常量名。r.name();// RED;
- static E[] values();//返回一個包含全部枚舉常量的的數組。RGB[] values = RGB.values();//
static <T extends Enum<T>> T
valueOf(Class<T> enumClass, String name);// 返回指定枚舉常量名指定枚舉類型的枚舉常量。RGB g = Enum.valueOf(RGB.class, "GREEN");
※,枚舉與switch
RGB r = RGB.BLUE;
switch (r) { case RED://這里只能用RED,不能用RGB.RED System.out.println("rgb.red"); break; case BLUE: System.out.println("rgb.blue"); break; default: System.out.println("rgb..."); }
※,
28,反射(Reflection)
※,能夠分析類能力的程序稱為反射。也就是說,反射的代碼研究的是類本身,有點元數據(meta-data)的感覺。反射是一種功能強大且復雜的機制 。 使用它的主要人員是工具構造者, 而不是應用程序
員。
※,Class 類:描述類的一個類。
※,獲取Class類的對象的三種方法:
- Employee e = new Employee(); Class c = e.getClass();
- Class c = Class.forName("java.util.Random");// 靜態方法forName()的參數只有是類名或接口名時才能夠執行,否則,forName()方法將拋出一個異常。
- Class c = Employee.class; Integer.class; int.class; Void.class; void.class; 等等。
Class類實際上是泛型類。實際上應該寫為Class<Employee> c = Employee.class;
※,API: java.lang.Class 1.0
- String getName();// 返回類的名字
- static Class forName(String className);//返回一個Class對象。
- Object newInstance(); // 返回這個類的一個新實例。如: e.getClass().newInstance();將返回一個與e具有相同類型的的實例。newInstance()方法調用默認的(即沒有參數的)構造器初始化新建的對象,如果這個類沒有默認的的構造器就會拋出異常。
※,java.lang.reflect包中有三個類Filed, Method, Constructor 分別用於描述類的域、方法和構造器。里面的一些方法有需要后面再研究。
※,
29,繼承的設計技巧
※,將公共操作和域放在超類。
※,盡量不要使用protected 域。
- 子類集合是無限制的,任何一個人都可以由某個類派生出一個類子類,並編寫代碼以直接訪問protected的實例域。這破壞的封裝性。
- 在Java中,同一個包中的所有類都可以訪問protected域,而不管它是否是這個類的子類。
- 不過,protected方法對於指示那些不提供一般用途而應該在子類中重新定義的方法很有用。
※,除非所有繼承的方法都有意義,否則不要使用繼承。
- 假設想編寫一個Holiday類,如果繼承GrigorianCalendar,那么GregorianCalendar中的add()方法可以將某個holiday變為非holiday。因此繼承GregorianCalendar不合適。
- 但是繼承LocalDate就沒有這個問題,因為LocalDate類是不可變的,沒有任何方法可以把假日變成非假日。
※,使用多態而非類型信息。使用多態方法或接口編寫的代碼比使用對多種類型進行檢測的代碼更加易於維護和擴展
※, 不要過多的使用反射。反射功能對於編寫系統程序來說極其實用, 但是通常不適於編寫應用程序。
第六章: 接口 、 lamda表達式 與內部類
一,接口
1,Java中,接口不是類,而是對類的一組需求描述。
2,一個例子: java.util.Arrays. static void sort(Object[] a) ,可以對數組a中的元素進行排序。前提是數組中的元素必須屬於實現了 Comparable 接口的類,並且元素間是可比較的。
// Comparable接口(是個泛型接口)源碼如下:
public interface Comparable<T> {
public int compareTo(T o); }
// 語言標准規定: x.compareTo(y)和y.compareTo(x)必定是相反的。如果一個拋異常,另一個也應該拋異常。所以涉及子類繼承時,需要判斷是否子類和超類可以比較分兩種情況處理。 // 情況1,如果子類和超類無法對比,那么就加上getClass類型判斷 class Employee implements Comparable<Employee> { public int compareTo(Employee e) { if (getClass() != e.getClass()) { throw new ClassCastException(); } /** * java.lang.Double/Integer * static void int compareTo(double x, double y) * x < y 返回負值,相等返回0,x > y返回正值 */ return Double.compare(salary, e.getSalary()); } } // 情況2,如果子類和超類可以比較,那么在超類中提供一個compareTo()方法並聲明為final class Employee implements Comparable<Employee> { public final compareTo(Employee e) { return Double.compare(salary, e.getSalary()); } }
- 接口不是類,不能實例化。但是可以聲明一個接口類型的變量,條件是這個變量必須引用實現了這個接口的類對象。如:Comparable x = new Employee();//Employee必須實現Comparable接口。
- instanceof除了用於檢查某個類是否屬於某個特定類,也可以用來檢測一個對象是否實現了某個特定的接口。if (anObject instanceof Comparble) {...}.
- 同類一樣,接口也可以也可以被繼承。使用extends(英文是拓展的意思,仔細品味一下)關鍵字。
- 接口中所有的方法自動地屬於public abstract (其中的abstract關鍵詞不包含后面講的靜態方法和默認方法),所有的域自動的屬於public static final(靜態常量)。所以在聲明接口時,可以不必添加這些關鍵字(Java規范也推薦不添加這些多余的關鍵字)。
- 每個類只能有一個超類, 但是可以實現多個接口。一個接口可以繼承(拓展)多個接口。接口與抽象類的區別就在於類的只可以單繼承與接口的可以多實現。C++支持多繼承。
3,接口中不能含有實例域,因為接口不能實例化。在Java SE 8 之前,也不能在接口中實現方法。但是在Java SE 8中,可以實現靜態方法和默認方法了。
- 關於靜態方法: 通常的做法是將靜態方法放置在伴隨類中,在標准庫中有很多成對出現的接口和實用工具類,比如:Collection / Collections; Path / Paths。
/** * 在Java SE 8中,可以為Path接口增加靜態方法,這樣一來,Paths實用工具類就不再是必要的。 * 不過,整個Java庫都以這種方式重構不太可能,但是在實現我們自己的接口時,可以不再提供一個伴隨類了。 */ public interface Path { public static Path of(URI uri) { . . . } public static Path of(String first, String... more) { . . . } . . . }
- 關於默認方法: Java SE 8中還可以為接口方法提供一個默認實現。必須用default修飾符標記這樣一個方法。
/** * Java SE 8中可以為接口方法提供一個默認實現,實現體中可以調用任何其他方法。 * 有了默認方法,實現這個接口的類就可以不必實現這個方法了, * 如果沒實現這個方法的類的實例對象調用了這個方法就會調用接口中定義的這個默認方法。 */ public interface Collection { int size(); // an abstract method default boolean isEmpty() { return size() == 0; } . . . }
- 解決默認方法沖突:
- 如果某個類A實現了2個接口B,C,其中一個提供了一個默認方法func(),如果另外一個接口也有一個同簽名的方法,那么無論另外一個接口中的這個方法是否實現了,則編譯器會報二義性錯:默認方法沖突了,類A必須覆蓋這個方法來解決沖突:或者類A實現自己的func()方法,或者指定一個接口中的默認方法,語法格式是B.super.func();即(接口名.super.方法名)。
- 如果某個類A繼承了超類B,實現了接口C,而類B和接口C中都含有一個同簽名的方法,則遵循“類優先(class wins)”原則,即類A始終繼承超類B的這個方法,無論接口C是否實現了這個方法(即默認方法)都不會帶來什么影響。
4,更多例子:
※,接口與回調。
/**
* javax.swing包里的Timer類有一個定時器方法:new Timer(int delay, ActionListener listener)。
* 將對象傳遞給定時器,要求此對象必須實現java.awt.event包中的ActionListener接口。
*/
ActionListener listener = new TimePrinter(); Timer t = new Timer(1000, listener);//每隔1000ms調用一下listener中的actionPerformed方法。 t.start();
/**
* 在關閉提示框之前,每隔1s執行一下。如果沒有下面這句代碼,程序注冊了事件之后就退出了,所以不會出現預期的間隔性效果。
* 除了下面這種方式外,還可以使用 java.lang. Thread.sleep(10000);阻止程序退出
*/ JOptionPane.showMessageDialog(null, "Quit Program?");class TimePrinter implements ActionListener { @Override public void actionPerformed(ActionEvent e) { System.out.println("At the tone, the time is " + Instant.ofEpochMilli(e.getWhen())); // System.out.println("At the tone, the time is " + new Date()); Toolkit.getDefaultToolkit().beep(); } }
javax.swing.JOptionPane 1.2
-
static void showMessageDialog(Component parent, Object message);// 顯示一個包含一條消息和OK按鈕的對話框。這個對話框將位於其parent組件的中央。如果parent為mill,對話框將顯示在屏幕的中央
java.awt.Toolkit 1.0
- static Toolkit getDefaultToolkit();// 獲得默認的工具箱。工具箱包含有關GUI環境的信息。
- void beep();//發出一聲鈴響。
※,Comparator 接口:(注意不是 上面的 Comparable 接口,而是 Comparator 接口)
- 上文已講,Arrays.sort(object[] a) 方法可以對數組a進行排序,前提是數組a中的元素必須是實現了Comparable接口。String類是實現了Comparable<String>接口的,String類實現Comparable<String>接口的的compareTo(String anotherString)方法的方式是按字典順序比較字符串。
- 假設現在希望按照字符串的長度來排序,則可以使用Arrays.sort()的另一個版本:Arrays.sort(Object[] a, Comparator c);傳入一個數組和一個比較器作為參數,比較器是實現了Compartor<T>接口的對象。
String[] srr = new String[]{"helloWorld", "bcde","abc","fuck", "good", "fuckU"}; System.out.println(Arrays.toString(srr));//[helloWorld, bcde, abc, fuck, good, fuckU] Comparator<String> lengthComparator = new LengthComparator(); Arrays.sort(srr, lengthComparator); System.out.println(Arrays.toString(srr));//[helloWorld, fuckU, bcde, fuck, good, abc] class LengthComparator implements Comparator<String> { @Override public int compare(String o1, String o2) { /**
* 首先默認按照參數的順序排,即(o1,o2),如果參數順序變化這個默認排序也要變,然后 * 返回1或正數(true)表示按照這個規則需要調換o1和o2的位置,即o2排在o1的前面; * 返回-1或負數(false)表示不需要調換o1和o2的位置,即o1排在o2的前面。 * 0 表示不排序,同-1. */ return o1.length() - o2.length();// 升序 /** * 如果o1.length > o2.length,返回正數,表示需要調整o1,o2,即o2在o1前面,即升序. * 如果o1.length < o2.length,返回負數,表示不許調整o1,o2,即o1在o2前面,即升序. */ } }
// 規律:參數順序 和 return中的順序 一致則是升序;相反則是降序;
※,對象克隆(Cloneable接口):有關克隆的細節技術性很強,克隆沒有你想象中那么常用。標准庫中只有不到5%的類實現了clone()方法。
- 如果將一個對象引用(即變量)賦值給另外一個變量,那么這兩個變量(對象引用) 都指向同一個對象,是同一個對象的引用,任何一個變量改變都會影響到另一個變量。
- 使用clone()方法可以克隆一個對象。clone()方法是Object類的一個protected方法,所以和Object不在同一個包(java.lang包)的其他類的對象都無法直接調用這個clone()方法(這是protected修飾符的限制,見上面相關內容)。其他類要想使用這個方法必須實現Cloneable接口【這個Cloneable接口比較特別,它是Java提供的一組標記接口(tagging interface)或叫記號接口(marker interface)。像Comparable<T>等接口的通常用途是確保一個類實現一個或一組特定的方法。但是標記接口中不包含任何方法,它唯一的作用就是允許在類型查詢中使用 instanceof :if (anObject instanceof Cloneable){...} 】。
- Object類中的clone()方法屬於“淺拷貝”:
Employee origin = new Employee(); Employee cloned = origin.clone(); 淺拷貝:即對象origin中如果有其他引用對象,則cloned對象中並沒有克隆這個內部的引用對象。即origin對象和cloned對象依然共享一些信息。
- 一個類實現Cloneable接口的時候,如果Object類中的clone()方法的淺拷貝可以滿足要求,那么實現Cloneable接口的時候可以如下:
class Employee implements Cloneable { @Override public Employee clone() throws CloneNotSupportedException { return (Employee) super.clone(); } }
- 如果Object類中的淺拷貝clone()方法無法滿足要求,那么可以自己實現深拷貝,代碼如下:
/** * 拋出異常,之所以不捕獲這個異常,是因為捕獲異常很適合用於final類。 * 如果不是final類最好還是保留throws符號。這樣就允許子類不支持克隆時 * 可以選擇拋出一個CloneNotSupportedException異常。 */ @Override public Employee clone() throws CloneNotSupportedException { //調用 Object.clone() Employee cloned = (Employee) super.clone(); // 克隆對象中的可變域 cloned.birthDay =(Date) this.birthDay.clone(); return cloned; }
- 所有數組類型都有一個public 的clone()方法,而不是protected。可以用這個方法克隆一個新數組,包含原數組所有元素的副本。
int[] arrA = {2, 3, 5, 7, 11}; int[] cloned = arrA.clone(); System.out.println(Arrays.toString(arrA));//[2, 3, 5, 7, 11] cloned[0] = 33;//不會改變arrA數組 System.out.println(Arrays.toString(arrA));//[2, 3, 5, 7, 11] System.out.println(Arrays.toString(cloned));//[33, 3, 5, 7, 11]
- 卷II的第2章將展示另一種克隆對象的機制,其中使用了Java的對象串行化特性。這個機制很容易實現,而且很安全,但效率不高。
二, lambda表達式:
5,語法格式:
-
//這是完全體形式,下面有各種特殊形式
Arrays.sort(srr, (String o1, String o2) -> { return o1.length() - o2.length(); }); - 無參數時或2個及以上參數時需要使用一對圓括號()。
- 一個參數時可以省略圓括號。
- 參數類型可以推斷出來,無需顯式指定,當然也可以顯式指定。
- 方法體若只有一行代碼,則可以不用大括號{},此時有返回值也不能用 return 關鍵字。如:Arrays.sort(arr, (first, second) -> first.length() - second.length());//方法體沒有大括號,沒有分號,沒有return。
- 方法體若超過一行代碼,則必須使用大括號{},方法體內如果有返回值需要有return關鍵字,當然一行代碼也可以使用這種形式。如:
Arrays.sort(srr, (o1, o2) -> { return o1.length() - o2.length(); });
6,函數式接口(Functional Interface)
- 當且僅當一個接口中只含有一個抽象方法時,這個接口叫做 函數式接口。Java SE 8專門引入了一個注解 @FunctionalInterface。該注解用於接口的定義上,一旦使用該注解來定義接口,編譯器將會強制檢查該接口是否確實有且僅有一個抽象方法,否則將會報錯。需要注意的是,即使不使用該注解,只要滿足函數式接口的定義,這仍然是一個函數式接口,使用起來都一樣。
- 函數式接口的特殊之處在於可以使用lambda表達式代替這種接口的對象。如Comparator<T>接口只有一個抽象方法,屬於函數式接口(下面專門再講了Comparator接口,里面實際有兩個抽象方法,但是依然屬於函數式接口,具體見下面再談Comparator接口)。所以Arrays.sort(Object[] a, Comparator<T> c)中的第二個參數除了可以使用一個Comparator<T>對象之外,還可以直接傳入一個lambda表達式。上面已經這么使用過了。
- 實際上,在Java中,對lambda表達式所能做的也只是能將其轉換為函數式接口。即只能用函數式接口類型接受一個lambda表達式。甚至都不能用Object來接受一個lambda表達式,因為Object不是一個函數式接口。
- java.util.function包中定義了很多非常通用的函數式接口。一個尤其有用的函數式接口是Predicate<T>接口。ArrayList類有一個removeIf()方法,它的參數就是一個Predicate<T>。
public interface Predicate<T> { boolean test(T t); // additional default and static methods } ArrayList<Integer> list = new ArrayList<>(); list.removeIf(e -> e == null);//將list中所有為null的元素刪除掉。 等價於以下: list.removeIf(new RemoveCond()); class RemoveCond implements Predicate<Integer> { @Override public boolean test(Integer t) { return t == null; } }
7,方法引用(Method Reference)
-
Timer timer = new Timer(1000, event -> System.out.println(event)); /** * object::instanceMethod * 表達式System.out::println是一個方法引用,等價於 event -> System.out.println(event) */ Timer timer = new Timer(1000, System.out::println); String[] srr = {"Abc", "abbbbb", "good", "Goaaaa"}; //第二個參數是函數式接口 Comparator<String> 的一個實現,表示不考慮字母大小寫對字符串數組排序 Arrays.sort(srr, (x, y) -> x.compareToIgnoreCase(y)); /** * Class.instanceMethod * String::compareToIgnoreCase是一個方法引用,等價於(x, y) -> x.compareToIgnoreCase(y) */ Arrays.sort(srr, String::compareToIgnoreCase); //關於方法引用 用“::”操作符分隔方法名 與 對象或類名。主要有三種情況(方法引用貌似都可以替換為lambda表達式): 1. object::instanceMethod,如System.out::println等價於 x->System.out.println(x); System.out是PrintStream類的一個對象 2. Class::staticMethod,如 Math::pow等價於Math.pow(x, y); 3. Class::instanceMethod,如String::compareToIgnoreCase等價於(x, y) -> x.compareToIgnoreCase(y);第一個參數會成為方法的隱式參數 //另外 1. 可以在方法引用中使用this。this::equals等價於x -> this.equals(x); 2. 也可以在方法引用中使用super。super::instanceMethod會使用this作為隱式參數,調用給定方法的超類版本。
8,構造器引用(Constructor Reference)
-
/** * 構造器引用和方法引用很相似,只不過方法名為new。比如Employee::new是Employee的構造器的一個引用。 * 注意:每個stream流只能用一次,否則會報錯:stream has already been operated upon or closed */ //現在將一個字符串列表轉換為Employee對象數組列表 List<String> names = Arrays.asList("Liverpool", "Kloop", "Chamberlain", "Mane", "Salah", "Firmino"); Stream<Employee> stream = names.stream().map(Employee::new);//map方法會為names中每個元素調用Employee(String name)構造器 List<Employee> staff = stream.collect(Collectors.toList()); //可以使用數組類型建立構造器引用。例如int[]::new 是一個構造器引用,它有一個參數即數組的長度。等價於lambda表達式:x -> new int[x] Stream<Employee> stream1 = names.stream().map(Employee::new); Object[] staff1 = stream1.toArray(); Stream<Employee> stream2 = names.stream().map(Employee::new); Employee[] staff2 = stream2.toArray(Employee[]::new);
9,lambda表達式的變量作用域
-
public static void repeatMessage(String text, int delay) { /** * 1,下面的lambda表達式由3個部分組成: * ①參數event * ②代碼塊 * ③自由變量,指的是非參數event而且不在lambda代碼塊中定義的變量。這里即是text變量 * *,2,表示lambda的數據結構必須存儲自由變量的值,我們說自由變量的值被lambda表達式捕獲(captured)了。 * 代碼塊加上自由變量的值構成了一個閉包。在Java中,lambda表達式就是閉包。 * *,3,lambda表達式中捕獲的變量必須是常量或者實際上是常量(final or effectively final)。所謂實際上是常量 * 意思是,這個變量初始化之后就不會再為它賦值。所以下面兩處給text再賦值的操作編譯器都會報錯。這個限制是有原因的:
* 如果在lambda表達式中改變變量,並發執行多個動作時就會不安全。 * *,4,lambda表達式嵌套在 repeatMessage 方法中,lambda表達式的作用域和嵌套塊有相同的作用域。所以下面在嵌套塊 * 中定義event變量就會和lambda表達式中的event變量沖突,編譯器報錯。 * *,5,由上面4中所述可推知,lambda表達式中如果使用了this關鍵字,這個this和在嵌套快中的this完全一樣,指的就是 * 實例化的對象。 */ //5,System.out.println(this.toString());// lambda表達式中若有this關鍵字,和此處的this含義相同。 //4, String event = "xxx";//lambda表達式的作用域和此處的event有相同的作用域,所以會報變量名已定義的編譯錯誤。 //3 text = "重新賦值"; ActionListener listener = event -> { //3 text = "重新賦值"; System.out.println(text); Toolkit.getDefaultToolkit().beep(); }; new Timer(delay, listener).start(); }
10,常用的函數式接口:略
11,再談Comparator接口:
- Comparator接口里實際上有兩個抽象方法,除了 int compare(T o1, T o2); 還有一個抽象方法:boolean equals(Object obj);但是Comparator依然屬於函數式接口。public @interface FunctionalInterface的官方文檔如下:
- If an interface declares an abstract method overriding one of the public methods of java.lang.Object, that also does not count toward the interface's abstract method count since any implementation of the interface will have an implementation from java.lang.Object or elsewhere. 翻譯如下:
- 如果一個接口聲明了一個抽象方法, 這個抽象方法覆蓋了java.lang.Object的public方法,那么這個接口的這個抽象方法不會被計入到接口的抽象方法數量中,因為任何實現這個接口的類都會從java.lang.Object 或者 其他類 繼承這個抽象方法的實現。(正是由於上面講到的 類優先"class wins"原則。)
- <T extends Comparable<? super T>> 含義解釋(點我):泛型T的上限是Comparable<? super T>,<? super T>表示Comparable的泛型的下限是T,即?是T的超類(接口)。
- Comparator中有很多靜態方法可以方便的創建比較器,一些用法如下:comparing()方法的第一個參數是鍵提取器。
- 假設有一個Employee對象數組 staff ,可以按如下按名字對這些對象排序: Arrays.sort(staff, Comparator.comparing(Employee::getName));實際使用中發現在調用reversed()方法時 Comparator.comparing(Employee::getName).reversed()正常,但是換成lambda表達式就報錯 Comparator.comparing(x -> x.getName()).reversed(),暫不明白原因。
- 如果名字一樣,按照年齡排序:Arrays.sort(staff, Comparator.comparing(Employee::getName).thenComparing(Employee::getAge)));
- Comparator.comparing()還有一個變體,可以傳入第二個參數為第一個參數指定一個自定義的比較器,比如按照名字的長度排序:
- Arrays.sort(staff, Comparator.comparing(Employee::getName, (s, t) -> Integer.compare(s.length(), t.length())));
- comparing()和thenComparing()方法還有很多變體,可以避免int, double, long值的裝箱,比如上面那個操作可以簡化為:
- Arrays.sort(staff, Comparator.comparingInt(p -> p.getName().length()));
如果comparing()的第一個鍵提取器提取的鍵值可以為null,就需要用到nullsFirst()和nullsLast()方法了(返回值是一個比較器,參數也是一個比較器)。這兩個靜態方法會修改現有的比較器,從而在遇到null值時不會拋出異常,而是將這個值標記為小於(nullsFirst())或大於(nullsLast())正常值。例如,如果名字為null就排在最前面可以如下使用:
- Arrays.sort(staff, Comparator.comparing(Employee::getName, Comparator.nullsFirst(Comparator.naturalOrder())))); // naturalOrder()是另一個靜態方法,返回一個比較器。
- Comparator.nullsFirst(Comparator.naturalOrder().reversed()); // 等同於 Comparator.nullsFirst(Comparator.reverseOrder());
三,內部類(Inner Class)
內容挺多,暫不記錄了。以后再看一遍在記錄
※,匿名內部類:
四,Service Loader(動態加載實現接口的所有類): 參見此篇文章
1,用法:只需將所有類文件打包到一個jar文件中,然后在jar文件中的 META-INF文件夾下新建一個services文件夾,services文件夾下新建一個文件,文件名為接口的全名(即帶着包名,如com.learn.java.HelloInterface)。文件的內容是實現這個接口的所有類的全名。然后使用ServiceLoader類就可以從此文件中讀取到所有實現該接口的類了。詳細細節如下。
- com.learn.java包下定義一個接口
package com.learn.java; public interface HelloInterface { void sayName(); void sayAge(); }
- tong.huang.shan包下定義兩個實現該接口的類
package tong.huang.shan; import com.learn.java.HelloInterface; public class Dog implements HelloInterface { public static void main(String[] args) { System.out.println(String.format("%s: main方法", Dog.class.getName())); } @Override public void sayName() { System.out.println(String.format("%s: 汪汪", this.getClass())); } @Override public void sayAge() { System.out.println(String.format("%s: 3歲", this.getClass())); } }
package tong.huang.shan; import com.learn.java.HelloInterface; public class Sheep implements HelloInterface { @Override public void sayName() { System.out.println(String.format("%s: 咩咩", this.getClass())); } @Override public void sayAge() { System.out.println(String.format("%s: 10歲了", this.getClass())); } }
- tong.huang.shan包下新建一個測試類
package tong.huang.shan; import java.util.ServiceLoader; import com.learn.java.HelloInterface; public class Study { public static void main(String[] args) { /** * 下面這行代碼是在靜態方法中獲取類名的方法。由於getClass()方法需要this來調 * 用,但是靜態方法中沒有this,所以在靜態方法中構造一個匿名內部類 * 【new Object(){},匿名內部類是這個類(此例中即Object)的子類】, * 獲取此匿名內部類的class類(class tong.huang.shan.Study$1), * 然后再調用getEnclosingClass()方法獲取包圍這個內部類的類, * 即此靜態方法的class類(class tong.huang.shan.Study)。 */ System.out.println(new Object(){}.getClass().getEnclosingClass() + ":"); ServiceLoader<HelloInterface> serviceLoader = ServiceLoader.load(HelloInterface.class); for (HelloInterface myServiceLoader : serviceLoader) { myServiceLoader.sayAge(); myServiceLoader.sayName(); } } }
- 進入包的第一層所在目錄(com以及tong所在的目錄),使用命令 jar cvfe D:\loader.jar tong.huang.shan.Study '.\tong\huang\shan\Study$1.class' .\tong\huang\shan\Study.class .\tong\huang\shan\Sheep.class .\tong\huang\shan\Dog.class .\com\learn\java\HelloInterface.class;這個命令將所有相關class文件打到一個jar包里。
- 然后打開jar包,在META-INF文件夾下新建services文件夾,在services文件夾下新建 com.learn.java.HelloInterface 文件,文件內容為:
tong.huang.shan.Dog
tong.huang.shan.Sheep - 然后 運行此jar包:java -jar D:\loader.jar 即可看到 serviceLoader讀取到了所有實現HelloInterface的類。
2, API
- java.util.ServiceLoader<S> 1.6
-
static <S> ServiceLoader<S> load(Class<S> service);//創建一個service loader,這個loader將會加載實現了給定接口的所有類。
- Iterator<S> iterator();
- Stream<ServiceLoader.Provider<S>> stream() 9;//JDK 9才有的方法。
- Optional<S> findFirst() 9;// JDK 9才有。
-
- java.util.ServiceLoader.Provider<S> 9;//JDK 9才有
- Class<? extends S> type(); //gets the type of this provider.
- S get(); //gets an instance of this provider.
3,
五,代理類(Proxy)
1,具體不是很理解,記錄幾個例子。
2, 利用代理可以在運行時創建一個實現了一組給定接口的新類。這種功能只有在編譯時無法確定需要實現哪個接口時才有必要使用。
3,創建一個代理對象,需要使用 java.lang.reflect.Proxy 類的newProxyInstance() 方法:
- Object java.lang.reflect.Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException
4,一個例子: 使用代理類 和 調用處理器 跟蹤方法調用。
- 先定義一個 調用處理器。定義一個TraceHandler包裝器類存儲包裝的對象。其中的invoke()方法打印了被調用方法的名字和參數,隨后用包裝好的對象作為隱式參數調用這個方法。
package tong.huang.shan; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; public class TraceHandler implements InvocationHandler { private Object target; public TraceHandler(Object t) { target = t; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.print(target); System.out.print("." + method.getName() + "("); if (null != args) { for (int i = 0; i < args.length; i++) { System.out.print(args[i]); if (i < args.length -1) { System.out.print(","); } } } System.out.println(")"); return method.invoke(target, args); } }
- 假設現在 使用代理對象對二分查找進行跟蹤,代碼如下
package tong.huang.shan; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.util.Arrays; import java.util.Date; import java.util.Random; import com.learn.java.HelloInterface; public class Study { public static void main(String[] args) { // Object value = "紅樓夢"; // InvocationHandler handler = new TraceHandler(value); // Class[] interfaces = new Class[] {Comparable.class}; // Object proxy = Proxy.newProxyInstance(null, interfaces, handler); // Class pc = Proxy.getProxyClass(null, interfaces); // System.out.println(proxy.getClass()); // System.out.println(value.getClass()); // System.out.println(Proxy.isProxyClass(proxy.getClass())); // System.out.println(Proxy.isProxyClass(value.getClass())); // proxy.equals("平兒"); // value = proxy; // System.out.println(value.equals("史湘雲")); Object[] elements = new Object[1000]; for (int i = 0; i < elements.length; i++) { Integer value = i + 1; InvocationHandler handler = new TraceHandler(value); Object proxy = Proxy.newProxyInstance(null, new Class[] { Comparable.class }, handler); elements[i] = proxy; } Integer key = new Random().nextInt(elements.length) + 1; // key = 500;int result = Arrays.binarySearch(elements, key);// 這句會調用invoke()方法,打印出二分查找的全過程。 if (result > 0) { System.out.println(elements[result]); } } }
- 打印結果如下
500.compareTo(159) 250.compareTo(159) 125.compareTo(159) 187.compareTo(159) 156.compareTo(159) 171.compareTo(159) 163.compareTo(159) 159.compareTo(159) 159.toString()//雖然toString()方法不屬於Comparable接口,但是toString()方法也被代理了,這是因為Object類中的一部分方法都會被代理。
5,代理類的特性
- 代理類是在程序運行期間創建的,一旦被創建,就變成了常規類,與虛擬機中的其他任何類沒有什么區別。
- 所有的代理類都重寫了Object類中的toString(), equals(),hashCode()方法,和所有的代理方法一樣,這些方法也會調用invocation handler中的invoke()方法。但是沒有重新定義其他的方法(如clone()和getClass()方法)。
- 代理類的名字沒有定義。但是虛擬機中的Proxy代理類將會產生以$Proxy打頭的類名,如com.sun.proxy.$Proxy0
- 對於特定的類加載器和預設的一組接口來說,只能有一個代理類。也就是說,如果使用同一個類加載器和接口數組調用兩次Proxy.newProxyInstance()方法的話,只能夠得到同一個類的兩個對象。
- 可以使用Class proxyClass = Proxy.getProxyClass(null, interfaces)方法獲取代理類的Class對象。
- 代理類一定都是public 和final的。如果代理類實現的所有接口都是public的,那么代理類就不屬於某個特定的包;否則,所有非公有的接口都必須屬於同一個包,同時,代理類也要屬於這個包。
- 可以通過調用 boolean java.lang.reflect.Proxy.isProxyClass(Class<?> cl)方法檢測一個特定的Class對象是否代表一個代理類。
6,API
- java.lang.reflect.InvocationHandler 1.3
-
Object invoke(Object proxy, Method method, Object[] args); //定義了代理對象調用方法時希望執行的動作
-
- java.lang.reflect.Proxy 1.3
- static Class<?> getProxyClass(ClassLoaderloader, Class<?>... interfaces);//返回實現指定接口的代理類
-
static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler handler);// 構造實現指定接口的代理類的一個新實例。所有方法會調用給定處理器對象的invoke方法。
- static boolean isProxyClass(Class<?> cl); // 如果cl是一個代理類則返回true。
7,
第七章,異常、斷言和日志
在Java語言中,有三種處理系統錯誤的機制
- 拋出一個異常
- 使用斷言
- 日志
一,處理錯誤
1,異常分類
- Java中的所有異常對象都是 Throwable類 的實例對象。
- Throwable類有兩個分支:Error類和Exception類。
- Error類 描述了Java運行時系統的內部錯誤和資源耗盡錯誤。如果出現了這樣的內部錯誤,除了通告給用戶並盡力使程序安全的終止之外,再也無能為力了。這種情況很少出現。
- Exception 類又分為兩支:RuntimeException 類 和 其他異常 類。划分規則是:由程序錯誤導致的異常屬於RuntimeException;而程序本身沒有問題,但由於像I/O錯誤這類的問題導致的異常屬於其他異常(如IOException)。“如果出現RuntimeException異常,那么一定是你的問題了!”
- Java語言規范將派生於 Error 類 和 RuntimeException 類的所有異常稱為 “unchecked exception”,所有其他的異常稱為“checked exception”。編譯器將會檢查是否為所有的checked exception提供了異常處理器。
2,拋出異常時,不需要拋出從Error繼承的錯誤(Error類也算是異常的一種)。任何程序代碼都具有拋出那些異常的潛能,而我們對其沒有任何控制能力。
3,子類拋出的異常不能比超類更通用(即子類拋出的異常要么和超類拋出的異常相同要么是超類拋出異常的子類)。特別是當超類沒有拋出異常時,子類也不能拋出任何異常。
4,自定義異常類
- 習慣上,自定義的異常類應該包含兩個構造器:一個是默認的無參數構造器,一個是帶有詳細描述信息的構造器,e.getMessage()就是這里的描述。
class CustomException extends Exception { CustomException() { } CustomException(String msg) { super(msg); } }
5,
二,捕獲異常
1,如果try子句中的某一行代碼拋出了異常,那么程序將跳過try塊中剩余的其余代碼。
2,一個try語句塊中可以捕獲多個異常類型,連續多個catch。
3,Java SE7中,同一個catch子句中可以捕獲多個異常類型,只有當捕獲的異常類型彼此之間不存在子類關系時才需要這個特性。例如:
-
try { ....... } catch (FileNotFoundException | UnknowHostException e) { System.out.println(e.getClass());// 由於一旦拋出一個異常,代碼就不繼續往下運行了,所以catch時只能catch一個異常,所以用一個變量e。 }
- 當在一個catch子句中捕獲多個異常時,異常變量e隱含為final變量,無法再對其進行賦值。
4,finally子句:
- try 語句可以只有finally子句而沒有catch子句。try {} finally {}
- 強烈建議解耦合 try / catch 和 try / finally語句塊。這樣可以提高代碼的清晰度。
/** * 內層的語句塊只有一個職責,就是確保關閉輸入流。 * 外層的try語句塊也只有一個職責,就是確保報告出現的錯誤。 * 這種設計方式不僅清楚,而且還具有一個功能, * 就是將會報告finally子句中出現的錯誤。 */ InputStream in = . . .; try { try { // code that might throw exceptions } finally { in.close(); } } catch (IOException e) { // show error message }
- finally子句中的代碼目的是清理資源,所以不要在其中編寫影響控制流的代碼,如return 語句,throw 語句,break / continue 語句等。由於finally子句中的代碼是無論如何都會執行的,所以它可能會覆蓋try子句中的代碼,比如:
- 如果finally子句中含有return語句:那么它將會覆蓋try中的return 的值。如果try中某個地方拋出了異常,那么finally中的return語句就會將這個異常吞噬掉!
- 如果finally子句中拋出了一個異常,那么這個異常會覆蓋try子句中拋出的異常。如果try中return 一個值,這個值也會被finally中拋出的異常所吞噬。
5, try-with-resources 語句
- try-with-resources 語句可以自動關閉打開的資源。要求是資源必須是一個實現了AutoCloseable接口或其子接口的類。AutoCloseable接口有一個方法:void close() throws Exception; AutoCloneable接口有一個子接口Closeable接口,這個接口也有一個方法:void close() throws IOException;
- 語法格式: try (Resource res = ...) {work with res}; try 塊正常退出時或者try塊中存在異常時,都會自動調用res.close()方法,就像使用了finally 塊一樣。還可以指定多個資源,例如
try (Scanner in = new Scanner(new FileInputStream("C:\\Users\\JoY-33\\Desktop\\1.txt"), "GBK"); PrintWriter out = new PrintWriter("C:\\Users\\JoY-33\\Desktop\\2.txt")) { while (in.hasNextLine()) { out.println(in.nextLine()); } }
- 上面已講過,如果try塊中拋出一個異常,而且close()方法也拋出一個異常,close()方法拋出的異常就會覆蓋try塊中的異常。但是try-with-resources語句可以很好的處理這種情況:try塊中的異常會被重新拋出,而close()方法拋出的異常會被抑制(Suppressed)。close()方法拋出的異常將會被自動捕獲,並由addSuppressed()方法增加到try塊中的異常。可以使用getSuppress()方法獲取到從close()方法拋出的所有異常組成的一個數組---->Throwable[]
- try-with-resources語句也可以有catch子句和finally子句。這些子句會在關閉資源后執行。
6,分析堆棧軌跡元素
- 堆棧軌跡(stack trace)是一個方法調用過程的列表,它包含了程序執行過程中方法調用的特定位置(包括文件名、類名、方法名、行號信息等)。平常當程序出現未捕獲的異常時,控制台打印的就是堆棧軌跡。
- 堆棧軌跡不止是被動出錯時打印出來,也可以主動使用。使用Throwable類的printStackTrace()方法可以主動打印代碼的堆棧軌跡,還可以用 Thread.dumpStack() 方法主動打印堆棧軌跡。
Throwable t = new Throwable(); t.printStackTrace(); // 打印結果如下 java.lang.Throwable at tong.huang.shan.Study.main(Study.java:15)
// 第二個例子
Throwable t = new Throwable();
StringWriter out = new StringWriter();
t.printStackTrace(new PrintWriter(out));
String desc = out.toString();
System.out.println(desc); - Throwable類還有一個getStackTrace()方法,此方法返回 StackTraceElement數組。可以在程序中分析這個對象數組。例如:
private static long factorial(int n) { System.out.println("factorial(" + n + ")"); Throwable t = new Throwable(); StackTraceElement[] frames = t.getStackTrace(); for (StackTraceElement frame : frames) { System.out.println(frame); } long r; if (n <= 1) { r = 1L; } else { r = n * factorial(n - 1); } System.out.println("return " + r); return r; } //調用 factorial(3)將打印如下堆棧軌跡: factorial(3) tong.huang.shan.Study.factorial(Study.java:53) tong.huang.shan.Study.main(Study.java:32) factorial(2) tong.huang.shan.Study.factorial(Study.java:53) tong.huang.shan.Study.factorial(Study.java:62) tong.huang.shan.Study.main(Study.java:32) factorial(1) tong.huang.shan.Study.factorial(Study.java:53) tong.huang.shan.Study.factorial(Study.java:62) tong.huang.shan.Study.factorial(Study.java:62) tong.huang.shan.Study.main(Study.java:32) return 1 return 2 return 6
- 靜態的 Thread.getAllStackTrace方法,可以產生所有線程的堆棧軌跡。
Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces(); for (Thread thread : map.keySet()) { StackTraceElement[] frames = map.get(thread); System.out.println("ThreadName: " + thread.getName()); System.out.println(Arrays.toString(frames)); }
7,API
※,java.lang.Throwable 1.0
- void addSuppressed(Throwable t) 7;//為這個異常增加一個“抑制”異常。這出現在 try-with-resources 語句中,其中的t是close()方法拋出的一個異常。
- Throwable[] getSuppressd() 7;// 得到這個異常的所有“抑制”異常。一般來說,這是是 try-with-resources 語句中的close()方法拋出的異常。
- StackTraceElement[] getStackTrace() 1.4; // 獲得構造這個對象時調用的堆棧的軌跡
※,java.lang.StackTraceElement 1.4
- String getFileName(); // 返回這個元素運行時對應的源文件名。如果這信息不存在,則返回null。
- int getLineNumber();
- String getClassName();
- String getMethodName();// 構造器名是<init> ; 靜態的構造器名是<clinit>。無法區分同名的重載方法。
- boolean isNativeMethod();// 如果這個元素運行時在一個本地方法中,則返回true。所謂native method(本地方法)定義如下
- A native method is a Java method whose implementation is provided by non-java code.
- 一個Native Method就是一個java調用非java代碼的接口。
※,java.lang.StackWalker 9
※,java.lang.StackWalker.StackFrame 9
8,
三,使用異常機制的技巧
1,能避免異常的盡量避免異常,因為捕獲異常非常耗時。例如用一些判斷條件避免異常。例如對一個棧進行退棧操作可以通過判斷是否為空規避EmptyStackException。
//使用條件規避異常 if (!s.empty()) { s.pop(); } /**
* 強行退棧並捕獲異常非常耗時!!! * 在測試的機器上,調用isEmpty的版本運行時間為646毫秒。 * 捕獲EmptyStackException的版本運行時間為21739毫秒。 */ try { s.pop(); } catch (EmptyStackException e) { }
2,盡量避免for循環內使用try-catch。而應該try整個for循環。
3,早拋出,晚捕獲。
- 早拋出:在異常出現的源頭處就應該拋出異常,比如當棧空時,Stack.pop()可以返回一個null,也可以拋出一個異常。我們認為在出錯的地方拋出一個EmptyStackException異常要比在后面拋出一個NullPointerException異常要好。
- 晚捕獲:盡量將異常傳遞給高層次的方法。讓高層次的方法通知用戶發生了錯誤。
4,
四,使用斷言
0,不同的IDE中開啟斷言(或其他jvm選項)的方法
- vscode 中開啟斷言選項的方法:在項目 launch.json文件 中添加 "vmArgs"配置項
"configurations": [ { "type": "java", "name": "CodeLens (Launch) - Study", "request": "launch", "mainClass": "tong.huang.shan.Study", "projectName": "java-study_adaad70d", "vmArgs": "-ea",//虛擬機參數 },
- IntellijIdea中 可以在 菜單 Run --> Edit Configurations 或直接點擊項目列表的下拉菜單中的 Edit Configurations,然后在VM options配置中添加選項。
1,在默認情況下,斷言機制是被禁用的。可以在運行程序時通過 -enableassertions 或 -ea 選項開啟斷言,如:java -ea myApp。在啟用或禁用斷言時不必重新編譯程序。啟用或禁用斷言是類加載器(class loader)的功能。當斷言被禁用時,類加載器將跳過斷言代碼。
- 也可以在某個類或某個包中使用開啟斷言: java -ea: myClass -ea:com.mycompany.mylib myApp
- 使用 -disableAssertions 或 -da 關閉(部分類、包等)斷言。java -ea:... -da:myClass myApp
- 有些類不是由類加載器加載,而是直接由虛擬機加載,這些類也可以使用上面選項開啟或關閉斷言
- 但是有些“系統類”沒有類加載器,-ea不再適用。對於這些系統類需要使用 -enablesystemassertions或 -esa 選項開啟或關閉斷言。
2,語法格式:斷言有兩種格式。如果啟用斷言,這兩種格式都會對條件進行檢測,如果為false則拋出一個AssertionError異常。
- assert 條件;
- assert 條件 : 表達式; //此種形式中,表達式將被傳入AssertionError的構造器,並轉換成一個消息字符串。
3,說明:
- 斷言檢查只用於開發和測試階段。
4,API:
java.lang.ClassLoader 1.0
-
void setDefaultAssertionStatus ( boolean b ) 1.4
對於通過類加載器加載的所有類來說,如果沒有顯式地說明類或包的斷言狀態,就啟用或禁用斷言。 -
void setCIassAssertionStatus ( String className , boolean b ) 1.4
對於給定的類和它的內部類,啟用或禁用斷言 。 -
void setPackageAssertionStatus ( String packageName , bool ean b ) 1.4
對於給定包和其子包中的所有類,啟用或禁用斷言。 -
void clearAssertionStatus () 1.4
移去所有類和包的顯式斷言狀態設置 ,並禁用所有通過這個類加載器加載的類的斷言。
5,
五,記錄日志(標准Java日志框架)
0,System.out 和 System.err 的區別
- out為標准輸出流,err為標准錯誤輸出流。在idea中控制台中打印的顏色不同。
- out在JVM和操作系統中都具有緩存功能,輸出的內容不一定實時輸出,可能積攢到一定數量才會輸出。err則會實時輸出。單獨使用感覺不到,兩種方式混合使用即可發現。
1,全局日志記錄器:
- (java.util.logging.)Logger.getGlobal().info("全局記錄器打印信息");
- Logger.getGlobal().setLevel(Level.OFF) 可以取消后續的所有日志輸出。
2,自定義日志記錄器
- private static final Logger myLogger = Logger.getLogger("com.mycompany.myapp"); 與包名類似,日志記錄器名也具有層次結構。事實上,與包名相比,日志記錄器的層次性更強。對於包來說,一個包的名字與其父包的名字之間沒有語義關系,但是日志記錄器的父與子之間將共享某些屬性。例如,如果對com.mycompany日志記錄器設置了日志級別,它的子記錄器也會繼承這個級別
- 日志級別按照記錄范圍大小從小到大依次為: OFF, SEVERE, WARNING, INFO, CONFIG, FINE, FINER, FINEST, ALL。
3,修改日志管理器配置
- 默認情況下,Java日志只會打印INFO及以上級別的日志,即使設置了Level.ALL也無法打印更低級別的日志。原因是Java默認的日志配置文件中默認配置是INFO及以上級別。JDK默認的日志配置文件位於 jre/lib/logging.properties (JDK9之后位於conf/logging.properties)。但是Java讀取的默認配置文件可能不是這個文件,因為修改了這里面的配置沒有生效,手動指定這個修改了的文件卻生效了。
- 可以通過添加jvm參數手動指定日志配置文件 java -Djava.util.logging.config.file=D:\\workware\\jre\\lib\\logging.properties MainClass 。
- 配置文件中 可以看到,Java默認的日志處理器(Handler)是ConsoleHandler(還可以設置為FileHandler):handlers= java.util.logging.ConsoleHandler。要想在控制台打印指定級別的日志,需要配置兩個地方:① 日志級別: .level=INFO ②控制台輸出日志級別:java.util.logging.ConsoleHandler.level = INFO。注意級別都要大寫。修改后通過手動指定此文件即可打印任一級別的日志。
- 還可以在日志配置文件中指定某個 logger 的級別。比如:com.mycompany.myapp.level = SEVERE,即可將 Logger.getLogger("com.mycompany.myapp") 這個日志記錄器的級別設為SEVERE。
- 日志管理器在JVM啟動過程中初始化,這在main執行之前完成。如果在main中調用System.setProperty("java.util_logging.config.file",file),也會調用LogManager.readConfiguration()來重新初始化日志管理器。如果未加JVM參數
-Djava.util.logging.config.file=D:\\workware\\jre\\lib\\logging.properties 則在main方法中獲取系統屬性System.getProperty("java.util.logging.config.file") 為null。如果設置了JVM參數,則可以獲取設置的值。
4,日志處理器 (P293)
5,日志過濾器
6,日志格式化器
public static void main(String[] args) { Logger logger = Logger.getLogger("tong.huang.shan"); logger.setLevel(Level.FINE);
/**
* 日志記錄器會將日志發送到父處理器中,最終的父處理器是一個叫做【""】的處理器,它也有一個ConsoleHandler。如果不設置false則控制台會打印兩次
* Logger.getLogger("")可以獲取到這個最終的父處理器。
*/ logger.setUseParentHandlers(false); Handler handler = new ConsoleHandler(); handler.setLevel(Level.FINE); handler.setFilter((t)->t.getMessage().contains("33"));//java.log.logging.Filter是一個函數式接口。 handler.setFormatter(new MyFormatter());// java.log.logging.Formatter是一個抽象類。抽象類和接口的區別在於抽象類只能單繼承,接口可以多實現。 logger.addHandler(handler); logger.fine("hello world33"); } class MyFormatter extends Formatter { @Override public String format(LogRecord logRecord) { return logRecord.getMessage().replace("33", "55"); } }
7,書中有一個自定義處理器(程序清單7.2 p298),通過擴展Handler類或StreamHandler類 實現在窗口中顯示日志記錄的功能。
8, API
- java.util.logging.Logger 1.4
- java.util.logging.Handler 1.4
- java.util.logging.ConsoleHandler 1.4
- java.util.logging.FileHander 1.4
- java.util.logging.LogRecord 1.4
- java.util.logging.Filter 1.4
- java.util.logging.Formatter 1.4
六,調試技巧
1,日志代理
3,堆棧軌跡
- 一般顯示在System.err 流上(new Throwable().printStackTrace()),
//不帶參數的printStackTrace()函數實際的實現如下:
public void printStackTrace() { printStackTrace(System.err); } - 也可以將堆棧軌跡發送到一個文件中,代碼如下
PrintWriter pw = new PrintWriter("C:\\Users\\JoY-33\\Desktop\\1.txt"); new Throwable().printStackTrace(pw); /* * 注意:使用PrintWriter往文件寫入時需要使用flush()或close()將緩沖區刷新,否則不會寫入文件中。 */ // pw.flush();//僅僅刷新緩沖區,刷新之后流對象還可以繼續使用 pw.close();// 關閉流對象,在關閉之前會先刷新一次緩沖區。關閉之后,流對象不可以繼續使用了。
- 還可以將堆棧軌跡捕獲到一個字符串中
StringWriter out = new StringWriter(); PrintWriter pw = new PrintWriter(out); new Throwable().printStackTrace(pw); System.out.println(out);
4,
- Linux系統中,0代表標准輸入(默認從鍵盤獲取輸入),1代表標准輸出(默認輸出到屏幕,即控制台),2代表標准錯誤(默認輸出到屏幕,即控制台)。
- 以下命令在Linux和Windows的shell中都可運行。
- java MyProgram > file.txt //程序中的標准輸出(System.out)都會重定向到file.txt文件中。實際等同於java MyProgram 1 > file.txt。
- java MyProgram 2 > file.txt // 程序中的標准錯誤(System.err)都會重定向到file.txt文件中。
- java MyProgram > file.txt 2>&1 // 程序中的標准輸出和標准錯誤都會重定向到file.txt文件中。
- java MyProgram 2>&1 > file.txt和上面效果一致。但是在Linux命令中,兩個順序不一致導致的效果是不一樣的。
5,
Thread.setDefaultUncaughtExceptionHandler( new Thread.UncaughtExceptionHandler() { public void uncaughtException(Thread t, Throwable e) { //save information in log file }; });
6,要想觀察類的加載過程,可以使用-verbose 參數啟動Java虛擬機。有時候這種方法有助於診斷由於類路徑引發的問題。
7,屬於“lint” 最初是指一種定位C程序中潛在問題的工具。現在泛指 查找可疑但是並不違背語法規則代碼的 工具。Java編譯器的 -Xlint 選項使用方法如下:
- 例子: javac -Xlint:fallthrough /path/to/myClass
- 使用 javac -X可以查看所有的-Xlint選項,一些列舉如下
- -Xlint 或 -Xlint:all 執行所有的檢查
- -Xlint:deprecation 和 -deprecation一樣,檢查廢棄的方法。
- -Xlint:fallthrough 檢查switch語句中是否缺少break語句。
- -Xlint:finally 警告finally子句不能正常執行。
- -Xlint:none 不執行任何檢查
- -Xlint:path 檢查類路徑或源代碼路徑上的所有目錄是否存在。
- -Xlint:serial 警告沒有 serialVersionUID 的串行化類
- -Xlint:unchecked 對通用類型與原始類型之間的危險轉換給予警告
8,Java虛擬機選項中的 -X 選項並沒有被正式支持,有些JDK選項中並沒有這些選項。可以使用 java -X 得到所有非標准選項的列表。 javac -X 也一樣。
9,jconsole的使用:
- JDK加載了一個稱為jconsole的圖形工具,可以用於顯示虛擬機性能的統計結果。使用方法是:jconsole processId 。其中的processID即是操作系統中Java虛擬機的進程ID。
- Java虛擬機進程ID: 在Windows下即是 任務管理器中 java.exe 進程。在Linux下可以使用ps工具找到。
- 無論 Linux還是Windows下,都可以使用 jps 查找Java虛擬機進程ID。只輸入jps會顯示jps進程本身的ID以及java虛擬機進程的ID。jps還有一些參數可選,可自行搜索用法。
- jsoncole控制台給出了有關運行程序的大量信息。可以參考文檔: https://www.oracle.com/technetwork/articles/java/jconsole-1564139.html
10,jmap 的使用:
- 可以使用jmap實用工具獲得一個堆的轉儲,其中顯示了堆中的每個對象。使用方法如下;(可以使用jmap -help 獲取幫助)
- jmap -dump:format=b,file=out.bin proessId (即Java虛擬機的進程ID)
- jhat out.bin
- 上個命令會運行啟動一個服務,然后通過瀏覽器 localhost:7000 查看堆中內容。
11,Java Mission Controller是一個類似於jconsole的專業的分析和診斷Java進程的工具。具體參考文檔: https://docs.oracle.com/javacomponents/index.html
12,
第八章:泛型程序設計 (Generic Programming)
一,概述
1,Java SE5.0新增了泛型機制。
2,類型參數(type parameters): 一般 E表示集合元素,K , V表示鍵值,T, U, S等表示任意類型。
二,泛型類和泛型方法
1,泛型類:Pair<T>
package tong.huang.shan;
// Pair<T>是一個泛型類,也可以說是一個泛型類型(Java中的類也表示一個類型)。在編譯器中,這個泛型類被翻譯為一個普通的Pair類。 public class Pair<T> { private T first; private T second; Pair() { first = null; second = null; } Pair(T first, T second) { this.first = first; this.second = second; } @Override public String toString() { return "Pair<" + this.first + "," + this.second + ">"; } public T getFirst() { return this.first; } public T getSecond() { return this.second; } public void setFirst(T first) { this.first = first; } public void setSecond(T second) { this.second = second; } }
2,泛型方法 和 類型參數的限定類型
package tong.huang.shan; import java.time.LocalDate; public class Study { public static void main(String[] args) { String[] arr = { "mary", "had", "a", "little", "lamb" }; Pair<String> minmax = ArrayAlg.minmax(arr); System.out.println(minmax); String middle = ArrayAlg.<String>getMiddle("John", "Q", "public"); System.out.println(middle); /** * 下面這個方法調用會編譯報錯。 * 編譯器自動打包參數為一個Double和兩個Integer對象,然后尋找這些類的公共超類。 * 事實上Double類和Integer類的共同超類有兩個:Number類和Comparator接口。 * 所以這意味着ArrayAlg的這個方法返回值可以賦值給Number類型或Comparator類型。 * 當然也可以將參數都改為double類型。(3.14,111d, 0d) */ // double mid = ArrayAlg.getMiddle(3.14, 111, 0); // System.out.println(mid); LocalDate[] birthdays = { LocalDate.of(1906, 12, 9), // G. Hopper LocalDate.of(1815, 12, 10), // A. Lovelace LocalDate.of(1903, 12, 3), // J. von Neumann LocalDate.of(1910, 6, 22), // K. Zuse }; Pair<LocalDate> mm = ArrayAlg.minmaxG(birthdays); System.out.println("min = " + mm.getFirst()); System.out.println("max = " + mm.getSecond()); } } class ArrayAlg { public static Pair<String> minmax(String[] a) { if (null == a || a.length == 0) { return null; } String min = a[0]; String max = a[0]; for (int i = 1; i < a.length; i++) { if (min.compareTo(a[i]) > 0) min = a[i]; if (max.compareTo(a[i]) < 0) max = a[i]; } return new Pair<String>(min, max); } /** * 泛型版本的 minmax 方法 */ public static <T extends Comparable> Pair<T> minmaxG(T[] a) { if (null == a || a.length == 0) { return null; } T min = a[0]; T max = a[0]; for (int i = 1; i < a.length; i++) { if (min.compareTo(a[i]) > 0) { min = a[i]; } if (max.compareTo(a[i]) < 0) { max = a[i]; } } return new Pair<T>(min, max); } // 泛型方法可以在普通類中定義,泛型參數放在修飾符和返回類型之間 public static <T> T getMiddle(T... a) { return a[a.length / 2]; } /** * 類型變量的限定類型(bounding types,或譯為界限類型)可以有多個,用&符號分隔:<T extend Comparable & Serializable> * 限定類型可以有多個接口,但是只能有一個類(類只能單繼承),如果限定類型中有類,它必須是 限定列表中的第一個。 */ public static <T extends Comparable> T min(T[] a) { if (a == null || a.length == 0) return null; T smallest = a[0]; for (int i = 1; i < a.length; i++) if (smallest.compareTo(a[i]) > 0) smallest = a[i]; return smallest; } } interface A { } interface B { } // 接口可以繼承接口,可以繼承多個接口 interface C extends A, B { } class AA { } class BB { } // 類只能繼承一個類,可以實現多個接口 class CC extends AA implements A, B { }
3,
三,泛型 和 JVM(Java 虛擬機)
1,Java 虛擬機中沒有泛型類型對象,所有的對象都屬於普通類。
2,類型擦除
- 當定義一個泛型類型(即定義一個泛型類)時,會自動定義提供一個相應的 原始類型(raw type)。原始類型的名字就是去除類型參數的后的泛型類型名。類中代碼中的類型變量會被替換為限定類型列表中的第一個,如果沒有限定類型就用Object替換。例如
//例子一:原來的泛型類型 public class Pair<T> { private T first; } //類型擦除后為: public class Pair { private Object first; } //例子二:原來的泛型變量 public class Pair<T extends Comparable & Serializable> { private T first; } // 類型擦除后為: public class Pair { private Comparable first; } // 注:Serializable 接口是標簽接口(tagging interface:即沒有任何方法的接口,只是用來instanceof用的),為了提高效率應將標簽接口放在限定類型列表的末尾。
- 編譯器翻譯泛型表達式:涉及到泛型的地方,編譯器都會加入強制類型轉換,比如:pair.getFirst()在編譯器中返回一個Object對象,如果pair是Pair<Employee>的一個實例,那么就會將pair.getFirst()返回的Object對象強制轉換為Employee對象。
- 編譯器翻譯泛型方法【參考一篇好文章】:橋方法( bridge method )。注意:多態需要子類重寫了父類的方法。
3,使用Java泛型時的約束與局限性: 大多數限制都是由類型擦除引起的。
- 類型參數不能為基本類型(類型參數不能用基本類型實例化):如只有Pair<Double> 而沒有Pair<double>。原因是因為類型擦除。擦除之后,Pair類還有Object類型的域,而Object不能存儲double值。注意:基本類型(primitive types)如int,double在Java中是獨立的類型,不是繼承自Object類,不屬於Object。本篇開篇關於基本類型的說明中有詳細解釋。
- 運行時類型檢查只適用於原始類型。
if (a instanceof Pair<String>) // Error if (a instanceof Pair<T>) //Error //強制類型轉換編譯器會警告 Pair<String> p = (Pair<String>) a; // getClass()方法總是返回原始類型。 Pair<String> stringPair = ... Pair<Employee> employeePair = ... if (stringPair.getClass() == employeePair.getClass()) // true:都返回Pair.class
- 不能創建參數化類型的數組。
//Error,不允許創建參數化類型的數組 Pair<String>[] table = Pair<String>[][3]; /* 注意:只是不能創建參數化類型的數組,但是聲明類型為Pair<String>[]變量仍是合法的, * 只是不能用new Pair<String>[3]初始化這個變量. * 可以聲明通配類型的數組然后進行類型轉換。但是結果是不安全的。 */ Pair<String>[] t = (Pair<String>[])new Pair<?>[3];
- 可變參數的方法:
/** * 可變參數ts實際上是一個數組。如果T是一個泛型類型,比如Pair<String>, * 那么Java虛擬機就必須建立一個Pair<String>數組,這就違反了不能創建泛型數組的規則。 * 但是,對於這種情況,規則有所放松,只會有一個警告而不是錯誤。 * 可以有兩種方法抑制這個警告: * 方法一:為addAll方法增加注解@SuppressWarnings("unchecked") * 方法二:自Java SE7開始,還可以使用@SafeVarargs注解標注addAll方法。 */ // @SafeVarargs public static <T> void addAll(Collection<T> coll, T... ts) { for (T t: ts) {coll.add(t);} }
- 不能實例化類型變量(類型變量):不能使用 new T(...), new T[...], T.class這樣的表達式。
- 不能構造泛型數組: T[] mm = new T[2] ;//Error
- 靜態域和靜態方法 的類型不能為類型變量。 如: private static T first;//Error public static T getFirst(){}// Error.
- 不能拋出或捕獲泛型類的實例。實際上,泛型類擴展Throwable都是不合法的。
4, 泛型類型的繼承原則
- 假設Manager是Employee的子類,則對於泛型來講有以下繼承關系:①ArrayList<Employee> ② List<Employee> ③ ArrayList<Manager>④ List<Manager> ⑤ArrayList ⑥List
- ArrayList<Manager> 和 ArrayList<Employee>沒有任何關系。List<Manager>和List<Employee>無任何關系。
- ArrayList<Employee> 可以實現List<Employee>接口。ArrayList<Manager>可以實現List<Manager>接口。
- ArrayList<Employee> 和 ArrayList<Manager>是 原始類型 ArrayList的子類。
- List<Employee> 和 List<Manager>是 原始類型List的子類。
- ArrayList(原始類型) 可以實現 List(原始類型)
- Java中的泛型和數組之間的一個區別和聯系:
LocalDate date = LocalDate.of(1111, 11, 11); /* * Java 泛型 */ Manager m1 = new Manager("name", 3d, date); Manager m2 = new Manager("name1", 4d, date); Pair<Manager> managerPair = new Pair<>(m1, m2); //編譯無法通過:Type mismatch: cannot convert from Pair<Manager> to Pair<Employee> Pair<Employee> employeePair = managerPair; /* * Java 數組 */ Manager[] managers = {m1, m2}; Employee[] employees = managers; employees[0] = new Employee();//編譯可以通過,但是運行時錯誤:java.lang.ArrayStoreException System.out.println(Arrays.toString(employees));
5,
四,泛型之 通配符類型 【點我參考另一篇文章】
1,通配符類型允許類型參數變化。注意:通配符不是在定義泛型類時使用的,而是在使用泛型類時才能用到通配符。如定義Pair類時,只能是public class Pair<T>{}不能是public class Pair<? extends Employee>
- 如 Pair<? extends Employee> 表示任何泛型Pair類型,它的類型參數是 Employee 的子類。
- 類型 Pair<Manager>是Pair<? extends Employee>的子類型。
- Pair<? extends Employee> 類型是原始類型Pair的子類。
2,通配符捕獲: 用於通配符不是一個類型,因此不能再編碼中使用 ? 作為一種類型。假設,要交換Pair<?>的first和second屬性,以下代碼是錯誤的
? t = p.getFirst(); // Error p.setFirst(p.getSecond()); p.setSecond(t);
解決方法是再寫一個輔助方法swapHelper,如下
// swapHelper是泛型方法 public static <T> void swapHelper(Pair<T> p) { T t = p.getFirst(); p.setFirst(p.getSecond()); p.setSecond(t); } / ** * swap方法可以調用泛型方法swapHelper,注意swap並不是泛型方法,swap的參數是 * Pair<?>類型的。 swap方法中static和void之間並沒有泛型類型。 */ public static void swap(Pair<?> p) { swapHelper(p); } 此時,swapHelper方法的參數T捕獲了通配符。通配符捕獲只有在許多有限制的情況下才是合法的。編譯器必須能夠確信通配符表達的是單個、確定的類型。
例如:ArrayList<Pair<T>>中的T永遠不能捕獲ArrayList<Pair<?>>中的通配符,因為數組列表ArrayList中可以保存兩個?不同的 Pair<?>。
3,
五,反射和泛型
1,
2,
第九章:集合
一,Java集合框架概述
1,集合的接口與實現分離
※,接口與實現分離虛擬例子:隊列接口:public interface Queue<E>{} ,隊列的實現方法通常有兩種:
一是使用循環數組(circular array)public class CircularArrayQueue<E> implements Queue<E>{},
二是使用鏈表(linked list)。
※,
2,collection接口 和 Iterator 迭代器接口
※,Java類庫中,集合的基本接口是Collection接口。Collection接口有兩個基本方法:
public interface Collection<E> { boolean add(E element); Iterator<E> iterator();//迭代器 ....//還有一些其他方法 }
※,Iterator接口包含4個方法:
public interface Iterator<E> { E next(); boolean hasNext(); void remove(); default void forEachRemaining(Consumer<? super E> action); }
※,for each循環是迭代器循環 while (iterator.hasNext()) {next = iterator.next();...}的簡化 形式。 for each循環可以與任何實現了Iterable接口的對象一起工作。Iterable接口只包含一個抽象方法:
public interface Iterable<E> { Iterator<E> iterator(); }
Collection 接口擴展(繼承)了Iterable接口,因此對於標准類庫中的任何集合都可以使用 for each 循環。
※,在Java SE8中,甚至可以不用寫循環。調用forEachRemaining方法並提供一個lamda表達式(它會處理一個元素). 例子如下:
Collection<Integer> collection = new ArrayList(); collection.add(3); collection.add(4); Iterator iterator = collection.iterator(); List<Integer> list = new ArrayList<>(); //e是Object類型 iterator.forEachRemaining(e-> { list.add((Integer) e + 3); }); System.out.println(list); //[6, 7]
※, 迭代時 元素被訪問的順序取決於集合類型。
- 如果對ArrayList進行迭代,迭代器將從索引0開始,每迭代一次,索引值加1。
- 如果對HashSet進行迭代,每個元素將會按照某種隨機的順序出現。可以遍歷到所有的元素,但是無法預知元素被訪問的次序。
※,實際上,JDK1.2中出現的Iterator接口中的next() 和 hasNext()方法與 JDK1.0中的Enumeration接口中的nextElement()和 hasMoreElement() 方法的作用是一樣的。Java類庫的設計者本可以選擇使用Enumeration接口,但是他們不喜歡這個冗長的名字,於是引入了具有較短方法名的新接口。
※,Iterator.next() 與 InputStream.read() 可以看作是等效的。
※,
3,泛型實用方法
4,集合框架中的接口
二,具體的集合
1,鏈表
2,數組列表
3,散列集
4,樹集
5,隊列與雙端隊列
6,優先級隊列
三,映射
1,
2,
四,視圖與包裝器
1,
2,
五,算法
1,
2,
六,遺留的集合
1,
2,
七,
1,
2,
八,
1,
2,