JAVA程式設計思想——分析閱讀
準備:
java程式設計思想電子版
别人整理的思維導圖
前言
适用範圍:Java SE5/6 版本。
Java的設計目标是:為程式員減少複雜性,縮短代碼的開發時間,跨平台複用。
學習方法:一模式或一節點就進入一練習,思維與實踐并行,現學現賣。
每當我認為我已經了解了并發程式設計時,又會有新的奇山峻嶺等待這我去征服。——作者都花了好幾個月寫并發這一篇章并發出這樣的感慨,我們又有什麼理由妄自菲薄呢。
緒論
學習語言時:需要在頭腦中建立一個模型,以加強對這種語言的深入了解;如果遇到了疑問,就将它回報到頭腦的模型中并推斷出答案。
一、對象導論
1 知識
人們所能夠解決的問題的複雜性直接取決于抽象的類型和品質。類型即指所抽象的是什麼,也可以說用的是什麼類型的語言。Java,C ,彙編,Python等。其中想C或者彙編要基于計算機的結構來求解問題,面向過程;而Java 等面向對象的語言是基于問題來求解,面向對象。
面向對象的5個基本特性:
- 萬物皆對象。抽象
- 程式是對象的集合,它們通過發送消息來告知彼此所要做的。對象之間的方法調用
- 每個對象都有自己的由其他對象所構成的存儲。封裝
- 每個對象都擁有其類型。class,繼承
- 某個特定類型的所有對象都可以接受同樣的消息。多态
類實際上就是一個資料類型,程式員根據需求,通過添加新的資料類型(class)來擴充程式設計語言,而不需要像面向過程語言那樣隻能使用現有的用來表示機器中的存儲單元的資料類型。
類創造出來的對象就是服務提供者(好處:有助于提高對象的内聚性,或者說通過設計模式的六大原則來設計對象提供服務),通過将問題的解決分解成對象集合的方式去調用(現有類庫)和設計建立對象。
通路控制的原因:
- 讓調用者無法觸及他們不應該觸及的部分,且通過控制符可以讓調用者很容易地區分哪些東西對他們很重要(public),哪些是可以忽略的(private)。
- 允許類或庫設計者可以改變類内部的工作方式而不用擔心會影響到調用方。
子類通過添加新方法(is like a 關系 )和覆寫父類方法(is a 關系)來改變與父類的差異。(但要滿足裡式替換原則)
單(跟)繼承的好處:
- 確定所有對象都屬于同一個基本類型。
- 保證所有對象都具備某些功能。
- 極大簡化參數的傳遞。(如參數可以直接放Object對象類型?)
- 使垃圾回收器的實作變得容易得多。
使用不同的容器選擇點是:
- 不同容器提供了不同類型的接口和外部行為。
- 不同的容器對于某些操作具有不同的效率。(例如:ArrayList查找快,增删慢(相對于LinkedList);LinkedList查找慢,增删快)
最後,最好要對其他語言(如python)有個清晰的認識,java語言是否符合項目的設計及未來的發展需要。
2 疑問
什麼是CGI?
CGI(Common Gateway Interface)公共網關接口,是外部擴充應用程式與 Web 伺服器互動的一個标準接口。其實就是我們通常說的接口。
3 思想總結
面向對象語言JAVA的特性是:抽象、封裝、繼承、多态。使用面向對象語言可以讓我們更好的面向問題來設計解決程式,也更容易讀懂和維護該程式的代碼,相對于面向過程語言來說。
二、一切皆對象
Java通過操作引用來操縱對象,就像我們用遙控器來操縱電視機一樣。
5個不同的地方存儲資料
- 寄存器。最快,因為它在處理器内部。
- 堆棧。速度僅次于寄存器,位于RAM,必須知道确切的生命周期來移動堆棧指針配置設定和釋放記憶體。基本資料類型和對象引用存儲在堆棧中。
- 堆。通用記憶體池也位于RAM。堆不同于堆棧的好處是不需要知道存儲資料或對象的生命周期,是以配置設定和釋放記憶體需要更多時間。用于存放所有的Java對象。
- 常量存儲。存放在程式代碼中或者隻讀的ROM(隻讀存儲器),因為不變所有是安全的。
- 非RAM存儲。一般是流對象和持久化對象。在程式不運作時也可以存在。
基本資料類型如下:
基本類型的值存儲在堆棧中,每種類型所占存儲空間大小不變,是以更具可移植性。
BigInteger(支援任意精度的整數) 和 BigDecimal (支援任意精度的定點數,貨币計算)屬于高精度資料類型,以方法調用代替運算符實作運算,是以運作速度較慢,是以速度換取精度的資料類型。
PS: 定點和浮點的差別
類的成員變量與局部變量是否需要初始化的差別:
類的成員變量如果是基本資料類型,即使沒有初始化值,java也會確定它獲得一個對應類型的預設值,防止程式錯誤。而java不會對局部變量進行預設初始化,如果局部變量沒有初始化指派,則會編譯報錯說變量沒有初始化。
類的成員變量預設初始化值如下:
PS:建構程式時通過反寫域名作為包名來避免名字沖突。(com.xixi)
什麼是處理器?什麼是寄存器?處理器内部結構是什麼樣的?為什麼寄存器存儲最快?
這是計算機組成原理的知識,需要補補。
堆棧通過上下移動堆棧指針來配置設定和釋放記憶體,那麼多線程的時候是如何配置設定記憶體的呢?
棧是線程私有的,是以多線程就有各自的棧,互不幹擾。
萬物皆對象!
三、操作符
别名現象是因為java操作中操作的是對對象的引用,是以會出現别名現象。注意方法參數傳遞是基本資料類型是值傳遞,對象類型是”引用傳遞“(如果替換整個對象沒事,但如果修改對象内的屬性值的話,原有對象會發生變化)。
整數乘除會直接去掉結果的小數位,不是四舍五入。
a++ :先生成值,再執行++運算。
++a : 先執行++運算,再生成值。
類對象比較時 : == 或者 != 比較的是對象的引用(即位址),而equals()比較的是内容。
普通的對象用equals()比較的還是引用,和 == 一樣,要想比較内容就得重寫equals()方法。
基本資料類型比較直接用 == 或者 !=
java指數的寫法
//編譯器通常會将指數作為雙精度(double)處理
double e = 1E-43d;// = 10 -43次方
double f = 1.6E43d;// = 1.6 * 10 43次
Random(47)的使用?随機數怎麼有效地創造及生成随機數的注意事項。
A:Random(47)使用見下:
@Test
public void randomTest(){
//如果不傳參數生成随機數對象Random,則java會把目前時間作為随機數生成器的種子seed,則每一次執行
//都會産生不同的輸出。而如果傳入固定值的seed,則每一次輸出都是可預見性的相同的值,每一次nextInt //的傳回值按次序都是相同的,固定seed友善測試驗證。
Random r = new Random(47);
//每次調用nextInt方法傳入的參數如100表示所産生的随機數的上限,下限為0,如果不想得到0的結果可以 +1
System.out.println( r.nextInt(100)+1);//随機數小于上限且不等于
System.out.println( r.nextInt(100)+1);
System.out.println( r.nextInt(100)+1);
//Random r1 = new Random();
}
如何重寫equals()方法比較内容?
單精度和雙精度的差別?
8進制和16進制的寫法?
@Test
public void otherTest(){
int c = 0x2f;//16進制 零x
System.out.println(Integer.toBinaryString(c));
int d = 0177;
System.out.println(Integer.toBinaryString(d));
//int e = 01987; //前面如果加了零,表示這個數是8進制的數,每位最大值不能超過7
}
java 按位操作符 & | ^ ~ 和移位操作符<< >> >>>的使用?在算法中用的是否普遍?
一些基本的操作符使用。
四、控制執行流程
無窮循環的形式
for(;;)
//或者
while(true){
//裡面沒有結束條件break;
}
禁止使用标簽跳躍,防止濫用,程式混亂。
switch 新特性:與 enum 枚舉或 String 一起使用。
吸血鬼數字解法
參考答案
@Test
public void xixueguiTest(){
int num =0;
for (int i=10;i<100;i++){
for (int j=i+1;j<100;j++){
int target=i*j;
if (target<1000||target>9999){
continue;
}
num++;
int[] targetNum = { target / 1000, target / 100 % 10, target / 10 % 100 % 10, target%10 };
int[] strNum = { i % 10, i / 10, j % 10, j / 10 };
Arrays.sort(targetNum);
Arrays.sort(strNum);
if (Arrays.equals(targetNum,strNum)){
System.out.println(target + " = " + i + " * " + j);
}
}
}
System.out.println(num);
}
一些普通的流程控制,如 while , for , break , continue , switch
五、初始化與清理
初始化
建立對象時通過自動調用構造器來確定初始化。
類和方法的命名:名字起的好可以使系統易于了解和修改。
方法重載或構造器重載相當于人類語言的備援性——可以從具體的語句中推斷出含義。重載的規則由參數類型,個數,順序的不同來确定,一般不推薦順序不同。注意,傳回值的不同不能用于重載,因為有時候調用有傳回值的方法并不必須要傳回值,這樣編譯器無法區分是調用哪個。
基本資料類型的重載思想是能從一個較小類型如int自動提升至一個較大類型如double,如果要把較大類型如double轉為較小類型如long則必須強轉。這部分我覺得除非不得已,絕對不進行這種自動提升重載方法,不便于了解。
this關鍵字的使用場合:
- 隻有當需要明确指出對目前對象的引用時,才需要使用this關鍵字。如需要傳回對目前對象的引用時,
- return this.
- 将目前對象傳遞給其他方法時(作為參數)。
- 一個類中有多個構造器,構造器之間調用另一構造器使用this。this(a,b),this(a)。注意構造器調用必須置于第一行,是以構造器調用其他構造器一次隻能調用一個,要調用多個就要構造器間嵌套調用。注意:隻能構造器調用構造器,構造器禁止被其他方法調用。
- 通過構造器給類成員變量指派,如 this.a = a;
靜态初始化隻有在必要時刻(類第一次加載.class檔案時:一般是類對象的第一次建立或第一次直接用類通路靜态資料時)才會進行。之後無論建立多少對象,靜态資料都隻占用一份存儲區域。
PS:構造器實際上也是static靜态方法。
對象的建立過程:
- java解釋器查找類路徑,定位如Monkey.class 檔案。
- 載入Monkey.class,執行是以靜态初始化動作。
- 在堆上為Monkey對象配置設定足夠的存儲空間。
- 存儲空間清零,Monkey對象的是以類成員變量置為預設值,如0,false,null。
- 執行所有類成員變量的初始化動作。
- 執行構造器。
用代碼塊來初始化類成員變量的執行個體與靜态成員變量的初始化差不多,代碼塊來初始化類成員變量的執行個體也在構造器之前執行,差別在于靜态成員變量的初始化隻有一次,而代碼塊來初始化非靜态類成員變量在每次建立對象時都會執行。
數組初始化
編譯器不允許指定數組的大小,數組的建立是在運作時刻進行的。數組的長度一旦确定則不可變。建立數組為非基本資料類型時,該數組為引用數組,數組元素存儲的是引用對象的引用(位址)。
注意:通過花括号{}初始化清單時最後一個逗号是可有可無的。
@Test
public void arrayTest(){
//數組内元素的值會根據資料類型自動初始化為空值,如int為 0.
int[] a = new int[5];
Integer[] b = {1,2,new Integer(3)};
//注意建構格式 new String[]{};
String[] maomao = new String[]{"mao","mao","hong"};
}
focus: 數組使用參考
清理
垃圾回收注意事項:
- 對象可能不被垃圾回收。
- 垃圾回收并不等于”析構“(c++)。
- 垃圾回收隻與記憶體有關。
fanalize()方法用于釋放為本地方法(一種在java中調用非java代碼的方式)配置設定的記憶體。
GC前,會調用finalize()方法,是以可以重寫finalize()方法來驗證終結條件。
System.gc():強制GC(不一定觸發)
自适應垃圾回收技術思想依據:對于任何“活”的對象,一定能最終追溯到其存活在堆棧或靜态存儲區之中的引用。
類的初始化順序:
成員變量定義的先後順序決定了變量初始化的順序。而類成員變量初始化是最早的,然後是構造器,再然後是方法。
可變參數清單
應用場合:參數個數和類型未知時使用。
可變參數清單本質上還是數組,當我們指定參數時,編譯器實際上會為什麼去填充數組,是以我們可以用foreach疊代周遊。可變參數清單也可以接受其對應類型的數組當做可變參數清單。
可變參數清單不依賴于自動包裝機制,實際使用的是基本資料類型,且可變參數清單可以使用自動包裝機制裝箱拆箱。
推薦:在使用可變參數清單時,如果有重載方法,則應該隻使用一個版本類型(如下)的重載方法,而不是參數類型個數不同等重載方法。
private void printArray(int a,Object... obj){
for (Object o : obj) {
System.out.print(o);
}
System.out.println();
}
private void printArray(float a,Object... obj){
for (Object o : obj) {
System.out.print(o);
}
System.out.println();
}
枚舉enum
常量命名:大寫字母及下劃線。
enum與switch是絕佳的組合,我們可以将enum用作另外一種建立資料類型的方式。
在建立一個類時,在定義時就被初始化了的String域域通過構造器初始化的String域兩種方式的差異?
A: 差别在于strA一開始被初始化為"strA",而strB先被初始化為null,然後再被初始化為"strB" .
class Test{
private String strA = "strA";
private String strB;
Test(){
strB = "strB";
}
}
代碼塊來初始化非靜态類成員變量的執行個體如何支撐匿名内部類的初始化?
java枚舉的使用?有什麼特别實用的使用技巧麼?
初步了解java的初始化和自動清理過程,注意數組的初始化使用,可變參數清單适用于參數個數和類型未知的場合,枚舉enum用于自定義資料類型,并最好能與switch配合使用最佳。
六、通路權限控制
通路權限控制的前景交代是:疊代,重構,需求變更等為了增加可讀性、了解性和可維護性。
通路權限控制修飾詞:把變動的事物與保持不變的事物區分開,是對具體實作的隐藏,即封裝。(package和import)
一個類隻有一個public class類,其他類都不算public 的,它們主要是為主public類提供支援的。
用導入限定的命名空間位置描述來提供一個管理名字空間的機制。
java包命名規則是全部使用小寫字母連接配接。
有沖突名字情況下,則必須傳回到指定全面的方式。
将構造器私有(private),則不能通過構造器來建立對象,而必須調用專門的如getInstance()方法來建立對象。且因為構造器是私有的,他阻止對此類的繼承。
protected 修飾符處理的是繼承的概念(包含包通路權限)。
類隻能定義包通路權限和public權限,如果是預設的包通路權限,則該類不能被非同一個包下的類建立通路。
java比c的名字空間控制好在:
- package 定義每個類的包路徑
- 包命名模式(域名反寫)
- 關鍵字import
如何把類庫空間置于包中?或者說設計項目時如何配置設定類的位置和建立包分類。
這個一般有個預設的分類。可以參考這個分類。
java如何實作條件編譯?如何定義程式為調試版和釋出版?現在這樣定義還有價值麼>
private在多線程環境下的重要性?
被private修飾的私有方法不會有線程安全問題。
通路權限控制的原因有二:
- 隔離不屬于用戶端所需接口的部分實作封裝,也使用戶端能更清楚地了解哪些對他們是重要的和哪些是可以忽略的。
- 讓方法或接口提供者可以安全地更改和疊代類内部的工作方式和結構。
權限是約定俗成的東西,權限的修飾符可以更好地友善于程式的了解疊代和維護。
七、複用類
複用類的方式有二:組合與繼承。
初始化類成員變量的方式有四:
- 在定義對象的地方。這樣則總能在被構造器調用之前被初始化。
- 在類構造器中。
- 在使用這些對象之前,稱為惰性初始化(一般是使用前判空,為空則初始化該對象)。
- 使用執行個體初始化。(代碼塊等)
繼承自動得到父類中所有的域和方法,所有一般在設計繼承時會将所有的成員變量(資料成員)指定為private,所有的方法指定為public。
子類調用父類方法用super關鍵字(如:super.action() )。
當建立一個子類對象時,該對象包含了一個父類的子對象。是以當子類建立對象時,會先去建立父類子對象,及父類對象,然後再初始化子類的成員變量,最後調用子類的構造器。(p130練習5)PS:建立父類時會調用父類構造器【注意:每次建立子類時都會去調用父類構造器建立一個父類的子對象,即每次建立子對象都會調用父類構造器建立對象,不是想static常量那樣隻建立一次的】,如果是預設構造器則可以不寫,預設調用父類的無參構造器,如果是隻有帶參數的構造器,則必須在子類構造器第一行顯式調用父類構造器【super(3) 】。
組合與繼承
組合和繼承都允許在新的類中放置子對象(成員變量),組合是顯示地這麼做,而繼承是隐式的這麼做。一般組合中的成員變量會修飾為private,為了安全而隐藏具體實作。
組合與繼承的選擇在于兩種之間的關系是"is -a"還是 "has-a"關系。選擇繼承時要判斷自己需不需要從新類向父類進行向上轉型,需要才可以用繼承。
向上轉型和向下轉型用于有繼承結構的對象,友善一視同仁地操作對象。
final關鍵字
final 修飾一般表示這是無法改變的。
不可改變的理由:設計或效率。
final資料
一個既是static又是final的域隻占據一段不能改變的存儲空間。
對于基本類型,final使數值恒定不變,而對于對象引用(數組也是),final使引用恒定不變,但對象自身卻可以被修改。是以,使引用成為final沒有使基本類型成為final的用處大。
空白final:指被聲明為final但又未給定初值的域。可以通過構造器初始化final域來對一個類的final域初始化值,使之可以根據對象的不同而有所不同,且又保持恒定不變的特性。
final參數:無法在方法中更改參數引用鎖指向的對象或基本參數的值(PS:參數引用的對象的值還是可以更改,太雞肋,沒用)。可以讀參數,無法修改參數。主要用來向匿名内部類傳遞資料。
final方法
使用的原因:一是鎖定方法,防止被繼承覆寫重寫修改該方法的含義。二是效率。(在虛拟機是HotSpot已經沒必要了)
類中所有private方法都隐式的指定為是final的。但因為方法是private的,所有隻在該類中私有使用,不影響其他繼承類也繼續叫這個方法名。
final類
被final修飾的類不可被繼承。final類的域是可變的,和其他的一樣,除非被final修飾;而final類的所有方法都無法覆寫重寫,因為final禁止繼承,是以的方法都隐式指定為final的。final類的方法就無需加final修飾了。
設計類時,除非明确不想被覆寫,否則不應給類或方法加final修飾,既沒有效率提高可能,又可能會妨礙其他人通過繼承來複用這個類。
繼承與初始化(p146案例)
- 一開始會先加載父類的static靜态變量初始化,
- 然後再加載子類的static靜态變量初始化,
- 再然後是初始化父類的成員變量為預設值,
- 然後是初始化子類的成員變量為預設值,
- 然後是調用父類的構造器,
- 再然後調用子類的構造器。
//典型常量定義方式:
/**
*public 公開的 ; static 靜态的,隻有一份 ; final 常量,不可變的
*/
public static final int VALUE_TWO = 3;
//final修飾的資料隻能在運作時才能确認他的值,編譯時是不能确認他的值的,從可以把随機值指派給final域得知
static final int INT_A = rand.nextInt(20);
其他:
toString()方法會在編譯器列印一個String而傳遞的是一個對象時自動調用該對象的toString()方法,是以如果想要使要列印日志的對象具備這樣的行為時隻有編寫一個toString()方法,否則列印的是對象的位址類型組合資訊。
每個類都可以建立一個main()方法,友善單元測試,且無需删除。即使一個程式中含有多個類,也隻有指令行鎖調用的哪個類的main()方法會被調用,及我們run的那個類方法。
ArrayList 代替了 Vector;
HashMap 代替了 HashTable
final參數主要用來向匿名内部類傳遞資料,對于引用參數的内部的值是可以改變的,加final有什麼意義麼
是以要看場合添加,不是為了防止内重寫或者确定是不可變的對象特别是基本類型值時,沒有必要添加。
多用組合,少用繼承。
設計類時要遵循單一職責,繼承是要遵循裡式替換原則。
final關鍵字主要用在設計上想得到不可變元素、方法和類上時。
設計系統時應考慮程式開發是增量過程,如圖人類的學習;要有擴充性,像進化的生命體而不是想設計摩天大樓一樣快速見效?
八、多态
多态的作用:消除類型之間的耦合關系。
綁定:将一個方法調用同一個方法主體關聯起來被稱作綁定。
前期綁定:在程式執行前進行綁定,如面向過程語言C。
後期綁定:在運作時根據對象的類型進行綁定,如JAVA。
Java除了static、final(private也屬于final)方法之外,都是後期綁定。是以當我們聲明一個方法為final時,意思是關閉動态綁定。
多态的"缺陷"
- 父類中隻有非private 方法才能被覆寫。否則其實在子類中名字相同的隻是個全新的方法,并沒有覆寫。
- 成員變量在多态時通路的是父類的成員變量值(如果成員變量相同的時候,這時候子類包含這兩個名字相同的成員變量值,一個是父類的,通過super.field調用,一個是子類的,this.field),方法通路的是子類的複寫方法(如果有覆寫重寫的話)
- static靜态方法是類方法,與對象無關,不具有多态性
在實際工作中,我們一般不會這樣,一般會把成員變量都修飾為private(再通過get/set方法提供通路),且對于父類和子類的成員變量命名也不會相同,避免引起混淆。
//多态
Shape s = new Cycle();
//普通建立對象
Cycle c = new Cycle();
多态和構造器
構造器的特殊任務:檢查對象是否被正确地構造。
複雜對象調用構造器的順序:
- 在對象建立之前,将配置設定給對象的存儲空間初始化為二進制零。
- 調用父類構造器,這個步驟會遞歸傳遞到Objcet對象。
- 按照聲明順序調用成員變量的初始化方法。
- 調用子類構造器的主體。
注意:每一個類的初始化都遵循如果第一次調用的話,會有static成員變量先初始化,然後是類的成員變量的初始化,再然後是構造器的調用。這一調用順序在父類或者成員對象的調用中都适用。當然要注意的是如果類的static成員變量已經不是第一次初始化則不會再調用了。
PS:如果父類在初始化的構造器中調用覆寫的方法,則根據多态調用的其實是子類覆寫的方法,隻是由于子類還未初始化,其中如果有成員變量的話則值為0.
編寫構造器準則:
用盡可能簡單的方法使對象進入正常狀态;如果可以的話,構造器避免調用其他方法。構造器唯一可以安全調用的是final修飾的方法(包括private)。其他的會多态到子類上去。
協變傳回類型允許我們寫方法時傳回更具體的對象類型。比如不是Shape而是Cycle.
通過繼承表達行為間的差異,并用成員變量(相同的接口,不同的類型賦予不同的子類)表達狀态上的變化。
注意:
多态時建立的對象調用隻能是父類有的方法,因為多态時引用是向上轉型的,子類的擴充方法會“丢失”,如果要使用子類的擴充方法則要向下轉型。如果向下轉型不成功(不是該類型或其父類)則會報ClassCastException(類轉型異常)。
不同的類型修飾構造器有什麼差別,一般private是不想讓對象被建立,用于單例,那public、protected、和預設的使用有什麼講究麼?
這個注意就是看作用的範圍域了,在使用時盡可能地縮小範圍,而不是一開始就使用public修飾,除非是對外提供的方法。
static可以修飾構造器麼?
A:構造器就是預設的static了,是以不允許。confirmed.
不同通路權限的修飾符修飾的static成員變量、類成員變量、構造器初始化的先後順序是按照聲明順序來的麼?
如果是private static 修飾的靜态類,隻會在明确調用到它時才會初始化,參考單例的靜态内部類寫法,利用了靜态内部類延遲初始化的特性。
多态讓程式針對一體性類型的對象統一處理有了可能,讓程式的開發更加迅速,代碼編寫更加人性化處理,也使得擴充和更加容易。
但是也要注意多态的缺陷,那就是多态針對的是對象的公共行為,對象的靜态方法和對象成員變量及私有行為(private、final)都是不能多态的。
還有,因為多态增加了對象的組織複雜和龐大,是以使用的原則是多用組合,少用繼承。
九、接口
接口和内部類(特别是匿名内部類)為我們提供了一種将接口與實作分離的更加結構化的方法。
抽象類:
它是普通的類與接口之間的一種中庸之道。特别是在不能使用純接口的時候。抽象類适用于重構,這樣我們可以将公共方法沿着繼承層次結構向上移動。(PS:隻有類名上修飾了abstract則不管有沒有抽象方法,該類都是一個抽象類,不能被創造出對象。當然更可以全部是abstract,這種其實就是接口了。)
接口:
接口可以包含成員變量。其成員變量都是static和final的(使接口成為便捷的用來建立常量組的工具,不過如果是enum枚舉類型常量的話最好還是用enum,直覺好看),即靜态常量;接口内所有的方法和成員變量都是public的,無論是否寫public修飾。
如果接口定義時不加public修飾符(接口或者類裡面的接口定義),則該接口隻有包通路權限,隻能在同一個包内使用。引申出接口可以嵌套在類或其他接口中。
Java通過多接口實作多重繼承,其他具體實作類或者抽象類都隻能單繼承。
在接口和抽象類選擇中盡量選擇接口來設計。(作者的建議是前期如果沒有必要可以直接選擇設計類而不是接口,看中需要性)
在繼承和實作的接口中方法名一樣時,一起按照重寫和重載的規則來,相同則隻要有一個實作就行(或者重寫),不同的則看方法簽名不同實作重載。(如果隻是傳回值類型不同則無法重載編譯器會報錯無法實作)
接口配合政策模式和擴充卡模式使得程式更加地靈活。
接口中的類呢?也是常量麼?
接口中可以放置嵌套類(内部類),并且是自動
public
和
static
的。
面向接口程式設計才能解耦,依賴倒置。
十、内部類
一個類的定義在另一個類的定義内部,就是内部類。
内部類的使用可以很友善地隐藏實作細節。
注意:注意是要在另一個類的内部,如果是在外面就是一個普通的類,當然該類不能是public的,因為一個類檔案隻能有一個public類型的類;如果是内部類的話,則可以是public的。
内部類可以直接通路其外部類的方法和成員變量,包括private等所有的方法。(這是因為内部類對象在建立時會秘密捕獲一個指向外部類的引用。)
内部類還是一個完整的類,跟其他類一模一樣,有類的通路權限限制等。差別在于
- 一:内部類依賴于外部類,是以。内部類對其外部類是完全透明可見的,包括其private的私有成員變量,外部類也能通路。
- 二:要注意内部的定義的作用域,超出作用域則内部類不可用。
在方法和作用域内的内部類
内部類可以定義在任何地方,包括方法的參數,方法内部(不能用private修飾),方法的作用域内(局部内部類)。
匿名類不可能有構造器。(想要用的話可以用父類的帶參構造器,父類不能是接口才行,必須是普通類或者抽象類)
匿名内部類可以有字段,方法,還能夠對其字段執行初始化操作。
匿名内部類使用外部定義的對象或值時,編譯器要求其參數引用是final類型的。如果是通過匿名内部類的父類構造器傳遞參數進來的話,則不需要是final類型的,因為這個參數并不會被匿名内部類直接使用。是以加final修飾是為了保證直接使用時該内部類的成員變量不可變?
匿名内部類與正規的繼承差別:
匿名内部類既可以擴充類,也可以實作接口,但不能兩者兼備。如是是實作接口,也隻能實作一個接口。
嵌套類
把内部類聲明為static類型的我們稱為嵌套類,嵌套類不需要内部類對象與其外部類對象之間有聯系。
- 建立嵌套類對象,不需要外部類的對象。
- 不能從嵌套類的對象中通路非靜态的外部類對象。(因為嵌套類沒有儲存外部類對象的引用,不需要依賴外部類)
嵌套類與内部類的差別
普通内部類的字段與方法,隻能放在類的外部層次上,是以普通的内部類不能有static資料和static域,也不能包含嵌套類。但是嵌套類可以有static資料和static域,也能包含嵌套類。
嵌套類是内部類的一種,其編譯後的class檔案還是在外部類檔案中。
在接口中寫的内部類因為是接口裡面的,自動是public和static的,是以接口中的内部類是嵌套類。我們可以在接口中放置内部類(嵌套類)代碼。可以用于建立某些公共代碼,使得他們可以被某個接口的是以不同實作所共用。
内部類無論嵌套了多少層的内部類,它都能透明地通路是以它嵌入的外部類的所有成員(即使是外部類的private成員、方法)。
為什麼需要内部類
内部類實作一個接口與外部類實作這個接口的差別:
外部類實作一個接口不是總能享受到接口帶來的友善,有時需要用到接口的實作。(比如外部類已經有同名的方法實作了,無法重複覆寫寫出想要的覆寫方法)
這時候由内部類實作接口的優勢:每個内部類都能獨立地繼承自一個(接口的)實作,是以無論外部類是否已經繼承了某個(接口的)實作,對于内部類都沒有影響。
内部類使得多重繼承的解決方案變得完整:内部類允許繼承多個非接口類型。(類或抽象類,因為每個内部類都可以去繼承不同的類,這樣這個外部類就有了多種繼承的能力)
實作2個接口時我們可以選擇使用單一類(多實作),或者使用内部類來實作。而如果實作的是2個抽象類或者具體的類,而不是接口,就隻能使用内部類才能實作多重繼承。
内部類使用前提:如果不需要解決多重繼承的問題,那麼自然還是用别的程式設計方式。
使用内部類可以獲得的特性:
- 内部類可以有多個執行個體,每個執行個體都有自己的狀态資訊,并且與其外部類對象的資訊互相獨立。
- 在單個外部類中,可以讓多個内部類以不同的方式實作同一個接口,或繼承同一個類。(實作不同的行為效果,有點政策模式的意思)
- 建立内部類對象的時刻并不依賴于外部類對象的建立。(通過static方法調用或者建立的是嵌套類?)
- 内部類并沒有令人迷糊的"is -a"關系,他就是一個獨立的實體。
主要用來響應事件的系統被稱為事件驅動系統。
注意這個處理事件結合的寫法,把要疊代處理的事件用一個新的集合包裝,再處理完畢之後對原有的處理事件集合進行移除事件(在此期間可能會原事件集合還可能會進行添加待處理事件,是以這樣處理邏輯是極好的),這樣就不會影響到疊代循環的長度。
通過内部類,使變化的事務與不變的事務互相分離(模闆方法)。内部類允許:
- 控制架構的完整實作是有的那個的類建立的,進而使得實作的細節被封裝起來。内部類用來表示解決問題所必需的各種不同的action()。
- 内部類能夠很容易地通路外部類的任意成員,是以可以避免這種實作變得笨拙。
内部類覆寫
如果兩個外圍類都有相同名字的内部類,而這兩個外圍類是繼承關系的話,這時候這兩個内部類是完全獨立的兩個實體,各自在自己的命名空間内,沒有繼承關系。而當這兩個内部類一個明确繼承另外一個的時候,則他們便有繼承關系。和普通類繼承沒什麼不同。
局部内部類
局部内部類指在代碼塊裡面的内部類,典型如方法體的裡面。局部内部類不能有通路說明符(public權限通路符),其他的和内部類一樣。局部内部類與匿名内部類的使用差別:
- 我們需要不止一個該内部類的對象。
- 我們需要一個已命名的構造器,或者需要重載構造器。匿名内部類做不到,因為匿名内部類隻能用于執行個體初始化。
如果不是這些需要,還是直接用匿名内部類好了。
其他知識點補充:
通路說明符:指的是通路權限符,如public,private。
回調:
參考
如下圖所示, 回調是一種雙向的調用方式, 其實而言, 回調也有同步和異步之分, 講解中是同步回調, 第二個例子使用的是異步回調 。
回調的思想是:
- 類A的a()方法調用類B的b()方法
- 類B的b()方法執行完畢主動調用類A的callback()方法
通俗而言: 就是A類中調用B類中的某個方法C, 然後B類中反過來調用A類中的方法D, D這個方法就叫回調方法。
//建立内部類對象
Outer outer = new Outer;
Outer.Inner inner = outer.getInner();
//内部類的基本使用如下
public class Outter {
public class Inner{
public void getShow(){
//兩種調用方法,第一種比較明顯能展示該方法是外部類的方法
Outter.this.show();
show();
}
public Outter getOuter(){
//内部類中,this表示該内部類,Outter.this 表示内部類對應的外部類的引用對象
return Outter.this;
}
}
//靜态内部類則不需要建立外部類對象,直接用類調用方式
public static class StaticInner{
public static void staticShow(){
System.out.println("staticShow");
}
}
public void show(){
System.out.println("outer");
}
public Inner getInner(){
return new Inner();
}
public static void main(String[] args) {
Outter outter =new Outter();
//内部類的建立可以如下:使用外部類的引用.new文法建立内部類對象
Outter.Inner inner = outter.new Inner();
//也可以通過外部類方法來建立一個
Inner in = outter.getInner();
in.getShow();
inner.getShow();
inner.getOuter().show();
Outter.StaticInner.staticShow();
}
}
//=====================================================
//有參匿名内部類使用方法 前提是得有普通類或者抽象類(帶有參數構造器)實作。
public abstract class Fishing {
int i =0;
public Fishing() {
}
public Fishing(int i) {
this.i = i;
}
public void fish(){
System.out.println("fish"+i);
}
}
public class Human {
public Fishing getFishing(int a){
return new Fishing(a){
private String name = "maomao";
@Override
public void fish() {
super.fish();
}
};
};
}
public static void main(String[] args) {
Human human = new Human();
Fishing fishing = human.getFishing(10);
fishing.fish();
}
}
//=====================================================
//p199 用匿名内部類實作工廠方法
public class Platform {
public static void ride( CycleFacrory facrory){
facrory.getCycle().getName();
}
public static void main(String[] args) {
ride(Bicycle.facrory);
ride(Unicycle.facrory);
ride(Tricycle.facrory);
}
}
public class Bicycle implements Cycle {
//靜态單例工廠建立
public static CycleFacrory facrory = new CycleFacrory() {
@Override
public Cycle getCycle() {
return new Bicycle();
}
};
@Override
public void getName() {
System.out.println("Bicycle");
}
}
//工廠接口
public interface CycleFacrory {
public Cycle getCycle();
}
//産品接口
public interface Cycle {
public void getName();
}
靜态類裡的方法沒有加static修飾還是靜态方法麼?
A:不是,待詳細解答。
為什麼方法内部的内部類不能用private修飾?
匿名内部類使用外部定義的對象或值時,編譯器要求其參數引用是final類型的,為什麼呢?
靜态類裡的類(static修飾的class)和方法都沒有static修飾,屬于靜态方法和靜态常量麼?
嵌套類的作用?
普通内部類的字段與方法,隻能放在類的外部層次上,是以普通的内部類不能有static資料和static域,也不能包含嵌套類。但是嵌套類可以有static資料和static域,也能包含嵌套類。為什麼通的内部類不能有static資料和static域?
建立内部類對象的時刻并不依賴于外部類對象的建立。為什麼不需要?什麼時候不需要?(通過static方法調用或者建立的是嵌套類?)
閉包?閉包的作用?
回調?回頭網上結合研究下
enclossingClassReference.super(); ?繼承内部類時為什麼要用這個?
P245 練習26裡如何生成一個帶參數的構造器?目前能實作的隻能是帶繼承的内部類的外部類的引用參數,而不能建立内部類自帶的參數,因為這樣無法繼承了。待處理?
内部類的使用主要用于配合彌補接口無法達到的多重繼承的時候使用,還有用于封裝實作,達到解耦。
其實如非必要,在設計階段盡量規避使用内部類實作,大部分情況下單繼承及接口實作便能滿足需求了。
十一、持有對象
基本的容器使用:List、Set、Queue、Map。
數組使用的局限:數組具有固定的尺寸的局限性,使用起來不夠靈活。
集合容器通過使用泛型,就可以在編譯期防止将錯誤類型的對象放置到容器中。
如果不需要使用每個元素的索引(對索引有操作),可以使用foreach來疊代資料。
使用多态來建立容器,但是,當想要使用具體容器類的額外的功能時,就要直接建立具體類。如下:
List list = new ArrayList();
//List沒有peek()方法
LinkedList linkedList = new LinkedList();
linkedList.peek();
//Arrays.asList();底層表示還是數組,是以不能調整尺寸,不能使用add()或delete(),但可以修改set()
List list1 = Arrays.asList();
//通過顯示類型參數說明,告訴編譯器對于Arrays.<Snow>asList();産生的List類型,這樣才能編譯成功。
List<Snow> list2 = Arrays.<Snow>asList();
//數組容器的列印 必須使用方法
Arrays.toString()
//其他容器不需要,直接toString()即可(即直接傳入對象就可以調用預設toString()列印容器)。
//Collection列印出來的内容用方括号包覆,逗号分隔。[1,2,3,4]
//Map列印出來的内容用大括号包覆,逗号分隔,鍵和值由等号聯系。{rat=1,tiger=2,monkey=3,drogon=4}
ArrayList 和 LinkedList 都是List類型,它們都按照被插入的順序儲存元素。
HashSet、 TreeSet 、 LinkedHashSet 都是Set類型,Set的儲存的值都是唯一的,存儲元素方式的差別:
HashSet: 通過散列值來存儲元素,使用HashSet可以最快地擷取元素。
TreeSet: 按照比較結果升序來儲存對象,如果存儲順序很重要用TreeSet 。
LinkedHashSet : 對存儲順序,按照被添加的順序儲存對象。
Map: 也稱關聯數組,像一個簡單的資料庫。
HashMap:提供最快的查詢技術,不是按照明顯的順序儲存元素。
TreeMap:按照比較結果升序來儲存鍵。
LinkedHashMap:按照被添加的順序儲存對象,同時保留了HasMap的查詢速度。
List
ArrayList :優勢在随機通路元素,但是在List的中間插入和移除元素時較慢。
LinkedList :優勢在與在List的中間插入和移除元素時代價較低,并提供了優化的順序通路。缺點是在随機通路方面相對比較慢。
P256 ListFeatures.class 示範了ArrayList 的主要常用操作方法。
ArrayList的indexOf()、remove()方法針對的是對象的引用操作及對象(比較基于equals()方法),這樣就不會因為相同的對象也能被remove。
ArrayList通過containsAll()方法來判斷包含關系,并不會因為排序和順序問題而不同,比較的隻是元素的包含關系。
retainAll()是取交集的操作。
isEmpty()判斷容器集合是否為空
clear()清除集合操作。
Pet[] pet = list.toArray(new Pet[0]);//傳回一個具有合适尺寸的數組。
疊代器
//疊代器模式,不關心具體的容器集合類型
Iterator iterator = list.iterator();
//ListIterator隻能用于各種List類的通路。可以進行雙向移動,往前往後,且可以增删改查。
ListIterator listIterator = list.listIterator();
LinkedList
LinkedList 還添加了可以使其用作棧、隊列或雙端隊列的方法。有些方法作用相同而名字不同,是為了在特定用法的上下文環境中更加适用(特别是在Queue)如getFirst()和element()與peek().三個都是擷取第一個元素的方法,前面兩個如果List為空則抛出異常,而第三個為空時傳回null。
還有其他一些也是一樣的。
TreeSet: 按照比較結果升序來儲存對象,我們還可以對TreeSet傳入我們想要的排序特性。
//不區分大小寫字母排序
SortedSet set = new TreeSet(String.CASE_INSENSITIVE_ORDER);
Stack
LinkedList具有能夠直接實作棧的是以功能的方法,是以可以直接将LinkedList作為棧使用。
類名之後的告訴編譯器這将是一個參數化類型,當類被使用時會被實際類型所替換,就是T.
Java.util.Stack 設計欠佳。
public class Stack<T> {
private LinkedList<T> storage = new LinkedList<>();
public void push(T v){
storage.addFirst(v);
}
public T peek(){
return storage.getFirst();
}
public T pop(){
return storage.removeFirst();
}
public boolean empty(){
return storage.isEmpty();
}
@Override
public String toString(){
return storage.toString();
}
}
Set
Set不儲存重複的元素。
Set最常被使用的是測試歸屬性,經常要詢問某個對象是否在某個Set中,是以查詢是Set中最重要的操作。而,使用HashSet可以最快地擷取元素。
//使用contains()方法測試歸屬性
boolean contains = set1.contains("a");
Map
将對象映射到其他對象的能力是一種解決程式設計問題的殺手锏。
Map可以傳回它的鍵Set(因為鍵都是唯一的),它的值Collection(因為它的值可以是相同的,是以不是set),或者它的鍵值對的Set(是個EntrySet)。
@Test
public void mapTest1() {
Map<Integer,Integer> map = new HashMap();
map.put(Integer.valueOf(1),1);
map.put(Integer.valueOf(2),1);
boolean containsKey = map.containsKey(1);
System.out.println("containsKey="+containsKey);
boolean containsValue = map.containsValue(1);
System.out.println("containsValue="+containsValue);
Set<Integer> keySet = map.keySet();
System.out.println("keySet="+keySet);
Collection<Integer> values = map.values();
System.out.println("values="+values);
Set<Map.Entry<Integer, Integer>> entrySet = map.entrySet();
System.out.println("entrySet="+entrySet);
}
Queue
隊列是一個典型的先進先出(FIFO)容器。
隊列在并發程式設計中特别重要,因為它可以安全地将對象從一個任務傳輸給另一個任務。
LinkedList 可以作為Queue的一種實作,因為它實作了Queue接口。
queue.offer()方法将一個元素插入到隊尾。
peek()和element()在不移除的情況下傳回隊頭。peek()在隊列為空是傳回null,element()則抛異常。
poll() 和 remove() 移除并傳回隊頭。poll()在隊列為空是傳回null,remove()則抛異常。
PriorityQueue
優先級隊列的下一個彈出元素是最需要的元素(具有最高的優先級)。
PriorityQueue 確定當我們調用peek()、poll() 和 remove()時,擷取的元素是隊列中優先級最高的元素。
如果需要,可以通過提供自己的Comparator來修改這個順序。
@Test
public void queueTest() {
Queue<String> queue = new LinkedList();
String[] split = "i am iron man".split(" ");
for (int i = 0; i < split.length; i++) {
queue.offer(split[i]);
}
while (queue.peek() != null) {
//System.out.print(JSON.toJSONString(queue.remove()));
System.out.print(queue.remove()+" ");
}
}
@Test
public void queueTest() {
Queue<String> queue = new LinkedList();
String[] split = "i am iron man".split(" ");
for (int i = 0; i < split.length; i++) {
queue.offer(split[i]);
}
while (queue.peek() != null) {
//System.out.print(JSON.toJSONString(queue.remove()));
System.out.print(queue.remove()+" ");
}
java.util.List<Integer> list = Arrays.asList(19, 2, 3, 5, 7, 10);
PriorityQueue<Integer> priorityQueue = new PriorityQueue(list);
//注意,必須用方法列印出來,如果隻是用JSON.toJSONString(priorityQueue)是不能得到效果的,因為存儲并不固定,而是通過擷取元素時比較來确定優先級的
while (priorityQueue.peek()!= null){
System.out.print(priorityQueue.remove()+" ");
}
System.out.println();
//System.out.println(JSON.toJSONString(priorityQueue));
PriorityQueue<Integer> priorityQueue1 = new PriorityQueue(list.size(),Collections.reverseOrder());
priorityQueue1.addAll(list);
while (priorityQueue1.peek()!= null){
System.out.print(priorityQueue1.remove()+" ");
}
// System.out.println(JSON.toJSONString(priorityQueue1));
}
我們可以通過參數化Collection來表示容器之間的共性或者實作iterator()方法來實作疊代器功能。C++沒有Collection而是通過疊代器來表示容器的共性。Java都有。
foreach:
隻要我們建立的類實作了Iterable接口,就能用foreach文法糖。Iterable接口包含一個能夠産生Iterator的Iterator()方法。
foreach語句可以用于數組或其他實作了Iterable接口的類,但注意:數組不是一個Iterable。
其他知識:
@Suppress Warnings (unchecked):表示隻有有關“不受檢查的異常”的警告資訊應該被抑制。這樣就不會有黃色警告了。
預設的toString()方法将列印類名@散列碼(例:Apple@11b86e7 。是hashCode()方法生成的無符号16進制數)。
容器結構圖:
既然LinkedHashMap按照被添加的順序儲存對象,同時保留了HasMap的查詢速度。為什麼我們不用LinkedHashMap替換HashMap?其實還是有影響的?
LinkedHashMap效率肯定沒有HashMap高,畢竟還要維護一個插入的先後順序,如果不需要關心插入順序,那用HashMap就好了,時間複雜度是O(1).
ArrayList 和 LinkedList 隻是在List中插入資料快慢有差別?在預設add方法插入有性能差別麼?LinkedList 缺點是在随機通路方面相對比較慢,那麼疊代器順序通路差別還很大麼?
要回答這個問題需要了解ArrayList 和LinkedList 底層的資料結構。ArrayList 底層是數組實作的,是以它要随機通路一個值隻要根據數組下标即可,而LinkedList底層是連結清單,則需要從頭到尾周遊一遍去尋找才行。相對于的,連結清單結構增删特别是在任何位置增删都非常快,隻要更換頭尾節點的引用即可,而數組則在對中間位置進行插入時則會導緻其他元素要進行位置騰挪。
ArrayList 和 Stack 這些後面尖括号裡面的值是有差別的麼?還是隻是一個類型代稱?
這隻是泛型的一個描述,沒有具體意思。
SortedSet set = new TreeSet(); SortedSet 不是 TreeSet的父類或者接口,為什麼也能多态呢?
A: 是有接口繼承關系的。可以在idea中右擊類選擇diagrams-show diagrams.檢視類繼承結構。
Set對于其填充的值隻能add一種類型麼?那為什麼ArrayList可以是Objcet類型的?set不行?
public class TreeSet<E> extends AbstractSet<E>{
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
}
不是這麼了解的,表示的是可以指定為一直類型的泛型。
Map.Entry<Integer, Integer> ?map.entrySet();傳回的這個entry,entrySet是個什麼類型的資料結構?本質是其實就是個Map麼?
Map.Entry<K,V> 這個結構其實就是一個節點的意思,在java 8 中直接就用節點Node名字去繼承實作Entry。
static class Node<K,V> implements Map.Entry<K,V> {}
數組一旦生成,其容量或者說長度是不能改變的,這是數組使用的局限性。
Collection儲存單一的元素,而Map儲存相關聯的鍵值對。
如果需要進行大量的随機通路,則使用ArrayList,如果經常從表中間插入或删除元素,則使用LinkedList。
Queue 或Stack(棧,不是java.util裡面的那個),由LinkedList 提供支援。
新程式不應該使用過時的Vector、Hashtable、Stack。
總的其實隻有四種容器:Map、List、Set、Queue
十二、通過異常處理錯誤
1、如何編寫正确的異常處理程式
2、如何自定義異常。
使用異常的好處(對程式設計語言來說加入異常的好處):
- 降低處理錯誤代碼的複雜度,否則處理錯誤代碼繁多且臃腫,影響正常核心邏輯的了解和處理。
- 隻需要在一個地方處理錯誤(異常處理程式,catch塊)。
- 将正常代碼和問題處理代碼相分離。
異常最重要的方面之一就是如果發生問題,它将不允許程式沿着其正常的路徑繼續走下去。C或 C++不行,尤其是C沒有任何辦法強制程式在出現問題時停止在某條路徑上運作下去。
我們抛出異常時總是用new在堆上建立異常對象。異常一般會有預設構造器或者接受字元串作為參數的構造器,以便把相關資訊放入異常對象的構造器;或者兩種都有。
隻有比對的catch子句才能得到執行,執行後變跳出異常處理程式塊,有finally則執行,否則結束。
異常處理的兩種模型:
- 終止模型。将異常抛出不處理。
- 恢複模型。修正錯誤,重新嘗試調用出問題的方法。
建立自定義異常
要自定義異常類,必須從已有的異常類繼承.對異常來說,最重要的部分就是類名,異常的名稱應該望文生義。如果需要自定義異常描述資訊則添加有參構造器即可。
異常說明
即在方法後加上聲明可能會抛出的異常,告知此方法的調用者。
public void exceptionTest() throws TestExecption {}
如果方法沒有throws 異常說明,則說明此方法不會抛出任何異常(也有可能被try catch了)。
PS: 任何繼承了RuntimeException的異常(包含它自己,這些稱為不受檢查異常,屬于錯誤,将被自動捕獲),是可以在沒有異常說明的情況下被抛出的,因為這時候是運作時異常。
在定義設計抽象基類和接口時可以預留聲明方法将抛出異常,友善子類和接口實作類可以抛出預先聲明的異常。
我們可以在catch處理時通過捕獲Exception基類來捕獲是以類型的異常,Throwable 也可以。是以如果要分批catch處理,那麼最好将Exception放在末尾,防止它搶先比對捕獲異常。
//會重新裝填異常資訊,如果調用fillInStackTrace,那麼會把調用的那一行變成異常的新發生地。
//fillInStackTrace()方法在thow 異常時的Throwable構造器裡面調用封裝異常資訊
fillInStackTrace();
異常鍊
異常鍊:在捕獲一個異常後抛出另一個異常,并且希望把原始異常的資訊儲存下來。
兩種方式實作吧其他類型的異常連結起來:
- 繼承Error、Exception、RuntimeException(主要是後面兩個),實作帶cause參數的構造器。
- 調用initCause()方法連接配接異常。
@Test
public void exceptionTest() throws Exception {
//throw new TestExecption();
try {
//throw new TestExecption("i am god");
//throw new TestExecption(new NullPointerException());
throw (Exception) new TestExecption().initCause(new IndexOutOfBoundsException());
} catch (Exception testExecption) {
//預設輸出标準錯誤流
testExecption.printStackTrace();
//可以指定輸出流——标準流
testExecption.printStackTrace(System.out);
}
}
Error用來表示編譯時和系統錯誤,一般程式員不關心;Exception是可以被抛出的基本類型,是程式員應該關心的。
隻能在代碼中忽略RuntimeException及其子類的異常(因為RuntimeException是種錯誤,無法執行下去了),其他類型異常的處理都是由編譯器強制實施的。
如果一個方法中有多個return,包括finally塊也有,則最後傳回的是finally塊裡的return.
使用try+finally則就是異常也正常執行,這種情況下可以丢失異常。
通過強制子類遵守父類方法的異常說明對象的可替換性就得到了保證。子類不能抛出大于父類異常聲明的類型,最大的是Throwable。
父類抛出異常,這時子類方法可以不抛出任何異常。因為不影響已有的程式。
不要在構造器中打開資源什麼的,否則構造器就算能執行finally或者catch關閉也不好。
通用的清理資源規則是:在建立需要關閉資源,清理對象之後,立即進入一個try-finally語句塊。
異常比對
抛出異常的時候,異常處理程式會按照代碼書寫順序找出“最近”的處理程式。比對處理之後将不再繼續查找。catch會捕獲其異常類及所有從它派生的異常。
當我們還不知道怎麼handle 異常的時候把異常catch了,導緻後面異常被吃了,這種是有問題的。
所有模型都是錯誤的,但有些是能用的。
異常處理的可選方式(重要)
被檢查異常的優缺點:
優點是一次說明能增加開發人員的效率,并提高代碼的品質,對小項目,小程式友好。
缺點是對于大項目來說,過多的異常類型聲明及檢查導緻項目無法管理,開發效率下降,也不能很好的提高代碼品質。
總的來說,Java的”被檢測異常“帶來的麻煩比好處要多。
原因是被檢查異常強迫程式員在不知道該采取什麼措施的時候提供異常處理程式,這是不現實的。(亞信的代碼就是這樣,方法裡一堆的異常聲明;優車就好很多,控制得很好。)
異常機制及強靜态類型檢查必要的原因是:
- 不在于編譯器是否會強制程式員去處理錯誤,而是要有一緻的、使用異常來報告錯誤的模型。
- 不在于什麼時候進行檢查,而是一定要有類型檢查。必須強制程式使用正确的類型,置于這種強制是在編譯器還是運作時并不重要。
減少編譯時施加的限制能顯著提高程式員的程式設計效率。反射和泛型就是用來補償靜态類型檢查所帶來的過多限制。
好的程式設計語言能幫助程式員寫出好程式,但無論哪種語言都避免不了程式員用它寫出壞程式。
對于被檢查異常的處理方式:
- 把異常傳遞給控制台。就不需要寫try-catch處理了。
- 把”被檢查異常“ 轉換為 ”不被檢查異常“。方法有2:
- 即把”被檢查異常“包裝進RuntimeException,這樣方法也不用異常聲明(因為RuntimeException是不被檢查異常,不需要聲明或者處理)。
- 建立自己的RuntimeException子類,這樣抛出的異常也是不受檢查的,也不需要異常聲明或者try-catch處理。
@Test
public void exceptionTest() {//不需要聲明異常 throws Execption
try {
throw new TestExecption();
} catch (TestExecption e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
異常使用指南:
- 在恰當的基本處理問題。(即在知道該如何處理異常的情況下才捕獲異常。)
- 解決問題并且重新調用産生異常的方法。(很少這樣處理)
- 進行少許修補,然後繞過異常發生的地方繼續執行。(一般是異常不影響流程或者可以忍受,直接忽略異常往下執行。)
- 用别的資料進行計算,以代替方法預計會傳回的值。(也很少用到)
- 把目前運作環境下能做的事情進來做完,然後把相同的異常重新抛到更高層。(跟第三點差不多,這個也有用到)
- 把目前運作環境下能做的事情進來做完,然後把不同的異常重新抛到更高層。
- 終止程式。
- 進行簡化。(如果異常模式使得問題變得太複雜,則相對惱人)
- 讓類庫和程式更安全。
什麼叫析構函數?好像是指垃圾回收?
析構函數(destructor) 與構造函數相反,當對象結束其生命周期,如對象所在的函數已調用完畢時,系統自動執行析構函數。析構函數往往用來做“清理善後” 的工作。
為什麼要用finally來關閉資源,關閉檔案呢?不關閉會怎麼樣?
不關閉就造成檔案描述符無法釋放,屬于一種系統檔案的浪費
不關閉可能造成對檔案的寫入丢失,寫入有可能存在緩存區,沒有關閉并且沒有主動flush到具體的檔案上,則可能造成丢失。
如果該檔案被檔案鎖獨占,那麼就會造成其他線程無法操作該檔案。
Too many open files錯誤,作業系統針對一個程序配置設定的檔案描述符表是有限大小的,是以打開不釋放可能造成該表溢出。
try+finally 的使用場景?是對異常忽略的時候用?
- 對異常的捕獲處理
- 加鎖解鎖操作
- 對檔案或流的打開和關閉
構造器也可以抛異常?為什麼要抛異常?相當于一個方法是麼?
可以抛異常,構造器本質上也是一個方法,是用于對對象的構造的初始化。
java異常處理機制将正常的邏輯與異常部分分開,讓我們可以更專注地分别處理這兩個問題。
異常處理的報告功能是異常的精髓所在,即錯誤日志列印或沉澱等。
一緻的錯誤報告系統意味着我們再也不必對所寫的每一段代碼都質問自己是否有錯誤被遺漏。
十三、字元串
String對象是不可變的。String對象方法等之間的傳遞實際上是引用的一個拷貝。而該引用所指向的對象一直待在單一的實體位置上,從未動過。
String對象具有隻讀特性,是以指向它的任何引用都不可能改變它的值,也不會對其他的引用有什麼影響。
在使用字元串連接配接時如果拿不準用哪種方式,可以用javap來分析程式代碼。檢視java代碼是如何工作的可以用javap反編譯代碼:
javap -c Demo //(類名) -c表示将生成JVM位元組碼
重載“+” 與StringBuilder
通過javap反編譯可看到,在使用重載“+”時,編譯器或自動幫我們建立一個StringBuilder對象來構造連接配接出最終的String對象。但是要注意的時,在循環體裡如果還是用重載“+”來連接配接String對象的話,編譯器自動建立的StringBuilder是在循環體内産生的,這意味着每一次循環都會建立一個新的StringBuilder對象。是以,在連接配接循環體内的String對象時,要自行建立StringBuilder對象在循環體外append()拼接,并且在使用append()方法連接配接時禁用append(a+ ":" +c)這種投機取巧的方式,否則編譯器又會自動建立一個StringBuilder對象來連接配接内部字元串操作;如果是簡單的字元串連接配接(特别是不是循環體的連接配接的話),可以信任編譯器來處理。
StringBuilder是線程不安全的,但也是以效率比StringBuffer快一點。
@Test
public void stringTest() {
StringBuffer sb = new StringBuffer();
sb.append("a").append("+").append("b").append("=").append("c").append(" ok");
//delete(int start, int end) 可以用于删除不想要的字元串連接配接。
sb.delete(sb.length()-2,sb.length());
System.out.println(sb);
}
無意識遞歸
public class InfiniteRecursion {
@Override
public String toString() {
//String"+"會自動類型轉換為Sting,會想把InfiniteRecursion轉化成String,這時候調用toString方法,又會到了原點,造成無意識遞歸,是以禁用this
//return "InfiniteRecursion{}"+this;
//正确的做法是用super.toString()
return "InfiniteRecursion{}"+super.toString();
}
public static void main(String[] args) {
List<InfiniteRecursion> list = new ArrayList<>();
for (int i =0;i<5;i++){
list.add(new InfiniteRecursion());
}
System.out.println(list);
}
}
String的基本方法:P321
@Test
public void stringTest1() {
String 啊 = "我是一個大頭兵";
char c = 啊.charAt(6);
System.out.println(c);
String a = "i am iron man";
char c1 = a.charAt(6);
System.out.println(c1);
//char是字元形式
char[] b = new char[50];
a.getChars(0,a.length(),b,0);
System.out.println(b);
//byte 是位元組形式
byte[] bytes = a.getBytes();
System.out.println(Arrays.toString(bytes));
//生成字元串的是以字元數組
char[] chars = a.toCharArray();
System.out.println(chars);
boolean we = a.contentEquals("i am iron man");
boolean b1 = a.contentEquals(new StringBuffer("i am iron man"));
System.out.println(we +" "+ b1);
//比較string是否相等
boolean regionMatches = a.regionMatches(3, "am iron man", 0, "am iron man".length());
System.out.println(regionMatches);
System.out.format("test%d",5);
}
Java intern() 方法
Formatter 是個解釋器
正規表達式
一般來說,正規表達式就是以某種方式來描述字元串。
java語言對反斜線\的處理與其它語言不同。
如: \d = java : \ \d .
\ \ \ \ :java用此表示一條普通的反斜線。
斜線是:/ : 左正右反 :\
正規表達式中有特殊意義的字元都要通過\ \ 雙反斜線來轉義。如+ :\ \ +
如果正規表達式不是隻使用一次的話,非String對象的正規表達式具備更加的性能。如用于比對手機号格式等的工具類。
比對規則:
CharSequence
接口CharSequence是從CharBuffer、String、StringBuffer、StringBuilder類之中抽象出了字元序列的一般化定義。是以這些實作了CharSequence的類都可以用于接收CharSequence參數的方法的使用;多數正規表達式操作也都接收CharSequence類型的參數。
Pattern和Matcher
使用Pattern pattern = Pattern.compile("[1].*。$");編譯正規表達式。
組(Groups)
組是用括号劃分的正規表達式,可以根據組的編号來引用某個組。組号為0表示整個表達式,組号為1表示被第一對括号括起來的組,以此類推。
//三個組 0:ABCD 1: BC 2:C
A(B(C))D
組的概念用得少,先不仔細了解。
例子:
@Test
public void matchTest() {
//通過String的方法如matches是應用正規表達式最簡單的途徑
boolean b = "-1234".matches("-?\\d+");
System.out.println(b);
//330
//在使用正規表達式時,利用好其預編譯功能,可以有效加快正則比對速度。 定義為私有靜态類常量
// private static Pattern NUMBER_PATTERN = Pattern.compile("[0-9]+");
// 說明:不要在方法體内定義:Pattern pattern = Pattern.compile(規則)
//首字母大寫,句号結束。注意.*都是特殊有特殊含義的,不需要轉義,轉義了就變成普通字元了
Pattern pattern = Pattern.compile("^[A-Z].*。$");
boolean matches = pattern.matcher("W水電費個勝多負。").matches();
System.out.println(matches);
pattern = Pattern.compile("W");
//如果是用于replace、split方法則不要加(^$)
String s = pattern.matcher("SSSSW水電費個勝多負。").replaceAll("WWW");
System.out.println(s);
String[] split = pattern.split("SSSSW水電W費個wW勝多負W。");
System.out.println(Arrays.toString(split));
//限制分割字元串數量最多3個
String[] split1 = pattern.split("SSSSW水電W費個wW勝多負W。", 3);
System.out.println(Arrays.toString(split1));
pattern = Pattern.compile(",");
String target = "i want to say , you either die hero, or live long enough to see youself become a villain,";
StringBuffer sb = new StringBuffer();
Matcher matcher = pattern.matcher(target);
while (matcher.find()) {
//) 将目前比對子串替換為指定字元串,并且将替換後的子串以及其之前到上次比對子串之後的字元串段添加到一個StringBuffer對象裡
//appendReplacement可以對比對到的資料做不同類型的處理,通過方法提供不同的replacement即可。
matcher.appendReplacement(sb, returnRandom());
System.out.println(sb);
}
//将最後一次比對工作後剩餘的字元串添加到一個StringBuffer對象裡。
System.out.println(matcher.appendTail(sb));
//将matcher對象重新設定到目前字元序列的起始位置
matcher.reset();
System.out.println( matcher.replaceAll(""));
//應用新的字元序列
matcher.reset("happy ending , ok?");
System.out.println( matcher.replaceAll(""));
}
private static Random random = new Random(47);
private String returnRandom() {
int i = random.nextInt(20);
return String.valueOf(i);
}
正規表達式可用于如日志搜尋,如linux系統中的grep 指令。
如下,通過對每一行reset比對對象,然後如果find為true,則通過group列印出值和start列印出位置。
Scanner
Scanner類大大減輕了掃描輸入的工作負擔。Scanner構造器可以接受任何類型的輸入對象,包括File、String、InputStream、Readable等對象。所有的輸入、分詞以及翻譯操作都隐藏在不同類型的next方法中。
/**
* 防火牆日志檔案掃描
*/
private static String log =
"10.25.253.123@31/12/2018\n"+
"11.25.253.123@31/11/2018\n"+
"12.25.253.123@31/10/2018\n"+
"13.25.253.123@31/09/2018\n";
@Test
public void fireScannerTest(){
Scanner scanner = new Scanner(log);
String pattern = "(\\d+[.]\\d+[.]\\d+[.]\\d+)@(\\d{2}/\\d{2}/\\d{4})";
while (scanner.hasNext(pattern)){
//scanner.next(pattern)如果不需要可以不傳回值,但一定要調用,如果不調用scanner.next(pattern),那麼比對指針位置就會一直不動保持在第一位,在while循環模式下一直循序下去不結束
String next = scanner.next(pattern);
//傳回比對值
System.out.println(next);
MatchResult match = scanner.match();
//match.group(0)指整個比對值,1是第一個括号的值;2是第二個括号的
System.out.println("group0="+match.group(0));
System.out.println("ip="+match.group(1)+"===date="+match.group(2));
}
}
javap反編譯出來的程式代碼是彙編語言麼?怎麼看?
不是,是java自己的定義的八種原子操作,這個要參考操作文檔檢視具體的意思就懂了。
String.contentEquals(CharSequence cs)方法中的CharSequence 是什麼類型?
A:接口CharSequence是從CharBuffer、String、StringBuffer、StringBuilder類之中抽象出了字元序列的一般化定義。是以這些實作了CharSequence的類都可以用于接收CharSequence參數的方法的使用;多數正規表達式操作也都接收CharSequence類型的參數。
收集正規表達式中的一些生活常用語句,如郵箱、手機号等比對規則。
定界符?
A:就是 設定界限的 符号。 比如字元 a,就需要用單引号做定界符 'a'; 比如字元串 abc,就需要用雙引号做定界符 "abc"。 就是 告訴計算機: 字元開始了a字元結束了。 字元串開始了abc字元串結束了。
在正規表達式中定界符就是(^.***$)
總的來說一般字元串的操作就是一些普通常用的操作,隻要熟練就行,如果需要字元串拼接的話看情況選用拼接方法。正規表達式的比對一般是用在像手機号,電子郵箱這種的驗證上,比較簡單;而如果是用在日志搜尋比對解析的話就比較複雜,針對解析日志等檔案需要Scanner來處理更友善靈活。
十四、類型資訊
RTTI——Run-Time Type Identification:在運作時識别一個對象的類型。
多态:同一個行為具有多個不同表現形式或形态的能力。
運作時類型資訊使得你可以在程式運作時發現和使用類型資訊。将我們從智能在編譯期執行面向類型的操作禁锢中解脫出來。
運作時識别對象和類的資訊方式有二:
- RTTI.RTTI假設我們在編譯時已經知道了是以的類型。
- 反射機制。反射允許我們在運作時發現和使用類的資訊。
面向對象程式設計的基本目的:讓代碼隻操作對基類的引用。這樣如果要添加新類擴充程式變不會影響原代碼(多态)。我們希望大部分代碼都用多态調用,盡可能少地了解對象的具體類型,使得代碼更容易讀、寫和維護;設計更容易實作、了解和改變。(多态時,雖然我們在寫代碼時隻讓這個類或對象表現出其父類的類型,但是在運作時這個對象一直是他該有的具體類型,隻不過是他不停的換着面具表示他是這一類,那一類,甚至是Object(God))
我們對抽象的基類用抽象類或者接口表示可以防止對執行個體化無意義的基類執行個體化。
Class對象
要了解RTTI在java中的工作原理,首先必須知道類型資訊在運作時是如何表示的。這項工作由特殊對象——Class對象來完成,它包含了與類有關的資訊。
Class對象就是用來建立類的是以的“正常”(程式要運作使用的)對象的。java使用Class對象來執行其RTTI,像轉型這樣的操作也是。每個類就是一個Class對象,經過虛拟機編譯後就是個 類名.class檔案,通過類加載器加載。
是以類都是在對其第一次使用時才動态加載到JVM中的。第一次使用指程式建立第一個對類的靜态成員的引用時(構造器也是類的靜态方法,是以在使用new建立類的新對象時也是對類的靜态成員的引用),就會加載這個類。
如果尚未加載,預設的類加載器就會根據類名查找.class檔案(也可能會在資料庫中查找位元組碼)。
使用newInstance()建立的類,必須帶有預設的構造器。
//不帶預設構造器會報執行個體化異常
java.lang.InstantiationException: com.fcar.thinkjava.duotai.Sandwich
at java.lang.Class.newInstance(Class.java:364)
at com.fcar.guava.GuavaTest.classTest(GuavaTest.java:385)
生成Class對象的引用方式:
Class<?> name = Class.forName("com.fcar.thinkjava.duotai.Sandwich");
類初始化步驟:
- 加載。通過類加載器查找位元組碼建立Class對象。
- 連結。驗證位元組碼,為靜态域配置設定存儲空間,如有需要将類的符合引用替換為直接引用。
- 初始化。如果有父類,先對父類初始化,執行靜态初始化器(包括構造器)和靜态初始化塊(static 修飾的代碼塊或域)
Java初始化的原則是盡可能的“惰性”。
僅使用.class文法來獲得對類的引用不會引發初始化。
編譯器常量的讀取不需要對類進行初始化。
如果是一個被static final 修飾的值(編譯器常量),那麼用類調用讀取值不需要對類進行初始化(因為在解析階段就會把常量的值直接指派給常量引用)(該類的static代碼塊什麼的不會執行,因為沒有初始化)。但是如果被static final 修飾的值 不是編譯器常量(如值是通過random調用獲得的,那麼讀取該值還是會執行初始化)。如果隻是static修飾(沒有final),則肯定要執行初始化。
泛化的Class引用
Class引用總是指向某個Class對象,Class引用表示的就是它所指向的對象的确切類型,而該對象便是Class類的一個對象。
通過使用泛型文法,可以讓編譯器強制執行額外的類型檢查。(否則普通的類引用是可以被重新指派的)
通配符 “?” 表示“任何事物”。
//表示擷取的是Cycle的父類的類引用
Class<? Super Cycle> = Cycle.getSuperClass();
//表示擷取的是Cycle的子類的類引用
Class<? extend Cycle> = someClass;
//轉型方式有2(也叫向下轉型):
Class<Cycle> cycleT = Cycle.class;
//1
Cycle c = cycleT.cast(a);
//2 直接強轉,一般都這樣
(Cycle)a;
RTTI形式包括:
- 傳統的類型轉換。如(Shape)cycle。
- 代表對象的類型的Class對象。通過查詢Class對象可以擷取運作時所需的資訊。
- 關鍵字 instanceof 。用于判斷是否特定類型的執行個體。
instanceof 隻可與命名類型進行比較,而不能與Class對象作比較。注意:如果程式中編寫了許多的instanceof 表達式,就說明設計存在瑕疵。
Class<?> name = Class.forName("com.fcar.thinkjava.duotai.Sandwich");
Object o1 = name.newInstance();
//name.isInstance等同于instanceof
System.out.println("isInstance="+name.isInstance(o1));
構造器就算一個工廠方法模式。
P364 工廠方法執行個體待實踐。
instanceof 與Class的等價性
instanceof 與isInstance()保持了類型的概念,指的是你是這個類或者這個類的子類麼?
而用==或equal()比較時不考慮繼承,比較的是确切的類型是否相同。
反射:運作時類資訊
Class類與java.lang.reflect(Field,Method,Constructor)類庫一起對反射的概念進行了支援。
通過反射,我們可以使用Constructor建立新的對象,用get()和set()方法(調用Method的方法)讀取和修改與Field對象關聯的字段,用invoke()方法調用與Method對象關聯的方法。還可以調用Class.getMethods(),getFields() ,getConstructors()傳回表示方法、字段以及構造器的對象的數組。
RTTI與反射的真正差別隻在于:對于RTTI,編譯器在編譯時打開和檢查.class檔案;而對于反射機制,編譯器在運作時打開和檢查.class檔案(因為編譯時不可擷取);
反射在java中是用來支援其他特性的,例如對象序列化和JavaBean。
運作時才擷取類資訊的動機:
- 需要擷取一個指向某個并不在你的程式空間中的對象的引用(如磁盤檔案、網絡連接配接中)。
- 希望提供在跨網絡的遠端平台上建立和運作對象的能力(遠端方法調用RMI)。
反射測試類:
@Test
public void reflectTest() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class<?> clazz = Class.forName("com.fcar.thinkjava.type.ReflectTest");
System.out.println("===========================擷取所有公用的構造方法==============================");
Constructor<?>[] constructors = clazz.getConstructors();
for (int i = 0; i < constructors.length; i++) {
System.out.println( constructors[i]);
}
System.out.println("===========================擷取所有的構造方法==============================");
Constructor<?>[] declaredConstructors = clazz.getDeclaredConstructors();
for (int i = 0; i < declaredConstructors.length; i++) {
System.out.println( declaredConstructors[i]);
}
System.out.println("=============================擷取公有 & 有參的構造方法============================");
Constructor<?> constructor = clazz.getConstructor(String.class);
System.out.println(constructor);
System.out.println("=============================擷取私有 & 有參 構造方法============================");
Constructor<?> declaredConstructor = clazz.getDeclaredConstructor(Integer.class);
System.out.println(declaredConstructor);
System.out.println("=============================擷取公有 & 無參的構造方法============================");
Constructor<?> constructor1 = clazz.getConstructor(null);
System.out.println(constructor1);
System.out.println("=============================建構有參構造器對象============================");
Object o = constructor.newInstance("constructorTest");
System.out.println(o);
}
@Test
public void reflectFieldTest() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException {
Class<?> clazz = Class.forName("com.fcar.thinkjava.type.ReflectTest");
System.out.println("===========================擷取所有公用的域==============================");
Field[] fields = clazz.getFields();
for (int i = 0; i < fields.length; i++) {
System.out.println( fields[i]);
}
System.out.println("===========================擷取所有的域==============================");
Field[] declaredFields = clazz.getDeclaredFields();
for (int i = 0; i < declaredFields.length; i++) {
System.out.println( declaredFields[i]);
}
System.out.println("=====擷取公有字段并使用=====");
Object instance = clazz.getConstructor().newInstance();
Field publicField = clazz.getField("publicField");
publicField.set(instance,"publicTest");
//有個疑問?如果我們代碼裡沒有這個類,那麼又如何強轉調用方法和域呢?
ReflectTest reflectTest = (ReflectTest)instance;
//擷取字段名稱
//直接get
Object publicField1 = publicField.get(instance);
System.out.println("publicField.get+++"+publicField1);
//通過多态擷取
System.out.println( reflectTest.getPublicField());
GetSet getSet1 = (GetSet)instance;
System.out.println("=====通過多态+反射擷取域值=====");
System.out.println(getSet1.getField(clazz,publicField,instance));
System.out.println("=====擷取私有字段并使用=====");
Field privateField = clazz.getDeclaredField("privateField");
//設定私有域要暴利反射
//在擷取私有屬性的時候如果沒有進行暴力反射,那麼會抛出異常。
//java.lang.IllegalAccessException: Class com.fcar.guava.GuavaTest can not access a member of class com.fcar.thinkjava.type.ReflectTest with modifiers "private"
privateField.setAccessible(true);
System.out.println("====before====="+reflectTest.getPrivateField());
privateField.set(instance,"privateTest");
System.out.println( reflectTest.getPrivateField());
}
@Test
public void reflectMethodTest() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException, InterruptedException {
Class<?> clazz = Class.forName("com.fcar.thinkjava.type.ReflectTest");
System.out.println("===========================擷取所有公用的方法==============================");
Method[] methods = clazz.getMethods();
for (int i = 0; i < methods.length; i++) {
System.out.println( methods[i]);
}
//否則會走下去
Thread.sleep(2000);
System.out.println("===========================擷取所有的方法==============================");
Method[] declaredMethods = clazz.getDeclaredMethods();
for (int i = 0; i < declaredMethods.length; i++) {
System.out.println( declaredMethods[i]);
}
Thread.sleep(5000);
System.err.println("======擷取特定方法(帶參)并使用=====");
Object instance = clazz.getConstructor().newInstance();
Method commonActionWithParam = clazz.getMethod("commonActionWithParam", String.class);
System.out.println(commonActionWithParam);
commonActionWithParam.invoke(instance,"METHODPARAM");
Thread.sleep(2000);
System.err.println("======擷取特定方法(不帶參)并使用=====");
Method commonAction = clazz.getMethod("commonAction");
System.out.println(commonAction);
commonAction.invoke(instance);
Thread.sleep(2000);
System.err.println("======擷取私有方法(不帶參)并使用=====");
//Method privateAction = clazz.getMethod("privateAction");
Method privateAction = clazz.getDeclaredMethod("privateAction");
//調用私有方法一定要設定權限
privateAction.setAccessible(true);
System.out.println(privateAction);
privateAction.invoke(instance);
}
@Test
public void reflectStaticTest() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException, InterruptedException {
Class<?> clazz = Class.forName("com.fcar.thinkjava.type.ReflectTest");
System.out.println("===========================反射執行STATIC方法==============================");
Method staticAction = clazz.getMethod("staticAction");
staticAction.invoke(null);
}
@Test
public void reflectMainTest() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException, InterruptedException {
Class<?> clazz = Class.forName("com.fcar.guava.GuavaTest");
System.out.println("===========================反射執行main==============================");
Method main = clazz.getMethod("main", String[].class);
//Object o = clazz.getConstructor().newInstance();
//main 是static方法不需要建立對象
main.invoke(null,(Object) new String[]{"A"});
}
public static void main(String[] args) {
System.out.println("I AM MAIN");
}
動态代理
參考文章1隻說明jdk的
參考文章2包含cglib
參考文章3包含cglib
使用場景:
你希望跟蹤目标對象中的方法的調用,或者希望度量這些調用的開銷,這些代碼并不希望将它們合并到應用的代碼中,是以代理使得你可以很容易的添加或移除它們。
在Spring項目中用的注解,例如依賴注入的@Bean、@Autowired,事務注解@Transactional等都有用到,換言之就是Spring的AOP(面向切面程式設計時)。
舉個例子:為什麼@Transactional 注解隻有public權限的才能生效呢?
A:因為@Transactional注解使用時是通體Spring的AOP(面向切面程式設計)實作的,這時候就用到了動态代理,而動态代理時因為生成的動态代理類實際上是個實作了被代理類的接口的類,而接口中的方法預設都是public的,是以是不可能有其他通路權限的方法生效的,因為動态代理代理不到這個方法。
動态代理的好處是比較靈活,可以在運作的時候才切入改變類的方法,而不需要預先定義它。
JDK動态代理:
@Test
public void dynamicProxyTest() {
TeaMan teaMan = new WuYiShanTeaMan();
InvocationHandler guangZhou13Hang = new GuangZhou13Hang(teaMan);
TeaMan guangZhou13HangProxy =(TeaMan) Proxy.newProxyInstance(teaMan.getClass().getClassLoader(), teaMan.getClass().getInterfaces(), guangZhou13Hang);
guangZhou13HangProxy.sellTea(5000);
guangZhou13HangProxy.buyOpium(3000);
}
public class WuYiShanTeaMan implements TeaMan {
@Override
public void sellTea(Integer hearvy) {
System.out.println("WuYiShanTeaMan sellTea "+hearvy+" KG");
}
@Override
public void buyOpium(Integer hearvy) {
System.out.println("WuYiShanTeaMan buyOpium "+hearvy+" KG, what a shame");
}
}
//接口
public interface TeaMan {
public void sellTea(Integer hearvy);
public void buyOpium(Integer hearvy);
}
//實作InvocationHandler
public class GuangZhou13Hang implements InvocationHandler {
private TeaMan teaMan;
public GuangZhou13Hang(TeaMan teaMan) {
this.teaMan = teaMan;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("===================before====================");
Object invoke;
if (method.getName().equals("buyOpium")) {
System.out.println("官府收賬款放行鴉片");
invoke = method.invoke(teaMan, args);
System.out.println("官老爺逛窯子");
} else {
invoke = method.invoke(teaMan, args);
}
System.out.println("===================after====================");
return invoke;
}
}
cglib動态代理:JDK的動态代理一定要繼承一個接口,如果要基于POJO類的動态代理 ,那麼可以用cglib。
@Test
public void cglibDynamicProxyTest() {
System.setProperty( DebuggingClassWriter.DEBUG_LOCATION_PROPERTY,"F:\\mywork\\fc-starter");
AmericanBusinessMan businessMan = new AmericanBusinessMan();
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(EastIndiaCompany.class);
enhancer.setCallback(businessMan);
EastIndiaCompany o = (EastIndiaCompany)enhancer.create();
o.buyTea(10000);
o.plantOpium(30000);
}
//代理要實作MethodInterceptor
public class AmericanBusinessMan implements MethodInterceptor {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("===================before Opium====================");
Object invoke;
if (method.getName().equals("plantOpium")) {
System.out.println("India MI FANS IN DANGER");
invoke = methodProxy.invokeSuper(o,objects);
System.out.println("india MI FANS GOODBYE");
} else {
invoke = methodProxy.invokeSuper(o, objects);
}
System.out.println("===================after Opium====================");
return invoke;
}
}
//被代理類
public class EastIndiaCompany {
public void buyTea(Integer hearvy) {
System.out.println("EastIndiaCompany buyTea "+hearvy+" KG");
}
public void plantOpium(Integer hearvy) {
System.out.println("EastIndiaCompany plantOpium "+hearvy+" AREA");
}
}
空對象
引入空對象後,我們可以假設所有的對象都是有效的,而不必浪費程式設計精力去檢查null。
以下的報錯實際上是因為我們用Class.forName()後并沒有建立對象,這時候報錯是因為對象還沒有建立,getClass()要用執行個體去調用才能保證該class已經加裝到虛拟機中了。
//成功
GuavaTest test = new GuavaTest();
Class instance1 = test.getClass();
Object o = instance1.newInstance();
//報錯
Class<?> name2 = Class.forName("com.fcar.thinkjava.duotai.Sandwich");
Class instance2 = name.getClass();
Object o2 = instance2.newInstance();
java.lang.IllegalAccessException: Can not call newInstance() on the Class for java.lang.Class
at java.lang.Class.newInstance(Class.java:344)
at com.fcar.guava.GuavaTest.classTest(GuavaTest.java:390)
為什麼1.7版本通過 Class.forName擷取類的引用不會立即進行初始化了呢(程式設計思想裡的會。)?
Class<?> initable3 = Class.forName("com.fcar.thinkjava.type.Initable");
在什麼情況下我們喜歡用反射?(畢竟自己拼接方法來調用乏味費時)
動态擷取對象或者建立對象時。
有個疑問?如果我們代碼裡沒有這個類,那麼又如何強轉調用方法和域呢?
Class<?> clazz = Class.forName("com.fcar.thinkjava.type.ReflectTest");
Object instance = clazz.getConstructor().newInstance();
Field publicField = clazz.getField("publicField");
publicField.set(instance,"publicTest");
//有個疑問?如果我們代碼裡沒有這個類,那麼又如何強轉調用方法和域呢?
ReflectTest reflectTest = (ReflectTest)instance;
//擷取字段名稱
//System.out.println( publicField.getName());
System.out.println( reflectTest.getPublicField());
A:如果我們代碼裡沒有這個類,那麼我們可以約定一個基類接口,定義get/set域方法,讓我們從遠處,比如說資料庫擷取的類實作這個基類,這樣我們就可以在擷取域值時,隻強轉為基類接口類型,然後通過多态加反射來拼接get方法來獲得域的值。
public class ReflectTest implements GetSet{
@Override
public Object getField(Class clazz, Field publicField, Object instance) throws InvocationTargetException, IllegalAccessException {
Method commonAction = null;
try {
commonAction = clazz.getMethod("get"+(publicField.getName().substring(0, 1).toUpperCase() + publicField.getName().substring(1)));
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
return commonAction.invoke(instance);
}
public interface GetSet {
public void setField(Field publicField, Object val);
public Object getField(Class clazz,Field publicField, Object instance) throws InvocationTargetException, IllegalAccessException;
}
//調用
GetSet getSet1 = (GetSet)instance;
System.out.println("=====通過多态+反射擷取域值=====");
System.out.println(getSet1.getField(clazz,publicField,instance));
多态與RTTI的差別和聯系?面向對象程式設計語言的目的是讓我們在凡是可以使用的地方都使用多态機制,隻在必需的時候使用RTTI。這句話怎麼了解?
A:我的了解是多态其實就是通過RTTI來實作的,隻是多态是基于父類類型來調用子類的具體方法,可是多态是不能調用子類的擴充方法的,因為發現不到;但如果通過RTTI判斷出其子類類型,便能強轉為子類類型,也就可以調用子類相對于父類的擴充方法了。
現實代碼中很少使用空對象?最多便是google的通過Optional判空,空對象用處不大?
null容易導緻空指針,這要求被調用方要寫非常多的判空代碼,如果沒有判空容易造成NPE.
所有的類型修飾符修飾的方法和域、構造器都可以被反射通路到。但是final域在遭遇修改時是安全的。它不會發生任何修改。
RTTI允許通過匿名基類的引用來發現類型資訊。
面向對象程式設計語言的目的是讓我們在凡是可以使用的地方都使用多态機制,隻在必需的時候使用RTTI。
如果使用多态時基類未包含我們想要的方法,那麼我們可以使用RTTI,繼承一個新類,添加自己需要的方法。
反射這一塊與動态代理在項目架構中用得比較多,需要熟悉使用。
十五、泛型
了解了邊界所在,你才能成為程式高手。因為隻有知道了某個技術不能做到什麼,你才能更好地做到所能做的。
Java泛型的優點與局限:
一般的類和方法,隻能使用具體的類型:要麼是基本類型,要麼是自定義的類(可以是基類或接口)。如果要編寫可以應用于多種類型的代碼,可以使用泛型。
一個類,如果我們在需要說明類型的地方都使用基類或接口,這樣确實能夠具備更好的靈活性,但也會有一些性能損耗;并且即使使用了接口,對程式的限制還是太強,因為這要求寫的代碼必須使用特定的接口。而我們通過使用泛型,可以是代碼能夠應用于“某種不具體的類型(在用該類時傳進來類型)。
泛型實作了參數化類型的概念。使代碼可以應用于多種類型。
使用場景:用于建立容器類。
JAVA泛型的核心概念:告訴編譯器想要使用什麼類型,然後編譯器幫你處理一切細節。
@Test
public void typeTest() {
Holder<String> holder = new Holder<>("I","LOVE","U");
System.out.println(holder);
holder.setFirst("she");
holder.setSecond("is");
holder.setThird("mine");
System.out.println(holder);
Holder<Integer> intHolder = new Holder<>(1,2,3);
System.out.println(intHolder);
}
//泛型類
public class Holder<T> {
private T first;
private T second;
private T third;
public Holder(T first, T second, T third) {
this.first = first;
this.second = second;
this.third = third;
}
public T getFirst() {
return first;
}
public void setFirst(T first) {
this.first = first;
}
public T getSecond() {
return second;
}
public void setSecond(T second) {
this.second = second;
}
public T getThird() {
return third;
}
public void setThird(T third) {
this.third = third;
}
@Override
public String toString() {
return "Holder{" +
"first=" + first +
", second=" + second +
", third=" + third +
'}';
}
}
元組類庫
元組:它是将一組對象直接打包存儲于其中的一個單一對象。這個容器對象允許讀取其中元素,但是不允許向其中存放新的對象。(也稱資料傳送對象、信使。不允許修改是因為屬性值被final修飾)。
利用繼承機制可以實作長度更長的元組。
@Test
public void tupleTest() {
Map<String,String> map= new HashMap<String,String>();
map.put("where","home");
ThreeTuple<String,Integer,Map<String,String>> threeTuple = new ThreeTuple<>("baobao",40,map);
System.out.println(threeTuple.toString());
System.out.println(threeTuple.third);
//threeTuple.first = "xiaobao";
}
//二維元組
public class TwoTuple<A,B> {
public final A first;
public final B second;
public TwoTuple(A first, B second) {
this.first = first;
this.second = second;
}
@Override
public String toString() {
return "TwoTuple{" +
"first=" + first +
", second=" + second +
'}';
}
}
//三維元組繼承自二維
public class ThreeTuple<A,B,C> extends TwoTuple<A,B> {
public final C third;
public ThreeTuple(A first, B second, C third) {
super(first, second);
this.third = third;
}
@Override
public String toString() {
return "ThreeTuple{" +
"third=" + third +
", first=" + first +
", second=" + second +
'}';
}
}
泛型接口
泛型接口就是對接口進行泛型,等實作類實作接口時傳入想要的類型即可。
public interface Generator<T> {
T getNext();
}
public class CoffeeGenerator implements Generator<Coffee> {
}
泛型方法
我們可以在類中包含參數化方法(參數化方法的意思就是使用泛型),而這個方法所在的類可以是泛型類,也可以不是。是否擁有泛型方法,與其所在的類是否泛型沒有關系。
泛型方法使得該方法能夠獨立于類而産生變化。
基本指導規則:無論何時,隻有你能做到,就應該盡量使用泛型方法。如果使用泛型方法可以取代将整個類泛型化,那麼就應該隻使用泛型方法,因為更清楚明白。
使用泛型方法不要指明參數類型,因為編譯器會進行類型參數推斷。(像可以無數次重載的方法)。如果傳入的是基本類型,那麼自動包裝機制會介入自動包裝。
可變參數與泛型方法
泛型方法與可變參數清單能夠很好地并存。
參考 Arrays.asList(); 方法
@Test
public void TypeTest1() {
List<String> a = makeList("A");
System.out.println(a);
//Serializable[3]@750 類型不同則泛型會找出他們通用的接口或父類作為泛型容器類型
System.out.println(makeList(1,2,"3"));
System.out.println(makeList("A","B","C"));
System.out.println(makeList("I LOVE MORTY,AND I HOPE MORTY LOVE ME".split("")));
}
public static<T> List<T> makeList(T ... args){
List<T> list = new ArrayList<>();
for (T t : args) {
list.add(t);
}
return list;
}
泛型還可以應用于内部類以及匿名内部類。
建構複雜模型
泛型的一個重要好處是能夠簡單而安全地建立複雜的模型。
泛型擦除
在泛型代碼内部,無法獲得任何有關泛型參數類型的資訊。
Java泛型是使用擦除來實作的。這意味着在使用泛型時,具體的類型資訊都被擦除了,List 和List 都被擦除成原生類型List。
當我們希望代碼能夠跨多個類工作時,使用泛型才有所幫助。
@Test
public void TypeTest2() {
HasF hasF = new HasF();
Manipulator<HasF> manipulator = new Manipulator<>(hasF);
manipulator.simulator();
}
//普通對象
public class HasF {
public void f(){
System.out.println("HasF f()");
}
}
//泛型類。邊界<T extends HasF> 聲明T 必須具有類型HasF或者是其子類,這時候我們便能使用obj.f();方法了。
//
public class Manipulator<T extends HasF> {
private T obj;
public Manipulator(T obj) {
this.obj = obj;
}
public void simulator(){
obj.f();
}
}
java泛型的缺點在于用擦除實作泛型:
擦除減少了泛型的泛化性,如果不使用擦除來實作,而是使用具體化,使類型參數保持為第一類實體,那麼他将能夠在類型參數上執行基于類型的語言操作和反射操作。
泛型類型隻有在靜态類型檢查期間才出現,在此之後,程式中的所有泛型類型都将被擦除,替換為它們的非泛型上界。如List将被擦除為List(即容器類型的被擦除為List), 普通的類型變量在未指定邊界的情況下被擦除為Object.
擦除的核心動機是為了“遷移的相容性”,使得泛化的用戶端可以用非泛化的類庫來使用。
用擦除來實作java泛型的優缺點:
優點是相容非泛化代碼,使我們可以從容的把非泛化代碼轉變為泛化代碼。
缺點是擦除實作的泛型參數類型資訊丢失,不能用于顯示地引用運作時類型的操作之中,如轉型、instanceof操作和new表達式。
擦除和遷移相容性意味着使用泛型并不是強制的。
P412對于泛型類型的反編譯可以看到,兩種寫法是一樣的。
泛型中的所有動作都發生在邊界處——對傳遞進來的值進行額外的編譯器檢查,并插入對傳遞出去的值的轉型。
擦除的補償
new T()無法實作的原因:一是因為泛型的類型擦除;二是因為編譯器不能驗證T具有預設(無參)構造器。
java解決無法建立泛型執行個體( 無法 new T() )的方案是傳遞一個工廠對象,使用它來建立新的執行個體。最便利的工廠對象就是Class對象,使用類型标簽(構造器使用 Class clazz),就可以使用clazz.newInstance()建立新對象。這種要求建立的對象必須有預設的構造器,否則會運作時異常。要避開這種異常可以選用顯式的工廠,限制其類型,使得隻能接受實作了這個工廠的類,這樣就不會有缺少預設構造器問題。
P414 工廠的選用。
成功建立泛型數組的唯一方式就是建立一個被擦除類型的新數組,然後對其轉型。
//因為我們不能聲明
T[] array = new T[sz];
//隻能強轉
T[] array = (T[])new Object[sz];
因為有了擦除,數組的運作時類型就隻能是Object[].如果我們立即将其轉型為T[],那麼在編譯器該數組的實際類型就将丢失,而編譯器可能會錯過某些潛在的錯誤檢查。正因為這樣,最後是在集合内部使用Object[],然後當你使用數組元素時,添加一個對T的轉型。
邊界
設定邊界的目的是為了可以按照自己的邊界類型來調用方法,而不是隻能調用Objcet的方法。
java使用extends關鍵字來限制泛型的邊界。
執行個體:
public class EpicBattle {
static <POWER extends SuperHearing> void useSuperHearing(SuperHero<POWER> hero){
hero.getPower().hearSubtleNoises();
}
static <power extends SuperHearing & SuperSmell> void useFind(SuperHero<power> hero){
hero.getPower().hearSubtleNoises();
hero.getPower().trackBySmell();
}
public static void main(String[] args) {
DogBoy dogBoy = new DogBoy();
useSuperHearing(dogBoy);
System.out.println("===================");
useFind(dogBoy);
List<? extends SuperHearing> dogBoyes1;
//<? extends SuperHearing & SuperSmell> 之是以不能編譯通過是因為&這個是用于定義泛型時寫的,使用泛型時不能這樣寫。
//List<? extends SuperHearing & SuperSmell> dogBoyes2;
}
}
interface SuperPower{
}
interface XRayVision extends SuperPower{
void seeThroughWalls();
}
interface SuperHearing extends SuperPower{
void hearSubtleNoises();
}
interface SuperSmell extends SuperPower{
void trackBySmell();
}
class SuperHero< POWER extends SuperPower>{
POWER power;
public SuperHero(POWER power) {
this.power = power;
}
POWER getPower(){
return power;
}
}
class SuperSleuth<POWER extends XRayVision> extends SuperHero<POWER>{
public SuperSleuth(POWER power) {
super(power);
}
void see(){
power.seeThroughWalls();
}
}
class CanineHero<power extends SuperHearing & SuperSmell> extends SuperHero<power>{
public CanineHero(power power) {
super(power);
}
void hear(){
power.hearSubtleNoises();
}
void smell(){
power.trackBySmell();
}
}
class SuperHearSmell implements SuperSmell,SuperHearing{
@Override
public void hearSubtleNoises() {
System.out.println("hearSubtleNoises");
}
@Override
public void trackBySmell() {
System.out.println("trackBySmell");
}
}
class DogBoy extends CanineHero<SuperHearSmell>{
public DogBoy() {
//子類可以是預設構造器,隻要調用了父類的構造器即可,表示能構造出父類來
super(new SuperHearSmell());
}
}
通配符
在建立多态數組時,該數組隻能存放具體的子類型的數組的類型,如 Fruit[] f = new Apple[10]; 隻能存放Apple及其子類的類型,否則雖然編譯期可以通過,運作時卻會報錯。
通配符引用的是明确的類型,是以下面這個例子裡(<? extends Fruit>)的通配符意味着該引用沒有指定的具體類型。這種情況下該泛型容器設定不能添加Object類型的值。
使用泛型容器必須指定具體的泛型容器類型。
@Test
public void fruitTest() {
List<? extends Fruit> list = new ArrayList<Apple>();
//add (capture<? extends com.fcar.thinkjava.Types.fruit.Fruit>)
//in List cannot be applied to (com.fcar.thinkjava.Types.fruit.Apple)
//list.add(new Apple());
//add (capture<? extends com.fcar.thinkjava.Types.fruit.Fruit>)
//in List cannot be applied to (com.fcar.thinkjava.Types.fruit.Fruit)
//list.add(new Fruit());
//list.add( new Object());
list.add(null);
}
<? extends Fruit> 隻能确認get獲得的值的有效邊界為Fruit類型,不能确認set設定的值,因為這樣意味着他可以是任何事物,比如Object也可以設定,因為它是最基本的父類,這樣編譯器就無法驗證“任何事物”的類型安全性,是以無法set。
超類型通配符:<? super T> 或 <? super Apple> 通過超類型通配符使我們可以确定邊界是Apple,該容器的存放的元素都是Apple及其子類,因為适配的最大父類類型就是Apple。
無界通配符:<?>
一般用于如聲明這段代碼是用java的泛型來編寫,并不是要用原生類型編寫;還有就是在處理多個泛型參數時,通過無界通配符來運作一個參數是任意類型的,而其他參數為特定類型,這種使用場景特别重要。
如Map<String, ?> map = new HashMap<String, ?>;
使用确切類型來替代通配符類型的好處是,可以用泛型參數來做更多的事,但是使用通配符使得你必須接受範圍更寬的參數化類型作為參數。是以,必須逐個情況地權衡利弊,找到更适合你的需求的方法。
泛型問題
任何基本類型都不能作為類型參數,但可以使用基本類型的包裝類來作為泛型類型參數。
注意:自動包裝機制不能應用于數組。
一個類不能實作同一個泛型接口的兩種變體(如一個類實作了該泛型接口又繼承了實作了這個泛型接口的類),由于擦除的原因,這兩個變體會成為相同的接口。(去掉泛型參數便可以了)
轉型和警告
使用帶有泛型類型參數的轉型或instanceof不會有任何效果。(因為運作時是類型擦除的)。
正确的轉型方式是通過泛型類來轉型:
List<String> list = List.class.cast(in.readObject());
自限定的類型
?
動态類型安全檢查
通過Collections.checkedList()這些工具類方法使得List、Map、Set等這類集合可以安全地存入泛型類型的元素。
@Test
public void fruitTest() {
List<Apple> list1 = new ArrayList<Apple>();
add(list1);//正常運作,隻有把Orange從容器取出時才會報錯。
List<Apple> apples = Collections.checkedList(new ArrayList<Apple>(), Apple.class);
add(apples);//報錯
}
異常
由于檢查型異常的緣故,将不能編寫出泛化的代碼。
混型
混型:值混合多個類的能力,以産生一個可以表示混型中所有類型的類。混型使組裝多個類變得簡單易行。
一般就是多重繼承,在java這邊是實作多個接口,然後在建立域建立出接口對應的實作類,在調用中通過代理調用域中實作類的方法,将方法調用轉發給恰當的對象。
潛在類型機制
java的泛型機制比支援潛在類型機制的語言更“缺乏泛化性”。
java想實作類似潛在類型機制則編譯器會強制要求這些類實作某個接口,以便通過泛型方法< ? extends Perform>來實作這種效果。
對前置類型機制的補償
- 反射。通過反射可以在實作或繼承任何接口的情況下調用其方法。
- 将一個方法應用于序列。(能夠實作在編譯器類型檢查)
- 用擴充卡仿真潛在類型機制。
p390 末端哨兵怎麼實作的,看不明白?
無論何時,隻有你能做到,就應該盡量使用泛型方法。這是對的麼?首先有個效率問題吧,如非需要适應多種不同類型,沒有必要這樣處理吧?
Java泛型是使用擦除來實作的,怎麼了解?是指都程式設計Object類型?
類型标簽?指的是ClassL類型的屬性标簽麼?
工廠對象?指的是用于建立工廠的東西,比如class、字元串?
什麼叫顯式的工廠?
p416,成功建立泛型數組的唯一方式就是建立一個被擦除類型的新數組,然後對其轉型。這句怎麼了解?(資料泛型轉型需重新看)
15.10通配符這塊不知道其具體用處,工作中有用到嗎?
p429例子待研究
15.16 潛在類型機制?Python和C++ 是支援潛在類型機制的語言執行個體。
泛型還需重新了解一遍,不夠全面。
泛型的最大用處便是在使用容器類集合時(如List、Map等)可以設定某種泛型版本的容器,而不會因為存取時向上轉型為Object而需要向下強轉。但這隻是個友善的編譯期檢查且使用時減少了強轉的代碼量,實際上由于泛型是後面添加到java中的,導緻泛型的實作為了相容以前非泛型的代碼而使用類型擦除來實作泛型,導緻java無法在運作時擷取真正的類型資訊,無法實作更加泛化的代碼。
十六、數組
數組的基本認識:數組通過整型索引值通路元素,并且數組的尺寸不能改變。
數組為什麼特殊
數組與其他種類的容器之間的差別有三方面:
- 效率。在java中,數組是效率最高的存儲和随機通路對象引用序列的方式。(數組是個簡單的線性序列)代價是數組對象的大小被固定(比如與ArrayList對比,因為ArrayList可以實作自動配置設定空間,這種彈性開銷時其效率比數組低),且在其生命周期中不可改變。
- 類型。在泛型之前,數組可以建立某種具體類型通過編譯期檢查來防止插入錯誤類型,而其他容器不行。泛型後都可以了。
- 儲存基本類型的能力。在泛型之前,數組可以持有基本類型,而其他容器不行。泛型後都可以了,因為有字段包裝機制。
數組和ArrayList之間的相似性是有意設計的,這使得兩者之間的切換比較容易。随着泛型和自動包裝機制的出現,數組相比于容器的僅有優點便是效率,而一般情況下,容器比數組擁有更多的功能方法,且數組限制更多,是以一般優先使用容器。
數組辨別符隻是一個引用(數組是個對象),指向在堆中建立的一個真實對象,這個數組對象用以儲存指向其他對象的引用。length方法(length是數組的大小,而不是實際儲存的元素個數。)和“[]”文法是通路數組對象唯一的方式。
對象數組與基本類型數組的差別是對象數組儲存的是引用(預設初始化為null),而基本類型數組直接儲存基本類型的值(按各基本類型的值初始化,如布爾型是false,int是0)。
數組中構成矩陣的每個向量都可以具有任意的長度(這被稱為粗糙數組)。
數組與泛型(待重新了解)
數組與泛型不能很好地結合,不能執行個體化具有參數化類型的數組,我們不能建立泛型數組。因為擦除會移除參數類型資訊,而數組必須知道它們所持有的确切類型,以強制保證類型安全。但是我們可以參數化數組本身的類型。
一般而言,泛型在類或方法的邊界處很有效,而在類或方法的内部,擦除通常會使泛型變得不适用。
//作用十分有限,隻能用同一個值填充各個位置,針對對象而言,就是複制一個引用進行填充。
int[] t = new int[5];
Arrays.fill(t,3);
System.out.println(Arrays.toString(t));
Arrays.fill(t,3,5,5);//填充下标3-4的值
System.out.println(Arrays.toString(t));
//複制數組
//注意複制對象數組時,複制的是對象的引用,而不是對象本身的拷貝,是淺複制。
//System.arraycopy(t,0,y,0,y.length);
//參數依次是:源數組,從源數組的什麼位置開始複制的偏移量,目标數組,從目标數組的什麼位置開始複制的偏移量,複制元素個數
System.arraycopy(t,0,y,0,y.length);
System.out.println(Arrays.toString(y));
數組的比較
//用來比較整個數組,比較數組元素個數和對應位置上的元素是否相等。注意比較的是元素的内容,不是位置。
Arrays.equals(y,t);
數組元素的比較
java有兩種方式提供比較功能:
- 實作 java.lang.Comparable接口,重寫compareTo()方法。
- 建立一個實作了Comparator接口的單獨的類。傳入Comparator比較對象。
Arrays.sort(u,Collections.reverseOrder());
針對已排序的數組,可以使用Arrays.binarySearch(t,5);查找元素的位置。如果查不到,傳回負值,表示若要保持數組的排序狀态次目标元素所應該插入的位置。負值的計算方式:-(插入點)-1
插入點指(以從小到大排序為例),第一個大于查找對象元素在數組中的位置,如果數組中所有的元素都小于要查找的對象,也就是說查找的值是最大的,那麼插入點就是a.size().
@Test
public void arrayTest() {
int[] a = {1, 2, 3, 4, 5};
int[] b = a.clone();
int[] c = a;//兩個引用指向同一個數組對象
System.out.println(Arrays.toString(a));
System.out.println(Arrays.toString(b));
System.out.println(a.equals(b));//false
System.out.println(a == b);//false
System.out.println(a.equals(c));//true
System.out.println(a == c);//true
int[][] aa = {{1, 2, 3, 4, 5}, {1, 2, 3, 4, 5}, {1, 2, 3, 4, 5}};
//Arrays.deepToString(aa)用于多元數組展示
System.out.println(Arrays.deepToString(aa));
boolean[][][] flagArr = new boolean[3][3][3];
System.out.println(Arrays.deepToString(flagArr));
System.out.println(Arrays.deepToString(fillArray(2, 3, 4)));
int[] t = new int[5];
Arrays.fill(t, 3);
System.out.println(Arrays.toString(t));
Arrays.fill(t, 3, 5, 5);
System.out.println(Arrays.toString(t));
int[] y = new int[5];
//複制數組
System.arraycopy(t, 0, y, 0, y.length);
System.out.println(Arrays.toString(y));
System.out.println(Arrays.equals(y, t));
Arrays.sort(t);
Integer[] u = new Integer[5];
Arrays.fill(u, 6);
u[2] = 7;
u[4] = 1;
Arrays.sort(u, Collections.reverseOrder());
System.out.println(Arrays.toString(u));
System.out.println(Arrays.binarySearch(t, 5));
//如果使用Comparator排序某個對象數組(基本類型數組無法使用Comparator),在使用binarySearch()時必須提供同樣的Comparator。
System.out.println(Arrays.binarySearch(u, 1, Collections.reverseOrder()));//4
//因為-1小于是以的值,對于颠倒排序來說,-1就是資料裡面最大的,應該是-size-1 = -6
System.out.println(Arrays.binarySearch(u, -1, Collections.reverseOrder()));//-6
}
數組是個簡單的線性序列,什麼叫線性序列?為什麼線性序列的元素通路非常快速?
16.6 忽略了建立測試資料這節。
為什麼未排序數組上執行二分法查找結果是不可預知的?我測試結果是可知不變的,不過是錯誤的。
@Test
public void array1Test() {
int[] a = {1, 2, 3, 4, 5, 8, 1, 3, 2};
//Arrays.sort(a);//7
int i = Arrays.binarySearch(a, 3);
System.out.println(i);
}
優先選擇容器而不是數組,隻有當性能成為問題且切換到數組對性能提高有所幫助時,才應該重構成數組。
十七、容器深入研究
針對容器這塊有很多疑問,肯定要經過二次過濾學習,所有不懂的可以先記下來跳過。回過頭來再看。
集合類庫圖
填充容器
所有的Collection 子類型都有一個接收另外一個Collection 對象的構造器,用所接收的Collection 對象中的元素來填充新的容器。
通過繼承Abstract容器類來定制Collection 和Map實作。
享元模式:在普通的解決方案需要過多的對象,或者産生普通對象太占用空間時使用享元。
Collection的功能方法
注意:Collection的方法不包括随機通路的get()方法。因為Collection包含Set,而Set是自己維護内部順序的。
//List 的 <T> T[] toArray(T[] a);方法用法
String[] array = names().toArray(new String[names.size()]);
String[] array1 = names().toArray(new String[0]);
System.out.println(Arrays.toString(array));
System.out.println(Arrays.toString(array1));
//不能強轉,要用上面的泛型方法
//Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
//String[] array2 = (String[]) names().toArray();
// System.out.println(Arrays.toString(array2));
可選操作
最常見的未獲支援的操作,都來自于背後由固定尺寸的資料結構支援的容器。
應該把Arrays.asList(array1)的結果作為構造器的參數傳遞給任何Collection(或者addAll()方法或Collections.addAll()方法), 這樣可以生成允許使用所有的方法的普通容器
//以下兩個都不支援修改List容器的操作,會抛出UnsupportedOperationException異常
//他們的唯一的差別是Arrays.asList(array1)支援set()方法,因為它不會改變該數組的長度,該List是基于一個固定大小的數組,
// 僅支援那些不會改變數組大小的操作,
// 而Collections.unmodifiableList(new ArrayList<String>()) 是禁止了是以的修改操作,是以不行。
//任何會引起對底層資料結構的尺寸進行修改的方法都會抛出UnsupportedOperationException異常,
List<String> list1 = Arrays.asList(array1);
List<String> list2 = Collections.unmodifiableList(new ArrayList<String>());
List的功能方法
public static void main(String[] args) {
System.out.println(capitals().get("CHINA"));
System.out.println(names());
String[] array = names().toArray(new String[names.size()]);
String[] array1 = names().toArray(new String[0]);
System.out.println(Arrays.toString(array));
System.out.println(Arrays.toString(array1));
//Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
//String[] array2 = (String[]) names().toArray();
// System.out.println(Arrays.toString(array2));
//以下兩個都不支援修改List容器的操作,會抛出UnsupportedOperationException異常
//他們的唯一的差別是Arrays.asList(array1)支援set()方法,因為它不會改變該數組的長度,該List是基于一個固定大小的數組,
// 僅支援那些不會改變數組大小的操作,
// 而Collections.unmodifiableList(new ArrayList<String>()) 是禁止了是以的修改操作,是以不行。
//任何會引起對底層資料結構的尺寸進行修改的方法都會抛出UnsupportedOperationException異常,
// 是以,應該把Arrays.asList(array1)的結果作為構造器的參數傳遞給任何Collection(或者addAll()方法或Collections.addAll()方法,
// 這樣可以生成允許使用所有的方法的普通容器)
List<String> list1 = Arrays.asList(array1);
List<String> list2 = Collections.unmodifiableList(new ArrayList<String>());
//判斷容器内是否有該元素,有則傳回該元素所在第一個位置,沒有傳回-1
int sudan = list1.indexOf("SUDAN");
//傳回最後出現該元素的位置,沒有傳回-1
int sudan1 = list1.lastIndexOf("SUDAN");
//判斷容器是否為空,沒有存任何元素,如果容器中添加了null也算是元素,會傳回false.
boolean empty = list1.isEmpty();
List<String> list3 = new ArrayList<String>();
list3.add(null);
boolean empty1 = list3.isEmpty();//false
System.out.println(empty);
ListIterator<String> listIterator = list1.listIterator();
//傳入index參數則會把遊标cursor從0移到參數為3疊代,也就是說隻疊代從第四個元素開始的容器元素。
ListIterator<String> listIterator1 = list1.listIterator(3);
System.out.println(listIterator.next());
System.out.println(listIterator1.next());
list3.add(null);
list3.add(null);
//可以通過下标參數移除元素,傳回該下标位置上元素的值(從0開始),如果傳入的index下标超過
// list長度則會抛IndexOutOfBoundsException
String remove = list3.remove(0);
//也可以通過想要移除的具體元素來移除容器中這個值的元素,如果有則移除傳回true,沒有傳回false,
// 多個相同元素隻移除第一個比對到的元素(一般都是從0到list.size()開始比對)
boolean b = list3.remove(null);
System.out.println(b);
//修改容器指定位置的值
list3.set(0,"1");
list3.add("2");
list3.add("1");
list3.add("1");
list3.add("5");
ArrayList<String> list4 = new ArrayList<>();
list4.add("1");
//保留list3中兩個List的交集
boolean b1 = list3.retainAll(list4);
System.out.println(list3);
//移除list3中所有與list4元素值相同的元素,不論有多少個相同的全部移除。
boolean b2 = list3.removeAll(list4);
System.out.println(list3);
//移除容器中所有的元素,包括null元素。
list3.clear();
System.out.println(list3);
}
Set和存儲順序
自定義對象去重(Set篇)
如果沒有其他的限制,預設選擇HashSet,因為它對速度進行了優化。
在使用容器時,必須為散列存儲和樹型存儲都建立或者重寫一個equals()方法。hashCode()方法隻有在這個類被置于HashSet或者LinkedHashSet中時才是必需的。
良好的程式設計風格要求我們在覆寫equals()方法時,總是同時要覆寫hashCode()方法。
SortedSet (被TreeSet實作)
SortedSet 中的元素可以保證處于排序狀态(按對象的比較函數進行排序)。
如果要按插入順序排序其實就是LinkedHashSet.
@Test
public void sortedSetTest() {
SortedSet<String> set = new TreeSet<>();
Collections.addAll(set,"all man are create equal".split(" "));
System.out.println(set);
System.out.println(set.first());
System.out.println(set.last());
System.out.println(set.comparator());
Iterator<String> iterator = set.iterator();
String low = null;
String high = null;
for (int i = 0 ;i<4;i++){
if (i==1) {
low = iterator.next();
}
if (i==3) {
high = iterator.next();
} else {
iterator.next();
}
}
//截取set,包括low,不包括high
System.out.println(set.subSet(low,high));
//截取set小于high
System.out.println(set.headSet(high));
//截取set大于等于low
System.out.println(set.tailSet(low));
}
隊列
LinkedList和PriorityQueue的差異僅在于排序行為而不是性能。
普通隊列一般是先進先出的。ArrayBlockingQueue、ConcurrentLinkedQueue、LinkedBlockingQueue、PriorityBlockingQueue這些除了最後一個優先級隊列,都是按照元素進入隊列的先後順序拿到的。
雙向隊列
雙向隊列還是一個隊列,但是我們可以在任何一端添加或移除元素。可以通過LinkedList建立雙向隊列。
了解Map
映射表(也稱關聯數組)的基本思想是它維護的是鍵值對,是以你可以使用鍵來查找值。
HashMap | TreeMap | LinkedHashMap | WeakHashMap | ConcurrentHashMap | IdentityHashMap .
以上的Map行為特性各個不同,表現在效率、鍵值對的儲存和呈現次序、對象的儲存周期、映射表如何在多線程程式中工作和判定“鍵”等價的政策等方面。
性能
當在get()中使用線性搜尋(for循環周遊)時,執行速度會相當的慢,HashMap使用了散列碼來取代對鍵的緩慢搜尋。散列碼是“相對唯一”的,用以代表對象的int值。
Map的鍵必須是唯一的,而值可以有重複。是以鍵keySet傳回的是Set;而值是Collection。
TreeMap是SortedMap目前的唯一實作,如果沒有傳或者值沒有comparator,則預設是自然排序。鍵值對是按鍵的次序排列的。
//accessOrder = TRUE 表示采用基于通路的LRU算法
LinkedHashMap<String,String> map = new LinkedHashMap(16,0.75F,true);
LinkedHashMap 是以插入的順序進行周遊的,基于LRU算法的版本也是如此。但是,在LRU版本中,通路越少的元素會出現在隊列的最前面。
散列與散列碼
Object的hashCode()使用對象的位址計算散列碼;equals()比較對象的位址。是以,如果要使用自己的類作為HashMap的鍵,那麼就要重寫equals()和hashCode()方法。
正确的equals()必須滿足以下5個條件:
- 自反性 x.equals(x)=true
- 對稱性 y.equals(x)=true→x.equals(y)=true
- 傳遞性 x.equals(y)=true,y.equals(z)=true→x.equals(z)=true
- 一緻性 無論比較對少次,結果都是一樣的
- x!=null→x.equals(null)=false
注意:Map.Entry是一個接口,用來描述依賴于實作的結構,是以如果你想要建立自己的Map類型,就必須同時定義Map.Entry的實作。
為速度而散列
線性查詢是最慢的查詢方式。
對查詢速度再進一步的解決方案是保持鍵的排序狀态,然後使用二分法進行查詢。Collections.binarySearch()。
散列的價值在于速度,散列使得查詢得以快速進行,它将鍵儲存在某處,以便能夠很快找到。
存儲一組元素最快的資料結構是數組,使用數組來表示鍵的資訊(散列值,作為數組的下标)。
為解決數組容量被固定的問題,不同的鍵可以産生相同的下标。當鍵相同時,則比較内容。
查詢一個值的過程首先是計算散列碼,然後使用散列碼查詢數組。如果沒有沖突,則是個完美的散列函數;如果沖突了,則沖突通過外部連結處理。數組不直接儲存值,而是保持值的List,如LinkedList,然後對list值使用equal方法進行線性查詢。(雖然線性查詢最慢,但是因為散列得好的話,每個位置隻有較少的值,這樣就不太好影響效率)
散清單的”槽位“(slot)通常稱為桶位(bucket)。
為使散列分布均勻,一開始是使用質數,後面發現質數并不是散列桶的理想容量,後面改用2的整數次長度的散清單,用掩碼代替除法。求餘的%操作是開銷最大的操作(因為get()是使用最多的)。
生成hoshcode()方法參考如下:
選擇不同的容器實作來使用
容器之間的差別通常歸結為由什麼樣的資料結構來實作它們。
雖然有些容器接口相同會擁有共同的操作,但操作的性能卻并不相同,在選擇使用哪個時可以基于某個特定操作的頻率和執行速度來進行選擇,一般用性能測試來選擇。
P538的性能測試demo可以學習一下。
結果:使用時最佳的做法是将ArrayList作為預設首選,隻有當需要額外的功能,如經常從表中間插入和删除時才去選擇LinkedList。
HashSet的性能基本上總是比TreeSet好,優先使用,除了當我們需要一個排好序的set時,才應該使用TreeSet,
TreeSet疊代通常比HashSet要快。插入操作,LinkedHashSet比HashSet代價要高是因為LinkedHashSet還要維護連結清單(保證插入順序)。
優先使用HashMap,隻有在要求Map始終保持有序時,才需要使用TreeMap。LinkedHashMap比HashMap代價要高是因為LinkedHashMap還要維護連結清單(保證插入順序)。
HashMap的性能因子
HashMap使用的預設負載因子是0.75。即尺寸/容量。 尺寸值表中目前存儲的項數,容量指表中的桶位數。HashMap可以指定初始容量。
java容器類類庫采用快速報錯(fail-fast)機制。一旦發現容器上有任何除了目前進行鎖進行的操作以外的變化就會抛出ConcurrentModificationException異常。
持有引用
軟引用 SoftReference 、弱引用 WeakReference、虛引用 PhantomReference。對象的可獲得程度由強到弱。使用這些類為垃圾回收提供了更大的靈活性。
使用前提:想繼續持有對某個對象的引用,但也希望能夠允許垃圾回收器釋放它。這樣我們便可以繼續使用該對象,而在記憶體消耗殆盡的時候又允許釋放該對象。
使用方法:使用Reference對象作為調用者與普通引用之間的包裝(代理),且不能有其他普通的引用指向那個對象,這樣才能達到上面的目的。(繼續使用該對象,而在記憶體消耗殆盡的時候又允許釋放該對象。)
如果引用的是個普通的引用,則該對象是可獲得的,那麼垃圾回收器就不能釋放它。
SoftReference 用以實作記憶體敏哥的高速緩存。
WeakReference 為了實作規範映射而設計的。
PhantomReference 用以排程回收前的清理工作,它比java終止機制更靈活。
注意:使用SoftReference 和 WeakReference 可以選擇是否要将它們放入ReferenceQueue隊列。(回收前清理工作的工具)。而PhantomReference 隻能依賴于ReferenceQueue。
552
我個人認為,容器的深入研究這一章不需要勉強自己第一遍就要看懂,看透,但需要多研究幾遍,直到看透了,了解了基本常用的這些容器底層原理。
什麼是掩碼?
散列機制是如何工作的?
使用散列容器時如何編寫hashCode()和equals()方法?
為什麼某些容器會有不同版本的實作?如何選擇?
17.2.1 的generator?
p494 中LinkedHashSet 的添加操作是不是重複添加了一遍?
容器類的列印是不是都是預設toString就行?
Map.Entry<String, String> 裡的這個Entry是個什麼?
Map.Entry是一個接口,用來描述依賴于實作的結構,是以如果你想要建立自己的Map類型,就必須同時定義Map.Entry的實作?
内部類在Map等容器裡的使用?
Map的内部實作和構造?
P525練習?
17.2重新研究一下
動态語言可以在任何對象上調用任何方法,并且可以在運作時發現某個特定調用是否可以工作。動态語言和靜态語言的差別?對比?研究一門動态語言自我感覺與靜态語言的對比?
List的内部結構和底層?
什麼叫連結清單,單向連結清單和雙向連結清單?
list1.listIterator()的使用?
p510練習8.
散列存儲和樹型存儲?指的是如HashSet和TreeSet麼?
怎麼自己重寫equals方法 hashcode方法?怎麼用idea重寫?
HashMap的底層實作?其他Map的底層實作?
P517下面注釋寫的好處怎麼解釋?
使用數組代替溢出桶,有兩個好處:
- 可以針對磁盤存儲方式做優化。
- 在建立和回收單獨的記錄時,能節約很多時間。
最近最少使用(LRU)算法?一般用在什麼場合?
待續。。。。。。
其他
roy mustang 原來鋼煉裡的羅伊.馬斯坦 是野馬的意思。
面向函數式程式設計 :Scala 、Elm 。可以多去了解下函數式程式設計的語言,語言的目标是寫出更簡潔的代碼,或者效率更高,或者你的代碼更容易被大家懂 。
語言方面更多關心的議題 :第一,多産。第二,多線程的問題。第三,錯誤,如何發現跟錯誤相關的一些議題
每當我有問題需要被解決的時候我發現Python是最快可以給我結果的一個語言 。
為什麼Python會作為機器學習非常好的一種語言,因為Python把其他語言做了一個封裝,調用其他語言做的包。很多的資料科學家他們其實是不希望學習過于複雜的程式設計語言,能夠把他們關于資料方面處理的智慧進行封裝起來,通過Python來調用這樣會友善很多,這也是為什麼Python這幾年這麼流行的原因。
mysql排序問題:比如我們以建立日期來排序,當日期相同時,mysql會随機排序。
海明威的硬币:老人與海
Bruce Eckel的問答和建議
疑問:
有點意識到自己喜歡理論大而泛的模糊知識的學習,而不喜歡實踐和細節的打磨,是因為粗心浮躁導緻的麼>
在我看來,其實是對知識的空洞和無知,導緻了落實不到細節,是以隻能宏觀上了去感受和學習,落實不到細節上來,還不到粗心浮躁,而是基礎太差,隻能一步一個腳印,找個清單,從頭開始學習起,就不會學習個新東西被俄羅斯套娃到另一個新東西再到另一個新東西上了,因為你懂的足夠多,更容易了解這樣東西或者技術。
參考文獻
JAVA程式設計思想
建議不繼續維護,整理新版本下的Java基礎。
- A-Z ↩︎