這年頭養車和養娃一樣貴…這些便宜好養的車你可別錯過!

6L自然吸氣車型還要低,加上非常運動犀利的外觀設計,值得入手,最起碼三四年都不會過時。2016款的起亞 k3對外觀進行了一次小手術,拋棄了以往的圓潤線條,燈組、中網、保險杠都進行了修改,總體風格更加硬朗、幹練,有些時候不得不佩服,家族式設計真是一個神奇的東西,換一下設計又像是一輛新車一樣。

韓系車

韓系車一向給人前衛、時尚、富有活力的形象,韓系車的零整比表現還是不錯的,平均都是200%左右,優於行業整體平均水平,在我國,韓系車的市場佔有率還是挺高的,這主要歸根於“性價比”這一塊,同樣的價錢選擇韓系車往往能夠得到更加時尚的設計與更加豐富的配置。

就在剛開幕不久的2017年北美車展上,起亞就展示了一款非常重磅的產品,起亞 Stinger,一輛定位於轎跑的車型,其搭配了一台3.3T發動機,百公里加速5.1秒,是起亞歷史上性能最強的量產車型。

不好意思,跑太遠了,還是老老實實看一下現在有什麼韓系車值得購買吧,首先是現代領動,它的1.4T發動機獲得了今年沃德十佳發動機獎項,雖然不是有什麼強暴的性能,但是油耗比自己的1.6L自然吸氣車型還要低,加上非常運動犀利的外觀設計,值得入手,最起碼三四年都不會過時。

2016款的起亞 k3對外觀進行了一次小手術,拋棄了以往的圓潤線條,燈組、中網、保險杠都進行了修改,總體風格更加硬朗、幹練,有些時候不得不佩服,家族式設計真是一個神奇的東西,換一下設計又像是一輛新車一樣。

國產車

國產車講究的就是性價比,維修保養不在話下,隨便修,總覺得好像修車不用錢一樣,在修理廠的每個角落都能看見其身影,對,多年前國產車的確是這樣,不過在今天,雖說和國際一線大廠還有差距,但已經擺脫舊況,實力還是有的。

雖然最近長安的SUV非常火爆,CS15、CS35和CS95的關注度都相當高,但還是想說一下長安逸動這輛車,不知不覺中它已經走過了將近5個年頭,2011年年底面世的它讓我們看見國產車的進步,能夠與同價位合資車相媲美的行駛質感,它雖然沒有什麼值得炫耀的豐富配置,但是卻擁有着國際范的設計和做工水準。

要說國產車中誰的維修保養最便宜,覺得應該是王者中的王者——五菱,無論是五菱宏光還是五菱之光,亦或者是五菱榮光保有量都是超級巨大的,配件的價格自然就要比其它車型要實惠很多,隨便在路邊的小維修廠都能夠很容易找到配件。

即將上市的寶馬1系三廂就是一個很好的例子,中國消費者對於轎車的看法非常獨特,三廂才是轎車,這也是為什麼兩廂版本的寶馬1系總是得不到很好的銷量,寶馬終於意識到要迎合消費者的口味,萬眾期待的寶馬1系三廂只要在價格上多一點誠意,要把奧迪A3同台對飈不是什麼難事。

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

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

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

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

※別再煩惱如何寫文案,掌握八大原則!

※產品缺大量曝光嗎?你需要的是一流包裝設計!

差點就翻車!用生命試駕中國最屌電動車

你TM就拿補貼造了這麼一個&^%$&%*^$(&出來。只要你開着它溜出個幾米,跨過減速帶,那種直接生硬的震動會讓你的香臀不知所措。對於知豆我覺得不需要說太多了,因為我直接建議大家去租一輛好好體驗一把。而奇瑞EQ則像是一輛升級再升級版的知豆。

相信近些年大家對新能源車是有了不少的了解,不論是國內還是國外的車企都着手推出新能源車型。而在國內車企推出新能源車型還能獲得國家或地方政府補貼,消費者也能選擇更多不同的車型。

然而,並不是每家車企都會出良心之作。於是,就有了這麼三輛新能源車,知豆D2,北汽EV160和奇瑞eQ,這三輛車的官方售價分別為15.88-18.88萬元、17.78-18.99萬和15.99-16.49萬元。如果沒有補貼按這價格來賣,估計排隊噴它們的人能繞地球三圈。

不過,今天我們不討論錢的問題。單從產品出發,就聊它們到底是不是一輛“車”。

上一年我們就試駕過了知豆D2,這輛雙門雙座的小車,簡直就是一輛升級版的老年代步車。什麼用料,質感,品牌統統要拋到一邊,因為不值得去說了。不過相比於其它兩者,知豆D2擁有一个中控大屏,不得不說麻雀雖小但逼格還是得有。

不知道有多少朋友體驗過坐進知豆D2里的感覺,但那處濃烈的塑料感絕對會讓你開懷的笑出聲來。你TM就拿補貼造了這麼一個&^%$&%*^$(&出來?只要你開着它溜出個幾米,跨過減速帶,那種直接生硬的震動會讓你的香臀不知所措。對於知豆我覺得不需要說太多了,因為我直接建議大家去租一輛好好體驗一把。

而奇瑞EQ則像是一輛升級再升級版的知豆。在租車點清一色雪白的車體,搭載暗紫色的封閉輪轂,還沒進去之前你會有點懷疑,但只要你坐進去,扣好安全帶,試了N次將車子啟動之後,你還是會覺得它至少還像輛車。用手輕輕一扭,將旋鈕式擋把切換至D擋,你就可以開始屬於你的傳奇之旅了。如果奇瑞的工程師們能把旋鈕做成啟動后能緩緩升上來的話,絕對會是新能源界的一段佳話。

踩住剎車,右腳油門到底,上車不來一發溫柔的彈射起步絕對不能算開過這車。在大長直的公路上稍稍變線,可以感受到強有力的側翻臨界體驗。更可貴的是,這車能開到100km/h。別問我怎麼知道的,肯定是工程師們為了讓你好受些將液晶時速表的数字調大了。

相比之下,北汽EV160就可以算是一輛車了。它不像知豆那麼簡陋,也不像奇瑞eQ那樣發飄,開完前兩輛車之後再開北汽EV160,你會感覺世界的秩序會稍稍正常一點。如果你哪天因為不可描述的原因買了這麼一輛車,那我覺得你需要上某寶收一個奔馳的標,貼在前進氣一點違和感都沒有。

除了洋氣的外觀之外,北汽EV160開起來就沒有像前兩輛車那麼充滿戲劇性。按照The Grand Tour的主持人Jeremy Clarkson的理論,一輛能讓人充滿笑容的車才是好車,那麼北汽EV160可能就有點達不到要求了。

不過這些都不重要了,如果今天的這條片子看完沒能讓你笑出聲來,那可能就說明了連我們的聰明才智都無法讓這三輛車挽回那一點點“車”的尊嚴。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※產品缺大量曝光嗎?你需要的是一流包裝設計!

過年買車套路深…怎樣才能用合理的價錢買到合適的車?

免息≠免手續費什麼“一年免息”、“無息貸款2年愛車開回家”這些廣告你們真敢信。等你帶好了身份證、銀行流水甚至房產證去店裡準備簽字畫押的時候,你就會明白即使減免或優惠了所謂的利息,但手續費、服務費等等費會讓你瞬間感覺上當,不過此時的你騎虎難下,買還是不買。

眼瞅着公曆新年正式開始,同時也意味着“購置稅5折優惠大酬賓”活動徹底結束,不知道大家有沒有趕在2017年之前買到自己喜愛的車呢?

叫獸在這裏對已經提了新車的朋友們說聲恭喜,你們算是“撿到了大便宜”。還沒買的也不要着急,畢竟今年購置稅還可以享受“75折優惠”(1.6L及以下排量乘用車)。更重要的是,如果您恰好在買車之前看到今天這篇文章保你買車不被坑,可以少花冤枉錢,這同樣是“真金白銀”一樣的福利,快收下吧。

叫獸是個實誠人,就單刀直入告訴大家我們在買車的時候會遇到哪些常見的套路。

一.網上低價陷阱

如今網絡信息這麼發達,大部分人買車之前都會先在網上做好功課。除了查詢車輛的配置差異外,還會通過媒體試駕評測(比如時刻關注叫獸)以及網友評價等消息對車輛做一個初步的了解和判斷,而報價更是大家的關注的重點。

來到敏感的價格部分,這時候就需要擦亮你的雙眼。網上都常會以特別顯眼(尤其是紅色)的顏色展示出一個非常有誘惑力的價格,不懂行的“小白”們會以為撿着大便宜了,趕緊點進鏈接一看,通常會遇到幾種情況:

1.需要在店內上牌、辦理保險甚至購買裝飾(所謂的優惠也就形同虛設)

2.大幅優惠只限於頂配車型(並且這個優惠通常來說一樣也辦不到),要買低配?不是沒車就是做不了那樣的優本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

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

※超省錢租車方案

※教你寫出一流的銷售文案?

網頁設計最專業,超強功能平台可客製化

※產品缺大量曝光嗎?你需要的是一流包裝設計!

除了家用致炫還能這麼玩,看完包你流口水…

5L車型前往午飯目的地,人生地不熟的在原廠車載導航的指引下,只用半個多小時便順利到達午飯地點——濱江大公館惠食佳。飲飽食醉后,馬不停蹄前往場地挑戰地點,媒體老師們駕馭着致炫,各顯神通、爭奪積分,無論是短小狹窄的緊湊型賽道,抑或是蜿蜒小徑,致炫都能游刃有餘,樂趣不言而喻。

常言道:“唯汽車與美食不可辜負”,每天沉浸在汽車與美食帶來的歡悅中,樂此不疲。無獨有偶,廣汽豐田邀請參与廠商悉心舉辦的“食貨大玩咖的美食探尋之旅”,正中下懷。

兵馬未動,糧草先行,首個行程便是前往遠近馳名的“食在廣州第一家”——廣州酒家,品味嶺南“一盅兩件”的早茶文化。

苦澀中夾雜一絲甘甜,被貪婪的口腔霸佔着,茶的蒸汽朦朧了雙眼,勾勒出朦朧的回憶,但記憶卻不再朦朧。(好了,這逼裝不下去了)

早茶過後,便駕馭豐田致炫1.5L車型前往午飯目的地,人生地不熟的在原廠車載導航的指引下,只用半個多小時便順利到達午飯地點——濱江大公館惠食佳。

飲飽食醉后,馬不停蹄前往場地挑戰地點,媒體老師們駕馭着致炫,各顯神通、爭奪積分,無論是短小狹窄的緊湊型賽道,抑或是蜿蜒小徑,致炫都能游刃有餘,樂趣不言而喻。

這歸功於致炫搭載S-CVT智能無級變速器(模擬8速),優化了動力系統的效率和加速響應性,使車輛的動力輸出更平順,燃油經濟性理想,只可惜變速箱並未配備S擋。

來到位於佛山北滘的晚飯地點——蚝專家,自不然要一嘗新鮮地道的海鮮盛宴。

這次致炫讓我們收穫了廣佛兩城的獨特味道,在快節奏的城市生活中與致炫偷得一絲閑逸,體驗新款致炫的多項升級。未來,致炫將憑藉其獨有的產品魅力,俘獲更多年輕人。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

明明買了奔馳寶馬卻被人嘲笑 我到底做錯了什麼

雖然面對他們的嘲笑我表示不解,但是車還是要買,聽說買了奔馳,妹子就會自動上車。於是我又買了輛最新款的SUV。結果還是被他們嘲笑。後來我又換了寶馬。路虎。但是結果都是一樣,我覺得不要買那麼好的車了,太容易買到假貨,車要那麼好的品牌幹嘛,能夠遮風擋雨代步就夠了,所以下定決心買一輛吉利熊貓,結果買回去他們卻說,我買車的這段時間,他們的臉上都長肌肉了。

眼看年關將至,為了過年回家的時候能在親戚朋友面前,展示一下自己一年的成績,決定去提輛車。一直都有聽朋友說本田的超跑不錯,還可以爆VTEC,所以決定買輛GK5。

不知怎麼買回來就被朋友取笑,難道在他們眼中Type R才是本田的超跑嗎?後來想了想。

於是我決定換個選擇。雖然超跑很刺激,但是過年車多路滑,安全也很重要,聽說奧迪的quattro四驅很厲害,所以決定去買輛奧迪的SUV。

結果買回來又被他們取笑,難道現在的奧迪是quattro嗎?

雖然面對他們的嘲笑我表示不解,但是車還是要買,聽說買了奔馳,妹子就會自動上車。於是我又買了輛最新款的SUV。

結果還是被他們嘲笑。

後來我又換了寶馬。

路虎。

但是結果都是一樣,我覺得不要買那麼好的車了,太容易買到假貨,車要那麼好的品牌幹嘛,能夠遮風擋雨代步就夠了,所以下定決心買一輛吉利熊貓,結果買回去他們卻說,我買車的這段時間,他們的臉上都長肌肉了。

我的人生觀彷彿在這一刻崩塌。

其實並不是笑話這些車是假車,段子聽聽就完了,畢竟他們除了換了個Logo之外知道自己是老年代步車,並沒有說自己是汽車,不想其他品牌。

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

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

都拖家帶口了…有哪些車適合80后?

能堅強地走到今天,已經是世界性的奇迹。對於這樣一群堅韌不拔的人,努力奮鬥奮鬥,過兩年開上保時捷911還真不是痴人說夢。

總結

俗話說80后是“被坑的一代”,沒趕上改革開放發財致富的機會,卻遇上了金融危機跟樓市瘋長的節點。能堅強地走到今天,已經是世界性的奇迹。對於這樣一群堅韌不拔的人,努力奮鬥奮鬥,過兩年開上保時捷911還真不是痴人說夢!本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※帶您來了解什麼是 USB CONNECTOR  ?

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※如何讓商品強力曝光呢? 網頁設計公司幫您建置最吸引人的網站,提高曝光率!

※綠能、環保無空污,成為電動車最新代名詞,目前市場使用率逐漸普及化

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※教你寫出一流的銷售文案?

怒肝倆月,新鮮出爐史上最有趣的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維修中心

C#9.0 終於來了,帶你一起解讀 nint 和 Pattern matching 兩大新特性玩法

一:背景

1. 講故事

上一篇跟大家聊到了Target-typed newLambda discard parameters,看博客園和公號里的閱讀量都達到了新高,甚是欣慰,不管大家對新特性是多頭還是空頭,起碼還是對它抱有一種極為關注的態度,所以我的這個系列還得跟,那就繼續開擼吧,今天繼續帶來兩個新特性,更多新特性列表,請大家關注:新特性預覽

二:新特性研究

1. Native ints

從字面上看貌似是什麼原生類型ints,有點莫名其妙,還是看一看Issues上舉得例子吧:


Summary: nint i = 1; and nuint i2 = 2;

Shipped in preview in 16.7p1.

有點意思,還是第一次看到有nint這麼個東西,應該就是C#9新增的關鍵詞,好奇心爆棚,快來實操一下。


   static void Main(string[] args)
   {
        nint i = 10;
        Console.WriteLine($"i={i}");
   }

從圖中看,可以原樣輸出,然後用ILSpy查查底層IL代碼,發現連IL代碼都不用看。如下圖:

從圖中看原來 nint 就是 IntPtr 結構體哈,如果你玩過 C# 到 C++ 之間的互操作,我相信你會對Ptr再熟悉不過了,從這個 nint 上看,你不覺得C#團隊對指針操作是前所未有的重視嗎? 前有指針類型IntPtr,後有內存段處理集合Span,到現在直接提供關鍵詞支持,就是盡最大努力讓你在類型安全的前提下使用指針。

這就讓我想起了前些天寫的一篇互操作的文章,現在就可以用nint進行簡化了,來段代碼給大家看一下。

  • 原來的寫法:

        [DllImport("ConsoleApplication1.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
        extern static IntPtr AddPerson(Person person);

        static void Main(string[] args)
        {
            var person = new Person() { username = "dotnetfly", password = "123456" };
            var ptr = AddPerson(person);
            var str = Marshal.PtrToStringAnsi(ptr);
        }

  • IntPtr -> nint 的寫法

總的來說這個關鍵詞不是最重要的,重要的是C#團隊對指針操作抱有前所未有的重視,這是一個非常积極的信號。

2. Pattern matching improvements

模式匹配這個不算是什麼新特性了,在本次C#9中也是繼續得到了完善,可能有很多朋友對模式匹配不是很熟悉,畢竟是C#7才有的新玩法,後面幾乎每一個新版本都在跟蹤完善,我先科普一下吧。

模式匹配到底解決了什麼問題

大家在編碼的過程中,不可能遇不到 if/else 嵌套 if/else 的這種情況,有時候嵌套甚至達到5,6層之多,特別影響代碼可讀性,我就來YY個例子。

現在各個地方都在發不同面值的消費券,為了實現千人千面,消費券的發放規則如下:

性別 年齡 地區 面值
<20 安徽 2000
<40 上海 4000
剩餘 剩餘 3000
<20 安徽 2500
<60 安徽 1500

如果用傳統的方式,你肯定要用各種花哨的if/else來實現,如下代碼:


        static decimal GetTicket(string sex, int age, string area)
        {
            if (sex == "男")
            {
                if (age < 20 && area == "安徽")
                {
                    return 2000;
                }
                else
                {
                    if (age < 40 && area == "上海")
                    {
                        return 4000;
                    }
                    else
                    {
                        return 3000;
                    }
                }
            }
            else
            {
                if (age < 20 && area == "安徽")
                {
                    return 2500;
                }
                if (age < 60 && area == "安徽")
                {
                    return 1500;
                }
            }

            return 0;
        }

這種代碼可讀性不是一般的差,就像大強子說的那樣:看着都想打人。。。 問題來了,這代碼還有救嗎??? 當然有了,這就需要用Pattern matching 去簡化,畢竟它就是為了這種問題而生的,修改后的代碼如下:


        static decimal GetTicket_Pattern(string sex, int age, string area)
        {
            return (sex, age, area) switch
            {
                ("男", < 20, "安徽") => 2000,
                ("男", < 40, "上海") => 4000,
                ("男", _, _) => 3000,
                ("女", < 20, "安徽") => 2500,
                ("女", < 60, "安徽") => 1500,
                _ => 0
            };
        }

看到這種化簡后的代碼是不是非常驚訝,這就是 Pattern matching 要幫你解決的場景,接下來看看底層的IL代碼是什麼樣子。

從圖中看,這反編譯后的代碼比我手工寫的還要爛,無力吐槽哈,當然 模式匹配 有各種千奇百怪的玩法,絕對讓你瞠目結舌,更多玩法可參考官方文檔:模式匹配

這個特性最重要的是你一定要明白它的客戶群在哪裡?

三: 總結

總的來說,這兩個特性都是比較實用的,尤其是 Pattern matching 化解了你多少不得不這麼寫的爛代碼,頭髮護理就靠它了,快來給它點個贊吧!

好了,先就這樣吧,感謝您的閱讀,希望本篇對你有幫助,謝謝。

如您有更多問題與我互動,掃描下方進來吧~

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

【其他文章推薦】

USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

台北網頁設計公司這麼多該如何選擇?

※智慧手機時代的來臨,RWD網頁設計為架站首選

※評比南投搬家公司費用收費行情懶人包大公開

※回頭車貨運收費標準

css3中的@font-face你真的了解嗎

css3中的自定義字體方法@font-face

@font-face屬性可以讓我們自定義網站字體屬性,然後引用到想要應用該字體的元素上。

基本語法:

@font-face {
  font-family: <font-name>;
  src: local( <family-name> ) | <url> [format("formatName")][,<url> [format("formatName")]]*;
  unicode-range: <unicode-range>;
  font-variant: <font-variant>;
  font-feature-settings: <font-feature-settings>;
  font-variation-settings: <font-variation-settings>;
  font-stretch: <font-stretch>;
  font-weight: <font-weight>;
  font-style: <font-style>;
  font-display: <font-display>;
}

屬性規則說明

font-family

給你引入的字體起一個專屬的字體名字,font-name,然後他會在元素font-family:中使用,如div{font-family:font-name}

src

用於指定加載字體文件的路徑或者加載本地字體

local

加載一個本地字體,font-name表示本地的字體名稱,比如Microsoft YaHei | 微軟雅黑;如果本地有應用此字體显示文本。

示例:

/* 加載一個本地字體 */
@font-face{
  font-family: myFont;
  src: local('Microsoft YaHei');
}
/* 加載多個本地字體 */
@font-face{
  font-family: myFont;
  src:  local(黑體), local("Microsoft YaHei"), local("HelveticaNeue-Light"), local("Helvetica Neue Light"),  local("PingFang SC"), local(sans-serif);
}
/* 應用自定義字體 */
.box{
  font-family: myFont;
}

在上邊代碼中看到,可以使用一個或多個local,多個之間用逗號分開,括號中的字體名稱可以使用單引號或者雙引號括起來,也可以不帶引號直接寫字體名稱,有空格的必須添加引號,但是只能寫一個字體名稱
上邊的寫法讓我們在定義字體的時候變得方便很多,我們只需要定義好自定義名稱然後直接引用該字體等同於下邊代碼:

.box{
  font-family: 黑體, "Microsoft YaHei", "HelveticaNeue-Light", "Helvetica Neue Light", "PingFang SC", sans-serif;
}

url

表示服務器端提供的字體地址,這個也是可以使用多個,多個之間用逗號隔開,一般寫多個是為了瀏覽器兼容加載不同格式的字體。目前web可以加載六種格式的字體:

  1. EOT:全拼:Embedded_OpenType,是由微軟開發的字體格式規範,所以只適用於IE瀏覽器。詳細介紹

兼容:

兼容詳情

  1. TTF:全拼:TrueType,是一種輪廓字體標準,最早是由蘋果公司研發,後來成為Mac OSMicrosoft Windows系統中最常用的字體格式。詳細介紹

兼容:

兼容詳情
3. OTF:全拼:OpenType,是可縮放計算機字體的格式,是由微軟和Adobe公司聯合開發。詳細介紹

兼容:

兼容詳情
4. WOFF:全拼:Web Open Font Formatweb網絡開放字體格式,他是專為網絡設計的一種字體格式,WOFF是把OpenTypeTrueType字體進行了封裝,並進行了壓縮優化,它使用了廣泛應用的zlib壓縮,並添加了XML元數據,這種字體格式體積更小,適用於網絡傳輸,可以使用戶體驗做到更好。詳細介紹

兼容:

兼容詳情
5. WOFF2:它是WOFF的升級版,它使用Brotli進行字節級壓縮,比WOFF體積更小

兼容:

兼容詳情

  1. SVG:全拼:Scalable Vector Graphics可縮放矢量圖形,是一種基於可擴展標記語言(XML)的矢量圖像格式,用於二維圖形,並支持交互性和動畫,字體中就是使用svg技術來呈現文字樣式。我測試只有蘋果Safari支持; 詳細介紹

兼容:

兼容詳情

format

可選值,表示給加載的外部字體指定字體格式,用來告訴瀏覽器讓瀏覽器能夠識別所使用的字體格式,可用的類型有 embedded-opentype, truetype, opentype, woff, woff2, svg。分別對應上邊我們介紹的字體格式。

語法:

/* 加載一種字體格式 */
@font-face{
  font-family: "myFontName";
  src:  url('font.woff') format('woff');
}

/* 加載多個字體格式,兼容更多瀏覽器 */
@font-face{
  font-family: "myFontName";
  src: url('font.eot'); /* IE9*/
  src: url('font.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
  url('font.woff2') format('woff2'),
  url('font.woff') format('woff'), /* chrome、firefox */
  url('font.ttf') format('truetype'), /* chrome、firefox、opera、Safari, Android, iOS 4.2+*/
  url('font.svg#Alibaba-PuHuiTi-Regular') format('svg'); /* iOS 4.1- */
}

從上邊語法來看我們可以加載一個格式的字體文件,也可以加載多個格式字體,之間用逗號分開,瀏覽器會優先讀取寫在前面的字體格式並且檢測是否支持,如果支持就使用該格式的字體文件。

font-weight

表示自定義字體規則的字重程度,我們可以給一個字體指定不同的粗細規則引用不同規格的字體文件。

語法:

/* Single values */
font-weight: normal;
font-weight: bold;
font-weight: 400;

/* Multiple Values */
font-weight: normal bold;
font-weight: 300 500;

取值說明:

  1. normal:默認值,表示該字體規則是在默認情況下的字體,也就是在應用改字體的元素中不規定字體的粗細情況或者font-weight: 400 | normal下應用該字體;
  2. bold:粗體,表示元素設置font-weight: bold | 700,或者使用<b><strong>元素的時候應用該字體。
  3. 400:也可以設置成數值,在CSS Fonts Level 4之前的版本只能去100-900的100倍數值,之後的數值可以去1-1000的任意數值。
  4. normal bold:可以使用多個關鍵字來定義此字體規則,多個關鍵字之間用逗號分開,表示元素字重設置為此關鍵字中的其中一個值時應用該字體。
  5. 300 500:也可以使用多個數值來定義此字體規則。

取數值情況下應該對應的每個字體:

value 對應的字體的自重名稱
100 Thin (Hairline)
200 Extra Light (Ultra Light)
300 Light
400 Normal
500 Medium
600 Semi Bold (Demi Bold)
700 Bold
800 Extra Bold (Ultra Bold)
900 Black (Heavy)

代碼示例:因為字體有版權限制,這裏我們使用阿里的免費商用字體來演示

https://codepen.io/qwguo88/full/jObgQYG

從上邊的案例我們可以看出,先自定義了一個名為FW的字體,並且使用font-weight定義不同字重使用不同的字體。在上邊的案例中定義了5中字重樣式,分別是bold阿里巴巴-普惠體-Heavy100楊任東竹石體-Bold200站酷高端黑300 600龐門正道標題體2900思源黑體-粗
然後給div設置font-family:FW;最後我們分別給這個div下的每個段落設置不同的font-weight,段落的字體就會根據不同的字重來應用不同的字體。
我們可以把自定義字體看成我們平常使用系統內置字體一樣,當我們設置字體為微軟雅黑,並且設置不同的字重他會在系統中尋找每個自重對應的字體,然後來显示。

font-style

表示自定義字體規則的樣式表現形式,我們可以給一個字體指定不同的樣式規則引用不同規格的字體文件。

語法:

font-style: normal | italic | oblique <angle>{0,2}

取值說明:

  1. normal:默認字樣式使用的字體規則,當我們不設置或者設置成此值時的字體。
  2. italic:表示字樣式設置成斜體的時候使用的字體規則。
  3. oblique:表示字樣式設置成斜體的時候使用的字體規則。

當我們同時定義italicoblique規則的字體時,寫在後邊的生效所設置的斜體字體显示。

代碼示例: https://codepen.io/qwguo88/full/RwWXONo

unicode-range

表示自定義字體規則的unicode字符範圍

語法:

/* unicode-range 取值規則 */
unicode-range: U+26;                /* 單個值 */
unicode-range: U+0-7F;              /* 字符編碼區間*/
unicode-range: U+0025-00FF;        /* 字符編碼區間 */
unicode-range: U+4??;              /* 通配符區間 */
unicode-range: U+0025-00FF, U+4??; /* 可以寫多個值,多個值之間用逗號分開 */

取值說明:
取值規則:前邊是U+後邊跟上字符的charCode

  1. 可以是單個值,表示文本中只有該字符的字應用該字體。
  2. 可以使用一個字符區間,表示文本中如果有在此區間的文字將應用改字體規則。
  3. 也可以使用通配符來設置一個區間規則其中?表示一個16進制0-F的之間的值U+4??表示 U+400U+4FF區間的字符編碼。
  4. 也可以使用多個值,多個值之間使用逗號分開。

案例:https://codepen.io/qwguo88/full/XWXWqmP

從上邊案例可以看出,unicode-range是用來規定應用當前字體規則的文字unicode碼在規則內的將以此字體規則显示字體。
他能讓我們來控制一個段落中的個別字的显示效果,一般要显示的字體規則排在最前面,將優先显示。

font-display

設置自定義字體在沒有加載完的显示方式取值如下:

語法:

font-display: auto | block | swap | fallback | optional
  1. auto:字體显示策略由用戶代理定義。
  2. block:為字體提供一個短暫的阻塞周期和無限的交換周期。也就是說等字體加載完以後字體显示效果會自動更新成改字體
  3. swap:為字體提供一個非常小的阻塞周期和無限的交換周期。也就是說等字體加載完以後字體显示效果會自動更新成改字體
  4. fallback:為字體提供一個非常小的阻塞周期和短暫的交換周期。也就是說等字體加載在過了一定的交互周期后加載完字體將不進行更新显示
  5. optional:為字體提供一個非常小的阻塞周期,並且沒有交換周期。也就是說等字體加載不進行更新显示

參考網站

  1. https://webplatform.github.io/docs/tutorials/typography/font-face/
  2. https://developer.mozilla.org/zh-CN/docs/Web/CSS/@font-face/font-display
  3. https://www.zhangxinxu.com/wordpress/2016/11/css-unicode-range-character-font-face/
  4. https://www.w3cplus.com/css/font-display-masses.html

字體下載格式轉換網站

  1. https://www.fontke.com/tool/fontface/
  2. http://www.fonts.net.cn/
  3. https://fonts.google.com/

字體壓縮工具

  1. http://www.fonts.net.cn/ 字體天下
  2. http://www.ziticq.com/ 字體傳奇
  3. https://www.hellofont.cn/ 字由
  4. http://fontstore.baidu.com/static/editor/index.html 百度在線字體編輯器
  5. https://efe.baidu.com/ 百度字體處理
  6. https://www.fontsquirrel.com/tools/webfont-generator 字體格式換
  7. https://www.fontke.com/tool/fontface/ 字體轉換

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

【其他文章推薦】

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

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

Flutter 中由 BuildContext 引發的血案

今天和各位分享一個博主在實際開發中遇到的問題,以及解決方法。廢話不多說,我們先來看需求:
我們要做一個iOS風格的底部菜單彈出組件,具體涉及showCupertinoModalPopup()方法,該方法被執行后,會出現如下圖類似所示的菜單彈出視圖:

相信這個彈出菜單視圖都有見過吧?下面重點來了:在本次的項目需求中,該視圖的選項文字是由Server端返回的。也就是說,這些選項的內容和個數都不固定,因此不能將其在代碼中寫固定值。
為了簡化代碼以突出重點,下面放上我在一開始的實現方案:

  openActionSheet() {
    List<Widget> menuWidgets = new List();
    menuItems.forEach((element) {
      menuWidgets.add(CupertinoActionSheetAction(
        child: Text(element),
        onPressed: () {
          Navigator.pop(context);
          debugPrint("操作$element被執行“);
        },
        isDefaultAction: true,
      ));
    });

    showCupertinoModalPopup(
        context: context,
        builder: (buildContext) {
          return CupertinoActionSheet(
              title: Text('測試菜單'),
              message: Text('點擊菜單項試試吧!'),
              actions: menuWidgets);
        });
  }

如上述代碼所示,openActionSheet()是显示該組件的方法。其中,showCupertinoModalPopup()為Flutter SDK內置方法,其作用即显示這個組件;再其上面的循環以及List聲明、賦值等操作實際上就是在動態添加菜單項。menuItems類型是List<String>。
通過對代碼的解釋,相信大家能夠一目瞭然地看出,當某個菜單項被點擊時,整個菜單組件消失,並打印Debug Log(對應為真實項目要執行的操作)。
大家覺得上述代碼有問題嗎?如果有問題,問題在哪兒呢?
現在公布答案:這段代碼有問題!
上述代碼執行時,當用戶點擊菜單項后,其運行結果並非如我們預想的那樣:菜單組消失並輸出Log,而變成了:整個頁面被Pop,菜單組保留,並輸出Log!
這是什麼原因呢?
實際上,罪魁禍首就在我們循環遍歷賦值操作時的這條語句:

Navigator.pop(context);

這裏的context是整個頁面的BuildContext,而非菜單組的。這裏我們要明確一個概念——我們想Pop誰,一定要用誰的BuildContext對象。
在這裏,正確的BuildContext對象是誰呢?它在這裏:

showCupertinoModalPopup(
    context: context,
    builder: (buildContext) {
      return CupertinoActionSheet(
          title: Text('測試菜單'),
          message: Text('點擊菜單項試試吧!'),
          actions: menuWidgets);
    }
);

注意到了嗎?上面第三行括號里的buildContext才是我們真正要用的對象。因此,正確的做法是什麼呢?

  openActionSheet() {
    BuildContext tempContext;
    List<Widget> menuWidgets = new List();
    menuItems.forEach((element) {
      menuWidgets.add(CupertinoActionSheetAction(
        child: Text(element),
        onPressed: () {
          Navigator.pop(tempContext);
          debugPrint("操作$element被執行");
        },
        isDefaultAction: true,
      ));
    });

    showCupertinoModalPopup(
        context: context,
        builder: (buildContext) {
          tempContext = buildContext;
          return CupertinoActionSheet(
              title: Text('測試菜單'),
              message: Text('點擊菜單項試試吧!'),
              actions: menuWidgets);
        });
  }

如上所示,我們只需將正確的對象“帶”到其作用域外面就可以了。
好了,這就是本篇文章的全部內容,希望能夠對你有所幫助!

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

【其他文章推薦】

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

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

網頁設計最專業,超強功能平台可客製化