這台可以“私人定製”的SUV 竟然跑到青藏線玩漂移

這個標定“黑科技”是可以使用手機App在線刷新標定,通過手機App將雲存儲中的標定數據,臨時覆蓋發動機控制器中的相關文件。此方式有以下幾點好處:1。刷新速度快,4G網絡條件下,可以做到秒刷 2。操作簡單,普通客戶秒懂,秒用 3。

在我記憶中,國內似乎沒有那家企業敢在一輛汽車尚在開發階段,就邀請用戶去參与試駕體驗,而且還聽取用戶的建議和需求,去改變汽車的參數,讓用戶立即就能體驗改變所帶來的不同感受,這就是上汽大通提出的C2B大規模定製模式的造車理念,讓用戶可能既是需求者也形同生產者。

前日,上汽大通將兩台價值150萬的D90工程樣車,拉到了高原地帶青藏線進行標定調整測試,也受邀其中,而且是進行了一系列的路況路試來反饋調整一個最佳的模式和狀態,雖然廠家已經將D90重度的偽裝,但還是可以看的出來整體上的輪廓與北京車展,展出的D90概念車相當的接近了。

D90作為一台全尺寸的SUV,D90的軸距達到2950mm,寬度達到1926 mm,從我在後排的乘坐體驗是一個可以翹着二朗腿的空間,那第三排也是可以而且是坐了兩個1米75左右的成年人,關健是D90可以提供5、6、7、8座四種產品空間,從工程樣車的尺寸看,對比競品傳祺的GS8和長城H9在車身的尺寸上是有優勢的。

在這次的D90工程樣車上,讓我非常驚喜的是,在中控上,工程樣車與概念版本的D90得到了更多的保留,可以透露的是,方向盤和中控都有着非常高的相似度,而且從方向盤的手感上,都有着非常好的握感,整體風格是非常的運動,大小也適中,當然還有那個超大的液晶显示屏了。

動力總成方面,大通D90是2.0T發動機搭配6AT的變速箱,最大馬力224,最大扭矩360牛米,能提供運動、冰雪、岩石等多重駕駛模式,這套動力總成的試駕體驗也是整個環節中最核心的部份。

行駛在高原3000-4800的海拔區間,D90的動力和變速箱的匹配都做的非常好,沒有很明顯的頓挫感,而且在起步的階段,非常的平順不會沖,在需要超車降檔的時候,扭矩也是來的比較快,但是在轉速的攀升上,會略顯有點不夠积極,力是有,但我覺得這個力是慢了點,除了發動機的內在問題,還有外在問題也是會影響性能的因素,在高原空氣稀少情況下,空氣中的氧含量低,進入燃燒室內的氧氣不足,會造成燃料燃燒不充分,車輛動力會減弱,但其實在整段的測試中,我開的D90是滿載七個人的情況下進行的(平均一百斤一個人也有七百斤的重量了)所以它的潛力還是有很多空間可去調整及挖掘的。

在高速的轉向上,也是有着非常好的反饋,電動助力轉向的特性也發揮的很不錯,低速輕,高速穩,但在底盤上就沒有更多的體驗了,因為這台D90的懸挂系統還是在一個調試階段,還未在一個平衡的數值上,但從定位上來看,車架底盤一定是走硬朗風格,如果在配合油門的平順性,這會是中大型SUV市場上不能小看的新生“黑馬”。

上汽大通的“黑科技”體驗

只要將此智能終端插到OBD接口,就可以調整發動機的動力輸出了,它和我們的車內駕駛模式區別就是在於,這個“黑科技”它只單單調整發動機的動力輸出,平常我們所用的運動模式,是會對車有一個整體的調整,比如轉向的靈敏,懸挂的軟硬,傳動等等,在G10用了之後,感覺最深的就是發動機的轉速攀升,非常快,在那一剎那,我感覺在開着一台本田的發動機,享受高轉速的樂趣。

這個標定“黑科技”是可以使用手機App在線刷新標定,通過手機App將雲存儲中的標定數據,臨時覆蓋發動機控制器中的相關文件。此方式有以下幾點好處:1.刷新速度快,4G網絡條件下,可以做到秒刷 2.操作簡單,普通客戶秒懂,秒用 3.即使刷新失敗,也不影響正常用車 4.客戶可以根據自己的不同需求,訂製化滿足自己需求的標定模式。

而在這次的試駕活動中,上汽大通不僅僅帶了兩台D90過來高原海撥,還將旗下“主將”都帶來了高原測試,T60皮卡,G10 MpV ,可以用兩句話概括它們,T60皮卡,自主品牌里顏值最高,動力搭配合理,內飾完勝同級皮卡。大通G10 自主品牌操控性最好的MpV,動力上我認為是沒有對手,曾在賽道上駕駛G10,大直路180KM時速,還感覺有上升的動力儲備,高速彎的側傾也是抑制的在一個合理範圍內,G10也是我最推薦的一款MpV。

格爾木站作為上汽大通C2B全球六站路試活動的首站,宣告上汽大通SUV D90正式進入第三階段試驗階段,接下來,上汽大通還將會邀請用戶前往極寒試驗場地黑河、亞洲最大的汽車專業試驗場廣德、黑河、澳大利亞、川西、迪拜等世界各地,在從零上60攝氏度高溫到零下40度極寒的極限環境下,全方位測試D90車輛的耐久性、動力總成性能匹配、机械完善度,而且值的贊的是,只要你是盲訂了D90的用戶,都會有可能,邀請去體驗給出建議,我相信這一台D90,在收集了如此龐大的終端數據下,它是有足夠的實力在這個市場“脫穎而出”。

如需“私人定製”SUV 的用戶可以掃描下方二維碼喔

本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

怒肝倆月,新鮮出爐史上最有趣的Java小白手冊,第一版,每個 Java 初學者都應該收藏

這麼說吧,在我眼裡,Java 就是最流行的編程語言,沒有之一(PHP 往一邊站)。不僅崗位多,容易找到工作,關鍵是薪資水平也到位,不學 Java 虧得慌,對吧?

那可能零基礎學編程的小夥伴就會頭疼了,網上關於 Java 的大部分技術文章都不夠幽默,不夠風趣,不夠系列,急需要一份能看得進去的學習手冊,那我覺得我肝的這份手冊正好符合要求,並且會一直持續更新下去。

第一版的內容暫時包含兩方面,Java 基礎和 Java 面向對象編程。來吧,先上目錄,一睹為快。

01、Java 基本語法簡介
02、Java 基本數據類型簡介
03、Java main() 方法簡介
04、Java 的流程控制語句
05、Java 包的簡介
06、Java 到底是值傳遞還是引用傳遞
07、Java 的類和對象
08、Java 構造方法
09、Java 抽象類
10、Java 接口
11、Java 繼承
12、this 關鍵字
13、super 關鍵字
14、重寫和重載
15、static 關鍵字
16、Java 枚舉
17、final 關鍵字

目錄欣賞完了,接下來就是拜讀精華內容的時間,搬個小板凳,認認真真好好學吧,學到就是賺到!

一、Java 基本語法簡介

01、數據類型

Java 有 2 種數據類型,一種是基本數據類型,一種是引用類型。

基本數據類型用於存儲簡單類型的數據,比如說,int、long、byte、short 用於存儲整數,float、double 用於存儲浮點數,char 用於存儲字符,boolean 用於存儲布爾值。

不同的基本數據類型,有不同的默認值和大小,來個表格感受下。

數據類型 默認值 大小
boolean false 1比特
char ‘\u0000’ 2字節
byte 0 1字節
short 0 2字節
int 0 4字節
long 0L 8字節
float 0.0f 4字節
double 0.0 8字節

引用類型用於存儲對象(null 表示沒有值的對象)的引用,String 是引用類型的最佳代表,比如說 String cmower = "沉默王二"

02、聲明變量

要聲明一個變量,必須指定它的名字和類型,來看一個簡單的示例:

int age;
String name;

count 和 name 在聲明後會得到一個默認值,按照它們的數據類型——不能是局部變量(否則 Java 編譯器會在你使用變量的時候提醒要先賦值),必須是類成員變量。

public class SyntaxLocalVariable {
    int age;
    String name;

    public static void main(String[] args) {
        SyntaxLocalVariable syntax = new SyntaxLocalVariable();
        System.out.println(syntax.age); // 輸出 0
        System.out.println(syntax.name);  // 輸出 null
    }
}

也可以在聲明一個變量后使用“=”操作符進行賦值,就像下面這樣:

int age = 18;
String name = "沉默王二";

我們定義了 2 個變量,int 類型的 age 和 String 類型的 name,age 賦值 18,name 賦值為“沉默王二”。

每行代碼後面都跟了一個“;”,表示當前語句結束了。

在 Java 中,變量最好遵守命名約定,這樣能提高代碼的可閱讀性。

  • 以字母、下劃線(_)或者美元符號($)開頭
  • 不能使用 Java 的保留字,比如說 int 不能作為變量名

03、數組

數組在 Java 中佔據着重要的位置,它是很多集合類的底層實現。數組屬於引用類型,它用來存儲一系列指定類型的數據。

聲明數組的一般語法如下所示:

type[] identiier = new type[length];

type 可以是任意的基本數據類型或者引用類型。來看下面這個例子:

public class ArraysDemo {
    public static void main(String[] args) {
        int [] nums = new int[10];
        nums[0] = 18;
        nums[1] = 19;
        System.out.println(nums[0]);
    }
}

數組的索引從 0 開始,第一個元素的索引為 0,第二個元素的索引為 1。為什麼要這樣設計?感興趣的話,你可以去探究一下。

通過變量名[索引]的方式可以訪問數組指定索引處的元素,賦值或者取值是一樣的。

04、關鍵字

關鍵字屬於保留字,在 Java 中具有特殊的含義,比如說 public、final、static、new 等等,它們不能用來作為變量名。為了便於你作為參照,我列舉了 48 個常用的關鍵字,你可以瞅一瞅。

  1. abstract: abstract 關鍵字用於聲明抽象類——可以有抽象和非抽象方法。

  2. boolean: boolean 關鍵字用於將變量聲明為布爾值類型,它只有 true 和 false 兩個值。

  3. break: break 關鍵字用於中斷循環或 switch 語句。

  4. byte: byte 關鍵字用於聲明一個可以容納 8 個比特的變量。

  5. case: case 關鍵字用於在 switch 語句中標記條件的值。

  6. catch: catch 關鍵字用於捕獲 try 語句中的異常。

  7. char: char 關鍵字用於聲明一個可以容納無符號 16 位比特的 Unicode 字符的變量。

  8. class: class 關鍵字用於聲明一個類。

  9. continue: continue 關鍵字用於繼續下一個循環。它可以在指定條件下跳過其餘代碼。

  10. default: default 關鍵字用於指定 switch 語句中除去 case 條件之外的默認代碼塊。

  11. do: do 關鍵字通常和 while 關鍵字配合使用,do 后緊跟循環體。

  12. double: double 關鍵字用於聲明一個可以容納 64 位浮點數的變量。

  13. else: else 關鍵字用於指示 if 語句中的備用分支。

  14. enum: enum(枚舉)關鍵字用於定義一組固定的常量。

  15. extends: extends 關鍵字用於指示一個類是從另一個類或接口繼承的。

  16. final: final 關鍵字用於指示該變量是不可更改的。

  17. finally: finally 關鍵字和 try-catch 配合使用,表示無論是否處理異常,總是執行 finally 塊中的代碼。

  18. float: float 關鍵字用於聲明一個可以容納 32 位浮點數的變量。

  19. for: for 關鍵字用於啟動一個 for 循環,如果循環次數是固定的,建議使用 for 循環。

  20. if: if 關鍵字用於指定條件,如果條件為真,則執行對應代碼。

  21. implements: implements 關鍵字用於實現接口。

  22. import: import 關鍵字用於導入對應的類或者接口。

  23. instanceof: instanceof 關鍵字用於判斷對象是否屬於某個類型(class)。

  24. int: int 關鍵字用於聲明一個可以容納 32 位帶符號的整數變量。

  25. interface: interface 關鍵字用於聲明接口——只能具有抽象方法。

  26. long: long 關鍵字用於聲明一個可以容納 64 位整數的變量。

  27. native: native 關鍵字用於指定一個方法是通過調用本機接口(非 Java)實現的。

  28. new: new 關鍵字用於創建一個新的對象。

  29. null: 如果一個變量是空的(什麼引用也沒有指向),就可以將它賦值為 null。

  30. package: package 關鍵字用於聲明類所在的包。

  31. private: private 關鍵字是一個訪問修飾符,表示方法或變量只對當前類可見。

  32. protected: protected 關鍵字也是一個訪問修飾符,表示方法或變量對同一包內的類和所有子類可見。

  33. public: public 關鍵字是另外一個訪問修飾符,除了可以聲明方法和變量(所有類可見),還可以聲明類。main() 方法必須聲明為 public。

  34. return: return 關鍵字用於在代碼執行完成后返回(一個值)。

  35. short: short 關鍵字用於聲明一個可以容納 16 位整數的變量。

  36. static: static 關鍵字表示該變量或方法是靜態變量或靜態方法。

  37. strictfp: strictfp 關鍵字並不常見,通常用於修飾一個方法,確保方法體內的浮點數運算在每個平台上執行的結果相同。

  38. super: super 關鍵字可用於調用父類的方法或者變量。

  39. switch: switch 關鍵字通常用於三個(以上)的條件判斷。

  40. synchronized: synchronized 關鍵字用於指定多線程代碼中的同步方法、變量或者代碼塊。

  41. this: this 關鍵字可用於在方法或構造函數中引用當前對象。

  42. throw: throw 關鍵字主動拋出異常。

  43. throws: throws 關鍵字用於聲明異常。

  44. transient: transient 關鍵字在序列化的使用用到,它修飾的字段不會被序列化。

  45. try: try 關鍵字用於包裹要捕獲異常的代碼塊。

  46. void: void 關鍵字用於指定方法沒有返回值。

  47. volatile: volatile 關鍵字保證了不同線程對它修飾的變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。

  48. while: 如果循環次數不固定,建議使用 while 循環。

05、操作符

除去“=”賦值操作符,Java 中還有很多其他作用的操作符,我們來大致看一下。

①、算術運算符

  • +(加號)
  • –(減號)
  • *(乘號)
  • /(除號)
  • %(取余)

來看一個例子:

public class ArithmeticOperator {
    public static void main(String[] args) {
        int a = 10;
        int b = 5;

        System.out.println(a + b);//15  
        System.out.println(a - b);//5  
        System.out.println(a * b);//50  
        System.out.println(a / b);//2  
        System.out.println(a % b);//0  
    }
}

“+”號比較特殊,還可以用於字符串拼接,來看一個例子:

String result = "沉默王二" + "一枚有趣的程序員";

②、邏輯運算符

邏輯運算符通常用於布爾表達式,常見的有:

  • &&(AND)多個條件中只要有一個為 false 結果就為 false
  • ||(OR)多個條件只要有一個為 true 結果就為 true
  • !(NOT)條件如果為 true,加上“!”就為 false,否則,反之。

來看一個例子:

public class LogicalOperator {
    public static void main(String[] args) {
        int a=10;
        int b=5;
        int c=20;
        System.out.println(a<b&&a<c);//false
        System.out.println(a>b||a<c);//true
        System.out.println(!(a<b)); // true
    }
}

③、比較運算符

  • &lt; (小於)
  • &lt;= (小於或者等於)
  • &gt; (大於)
  • &gt;= (大於或者等於)
  • == (相等)
  • != (不等)

06、程序結構

Java 中最小的程序單元叫做類,一個類可以有一個或者多個字段(也叫作成員變量),還可以有一個或者多個方法,甚至還可以有一些內部類。

如果一個類想要執行,就必須有一個 main 方法——程序運行的入口,就好像人的嘴一樣,嗯,可以這麼牽強的理解一下。

public class StructureProgram {
    public static void main(String[] args) {
        System.out.println("沒有成員變量,只有一個 main 方法");
    }
}
  • 類名叫做 StructureProgram,在它裏面,只有一個 main 方法。
  • {} 之間的代碼稱之為代碼塊。
  • 以上源代碼將會保存在一個後綴名為 java 的文件中。

07、編譯然後執行代碼

通常,一些教程在介紹這塊內容的時候,建議你通過命令行中先執行 javac 命令將源代碼編譯成字節碼文件,然後再執行 java 命令指定代碼。

但我不希望這個糟糕的局面再繼續下去了——新手安裝配置 JDK 真的蠻需要勇氣和耐心的,稍有不慎,沒入門就先放棄了。況且,在命令行中編譯源代碼會遇到很多莫名其妙的錯誤,這對新手是極其致命的——如果你再遇到這種老式的教程,可以吐口水了。

好的方法,就是去下載 IntelliJ IDEA,簡稱 IDEA,它被業界公認為最好的 Java 集成開發工具,尤其在智能代碼助手、代碼自動提示、代碼重構、代碼版本管理(Git、SVN、Maven)、單元測試、代碼分析等方面有着亮眼的發揮。IDEA 產於捷克(位於東歐),開發人員以嚴謹著稱。IDEA 分為社區版和付費版兩個版本,新手直接下載社區版就足夠用了。

安裝成功后,可以開始敲代碼了,然後直接右鍵運行(連保存都省了),結果會在 Run 面板中显示,如下圖所示。

想查看反編譯后的字節碼的話,可以在 src 的同級目錄 target/classes 的包路徑下找到一個 StructureProgram.class 的文件(如果找不到的話,在目錄上右鍵選擇「Reload from Disk」)。

可以雙擊打開它。

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.cmower.baeldung.basic;

public class StructureProgram {
    public StructureProgram() {
    }

    public static void main(String[] args) {
        System.out.println("沒有成員變量,只有一個 main 方法");
    }
}

IDEA 默認會用 Fernflower 將 class 字節碼反編譯為我們可以看得懂的 Java 代碼。實際上,class 字節碼(請安裝 show bytecode 插件)長下面這個樣子:

// class version 57.65535 (-65479)
// access flags 0x21
public class com/cmower/baeldung/basic/StructureProgram {

  // compiled from: StructureProgram.java

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 3 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lcom/cmower/baeldung/basic/StructureProgram; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x9
  public static main([Ljava/lang/String;)V
   L0
    LINENUMBER 5 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream
;
    LDC "\u6ca1\u6709\u6210\u5458\u53d8\u91cf\uff0c\u53ea\u6709\u4e00\u4e2a main \u65b9\u6cd5"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L1
    LINENUMBER 6 L1
    RETURN
   L2
    LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1
}

新手看起來還是有些懵逼的,建議過過眼癮就行了。

二、Java 基本數據類型簡介

01、布爾

布爾(boolean)僅用於存儲兩個值:true 和 false,也就是真和假,通常用於條件的判斷。代碼示例:

boolean flag = true;

02、byte

byte 的取值範圍在 -128 和 127 之間,包含 127。最小值為 -128,最大值為 127,默認值為 0。

在網絡傳輸的過程中,為了節省空間,常用字節來作為數據的傳輸方式。代碼示例:

byte a = 10;
byte b = -10;

03、short

short 的取值範圍在 -32,768 和 32,767 之間,包含 32,767。最小值為 -32,768,最大值為 32,767,默認值為 0。代碼示例:

short s = 10000;
short r = -5000;

04、int

int 的取值範圍在 -2,147,483,648(-2 ^ 31)和 2,147,483,647(2 ^ 31 -1)(含)之間,默認值為 0。如果沒有特殊需求,整形數據就用 int。代碼示例:

int a = 100000;
int b = -200000;

05、long

long 的取值範圍在 -9,223,372,036,854,775,808(-2^63) 和 9,223,372,036,854,775,807(2^63 -1)(含)之間,默認值為 0。如果 int 存儲不下,就用 long,整形數據就用 int。代碼示例:

long a = 100000L
long b = -200000L;

為了和 int 作區分,long 型變量在聲明的時候,末尾要帶上大寫的“L”。不用小寫的“l”,是因為小寫的“l”容易和数字“1”混淆。

06、float

float 是單精度的浮點數,遵循 IEEE 754(二進制浮點數算術標準),取值範圍是無限的,默認值為 0.0f。float 不適合用於精確的數值,比如說貨幣。代碼示例:

float f1 = 234.5f;

為了和 double 作區分,float 型變量在聲明的時候,末尾要帶上小寫的“f”。不需要使用大寫的“F”,是因為小寫的“f”很容易辨別。

07、double

double 是雙精度的浮點數,遵循 IEEE 754(二進制浮點數算術標準),取值範圍也是無限的,默認值為 0.0。double 同樣不適合用於精確的數值,比如說貨幣。代碼示例:

double d1 = 12.3

那精確的數值用什麼表示呢?最好使用 BigDecimal,它可以表示一個任意大小且精度完全準確的浮點數。針對貨幣類型的數值,也可以先乘以 100 轉成整形進行處理。

Tips:單精度是這樣的格式,1 位符號,8 位指數,23 位小數,有效位數為 7 位。

雙精度是這樣的格式,1 位符號,11 位指數,52 為小數,有效位數為 16 位。

取值範圍取決於指數位,計算精度取決於小數位(尾數)。小數位越多,則能表示的數越大,那麼計算精度則越高。

一個數由若干位数字組成,其中影響測量精度的数字稱作有效数字,也稱有效數位。有效数字指科學計算中用以表示一個浮點數精度的那些数字。一般地,指一個用小數形式表示的浮點數中,從第一個非零的数字算起的所有数字。如 1.24 和 0.00124 的有效数字都有 3 位。

08、char

char 可以表示一個 16 位的 Unicode 字符,其值範圍在 ‘\u0000’(0)和 ‘\uffff’(65,535)(包含)之間。代碼示例:

char letterA = 'A'// 用英文的單引號包裹住。

三、Java main() 方法簡介

每個程序都需要一個入口,對於 Java 程序來說,入口就是 main 方法。

public static void main(String[] args) {}

public、static、void 這 3 個關鍵字在前面的內容已經介紹過了,如果覺得回去找比較麻煩的話,這裏再貼一下:

  • public 關鍵字是另外一個訪問修飾符,除了可以聲明方法和變量(所有類可見),還可以聲明類。main() 方法必須聲明為 public。

  • static 關鍵字表示該變量或方法是靜態變量或靜態方法,可以直接通過類訪問,不需要實例化對象來訪問。

  • void 關鍵字用於指定方法沒有返回值。

另外,main 關鍵字為方法的名字,Java 虛擬機在執行程序時會尋找這個標識符;args 為 main() 方法的參數名,它的類型為一個 String 數組,也就是說,在使用 java 命令執行程序的時候,可以給 main() 方法傳遞字符串數組作為參數。

java HelloWorld 沉默王二 沉默王三

javac 命令用來編譯程序,java 命令用來執行程序,HelloWorld 為這段程序的類名,沉默王二和沉默王三為字符串數組,中間通過空格隔開,然後就可以在 main() 方法中通過 args[0]args[1] 獲取傳遞的參數值了。

public class HelloWorld {
    public static void main(String[] args) {
        if ("沉默王二".equals(args[0])) {

        }

        if ("沉默王三".equals(args[1])) {

        }
    }
}

main() 方法的寫法並不是唯一的,還有其他幾種變體,儘管它們可能並不常見,可以簡單來了解一下。

第二種,把方括號 [] 往 args 靠近而不是 String 靠近:

public static void main(String []args) { }

第三種,把方括號 [] 放在 args 的右側:

public static void main(String args[]) { }

第四種,還可以把數組形式換成可變參數的形式:

public static void main(String...args) { }

第五種,在 main() 方法上添加另外一個修飾符 strictfp,用於強調在處理浮點數時的兼容性:

public strictfp static void main(String[] args) { }

也可以在 main() 方法上添加 final 關鍵字或者 synchronized 關鍵字。

第六種,還可以為 args 參數添加 final 關鍵字:

public static void main(final String[] args) { }

第七種,最複雜的一種,所有可以添加的關鍵字統統添加上:

final static synchronized strictfp void main(final String[] args) { }

當然了,並不需要為了裝逼特意把 main() 方法寫成上面提到的這些形式,使用 IDE 提供的默認形式就可以了。

四、Java 的流程控制語句

在 Java 中,有三種類型的流程控制語句:

  • 條件分支,用於在兩個或者多個條件之間做出選擇,常見的有 if/else/else if、三元運算符和 switch 語句。

  • 循環或者遍歷,常見的有 for、while 和 do-while。

  • break 和 continue,用於跳出循環或者跳過進入下一輪循環。

if 語句

if 語句的格式如下:

if(布爾表達式){  
// 如果條件為 true,則執行這塊代碼

畫個流程圖表示一下:

來寫個示例:

public class IfExample {
    public static void main(String[] args) {
        int age = 20;
        if (age < 30) {
            System.out.println("青春年華");
        }
    }
}

輸出:

青春年華

if-else 語句

if-else 語句的格式如下:

if(布爾表達式){  
// 條件為 true 時執行的代碼塊
}else{  
// 條件為 false  時執行的代碼塊
}  

畫個流程圖表示一下:

來寫個示例:

public class IfElseExample {
    public static void main(String[] args) {
        int age = 31;
        if (age < 30) {
            System.out.println("青春年華");
        } else {
            System.out.println("而立之年");
        }
    }
}

輸出:

而立之年

除了這個例子之外,還有一個判斷閏年(被 4 整除但不能被 100 整除或者被 400 整除)的例子:

public class LeapYear {
    public static void main(String[] args) {
        int year = 2020;
        if (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0)) {
            System.out.println("閏年");
        } else {
            System.out.println("普通年份");
        }
    }
}

輸出:

閏年

如果執行語句比較簡單的話,可以使用三元運算符來代替 if-else 語句,如果條件為 true,返回 ? 後面 : 前面的值;如果條件為 false,返回 : 後面的值。

public class IfElseTernaryExample {
    public static void main(String[] args) {
        int num = 13;
        String result = (num % 2 == 0) ? "偶數" : "奇數";
        System.out.println(result);
    }
}

輸出:

奇數

if-else-if 語句

if-else-if 語句的格式如下:

if(條件1){  
// 條件1 為 true 時執行的代碼
}else if(條件2){  
// 條件2 為 true 時執行的代碼
}  
else if(條件3){  
// 條件3 為 true 時執行的代碼
}  
...  
else{  
// 以上條件均為 false 時執行的代碼

畫個流程圖表示一下:

來寫個示例:

public class IfElseIfExample {
    public static void main(String[] args) {
        int age = 31;
        if (age < 30) {
            System.out.println("青春年華");
        } else if (age >= 30 && age < 40 ) {
            System.out.println("而立之年");
        } else if (age >= 40 && age < 50 ) {
            System.out.println("不惑之年");
        } else {
            System.out.println("知天命");
        }
    }
}

輸出:

而立之年

if 嵌套語句

if 嵌套語句的格式如下:

if(外側條件){    
     // 外側條件為 true 時執行的代碼 
          if(內側條件){  
             // 內側條件為 true 時執行的代碼
    }    
}  

畫個流程圖表示一下:

來寫個示例:

public class NestedIfExample {
    public static void main(String[] args) {
        int age = 20;
        boolean isGirl = true;
        if (age >= 20) {
            if (isGirl) {
                System.out.println("女生法定結婚年齡");
            }
        }
    }
}

輸出:

女生法定結婚年齡

switch 語句的格式:

switch(變量) {    
case 可選值1:    
 // 可選值1匹配后執行的代碼;    
 break;  // 該關鍵字是可選項
case 可選值2:    
 // 可選值2匹配后執行的代碼;    
 break;  // 該關鍵字是可選項
......    

default// 該關鍵字是可選項     
 // 所有可選值都不匹配后執行的代碼 
}    
  • 變量可以有 1 個或者 N 個值。

  • 值類型必須和變量類型是一致的,並且值是確定的。

  • 值必須是唯一的,不能重複,否則編譯會出錯。

  • break 關鍵字是可選的,如果沒有,則執行下一個 case,如果有,則跳出 switch 語句。

  • default 關鍵字也是可選的。

畫個流程圖:

來個示例:

public class Switch1 {
    public static void main(String[] args) {
        int age = 20;
        switch (age) {
            case 20 :
                System.out.println("上學");
                break;
            case 24 :
                System.out.println("蘇州工作");
                break;
            case 30 :
                System.out.println("洛陽工作");
                break;
            default:
                System.out.println("未知");
                break// 可省略
        }
    }
}

輸出:

上學

當兩個值要執行的代碼相同時,可以把要執行的代碼寫在下一個 case 語句中,而上一個 case 語句中什麼也沒有,來看一下示例:

public class Switch2 {
    public static void main(String[] args) {
        String name = "沉默王二";
        switch (name) {
            case "詹姆斯":
                System.out.println("籃球運動員");
                break;
            case "穆里尼奧":
                System.out.println("足球教練");
                break;
            case "沉默王二":
            case "沉默王三":
                System.out.println("乒乓球愛好者");
                break;
            default:
                throw new IllegalArgumentException(
                        "名字沒有匹配項");

        }
    }
}

輸出:

乒乓球愛好者

枚舉作為 switch 語句的變量也很常見,來看例子:

public class SwitchEnumDemo {
    public enum PlayerTypes {
        TENNIS,
        FOOTBALL,
        BASKETBALL,
        UNKNOWN
    }

    public static void main(String[] args) {
        System.out.println(createPlayer(PlayerTypes.BASKETBALL));
    }

    private static String createPlayer(PlayerTypes playerType) {
        switch (playerType) {
            case TENNIS:
                return "網球運動員費德勒";
            case FOOTBALL:
                return "足球運動員C羅";
            case BASKETBALL:
                return "籃球運動員詹姆斯";
            case UNKNOWN:
                throw new IllegalArgumentException("未知");
            default:
                throw new IllegalArgumentException(
                        "運動員類型: " + playerType);

        }
    }
}

輸出:

籃球運動員詹姆斯

循環語句比較

比較方式 for while do-while
簡介 for 循環的次數是固定的 while 循環的次數是不固定的,並且需要條件為 true do-while 循環的次數也不固定,但會至少執行一次循環,無聊條件是否為 true
何時使用 循環次數固定的 循環次數是不固定的 循環次數不固定,並且循環體至少要執行一次
語法 for(init:condition;++/–) {// 要執行的代碼} while(condition){// 要執行的代碼} do{//要執行的代碼}while(condition);

普通的 for 循環

普通的 for 循環可以分為 4 個部分:

1)初始變量:循環開始執行時的初始條件。

2)條件:循環每次執行時要判斷的條件,如果為 true,就執行循環體;如果為 false,就跳出循環。當然了,條件是可選的,如果沒有條件,則會一直循環。

3)循環體:循環每次要執行的代碼塊,直到條件變為 false。

4)自增/自減:初識變量變化的方式。

來看一下普通 for 循環的格式:

for(初識變量;條件;自增/自減){  
// 循環體
}  

畫個流程圖:

來個示例:

public class ForExample {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            System.out.println("沉默王二好帥啊");
        }
    }
}

輸出:

沉默王二好帥啊
沉默王二好帥啊
沉默王二好帥啊
沉默王二好帥啊
沉默王二好帥啊

循環語句還可以嵌套呢,這樣就可以打印出更好玩的呢。

public class PyramidForExample {
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            for (int j = 0;j<= i;j++) {
                System.out.print("");
            }
            System.out.println();
        }
    }
}

打印出什麼玩意呢?






for-each

for-each 循環通常用於遍曆數組和集合,它的使用規則比普通的 for 循環還要簡單,不需要初始變量,不需要條件,不需要下標來自增或者自減。來看一下語法:

for(元素類型 元素 : 數組或集合){  
// 要執行的代碼
}  

來看一下示例:

public class ForEachExample {
    public static void main(String[] args) {
        String[] strs = {"沉默王二""一枚有趣的程序員"};

        for (String str : strs) {
            System.out.println(str);
        }
    }
}

輸出:

沉默王二
一枚有趣的程序員

無限 for 循環

想不想體驗一下無限 for 循環的威力,也就是死循環?

public class InfinitiveForExample {
    public static void main(String[] args) {
        for(;;){
            System.out.println("停不下來。。。。");
        }
    }
}

輸出:

停不下來。。。。
停不下來。。。。
停不下來。。。。
停不下來。。。。

一旦運行起來,就停不下來了,除非強制停止。

while 循環

while(條件){  
//循環體  
}  

畫個流程圖:

來個示例:

public class WhileExample {
    public static void main(String[] args) {
        int i = 0;
        while (true) {
            System.out.println("沉默王二");
            i++;
            if (i == 5) {
                break;
            }
        }
    }
}

猜猜會輸出幾次?

沉默王二
沉默王二
沉默王二
沉默王二
沉默王二

do-while 循環

do{  
// 循環體
}while(提交);  

畫個流程圖:

來個示例:

public class DoWhileExample {
    public static void main(String[] args) {
        int i = 0;
        do {
            System.out.println("沉默王二");
            i++;
            if (i == 5) {
                break;
            }
        } while (true);
    }
}

程序輸出結果如下所示:

沉默王二
沉默王二
沉默王二
沉默王二
沉默王二

break

break 關鍵字通常用於中斷循環或 switch 語句,它在指定條件下中斷程序的當前流程。如果是內部循環,則僅中斷內部循環。

可以將 break 關鍵字用於所有類型循環語句中,比如說 for 循環,while 循環,以及 do-while 循環。

來畫個流程圖感受一下:

用在 for 循環中的示例:

for (int i = 1; i <= 10; i++) {
    if (i == 5) {
        break;
    }
    System.out.println(i);
}

用在嵌套 for 循環中的示例:

for (int i = 1; i <= 3; i++) {
    for (int j = 1; j <= 3; j++) {
        if (i == 2 && j == 2) {
            break;
        }
        System.out.println(i + " " + j);
    }
}

用在 while 循環中的示例:

int i = 1;
while (i <= 10) {
    if (i == 5) {
        i++;
        break;
    }
    System.out.println(i);
    i++;
}

用在 do-while 循環中的示例:

int j = 1;
do {
    if (j == 5) { 
        j++;
        break;
    }
    System.out.println(j);
    j++;
while (j <= 10);

continue

當我們需要在 for 循環或者 (do)while 循環中立即跳轉到下一個循環時,就可以使用 continue 關鍵字,通常用於跳過指定條件下的循環體,如果循環是嵌套的,僅跳過當前循環。

來個示例:

public class ContinueDemo {
    public static void main(String[] args) {
        for (int i = 1; i <= 10; i++) {
            if (i == 5) {
                // 使用 continue 關鍵字
                continue;// 5 將會被跳過
            }
            System.out.println(i);
        }
    }
}

輸出:

1
2
3
4
6
7
8
9
10

5 真的被跳過了。

再來個循環嵌套的例子。

public class ContinueInnerDemo {
    public static void main(String[] args) {
        for (int i = 1; i <= 3; i++) {
            for (int j = 1; j <= 3; j++) {
                if (i == 2 && j == 2) {
                    //  當i=2,j=2時跳過
                    continue;
                }
                System.out.println(i + " " + j);
            }
        }
    }
}

打印出什麼玩意呢?

1 1
1 2
1 3
2 1
2 3
3 1
3 2
3 3

“2 2” 沒有輸出,被跳過了。

再來看一下 while 循環時 continue 的使用示例:

public class ContinueWhileDemo {
    public static void main(String[] args) {
        int i = 1;
        while (i <= 10) {
            if (i == 5) {
                i++;
                continue;
            }
            System.out.println(i);
            i++;
        }
    }
}

輸出:

1
2
3
4
6
7
8
9
10

注意:如果把 if 條件中的“i++”省略掉的話,程序就會進入死循環,一直在 continue。

最後,再來看一下 do-while 循環時 continue 的使用示例:

public class ContinueDoWhileDemo {
    public static void main(String[] args) {
        int i=1;
        do{
            if(i==5){
                i++;
                continue;
            }
            System.out.println(i);
            i++;
        }while(i<=10);
    }
}

輸出:

1
2
3
4
6
7
8
9
10

注意:同樣的,如果把 if 條件中的“i++”省略掉的話,程序就會進入死循環,一直在 continue。

五、Java 包的簡介

在 Java 中,我們使用 package(包)對相關的類、接口和子包進行分組。這樣做的好處有:

  • 使相關類型更容易查找
  • 避免命名衝突,比如說 com.itwanger.Hello 和 com.itwangsan.Hello 不同
  • 通過包和訪問權限控制符來限定類的可見性

01、創建一個包

package com.itwanger;

可以使用 package 關鍵字來定義一個包名,需要注意的是,這行代碼必須處於一個類中的第一行。強烈建議在包中聲明類,不要缺省,否則就失去了包結構的帶來的好處。

包的命名應該遵守以下規則:

  • 應該全部是小寫字母
  • 可以包含多個單詞,單詞之間使用“.”連接,比如說 java.lang
  • 名稱由公司名或者組織名確定,採用倒序的方式,比如說,我個人博客的域名是 www.itwanger.com,所以我創建的包名是就是 com.itwanger.xxxx

每個包或者子包都在磁盤上有自己的目錄結構,如果 Java 文件時在 com.itwanger.xxxx 包下,那麼該文件所在的目錄結構就應該是 com->itwanger->xxxx

02、使用包

讓我們在名為 test 的子包里新建一個 Cmower 類:

package com.itwanger.test;

public class Cmower {
    private String name;
    private int age;
}

如果需要在另外一個包中使用 Cmower 類,就需要通過 import 關鍵字將其引入。有兩種方式可供選擇,第一種,使用 * 導入包下所有的類:

import com.itwanger.test.*;

第二種,使用類名導入該類:

import com.itwanger.test.Cmower;

Java 和第三方類庫提供了很多包可供使用,可以通過上述的方式導入類庫使用。

package com.itwanger.test;

import java.util.ArrayList;
import java.util.List;

public class CmowerTest {
    public static void main(String[] args) {
        List<Cmower> list = new ArrayList<>();
        list.add(new Cmower());
    }
}

03、全名

有時,我們可能會使用來自不同包下的兩個具有相同名稱的類。例如,我們可能同時使用 java.sql.Datejava.util.Date。當我們遇到命名衝突時,我們需要對至少一個類使用全名(包名+類名)。

List<com.itwanger.test.Cmower> list1 = new ArrayList<>();
list.add(new com.itwanger.test.Cmower());

六、Java 到底是值傳遞還是引用傳遞

將參數傳遞給方法有兩種常見的方式,一種是“值傳遞”,一種是“引用傳遞”。C 語言本身只支持值傳遞,它的衍生品 C++ 既支持值傳遞,也支持引用傳遞,而 Java 只支持值傳遞。

01、值傳遞 VS 引用傳遞

首先,我們必須要搞清楚,到底什麼是值傳遞,什麼是引用傳遞,否則,討論 Java 到底是值傳遞還是引用傳遞就顯得毫無意義。

當一個參數按照值的方式在兩個方法之間傳遞時,調用者和被調用者其實是用的兩個不同的變量——被調用者中的變量(原始值)是調用者中變量的一份拷貝,對它們當中的任何一個變量修改都不會影響到另外一個變量。

而當一個參數按照引用傳遞的方式在兩個方法之間傳遞時,調用者和被調用者其實用的是同一個變量,當該變量被修改時,雙方都是可見的。

Java 程序員之所以容易搞混值傳遞和引用傳遞,主要是因為 Java 有兩種數據類型,一種是基本類型,比如說 int,另外一種是引用類型,比如說 String。

基本類型的變量存儲的都是實際的值,而引用類型的變量存儲的是對象的引用——指向了對象在內存中的地址。值和引用存儲在 stack(棧)中,而對象存儲在 heap(堆)中。

之所以有這個區別,是因為:

  • 棧的優勢是,存取速度比堆要快,僅次於直接位於 CPU 中的寄存器。但缺點是,棧中的數據大小與生存周期必須是確定的。
  • 堆的優勢是可以動態地分配內存大小,生存周期也不必事先告訴編譯器,Java 的垃圾回收器會自動收走那些不再使用的數據。但由於要在運行時動態分配內存,存取速度較慢。

02、基本類型的參數傳遞

眾所周知,Java 有 8 種基本數據類型,分別是 int、long、byte、short、float、double 、char 和 boolean。它們的值直接存儲在棧中,每當作為參數傳遞時,都會將原始值(實參)複製一份新的出來,給形參用。形參將會在被調用方法結束時從棧中清除。

來看下面這段代碼:

public class PrimitiveTypeDemo {
    public static void main(String[] args) {
        int age = 18;
        modify(age);
        System.out.println(age);
    }

    private static void modify(int age1) {
        age1 = 30;
    }
}

1)main 方法中的 age 是基本類型,所以它的值 18 直接存儲在棧中。

2)調用 modify() 方法的時候,將為實參 age 創建一個副本(形參 age1),它的值也為 18,不過是在棧中的其他位置。

3)對形參 age 的任何修改都只會影響它自身而不會影響實參。

03、引用類型的參數傳遞

來看一段創建引用類型變量的代碼:

Writer writer = new Writer(18"沉默王二");

writer 是對象嗎?還是對象的引用?為了搞清楚這個問題,我們可以把上面的代碼拆分為兩行代碼:

Writer writer;
writer = new Writer(18"沉默王二");

假如 writer 是對象的話,就不需要通過 new 關鍵字創建對象了,對吧?那也就是說,writer 並不是對象,在“=”操作符執行之前,它僅僅是一個變量。那誰是對象呢?new Writer(18, "沉默王二"),它是對象,存儲於堆中;然後,“=”操作符將對象的引用賦值給了 writer 變量,於是 writer 此時應該叫對象引用,它存儲在棧中,保存了對象在堆中的地址。

每當引用類型作為參數傳遞時,都會創建一個對象引用(實參)的副本(形參),該形參保存的地址和實參一樣。

來看下面這段代碼:

public class ReferenceTypeDemo {
    public static void main(String[] args) {
        Writer a = new Writer(18);
        Writer b = new Writer(18);
        modify(a, b);

        System.out.println(a.getAge());
        System.out.println(b.getAge());
    }

    private static void modify(Writer a1, Writer b1) {
        a1.setAge(30);

        b1 = new Writer(18);
        b1.setAge(30);
    }
}

1)在調用 modify() 方法之前,實參 a 和 b 指向的對象是不一樣的,儘管 age 都為 18。

2)在調用 modify() 方法時,實參 a 和 b 都在棧中創建了一個新的副本,分別是 a1 和 b1,但指向的對象是一致的(a 和 a1 指向對象 a,b 和 b1 指向對象 b)。

3)在 modify() 方法中,修改了形參 a1 的 age 為 30,意味着對象 a 的 age 從 18 變成了 30,而實參 a 指向的也是對象 a,所以 a 的 age 也變成了 30;形參 b1 指向了一個新的對象,隨後 b1 的 age 被修改為 30。

修改 a1 的 age,意味着同時修改了 a 的 age,因為它們指向的對象是一個;修改 b1 的 age,對 b 卻沒有影響,因為它們指向的對象是兩個。

程序輸出的結果如下所示:

30
18

果然和我們的分析是吻合的。

七、Java 的類和對象

類和對象是 Java 中最基本的兩個概念,可以說撐起了面向對象編程(OOP)的一片天。對象可以是現實中看得見的任何物體(一隻特立獨行的豬),也可以是想象中的任何虛擬物體(能七十二變的孫悟空),Java 通過類(class)來定義這些物體,有什麼狀態(通過字段,或者叫成員變量定義,比如說豬的顏色是純色還是花色),有什麼行為(通過方法定義,比如說豬會吃,會睡覺)。

來,讓我來定義一個簡單的類給你看看。

public class Pig {
    private String color;

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

默認情況下,每個 Java 類都會有一個空的構造方法,儘管它在源代碼中是缺省的,但卻可以通過反編譯字節碼看到它。

public class Pig {
    private String color;

    public Pig() {
    }

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

沒錯,就是多出來的那個 public Pig() {},參數是空的,方法體是空的。我們可以通過 new 關鍵字利用這個構造方法來創建一個對象,代碼如下所示:

 Pig pig = new Pig();

當然了,我們也可以主動添加帶參的構造方法。

public class Pig {
    private String color;

    public Pig(String color) {
        this.color = color;
    }

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

這時候,再查看反編譯后的字節碼時,你會發現缺省的無參構造方法消失了——和源代碼一模一樣。

public class Pig {
    private String color;

    public Pig(String color) {
        this.color = color;
    }

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

這意味着無法通過 new Pig() 來創建對象了——編譯器會提醒你追加參數。

比如說你將代碼修改為 new Pig("純白色"),或者添加無參的構造方法。

public class Pig {
    private String color;

    public Pig(String color) {
        this.color = color;
    }

    public Pig() {
    }

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

使用無參構造方法創建的對象狀態默認值為 null(color 字符串為引用類型),如果是基本類型的話,默認值為對應基本類型的默認值,比如說 int 為 0,更詳細的見下圖。

(圖片中有一處錯誤,boolean 的默認值為 false)

接下來,我們來創建多個 Pig 對象,它的顏色各不相同。

public class PigTest {
    public static void main(String[] args) {
        Pig pigNoColor = new Pig();
        Pig pigWhite = new Pig("純白色");
        Pig pigBlack = new Pig("純黑色");
    }
}

你看,我們創建了 3 個不同花色的 Pig 對象,全部來自於一個類,由此可見類的重要性,只需要定義一次,就可以多次使用。

那假如我想改變對象的狀態呢?該怎麼辦?目前毫無辦法,因為沒有任何可以更改狀態的方法,直接修改 color 是行不通的,因為它的訪問權限修飾符是 private 的。

最好的辦法就是為 Pig 類追加 getter/setter 方法,就像下面這樣:

public String getColor() {
    return color;
}

public void setColor(String color) {
    this.color = color;
}

通過 setColor() 方法來修改,通過 getColor() 方法獲取狀態,它們的權限修飾符是 public 的。

Pig pigNoColor = new Pig();
pigNoColor.setColor("花色");
System.out.println(pigNoColor.getColor()); // 花色

為什麼要這樣設計呢?可以直接將 color 字段的訪問權限修飾符換成是 public 的啊,不就和 getter/setter 一樣的效果了嗎?

因為有些情況,某些字段是不允許被隨意修改的,它只有在對象創建的時候初始化一次,比如說豬的年齡,它只能每年長一歲(舉個例子),沒有月光寶盒讓它變回去。

private int age;

public int getAge() {
    return age;
}

public void increaseAge() {
    this.age++;
}

你看,age 就沒有 setter 方法,只有一個每年可以調用一次的 increaseAge() 方法和 getter 方法。如果把 age 的訪問權限修飾符更改為 public,age 就完全失去控制了,可以隨意將其重置為 0 或者負數。

訪問權限修飾符對於 Java 來說,非常重要,目前共有四種:public、private、protected 和 default(缺省)。

一個類只能使用 public 或者 default 修飾,public 修飾的類你之前已經見到過了,現在我來定義一個缺省權限修飾符的類給你欣賞一下。

class Dog {
}

哈哈,其實也沒啥可以欣賞的。缺省意味着這個類可以被同一個包下的其他類進行訪問;而 public 意味着這個類可以被所有包下的類進行訪問。

假如硬要通過 private 和 protected 來修飾類的話,編譯器會生氣的,它不同意。

private 可以用來修飾類的構造方法、字段和方法,只能被當前類進行訪問。protected 也可以用來修飾類的構造方法、字段和方法,但它的權限範圍更寬一些,可以被同一個包中的類進行訪問,或者當前類的子類。

可以通過下面這張圖來對比一下四個權限修飾符之間的差別:

  • 同一個類中,不管是哪種權限修飾符,都可以訪問;
  • 同一個包下,private 修飾的無法訪問;
  • 子類可以訪問 public 和 protected 修飾的;
  • public 修飾符面向世界,哈哈,可以被所有的地方訪問到。

八、Java 構造方法

假設現在有一個 Writer 類,它有兩個字段,姓名和年紀:

public class Writer {
    private String name;
    private int age;

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

重寫了 toString() 方法,用於打印 Writer 類的詳情。由於沒有構造方法,意味着當我們創建 Writer 對象時,它的字段值並沒有初始化:

Writer writer = new Writer();
System.out.println(writer.toString());

輸出結果如下所示:

Writer{name='null', age=0}

name 是字符串類型,所以默認值為 null,age 為 int 類型,所以默認值為 0。

讓我們為 Writer 類主動加一個無參的構造方法:

public Writer() {
    this.name = "";
    this.age = 0;
}

構造方法也是一個方法,只不過它沒有返回值,默認返回創建對象的類型。需要注意的是,當前構造方法沒有參數,它被稱為無參構造方法。如果我們沒有主動創建無參構造方法的話,編譯器會隱式地自動添加一個無參的構造方法。這就是為什麼,一開始雖然沒有構造方法,卻可以使用 new Writer() 創建對象的原因,只不過,所有的字段都被初始化成了默認值。

接下來,讓我們添加一個有參的構造方法:

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

現在,我們創建 Writer 對象的時候就可以通過對字段值初始化值了。

Writer writer1 = new Writer("沉默王二",18);
System.out.println(writer1.toString());

來看一下打印結果:

Writer{name='沉默王二', age=18}

可以根據字段的數量添加不同參數數量的構造方法,比如說,我們可以單獨為 name 字段添加一個構造方法:

public Writer(String name) {
    this.name = name;
}

為了能夠兼顧 age 字段,我們可以通過 this 關鍵字調用其他的構造方法:

public Writer(String name) {
    this(name,18);
}

把作者的年齡都默認初始化為 18。如果需要使用父類的構造方法,還可以使用 super 關鍵字,手冊後面有詳細的介紹。

九、Java 抽象類

當我們要完成的任務是確定的,但具體的方式需要隨後開個會投票的話,Java 的抽象類就派上用場了。這句話怎麼理解呢?搬個小板凳坐好,聽我來給你講講。

01、抽象類的 5 個關鍵點

1)定義抽象類的時候需要用到關鍵字 abstract,放在 class 關鍵字前。

public abstract class AbstractPlayer {
}

關於抽象類的命名,阿里出品的 Java 開發手冊上有強調,“抽象類命名要使用 Abstract 或 Base 開頭”,記住了哦。

2)抽象類不能被實例化,但可以有子類。

嘗試通過 new 關鍵字實例化的話,編譯器會報錯,提示“類是抽象的,不能實例化”。

通過 extends 關鍵字可以繼承抽象類,繼承后,BasketballPlayer 類就是 AbstractPlayer 的子類。

public class BasketballPlayer extends AbstractPlayer {
}

3)如果一個類定義了一個或多個抽象方法,那麼這個類必須是抽象類。

當在一個普通類(沒有使用 abstract 關鍵字修飾)中定義了抽象方法,編譯器就會有兩處錯誤提示。

第一處在類級別上,提醒你“這個類必須通過 abstract 關鍵字定義”,or 的那個信息沒必要,見下圖。

第二處在方法級別上,提醒你“抽象方法所在的類不是抽象的”,見下圖。

4)抽象類可以同時聲明抽象方法和具體方法,也可以什麼方法都沒有,但沒必要。就像下面這樣:

public abstract class AbstractPlayer {
    abstract void play();

    public void sleep() {
        System.out.println("運動員也要休息而不是挑戰極限");
    }
}

5)抽象類派生的子類必須實現父類中定義的抽象方法。比如說,抽象類中定義了 play() 方法,子類中就必須實現。

public class BasketballPlayer extends AbstractPlayer {
    @Override
    void play() {
        System.out.println("我是張伯倫,籃球場上得過 100 分");
    }
}

如果沒有實現的話,編譯器會提醒你“子類必須實現抽象方法”,見下圖。

02、什麼時候用抽象類

與抽象類息息相關的還有一個概念,就是接口,我們留到下一篇文章中詳細說,因為要說的知識點還是蠻多的。你現在只需要有這樣一個概念就好,接口是對行為的抽象,抽象類是對整個類(包含成員變量和行為)進行抽象。

(是不是有點明白又有點不明白,別著急,翹首以盼地等下一篇文章出爐吧)

除了接口之外,還有一個概念就是具體的類,就是不通過 abstract 修飾的普通類,見下面這段代碼中的定義。

public class BasketballPlayer {
   public void play() {
        System.out.println("我是詹姆斯,現役第一人");
    }
}

有接口,有具體類,那什麼時候該使用抽象類呢?

1)我們希望一些通用的功能被多個子類復用。比如說,AbstractPlayer 抽象類中有一個普通的方法 sleep(),表明所有運動員都需要休息,那麼這個方法就可以被子類復用。

public abstract class AbstractPlayer {
    public void sleep() {
        System.out.println("運動員也要休息而不是挑戰極限");
    }
}

雖然 AbstractPlayer 類可以不是抽象類——把 abstract 修飾符去掉也能滿足這種場景。但 AbstractPlayer 類可能還會有一個或者多個抽象方法。

BasketballPlayer 繼承了 AbstractPlayer 類,也就擁有了 sleep() 方法。

public class BasketballPlayer extends AbstractPlayer {
}

BasketballPlayer 對象可以直接調用 sleep() 方法:

BasketballPlayer basketballPlayer = new BasketballPlayer();
basketballPlayer.sleep();

FootballPlayer 繼承了 AbstractPlayer 類,也就擁有了 sleep() 方法。

public class FootballPlayer extends AbstractPlayer {
}

FootballPlayer 對象也可以直接調用 sleep() 方法:

FootballPlayer footballPlayer = new FootballPlayer();
footballPlayer.sleep();

2)我們需要在抽象類中定義好 API,然後在子類中擴展實現。比如說,AbstractPlayer 抽象類中有一個抽象方法 play(),定義所有運動員都可以從事某項運動,但需要對應子類去擴展實現。

public abstract class AbstractPlayer {
    abstract void play();
}

BasketballPlayer 繼承了 AbstractPlayer 類,擴展實現了自己的 play() 方法。

public class BasketballPlayer extends AbstractPlayer {
    @Override
    void play() {
        System.out.println("我是張伯倫,我籃球場上得過 100 分,");
    }
}

FootballPlayer 繼承了 AbstractPlayer 類,擴展實現了自己的 play() 方法。

public class FootballPlayer extends AbstractPlayer {
    @Override
    void play() {
        System.out.println("我是C羅,我能接住任意高度的頭球");
    }
}

3)如果父類與子類之間的關係符合 is-a 的層次關係,就可以使用抽象類,比如說籃球運動員是運動員,足球運動員是運動員。

03、具體示例

為了進一步展示抽象類的特性,我們再來看一個具體的示例。假設現在有一個文件,裏面的內容非常簡單——“Hello World”,現在需要有一個讀取器將內容讀取出來,最好能按照大寫的方式,或者小寫的方式。

這時候,最好定義一個抽象類,比如說 BaseFileReader:

public abstract class BaseFileReader {
    protected Path filePath;

    protected BaseFileReader(Path filePath) {
        this.filePath = filePath;
    }

    public List<String> readFile() throws IOException {
        return Files.lines(filePath)
                .map(this::mapFileLine).collect(Collectors.toList());
    }

    protected abstract String mapFileLine(String line);
}

filePath 為文件路徑,使用 protected 修飾,表明該成員變量可以在需要時被子類訪問。

readFile() 方法用來讀取文件,方法體裏面調用了抽象方法 mapFileLine()——需要子類擴展實現大小寫的方式。

你看,BaseFileReader 設計的就非常合理,並且易於擴展,子類只需要專註於具體的大小寫實現方式就可以了。

小寫的方式:

public class LowercaseFileReader extends BaseFileReader {
    protected LowercaseFileReader(Path filePath) {
        super(filePath);
    }

    @Override
    protected String mapFileLine(String line) {
        return line.toLowerCase();
    }
}

大寫的方式:

public class UppercaseFileReader extends BaseFileReader {
    protected UppercaseFileReader(Path filePath) {
        super(filePath);
    }

    @Override
    protected String mapFileLine(String line) {
        return line.toUpperCase();
    }
}

你看,從文件裏面一行一行讀取內容的代碼被子類復用了——抽象類 BaseFileReader 類中定義的普通方法 readFile()。與此同時,子類只需要專註於自己該做的工作,LowercaseFileReader 以小寫的方式讀取文件內容,UppercaseFileReader 以大寫的方式讀取文件內容。

接下來,我們來新建一個測試類 FileReaderTest:

public class FileReaderTest {
    public static void main(String[] args) throws URISyntaxException, IOException {
        URL location = FileReaderTest.class.getClassLoader().getResource("helloworld.txt");
        Path path = Paths.get(location.toURI());
        BaseFileReader lowercaseFileReader = new LowercaseFileReader(path);
        BaseFileReader uppercaseFileReader = new UppercaseFileReader(path);
        System.out.println(lowercaseFileReader.readFile());
        System.out.println(uppercaseFileReader.readFile());
    }
}

項目的 resource 目錄下有一個文本文件,名字叫 helloworld.txt。

可以通過 ClassLoader.getResource() 的方式獲取到該文件的 URI 路徑,然後就可以使用 LowercaseFileReader 和 UppercaseFileReader 兩種方式讀取到文本內容了。

輸出結果如下所示:

[hello world]
[HELLO WORLD]

十、Java 接口

對於面向對象編程來說,抽象是一個極具魅力的特徵。如果一個程序員的抽象思維很差,那他在編程中就會遇到很多困難,無法把業務變成具體的代碼。在 Java 中,可以通過兩種形式來達到抽象的目的,一種是抽象類,另外一種就是接口。

如果你現在就想知道抽象類與接口之間的區別,我可以提前給你說一個:

  • 一個類只能繼承一個抽象類,但卻可以實現多個接口。

當然了,在沒有搞清楚接口到底是什麼,它可以做什麼之前,這個區別理解起來會有點難度。

01、接口是什麼

接口是通過 interface 關鍵字定義的,它可以包含一些常量和方法,來看下面這個示例。

public interface Electronic {
    // 常量
    String LED = "LED";

    // 抽象方法
    int getElectricityUse();

    // 靜態方法
    static boolean isEnergyEfficient(String electtronicType) {
        return electtronicType.equals(LED);
    }

    // 默認方法
    default void printDescription() {
        System.out.println("电子");
    }
}

1)接口中定義的變量會在編譯的時候自動加上 public static final 修飾符,也就是說 LED 變量其實是一個常量。

Java 官方文檔上有這樣的聲明:

Every field declaration in the body of an interface is implicitly public, static, and final.

換句話說,接口可以用來作為常量類使用,還能省略掉 public static final,看似不錯的一種選擇,對吧?

不過,這種選擇並不可取。因為接口的本意是對方法進行抽象,而常量接口會對子類中的變量造成命名空間上的“污染”。

2)沒有使用 privatedefault 或者 static 關鍵字修飾的方法是隱式抽象的,在編譯的時候會自動加上 public abstract 修飾符。也就是說 getElectricityUse() 其實是一個抽象方法,沒有方法體——這是定義接口的本意。

3)從 Java 8 開始,接口中允許有靜態方法,比如說 isEnergyEfficient() 方法。

靜態方法無法由(實現了該接口的)類的對象調用,它只能通過接口的名字來調用,比如說 Electronic.isEnergyEfficient("LED")

接口中定義靜態方法的目的是為了提供一種簡單的機制,使我們不必創建對象就能調用方法,從而提高接口的競爭力。

4)接口中允許定義 default 方法也是從 Java 8 開始的,比如說 printDescription(),它始終由一個代碼塊組成,為實現該接口而不覆蓋該方法的類提供默認實現,也就是說,無法直接使用一個“;”號來結束默認方法——編譯器會報錯的。

允許在接口中定義默認方法的理由是很充分的,因為一個接口可能有多個實現類,這些類就必須實現接口中定義的抽象類,否則編譯器就會報錯。假如我們需要在所有的實現類中追加某個具體的方法,在沒有 default 方法的幫助下,我們就必須挨個對實現類進行修改。

來看一下 Electronic 接口反編譯后的字節碼吧,你會發現,接口中定義的所有變量或者方法,都會自動添加上 public 關鍵字——假如你想知道編譯器在背後都默默做了哪些輔助,記住反編譯字節碼就對了。

public interface Electronic
{

    public abstract int getElectricityUse();

    public static boolean isEnergyEfficient(String electtronicType)
    
{
        return electtronicType.equals("LED");
    }

    public void printDescription()
    
{
        System.out.println("\u7535\u5B50");
    }

    public static final String LED = "LED";
}

有些讀者可能會問,“二哥,為什麼我反編譯后的字節碼和你的不一樣,你用了什麼反編譯工具?”其實沒有什麼秘密,微信搜「沉默王二」回復關鍵字「JAD」就可以免費獲取了,超級好用。

02、定義接口的注意事項

由之前的例子我們就可以得出下面這些結論:

  • 接口中允許定義變量
  • 接口中允許定義抽象方法
  • 接口中允許定義靜態方法(Java 8 之後)
  • 接口中允許定義默認方法(Java 8 之後)

除此之外,我們還應該知道:

1)接口不允許直接實例化。

需要定義一個類去實現接口,然後再實例化。

public class Computer implements Electronic {

    public static void main(String[] args) {
        new Computer();
    }

    @Override
    public int getElectricityUse() {
        return 0;
    }
}

2)接口可以是空的,既不定義變量,也不定義方法。

public interface Serializable {
}

Serializable 是最典型的一個空的接口,我之前分享過一篇文章《Java Serializable:明明就一個空的接口嘛》,感興趣的讀者可以去我的個人博客看一看,你就明白了空接口的意義。

http://www.itwanger.com/java/2019/11/14/java-serializable.html

3)不要在定義接口的時候使用 final 關鍵字,否則會報編譯錯誤,因為接口就是為了讓子類實現的,而 final 阻止了這種行為。

4)接口的抽象方法不能是 private、protected 或者 final。

5)接口的變量是隱式 public static final,所以其值無法改變。

03、接口可以做什麼

1)使某些實現類具有我們想要的功能,比如說,實現了 Cloneable 接口的類具有拷貝的功能,實現了 Comparable 或者 Comparator 的類具有比較功能。

Cloneable 和 Serializable 一樣,都屬於標記型接口,它們內部都是空的。實現了 Cloneable 接口的類可以使用 Object.clone() 方法,否則會拋出 CloneNotSupportedException。

public class CloneableTest implements Cloneable {
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        CloneableTest c1 = new CloneableTest();
        CloneableTest c2 = (CloneableTest) c1.clone();
    }
}

運行后沒有報錯。現在把 implements Cloneable 去掉。

public class CloneableTest {
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        CloneableTest c1 = new CloneableTest();
        CloneableTest c2 = (CloneableTest) c1.clone();

    }
}

運行后拋出 CloneNotSupportedException:

Exception in thread "main" java.lang.CloneNotSupportedException: com.cmower.baeldung.interface1.CloneableTest
    at java.base/java.lang.Object.clone(Native Method)
    at com.cmower.baeldung.interface1.CloneableTest.clone(CloneableTest.java:6)
    at com.cmower.baeldung.interface1.CloneableTest.main(CloneableTest.java:11)

至於 Comparable 和 Comparator 的用法,感興趣的讀者可以參照我之前寫的另外一篇文章《來吧,一文徹底搞懂Java中的Comparable和Comparator》。

http://www.itwanger.com/java/2020/01/04/java-comparable-comparator.html

2)Java 原則上只支持單一繼承,但通過接口可以實現多重繼承的目的。

可能有些讀者會問,“二哥,為什麼 Java 只支持單一繼承?”簡單來解釋一下。

如果有兩個類共同繼承(extends)一個有特定方法的父類,那麼該方法會被兩個子類重寫。然後,如果你決定同時繼承這兩個子類,那麼在你調用該重寫方法時,編譯器不能識別你要調用哪個子類的方法。這也正是著名的菱形問題,見下圖。

ClassC 同時繼承了 ClassA 和 ClassB,ClassC 的對象在調用 ClassA 和 ClassB 中重載的方法時,就不知道該調用 ClassA 的方法,還是 ClassB 的方法。

接口沒有這方面的困擾。來定義兩個接口,Fly 會飛,Run 會跑。

public interface Fly {
    void fly();
}
public interface Run {
    void run();
}

然後讓一個類同時實現這兩個接口。

public class Pig implements Fly,Run{
    @Override
    public void fly() {
        System.out.println("會飛的豬");
    }

    @Override
    public void run() {
        System.out.println("會跑的豬");
    }
}

這就在某種形式上達到了多重繼承的目的:現實世界里,豬的確只會跑,但在雷軍的眼裡,站在風口的豬就會飛,這就需要賦予這隻豬更多的能力,通過抽象類是無法實現的,只能通過接口。

3)實現多態。

什麼是多態呢?通俗的理解,就是同一個事件發生在不同的對象上會產生不同的結果,鼠標左鍵點擊窗口上的 X 號可以關閉窗口,點擊超鏈接卻可以打開新的網頁。

多態可以通過繼承(extends)的關係實現,也可以通過接口的形式實現。來看這樣一個例子。

Shape 是表示一個形狀。

public interface Shape {
    String name();
}

圓是一個形狀。

public class Circle implements Shape {
    @Override
    public String name() {
        return "圓";
    }
}

正方形也是一個形狀。

public class Square implements Shape {
    @Override
    public String name() {
        return "正方形";
    }
}

然後來看測試類。

List<Shape> shapes = new ArrayList<>();
Shape circleShape = new Circle();
Shape squareShape = new Square();

shapes.add(circleShape);
shapes.add(squareShape);

for (Shape shape : shapes) {
    System.out.println(shape.name());
}

多態的存在 3 個前提:

1、要有繼承關係,Circle 和 Square 都實現了 Shape 接口
2、子類要重寫父類的方法,Circle 和 Square 都重寫了 name() 方法
3、父類引用指向子類對象,circleShape 和 squareShape 的類型都為 Shape,但前者指向的是 Circle 對象,後者指向的是 Square 對象。

然後,我們來看一下測試結果:


正方形

也就意味着,儘管在 for 循環中,shape 的類型都為 Shape,但在調用 name() 方法的時候,它知道 Circle 對象應該調用 Circle 類的 name() 方法,Square 對象應該調用 Square 類的 name() 方法。

04、接口與抽象類的區別

好了,關於接口的一切,你應該都搞清楚了。現在回到讀者春夏秋冬的那條留言,“兄弟,說說抽象類和接口之間的區別?”

1)語法層面上

  • 接口中不能有 public 和 protected 修飾的方法,抽象類中可以有。
  • 接口中的變量只能是隱式的常量,抽象類中可以有任意類型的變量。
  • 一個類只能繼承一個抽象類,但卻可以實現多個接口。

2)設計層面上

抽象類是對類的一種抽象,繼承抽象類的類和抽象類本身是一種 is-a 的關係。

接口是對類的某種行為的一種抽象,接口和類之間並沒有很強的關聯關係,所有的類都可以實現 Serializable 接口,從而具有序列化的功能。

就這麼多吧,能說道這份上,我相信面試官就不會為難你了。

十一、Java 繼承

在 Java 中,一個類可以繼承另外一個類或者實現多個接口,我想這一點,大部分的讀者應該都知道了。還有一點,我不確定大家是否知道,就是一個接口也可以繼承另外一個接口,就像下面這樣:

public interface OneInterface extends Cloneable {
}

這樣做有什麼好處呢?我想有一部分讀者應該已經猜出來了,就是實現了 OneInterface 接口的類,也可以使用 Object.clone() 方法了。

public class TestInterface implements OneInterface {
    public static void main(String[] args) throws CloneNotSupportedException {
        TestInterface c1 = new TestInterface();
        TestInterface c2 = (TestInterface) c1.clone();
    }
}

除此之外,我們還可以在 OneInterface 接口中定義其他一些抽象方法(比如說深拷貝),使該接口擁有 Cloneable 所不具有的功能。

public interface OneInterface extends Cloneable {
    void deepClone();
}

看到了吧?這就是繼承的好處:子接口擁有了父接口的方法,使得子接口具有了父接口相同的行為;同時,子接口還可以在此基礎上自由發揮,添加屬於自己的行為

以上,把“接口”換成“類”,結論同樣成立。讓我們來定義一個普通的父類 Wanger:

public class Wanger {
    int age;
    String name;
    void write() {
        System.out.println("我寫了本《基督山伯爵》");
    }
}

然後,我們再來定義一個子類 Wangxiaoer,使用關鍵字 extends 來繼承父類 Wanger:

public class Wangxiaoer extends Wanger{
    @Override
    void write() {
        System.out.println("我寫了本《茶花女》");
    }
}

我們可以將通用的方法和成員變量放在父類中,達到代碼復用的目的;然後將特殊的方法和成員變量放在子類中,除此之外,子類還可以覆蓋父類的方法(比如write() 方法)。這樣,子類也就煥發出了新的生命力。

Java 只支持單一繼承,這一點,我在上一篇接口的文章中已經提到過了。如果一個類在定義的時候沒有使用 extends 關鍵字,那麼它隱式地繼承了 java.lang.Object 類——在我看來,這恐怕就是 Java 號稱萬物皆對象的真正原因了。

那究竟子類繼承了父類的什麼呢?

子類可以繼承父類的非 private 成員變量,為了驗證這一點,我們來看下面這個示例。

public class Wanger {
    String defaultName;
    private String privateName;
    public String publicName;
    protected String protectedName;
}

父類 Wanger 定義了四種類型的成員變量,缺省的 defaultName、私有的 privateName、共有的 publicName、受保護的 protectedName。

在子類 Wangxiaoer 中定義一個測試方法 testVariable()

可以確認,除了私有的 privateName,其他三種類型的成員變量都可以繼承到。

同理,子類可以繼承父類的非 private 方法,為了驗證這一點,我們來看下面這個示例。

public class Wanger {
    void write() {
    }

    private void privateWrite() {
    }

    public void publicWrite() {
    }

    protected void protectedWrite() {
    }
}

父類 Wanger 定義了四種類型的方法,缺省的 write、私有的 privateWrite()、共有的 publicWrite()、受保護的 protectedWrite()。

在子類 Wangxiaoer 中定義一個 main 方法,並使用 new 關鍵字新建一個子類對象:

可以確認,除了私有的 privateWrite(),其他三種類型的方法都可以繼承到。

不過,子類無法繼承父類的構造方法。如果父類的構造方法是帶有參數的,代碼如下所示:

public class Wanger {
    int age;
    String name;

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

則必須在子類的構造器中顯式地通過 super 關鍵字進行調用,否則編譯器將提示以下錯誤:

修復后的代碼如下所示:

public class Wangxiaoer extends Wanger{
    public Wangxiaoer(int age, String name) {
        super(age, name);
    }
}

is-a 是繼承的一個明顯特徵,就是說子類的對象引用類型可以是一個父類類型。

public class Wangxiaoer extends Wanger{
    public static void main(String[] args) {
        Wanger wangxiaoer = new Wangxiaoer();
    }
}

同理,子接口的實現類的對象引用類型也可以是一個父接口類型。

public interface OneInterface extends Cloneable {
}
public class TestInterface implements OneInterface {
    public static void main(String[] args) {
        Cloneable c1 = new TestInterface();
    }
}

儘管一個類只能繼承一個類,但一個類卻可以實現多個接口,這一點,我在上一篇文章也提到過了。另外,還有一點我也提到了,就是 Java 8 之後,接口中可以定義 default 方法,這很方便,但也帶來了新的問題:

如果一個類實現了多個接口,而這些接口中定義了相同簽名的 default 方法,那麼這個類就要重寫該方法,否則編譯無法通過。

FlyInterface 是一個會飛的接口,裏面有一個簽名為 sleep() 的默認方法:

public interface FlyInterface {
    void fly();
    default void sleep() {
        System.out.println("睡着飛");
    }
}

RunInterface 是一個會跑的接口,裏面也有一個簽名為 sleep() 的默認方法:

public interface RunInterface {
    void run();
    default void sleep() {
        System.out.println("睡着跑");
    }
}

Pig 類實現了 FlyInterface 和 RunInterface 兩個接口,但這時候編譯出錯了。

原本,default 方法就是為實現該接口而不覆蓋該方法的類提供默認實現的,現在,相同方法簽名的 sleep() 方法把編譯器搞懵逼了,只能重寫了。

public class Pig implements FlyInterfaceRunInterface {

    @Override
    public void fly() {
        System.out.println("會飛的豬");
    }

    @Override
    public void sleep() {
        System.out.println("只能重寫了");
    }

    @Override
    public void run() {
        System.out.println("會跑的豬");
    }
}

類雖然不能繼承多個類,但接口卻可以繼承多個接口,這一點,我不知道有沒有觸及到一些讀者的知識盲區。

public interface WalkInterface extends FlyInterface,RunInterface{
    void walk();
}

十二、this 關鍵字

在 Java 中,this 關鍵字指的是當前對象(它的方法正在被調用)的引用,能理解吧,各位親?不理解的話,我們繼續往下看。

看完再不明白,你過來捶爆我,我保證不還手,只要不打臉。

01、消除字段歧義

我敢賭一毛錢,所有的讀者,不管男女老少,應該都知道這種用法,畢竟寫構造方法的時候經常用啊。誰要不知道,過來,我給你發一毛錢紅包,只要你臉皮夠厚。

public class Writer {
    private int age;
    private String name;

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

Writer 類有兩個成員變量,分別是 age 和 name,在使用有參構造函數的時候,如果參數名和成員變量的名字相同,就需要使用 this 關鍵字消除歧義:this.age 是指成員變量,age 是指構造方法的參數。

02、引用類的其他構造方法

當一個類的構造方法有多個,並且它們之間有交集的話,就可以使用 this 關鍵字來調用不同的構造方法,從而減少代碼量。

比如說,在無參構造方法中調用有參構造方法:

public class Writer {
    private int age;
    private String name;

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

    public Writer() {
        this(18"沉默王二");
    }
}

也可以在有參構造方法中調用無參構造方法:

public class Writer {
    private int age;
    private String name;

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

    public Writer() {
    }
}

需要注意的是,this() 必須是構造方法中的第一條語句,否則就會報錯。

03、作為參數傳遞

在下例中,有一個無參的構造方法,裏面調用了 print() 方法,參數只有一個 this 關鍵字。

public class ThisTest {
    public ThisTest() {
        print(this);
    }

    private void print(ThisTest thisTest) {
        System.out.println("print " +thisTest);
    }

    public static void main(String[] args) {
        ThisTest test = new ThisTest();
        System.out.println("main " + test);
    }
}

來打印看一下結果:

print com.cmower.baeldung.this1.ThisTest@573fd745
main com.cmower.baeldung.this1.ThisTest@573fd745

從結果中可以看得出來,this 就是我們在 main() 方法中使用 new 關鍵字創建的 ThisTest 對象。

04、鏈式調用

學過 JavaScript,或者 jQuery 的讀者可能對鏈式調用比較熟悉,類似於 a.b().c().d(),彷彿能無窮無盡調用下去。

在 Java 中,對應的專有名詞叫 Builder 模式,來看一個示例。

public class Writer {
    private int age;
    private String name;
    private String bookName;

    public Writer(WriterBuilder builder) {
        this.age = builder.age;
        this.name = builder.name;
        this.bookName = builder.bookName;
    }

    public static class WriterBuilder {
        public String bookName;
        private int age;
        private String name;

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

        public WriterBuilder writeBook(String bookName) {
            this.bookName = bookName;
            return this;
        }

        public Writer build() {
            return new Writer(this);
        }
    }
}

Writer 類有三個成員變量,分別是 age、name 和 bookName,還有它們仨對應的一個構造方法,參數是一個內部靜態類 WriterBuilder。

內部類 WriterBuilder 也有三個成員變量,和 Writer 類一致,不同的是,WriterBuilder 類的構造方法裏面只有 age 和 name 賦值了,另外一個成員變量 bookName 通過單獨的方法 writeBook() 來賦值,注意,該方法的返回類型是 WriterBuilder,最後使用 return 返回了 this 關鍵字。

最後的 build() 方法用來創建一個 Writer 對象,參數為 this 關鍵字,也就是當前的 WriterBuilder 對象。

這時候,創建 Writer 對象就可以通過鏈式調用的方式。

Writer writer = new Writer.WriterBuilder(18,"沉默王二")
                .writeBook("《Web全棧開發進階之路》")
                .build();

05、在內部類中訪問外部類對象

說實話,自從 Java 8 的函數式編程出現后,就很少用到 this 在內部類中訪問外部類對象了。來看一個示例:

public class ThisInnerTest {
    private String name;

    class InnerClass {
        public InnerClass() {
            ThisInnerTest thisInnerTest = ThisInnerTest.this;
            String outerName = thisInnerTest.name;
        }
    }
}

在內部類 InnerClass 的構造方法中,通過外部類.this 可以獲取到外部類對象,然後就可以使用外部類的成員變量了,比如說 name。

十三、super 關鍵字

簡而言之,super 關鍵字就是用來訪問父類的。

先來看父類:

public class SuperBase {
    String message = "父類";

    public SuperBase(String message) {
        this.message = message;
    }

    public SuperBase() {
    }

    public void printMessage() {
        System.out.println(message);
    }
}

再來看子類:

public class SuperSub extends SuperBase {
    String message = "子類";

    public SuperSub(String message) {
        super(message);
    }

    public SuperSub() {
        super.printMessage();
        printMessage();
    }

    public void getParentMessage() {
        System.out.println(super.message);
    }

    public void printMessage() {
        System.out.println(message);
    }
}

1)super 關鍵字可用於訪問父類的構造方法

你看,子類可以通過 super(message) 來調用父類的構造方法。現在來新建一個 SuperSub 對象,看看輸出結果是什麼:

SuperSub superSub = new SuperSub("子類的message");

new 關鍵字在調用構造方法創建子類對象的時候,會通過 super 關鍵字初始化父類的 message,所以此此時父類的 message 會輸出“子類的message”。

2)super 關鍵字可以訪問父類的變量

上述例子中的 SuperSub 類中就有,getParentMessage() 通過 super.message 方法父類的同名成員變量 message。

3)當方法發生重寫時,super 關鍵字可以訪問父類的同名方法

上述例子中的 SuperSub 類中就有,無參的構造方法 SuperSub() 中就使用 super.printMessage() 調用了父類的同名方法。

十四、重寫和重載

先來看一段重寫的代碼吧。

class LaoWang{
    public void write() {
        System.out.println("老王寫了一本《基督山伯爵》");
    }
}
public class XiaoWang extends LaoWang {
    @Override
    public void write() {
        System.out.println("小王寫了一本《茶花女》");
    }
}

重寫的兩個方法名相同,方法參數的個數也相同;不過一個方法在父類中,另外一個在子類中。就好像父類 LaoWang 有一個 write() 方法(無參),方法體是寫一本《基督山伯爵》;子類 XiaoWang 重寫了父類的 write() 方法(無參),但方法體是寫一本《茶花女》。

來寫一段測試代碼。

public class OverridingTest {
    public static void main(String[] args) {
        LaoWang wang = new XiaoWang();
        wang.write();
    }
}

大家猜結果是什麼?

小王寫了一本《茶花女》

在上面的代碼中,們聲明了一個類型為 LaoWang 的變量 wang。在編譯期間,編譯器會檢查 LaoWang 類是否包含了 write() 方法,發現 LaoWang 類有,於是編譯通過。在運行期間,new 了一個 XiaoWang 對象,並將其賦值給 wang,此時 Java 虛擬機知道 wang 引用的是 XiaoWang 對象,所以調用的是子類 XiaoWang 中的 write() 方法而不是父類 LaoWang 中的 write() 方法,因此輸出結果為“小王寫了一本《茶花女》”。

再來看一段重載的代碼吧。

class LaoWang{
    public void read() {
        System.out.println("老王讀了一本《Web全棧開發進階之路》");
    }

    public void read(String bookname) {
        System.out.println("老王讀了一本《" + bookname + "》");
    }
}

重載的兩個方法名相同,但方法參數的個數不同,另外也不涉及到繼承,兩個方法在同一個類中。就好像類 LaoWang 有兩個方法,名字都是 read(),但一個有參數(書名),另外一個沒有(只能讀寫死的一本書)。

來寫一段測試代碼。

public class OverloadingTest {
    public static void main(String[] args) {
        LaoWang wang = new LaoWang();
        wang.read();
        wang.read("金");
    }
}

這結果就不用猜了。變量 wang 的類型為 LaoWang,wang.read() 調用的是無參的 read() 方法,因此先輸出“老王讀了一本《Web全棧開發進階之路》”;wang.read("金") 調用的是有參的 read(bookname) 方法,因此後輸出“老王讀了一本《》”。在編譯期間,編譯器就知道這兩個 read() 方法時不同的,因為它們的方法簽名(=方法名稱+方法參數)不同。

簡單的來總結一下:

1)編譯器無法決定調用哪個重寫的方法,因為只從變量的類型上是無法做出判斷的,要在運行時才能決定;但編譯器可以明確地知道該調用哪個重載的方法,因為引用類型是確定的,參數個數決定了該調用哪個方法。

2)多態針對的是重寫,而不是重載。

哎,後悔啊,早年我要是能把這道面試題吃透的話,也不用被老馬刁難了。吟一首詩感慨一下人生吧。

青青園中葵,朝露待日晞。
陽春布德澤,萬物生光輝。
常恐秋節至,焜黃華恭弘=叶 恭弘衰。
百川東到海,何時復西歸?
少壯不努力,老大徒傷悲

另外,我想要告訴大家的是,重寫(Override)和重載(Overload)是 Java 中兩個非常重要的概念,新手經常會被它們倆迷惑,因為它們倆的英文名字太像了,中文翻譯也只差一個字。難,太難了。

十五、static 關鍵字

先來個提綱挈領(唉呀媽呀,成語區博主上線了)吧:

static 關鍵字可用於變量、方法、代碼塊和內部類,表示某個特定的成員只屬於某個類本身,而不是該類的某個對象。

01、靜態變量

靜態變量也叫類變量,它屬於一個類,而不是這個類的對象。

public class Writer {
    private String name;
    private int age;
    public static int countOfWriters;

    public Writer(String name, int age) {
        this.name = name;
        this.age = age;
        countOfWriters++;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

其中,countOfWriters 被稱為靜態變量,它有別於 name 和 age 這兩個成員變量,因為它前面多了一個修飾符 static

這意味着無論這個類被初始化多少次,靜態變量的值都會在所有類的對象中共享。

Writer w1 = new Writer("沉默王二",18);
Writer w2 = new Writer("沉默王三",16);

System.out.println(Writer.countOfWriters);

按照上面的邏輯,你應該能推理得出,countOfWriters 的值此時應該為 2 而不是 1。從內存的角度來看,靜態變量將會存儲在 Java 虛擬機中一個名叫“Metaspace”(元空間,Java 8 之後)的特定池中。

靜態變量和成員變量有着很大的不同,成員變量的值屬於某個對象,不同的對象之間,值是不共享的;但靜態變量不是的,它可以用來統計對象的數量,因為它是共享的。就像上面例子中的 countOfWriters,創建一個對象的時候,它的值為 1,創建兩個對象的時候,它的值就為 2。

簡單小結一下:

1)由於靜態變量屬於一個類,所以不要通過對象引用來訪問,而應該直接通過類名來訪問;

2)不需要初始化類就可以訪問靜態變量。

public class WriterDemo {
    public static void main(String[] args) {
        System.out.println(Writer.countOfWriters); // 輸出 0
    }
}

02、靜態方法

靜態方法也叫類方法,它和靜態變量類似,屬於一個類,而不是這個類的對象。

public static void setCountOfWriters(int countOfWriters) {
    Writer.countOfWriters = countOfWriters;
}

setCountOfWriters() 就是一個靜態方法,它由 static 關鍵字修飾。

如果你用過 java.lang.Math 類或者 Apache 的一些工具類(比如說 StringUtils)的話,對靜態方法一定不會感動陌生。

Math 類的幾乎所有方法都是靜態的,可以直接通過類名來調用,不需要創建類的對象。

簡單小結一下:

1)Java 中的靜態方法在編譯時解析,因為靜態方法不能被重寫(方法重寫發生在運行時階段,為了多態)。

2)抽象方法不能是靜態的。

3)靜態方法不能使用 this 和 super 關鍵字。

4)成員方法可以直接訪問其他成員方法和成員變量。

5)成員方法也可以直接方法靜態方法和靜態變量。

6)靜態方法可以訪問所有其他靜態方法和靜態變量。

7)靜態方法無法直接訪問成員方法和成員變量。

03、靜態代碼塊

靜態代碼塊可以用來初始化靜態變量,儘管靜態方法也可以在聲明的時候直接初始化,但有些時候,我們需要多行代碼來完成初始化。

public class StaticBlockDemo {
    public static List<String> writes = new ArrayList<>();

    static {
        writes.add("沉默王二");
        writes.add("沉默王三");
        writes.add("沉默王四");

        System.out.println("第一塊");
    }

    static {
        writes.add("沉默王五");
        writes.add("沉默王六");

        System.out.println("第二塊");
    }
}

writes 是一個靜態的 ArrayList,所以不太可能在聲明的時候完成初始化,因此需要在靜態代碼塊中完成初始化。

簡單小結一下:

1)一個類可以有多個靜態代碼塊。

2)靜態代碼塊的解析和執行順序和它在類中的位置保持一致。為了驗證這個結論,可以在 StaticBlockDemo 類中加入空的 main 方法,執行完的結果如下所示:

第一塊
第二塊

04、靜態內部類

Java 允許我們在一個類中聲明一個內部類,它提供了一種令人信服的方式,允許我們只在一個地方使用一些變量,使代碼更具有條理性和可讀性。

常見的內部類有四種,成員內部類、局部內部類、匿名內部類和靜態內部類,限於篇幅原因,前三種不在我們本次文章的討論範圍,以後有機會再細說。

public class Singleton {
    private Singleton() {}

    private static class SingletonHolder {
        public static final Singleton instance = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
}

以上這段代碼是不是特別熟悉,對,這就是創建單例的一種方式,第一次加載 Singleton 類時並不會初始化 instance,只有第一次調用 getInstance() 方法時 Java 虛擬機才開始加載 SingletonHolder 並初始化 instance,這樣不僅能確保線程安全也能保證 Singleton 類的唯一性。不過,創建單例更優雅的一種方式是使用枚舉。

簡單小結一下:

1)靜態內部類不能訪問外部類的所有成員變量。

2)靜態內部類可以訪問外部類的所有靜態變量,包括私有靜態變量。

3)外部類不能聲明為 static。

十六、Java 枚舉

開門見山地說吧,enum(枚舉)是 Java 1.5 時引入的關鍵字,它表示一種特殊類型的類,默認繼承自 java.lang.Enum。

為了證明這一點,我們來新建一個枚舉 PlayerType:

public enum PlayerType {
    TENNIS,
    FOOTBALL,
    BASKETBALL
}

兩個關鍵字帶一個類名,還有大括號,以及三個大寫的單詞,但沒看到繼承 Enum 類啊?別著急,心急吃不了熱豆腐啊。使用 JAD 查看一下反編譯后的字節碼,就一清二楚了。

public final class PlayerType extends Enum
{

    public static PlayerType[] values()
    {
        return (PlayerType[])$VALUES.clone();
    }

    public static PlayerType valueOf(String name)
    
{
        return (PlayerType)Enum.valueOf(com/cmower/baeldung/enum1/PlayerType, name);
    }

    private PlayerType(String s, int i)
    
{
        super(s, i);
    }

    public static final PlayerType TENNIS;
    public static final PlayerType FOOTBALL;
    public static final PlayerType BASKETBALL;
    private static final PlayerType $VALUES[];

    static 
    {
        TENNIS = new PlayerType("TENNIS"0);
        FOOTBALL = new PlayerType("FOOTBALL"1);
        BASKETBALL = new PlayerType("BASKETBALL"2);
        $VALUES = (new PlayerType[] {
            TENNIS, FOOTBALL, BASKETBALL
        });
    }
}

看到沒?PlayerType 類是 final 的,並且繼承自 Enum 類。這些工作我們程序員沒做,編譯器幫我們悄悄地做了。此外,它還附帶幾個有用靜態方法,比如說 values()valueOf(String name)

01、內部枚舉

好的,小夥伴們應該已經清楚枚舉長什麼樣子了吧?既然枚舉是一種特殊的類,那它其實是可以定義在一個類的內部的,這樣它的作用域就可以限定於這個外部類中使用。

public class Player {
    private PlayerType type;
    public enum PlayerType {
        TENNIS,
        FOOTBALL,
        BASKETBALL
    }

    public boolean isBasketballPlayer() {
      return getType() == PlayerType.BASKETBALL;
    }

    public PlayerType getType() {
        return type;
    }

    public void setType(PlayerType type) {
        this.type = type;
    }
}

PlayerType 就相當於 Player 的內部類,isBasketballPlayer() 方法用來判斷運動員是否是一個籃球運動員。

由於枚舉是 final 的,可以確保在 Java 虛擬機中僅有一個常量對象(可以參照反編譯后的靜態代碼塊「static 關鍵字帶大括號的那部分代碼」),所以我們可以很安全地使用“==”運算符來比較兩個枚舉是否相等,參照 isBasketballPlayer() 方法。

那為什麼不使用 equals() 方法判斷呢?

if(player.getType().equals(Player.PlayerType.BASKETBALL)){};
if(player.getType() == Player.PlayerType.BASKETBALL){};

“==”運算符比較的時候,如果兩個對象都為 null,並不會發生 NullPointerException,而 equals() 方法則會。

另外, “==”運算符會在編譯時進行檢查,如果兩側的類型不匹配,會提示錯誤,而 equals() 方法則不會。

02、枚舉可用於 switch 語句

這個我在之前的一篇我去的文章中詳細地說明過了,感興趣的小夥伴可以點擊鏈接跳轉過去看一下。

switch (playerType) {
        case TENNIS:
            return "網球運動員費德勒";
        case FOOTBALL:
            return "足球運動員C羅";
        case BASKETBALL:
            return "籃球運動員詹姆斯";
        case UNKNOWN:
            throw new IllegalArgumentException("未知");
        default:
            throw new IllegalArgumentException(
                    "運動員類型: " + playerType);

    }

03、枚舉可以有構造方法

如果枚舉中需要包含更多信息的話,可以為其添加一些字段,比如下面示例中的 name,此時需要為枚舉添加一個帶參的構造方法,這樣就可以在定義枚舉時添加對應的名稱了。

public enum PlayerType {
    TENNIS("網球"),
    FOOTBALL("足球"),
    BASKETBALL("籃球");

    private String name;

    PlayerType(String name) {
        this.name = name;
    }
}

04、EnumSet

EnumSet 是一個專門針對枚舉類型的 Set 接口的實現類,它是處理枚舉類型數據的一把利器,非常高效(內部實現是位向量,我也搞不懂)。

因為 EnumSet 是一個抽象類,所以創建 EnumSet 時不能使用 new 關鍵字。不過,EnumSet 提供了很多有用的靜態工廠方法:

下面的示例中使用 noneOf() 創建了一個空的 PlayerType 的 EnumSet;使用 allOf() 創建了一個包含所有 PlayerType 的 EnumSet。

public class EnumSetTest {
    public enum PlayerType {
        TENNIS,
        FOOTBALL,
        BASKETBALL
    }

    public static void main(String[] args) {
        EnumSet<PlayerType> enumSetNone = EnumSet.noneOf(PlayerType.class);
        System.out.println(enumSetNone);

        EnumSet<PlayerType> enumSetAll = EnumSet.allOf(PlayerType.class);
        System.out.println(enumSetAll);
    }
}

程序輸出結果如下所示:

[]
[TENNIS, FOOTBALL, BASKETBALL]

有了 EnumSet 后,就可以使用 Set 的一些方法了:

05、EnumMap

EnumMap 是一個專門針對枚舉類型的 Map 接口的實現類,它可以將枚舉常量作為鍵來使用。EnumMap 的效率比 HashMap 還要高,可以直接通過數組下標(枚舉的 ordinal 值)訪問到元素。

和 EnumSet 不同,EnumMap 不是一個抽象類,所以創建 EnumMap 時可以使用 new 關鍵字:

EnumMap<PlayerType, String> enumMap = new EnumMap<>(PlayerType.class);

有了 EnumMap 對象后就可以使用 Map 的一些方法了:

和 HashMap 的使用方法大致相同,來看下面的例子:

EnumMap<PlayerType, String> enumMap = new EnumMap<>(PlayerType.class);
enumMap.put(PlayerType.BASKETBALL,"籃球運動員");
enumMap.put(PlayerType.FOOTBALL,"足球運動員");
enumMap.put(PlayerType.TENNIS,"網球運動員");
System.out.println(enumMap);

System.out.println(enumMap.get(PlayerType.BASKETBALL));
System.out.println(enumMap.containsKey(PlayerType.BASKETBALL));
System.out.println(enumMap.remove(PlayerType.BASKETBALL));

程序輸出結果如下所示:

{TENNIS=網球運動員, FOOTBALL=足球運動員, BASKETBALL=籃球運動員}
籃球運動員
true
籃球運動員

06、單例

通常情況下,實現一個單例並非易事,不信,來看下面這段代碼

public class Singleton {  
    private volatile static Singleton singleton; 
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {
        synchronized (Singleton.class) { 
        if (singleton == null) {  
            singleton = new Singleton(); 
        }  
        }  
    }  
    return singleton;  
    }  
}

但枚舉的出現,讓代碼量減少到極致:

public enum EasySingleton{
    INSTANCE;
}

完事了,真的超級短,有沒有?枚舉默認實現了 Serializable 接口,因此 Java 虛擬機可以保證該類為單例,這與傳統的實現方式不大相同。傳統方式中,我們必須確保單例在反序列化期間不能創建任何新實例。

07、枚舉可與數據庫交互

我們可以配合 Mybatis 將數據庫字段轉換為枚舉類型。現在假設有一個數據庫字段 check_type 的類型如下:

`check_type` int(1) DEFAULT NULL COMMENT '檢查類型(1:未通過、2:通過)',

它對應的枚舉類型為 CheckType,代碼如下:

public enum CheckType {
    NO_PASS(0"未通過"), PASS(1"通過");
    private int key;

    private String text;

    private CheckType(int key, String text) {
        this.key = key;
        this.text = text;
    }

    public int getKey() {
        return key;
    }

    public String getText() {
        return text;
    }

    private static HashMap<Integer,CheckType> map = new HashMap<Integer,CheckType>();
    static {
        for(CheckType d : CheckType.values()){
            map.put(d.key, d);
        }
    }

    public static CheckType parse(Integer index) {
        if(map.containsKey(index)){
            return map.get(index);
        }
        return null;
    }
}

1)CheckType 添加了構造方法,還有兩個字段,key 為 int 型,text 為 String 型。

2)CheckType 中有一個public static CheckType parse(Integer index)方法,可將一個 Integer 通過 key 的匹配轉化為枚舉類型。

那麼現在,我們可以在 Mybatis 的配置文件中使用 typeHandler 將數據庫字段轉化為枚舉類型。

<resultMap id="CheckLog" type="com.entity.CheckLog">
  <id property="id" column="id"/>
  <result property="checkType" column="check_type" typeHandler="com.CheckTypeHandler"></result>
</resultMap>

其中 checkType 字段對應的類如下:

public class CheckLog implements Serializable {

    private String id;
    private CheckType checkType;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public CheckType getCheckType() {
        return checkType;
    }

    public void setCheckType(CheckType checkType) {
        this.checkType = checkType;
    }
}

CheckTypeHandler 轉換器的類源碼如下:

public class CheckTypeHandler extends BaseTypeHandler<CheckType{

    @Override
    public CheckType getNullableResult(ResultSet rs, String index) throws SQLException {
        return CheckType.parse(rs.getInt(index));
    }

    @Override
    public CheckType getNullableResult(ResultSet rs, int index) throws SQLException {
        return CheckType.parse(rs.getInt(index));
    }

    @Override
    public CheckType getNullableResult(CallableStatement cs, int index) throws SQLException {
        return CheckType.parse(cs.getInt(index));
    }

    @Override
    public void setNonNullParameter(PreparedStatement ps, int index, CheckType val, JdbcType arg3) throws SQLException {
        ps.setInt(index, val.getKey());
    }
}

CheckTypeHandler 的核心功能就是調用 CheckType 枚舉類的 parse() 方法對數據庫字段進行轉換。

恕我直言,我覺得小夥伴們肯定會用 Java 枚舉了,如果還不會,就過來砍我!

十七、final 關鍵字

儘管繼承可以讓我們重用現有代碼,但有時處於某些原因,我們確實需要對可擴展性進行限制,final 關鍵字可以幫助我們做到這一點。

01、final 類

如果一個類使用了 final 關鍵字修飾,那麼它就無法被繼承。如果小夥伴們細心觀察的話,Java 就有不少 final 類,比如說最常見的 String 類。

public final class String
    implements java.io.SerializableComparable<String>, CharSequence,
               ConstableConstantDesc 
{}

為什麼 String 類要設計成 final 的呢?原因大致有以下三個:

  • 為了實現字符串常量池
  • 為了線程安全
  • 為了 HashCode 的不可變性

更詳細的原因,可以查看我之前寫的一篇文章。

任何嘗試從 final 類繼承的行為將會引發編譯錯誤,為了驗證這一點,我們來看下面這個例子,Writer 類是 final 的。

public final class Writer {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

嘗試去繼承它,編譯器會提示以下錯誤,Writer 類是 final 的,無法繼承。

不過,類是 final 的,並不意味着該類的對象是不可變的。

Writer writer = new Writer();
writer.setName("沉默王二");
System.out.println(writer.getName()); // 沉默王二

Writer 的 name 字段的默認值是 null,但可以通過 settter 方法將其更改為“沉默王二”。也就是說,如果一個類只是 final 的,那麼它並不是不可變的全部條件。

如果,你想了解不可變類的全部真相,請查看我之前寫的文章這次要說不明白immutable類,我就怎麼地。突然發現,寫系列文章真的妙啊,很多相關性的概念全部涉及到了。我真服了自己了。

把一個類設計成 final 的,有其安全方面的考慮,但不應該故意為之,因為把一個類定義成 final 的,意味着它沒辦法繼承,假如這個類的一些方法存在一些問題的話,我們就無法通過重寫的方式去修復它。

02、final 方法

被 final 修飾的方法不能被重寫。如果我們在設計一個類的時候,認為某些方法不應該被重寫,就應該把它設計成 final 的。

Thread 類就是一個例子,它本身不是 final 的,這意味着我們可以擴展它,但它的 isAlive() 方法是 final 的:

public class Thread implements Runnable {
    public final native boolean isAlive();
}

需要注意的是,該方法是一個本地(native)方法,用於確認線程是否處於活躍狀態。而本地方法是由操作系統決定的,因此重寫該方法並不容易實現。

Actor 類有一個 final 方法 show()

public class Actor {
    public final void show() {

    }
}

當我們想要重寫該方法的話,就會出現編譯錯誤:

如果一個類中的某些方法要被其他方法調用,則應考慮事被調用的方法稱為 final 方法,否則,重寫該方法會影響到調用方法的使用。

一個類是 final 的,和一個類不是 final,但它所有的方法都是 final 的,考慮一下,它們之間有什麼區別?

我能想到的一點,就是前者不能被繼承,也就是說方法無法被重寫;後者呢,可以被繼承,然後追加一些非 final 的方法。沒毛病吧?看把我聰明的。

03、final 變量

被 final 修飾的變量無法重新賦值。換句話說,final 變量一旦初始化,就無法更改。之前被一個小夥伴問過,什麼是 effective final,什麼是 final,這一點,我在之前的文章也有闡述過,所以這裏再貼一下地址:

http://www.itwanger.com/java/2020/02/14/java-final-effectively.html

1)final 修飾的基本數據類型

來聲明一個 final 修飾的 int 類型的變量:

final int age = 18;

嘗試將它修改為 30,結果編譯器生氣了:

2)final 修飾的引用類型

現在有一個普通的類 Pig,它有一個字段 name:

public class Pig {
   private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

在測試類中聲明一個 final 修飾的 Pig 對象:

 final Pig pig = new Pig();

如果嘗試將 pig 重新賦值的話,編譯器同樣會生氣:

但我們仍然可以去修改 Pig 的字段值:

final Pig pig = new Pig();
pig.setName("特立獨行");
System.out.println(pig.getName()); // 特立獨行

3)final 修飾的字段

final 修飾的字段可以分為兩種,一種是 static 的,另外一種是沒有 static 的,就像下面這樣:

public class Pig {
   private final int age = 1;
   public static final double PRICE = 36.5;
}

非 static 的 final 字段必須有一個默認值,否則編譯器將會提醒沒有初始化:

static 的 final 字段也叫常量,它的名字應該為大寫,可以在聲明的時候初始化,也可以通過 static [代碼塊初始化]()。

4) final 修飾的參數

final 關鍵字還可以修飾參數,它意味着參數在方法體內不能被再修改:

public class ArgFinalTest {
    public void arg(final int age) {
    }

    public void arg1(final String name) {
    }
}

如果嘗試去修改它的話,編譯器會提示以下錯誤:

。。。。。。

後續還會繼續更新,但有些小夥伴可能就忍不住了,這份小白手冊有沒有 PDF 版可以白嫖啊,那必須得有啊,直接「沉默王二」公眾號後台回復「小白」就可以了,不要手軟,覺得不錯的,請多多分享——贈人玫瑰,手有餘香哦。

沒關注的話,掃描上面的二維碼就可以了,然後回復「小白」。

我是沉默王二,一枚有顏值卻靠才華苟且的程序員。關注即可提升學習效率,別忘了三連啊,點贊、收藏、留言,我不挑,嘻嘻

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

難以想像!座頭鯨游進澳洲內陸30公里 專家憂被鱷魚吃掉

摘錄自2020年9月14日自由時報報導

澳洲卡卡杜國家公園的東鱷魚河,近來有座頭鯨迷路游進去深入內陸30公里,導致鱷魚和鯨魚共存的狀況出現,專家憂心一旦鯨魚擱淺,可能就會成為鱷魚現成的美食。

據《BBC》報導,海洋生態學家帕爾默(Carole Palmer)表示,這是澳洲從未發生過的事情。由於這隻座頭鯨的身長達到16公尺,體型相當龐大,因此帕爾默認為在一般狀況下牠不會被河裡眾多的鱷魚所干擾,但假如座頭鯨擱淺在淺水區那就不一樣了,畢竟人力不可能迅速移動那麼大隻的座頭鯨,鱷魚這時就會趁機而入。

目前並不清楚為什麼這些座頭鯨會迷路,鯨魚通常會在春季到較溫暖的海域生活,以便養育後代,之後才會返回南極海域覓食,因此有人推測座頭鯨在往南游的過程中可能誤入河口,就這樣游到了東鱷魚河上游。

生物多樣性
國際新聞
澳洲
座頭鯨
鱷魚

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

內在藏有硬實力!20萬起買這款SUV很超值!

發動機:1。8T/2。0T最大馬力(ps):180/186/220最大扭矩(Nm):300/320/350變速箱:7DCT驅動方式:前置前驅/前置四驅底盤懸挂:麥弗遜式獨立懸架/多連桿獨立懸挂動態表現應該算是車主們對柯迪亞克最滿意的一個方面吧。在此前的對比評測中,無論是加速、行駛平順性、油耗表現,還是剎車的成績,柯迪亞克都算得上是同級數一數二的水平。

曾經有不少想要買車的朋友說,20萬以下不買SUV,因為只有去到中型SUV的級別,才有SUV應有的空間和通過性。而現在市場上有許多合資中型SUV的空間真的讓人垂涎欲滴,比如日系的豐田漢蘭達和本田冠道,也有德系的大眾途觀L和美系的別克昂科威。選擇真的挺多。

然而在這麼多完美選擇之中,卻有人選擇了比較非主流的斯柯達-柯迪亞克,這車買得到底值不值?一起看看車主怎說吧!

長寬高:4698*1883*1676mm

軸距:2791mm

定位:中型SUV

雖然斯柯達在中國的品牌影響力比不上大眾或豐田這些一線品牌,可是在車型設計上也算不上遜色,車主們對整體外觀設計還是比較滿意的。前臉的“凹”字型中網是家族式設計比較顯著的特徵,上中貫穿着兩大燈,下進氣格柵很舒展,整體前臉比較霸氣;車身側面沒有太多設計亮點,不過整體還算協調,車尾設計也比較低調。

內飾風格很簡潔,很居家,大面積的木飾板看上去很有質感,儀錶盤採用的綠色點綴也是斯柯達比較標誌性的設計,個人感覺很具特色,呈現出比較“清新”的風格。柯迪亞克的外觀內飾設計沒有漢蘭達和冠道那麼具有吸引力,也可以體驗出車主們都比較喜歡低調點的風格。

發動機:1.8T/2.0T

最大馬力(ps):180/186/220

最大扭矩(Nm):300/320/350

變速箱:7DCT

驅動方式:前置前驅/前置四驅

底盤懸挂:麥弗遜式獨立懸架/多連桿獨立懸挂

動態表現應該算是車主們對柯迪亞克最滿意的一個方面吧。在此前的對比評測中,無論是加速、行駛平順性、油耗表現,還是剎車的成績,柯迪亞克都算得上是同級數一數二的水平。

1米8的試乘員坐在前排調整到舒適坐姿,頭部空間為4指,保持前排坐姿不變,同一位試乘員坐進後排,頭部空間為4指,腿部空間大於兩拳。在同級車型中,柯迪亞克的乘坐空間表現其實比較中庸,不過除了動力方面,車主們表示最為滿意的是其空間。不過一般的中型SUV,其實空間表現都能滿足大部分人的需求吧。

從車主口碑中,我們可以看出,動力和空間是車主們最滿意的方面,其外觀也是喜歡的人特別喜歡。而最不滿意的基本都是配置方面比較遜色。

如果從價位出發,柯迪亞克瞄準的競爭對手應該是昂科威和探界者,不過也比較難體現出競爭力,最主要還是在動力方面有一定的優勢。所以大多數選擇柯迪亞克的車主們都是奔着自己喜歡的外觀內飾,不錯的動力表現,以及相對於冠道和途觀L更低的售價。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

多線程高併發編程(10) — ConcurrentHashMap源碼分析

  一.背景

  前文講了HashMap的源碼分析,從中可以看到下面的問題:

  • HashMap的put/remove方法不是線程安全的,如果在多線程併發環境下,使用synchronized進行加鎖,會導致效率低下;
  • 在遍歷迭代獲取時進行修改(put/remove)操作,會導致發生併發修改異常(ConcurrentModificationException);
  • 在JDK1.7之前,對HashMap進行put添加操作,會導致鏈表反轉,造成鏈表迴路,從而發生get死循環,(當然這個問題在JDK1.8被改進了按照原鏈表順序進行重排移動);
  • 如果多個線程同時檢測到元素個數超過 數組大小 * loadFactor,這樣就會發生多個線程同時對數組進行擴容,都在重新計算元素位置以及複製數據,但是最終只有一個線程擴容后的數組會賦給 table,也就是說其他線程的都會丟失,並且各自線程 put 的數據也丟失;

  基於上述問題,都可以使用ConcurrentHashMap進行解決,ConcurrentHashMap使用分段鎖技術解決了併發訪問效率,在遍歷迭代獲取時進行修改操作也不會發生併發修改異常等等問題。

  二.源碼解析

  1. 構造方法:

    //最大容量大小
        private static final int MAXIMUM_CAPACITY = 1 << 30;
        //默認容量大小
        private static final int DEFAULT_CAPACITY = 16;
        /**
         *控制標識符,用來控制table的初始化和擴容的操作,不同的值有不同的含義
         *  多線程之間,以volatile方式讀取sizeCtl屬性,來判斷ConcurrentHashMap當前所處的狀態。
         *  通過cas設置sizeCtl屬性,告知其他線程ConcurrentHashMap的狀態變更
         *未初始化:
         *  sizeCtl=0:表示沒有指定初始容量。
         *  sizeCtl>0:表示初始容量。
         *初始化中:
         *  sizeCtl=-1,標記作用,告知其他線程,正在初始化
         *正常狀態:
         *  sizeCtl=0.75n ,擴容閾值
         *擴容中:
         *  sizeCtl < 0 : 表示有其他線程正在執行擴容
         *  sizeCtl = (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2 :表示此時只有一個線程在執行擴容
         */
        private transient volatile int sizeCtl;
        //併發級別
        private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
        //創建一個新的空map,默認大小是16
        public ConcurrentHashMap() {
        }
        public ConcurrentHashMap(int initialCapacity) {
            if (initialCapacity < 0)
                throw new IllegalArgumentException();
            //調整table的大小,tableSizeFor的實現查看前文HashMap源碼分析的構造方法模塊
            int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                       MAXIMUM_CAPACITY :
                       tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
            this.sizeCtl = cap;
        }
        public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
            this.sizeCtl = DEFAULT_CAPACITY;
            putAll(m);
        }
        public ConcurrentHashMap(int initialCapacity, float loadFactor) {
            this(initialCapacity, loadFactor, 1);
        }
        /**
         * concurrencyLevel:併發度,預估同時操作數據的線程數量
         * 表示能夠同時更新ConccurentHashMap且不產生鎖競爭的最大線程數。
         * 默認值為16,(即允許16個線程併發可能不會產生競爭)。
         */
        public ConcurrentHashMap(int initialCapacity,
                                 float loadFactor, int concurrencyLevel) {
            if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
                throw new IllegalArgumentException();
            //至少使用同樣多的桶容納同樣多的更新線程來操作元素
            if (initialCapacity < concurrencyLevel)   // Use at least as many bins
                initialCapacity = concurrencyLevel;   // as estimated threads
            long size = (long)(1.0 + (long)initialCapacity / loadFactor);
            int cap = (size >= (long)MAXIMUM_CAPACITY) ?
                MAXIMUM_CAPACITY : tableSizeFor((int)size);
            this.sizeCtl = cap;
        }
  2. put:

    public V put(K key, V value) {
            return putVal(key, value, false);
        }
        static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash普通節點哈希的可用位
        //把位數控制在int最大整數之內,h ^ (h >>> 16)的含義查看前文的put源碼解析
        static final int spread(int h) {
            return (h ^ (h >>> 16)) & HASH_BITS;
        }
        final V putVal(K key, V value, boolean onlyIfAbsent) {
            //key和value為空拋出異常
            if (key == null || value == null) throw new NullPointerException();
            //得到hash值
            int hash = spread(key.hashCode());
            int binCount = 0;
            //自旋對table進行遍歷
            for (Node<K,V>[] tab = table;;) {
                Node<K,V> f; int n, i, fh;
                //初始化table
                if (tab == null || (n = tab.length) == 0)
                    tab = initTable();
                //如果hash計算出的槽位元素為null,CAS將元素填充進當前槽位並結束遍歷
                else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                    if (casTabAt(tab, i, null,
                                 new Node<K,V>(hash, key, value, null)))
                        break;                   // no lock when adding to empty bin
                }
                //hash為-1,說明正在擴容,那麼就幫助其擴容。以加快速度
                else if ((fh = f.hash) == MOVED)
                    tab = helpTransfer(tab, f);
                else {
                    V oldVal = null;
                    synchronized (f) {// 同步 f 節點,防止增加鏈表的時候導致鏈表成環
                        if (tabAt(tab, i) == f) {// 如果對應的下標位置的節點沒有改變
                            if (fh >= 0) {//f節點的hash值大於0
                                binCount = 1;//鏈表初始長度
                                // 死循環,直到將值添加到鏈表尾部,並計算鏈表的長度
                                for (Node<K,V> e = f;; ++binCount) {
                                    K ek;
                                    //hash和key相同,值進行覆蓋
                                    if (e.hash == hash &&
                                        ((ek = e.key) == key ||
                                         (ek != null && key.equals(ek)))) {
                                        oldVal = e.val;
                                        if (!onlyIfAbsent)
                                            e.val = value;
                                        break;
                                    }
                                    Node<K,V> pred = e;
                                    //hash和key不同,添加到鏈表後面
                                    if ((e = e.next) == null) {
                                        pred.next = new Node<K,V>(hash, key,
                                                                  value, null);
                                        break;
                                    }
                                }
                            }
                            //是樹節點,添加到樹中
                            else if (f instanceof TreeBin) {
                                Node<K,V> p;
                                binCount = 2;
                                if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                               value)) != null) {
                                    oldVal = p.val;
                                    if (!onlyIfAbsent)
                                        p.val = value;
                                }
                            }
                        }
                    }
                     //如果節點添加到鏈表和樹中
                    if (binCount != 0) {
                        //鏈表長度大於等於8時,將鏈錶轉換成紅黑樹樹
                        if (binCount >= TREEIFY_THRESHOLD)
                            treeifyBin(tab, i);
                        if (oldVal != null)
                            return oldVal;
                        break;
                    }
                }
            }
            // 判斷是否需要擴容
            addCount(1L, binCount);
            return null;
        }
    1. initTable:初始化

      private final Node<K,V>[] initTable() {
              Node<K,V>[] tab; int sc;
              while ((tab = table) == null || tab.length == 0) {
                  //如果一個線程發現sizeCtl<0,意味着另外的線程執行CAS操作成功,當前線程只需要讓出cpu時間片,即保證只有一個線程初始化
                  //由於sizeCtl是volatile的,保證了順序性和可見性
                  if ((sc = sizeCtl) < 0)
                      Thread.yield(); // lost initialization race; just spin
                  else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {//cas操作判斷並置為-1
                      try {
                          if ((tab = table) == null || tab.length == 0) {
                              int n = (sc > 0) ? sc : DEFAULT_CAPACITY;//若沒有參數則默認容量為16
                              @SuppressWarnings("unchecked")
                              Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];//創建數組
                              table = tab = nt;//數組賦值給當前ConcurrentHashMap
                              //計算下一次元素到達擴容的閥值,如果n為16的話,那麼這裏 sc = 12,其實就是 0.75 * n
                              sc = n - (n >>> 2);
                          }
                      } finally {
                          sizeCtl = sc;
                      }
                      break;
                  }
              }
              return tab;
          }
    2. tabAt:尋找指定數組在內存中i位置的數據

      static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
              /**getObjectVolatile:獲取obj對象中offset偏移地址對應的object型field的值,支持volatile load語義。
               * 數組的尋址計算方式:a[i]_address = base_address + i * data_type_size
               * base_address:起始地址;i:索引;data_type_size:數據類型長度大小
               */
              return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
          }
    3. helpTransfer:幫助擴容

      private static int RESIZE_STAMP_BITS = 16;
          /**
           * numberOfLeadingZeros()的具體算法邏輯請參考:https://www.jianshu.com/p/2c1be41f6e59
           * numberOfLeadingZeros(n)返回的是n的二進制標識的從高位開始到第一個非0的数字的之間0的個數,比如numberOfLeadingZeros(8)返回的就是28 ,因為0000 0000 0000 0000 0000 0000 0000 1000在1前面有28個0
           * RESIZE_STAMP_BITS 的值是16,1 << (RESIZE_STAMP_BITS - 1)就是將1左移位15位,0000 0000 0000 0000 1000 0000 0000 0000
           * 然後將兩個数字再按位或,將相當於 將移位后的 兩個數相加。
           * 比如:
           * 8的二進製表示是: 0000 0000 0000 0000 0000 0000 0000 1000 = 8
           * 7的二進製表示是: 0000 0000 0000 0000 0000 0000 0000 0111 = 7
           * 按位或的結果是:  0000 0000 0000 0000 0000 0000 0000 1111 = 15
           * 相當於 8 + 7 =15
           * 為什麼會出現這種效果呢?因為8是2的整數次冪,也就是說8的二進製表示只會在某個高位上是1,其餘地位都是0,所以在按位或的時候,低位表示的全是7的位值,所以出現了這種效果。
           */
          static final int resizeStamp(int n) {
              return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
          }
          final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
              Node<K,V>[] nextTab; int sc;
               //如果table不是空,且node節點是轉移類型,且node節點的nextTable(新 table)不是空,嘗試幫助擴容
              if (tab != null && (f instanceof ForwardingNode) &&
                  (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
                  //根據length得到一個標識符號
                  int rs = resizeStamp(tab.length);
                   //如果nextTab沒有被併發修改,且tab也沒有被併發修改,且sizeCtl<0(說明還在擴容)
                  while (nextTab == nextTable && table == tab &&
                         (sc = sizeCtl) < 0) {
                      /**
                       * 如果 sizeCtl 無符號右移16不等於rs( sc前16位如果不等於標識符,則標識符變化了)
                       * 或者 sizeCtl == rs + 1(擴容結束了,不再有線程進行擴容)(默認第一個線程設置 sc ==rs 左移 16 位 + 2,當第一個線程結束擴容了,就會將 sc 減1。這個時候,sc 就等於 rs + 1)
                       * 或者 sizeCtl == rs + 65535  (如果達到最大幫助線程的數量,即 65535)
                       * 或者轉移下標正在調整 (擴容結束)
                       * 結束循環,返回 table
                       * 【即如果還在擴容,判斷標識符是否變化,判斷擴容是否結束,判斷是否達到最大線程數,判斷擴容轉移下標是否在調整(擴容結束),如果滿足任意條件,結束循環。】
                       */
                      if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                          sc == rs + MAX_RESIZERS || transferIndex <= 0)
                          break;
                      // 如果以上都不是, 將 sizeCtl + 1, (表示增加了一個線程幫助其擴容)
                      if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                          transfer(tab, nextTab);//進行擴容和數據遷移
                          break;
                      }
                  }
                  return nextTab;//返回擴容后的數組
              }
              return table;//沒有擴容,返回原數組
          }
    4.  transfer:擴容和數據遷移,採用多線程擴容,整個擴容過程,通過cas設置sizeCtl、transferIndex等變量協調多個線程進行併發擴容;

      1.  transferIndex屬性:
        //擴容索引,表示已經分配給擴容線程的table數組索引位置。主要用來協調多個線程,併發安全地獲取遷移任務(hash桶)。
        private transient volatile int transferIndex;
        1. 在擴容之前,transferIndex 在數組的最右邊 。此時有一個線程發現已經到達擴容閾值,準備開始擴容。

        2. 擴容線程,在遷移數據之前,首先要將transferIndex左移(以cas的方式修改 transferIndex=transferIndex-stride(要遷移hash桶的個數)),獲取遷移任務。每個擴容線程都會通過for循環+CAS的方式設置transferIndex,因此可以確保多線程擴容的併發安全。( 換個角度,我們可以將待遷移的table數組,看成一個任務隊列,transferIndex看成任務隊列的頭指針。而擴容線程,就是這個隊列的消費者。擴容線程通過CAS設置transferIndex索引的過程,就是消費者從任務隊列中獲取任務的過程。 )
      2.  擴容過程:
        1.  容量已經達到擴容閾值,需要進行擴容操作,此時transferindex=tab.length=32
        2. 擴容線程A 以cas的方式修改transferindex=31-16=16 ,然後按照降序遷移table[31]–table[16]這個區間的hash桶
        3. 遷移hash桶時,會將桶內的鏈表或者紅黑樹,按照一定算法,拆分成2份,將其插入nextTable[i]和nextTable[i+n](n是table數組的長度)。 遷移完畢的hash桶,會被設置成ForwardingNode節點,以此告知訪問此桶的其他線程,此節點已經遷移完畢
        4. 此時線程2訪問到了ForwardingNode節點,如果線程2執行的put或remove等寫操作,那麼就會先幫其擴容。如果線程2執行的是get等讀方法,則會調用ForwardingNode的find方法,去nextTable裏面查找相關元素
        5. 線程2加入擴容操作
        6. 如果準備加入擴容的線程,發現以下情況,放棄擴容,直接返回。
          1. 發現transferIndex=0,即所有node均已分配

          2. 發現擴容線程已經達到最大擴容線程數

                                                    

      1.  源碼解析
        private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
            int n = tab.length, stride;
            //先判斷CPU核數,如果是多核,將數組長度/8,再/核數,得到stride,否則stride=數組長度,如果stride<16,則stride=16
            //這裏的目的是讓每個CPU處理的桶一樣多,避免出現轉移任務不均勻的現象,如果桶較少的話,默認一個CPU(一個線程)處理16個桶,即確保每次至少獲取16個桶(遷移任務)
            if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
                stride = MIN_TRANSFER_STRIDE; // subdivide range
            //未初始化進行初始化
            if (nextTab == null) {            // initiating
                try {
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];//擴容2倍
                    nextTab = nt;//更新
                } catch (Throwable ex) {      // try to cope with OOME
                    sizeCtl = Integer.MAX_VALUE;//擴容失敗,sizeCtl使用int最大值。
                    return;
                }
                nextTable = nextTab;//更新成員變量
                //transferIndex默認=table.length
                transferIndex = n;
            }
            int nextn = nextTab.length;//新tab的長度
            //創建一個fwd節點,用於佔位。當別的線程發現這個槽位中是fwd類型的節點,表示其他線程正在擴容,並且此節點已經擴容完畢,跳過這個節點。關聯了nextTab,可以通過ForwardingNode.find()訪問已經遷移到nextTab的數據。
            ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
            //首次推進為 true,如果等於true,說明需要再次推進一個下標(i--),反之,如果是false,那麼就不能推進下標,需要將當前的下標處理完畢才能繼續推進
            boolean advance = true;
            //完成狀態,如果是true,就結束此方法。
            boolean finishing = false; // to ensure sweep before committing nextTab
            //自旋,i表示當前線程可以處理的當前桶區間最大下標,bound表示當前線程可以處理的當前桶區間最小下標
            for (int i = 0, bound = 0;;) {
                Node<K,V> f; int fh;
                 //while:如果當前線程可以向後推進;這個循環就是控制i遞減。同時,每個線程都會進入這裏取得自己需要轉移的桶的區間
                //分析場景:table.length=32,此時執行到這個地方nextTab.length=64 A,B線程同時進行擴容。
                //A,B線程同時執行到while循環中cas這段代碼
                //A線程獲第一時間搶到資源,設置bound=nextBound=16,i = nextIndex - 1=31 A線程搬運table[31]~table[16]中間16個元素
                //B線程再次回到while起點,然後在次獲取到 bound = nextBound-0,i=nextIndex - 1=15,B線程搬運table[15]~table[0]中間16個元素
                //當transferIndex=0的時候,說明table裏面所有搬運任務都已經完成,無法在分配任務。
                while (advance) {
                    int nextIndex, nextBound;
                    // 對i減1,判斷是否大於等於bound(正常情況下,如果大於bound不成立,說明該線程上次領取的任務已經完成了。那麼,需要在下面繼續領取任務)
                    // 如果對i減1大於等於 bound,或者完成了,修改推進狀態為 false,不能推進了。任務成功后修改推進狀態為 true。
                    // 通常,第一次進入循環,i-- 這個判斷會無法通過,從而走下面的nextIndex = transferIndex(獲取最新的轉移下標)。其餘情況都是:如果可以推進,將i減1,然後修改成不可推進。如果i對應的桶處理成功了,改成可以推進。
                    if (--i >= bound || finishing)
                        advance = false;//這裏設置false,是為了防止在沒有成功處理一個桶的情況下卻進行了推進
                   // 這裏的目的是:1. 當一個線程進入時,會選取最新的轉移下標。
                   //             2. 當一個線程處理完自己的區間時,如果還有剩餘區間的沒有別的線程處理,再次CAS獲取區間。
                    else if ((nextIndex = transferIndex) <= 0) {
                        // 如果小於等於0,說明沒有區間可以獲取了,i改成-1,推進狀態變成false,不再推進
                        // 這個-1會在下面的if塊里判斷,從而進入完成狀態判斷
                        i = -1;
                        advance = false;//這裏設置false,是為了防止在沒有成功處理一個桶的情況下卻進行了推進
                    }
                    // CAS修改transferIndex,即 length - 區間值,留下剩餘的區間值供後面的線程使用
                    else if (U.compareAndSwapInt
                             (this, TRANSFERINDEX, nextIndex,
                              nextBound = (nextIndex > stride ?
                                           nextIndex - stride : 0))) {
                        bound = nextBound;//這個值就是當前線程可以處理的最小當前區間最小下標
                        i = nextIndex - 1;//初次對i賦值,這個就是當前線程可以處理的當前區間的最大下標
                        advance = false;// 這裏設置false,是為了防止在沒有成功處理一個桶的情況下卻進行了推進,這樣導致漏掉某個桶。下面的 if(tabAt(tab, i) == f) 判斷會出現這樣的情況。
                    }
                }
                //i<0(不在 tab 下標內,按照上面的判斷,領取最後一段區間的線程結束)
                if (i < 0 || i >= n || i + n >= nextn) {
                    int sc;
                    if (finishing) {// 如果完成了擴容和數據遷移
                        nextTable = null;//刪除成員遍歷
                        table = nextTab;//更新table
                        sizeCtl = (n << 1) - (n >>> 1);//更新閥值
                        return;//結束transfer
                    }
                    //如果沒完成,嘗試將sc -1. 表示這個線程結束幫助擴容了,將 sc 的低 16 位減一。
                    if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                        //如果 sc - 2 不等於標識符左移 16 位。如果他們相等了,說明沒有線程在幫助他們擴容了。也就是說,擴容結束了。
                        /**
                         *第一個擴容的線程,執行transfer方法之前(helpTransfer方法中),會設置 sizeCtl = (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2)
                         *後續幫其擴容的線程,執行transfer方法之前,會設置 sizeCtl = sizeCtl+1
                         *每一個退出transfer的方法的線程,退出之前,會設置 sizeCtl = sizeCtl-1
                         *那麼最後一個線程退出時:
                         *必然有sc == (resizeStamp(n) << RESIZE_STAMP_SHIFT) + 2),即 (sc - 2) == resizeStamp(n) << RESIZE_STAMP_SHIFT
                        */
                        if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                            return;// 不相等,說明不到最後一個線程,直接退出transfer方法
                        finishing = advance = true;// 如果相等,擴容結束了,更新 finising 變量
                        i = n; // recheck before commit,最後退出的線程要重新check下是否全部遷移完畢
                    }
                }
                else if ((f = tabAt(tab, i)) == null) // 獲取老tab的i下標位置的變量,如果是 null,就使用 fwd 佔位。
                    advance = casTabAt(tab, i, null, fwd);// 如果成功寫入 fwd 佔位,再次推進一個下標
                else if ((fh = f.hash) == MOVED)// 如果不是 null 且 hash 值是 MOVED。
                    advance = true; // already processed,說明別的線程已經處理過了,再次推進一個下標
                else {// 到這裏,說明這個位置有實際值了,且不是佔位符。對這個節點上鎖。為什麼上鎖,防止 putVal 的時候向鏈表插入數據
                    synchronized (f) {
                        // 判斷 i 下標處的桶節點是否和 f 相同
                        if (tabAt(tab, i) == f) {
                            Node<K,V> ln, hn;// low, height 高位桶,低位桶
                            // 如果 f 的 hash 值大於 0 。TreeBin 的 hash 是 -2
                            if (fh >= 0) {
                                // 對老長度進行與運算(第一個操作數的的第n位於第二個操作數的第n位如果都是1,那麼結果的第n為也為1,否則為0)
                                // 由於 Map 的長度都是 2 的次方(000001000 這類的数字),那麼取於 length 只有 2 種結果,一種是 0,一種是1
                                //  如果是結果是0 ,Doug Lea 將其放在低位,反之放在高位,目的是將鏈表重新 hash,放到對應的位置上,讓新的取於算法能夠擊中他。
                                int runBit = fh & n;
                                Node<K,V> lastRun = f; // 尾節點,且和頭節點的 hash 值取於不相等
                                // 遍歷這個桶
                                for (Node<K,V> p = f.next; p != null; p = p.next) {
                                    // 取於桶中每個節點的 hash 值
                                    int b = p.hash & n;
                                    // 如果節點的 hash 值和首節點的 hash 值取於結果不同
                                    if (b != runBit) {
                                        runBit = b; // 更新 runBit,用於下面判斷 lastRun 該賦值給 ln 還是 hn。
                                        lastRun = p; // 這個 lastRun 保證後面的節點與自己的取於值相同,避免後面沒有必要的循環
                                    }
                                }
                                if (runBit == 0) {// 如果最後更新的 runBit 是 0 ,設置低位節點
                                    ln = lastRun;
                                    hn = null;
                                }
                                else {
                                    hn = lastRun; // 如果最後更新的 runBit 是 1, 設置高位節點
                                    ln = null;
                                }// 再次循環,生成兩個鏈表,lastRun 作為停止條件,這樣就是避免無謂的循環(lastRun 後面都是相同的取於結果)
                                for (Node<K,V> p = f; p != lastRun; p = p.next) {
                                    int ph = p.hash; K pk = p.key; V pv = p.val;
                                    // 如果與運算結果是 0,那麼就還在低位
                                    if ((ph & n) == 0) // 如果是0 ,那麼創建低位節點
                                        ln = new Node<K,V>(ph, pk, pv, ln);
                                    else // 1 則創建高位
                                        hn = new Node<K,V>(ph, pk, pv, hn);
                                }
                                // 其實這裏類似 hashMap
                                // 設置低位鏈表放在新數組的 i
                                setTabAt(nextTab, i, ln);
                                // 設置高位鏈表,在原有長度上加 n
                                setTabAt(nextTab, i + n, hn);
                                // 將舊的鏈表設置成佔位符,表示處理過了
                                setTabAt(tab, i, fwd);
                                // 繼續向後推進
                                advance = true;
                            }// 如果是紅黑樹
                            else if (f instanceof TreeBin) {
                                TreeBin<K,V> t = (TreeBin<K,V>)f;
                                TreeNode<K,V> lo = null, loTail = null;
                                TreeNode<K,V> hi = null, hiTail = null;
                                int lc = 0, hc = 0;
                                // 遍歷
                                for (Node<K,V> e = t.first; e != null; e = e.next) {
                                    int h = e.hash;
                                    TreeNode<K,V> p = new TreeNode<K,V>
                                        (h, e.key, e.val, null, null);
                                    // 和鏈表相同的判斷,與運算 == 0 的放在低位
                                    if ((h & n) == 0) {
                                        if ((p.prev = loTail) == null)
                                            lo = p;
                                        else
                                            loTail.next = p;
                                        loTail = p;
                                        ++lc;
                                    } // 不是 0 的放在高位
                                    else {
                                        if ((p.prev = hiTail) == null)
                                            hi = p;
                                        else
                                            hiTail.next = p;
                                        hiTail = p;
                                        ++hc;
                                    }
                                }
                                // 如果樹的節點數小於等於 6,那麼轉成鏈表,反之,創建一個新的樹
                                ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                                    (hc != 0) ? new TreeBin<K,V>(lo) : t;
                                hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                                    (lc != 0) ? new TreeBin<K,V>(hi) : t;
                                // 低位樹
                                setTabAt(nextTab, i, ln);
                                // 高位數
                                setTabAt(nextTab, i + n, hn);
                                // 舊的設置成佔位符
                                setTabAt(tab, i, fwd);
                                // 繼續向後推進
                                advance = true;
                            }
                        }
                    }
                }
            }
        }
    1. addCount:計數

      // 從 putVal 傳入的參數是x=1,check=binCount默認是0,只有hash衝突了才會大於1,且他的大小是鏈表的長度(如果不是紅黑樹結構的話,紅黑樹=2)。
          private final void addCount(long x, int check) {
              CounterCell[] as; long b, s;
               //如果計數盒子不是空或者修改 baseCount 失敗
              if ((as = counterCells) != null ||
                  !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
                  CounterCell a; long v; int m;
                  boolean uncontended = true;
                   // 如果計數盒子是空(尚未出現併發)
                   // 如果隨機取餘一個數組位置為空 或者
                   // 修改這個槽位的變量失敗(出現併發了)
                   // 執行 fullAddCount 方法,在fullAddCount自旋直到CAS操作成功才結束退出
                  if (as == null || (m = as.length - 1) < 0 ||
                      (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                      !(uncontended =
                        U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
                      fullAddCount(x, uncontended);
                      return;
                  }
                  if (check <= 1)
                      return;
                  s = sumCount();
              }
              // 檢查是否需要擴容,在 putVal 方法調用時,默認就是要檢查的(check默認是0,鏈表是鏈表長度,紅黑樹是2),如果是值覆蓋了,就忽略
              if (check >= 0) {
                  Node<K,V>[] tab, nt; int n, sc;
                  // 如果map.size() 大於 sizeCtl(達到擴容閾值需要擴容) 且
                  // table 不是空;且 table 的長度小於 1 << 30。(可以擴容)
                  while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                         (n = tab.length) < MAXIMUM_CAPACITY) {
                      // 根據 length 得到一個標識
                      int rs = resizeStamp(n);
                      if (sc < 0) {//表明此時有別的線程正在進行擴容
                          // 如果 sc 的低 16 位不等於 標識符(校驗異常 sizeCtl 變化了)
                          // 如果 sc == 標識符 + 1 (擴容結束了,不再有線程進行擴容)(默認第一個線程設置 sc ==rs 左移 16 位 + 2,當第一個線程結束擴容了,就會將 sc 減一。這個時候,sc 就等於 rs + 1)
                          // 如果 sc == 標識符 + 65535(幫助線程數已經達到最大)
                          // 如果 nextTable == null(結束擴容了)
                          // 如果 transferIndex <= 0 (轉移狀態變化了)
                          // 結束循環
                          if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                              sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                              transferIndex <= 0)
                              break;
                          // 不滿足前面5個條件時,嘗試參与此次擴容,把正在執行transfer任務的線程數加1,+2代表有1個,+1代表有0個,表示多了一個線程在幫助擴容,執行transfer
                          if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                              transfer(tab, nt);
                      }
                      //如果不在擴容,將 sc 更新:標識符左移 16 位 然後 + 2. 也就是變成一個負數。高 16 位是標識符,低 16 位初始是 2.
                      //試着讓自己成為第一個執行transfer任務的線程
                      else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                                   (rs << RESIZE_STAMP_SHIFT) + 2))
                          transfer(tab, null);
                      s = sumCount();// 重新計數,判斷是否需要開啟下一輪擴容
                  }
              }
          }
  1. get:

    public V get(Object key) {
            Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
            //得到hash
            int h = spread(key.hashCode());
            //table有值,且查找到的槽位有值(tabAt方法通過valatile讀)
            if ((tab = table) != null && (n = tab.length) > 0 &&
                (e = tabAt(tab, (n - 1) & h)) != null) {
                //hash、key、value都相同返回當前查找到節點的值
                if ((eh = e.hash) == h) {
                    if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                        return e.val;
                }
                //遍歷特殊節點:紅黑樹、已經遷移的節點(ForwardingNode)等
                else if (eh < 0)
                    return (p = e.find(h, key)) != null ? p.val : null;
                //遍歷node鏈表(e.next也是valitle變量)
                while ((e = e.next) != null) {
                    if (e.hash == h &&
                        ((ek = e.key) == key || (ek != null && key.equals(ek))))
                        return e.val;
                }
            }
            return null;
        }
        Node<K,V> find(int h, Object k) {
            Node<K,V> e = this;
            if (k != null) {
                do {
                    K ek;
                    if (e.hash == h &&
                        ((ek = e.key) == k || (ek != null && k.equals(ek))))
                        return e;
                } while ((e = e.next) != null);
            }
            return null;
        }
  2. remove:

    public V remove(Object key) {
            return replaceNode(key, null, null);
        }
        //通過volatile設置第i個節點的值
        static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
            U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
        }
        final V replaceNode(Object key, V value, Object cv) {
            int hash = spread(key.hashCode());
            //自旋
            for (Node<K,V>[] tab = table;;) {
                Node<K,V> f; int n, i, fh;
                //數組或查找的槽位為空,結束自旋返回null
                if (tab == null || (n = tab.length) == 0 ||
                    (f = tabAt(tab, i = (n - 1) & hash)) == null)
                    break;
                //正在擴容,幫助擴容
                else if ((fh = f.hash) == MOVED)
                    tab = helpTransfer(tab, f);
                else {
                    V oldVal = null;//返回的舊值
                    boolean validated = false;//是否進行刪除鏈表或紅黑樹節點
                    synchronized (f) {//槽位加鎖
                        //getObjectVolatile獲取tab[i],如果此時tab[i]!=f,說明其他線程修改了tab[i]。回到for循環開始處,重新執行
                        if (tabAt(tab, i) == f) {//槽位節點沒有變化
                            if (fh >= 0) {//槽位節點是鏈表
                                validated = true;
                                //遍歷鏈表
                                for (Node<K,V> e = f, pred = null;;) {
                                    K ek;
                                    //hash、key、value相同
                                    if (e.hash == hash &&
                                        ((ek = e.key) == key ||
                                         (ek != null && key.equals(ek)))) {
                                        V ev = e.val;//臨時節點緩存當前節點值
                                        //值相同
                                        if (cv == null || cv == ev ||
                                            (ev != null && cv.equals(ev))) {
                                            oldVal = ev;//給舊值賦值
                                            if (value != null)//值覆蓋,replace()調用
                                                e.val = value;
                                            else if (pred != null)//有前節點,表示當前節點不是頭節點
                                                pred.next = e.next;//刪除當前節點
                                            else
                                                setTabAt(tab, i, e.next);//刪除頭節點,即更新當前槽位(數組槽位)節點為頭節點的下一節點
                                        }
                                        break;
                                    }
                                    //當前節點不是目標節點,繼續遍歷下一個節點
                                    pred = e;
                                    //到達鏈表尾部,依舊沒有找到,跳出循環
                                    if ((e = e.next) == null)
                                        break;
                                }
                            }
                            else if (f instanceof TreeBin) {//紅黑樹
                                validated = true;
                                TreeBin<K,V> t = (TreeBin<K,V>)f;
                                TreeNode<K,V> r, p;
                                //樹有節點且查找的節點不為null
                                if ((r = t.root) != null &&
                                    (p = r.findTreeNode(hash, key, null)) != null) {
                                    V pv = p.val;
                                    //值相同
                                    if (cv == null || cv == pv ||
                                        (pv != null && cv.equals(pv))) {
                                        oldVal = pv;//給舊值賦值
                                        if (value != null)//值覆蓋,replace()調用
                                            p.val = value;
                                        else if (t.removeTreeNode(p))//刪除節點成功
                                            setTabAt(tab, i, untreeify(t.first));//更新當前槽位(數組槽位)節點為樹的第一個節點
                                    }
                                }
                            }
                        }
                    }
                    if (validated) {
                        //如果刪除了節點,更新size
                        if (oldVal != null) {
                            if (value == null)
                                addCount(-1L, -1);//數量-1
                            return oldVal;
                        }
                        break;
                    }
                }
            }
            return null;
        }

  三.總結

  1.  put:使用cas插入,如果是鏈表或樹節點才會加鎖同步操作,提高了性能

    1. 不允許有key或value為null,否則拋出異常;
    2. 在第一次put時初始化table(initTable()),初始化有併發控制,通過sizeCtl變量判斷,sizeCtl<0表示已經有線程在初始化,當前線程就不在進行,否則sizeCtl置為-1(CAS)並創建數組;
    3. 當hash計算出的槽位節點為null時,使用CAS插入元素;
    4. 當hash為MOVED(-1)時,幫助擴容,但可能幫助不了,因為每個線程默認16個桶,如果只有16個桶,第二個線程無法幫助擴容;
    5. 如果hash衝突了,同步槽位節點,如果槽位是鏈表結構,進行鏈表操作,覆蓋舊值或插入到鏈表尾部;如果是樹結構,添加到樹中;
    6. 元素添加到鏈表或樹中,如果鏈表長度大於8,將鏈錶轉換為紅黑樹;
    7. 調用addCount(),對size+1,並判斷是否需要擴容addCount(),如果是值覆蓋操作就不需要調用該方法;
  2. initTable:初始化

    1. 數組table為null才進行初始化,否則直接返回舊數組;
    2. 如果當前sizeCtl小於0,表示有線程正在初始化,則當前線程禮讓CPU,保證只有一個線程正在初始化數組;
    3. 如果沒有線程在初始化,則當前線程CAS將sizeCtl置為-1並創建數組,然後重新計算閥值;
  3. helpTransfer:幫助擴容

    1. 當嘗試插入操作時,發現節點是forward類型,則會幫助擴容;
    2. 每次加入一個線程都會將sizeCtl的低16位+1,同時校驗高16位的標識符;
    3. 擴容最大的幫助線程是65535,這是低16位的最大值限制;
    4. 每個線程默認分配16個桶,如果桶的數量是16,那麼第二個線程無法幫助擴容,即桶被分配完其他線程無法進場擴容;
  4. transfer:擴容和數據遷移

    1. 根據CPU核數平均分配給每個CPU相同數量的桶,如果不夠16個,默認就是16個;
    2. 按照2倍容量進行擴容;
    3. 每個線程在處理完自己領取的區間后,還可以繼續領取,如果還有的話,通過transferIndex變量遞減16實現桶數量控制;
    4. 每次處理空桶的時候,會把當前桶標識為forward節點,告訴put的其他線程說“我正在擴容,快來幫忙”,但如果只有16個桶,只能有一個線程進行擴容;
    5. 如果有了佔位符MOVED,表示已經被處理過,跳過這個桶,繼續推進處理其他桶;
    6. 如果有真正的實際值,那麼就同步加鎖頭節點,防止putVal的併發;
    7. 同步塊里將鏈表拆分成兩份,根據 hash & length 得到是否是0,如果是0,放在新數組低位,反之放在length+i的高位。這是防止下次取值hash找不到正確的位置;
    8. 如果該桶類型是紅黑樹,也會拆分成2個,然後判斷拆分過的桶的大小是否小於等於6,如果是轉換成鏈表;
    9. 線程處理完如果沒有可選區間,且任務沒有完成,則會將整個表檢查一遍,防止遺漏;
  5. addCount:擴容判斷

    1. 當插入結束時,會對size+1,並判斷是否需要擴容的判斷;
    2. 優先使用計數盒子(如果不是空,說明併發了),如果計數盒子是空,使用baseCount變量+1;
    3. 如果修改baseCount失敗,使用計數盒子,如果還是修改失敗,在fullAddCount()中自旋直到CAS操作成功;
    4. 檢查是否需要擴容;
    5. 如果size大於等於sizeCtl且長度小於1<<30,可以擴容;
    6. 如果已經在擴容,幫助其擴容;
    7. 如果沒有在擴容,自行開啟擴容,更新sizeCtl變量為負數,賦值為標識符高16位+2;
  6. remove:刪除元素

    1. 自旋遍曆數量,如果數組或根據hash計算的槽位節點值為null,直接結束自旋返回null;
    2. 如果槽位節點正在擴容,幫助擴容;
    3. 如果槽位節點有值,同步加鎖;
    4. 如果該槽位節點還是沒有任何變化,判斷是鏈表結構類型節點還是樹結構類型節點,通過遍歷查找元素,找到刪除該節點或重新設置頭節點;
    5. 如果刪除了節點,更新size-1,如果有舊值則返回舊值,否則返回null;

  四.參考

  1. https://www.jianshu.com/p/2829fe36a8dd
  2. https://www.jianshu.com/p/487d00afe6ca
  3. https://juejin.im/post/5b001639f265da0b8f62d0f8#comment

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

口罩繩易纏動物 動保籲丟棄前先剪掉

摘錄自2020年9月24日公視報導

英國陸續發現有海鷗的雙腳被口罩繩纏住,就像戴了腳鐐一樣。根據統計,全球目前每個月消耗1000多億個拋棄式口罩和手套,對環境造成很大的負擔;動保團體呼籲民眾在丟掉口罩之前,至少先把繩子剪掉,以免傷害無辜的動物。

南韓環境運動聯合會白娜媛說:「一次性口罩是塑料做的,因此幾乎不能分解,因為無法回收,所以不是焚化就是掩埋,這對自然造成巨大負擔。」

英國皇家防止虐待動物協會10號指出,疫情爆發僅半年,他們就救援900多隻因廢棄口罩或塑膠垃圾受困的動物,呼籲大眾丟口罩前先剪斷鬆緊繩,以免纏住動物。

生物多樣性
國際新聞
口罩
海鷗

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

日本群馬縣養豬場爆發豬瘟 將撲殺5400頭豬隻

摘錄自2020年09月27日中央通訊社日本報導

日本農林水產省26日宣布,群馬縣高崎市一座養豬場發現豬隻感染家畜傳染病豬瘟(CSF),縣當局將依法撲殺養豬場內全部約5400頭豬隻。

時事通信社報導,這座養豬場為預防感染豬瘟,曾為豬隻接種疫苗,但確診感染的小豬並未接種疫苗。

這是時隔約半年,日本國內養豬場再度爆發豬瘟疫情,也是自2018年日本國內睽違26年首度爆發豬瘟疫情以來,群馬縣內養豬場首度確認感染。

群馬縣當局於發現野生野豬感染豬瘟後,從去年10月起,著手為養豬場接種疫苗。

生物多樣性
循環經濟
國際新聞
日本
豬瘟
經濟動物

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

比昂科威貴不了幾萬,都20多萬了為啥不看看奔馳寶馬?

奔馳GLA的發動機動力表現不錯,低扭頗為強勁。但是它搭載的7DCT變速箱在調校方面離完美還差一些,它的低速有一些頓挫。其整體傳動效率較高,動力和油耗表現都不錯。寶馬X1的車身尺寸、車內空間比起同場的奔馳GLA、奧迪Q3要更富有優勢。

您是否看見滿大街跑的途觀L、昂科威、冠道等車型后,覺得有些千人一面、審美疲勞?其實差不多的價位,已經有不少豪華品牌的入門SUV車型供我們選擇!下面咱們就來看看這些車型是否具備良好表現!

奧迪Q3的車身設計“德味”很重,它造型飽滿、大氣,尾部的設計精緻感強。但屬於產品生命末期的它,內飾設計保守,而做工的精緻度頗高、用料也不錯。

奧迪Q3日常開起來能讓人感到“零壓力”,它的轉向和油門很輕盈,在市區中穿行尤為輕鬆。它的動力匹配程度高,降擋速度很快捷,用戶想要稍為激烈駕駛,它也能給出很好的配合!

作為廣汽謳歌推出的首款車型,謳歌CDX肩負着打開市場的重任。它的造型極其前衛,而且看到實車時你就會能感受到其設計的精緻感!而內飾造型層次感突出,還採用了大面積的軟性材質和皮革包裹,質感不錯。

謳歌CDX的1.5T發動機採用了缸內直噴技術,與之匹配的是一副8擋雙離合變速箱。該雙離合變速箱帶有液力變矩器,這讓它換擋平順,降擋的速度也相當快,動力系統匹配是高效的。這讓它的油耗較低,兩驅車型也能在8.35秒內破百,表現良好。

奔馳GLA的車身設計除了動感、柔美以外,還富有性感氣質!最喜歡的是它的側面輪廓緊湊,動感氣息濃厚!而且它的內飾帶有較高的豪華氣息,也富有動感!當然,它的配置方面有待提高,起步價也不算低。

奔馳GLA的發動機動力表現不錯,低扭頗為強勁。但是它搭載的7DCT變速箱在調校方面離完美還差一些,它的低速有一些頓挫。其整體傳動效率較高,動力和油耗表現都不錯。

寶馬X1的車身尺寸、車內空間比起同場的奔馳GLA、奧迪Q3要更富有優勢。它2780mm的軸距,4565mm的車身長度要更符合緊湊型SUV的定位!車身設計富有力量感,氣勢較強。

寶馬X1入門車型採用的1.5T發動機,動力參數略顯保守,而匹配的6AT變速箱平順性挺好,換擋邏輯中規中矩。中高配車型採用的2.0T+8AT動力總成表現頗為出眾!因此更推薦大家提高預算選擇2.0T車型。

雷克薩斯NX的設計相當前衛,犀利的紡錘型中網富有氣勢,車身線條硬朗,燈組造型獨特。而且內飾精緻感強,用料較好。

而雷克薩斯NX提供了豐富的動力組合,其300h全驅鋒致版車型上搭載的電機總扭矩達409牛米,官方破百時間9.2秒,百公里綜合油耗為6.1L,更側重於低能耗的調校讓它油耗很低,續航里程頗高。

寫在最後:這6款豪華SUV車型,具備良好的實用性,設計也大多前衛、個性,年輕人駕駛起來相當合適。當然它們之中部分車型優惠較大,喜歡的朋友可以去了解一下!本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

巴西網購郵包夾帶不明種子 當局展開調查

摘錄自2020年9月30日中央社報導

巴西農業部29日指出,接獲8州36起民眾收到從國外寄來、未經訂購的不明種子郵包的投訴案例,已展開調查。

根據巴西新聞網站G1報導,這些神秘的種子通常裝在塑膠袋裡,夾帶在透過網路、網站或應用程式購買的產品一起寄給消費者。包裹來源都是中國、馬來西亞和香港等亞洲國家。

所有可疑種子包裹將由戈恩尼亞(Goiânia)聯邦農業防禦實驗室進行分析。巴西農業部要求民眾小心謹慎,無論來源國是哪裡,都不要打開未經訂購的不明種子郵包或直接丟棄,避免種子與土壤接觸,否則可能破壞環境和農業地區。這些種子應交給農業部在各州的辦事處或農業防禦單位。

污染治理
國際新聞
巴西
網購
種子

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心

NASH:基於豐富網絡態射和爬山算法的神經網絡架構搜索 | ICLR 2018

論文提出NASH方法來進行神經網絡結構搜索,核心思想與之前的EAS方法類似,使用網絡態射來生成一系列效果一致且繼承權重的複雜子網,本文的網絡態射更豐富,而且僅需要簡單的爬山算法輔助就可以完成搜索,耗時0.5GPU day

來源:曉飛的算法工程筆記 公眾號

論文: Simple And Efficient Architecture Search for Convolutional Neural Networks

  • 論文地址:https://arxiv.org/pdf/1711.04528.pdf

Introduction

  論文目標在於大量減少網絡搜索的計算量並保持結果的高性能,核心思想與EAS算法類似,主要貢獻如下:

  • 提供baseline方法,隨機構造網絡並配合SGDR進行訓練,在CIFAR-10上能達到6%-7%的錯誤率,高於大部分NAS方法。
  • 拓展了EAS在網絡態射(network morphisms)上的研究,能夠提供流行的網絡構造block,比如skip connection和BN。
  • 提出基於爬山算法的神經網絡結構搜索NASH,該方法迭代地進行網絡搜索,在每次迭代中,對當前網絡使用一系列網絡態射得到多個新網絡,然後使用餘弦退火進行快速優化,最終得到性能更好的新網絡。在CIFAR-10上,NASH僅需要單卡12小時就可以達到baseline的準確率。

Network Morphism

  $\mathcal{N}(\mathcal{X})$為$\mathcal{X}\in \mathbb{R}^n$上的一系列網絡,網絡態射(network morphism)為映射$M: \mathcal{N}(\mathcal{X}) \times \mathbb{R}^k \to \mathcal{N}(\mathcal{X}) \times \mathbb{R}^j$,從參數為$w\in \mathbb{R}k$的網絡$fw \in \mathcal{N}(\mathcal{X})$轉換為參數為$\tilde{w} \in \mathbb{R}j$的網絡$g\tilde{w} \in \mathcal{N}(\mathcal{X})$,並且滿足公式1,即對於相同的輸入,網絡的輸出不變。

  下面給出幾種標準網絡結構的網絡態射例子:

Network morphism Type I

  將$f^w$進行公式2的替換,$\tilde{w}=(w_i, C, d)$,為了滿足公式1,設定$A=1$和$b=0$,可用於添加全連接層。

  另外一種複雜點的策略如公式3,$\tilde{w}=(w_i, C, d)$,設定$C=A^{-1}$和$d=-Cb$,可用於表達BN層,其中$A$和$b$表示統計結構,$C$和$d$為可學習的$\gamma$和$\beta$。

Network morphism Type II

  假設$f_i{w_i}$可由任何函數$h$表示,即$f_i{w_i}=Ah^{w_h}(x)+b$

  則可以將$f^w$,$w_i = (w_h, A, b)$配合任意函數$\tilde{h}{w_{\tilde{h}}}(x)$根據公式4替換為$\tilde{f}{\tilde{w}i}$,$\tilde{w}=(w_i, w{\tilde{h}}, \tilde{A})$,設定$\tilde{A}=0$。這個態射可以表示為兩種結構:

  • 增加層寬度,將$h(x)$想象為待拓寬的層,設定$\tilde{h}=h$則可以增加兩倍的層寬度。
  • concatenation型的skip connection,假設$h(x)$本身就是一系列層操作$h(x)=h_n(x) \circ \cdots \circ h_0(x)$,設定$\tilde{h}(x)=x$來實現短路連接。

Network morphism Type III

  任何冪等的函數$f_i^{w_i}$都可以通過公式5進行替換,初始化$\tilde{w}_i=w_i$,公式5在無權重的冪等函數上也成立,比如ReLU。

Network morphism Type IV

  任何層$f_i^{w_i}$都可以配合任意函數$h$進行公式6的替換,初始化$\lambda=1$,可用於結合任意函數,特別是非線性函數,也可以用於加入additive型的skip connection。
  此外,不同的網絡態射組合也可以產生新的態射,比如可以通過公式2、3和5在ReLU層後面插入”Conv-BatchNorm-Relu”的網絡結構。

Architecture Search by Network Morphisms

  NASH方法基於爬山算法,先從小網絡開始,對其進行網絡態射生成更大的子網絡,由於公式1的約束,子網的性能與原網絡是一樣的,後續子網進行簡單的訓練看是否有更好的性能,最後選擇性能優異的子網進行重複的操作。

  圖1可視化了NASH方法的一個step,算法1的ApplyNetMorph(model, n)包含n個網絡態射操作,每個為以下方法的隨機一種:

  • 加深網絡,例如添加Conv-BatchNorm-Relu模塊,插入位置和卷積核大小都是隨機的,channel數量跟最近的卷積操作一致。
  • 加寬網絡,例如使用network morphism type II來加寬輸出的channel,加寬比例隨機。
  • 添加從層$i$到層$j$的skup connection,使用network morphism type II或IV,插入位置均隨機選擇。

  由於使用了網絡態射,子網繼承了原網絡的權重且性能一致,NASH方法優勢在於能夠很快的評估子網的性能,論文使用了簡單的爬山算法,當然也可以選擇其它的優化策略。

Experiments

Baslines

Retraining from Scratch

CIFAR-10

CIFAR-100

CONCLUSION

  論文提出NASH方法來進行神經網絡結構搜索,核心思想與之前的EAS方法類似,使用網絡態射來生成一系列效果一致且繼承權重的複雜子網,本文的網絡態射更豐富,而且僅需要簡單的爬山算法輔助就可以完成搜索,耗時0.5GPU day



如果本文對你有幫助,麻煩點個贊或在看唄~
更多內容請關注 微信公眾號【曉飛的算法工程筆記】

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※為什麼 USB CONNECTOR 是電子產業重要的元件?

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※台北網頁設計公司全省服務真心推薦

※想知道最厲害的網頁設計公司"嚨底家"!

※推薦評價好的iphone維修中心