Java 包装类型及易错陷阱详解

目录
  • 一、预备知识
    • 1.1 Java内存管理
    • 1.2 基本数据类型的包装类
    • 1.3 包装类的构造方法
    • 1.4 包装类的优缺点
    • 1.5 包装类易错点
  • 二、自动拆/装箱
  • 三、整形池
  • 四、优先选择基本数据类型

一、预备知识

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("包装类型参数的方法被调用");
    }
}

这段程序的输出结果为:

基本类型参数的方法被调用
包装类型参数的方法被调用

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • 浅谈Java包装类型Long的==操作引发的低级bug

    目录 背景 两个 Long  类型的  == 对 Collections.EMPTY_SET 进行 add 引发的异常 Collections 的空集合使用注意事项 启示录 背景 一个简单的列表检索功能,列表元素有一个 Long 类型的属性,遍历过程中犯了一个低级错误,导致功能流程始终错误,本文将分享两个低级错误引发的 bug. 两个 Long  类型的  == 查找某个元素 A 在列表 B 中对应的对象的时候,根据元素主键查询,主键类型为包装类型 Long ,遍历流程如下: for(MyDat

  • Java基础教程之基本类型数据类型、包装类及自动拆装箱

    前言 我们知道基本数据类型包括byte, short, int, long, float, double, char, boolean,对应的包装类分别是Byte, Short, Integer, Long, Float, Double, Character, Boolean.关于基本数据类型的介绍可参考Java基础(一) 八大基本数据类型 那么为什么需要包装类? JAVA是面向对象的语言,很多类和方法中的参数都需使用对象,但基本数据类型却不是面向对象的,这就造成了很多不便. 如:List<in

  • Java基本数据类型与对应的包装类(动力节点java学院整理)

    Java是面向对象的编程语言,包装类的出现更好的体现这一思想. 其次,包装类作为类是有属性有方法的,功能比基本数据类型要强大. Java语言提供了八种基本类型.六种数字类型(四个整数型,两个浮点型),一种字符类型,还有一种布尔型. 1.整数:包括int,short,byte,long ,初始值为0 2.浮点型:float,double ,初始值为0.0 3.字符:char ,初始值为空格,即'' ",如果输出,在Console上是看不到效果的. 4.布尔:boolean ,初始值为false 注

  • Java基本类型与包装类详细解析

    Java语言提供了八种基本类型.六种数字类型(四个整数型,两个浮点型),一种字符类型,还有一种布尔型. 1.整数:包括int,short,byte,long ,初始值为0 2.浮点型:float,double ,初始值为0.0 3.字符:char ,初始值为空格,即'' ",如果输出,在Console上是看不到效果的. 4.布尔:boolean ,初始值为false 基本型别 大小 最小值 最大值 boolean ----- ----- ------ char 16-bit Unicode 0

  • Java基本类型和包装类型的区别

    包装类型可以为 null,而基本类型不可以 别小看这一点区别,它使得包装类型可以应用于 POJO 中,而基本类型则不行. POJO 是什么呢?这里稍微说明一下. POJO 的英文全称是 Plain Ordinary Java Object,翻译一下就是,简单无规则的 Java 对象,只有属性字段以及 setter 和 getter 方法,示例如下. class Writer { private Integer age; private String name; public Integer ge

  • Java基本类型包装类概述与Integer类、Character类用法分析

    本文实例讲述了Java基本类型包装类概述与Integer类.Character类用法.分享给大家供大家参考,具体如下: 基本类型包装类概述 将基本数据类型封装成对象的好处在于可以在对象中定义更多的功能方法操作该数据. 常用的操作之一:用于基本数据类型与字符串之间的转换. 基本类型和包装类的对应 Byte,Short,Integer,Long,Float,Double,Character,Boolean Integer类 为了让基本类型的数据进行更多的操作,Java就为每种基本类型提供了对应的包装

  • Java基本数据类型包装类原理解析

    数据类型包装类 Java语言是一个面向对象的语言,但是Java中的基本数据类型却是不面向对象的,这在实际使用时存在很多的不便,为了解决这个不足,在设计类时为每个基本数据类型设计了一个对应的类进行代表,这样八个和基本数据类型对应的类统称为包装类(WrapperClass),有些地方也翻译为外覆类或数据类型类. 包装类均位于java.lang包,包装类和基本数据类型的对应关系如下表所示: 包装类的用途 对于包装类说,这些类的用途主要包含两种: a.作为和基本数据类型对应的类类型存在,方便涉及到对象的

  • Java 包装类型及易错陷阱详解

    目录 一.预备知识 1.1 Java内存管理 1.2 基本数据类型的包装类 1.3 包装类的构造方法 1.4 包装类的优缺点 1.5 包装类易错点 二.自动拆/装箱 三.整形池 四.优先选择基本数据类型 一.预备知识 1.Java把内存划分成两种:一种是栈内存,另一种是堆内存. 2.int是基本类型,直接存数值:而 Integer是类,产生对象时用一个引用指向这个对象. 3.包装器(wrapper)--这是<JAVA核心技术>一书中对Integer这类对象的称呼. 4.包装器位于java.la

  • Java语言中的内存泄露代码详解

    Java的一个重要特性就是通过垃圾收集器(GC)自动管理内存的回收,而不需要程序员自己来释放内存.理论上Java中所有不会再被利用的对象所占用的内存,都可以被GC回收,但是Java也存在内存泄露,但它的表现与C++不同. JAVA中的内存管理 要了解Java中的内存泄露,首先就得知道Java中的内存是如何管理的. 在Java程序中,我们通常使用new为对象分配内存,而这些内存空间都在堆(Heap)上. 下面看一个示例: public class Simple { public static vo

  • java web中对json的使用详解

    一.在Java Web的开发过程中,如果希望调用Java对象转化成JSON对象等操作.则需要引入以下jar包,不然运行时则报错. 1.commons-beanutils.jar 2.commons-collections.jar 3.commons-lang.jar 4.commons-logging-1.1.jar 5.ezmorph-1.0.3.jar 6.json-lib-2.0-jdk15.jar 7.有人说还需要 commons-httpclient.jar 引入成功之后,使用JSON

  • Java使用FileInputStream流读取文件示例详解

    一.File流概念 JAVA中针对文件的读写操作设置了一系列的流,其中主要有FileInputStream,FileOutputStream,FileReader,FileWriter四种最为常用的流 二.FileInputStream 1)FileInputStream概念  FileInputStream流被称为文件字节输入流,意思指对文件数据以字节的形式进行读取操作如读取图片视频等 2)构造方法 2.1)通过打开与File类对象代表的实际文件的链接来创建FileInputStream流对象

  • java中的switch case语句使用详解

    java中的switch case语句 switch-case语句格式如下: switch(变量){ case 变量值1: //; break; case 变量值2: //...; break; ... case default: //...; break; } swtich()变量类型只能是int.short.char.byte和enum类型(JDK 1.7 之后,类型也可以是String了).当进行case判断时,JVM会自动从上到小扫描,寻找匹配的case,可能存在以下情况: 情况一:若未

  • Java数组的声明与创建示例详解

    今天在刷Java题的时候,写惯了C++发现忘记了Java数组的操作,遂把以前写的文章发出来温习一下. 首先,数组有几种创建方式? Java程序中的数组必须先进行初始化才可以使用,所谓初始化,就是为数组对象的元素分配内存空间,并为每个数组元素指定初始值,而在Java中,数组是静态的,数组一旦初始化,长度便已经确定,不能再随意更改. 声明数组变量 首先必须声明数组变量,才能在程序中使用数组.下面是声明数组变量的语法: dataType[] arrayRefVar; // 首选的方法 或 dataTy

  • java安全编码指南之:Mutability可变性详解

    简介 mutable(可变)和immutable(不可变)对象是我们在java程序编写的过程中经常会使用到的. 可变类型对象就是说,对象在创建之后,其内部的数据可能会被修改.所以它的安全性没有保证. 而不可变类型对象就是说,对象一旦创建之后,其内部的数据就不能够被修改,我们可以完全相信这个对象. 虽然mutable对象安全性不够,但是因为其可以被修改,所以会有效的减少对该对象的拷贝. 而immutable对象因为不可改变,所以尝试对该对象的修改都会导致对象的拷贝,从而生成新的对象. 我们最常使用

  • java向上转型发生的时机知识点详解

    1.直接赋值 public static void main(String[] args) { //父类引用 引用了 子类引用所引用的对象 Animal animal = new Cat();;//向上转型 } 2.方法传参 把一个Cat的子类传给一个Animal类型的父类,这里也是能发生向上转型的. public class Test extends TestDemo { public static void func(Animal animal) { } public static void

  • Java struts2请求源码分析案例详解

    Struts2是Struts社区和WebWork社区的共同成果,我们甚至可以说,Struts2是WebWork的升级版,他采用的正是WebWork的核心,所以,Struts2并不是一个不成熟的产品,相反,构建在WebWork基础之上的Struts2是一个运行稳定.性能优异.设计成熟的WEB框架. 我这里的struts2源码是从官网下载的一个最新的struts-2.3.15.1-src.zip,将其解压即可.里面的目录页文件非常的多,我们只需要定位到struts-2.3.15.1\src\core

随机推荐