一、JAVA基礎-基本類型與對應的包裝類型


1.基本數據類型8個,其余都是引用類型
 
六種數字類型(4個整數類型,2個浮點型),一種字符型,一種bool型
 
2.包裝類型
每個基本類型,有對應的包裝類型,包裝類型提供對象的最大值、最小值及對象的相關操作。
 
 
值類型一般存在棧中,引用類型一般存在堆中、
 
基本類型
字節數
位數
最大值
最小值
包裝類
boolean
       
Boolean
byte
1byte
8bit
2^7 - 1
-2^7
Byte
short
2byte
16bit
2^15 - 1
-2^15
Short
int
4byte
32bit
2^31 - 1
-2^31
Integer
long
8byte
64bit
2^63 - 1
-2^63
Long
float
4byte
32bit
3.4028235E38
1.4E - 45
Float
double
8byte
64bit
1.7976931348623157E308
4.9E - 324
Double
char
2byte
16bit
2^16 - 1
0
Character
 
F:\java demo\基本數據類型\demo1>javac demo1.java
F:\java demo\基本數據類型\demo1>java demo1
基本類型:byte 二進制位數:8
包裝類:java.lang.Byte
最小值:Byte.MIN_VALUE=-128
最大值:Byte.MAX_VALUE=127
 
基本類型:short 二進制位數:16
包裝類:java.lang.Short
最小值:Short.MIN_VALUE=-32768
最大值:Short.MAX_VALUE=32767
 
基本類型:int 二進制位數:32
包裝類:java.lang.Integer
最小值:Integer.MIN_VALUE=-2147483648
最大值:Integer.MAX_VALUE=2147483647
 
基本類型:long 二進制位數:64
包裝類:java.lang.Long
最小值:Long.MIN_VALUE=-9223372036854775808
最大值:Long.MAX_VALUE=9223372036854775807
 
基本類型:float 二進制位數:32
包裝類:java.lang.Float
最小值:Float.MIN_VALUE=1.4E-45
最大值:Float.MAX_VALUE=3.4028235E38
 
基本類型:double 二進制位數:64
包裝類:java.lang.Double
最小值:Double.MIN_VALUE=4.9E-324
最大值:Double.MAX_VALUE=1.7976931348623157E308
 
基本類型:char 二進制位數:16
包裝類:java.lang.Character
最小值:Character.MIN_VALUE=0
最大值:Character.MAX_VALUE=65535
 
 
3.demo
 
 
4.討論
基本類型強制轉換
 
隱式類型轉換
byte->short-> int ->float,long -> double
char-> int ->float,long -> double
注意:
  • byte不能隱式轉化為char
  • short和char之間不能相互轉換
  • float和long之間不能相互轉換
  • boolean不能隱式轉換為任何其他類型
隱式類型轉換舉例:
(1)short s1 = 1;s1 = s1 + 1;
編譯不能通過,因為1默認為int類型,s1+1運算后的結果是int類似,再賦值給short類型就會出錯。
(2)ReturnType method(byte x, double y) {
return (short)x/y*2;
}
返回類型應該是double類型。因為x強制轉換為short類型后,除以double類型的y,結果會自動升級為double類型。
注:強制轉換比四級運算的優先級高。
 
二.強制類型轉換
使用強制類型轉換時,可能會失去精度,所以在進行強制類型轉換之前,一定要注意轉換后的值是否與轉換前一致。
注意:
在Java中,默認小數的類型為double,如果要將小數定義為float,就必須通過下面的兩種方法將其轉化為float類型,否則編譯無法通過。
float f = 3.1f;
float f = (float)3.1;
 
hashcode
 
本文中涉及到的JDK源碼基於JDK1.8_102.
Boolean的hashCode()方法源碼如下:
public static int hashCode(boolean value) { return value ? 1231 : 1237; }
 
為什么是1231、1237?很奇怪是不是?
結論
直接先上結論,因為1231和1237是兩個比較大的素數(質數),實際上這里采用其它任意兩個較大的質數也是可以的.而采用1231和1237更大程度的原因是Boolean的作者個人愛好(看到這句別打我).
如果你看懂了上面這段話,或者你對hash碰撞有了解,並且清楚計算hashCode的散列算法設計原則,以及為什么常見的散列算法大都采用了31作為乘法因子,那么可以直接關閉這篇文章.
如果你沒看懂或者不清楚,那下面我來一步一步來剖析,能用JDK源碼的我盡量用源碼.
分析
hashCode是用來干嘛的
首先先要明白,hashCode是用來干嘛的.
/* * Returns a hash code value for the object. This method is * supported for the benefit of hash tables such as those provided by * {@link java.util.HashMap}. * ... * /
 
以上內容來自java.lang.Object的hashCode()方法.
根據這個,我們得出結論,hashCode的目的就是為了提高諸如java.util.HashMap這樣的散列表(hash table)的性能.
hashCode是怎樣影響散列表的性能的–以HashMap為例分析
既然是服務於hash table,那么我們干脆就以上面注釋里面提到的也是最常用的HashMap為例來說明.
通過查看源碼,可以清楚的看到hashCode主要是在對HashMap進行put和get的時候使用,就以put來說明,畢竟沒put哪來get.
public V put(K key, V value) { return putVal(hash(key), key, value, false, true); }
 
調用的是putVal方法,再看putVal.下面是putVal的源碼,可以先跳過直接往下看.
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) { Node<K,V>[] tab; Node<K,V> p; int n, i; if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length; if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else { Node<K,V> e; K k; if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p; else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value); else { for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNode(hash, key, value, null); if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; } if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } } if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount; if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }
 
其實只看這兩行就行.
if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else{ //省略.. }
 
其中:
(n - 1) & hash == hash % n;
 
其中hash就是我們調用put(key,value)方法時候的key,table是HashMap實際存儲時用的對象,本質上是個散列表,n為table的size.
HashMap在設計的時候,是希望put進去的對象均勻的分布在table里,即散列表table里第i個位置至多存放一個put對象,
在這個時候進行查找效率是最優的,因為只需要根據key的hashCode,計算出i,table[i]就是要查找的value.
但是,如果table[i]存放的對象超過了一個,table[i]的地方就會變成一個鏈表,在根據key的hashCode找到table[i]后還要在遍歷一遍數組才能找到對應的對象,效率就會變得比較糟糕,假設一種比較極端的情況,所有的對象都放在table[0]的位置,那么整個HashMap就相當於變成了一個鏈表,這就完全背離了HashMap的設計初衷.
在put時,i就是上面這段代碼計算出的,可以看到在不考慮table擴容的情況下,唯一的決定因素就只有hash(put進來的key的hashCode).
那么我們就得出個結論,hashCode會決定對象在HashMap中的存放位置,以此影響性能,只要保證key的hashCode不重復,就能避免出現hash碰撞的情況,進而保證HashMap性能最優.
問題又來了,如何保證hashCode盡可能的不重復呢,這就是下面要說的問題.
hashCode散列算法設計簡要分析
一般hashCode()方法的實現里其實就是一個散列算法,每個對象的hashCode()方法都不盡相同.
先來看下hashCode散列算法的原則,依舊來自java.lang.Object的hashCode()方法的注釋.我就不貼原文了,直接上譯文:
*<li>在一個Java應用運行期間,只要一個對象的{@code equals}方法所用到的信息沒有被修改, * 那么對這同一個對象調用多次{@code hashCode}方法,都必須返回同一個整數. * 在同一個應用程序的多次執行中,每次執行所返回的整數可以不一樣. * <li>如果兩個對象根據{@code equals(Object)}方法進行比較結果是相等的,那么調用這兩個對象的 * {@code hashCode}方法必須返回同樣的結果. * <li>如果兩個對象根據{@link java.lang.Object#equals(java.lang.Object)}進行比較是不相等的, * 那么並<em>不</em>要求調用這兩個對象的{@code hashCode}方法必須返回不同的整數結果. * 但是程序員應該了解到,給不相等的對象產生不同的整數結果,可能提高散列表(hash tables)的性能.
 
簡要的說,在一個應用的一次運行期間,equals為true的對象必須返回相同的hashCode,而equals為false的對象不是必須返回不同的hashCode.但是應當讓不同的對象返回不同的hashCode.
再來扒一扒JDK的源碼,比如String的是這樣實現的:
public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; }
 
IntBuffer中是這樣實現的:
public int hashCode() { int h = 1; int p = position(); for (int i = limit() - 1; i >= p; i--) h = 31 * h + get(i); return h; }
 
再比如Array中,其中一個的實現是這樣的:
public static int hashCode(int a[]) { if (a == null) return 0; int result = 1; for (int element : a) result = 31 * result + element; return result; }
 
而利用開發工具生成的hashCode方法,比如IDEA生成的,是這樣的:
@Override public int hashCode() { int result = id != null ? id.hashCode() : 0; result = 31 * result + (enable != null ? enable.hashCode() : 0); result = 31 * result + (remark != null ? remark.hashCode() : 0); result = 31 * result + (createBy != null ? createBy.hashCode() : 0); result = 31 * result + (createTime != null ? createTime.hashCode() : 0); result = 31 * result + (updateBy != null ? updateBy.hashCode() : 0); result = 31 * result + (updateTime != null ? updateTime.hashCode() : 0); return result; }
 
eclipse生成的跟IDEA的基本一樣,不再浪費篇幅.
可以看到這些散列算法在計算hashCode的時候,除了對組成該對象的每個因子進行循環迭代(實際上這些參與循環迭代的因子也是該對象的equals方法進行比較時要參與對比的)–例如String的hashCode方法中對字符串的每個字符進行了循環迭代,IntBuffer中對每一位進行了循環迭代–之外,還有一個很明顯的地方就是,每次循環的時候,都對前一次循環計算出的hashCode乘以31.
這些散列算法的結果等價於下面這個表達式:
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
 
其中s[n]為參與散列計算的第n個因子.
為什么是31?
因為31是一個素數.
那么為什么要采用素數?為什么要是31而不是其它素數?
一個一個來.
為什么是素數
還是拿最常用的HashMap來說,還記得前面說的put(key,value)方法中,table[i]中下標i是怎么得來的不?忘了沒關系,代碼再貼下:
if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else{ //省略.. }
 
下標i就是(n - 1) & hash,它等價於hash % n,那么要保證hash % n盡可能的不一樣.
鑒於一般情況下n為偶數(下文中會有說明),那么就需要盡可能的保證
對於hash的值,前面說過,常用的散列算法的計算結果等價於這個:
s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] //用常量M替換下31就是 s[0]*M^(n-1) + s[1]*M^(n-2) + ... + s[n-1] //即 hash = s[0]*M^(i-1) + s[1]*M^(i-2) + ... + s[i-1]
 
對於n的取值
繼續看HashMap源碼,HashMap的構造方法:
public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); }
 
內在實現是調用了tableSizeFor方法:
/** * Returns a power of two size for the given target capacity. */ static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
 
這段代碼的作用就是檢查n,如果n為2^x,那么就返回n,否則就返回比n大的最小的正整數2^x,當然這一切都要保證不超出MAXIMUM_CAPACITY即2^30.
這就意味着,存放HashMap數據的散列表的大小(不是HashMap的size),一定是2的x次方.
這樣我們的問題就變成了:
hash = s[0]*M^(i-1) + s[1]*M^(i-2) + ... + s[i-1]; n = 2^x; 求在hash % n余數最多的情況下M的取值.
 
M肯定是越大越好,而素數在做取模運算時,余數的個數是最多的.
而如果M為素數,上面的hash的計算表達式里相當於每項都有了素數,那么hash % n時也就近似相當於素數對n取模,這個時候余數也就會盡可能的多.關於這個結論我是找了個數學系的博士特意求證了下.
那么我們就可以得出個結論,常量M越大越好,且需要是個素數.
為什么是31
素數那么多,為什么要選擇31?
根據為什么是素數這一部分的結論,我們應該選擇一個盡可能大的素數,但是實際上,又不能選擇太大的素數.原因在於hashCode的值為int類型,計算結果不能溢出啊.
所以這個素數不能太小,但也不能太大.
選擇31,除了Effective Java一書中提到的計算機計算31比較快(可以直接采用位移操作得到 1<<5-1)之外,個人認為還有一個原因:Java是美帝人編寫的語言,再加上大多數情況下我們都是采用String作為key,曾有人對超過5W個英文單詞做了測試,在常量取31情況下,碰撞的次數都不超過7次(本人沒去驗證).采用31已經足夠了.
為什么Boolean的又是1231和1237
回到本文標題提出的問題,為什么會是1231和1237,Boolean只有true和false兩個值,為什么不能是3或者7,或者其它的素數?
誠然,Boolean只有true和false兩個值,理論上任何兩個素數都可以.但是在實際使用時,可能作為key的不只是Boolean一種類型啊,可能還會有其它類型,比如最常見的字符串作為key,還有int作為key.至少要保證避開常見hashCode的取值范圍吧,Integer還緩存了常用的256個數字着呢…但是太大了也沒意義,比如說字符串”00”的hashCode為1536,Boolean的hashCode取值太大的話,指不定又跟字符串的hashCode撞上了,更別說其它對象的了.
所以Boolean的hashCode取值也是一個不能太小也不能太大的事情,至於取值1231和1237就真的沒有什么數學上的依據了,更大程度上就是Boolean作者個人愛好罷了.
 
 
 
自動裝箱拆箱
實驗1
  1. Integer integer400=400;
  2. int int400=400;
  3. System.out.println(integer400==int400);
在以上代碼的第三行中,integer400與int400執行了==運行。而這兩個是不同類型的變量,到底是integer400拆箱了,還是int400裝箱了呢?運行結果是什么呢?
==運算是判斷兩個對象的地址是否相等或者判斷兩個基礎數據類型的值是否相等。所以,大家很容易推測到,如果integer400拆箱了,則說明對比的是兩個基礎類型的值,那此時必然相等,運行結果為true;如果int400裝箱了,則說明對比的是兩個對象的地址是否相等,那此時地址必然不相等,運行結果為false。(至於為什么筆者對它們賦值為400,就是后面將要講到的陷阱有關)。
我們實際的運行結果為true。所以是integer400拆箱了。對代碼跟蹤的結果也證明這一點。
實驗2
  1. Integer integer100=100;
  2. int int100=100;
  3. System.out.println(integer100.equals(int100));
在以上代碼的第三行中,integer100的方法equals的參數為int100。我們知道equals方法的參數為Object,而不是基礎數據類型,因而在這里必然是int100裝箱了。對代碼跟蹤的結果也證明了這一點。
其實,如果一個方法中參數類型為原始數據類型,所傳入的參數類型為其封裝類,則會自動對其進行拆箱;相應地,如果一個方法中參數類型為封裝類型,所傳入的參數類型為其原始數據類型,則會自動對其進行裝箱。
實驗3
  1. Integer integer100 = 100;
  2. int int100 = 100;
  3. Long long200 = 200l;
  4. System.out.println(integer100 + int100);
  5. System.out.println(long200 == (integer100 + int100));
  6. System.out.println(long200.equals(integer100 + int100));
 
在第一個實驗中,我們已經得知,當一個基礎數據類型與封裝類進行==運算時,會將封裝類進行拆箱。那如果+、-、*、/呢?我們在這個實驗中,就可知道。
如果+運算,會將基礎數據類型裝箱,那么:
  • 第4行中,integer100+int100就會得到一個類型為Integer且value為200的對象o,並執行這個對象的toString()方法,並輸出”200”;
  • 第5行中,integer100+int100就會得到一個類型為Integer且value為200的對象o,==運算將這個對象與long200對象進行對比,顯然,將會輸出false;
  • 第6行中,integer100+int100就會得到一個類型為Integer且value為200的對象o,Long的equals方法將long200與o對比,因為兩都是不同類型的封裝類,因而輸出false;
如果+運算,會將封裝類進行拆箱,那么:
  • 第4行中,integer100+int100就會得到一個類型為int且value為200的基礎數據類型b,再將b進行裝箱得到o,執行這個對象的toString()方法,並輸出”200”;
  • 第5行中,integer100+int100就會得到一個類型為int且value為200的基礎數據類型b1,==運算將long200進行拆箱得到b2,顯然b1==b2,輸出true;
  • 第6行中,integer100+int100就會得到一個類型為int且value為200的基礎數據類型b,Long的equals方法將b進行裝箱,但裝箱所得到的是類型為Integer的對象o,因為o與long200為不同的類型的對象,所以輸出false;
程序運行的結果為:
  1. 200
  2. true
  3. false
因而,第二種推測是正確,即在+運算時,會將封裝類進行拆箱。
陷阱
陷阱1
  1. Integer integer100=null;
  2. int int100=integer100;
這兩行代碼是完全合法的,完全能夠通過編譯的,但是在運行時,就會拋出空指針異常。其中,integer100為Integer類型的對象,它當然可以指向null。但在第二行時,就會對integer100進行拆箱,也就是對一個null對象執行intValue()方法,當然會拋出空指針異常。所以,有拆箱操作時一定要特別注意封裝類對象是否為null。
陷阱2
  1. Integer i1=100;
  2. Integer i2=100;
  3. Integer i3=300;
  4. Integer i4=300;
  5. System.out.println(i1==i2);
  6. System.out.println(i3==i4);
因為i1、i2、i3、i4都是Integer類型的,所以我們想,運行結果應該都是false。但是,真實的運行結果為“System.out.println(i1==i2);”為 true,但是“System.out.println(i3==i4);”為false。也就意味着,i1與i2這兩個Integer類型的引用指向了同一個對象,而i3與i4指向了不同的對象。為什么呢?不都是調用Integer.valueOf(int i)方法嗎?
讓我們再看看Integer.valueOf(int i)方法。
  1. /**
  2. * Returns a <tt>Integer</tt> instance representing the specified
  3. * <tt>int</tt> value.
  4. * If a new <tt>Integer</tt> instance is not required, this method
  5. * should generally be used in preference to the constructor
  6. * {@link #Integer(int)}, as this method is likely to yield
  7. * significantly better space and time performance by caching
  8. * frequently requested values.
  9. *
  10. * @param i an <code>int</code> value.
  11. * @return a <tt>Integer</tt> instance representing <tt>i</tt>.
  12. * @since 1.5
  13. */
  14. public static Integer valueOf(int i) {
  15. if(i >= -128 && i <= IntegerCache.high)
  16. return IntegerCache.cache[i + 128];
  17. else
  18. return new Integer(i);
  19. }
 
我們可以看到當i>=-128且i<=IntegerCache.high時,直接返回IntegerCache.cache[i + 128]。其中,IntegerCache為Integer的內部靜態類,其原碼如下:
  1. private static class IntegerCache {
  2. static final int high;
  3. static final Integer cache[];
  4. static {
  5. final int low = -128;
  6. // high value may be configured by property
  7. int h = 127;
  8. if (integerCacheHighPropValue != null) {
  9. // Use Long.decode here to avoid invoking methods that
  10. // require Integer's autoboxing cache to be initialized
  11. int i = Long.decode(integerCacheHighPropValue).intValue();
  12. i = Math.max(i, 127);
  13. // Maximum array size is Integer.MAX_VALUE
  14. h = Math.min(i, Integer.MAX_VALUE - -low);
  15. }
  16. high = h;
  17. cache = new Integer[(high - low) + 1];
  18. int j = low;
  19. for(int k = 0; k < cache.length; k++)
  20. cache[k] = new Integer(j++);
  21. }
  22. private IntegerCache() {}
  23. }
 
我們可以清楚地看到,IntegerCache有靜態成員變量cache,為一個擁有256個元素的數組。在IntegerCache中也對cache進行了初始化,即第i個元素是值為i-128的Integer對象。而-128至127是最常用的Integer對象,這樣的做法也在很大程度上提高了性能。也正因為如此,“Integeri1=100;Integer i2=100;”,i1與i2得到是相同的對象。
對比擴展中的第二個實驗,我們得知,當封裝類與基礎類型進行==運行時,封裝類會進行拆箱,拆箱結果與基礎類型對比值;而兩個封裝類進行==運行時,與其它的對象進行==運行一樣,對比兩個對象的地址,也即判斷是否兩個引用是否指向同一個對象。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM