Java中的自动拆装箱、基本类型的转换、包装类的缓存详解
作者:骑个小蜗牛
数据类型的拆装箱
1. 拆箱、装箱
拆箱
把包装类转换为基本数据类型就是拆箱。拆箱通过包装类的xxValue方法实现。
装箱
把基本类型转换为包装类的过程就是装箱。装箱通过包装类的valueOf方法实现。
2. 自动拆箱
将包装类自动转化成对应的基本数据类型(通过包装类的xxValue方法)。
public static void main(String[] args) { int int1 = new Integer(100); long long1 = new Long(100); double double1 = new Double(100.0); }
实际等于
public static void main(String[] args) { int int1 = new Integer(100).intValue(); long long1 = new Long(100).longValue(); double double1 = new Double(100.0).doubleValue(); }
3. 自动装箱
将基本数据类型自动转化为对应的包装类(通过包装类的valueOf方法)。
public static void main(String[] args) { Integer int2 = 100; Long long2 = 100L; Double double2 = 100.0; }
实际等于
public static void main(String[] args) { Integer int2 = Integer.valueOf(100); Long long2 = Long.valueOf(100L); Double double2 = Double.valueOf(100.0); }
4. 自动拆装箱使用场景
将基本类型放入集合类(自动装箱)
集合类只支持包装类型(不支持基本数据类型),但是我们add(基本数据类型)也不会报错,是因为Java给我们做了自动装箱。
实际等于
public static void main(String[] args) { List<Integer> list = new ArrayList<>(); list.add(Integer.valueOf(1)); list.add(Integer.valueOf(2)); }
包装类型和基本类型比较大小(自动拆箱)
包装类与基本数据类型进行比较运算,其实是先将包装类进行拆箱成基本数据类型,然后比较。
public static void main(String[] args) { boolean b = new Integer(2) > 1; }
实际等于
public static void main(String[] args) { boolean b = new Integer(2).intValue() > 1; }
包装类型的运算(自动拆箱)
运算中包含包装类型时,会将包装类型自动拆箱为基本类型再进行运算
public static void main(String[] args) { int a = new Integer(1) + 1; }
实际等于
public static void main(String[] args) { int a = new Integer(1).intValue() + 1; }
三目运算(自动拆箱)
value = flag ? value1 : value2
三目运算中,如果value1和value2中一个是基本数据类型、另一个是包装类型。那么无论value是基本数据类型还是包装类型,只要根据判断结果从value1和value2选择的那个值是包装类型时,都会先把包装类型拆箱为基本数据类型,再根据value的类型来决定是否再对值进行拆封箱处理。
public static void main(String[] args) { Integer a = null; int v1 = true ? 0 : a;// 正常 int v2 = false ? 0 : a;// 报错 Integer v3 = true ? 0 : a;// 正常 Integer v4 = false ? 0 : a;// 报错 }
实际等于
public static void main(String[] args) { Integer a = null; int v1 = true ? 0 : a;// 正常 int v2 = false ? 0 : a.intValue();// 报错 Integer v3 = true ? 0 : a;// 正常 Integer v4 = false ? 0 : Integer.valueOf(a.intValue());// 报错 }
方法的返回值(自动拆装箱)
方法的返回值类型与实际返回值类型不同时,会自动拆装箱,变成方法返回值相同的类型。
//自动拆箱 public int getNum1() { return new Integer(1); } //自动装箱 public Integer getNum2() { return 1; }
实际等于
//自动拆箱 public int getNum1() { return new Integer(1).byteValue(); } //自动装箱 public Integer getNum2() { return Integer.valueOf(1); }
5. 自动拆装箱的触发时机
在编译期,Java文件编译称Class(字节码)文件的过程中触发自动拆装箱的动作。
通过查看字节码文件可以予以证明。两种查看字节码信息的方式:
1.通过javac和javap查看
先通过javac将.java代码编译成.class字节码,然后通过javap分析字节码。
javac:编译成class(字节码)
javac DemoTest.java
javap:分析字节码
javap -verbose DemoTest.class
若出现编码问题,可指定编码
javac -encoding utf-8 DemoTest.java javap -encoding utf-8 -verbose DemoTest.class
2.IDEA使用ASM Bytecode Outline插件查看字节码
类文件上右键->选择: Show Bytecode outline
面板上的三个选项:
- Bytecode 表示对应的class字节码文件
- ASMified 表示利用ASM框架生成字节码对应的代码
- Groovified 对应的是class字节码指令
6. 自动拆装箱带来的问题
- 包装对象之间的数值比较不能简单的使用==,除了特殊有缓存的情况(如Integer的-128~127),其他比较都需要使用equals比较。
- 如果包装类对象为NULL,那么自动拆箱就可能会抛出空指针异常
- 如果一个for循环中有大量拆装箱操作,会浪费很多资源
基本数据类型的转换
1. 容量大小排序
基本数据类型容量大小排序(不含布尔类型、容量由小到大):
- byte<short/char<int<long<float<double
- byte<<int<long<float<double
2. 转换规则
- 8种基本数据类型中,除了布尔类型之外,其他的8中数据类型之间都可以相互转换
- 任何浮点类型不管占用多少个字符,都比整数型容量大
- char和short可表示的种类数量相同,但是char可以取更大的整数
- 整数的默认类型为int,小数的默认类型为double
- 小容量向大容量转换,称为自动类型转换(又称隐式类型转换),不会丢失精度
- 大容量转换成小容量,叫做强制类型转换(又称显式类型转换),可能会会丢失精度,需要加强制类型转换符。
- 当整数字面值没有超出byte,short,char的取值范围,可以直接赋值给byte,short,char类型的变量
- 多种数据类型混合运算,先转换成容量大的那种类型再做运算(byte,short,char混合运算的时候,各自先转换成int类型再做运算)
3. 自动类型转换
小容量向大容量转换,称为自动类型转换(又称隐式类型转换)。
规则
- 小容量的类型转化为大容量的类型自动使用自动类型转换
- 整数类型可以自动转化为浮点类型,可能会产生舍入误差
- 字符可以自动提升为整数
4. 强制类型转换
大容量转换成小容量,叫做强制类型转换(又称显式类型转换)。
强制类型转换需要在要强制类型转换的前面加上括号,然后在括号里面加上你要转换的类型。
规则
- 大容量的类型转化为大容量的类型必须使用强制类型转换
- 强制类型转换可能导致溢出或损失精度
- 浮点数到整数的转换是通过舍弃小数得到,而不是四舍五入
- 不能把对象类型转换为不相干的类型
包装类的缓存
Java包装类的缓存机制,是在Java 5中引入的一个有助于节省内存、提高性能的功能。
包装类的缓存只在装箱(通过包装类的valueOf方法来实现)时有效。
想必大家一定遇到过这样的问题:两个数值相等的包装类型对象通过==进行比较时,得到的结果有时是true,有时又是false。
这可能就是包装类的缓存在捣乱:当包装类对象经历过装箱操作,得到的对象可能是缓存好的对象,也可能是新创建的对象。
1. 包装类型的缓存值范围
基本类型 | 大小(bit) | 默认值 | 取值范围 | 包装类 | 包装类缓存范围 |
---|---|---|---|---|---|
byte(字节) | 8 | 0 | [-2^7,2^7-1] [-128,127] | Byte | [-128,127] |
char(字符) | 16 | 空值(\u0000) (unicode编码) | [0,2^16-1] [0,65535] | Character | [0,127] |
short(短整数) | 16 | 0 | [-2^15,2^15-1] [-32768,32767] | Short | [-128,127] |
int(整数) | 32 | 0 | [-2^31,2^31-1] [-2147483648,2147483647] | Integer | [-128,127] |
long(长整数) | 64 | 0L | [-2^63,2^63-1] [-9223372036854774808,9223372036854774807] | Long | [-128,127] |
float(单精度小数) | 32 | 0.0F | [-2^31,2^31-1] [3.402823e+38,1.401298e-45] | Float | 无 |
double(双精度小数) | 64 | 0.0 | [-2^63,2^63-1] [1.797693e+308,4.9000000e-324] | Double | 无 |
boolean(布尔值) | 8 | false | true,false | Boolean | true,false |
boolean类型的底层是转换为1,0存储的,所以大小只有一个字节(8位)
- valueOf方法的逻辑是先从判断值是否在缓存值范围中。如果在,直接返回缓存中的对象;如果不在,创建新的对象。
- 在 jdk 1.8 所有的数值类缓冲池中,Integer 的缓冲池 IntegerCache 很特殊,这个缓冲池的下界是 - 128,上界默认是 127,但是这个上界是可调的。
- 在启动 jvm 的时候,通过 -XX:AutoBoxCacheMax=来指定这个缓冲池的大小,该选项在 JVM 初始化的时候会设定一个名为 java.lang.IntegerCache.high 系统属性,然后IntegerCache 初始化的时候就会读取该系统属性来决定上界。
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。