一、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得到是相同的对象。
对比扩展中的第二个实验,我们得知,当封装类与基础类型进行==运行时,封装类会进行拆箱,拆箱结果与基础类型对比值;而两个封装类进行==运行时,与其它的对象进行==运行一样,对比两个对象的地址,也即判断是否两个引用是否指向同一个对象。
posted @ 2018-08-15 16:30  码上开车  阅读(1660)  评论(0编辑  收藏  举报