終于要寫點干貨了,其實思考了很久下面一篇文章要寫什么,主要的糾結點在于,既想要分享那些精美的知識,又怕這些知識不太好嚼。后來想想還是對初學者不太好友算了..一來這系列文章叫做學習筆記,我的。另外寫得足夠有料,才能發揮筆記的作用,不然索然無味的,連收藏、喜歡的意義也沒有了。
寫在文章之前
終于寫點干貨了,想先簡單談談自己的一些看法。對于我自己而言,我比較厭煩那些繁瑣的無聊的知識點,反而更在乎一些實際應用的東西。但了解一些底層的東西是非常有意義的,它有助于我們理解程序。
每一點知識的積累,終會有用武之地。也許,它會使您在面試過程中正確地回答一道面試題;也許,它會讓您更加清楚Java底層的實現方式;也許,它能讓您在學業上感到更加充實...(以上摘自梁勇著的Java深入解析_前言)
Java中的數據類型
Java是一種強類型的語言。這意味著必須為每一個變量都聲明一種類型。
在Java中,你可以把數據類型分為兩部分,一部分是基本類型(primitive type):4種整形、2種浮點類型、1種用于表示Unicode編碼的字符單元的字符類型char和1種用于表示真值的boolean類型。
另外一部分是引用類型(reference type),如String和List。每個基本類型都有一個對應的引用類型,稱作裝箱基本類型(boxed primitive)。裝箱基本類中對應于int、double、boolean的是Integer、Double和Boolean。
Java中的特例
Java是一種完全面向對象的語言,從理論上來說,在Java中應該不存在對象以外的事務,即所有的類型都是對象。然而,在Java8中的8種基本數據類型不是對象,之所以這樣設計,是因為相對于對象來說,基本數據在使用上更加方便,并且在效率上也高于對象類型。所以這就需要去了解一下Java中創建對象的過程。
創建對象的過程
當程序運行時,對象是怎么進行安排放置的呢?特別是內存是怎樣分配的呢?
Java大體上會把內存分為四塊區域:堆、棧、靜態區、常量區。
- 堆 : 位于RAM中,用于存放所有的java對象。
- 棧 : 位于RAM中,引用就存在于棧中。
- 靜態區: 位于RAM中,被static修飾符修飾的變量會被放在這里
- 常量區:位于ROM中, 很明顯,放常量的。(其實常量通常直接存放在程序代碼的內部,因為這樣非常安全,因為它們永遠都不會被改變)
所以當我們創建對象,例如實例化一個Person類:
Person p = new Person()
首先,會在堆中開辟一塊空間存放這個新來的Person對象。然后,會創建一個引用p,存放在棧中,這個引用p指向Person對象(事實上是,p的值就是Person對象的內存地址)。
這樣,我們通過訪問p,然后得到了Person的內存地址,進而找到了Person對象。
然后又有了這樣一句代碼:
Person p2 = p;
這句代碼的含義是:
創建了一個新的引用p2,保存在棧中,引用的地址也指向Person的地址。這個時候,你通過p2來改變Person對象的狀態,也會改變p的結果。因為它們指向同一個對象。(String除外,之后會專門講String)
此時,內存中是這樣的:
有一個很通俗的方式來講解引用和對象。大家對于快捷方式應該不會陌生吧?我們桌面的圖標大部分都是快捷方式。它并不是我們安裝在電腦上的應用的可執行文件(不是.exe文件),那么為什么點擊它可以打開應用程序呢?是因為快捷方式連接了文件,這就像是引用和對象的關系了。
我們不直接對文件進行操作,而是通過快捷方式來進行操作。快捷方式不能獨立存在,同樣,引用也不能獨立存在(你可以只創建一個引用,但是當你要使用它的時候必須得給它賦值,否則它將毫無用處)。
一個文件可以有多個快捷方式,同樣一個對象也可以有多個引用。而一個引用只能同時對應一個對象。
在java里,“=”不能被看成是一個賦值語句,它不是在把一個對象賦給另外一個對象,它的執行過程實質上是將右邊對象的地址傳給了左邊的引用,使得左邊的引用指向了右邊的對象。java表面上看起來沒有指針,但它的引用其實質就是一個指針。在java里,“=”語句不應該被翻譯成賦值語句,因為它所執行的確實不是一個簡單的賦值過程,而是一個傳地址的過程,被譯成賦值語句會造成很多誤解,譯得不準確。
特例:基本數據類型
為什么要有特例呢?是因為new將對象存儲在“堆”里,一是用new創建一個對象——特別是小的,簡單的變量(Java中數據定長,為了可移植性)往往不是很明智而且有效的方法,二是因為“堆”空間本來就有限,如果頻繁的操作會導致不可想象的錯誤,并且別忘了第一篇文章里面提到的,Java的設計初衷是什么。
所以針對這些類型,Java采取了與C和C++相同的方法,也就是說,不用new來創建變量,二是創建一個并非是引用的“自動”變量。這個變量直接存儲“值”并置于常量區中,因此更加高效。
先來看一個例子:
int i = 2;
int j = 2;
我們需要知道的是,在常量區中,相同的常量只會存在一個。當執行第一句代碼時。先查找常量區中有沒有2,沒有,則開辟一個空間存放2,然后在棧中存入一個變量i,讓i指向2;
執行第二句的時候,查找發現2已經存在了,所以就不開辟新空間了。直接在棧中保存一個新變量j,讓j指向2;
當然,java堆每一個基本數據類型都提供了對應的包裝類。我們依舊可以用new操作符來創建我們想要的變量。
Integer i = new Integer(1);
Integer j = new Integer(1);
但是,用new操作符創建的對象是不同的,也就是說,此時,i和j指向不同的內存地址。因為每次調用new操作符,都會在堆開辟新的空間。
深入了解Integer
來看一個例子:
第一個返回true很好理解,就像上面講的,a和b指向相同的地址。
第二個返回false是為什么呢?下面細說
第三個返回false是因為用了new關鍵字來開辟了新的空間,i和j兩個對象分別指向堆區中的兩塊內存空間。
我們可以跟蹤一下Integer的源碼,看看到底怎么回事。在IDEA中,你只需要按住Ctrl然后點擊Integer,就會自動進入jar包中對應的類文件。
跟蹤到文件的700多行,你會看到這么一段,感興趣可以仔細讀一下,不用去讀也沒有關系,因為你只需要知道這是Java的一個緩存機制。Integer類的內部類緩存了-128到127的所有數字。(事實上,Integer類的緩存上限是可以通過修改系統來更改的。了解就行了,不必去深究。)
為什么引入緩存機制
這回到了為什么引入基礎類型這個特例的問題上。我們看看Java語言規范是怎么規定的:
If the value p being boxed is an integer literal of type int between -128 and 127 inclusive (§3.10.1**), or the boolean literal true or false (§3.10.3**), or a character literal between '\u0000' and'\u007f' inclusive (§3.10.4**), then let a and b be the results of any two boxing conversions of p. It is always the case that a == b.
Ideally, boxing a primitive value would always yield an identical reference. In practice, this may not be feasible using existing implementation techniques. The rule above is a pragmatic compromise, requiring that certain common values always be boxed into indistinguishable objects. The implementation may cache these, lazily or eagerly. For other values, the rule disallows any assumptions about the identity of the boxed values on the programmer's part. This allows (but does not require) sharing of some or all of these references. Notice that integer literals of type long are allowed, but not required, to be shared.
This ensures that in most common cases, the behavior will be the desired one, without imposing an undue performance penalty, especially on small devices. Less memory-limited implementations might, for example, cache all char and short values, as well as int and long values in the range of -32K to +32K.
事實上,不光是Integer這么特別,還包括boolean還有char類型。并且文章的最后提到了為了實現更少內存的可能。
另一個特例:String
String是一個特殊的類,因為它被final修飾符所修飾,是一個不可改變的類。當然,看過java源碼后你會發現,基本類型的各個包裝類也被final所修飾。這里以String為例。
我們來看這樣一個例子:
執行第一句 : 常量區開辟空間存放“abc”,s1存放在棧中指向“abc”
執行第二句,s2 也指向 “abc”,
執行第三句,因為“abc”已經存在,所以直接指向它。
所以三個變量指向同一塊內存地址,結果都為true。
當s1內容改變的時候。這個時候,常量區開辟新的空間存放“bcd”,s1指向“bcd”,而s2和s3指向“abc”所以只有s2和s3相等。
這種情況下,s1,s2,s3都是字符串常量,類似于基本數據類型。(如果執行的是s1 = "abc",那么結果會都是true)
我們再看一個例子:
執行第一行代碼: 在堆里分配空間存放String對象,在常量區開辟空間存放常量“abc”,String對象指向常量,s1指向該對象。
執行第二行代碼:s2指向上一步new出來的string對象。
執行第三行代碼: 在堆里分配新的空間存放String對象,新對象指向常量“abc”,s3指向該對象。
到這里,很明顯,s1和s2指向的是同一個對象接著就很詭異了,我們讓s1 依舊= “abc",但是結果s1和s2指向的地址不同了。
怎么回事呢?這就是String類的特殊之處了,new出來的String不再是上面的字符串常量,而是字符串對象。
由于String類是不可改變的,所以String對象也是不可改變的,我們每次給String賦值都相當于執行了一次new String(),然后讓變量指向這個新對象,而不是在原來的對象上修改。
當然,java還提供了StringBuffer類,這個是可以在原對象上做修改的。如果你需要修改原對象,那么請使用StringBuffer類。
引發的問題:值傳遞還是引用傳遞?
java是值傳遞還是引用傳遞的呢?毫無疑問,java是值傳遞的。那么什么又叫值傳遞和引用傳遞呢?
我們先來看一個例子:
這是一個很經典的例子,我們希望調用了swap函數以后,a和b的值可以互換,但是事實上并沒有。為什么會這樣呢?
這就是因為java是值傳遞的。也就是說,我們在調用一個需要傳遞參數的函數時,傳遞給函數的參數并不是我們傳進去的參數本身,而是它的副本。說起來比較拗口,但是其實原理很簡單。我們可以這樣理解:
一個有形參的函數,當別的函數調用它的時候,必須要傳遞數據。比如swap函數,別的函數要調用swap就必須傳兩個整數過來。
這個時候,有一個函數按耐不住寂寞,扔了兩個整數過來,但是,swap函數有潔癖,它不喜歡用別人的東西,于是它把傳過來的參數復制了一份,然后對復制的數據修修改改,而別人傳過來的參數動根本沒動。
所以,當swap函數執行完畢之后,交換了的數據只是swap自己復制的那一份,而原來的數據沒變。
也可以理解為別的函數把數據傳遞給了swap函數的形參,最后改變的只是形參而實參沒變,所以不會起到任何效果。
我們再來看一個復雜一點的例子(Person類添加了get,set方法):
可以看到,我們把p1傳進去,它并沒有被替換成新的對象。因為change函數操作的不是p1這個引用本身,而是這個引用的一個副本。
你依然可以理解為,主函數將p1復制了一份然后變成了chagne函數的形參,最終指向新Person對象的是那個副本引用,而實參p1并沒有改變。
再來看一個例子:
這次為什么就改變了呢?分析一下。
首先,new了一個Person對象,暫且叫他小明吧。然后p1指向小明。
小明10歲了,隨著時間的推移,小明的年齡要變了,調用了一下changgeAge方法,把小明的引用傳了進去。
傳遞的過程中,changgeAge也有潔癖,于是復制了一份小明的引用,這個副本也指向小明。
然后changgeAge通過自己的副本引用,改變了小明的年齡。
由于是小明這個對象被改變了,所以所有小明的引用調用方法得到的年齡都會改變
所以就變了。
最后簡單的總結一下。
java的傳值過程,其實傳的是副本,不管是變量還是引用。所以,不要期待把變量傳遞給一個函數來改變變量本身。
“+”是怎么連接字符串的?
先拋個磚:對Java程序員來說,使用運算符“+”來連接字符串是非常普遍的,當“+”兩邊的操作數是String類型時(如果只有一個操作數是String類型,則系統也會將另外一個操作數轉換成String類型),就會執行字符串連接的運算。但是,運算符“+”是怎樣連接String對象的呢?編譯器又是如何實現的呢?
之后我再來補這個內容,先發表啦。
浮點類型
浮點類型用于表示有小數部分的數值。在Java中有兩種浮點類型,一個是4字節的float,一個是8字節的double。我們平時用來編寫程序用來表示增長率、物品重量等方面也非常有用。不過,在使用浮點類型時,也需要留意一些問題。
浮點類型只是近似的存儲
請問一個問題:0.1+0.2等于多少?請不要慌著報答案,我沒有開玩笑的意思,看一下Java給出的答案你就知道了:
結果似乎有些令人驚訝,這么簡單的算術竟然也會算錯。
其實,這并不是計算錯誤,這只是浮點數類型存儲的問題。計算機使用二進制來存儲數據,而二進制無法準確的表示分數 1/10 ,就像使用十進制時,無法準確地表示 1/3 一樣。
數量級差很大的浮點運算
當浮點數值的數量級相差很大的時候,運算又會有什么問題呢?
又發生了預期外的結果。從輸出結果來看,f3竟然和f4是相等的,也就是意味著對f3+1并沒有改變f3的值。
這同樣是因為浮點數的存儲造成的,二進制所能表示的兩個相鄰的浮點值之間存在一定的空隙。浮點值越大,這個間隙也會越大。當浮點值大道一定程度的時,如果對浮點值的改變很?。ɡ缟厦娴?0000000+1),就不足以使浮點值發生改變。就好比蒸發掉大海中的一滴水,大海還是大海,幾乎不存在變化。
如果想要準確的存儲,就去使用BigDecimal吧,有必要了解的可以去自行百度,這里就不做過多介紹了,已經是Java封裝好的類庫了
拋出一個有趣的問題
我們知道,在Java中,long類型占用了8個字節,float類型占用了4個字節。
照理來說,long類型的容量應該比float大許多,然而事實正好相反,float反而擁有比8字節long類型更大的取值范圍。這同樣是因為浮點數的存儲格式造成的。有興趣的可以去自行百度了解。
參考資料:
http://www.lxweimin.com/p/39753aad9a38 ,原文作者:CleverFan
《Java深入解析》——梁勇著
《Effective Java》——第二版
《Java核心技術 卷I》——第九版
《Java編程思想》——第四版
歡迎轉載,轉載請注明出處!
簡書ID:@我沒有三顆心臟
github:wmyskxz
歡迎關注公眾微信號:wmyskxz
分享自己的學習 & 學習資料 & 生活
想要交流的朋友也可以加qq群:3382693