Java学习者论坛

 找回密码
 立即注册

QQ登录

只需一步,快速开始

手机号码,快捷登录

恭喜Java学习者论坛(https://www.javaxxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,购买链接:点击进入购买VIP会员
JAVA高级面试进阶视频教程Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程

Go语言视频零基础入门到精通

Java架构师3期(课件+源码)

Java开发全终端实战租房项目视频教程

SpringBoot2.X入门到高级使用教程

大数据培训第六期全套视频教程

深度学习(CNN RNN GAN)算法原理

Java亿级流量电商系统视频教程

互联网架构师视频教程

年薪50万Spark2.0从入门到精通

年薪50万!人工智能学习路线教程

年薪50万!大数据从入门到精通学习路线年薪50万!机器学习入门到精通视频教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程 MySQL入门到精通教程
查看: 401|回复: 0

[Java基础知识]逐渐挖掘Autoboxing-Auto-Unboxing

[复制链接]
  • TA的每日心情
    开心
    2021-3-12 23:18
  • 签到天数: 2 天

    [LV.1]初来乍到

    发表于 2014-10-2 02:11:00 | 显示全部楼层 |阅读模式
    逐渐挖掘Autoboxing/Auto-Unboxing
    更简单的整合两套类型系统


          J2SE 1.5提供了“Autoboxing”和“Auto-Unboxing”的机制,可以让编译器来自动完成在基本类型和它们的包裹对象之间的转化工作,从而能够用一种更简单的方式,来避免同时存在两套类型系统所带来的一些麻烦。本文介绍Autoboxing/Auto-Unboxing机制的使用方法、实质、发生时机、局限、对重载机制的影响以及对性能的妨碍等问题。
    传统上,在java程序中,可以往一个容器类(无论是Collection还是Map)里直接放入一个对象;但是如果打算放入的是一个数字、字符或布尔值的话,就要先加入一个“生成包裹它们的对象”的步骤。 造成这种现象的原因是,在Java语言当中一直存在着两套非常不同的类型系统:

    一套是所谓的“引用类型”(Reference Types),包括所有的类和接口。这些类型的数据被看作对象,所以可以用一个Object型的变量来保存。

    一套是所谓的“基本类型”(Primitive Types),包括:byte、short、int、long、float、double、char和boolean。这些类型的数据不是对象,因此也不能用Object型的变量来保存。
      
    同时采用这样两套类型系统,可以得到一些性能方面的好处――因为基本类型的数据不是对象,所以创建得更快、占用的空间更少、收回它们占用的资源也更容易;但是,这样的做法同时也会造成一些编码方面的问题――例如,不能定义一个变量(或数组),让它既能保存基本类型的数据,又能保存引用类型的数据(类似的,也不能定义一个同时能匹配这两种类型的数据的形参,不过这个问题可以借助Java里的重载机制来回避)。 实际上需要定义“不知道用来保存什么类型的数据”的变量(和形参)时,一般对这个问题采取回避的态度,将它们的类型定义成Object,然后借助可以称为“Boxing”和“Unboxing”的操作来解决Object不能涵盖基本类型的问题。 1. Boxing和Unboxing操作 所谓Boxing操作,是指通过生成一个能包裹基本类型数据的对象,来让基本类型的数据出现在只能接受引用类型的地方。 清单1:手工Boxing的典型情况

    Collection integers = new ArrayList();

    for(int i = 0; i < 10; i++) {

         integers.add(
    new Integer(i));

    }


    用于生成这些的对象的类,被称作“包裹类”(Wrapper Classes)。Java中的包裹类有Byte 、Short、Integer、Long、Float、Double、Character和Boolean(都在java.lang包里定义)等八种,分别用于包裹byte、short、int、long、float、double、char和boolean类型的数据。 而所谓Unboxing操作,则是指调用包裹类对象的相应方法,得到它们所代表的“基本类型的数据”,以便进行进一步的处置。 清单2:手工Unboxing的典型情况

    for(Iterator itr = integers.iterator(); itr.hasNext(); ) {

         Integer i = (Integer) itr.next();

         System.out.println(
    i.intValue() + 1);

    }


    而在Java语言的最新版本――J2SE 1.5中,提供了“Autoboxing”和“Auto-Unboxing”的机制,可以让编译器来自动完成这些琐碎的操作,从而用一种更简单的方式,来整合两套类型系统。

    熟悉的陌生名词
    尽管这一对操作的历史很悠久,但是把它们称作“Boxing”和“Unboxing”的做法,基本是在出现“Autoboxing”和“Auto-Unboxing”的概念之后,才得到了广泛的接受。在那之前,它们似乎并没有通用的、专门的名字。不过由于那时也很少提及这两个概念,所以这个问题倒也没有造成什么严重的影响。
    2. 使用Autoboxing和Auto-Unboxing 使用Autoboxing和Auto-Unboxing,并不需要什么特别的步骤,一切都会在编译器的安排下自动发生。 现在可以这样来对待一个int型的数据: 清单3:自动完成的Boxing操作

    Collection al = new ArrayList();

    al.add(
    1);


    因为编译器会悄悄的把这段代码转换成接近这个样子: 清单4:作了Autoboxing之后的等价形式

    Collection al = new ArrayList();

    al.add(
    Integer.valueOf(1));


    这里所用的能接受int类型的值为参数,生成Integer实例的valueOf方法,是J2SE 1.5中新加入的内容。其它包裹类也都有可以接受对应的基本类型的值为参数,生成对应的包裹类实例的valueOf方法加入。 而这样对待一个Integer型的对象也是可以的: 清单5:自动完成的Unboxing操作

    Integer one = new Integer(1);

    int two =
    one + 1;


    因为编译器会悄悄的把这段代码转换成类似这个形状: 清单6:作了Auto-Unboxing之后的等价形式

    Integer one = new Integer(1);

    int two =
    one.intValue() + 1;


    大体上,只要把一个结果类型是基本类型的表达式,放到需要让它们的包裹类出现的位置上,就会诱发Autoboxing;类似的,只要把一个结果类型是包裹类的表达式,放到只允许相应的基本类型出现的位置上,就会诱发Auto-Unboxing。

    “Autoboxing/Auto-Unboxing”特性的来源
    J2SE 1.5中增加的许多语言特性都可以在C#里找到对应的东西。不过根据Bruce Eckel对Joshua Bloch的采访,尽管Java的研发小组确实很关注C#(Joshua Bloch本人的案头就放着一本关于C#的书),但是只有“Autoboxing/Auto-Unboxing”和“Metadata”确实是从C#中直接借鉴来的特性。
    3. 发生Autoboxing的具体时机 发生Autoboxing的具体时机,主要有这么三种:

    把基本类型的数据赋给引用类型的变量时。例如把一个int型的数据赋给一个Integer型变量。
    清单7:赋给引用类型的变量基本类型的数据
      
       Integer i =
       31415;
       

       
    把基本类型的数据传给引用类型的参数时。例如给一个定义成Object的参数传递一个boolean型的数据。
    清单8:传给引用类型的参数基本类型的数据
      
       HashMap map = new HashMap();
       
    map.put(
       true, null);
       

       
    把基本类型的数据往引用类型上强制转化时。例如在一个long型的数据前面加上(Long)。
    清单9:从基本类型的数据到引用类型上强制转化
      
       System.out.println(
       (Long) 27828L);
       

       
    4. Autoboxing的局限 Autoboxing的机制有一个局限――只能把基本类型的数据往它们自己的包裹类(以及包裹类的上级类)上转化。 类似这样的代码是不能工作的,尽管int型的数据完全可以用一个Long对象来表示: 清单10:不能同时进行自动向上转型和Autoboxing

    int i = 27828;

    System.out.println(
    (Long) i);/* 编译时出错 */


    这是因为这段代码实际上相当于: 清单11:Autoboxing操作会在自动向上转型之前发生

    int i = 27828;

    System.out.println(
    (Long) Integer.valueOf(i));/* 编译时出错 */


    而Integer并不是Long的子类,所以这个转化无法进行。如果一定要进行这种操作,需要手工追加一次转型: 清单12:需要先强制向上转型,再作Boxing

    int i = 27828;

    System.out.println((Long)
    (long) i);


    5. 发生Auto-Unboxing的具体时机 发生Auto-Unboxing的具体时机,则主要有这么七种:

    把包裹类对象赋给基本类型的变量时。例如把一个Integer型的数据赋给一个int型变量。
    清单13:赋给基本类型的变量包裹类对象
      
       int i =
       new Integer(32);
       

       
    把包裹类对象传给基本类型的参数时。例如给一个定义成boolean的参数传递一个Boolean型的数据。
    清单14:传给基本类型的参数包裹类对象
      
       JFrame frame = new JFrame("^_^");
       
    frame.setSize(320, 200);
       
    frame.setVisible(
       new Boolean(true));
       

       
    把包裹类对象往基本类型上强制转化时。例如在一个Long型的数据前面加上(long)。
    清单15:从包裹类对象到基本类型的强制转化
      
       Long l = new Long(31415L);
       
    System.out.println(
       (long) l);
       

       
    把包裹类对象当作运算符的操作数时。例如在两个Byte型的数据之间放上“+”号。
    清单16:把包裹类对象当作运算符的操作数
      
       Byte a = new Byte((byte) 1);
       

       Byte b = new Byte((byte) -1);
       
    System.out.println(
       ((a++) << 2) + (~b));/* 输出“4” */
       
    System.out.println(a);/* 输出“2” */
       

       
    用包裹类对象来指定数组的大小时。当然,从语义上说,这个对象的类型必须是Byte、Short、Integer或Character。
    清单17:用包裹类对象来指定数组的大小
      
       Character size = new Character("★");/* Unicode: 9733 */
       
    int[] integers = new int[
       size];/* 生成一个可放9733个int元素的数组 */
       

       
    把包裹类对象在switch语句里使用时。当然,从语义上说,这个对象的类型必须是Byte、Short、Integer或Character。
    清单18:在switch语句里使用包裹类对象
      
       Character c = new Character("a");
       

       switch (c) {
       
         case "a":
       
         case "e":
       
         case "i":
       
         case "o":
       
         case "u":
       
             System.out.println("A Vowel in English");
       
             break;
       
         default:
       
             System.out.println("Not A Vowel in English");
       
             break;
       
    }
       

       
    把Boolean对象在if/for/while/do-while语句中作为条件表达式使用时。
    清单19:把Boolean对象作为条件表达式
      
       Boolean bool = new Boolean(Math.random() > 0.5);
       

       if (bool) {
       
         System.out.println("Aye!");
       
    } else {
       
         System.out.println("Nay!");
       
    }
       

       
    6. Auto-Unboxing的局限 Auto-Unboxing的机制则有这样一个局限――只能把包裹类对象往它们对应的基本类型(以及容纳范围更广的类型)上转化。 类似这样的代码是不能工作的,尽管32并未超出byte所能表示的范围: 清单20:不能同时进行Auto-Unboxing和强制向下转型

    Integer i = new Integer(32);

    System.out.println(
    (byte) i);/* 编译时出错 */


    这是因为编译器并不认可同时进行Auto-Unboxing和强制向下转型的操作,所以这个转化无法进行。如果一定要进行这种操作,需要手工补充一次转型: 清单21:需要先作Unboxing,再强制向下转型

    Integer i = new Integer(32);

    System.out.println((byte)
    (int) i);


    不过同时进行Auto-Unboxing和强制向上转型的操作是没有问题的,所以下面的代码工作得很正常: 清单22:可以同时进行Auto-Unboxing和强制向上转型

    Integer i = new Integer(32);

    System.out.println(
    (double) i);


    7. 其它不能自动转化的情况 除去强制类型转化时的限制之外,还有这样一些情况下不会发生Autoboxing/Auto-Unboxing: 1. 基本类型的数组和包裹类数组之间不会自动转化。这样的代码完全不被编译器接受: 清单23:元素可以,容器不行

    int[] ints = {1, 2, 3};


    Integer[] integers = ints;/* 编译时出错 */


    2. 不能对着基本类型的表达式来调用包裹类里的方法。这样的申请会被编译器彻底拒绝: 清单24:没有方法,就是没有方法

    int i = 1;

    byte b =
    i.byteValue();/* 编译时出错 */


    8. null的转化问题 Java里的引用类型可以有一个特别的取值――“null”。试图对null进行Auto-Unboxing操作会导致一个“NullPointerException”。 例如这段代码就会在运行时抛出异常,尽管在编译期间会表现得非常正常: 清单25:表面上,只是普通的赋值

    Integer
    i =
    null;

    int j =
    i;/* 运行时错误 */


    这是因为这段代码实际上相当于: 清单26:实际上,是在试图调用null的方法

    Integer
    i =
    null;

    int j =
    i.intValue();/* 运行时错误 */


    而试图调用null的方法是一种不被虚拟机认可的行为。

    如果没记住有关的规则
    大部分违反了使用“Autoboxing/Auto-Unboxing”机制时,需要遵守的约束的代码,都会造成编译错误。因此,即使并未准确的记住有关的规则,也不难及时发现、改正。只有违背了“不能对null进行Auto-Unboxing操作”的限制时,引发的是运行时异常,需要特别小心。
    9. 对重载的影响 Java支持“重载”的机制,允许在同一个类拥有许多名称相同而形参列表不同的方法。然后,由编译器根据调用时的实参来选择到底要执行哪一个。 Autoboxing/Auto-Unboxing机制的引入,稍微增加了一些作这种选择时要考虑的因素――因为可能会有一个方法,既有一个能接受一个Integer型参数的版本,又有一个能接受一个int型参数的版本,而Autoboxing/Auto-Unboxing机制能自动的把实参在这两种类型之间转化,光凭原有的判断规则,二者是难以取舍的。但是,因为同时有这两个版本的做法完全合情合理,又不能在这里给出一个“reference to 被调用的方法名 is ambiguous”的编译错误来推卸责任。这就需要增加一条新的判断规则。 这条新增的规则是,不用进行Autoboxing/Auto-Unboxing的版本,优先于需要进行Autoboxing/Auto-Unboxing的版本。 因此,在这种情况下具体选择哪一个,要看传递的实参最初是什么类型。 清单27:不用进行Autoboxing/Auto-Unboxing的版本优先

    public class OverloadingTest

    {

         private static void testOverloading(
    int i){

             System.out.println("int");

         }

         private static void testOverloading(
    Integer i){

             System.out.println("Integer");

         }

         public static void main(String[] args)

         {

             
    int i = 1;

             
    Integer j = new Integer(1);

             testOverloading(
    i);/* 输出“int” */

             testOverloading(
    j);/* 输出“Integer” */

         }

    }


    10. 值相等和引用相等 在Java语言中有两个不同的“相等”概念――值相等和引用相等。这样就有一个“两个值相等的基本类型数据,经过Autoboxing之后,得到的对象的引用是否相等”的问题。 在《JSR 201: Extending the Java Programming Language with Enumerations, Autoboxing, Enhanced for loops and Static Import》中,对这个问题,是作了这样的规定:
    [blockquote]
    If the value p being boxed is true, false, a byte, an ASCII character, or an integer or short number between -127 and 128, then let r1 and r2 be the results of any two boxing conversions of p. It is always the case that r1 == r2.
    [/blockquote] 这意味着这个答案可能是“是”也可能是“否”,由被Autoboxing的数据的类型和取值来决定。因此在检测两个对象是否代表相同的值的时候,还是有必要调用equals()方法来进行。 不过在J2SDK 1.5 Beta 1和Beta 2里的实际情况,和这稍微有些出入,“Autoboxing之后得到相同的对象引用”的范围被缩小了: 清单28:原来的值相等,经过Autoboxing之后的引用可能相等,也可能不相等

    boolean b = true;

    Boolean
    b1 = b;

    Boolean
    b2 = b;

    System.out.println(
    b1 == b2);/* 输出“
    true” */

    char c = "1";

    Character
    c1 = c;

    Character
    c2 = c;

    System.out.println(
    c1 == c2);/* 输出“
    false” */


    11. 对性能的妨碍 由于Autoboxing机制的实质是“自动创建能代表基本类型数据的对象”,所以,不可避免的会对性能造成一些妨碍。 如果只是利用Autoboxing/Auto-Unboxing机制来保存基本类型的数据(例如把基本类型的数据放到Collection里面之类),这种影响倒还可以忽略,因为这只是把原来需要手工进行的工作自动化了;但是,如果要频繁的借助Autoboxing来给一个包裹类变量赋值,这开销很容易上升到需要加以注意的程度。 注意对包裹类的变量使用“++”和“--”运算符的时候,也会创建新的对象,而不是在修改原来对象的状态。 清单29:是替换不是修改

    Integer i = new Integer(1);

    Integer
    j = i;/* 让j、i指向同一对象 */

    System.out.println(
    j == i);/* 目前j、i是同一对象,因此输出“true” */


    i++;

    System.out.println(
    j == i);/* 现在j、i是不同对象,因此输出“false” */

    System.out.println(i);/* 现在i的值是“2” */

    System.out.println(j);/* 而j的值仍是“1” */


    这个现象是由于Java里的包裹类是“不可变的(immutable)”――即没有提供一种能让自己所代表的值发生变化的途径――而造成的。 在需要大量赋值操作的时候,可以通过适当使用一些基本类型的局部变量来减轻对性能方面的影响。不过,如果性能的瓶颈在于要往一个容器里频繁放入基本类型的数据的话,恐怕就得靠改用一些专门为容纳基本类型的数据而设计的容器类来解决了(例如Jarkata Commons Primitives组件里提供的那些)。 清单30:一段需要往一个容器里频繁放入基本类型的数据的程序

    import java.util.*;

    public class WordCounter {

         public static void main(String[] args) {

             HashMap counts = new HashMap();

             for (int i = 0; i < args.length; i++) {

                 String current = args;

                 if (counts.containsKey(current)) {

                     counts.put(current,
    ((Integer) counts.get(current)) + 1);

                 } else {

                     counts.put(current, 1);

                 }

             }

             for (Iterator itr = counts.keySet().iterator(); itr.hasNext();) {

                 String key = (String) itr.next();

                 System.out.println(key + ":" + counts.get(key));

             }

         }

    }


    12. 归纳总结 借助J2SE 1.5里提供的Autoboxing/Auto-Unboxing机制,可以用一种更简单的方式,来解决同时存在两套类型系统而造成的一些不方便。不过,这种机制并没有解决所有的相关问题,有些工作还是需要靠手工操作来进行。另外,由于不恰当的使用这一机制会造成一些性能方面的负面影响,所以在使用的时候还要注意一些问题才行。
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|Java学习者论坛 ( 声明:本站资料整理自互联网,用于Java学习者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2025-2-27 01:58 , Processed in 0.347855 second(s), 36 queries .

    Powered by Discuz! X3.4

    © 2001-2017 Comsenz Inc.

    快速回复 返回顶部 返回列表