阿里开发手册中关于基本类型的问题
7. 【强制】所有的相同类型的包装类对象之间值的比较,全部使用equals方法比较。 说明:对于Integer var = ? 在-128至127范围内的赋值,Integer对象是在IntegerCache.cache产生,会复用已有对象,这个区间内的Integer值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用equals方法进行判断。
8. 关于基本数据类型与包装数据类型的使用标准如下:
1) 【强制】所有的POJO类属性必须使用包装数据类型。
2) 【强制】RPC方法的返回值和参数必须使用包装数据类型。
3) 【推荐】所有的局部变量使用基本数据类型。
说明:POJO类属性没有初值是提醒使用者在需要使用时,必须自己显式地进行赋值,任何NPE问题,或者入库检查,都由使用者来保证。
正例:数据库的查询结果可能是null,因为自动拆箱,用基本数据类型接收有NPE风险。
反例:比如显示成交总额涨跌情况,即正负x%,x为基本数据类型,调用的RPC服务,调用不成功时,返回的是默认值,页面显示为0%,这是不合理的,应该显示成中划线。所以包装数据类型的null值,能够表示额外的信息,如:远程调用失败,异常退出。
Java共有8种基本数据类型
在这八个类名中,除了Integer和Character类以后,其它六个类的类名和基本数据类型一直,只是类名的第一个字母大写即可。
序号 | 基本类型 | 包装类 |
1 | boolean | Boolean |
2 | byte | Byte |
3 | short | Short |
4 | int | Integer |
5 | long | Long |
6 | float | Float |
7 | double | Double |
8 | char | Character |
基本类型范围
序号 | 基本类型 | 位数 | 数据范围 |
1 | boolean | 1位 | 只有true和false两个取值 |
2 | byte | 8位 | -2^7~2^7-1 |
3 | short | 16位 | -2^15~2^15-1 |
4 | int | 32位 | -2^31~2^31-1。 |
5 | long | 64位 | -2^63~2^63-1 |
6 | float | 32位 | 3.4e-45~1.4e38 |
7 | double | 64位 | 4.9e-324~1.8e308 |
8 | char | 16位 | 0, 2^16-1 |
超出范围怎么办
上面说过了,整型中,每个类型都有一定的表示范围,但是,在程序中有些计算会导致超出表示范围,即溢出。如以下代码:
int i = Integer.MAX_VALUE;
int j = Integer.MAX_VALUE;
int k = i + j;
System.out.println("i (" + i + ") + j (" + j + ") = k (" + k + ")");
运行结果
这就是发生了溢出,溢出的时候并不会抛异常,也没有任何提示。所以,在程序中,使用同类型的数据进行运算的时候,一定要注意数据溢出的问题。
为什么要有包装类
由于Java是一种面向对象的变成语言,OO的思想渗透在Java编程的点点滴滴,而8种基本类型并不属于对象,这会给Java程序带来很大的困难,因此Java提供了封装类型,将基本类型包装成对象并提供了自动拆装箱机制,使得基本数据类型也可以作为对象在程序中被使用和传递。
举个例子,具体一点
当需要往ArrayList,HashMap中放东西时,像int,double这种基本类型是放不进去的,因为容器都是装Object的,但是基本类型并不是对象,所以就需要这些基本类型的包装类了。Java中每种基本类型都有相应的包装类。
为什么保留基本数据类型
按照上面说的,基本类型在使用中有诸多不便,为什么Java不直接把基本类型删去呢?
我们都知道在Java语言中,new一个对象是存储在堆里的,我们通过栈中的引用来使用这些对象;而基本类型是存储在栈里的,而栈的存取相比与堆,是更高效的。所以,相比于基本类型,对象的存取比较消耗资源的。
两者区别
1、声明方式不同
包装类创建的是对象,拥有方法和字段.对象的调用都是通过引用对象的地址。基本类型不是.
基本类型不使用new关键字,而包装类型需要使用new关键字来在堆中分配存储空间;
2、存储区域不同
基本类型是在栈内存中分配 而对象是在存储堆内存中
即
int a= 5;//直接在栈中分配空间
Integer b = new Integr(5);//对象是在堆内存中
Tips:在堆中分配空间所需的时间远大于从栈中分配存储空间,相比而言,栈更高效,这也是Java保留基本类型的原因
3、存储内容不一样
基本类型保存就是数值
而对象类型保存的是对象的内存地址
4、占用空间大小不同
显然,相对于基本类型的变量来说,对象类型的变量需要占用更多的内存空间。
之所以 Java 里没有一刀切了基本类型,就是看在基本类型占用内存空间相对较小,在计算上具有高于对象类型的性能优势
5、初始值不同
基本类型的初始值如int为0,boolean为false,而包装类型的初始值为null;
6、使用方式不同
- 集合类中只能使用包装类(基本类型可自动装箱)
- 泛型中使用包装类
- 反射调用函数中使用包装类
- 数据库查询的结果为null时,不能赋值给基本类型,应该使用包装类
缓存池介绍
除了Float和Double之外,其他六个包装类都有常量缓存池 .
拿Integer来举例子,Integer类内部中内置了 256个Integer类型数据,当使用的数据范围在 -128~127之间时,会直接返回常量池中数据的引用,而不是创建对象,超过这个范围则会创建对象。
常见面试题
public static void main(String[] args){
Integer i1 = 100;
Integer i2 = 100;
Integer i3 = 200;
Integer i4 = 200;
System.out.println(i1==i2); // true
System.out.println(i3==i4); // false
}
这种赋值方式会触发自动装箱操作,而自动装箱操作调用的是valueOf方法。
valueOf方法会根据数组的大小来判断是否从缓存池中拿,数值的在[-128~127]范围内,则直接返回缓存池中的数据,如果不是,则返回一个戏对象
注意:这个问题只会发生在装箱时,也就是调用valueOf方法时。
如果没有触发装箱操作时,不论值是否相等,新建对象的==比较都是false。
Integer a1 = 1;//调用Integer.valueOf()自动装箱
Integer b1 = 1;//调用Integer.valueOf()自动装箱
Integer c1 = 300;//调用Integer.valueOf()自动装箱
Integer d1 = 300;//调用Integer.valueOf()自动装箱
System.out.println(a1==b1);//true
System.out.println(c1==d1);//false
Integer a2 = Integer.valueOf(1);//手动装箱
Integer b2 = Integer.valueOf(1);//手动装箱
Integer c2 = Integer.valueOf(300);//手动装箱
Integer d2 = Integer.valueOf(300);//手动装箱
System.out.println(a2==b2);//true
System.out.println(c2==d2);//false
// 构造器传值,并没有调用valueOf方法,所以也没有缓冲池
Integer a3 = new Integer(1);//新建对象
Integer b3 = new Integer(1);//新建对象
Integer c3 = new Integer(300);//新建对象
Integer d3 = new Integer(300);//新建对象
System.out.println(a3==b3);//false
System.out.println(c3==d3);//false
Integer.valueOf();源码分析
/**
* Returns an {@code Integer} instance representing the specified
* {@code int} value. If a new {@code Integer} instance is not
* required, this method should generally be used in preference to
* the constructor {@link #Integer(int)}, as this method is likely
* to yield significantly better space and time performance by
* caching frequently requested values.
*
* This method will always cache values in the range -128 to 127,
* inclusive, and may cache other values outside of this range.
*
* @param i an {@code int} value.
* @return an {@code Integer} instance representing {@code i}.
* @since 1.5
*/
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
其实官方JavaDoc讲的很清楚了,英文好的小伙伴直接看官方JavaDoc。
IntegerCache.cache是一个数组,范围是[-128~127],如果传进来的数值在这个范围内,就直接返回这个数组里的内容。如果超出这个范围,就返回一个新建对象。
其他包装类缓存池
包装类 | 缓存池类型 | 缓存池内容 |
Integer | IntegerCache | -128~127 |
Byte | ByteCache | -128~127 (此区间的数字转换成字节,其他包装类也是转成自己的类型) |
Long | LongCache | -128~127 |
Short | ShortCache | -128~127 |
Character | CharacterCache | 0~127 |
Boolean | true , false | true , false |
类型之间的比较
基本类型 | == | equals |
基本类型 | 值 | 不可用 |
包装类 | 地址 | 内容 |
有两个类型
- 基本数据类型
- 包装类型
比较有两种方式
- ==
- equals(仅包装类型的方法)
小结
- ==如果是基本类型,比较的是数值
- ==如果是对象,比较的是对象的内存地址
- equals是包装类型的方法,只能包装类使用
- 因为基本类型的包装类都重写了equals方法,所以比较的是数值
Tips:包装类比较,阿里强制使用equals
7. 【强制】所有的相同类型的包装类对象之间值的比较,全部使用equals方法比较。 说明:对于Integer var = ? 在-128至127范围内的赋值,Integer对象是在IntegerCache.cache产生,会复用已有对象,这个区间内的Integer值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用equals方法进行判断。
代码,同类型比较
// 同类型比较
int a1 = 1;
int b1 = 1;
System.out.println(a1==b1);//true,比较的是数值
Integer a2 = new Integer(1);
Integer b2 = new Integer(1);
System.out.println(a2==b2);//false,比较的是对象的地址
System.out.println(a2.equals(b2));//true,重写了equals,比较的是数值
equals源码
包装类型都重写了equals方法,比较的都是数值
Integer.equals()举例
public boolean equals(Object obj) {
// 先判断是不是Integer类型
if (obj instanceof Integer) {
// 拆箱成基本类型,然后比较数值
return value == ((Integer)obj).intValue();
}
return false;
}
代码,交叉对比
int a1 = 300;
Integer a2 = new Integer(300);
// 交叉对比
System.out.println(a1==a2);//true自动拆箱,比较的是数值
System.out.println(a2.equals(a1));//true,a1自动装箱后传入equals方法,equals比较的是数值
类型之间转换
类型转换方式
- 自动转换
- 强制转换
基本类型之间的转换
Java中除了boolean类型之外,其他7中类型相互之间可以进行转换。转换分为自动转换和强制转换。对于自动转换(隐式),无需任何操作,而强制类型转换需要显式转换,即使用转换操作符(type)。7种类型按照其占用空间大小进行排序:
byte <(short=char)< int < long < float < double
类型转换的总则是:小可直接转大、大转小会失去精度。这句话的意思是较小的类型直接转换成较大的类型,没有任何影响;
而较大的类型也可以强制转换为较小的类型,但是会失去精度。他们之间的转换都不会抛出任何运行时异常。小转大是Java帮我们自动进行转换的,与正常的赋值操作完全一样;大转小需要进行强制转换操作,其语法是target-type var =(target-type) value。
数据类型转换必须满足如下规则:
1.基本数据类型中,布尔类型boolean占有一个字节,由于其本身所代码的特殊含义,boolean2.类型与其他基本类型不能进行类型的转换(既不能进行自动类型的提升,也不能强制类型转换), 否则将编译出错。
3.在把容量大的类型转换为容量小的类型时必须使用强制类型转换。
4.转换过程中可能导致溢出或损失精度,例如:
int i =128;
byte b = (byte)i;//溢出
因为 byte 类型是 8 位,最大值为127,所以当 int 强制转换为 byte 类型时,值 128 时候就会导致溢出。
5. 浮点数到整数的转换是通过舍弃小数得到,而不是四舍五入,例如:
(int)23.7 == 23;
(int)-45.89f == -45
6. byte,char,short虽然类型从小到大自动转换,但是byte不能转成char,char也不能转成short。因为byte和short是数值型的变量,char字符型的变量。数值型变量有正负之分而在char中则无正负之分。byte转short自然就是可以的了。
自动类型转换
强制类型转换
// 强制类型转换,大转小
double b1 = 1.1;
float b2 = (float) b1;
System.out.println(b2);//1.1
int b3 = (int) b2;
System.out.println(b3);//1,损失精度
注意:有些时候会损失精度
值得注意是,大转小是一个很不安全的动作,可能导致莫名其妙的错误。比如,1111111111111L强转成int类型后,其值(-1285418553)与转换前的值相差巨大。这是由于在进行强制转换时,在二进制层面上直接截断,导致结果“面目全非”。
char的转换
byte,char,short虽然类型从小到大自动转换,但是byte不能转成char,char也不能转成short。因为byte和short是数值型的变量,char字符型的变量。数值型变量有正负之分而在char中则无正负之分。byte转short自然就是可以的了。
因为char是保存ASIIC码,并不是数值,所以单独拿出来讲。
char->int转换
char c = 'a';
int i = c;
System.out.println(i);// 97
byte,short,char之间的转换
注意
- byte可以自动转换成short
- byte不能自动转换成char
- shrot不能自动转换成char
数据类型自动提升
1、所有的byte,short,char型的值将被提升为int型;
因为Java中最小运算为int。小于int的byte,short都会自动被转换成int
这是因为Java中的数值运算最低要求是int类型,如果参与运算的变量类型都没有超过int类型,则它们都会被自动升级为int类型再进行运算,所以它们运算后的结果类型也是int类型。
2、如果有一个操作数是long型,计算结果是long型;
这里表达式中有long的类型,所以结果也为long。不能用int接收
注意:计算结果虽然是long,但是可以用大于long的类型来接收,其他同理
之所以能这样,是因为前面讲的自动转换
3、如果有一个操作数是float型,计算结果是float型;
4、如果有一个操作数是double型,计算结果是double型;
其它类型向字符串的转换
几种方法
- 调用类的串转换方法:X.toString();
- 自动转换:X+””;
- 使用String的方法:String.valueOf(X);
代码
// 基本类型
int a1 = 1;
String s1 = a1 + "";
String s2 = String.valueOf(a1);
//包装类型
Integer a2 = 1;
String s3 = a2 + "";
String s4 = String.valueOf(a2);
String s5 = a2.toString();
字符串->其它类型的转换
字符串->包装类型
两种方法
- 构造器传值
- valueOf()传值
代码
String s = "3.9";
// 方法1,构造器传值
Byte b1 = new Byte(s);
Short t1 = new Short(s);
Integer i1 = new Integer(s);
Long l1 = new Long(s);
Float f1 = new Float(s);
Double d1 = new Double(s);
// 方法2,valueOf传值
Byte b2 = Byte.valueOf(s);
Short t2 = Short.valueOf(s);
Integer i2 = Integer.valueOf(s);
Long l2 = Long.valueOf(s);
Float f2 = Float.valueOf(s);
Double d2 = Double.valueOf(s);
字符串->基本类型
两种方法
- parseType方法
- 先转换成包装类型,然后拆包成基本类型
静态parseType方法
String s = "3.9";
byte b = Byte.parseByte(s);
short t = Short.parseShort(s);
int i = Integer.parseInt(s);
long l = Long.parseLong(s);
float f = Float.parseFloat(s);
double d = Double.parseDouble(s);
字符串->包装类型->基本类型
int a1 = new Byte(s). intValue();
int a2 = Integer.valueOf(s).intValue();
自动装箱/拆箱(Autoboxing/unboxing)
包装类与基本类型的转换
包装类 | 包装类转基本类型 | 基本类型转包装类 |
Byte | Byte.valueOf(byte) | byteInstance.byteValue() |
Short | Short.valueOf(short) | shortInstance.shortValue() |
Integer | Integer.valueOf(int) | integerInstance.intValue() |
Long | Long.valueOf(long) | longInstance.longValue() |
Float | Float.valueOf(float) | floatInstance.floatValue() |
Double | Double.valueOf(double) | doubleInstance.doubleValue() |
Character | Character.valueOf(char) | charInstance.charValue() |
boolean | Boolean.valueOf(booleann) | booleanInstance.booleanValue() |
手动装箱拆箱
int a1 = 1;
Integer a2 = Integer.valueOf(a1);//手动装箱
Integer b1 = new Integer(1);
int b2 = b1.intValue();//手动拆箱
自动装箱与自动拆箱的实现原理
既然Java提供了自动拆装箱的能力,那么,我们就来看一下,到底是什么原理,Java是如何实现的自动拆装箱功能。
我们有以下自动拆装箱的代码:
int a1 = 1;
Integer a2 = a1;//自动装箱
Integer b1 = new Integer(1);
int b2 = b1;//自动拆箱
对以上代码进行反编译后可以得到以下代码:
从上面反编译后的代码可以看出,int的自动装箱都是通过Integer.valueOf()方法来实现的,Integer的自动拆箱都是通过integer.intValue来实现的。如果读者感兴趣,可以试着将八种类型都反编译一遍 ,你会发现以下规律:
自动装箱都是通过包装类的valueOf()方法来实现的.自动拆箱都是通过包装类对象的xxxValue()来实现的。
哪些地方会自动拆装箱
自动装箱
1、将基本数据类型放入集合类
我们知道,Java中的集合类只能接收对象类型,那么以下代码为什么会不报错呢?
ArrayList<Integer> list = new ArrayList<>();
for (int i = 1; i < 50; i ++){
list.add(i);
}
将上面代码进行反编译,可以得到以下代码:
以上,我们可以得出结论,当我们把基本数据类型放入集合类中的时候,会进行自动装箱。
2、函数参数与返回值
这个比较容易理解,直接上代码了:
//自动装箱
public Integer getNum(int num) {
return num;
}
自动拆箱
1、包装类型和基本类型的大小比较
有没有人想过,当我们对Integer对象与基本类型进行大小比较的时候,实际上比较的是什么内容呢?看以下代码:
int a = 1;
Integer b = new Integer(1);
System.out.println(a==b);//true,比较时自动拆箱,比较的是数值
反编译
可以看到,包装类与基本数据类型进行比较运算,是先将包装类进行拆箱成基本数据类型,然后进行比较的。
2、包装类型的运算
有没有人想过,当我们对Integer对象进行四则运算的时候,是如何进行的呢?看以下代码:
Integer a = new Integer(1);
Integer b = new Integer(1);
System.out.println(a+b);//2
反编译后代码如下:
我们发现,两个包装类型之间的运算,会被自动拆箱成基本类型进行。
3、三目运算符的使用
这是很多人不知道的一个场景,作者也是一次线上的血淋淋的Bug发生后才了解到的一种案例。看一个简单的三目运算符的代码:
boolean flag = true;
Integer i = 0;
int j = 1;
int k = flag ? i : j;
很多人不知道,其实在int k = flag ? i : j;这一行,会发生自动拆箱。反编译后代码如下:
boolean flag = true;
Integer i = Integer.valueOf(0);
int j = 1;
int k = flag ? i.intValue() : j;
System.out.println(k);
这其实是三目运算符的语法规范。当第二,第三位操作数分别为基本类型和对象时,其中的对象就会拆箱为基本类型进行操作。
因为例子中,flag ? i : j;片段中,第二段的i是一个包装类型的对象,而第三段的j是一个基本类型,所以会对包装类进行自动拆箱。如果这个时候i的值为null,那么久会发生NPE。
4、函数参数与返回值
这个比较容易理解,直接上代码了:
//自动拆箱
public int getNum(Integer num) {
return num;
}
自动拆装箱带来的问题
- 自动拆箱时的NPL问题
- ==比较问题
- 效率问题
1、自动拆箱时的NPL问题
Integer a = null;
int b = a.intValue();//拆箱,报错
2、==比较问题
这个前面讲了一些,这里就不多讲了
// valueOf传值,(自动装箱调用的方法)
// 缓冲池范围-128~127
Integer a1 = Integer.valueOf(1);
Integer a2 = Integer.valueOf(1);
Integer a3 = Integer.valueOf(300);
Integer a4 = Integer.valueOf(300);
System.out.println(a1==a2);//true缓冲池内
System.out.println(a3==a4);//false缓冲池外
//构造器传值
Integer b1 = new Integer(1);
Integer b2 = new Integer(1);
Integer b3 = new Integer(300);
Integer b4 = new Integer(300);
// 并没有调用valueOf方法,所以并没有缓冲池。
// 用这种方法创建出来的都是新对象
System.out.println(b1==b2);//false
System.out.println(b3==b4);//false
3、效率问题
基本类型在栈里,包装类型在堆里。
拆装箱操作本身也会浪费资源
参考
http://hinylover.space/2016/06/16/relearn-java-base-type-and-wrapper/
https://www.liupeng.mobi/archives/1198
http://www.importnew.com/11915.html