Java 包装类型及易错陷阱详解
作者:FXBStudy
一、预备知识
1、Java把内存划分成两种:一种是栈内存,另一种是堆内存。
2、int是基本类型,直接存数值;而 Integer是类,产生对象时用一个引用指向这个对象。
3、包装器(wrapper)——这是《JAVA核心技术》一书中对Integer这类对象的称呼。
4、包装器位于java.lang包中。
5、包装类是引用传递而基本类型是值传递(下面的内存管理给予解释)
6、基本类型的变量和对象的引用变量都是在函数的栈内存中分配 ,而实际的对象是在存储堆内存中
int i = 5;//直接在栈中分配空间 Integer i = new Integr(5);//对象是在堆内存中,而i(引用变量)是在栈内存中
1.1 Java内存管理
在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配。当在一段代码块中定义一个变量时,Java就在栈中为这个变量分配内存空间,当超过变量的作用域后,Java会自动释放掉为该变量分配的内存空间,该内存空间可以立刻被另作他用。
堆内存用于存放由new创建的对象和数组。在堆中分配的内存,由Java虚拟机自动垃圾回收器来管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,在栈中的这个特殊的变量就变成了数组或者对象的引用变量,以后就可以在程序中使用栈内存中的引用变量来访问堆中的数组或者对象,引用变量相当于为数组或者对象起的一个别名,或者代号。
引用变量是普通变量,定义时在栈中分配内存,引用变量在程序运行到作用域外释放。而数组&对象本身在堆中分配,即使程序运行到使用new产生数组和对象的语句所在地代码块之外,数组和对象本身占用的堆内存也不会被释放,数组和对象在没有引用变量指向它的时候,才变成垃圾,不能再被使用,但是仍然占着内存,在随后的一个不确定的时间被垃圾回收器释放掉。这也就是Java比较占内存的主要原因,实际上栈中的变量指向堆内存中的变量,这就是 Java 中的指针!
1.2 基本数据类型的包装类
基本类型 | 包装类 |
---|---|
boolean | Boolean |
char | Character |
byte | Byte |
int | integer |
long | Long |
float | Float |
double | Double |
short | Short |
1.3 包装类的构造方法
1、所有包装类都可将与之对应的基本数据类型作为参数,来构造它们的实例
2、除Character类外,其他包装类可将一个字符串作为参数构造它们的实例
注意事项:
- Boolean类构造方法参数为String类型时,若该字符串内容为true(不考虑大小写),则该Boolean对象表示true,否则表示false。
- 当Number包装类构造方法参数为String类型时,字符串不能为null,且该字符串必须可解析为相应的基本数据类型的数据,否则编译通过,运行时报NumberFormatException异常。
1.4 包装类的优缺点
包装类优点:
1、提供了一系列实用的方法
2、集合不允许存放基本数据类型数据,存放数字时,要用包装类型
包装类缺点:
- 由于每个值分别包装在对象中,所以ArrayList<Integer>的效率远远低于int[]数组。(应该用其构造小型集合,其原因是程序员操作的方便性要比执行效率更加重要)
1.5 包装类易错点
- 对象包装器类是不可变的,即一旦构造了包装器,就不允许更改包装在其中的值。
- 对象包装器类是不可变的,因此不能定义他们的子类。
Integer i = new Integer(20); i = 50; System.out.println(i); // 50
疑问:为什么变了,前面说是不可变的咋变了,前后不是矛盾吗?
想想前面介绍的Java内存管理方式,也许你已经明白了,如果还不明白就看看我的解释:
Integer i 中的 i 只是栈中指向对象的一个引用,后来 i = 50 又将i指向了50(此处运用到了自动装箱技术),这就是变化的原因,但是原来堆中创建的对象还是不变的。
除了包装器类型:Integer、Long、Short、Byte、Character、Boolean、Float和Double之外,还有BigInteger(java.math包)实例是不可变的,String、BigDecimal也是如此,不能修改它的值。不能修改现有实例的值,对这些类型的操作将返回新的实例。起先,不可变类型看起来可能很不自然,但是它具有很多胜过与其向对应的可变类型的优势。不可变类型更容易设计、实现和使用;它出错的可能性更小,并且更加安全
为了在一个包含对不可变对象引用的变量上执行计算,需要将计算的结果赋值给该变量。如下面的示例:
BigInteger fiveThousand = new BigInteger("5000"); BigInteger fiftyThousand = new BigInteger("50000"); BigInteger fiveHundredThousand = new BigInteger("500000"); BigInteger total = BigInteger.ZERO; total = total.add(fiveThousand); total = total.add(fiftyThousand); total = total.add(fiveHundredThousand); System.out.println(total);
二、自动拆/装箱
基本数据(Primitive)类型的自动装箱(autoboxing)、拆箱(unboxing)是自J2SE 5.0开始提供的功能。
Java语言规范中说道:在许多情况下包装与解包装是由编译器自行完成的(在这种情况下包装称为装箱,解包装称为拆箱)。
//声明一个Integer对象 Integer num = 10; /*以上的声明就是用到了自动的装箱,解析为: Integer num = new Integer(10); 以上就是一个很好的体现,因为10是属于基本数据类型的,原则上它是不能直接赋值给一个对象Integer的,但jdk1.5后你就可以进行这样的声明,这就是自动装箱的魅力,自动将基本数据类型转化为对应的封装类型。成为一个对象以后就可以调用对象所声明的所有的方法 自动拆箱:故名思议就是将对象重新转化为基本数据类型:*/ //装箱 Integer num_1 = 10; //拆箱 int num_2 = num_1; /*自动拆箱有个很典型的用法就是在进行运算的时候:因为对象时不能直接进行运算的,而是要转化为基本数据类型后才能进行加减乘除*/ Integer num_3 = 10; //进行计算时隐含的有自动拆箱 System.out.print(num_3--); /*哈哈 应该感觉很简单吧,下面我再来讲点稍微难点的.*/ //在-128~127 之外的数 Integer num_4 = 297; Integer num_5 = 297; System.out.println("num_4==num_5: "+(num_4==num_5)); // 在-128~127 之内的数 Integer num_6 = 97; Integer num_7 = 97; System.out.println("num_6==num_7: "+(num_6==num_7)); /*打印的结果是: num_4==num_5: false num_6==num_7: true */ //此处的解释在下方
注意事项:
1、装箱和拆箱是编译器认可的,而不是虚拟机。编译器在生成类的字节码时,插入必要的方法调用。虚拟机只是执行字节码。
2、包装对象和拆箱对象可以自由转换,但是要剔除NULL值,因为null值并不能转化为基本类型。
import java.util.ArrayList; import java.util.List; public class Ceshi { // 计算list中所有元素之和 public static int f(List<Integer> list) { int count = 0; for (int i : list) { count += i; } return count; } public static void main(String[] args) { List<Integer> list = new ArrayList<Integer>(); list.add(1); list.add(2); list.add(null); System.out.println(f(list)); } }
运行结果:Exception in thread "main" java.lang.NullPointerException
运行失败,报空指针异常,我们稍稍思考一下很快就知道原因了:在程序的for循环中,隐含了一个拆箱过程,再此过程中包装类型转换为了基本类型。我们知道拆箱过程是通过调用包装对象的intValue方法来实现的,由于包装对象是null值,访问其intValue方法报空指针异常也就在所难免了。问题找到了,那就解决。(即加入null值检查即可)
// 计算list中所有元素之和 public static int f(List<Integer> list) { int count = 0; for (Integer i : list) { count += (null != i) ? i : 0; } return count; }
针对此类问题:谨记包装类型参与运算时,要做null值校验。
三、整形池
@SuppressWarnings("resource") Scanner input = new Scanner(System.in); while (input.hasNextInt()) { int i = input.nextInt(); System.out.println("\n*********" + i + "的相等判断**********"); // 两个通过new产生的integer对象 Integer temp1 = new Integer(i); Integer temp2 = new Integer(i); System.out.println("new产生的对象:" + (temp1 == temp2)); // 基本类型转为包装类型后比较 temp1 = i; temp2 = i; System.out.println("基本类型转换的对象:" + (temp1 == temp2)); // 通过静态方法生成一个实例 temp1 = Integer.valueOf(i); temp2 = Integer.valueOf(i); System.out.println("valueOf产生的对象:" + (temp1 == temp2)); }
运行结果:
127 128 258
*********127的相等判断**********
new产生的对象:false
基本类型转换的对象:true
valueOf产生的对象:true
*********128的相等判断**********
new产生的对象:false
基本类型转换的对象:false
valueOf产生的对象:false
*********258的相等判断**********
new产生的对象:false
基本类型转换的对象:false
valueOf产生的对象:false
很不可思议,数字127的比较结果与另外两个竟然不一样,原因在哪里?
- new产生的Integer对象:new声明的就是要生成一个新的对象,因为是两个对象,地址肯定不一样,所以比较结果为false毫无疑问。
- 装箱生成的对象:首先说明一点,自动装箱的动作是通过valueOf方法实现的,也就是说后两个算法是相同的,所以他们的结果一样,产生上面现象的原因是什么呢?
我们来看一下valueOf的源代码:
/*** * Cache to support the object identity semantics of autoboxing for values * between*-128 and 127(inclusive)as required by JLS.**The cache is initialized * on first usage.The size of the cache*may be controlled by the{ * * @code -XX:AutoBoxCacheMax=<size>}option.*During VM * initialization,java.lang.Integer.IntegerCache.high property*may be set * and saved in the private system properties in the*sun.misc.VM class. */ private static class IntegerCache { static final int low = -128; static final int high; static final Integer cache[]; static { // high value may be configured by property int h = 127; String integerCacheHighPropValue = sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); if (integerCacheHighPropValue != null) { try { int i = parseInt(integerCacheHighPropValue); i = Math.max(i, 127); // Maximum array size is Integer.MAX_VALUE h = Math.min(i, Integer.MAX_VALUE - (-low) - 1); } catch (NumberFormatException nfe) { // If the property cannot be parsed into an int, ignore it. } } high = h; cache = new Integer[(high - low) + 1]; int j = low; for (int k = 0; k < cache.length; k++) cache[k] = new Integer(j++); // range [-128, 127] must be interned (JLS7 5.1.7) assert IntegerCache.high >= 127; } private IntegerCache() { } } /** * 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); }
如果不是-128到127之间的 int 类型转换为 Integer 对象,则直接返回一个新的对象。否则直接从cache数组中获得。
cache是IntegerCache内部类的一个静态数组,容纳的是-128到127之间的Integer对象。通过valueOf产生包装对象时,如果int参数在-128到127之间,则直接从整型池中获得对象,不在该范围的int类型则通过new生成包装对象。
这就是整形池,其存在不仅提高了系统性能,同时节约了内存空间。
所以在声明包装对象的时候使用valueOf生成,而不是通过构造函数来生成的原因,在判断对象是否相等的时候,最好是用equals方法,避免使用==产生非预期结果。
注意:通过包装类的valueOf生成包装实例可以显著提高空间和时间性能。
四、优先选择基本数据类型
包装类型是一个类,它提供了诸如构造方法、类型转换、比较等非常实用的功能,而且自动装箱(拆箱)更是如虎添翼,但是无论是从安全性、性能方面来说,还是从稳定性方面来说,基本类型是首选方案。
public class Ceshi { public static void main(String[] args) { Ceshi temp=new Ceshi(); int a=500; //分别传递int类型和Integer类型 temp.f(a); temp.f(Integer.valueOf(a)); } public void f(long i){ System.out.println("基本类型参数的方法被调用"); } public void f(Long i){ System.out.println("包装类型参数的方法被调用"); } }
上面程序的运行结果是:
基本类型参数的方法被调用
基本类型参数的方法被调用
很诧异是吧!感觉应该输出不一样的,第一个输出毫无疑问,系统进行了自动的类型转换,这种转换只能往高提升。第二个为什么没有调用包装类参数的函数呢?
原因是自动装箱有一个重要的原则:基本类型可以先加宽,再转成宽类型的包装类型,但不能直接转变成宽类型的包装类型。换句话说int可以加宽转变成long,然后在转变成Long对象,但不能直接转变成包装类型,注意这里指的都是自动转换,不是通过构造函数生成。
temp.f(Integer.valueOf(a));
这段代码的执行过程为
1、a 通过valueOf方法包装成一个Integer对象。
2、由于没有f(Integer i)方法,编译器“聪明”地把 Integer 对象又转换成 int。
3、int 自动拓宽为 long,编译结束。
注意:基本数据类型优先考虑。
public class Ceshi { public static void main(String[] args) { Ceshi temp=new Ceshi(); int a=500; //分别传递int类型和Long类型 temp.f(a); temp.f(Long.valueOf(a)); } public void f(long i){ System.out.println("基本类型参数的方法被调用"); } public void f(Long i){ System.out.println("包装类型参数的方法被调用"); } }
这段程序的输出结果为:
基本类型参数的方法被调用
包装类型参数的方法被调用
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。