背景

有Java基础的同学都知道Java中有Primitive Type(原始类型),比如int、short。作为面向对象的语言,Java同时提供了每个原始类型的包装类型(本质是引用类型Reference Type),比如Integer、Long、Boolean.

为了方便大家写代码,JDK 5以后引入了自动拆装箱的机制. 比如对于函数:

add(Integer a)

我们在调用的时候,传一个Integer对象并不是必须的,有时直接传一个原始类型即可:

//int型变量直接传

int i = 1;

add(i);

//或者数字字面量,本身也是int型

add(5);

Java会自动将int装换成Integer,这个过程称为装箱,由于是Java自动做的,所以叫自动装箱(autoboxing),反之如果是将Integer自动装换成int,则称为自动拆箱(autounboxing)。有了自动拆装箱,平时在写代码的时候很Happy,瞬间觉得世界真美好~

意外

然而,事情并不总是很顺利,比如我们有时会遇到这种场景(演示实例来自同事琛总):

//A.java有类A,A调了B的方法add(int i),这时传的是个原始类型, 完美匹配class A {

public static void main(String[] args) {

B.add(1);

}

}

//B.javaclass B {

public static void add(int i) {

System.out.println(i);

}

}

执行命令

javac A.java //这时会同时生成A.class和B.class

java A //运行成功

然后我们做一件事,把B稍作修改,让B的add方法接受Integer包装类型

//B.javaclass B {

public static void add(Integer i) { // 这里把int i 改成 Integer i后重新编译 System.out.println(i);

}

}

接着重新编译B.java文件,注意:只重新编译B.java,相当于B类做了升级,而调用方A并不做任何改变,A.class也不重新生成。然后,我们执行java A命令运行,结果却并没有像我们想象中的那样,而是报了如下错误

Exception in thread "main" java.lang.NoSuchMethodError: B.add(I)V

at A.main(A.java:4)

说好的自动拆装箱呢

NoSuchMethodError的错误报出来的时候,一脸的黑人问号:不是有个add(Integer i)方法吗?怎么会说找不到方法?说好的自动拆装箱呢?

肯定是哪里出了问题!带着问题搜到了知乎R大的一个关于Java自动拆装箱的回答:

根据R大的解释,Java的自动拆装箱发生在编译期,即javac编译的那一刻,而不是在运行期!笔者的潜意识里认为,自动拆装箱会发生在运行期,所以会觉得NoSuchMethodError的错误简直不可思议。

如果编译器发现需要自动拆装箱,会用语法糖的方法自动给你加上Integer.valueOf(),即将A类里面的1变成Integer.valueOf(1),然后生成在A.class文件里。但是我们编译A文件的时候,B的add方法接受的是int型,所以A.class文件里并没有Integer.valueOf(1)这一步,A.class文件里要调用的还是add(int i)。

后来,我们把B文件的add(int i)方法变成了add(Integer i), 本质上相当于删除了一个旧方法,添加了一个全新的方法,这个时候A.class还是老的样子,一旦运行java A,java虚拟机就去找B中的add(int i)方法,然而它已经找不到这个方法了,因为已经被删除了,只留下了add(Integer i)方法,所以会报NoSuchMethodError.

继续扒开自动拆装箱的底裤

我们执行以下命令来查看A.class的具体信息

javap -verbose A.class

当B类的方法为add(int i)时,A.class的信息如下:

class A

minor version: 0

major version: 52

flags: ACC_SUPER

Constant pool:

#1 = Methodref #4.#13 // java/lang/Object."":()V

#2 = Methodref #14.#15 // B.add:(I)V

#3 = Class #16 // A

#4 = Class #17 // java/lang/Object

#5 = Utf8

#6 = Utf8 ()V

#7 = Utf8 Code

#8 = Utf8 LineNumberTable

#9 = Utf8 main

#10 = Utf8 ([Ljava/lang/String;)V

#11 = Utf8 SourceFile

#12 = Utf8 A.java

#13 = NameAndType #5:#6 // "":()V

#14 = Class #18 // B

#15 = NameAndType #19:#20 // add:(I)V

#16 = Utf8 A

#17 = Utf8 java/lang/Object

#18 = Utf8 B

#19 = Utf8 add

#20 = Utf8 (I)V

{

A();

descriptor: ()V

flags:

Code:

stack=1, locals=1, args_size=1

0: aload_0

1: invokespecial #1 // Method java/lang/Object."":()V

4: return

LineNumberTable:

line 1: 0

public static void main(java.lang.String[]);

descriptor: ([Ljava/lang/String;)V

flags: ACC_PUBLIC, ACC_STATIC

Code:

stack=1, locals=1, args_size=1

0: iconst_1

1: invokestatic #2 // Method B.add:(I)V

4: return

LineNumberTable:

line 4: 0

line 5: 4

}

SourceFile: "A.java"

重点关注下1: invokestatic #2这段,即对应A.java中的add(1)要调用add方法的逻辑,#2指向常量池#2 = Methodref #14.#15, 然后把 #14.#15继续展开,即 #18.#19:#20 , 最后展开的样子其实就是注释的样子 B.add:(I)V,这说明到了汇编这一层,运行期找的就是add(int i)方法。

然后,如果我们把B类的方法改为add(Integer i)时,重新编译后的A.class的信息如下:

class A

minor version: 0

major version: 52

flags: ACC_SUPER

Constant pool:

#1 = Methodref #5.#14 // java/lang/Object."":()V

#2 = Methodref #15.#16 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;

#3 = Methodref #17.#18 // B.add:(Ljava/lang/Integer;)V

#4 = Class #19 // A

#5 = Class #20 // java/lang/Object

#6 = Utf8

#7 = Utf8 ()V

#8 = Utf8 Code

#9 = Utf8 LineNumberTable

#10 = Utf8 main

#11 = Utf8 ([Ljava/lang/String;)V

#12 = Utf8 SourceFile

#13 = Utf8 A.java

#14 = NameAndType #6:#7 // "":()V

#15 = Class #21 // java/lang/Integer

#16 = NameAndType #22:#23 // valueOf:(I)Ljava/lang/Integer;

#17 = Class #24 // B

#18 = NameAndType #25:#26 // add:(Ljava/lang/Integer;)V

#19 = Utf8 A

#20 = Utf8 java/lang/Object

#21 = Utf8 java/lang/Integer

#22 = Utf8 valueOf

#23 = Utf8 (I)Ljava/lang/Integer;

#24 = Utf8 B

#25 = Utf8 add

#26 = Utf8 (Ljava/lang/Integer;)V

{

A();

descriptor: ()V

flags:

Code:

stack=1, locals=1, args_size=1

0: aload_0

1: invokespecial #1 // Method java/lang/Object."":()V

4: return

LineNumberTable:

line 1: 0

public static void main(java.lang.String[]);

descriptor: ([Ljava/lang/String;)V

flags: ACC_PUBLIC, ACC_STATIC

Code:

stack=1, locals=1, args_size=1

0: iconst_1

1: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;

4: invokestatic #3 // Method B.add:(Ljava/lang/Integer;)V

7: return

LineNumberTable:

line 4: 0

line 5: 7

}

SourceFile: "A.java"

重点在这里

0: iconst_1

1: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;

4: invokestatic #3 // Method B.add:(Ljava/lang/Integer;)V

7: return

明显多了一行invokestatic #2, 而这一行显然就是Integer.valueOf(1)的过程,即自动装箱的过程,也就是编译自动帮我们加上的一段代码,之后4: invokestatic #3才去调用B类的add(Integer i) 方法。

所以,我们潜意识里以为会在运行期执行的拆装箱的过程,其实在编译期就做好了;在运行期JVM会严格按照class文件中的执行过程来寻找相应的匹配方法,而add(int i)和add(Integer i)方法显然不是同一个方法,当然会报NoSuchMethodError.

聊聊代码兼容问题

以上案例是我们刻意设计出来的,其实在真实的场景中是会碰到这种问题的。比如我们在自己的项目中引用了两个不同项目jar1 和jar2,而jar1同时引用了jar2的方法,如果jar2把方法add(int i)改为add(Integer i),而我们引用的jar1还是老的,这个时候我们项目再去调jar1里的方法,jar1再调新jar2的方法add(int),就会报NoSuchMethodError.

其实这类问题背后反映的是代码兼容性的问题,比如:

//B类version1

class B {

public static void add(int i) {

System.out.println(i);

}

}

//B类version2

class B {

public static void add(Integer i) {

System.out.println(i);

}

}

B类从version1 到 version2的升级,并不是一个兼容性的升级, add(int i)方法和 add(Integer i)不是同一个方法,version2的版本相当于删除了原来的方法,新加了一个方法,如果有历史jar包还在调用老的方法而且没有重新编译,而且JVM中加载的又是version2的B类,那么最终的结果一定是报错。

兼容性的升级是重载这个方法:

//B类version3class B {

public static void add(int i) {

System.out.println(i);

}

public static void add(Integer i) {

System.out.println(i);

}

}

类似的代码兼容性问题还有很多,比如我们给别人提供的RPC方法中,显然是不能随便删除字段的,这个很容易理解,删除字段后,别人在线上跑的应用用的还是旧的API,他们获取不到想要的字段肯定是会出问题的。

而添加字段就是一个对兼容友好的升级行为,我们添加的字段,使用旧API的消费方虽然看不到新字段的存在,但是老字段依然还是可用的。比如服务端给客户端提供的JSON API,客户端只关心自己需要的字段,服务端添加字段上线,并不会影响老版本的客户端的使用,因为老版本客户端在做JSON反序列化的时候只根据字段名反序列化。

而服务端通信协议Thrift的反序列化和JSON又不一样,Thrift反序列化过程并不以是字段名为参考,而是和顺序强相关,比如对于Thrfit 的 struct类型:

//Person version1struct Person{

1:i32 id

2:string name

}

//Person version2struct Person{

1:i32 id

2:i32 age

3:string name

}

//Person version3struct Person{

1:i32 id

2:string name

3:i32 age

}

如果服务提供方将Person升级到version2,那么对于还在使用version1 的消费者来说,Person实例请求回来要反序列化的时候,会把第二个age反序列化成name,显然这不是我们想要的结果,而对兼容友好的升级应该是version3那种,不影响前面字段的排序,在后面添加字段,这样就不会对老版本的API造成反序列的错乱。

软件行业兼容性的典型案例就是微软家的Windows操作系统,有人尝试过过把Windows 3.1的很多程序放到Windows XP上去安装使用,竟然发现还能正常运行,甚至很流畅,真是惊叹Windows对兼容性的执着!有人说,Windows几乎是业界兼容性做的最好的OS,而这一点也许是Windows在桌面市场领域能独占鳌头的重要原因之一。

java自动装箱的好处_Java自动拆装箱为什么不起作用了相关推荐

  1. java自动装箱的好处_Java自动装箱、自动拆箱

    一.前言 Java自动装箱和自动拆箱是JDK5.0版本提供的新特性,所以在JDK5.0后的版本中都可以使用,之前的版本则不支持该特性. 理解自动装箱和自动拆箱需要先对java中的8种原始数据类型和相对 ...

  2. java自动装箱怎么实现_Java 自动装箱与拆箱的实现原理

    什么是自动装箱和拆箱 自动装箱就是Java自动将原始类型值转换成对应的对象,比如将int的变量转换成Integer对象,这个过程叫做装箱,反之将Integer对象转换成int类型值,这个过程叫做拆箱. ...

  3. java泛型机制的好处_java 泛型机制

    Java 泛型 泛型这种语法机制,只在程序编译阶段起作用,只是给编译器参考的(运行阶段泛型没用) 使用泛型的好处是什么? 1.集合中存储的元素类型统一了 2.从集合中取出的元素类型是泛型指定的类型,不 ...

  4. java dao 泛型的好处_java中泛型有什么作用

    泛型的作用如下: 1.类型安全 泛型的主要目标是提高 Java 程序的类型安全.编译时的强类型检查:通过知道使用泛型定义的变量的类型限制,编译器可以在一个高得多的程度上验证类型假设.没有泛型,这些假设 ...

  5. java socket中的方法_Java中关于Socket的方法与作用详解

    1.java.net.Socket;套接字.封装了TCP通讯协议,使用它可以基于TCP与远端计算机上的服务端应用程序链接并进行通讯. 实例化Socket就是与服务器端建立连接的过程.这里需要传入两个参 ...

  6. java基本数据类型自动转包装类_Java基础教程之基本类型数据类型、包装类及自动拆装箱...

    前言 我们知道基本数据类型包括byte, short, int, long, float, double, char, boolean,对应的包装类分别是Byte, Short, Integer, L ...

  7. java装箱和拆箱_java自动装箱和拆箱

    这个是jdk1.5以后才引入的新的内容,作为秉承发表是最好的记忆,毅然决定还是用一篇博客来代替我的记忆: java语言规范中说道:在许多情况下包装与解包装是由编译器自行完成的(在这种情况下包装成为装箱 ...

  8. java bufferedwriter会自动创建文件吗_Java中为什么会有包装类?自动拆装箱必要吗?关于Wrapping Class这是重点!...

    入题 自动封箱与拆箱人人皆可言之道之,但封箱和拆箱却被多数人略之!如此简单的一个机制,却影射着Java的核心理念,不清楚?继续向下看吧~ Java中的数据类别 目前Java中的数据类别分为两种,一种是 ...

  9. java 包装类缺点_Java 自动拆箱和自动装箱学习笔记

    Java 自动拆箱和自动装箱学习笔记 详情参考以下 1. 概述 Java 中的自动装箱和自动拆箱算是一种语法糖,也就是在编译阶段编译器在合适的情况下帮我们的做了自动拆箱和自动装箱. 众所周知,Java ...

最新文章

  1. ES6 Proxy 性能之我见
  2. 【Qt】错误处理:error: undefined reference to `qMain(int, char**)‘
  3. Quartz 2D Programming Guide笔记
  4. 简单读懂微生物基因组的泛基因组学
  5. java 模拟ajax上传图片
  6. Hive Error : Java heap space 解决方案
  7. SQL Server-聚焦事务对本地变量、临时表、表变量影响以及日志文件存满时如何收缩(三十一)...
  8. mysql 客户端_技术分享 | MySQL 客户端连不上(1045 错误)原因全解析
  9. OpenGL 光照贴图Lighting maps
  10. Linux(CentOS)中常用软件安装,使用及异常——Zookeeper, Kafka
  11. 3-3:常见任务和主要工具之网络
  12. Django:DjangoProject项目结构简介
  13. SQL.H 通过此文件寻找sqlAPI编程的一种捷径
  14. java获取中文拼音首字母
  15. python简单笔试题_这十道经典Python笔试题,全做对算我输
  16. ArduinoUNO实战-第八章-无源蜂鸣器
  17. 欲戴王冠,必承其重。
  18. 大数据时代个人信息保护的困境与思考
  19. 最近网上比较火的虎年西游记金钱豹头像制作小程序源码
  20. python调用scp上传目录_python执行scp命令拷贝文件及文件夹到远程主机的目录方法...

热门文章

  1. Java面向对象思想、类的定义、对象的使用、对象内存图、成员变量和局部变量的区别、封装、private关键字、this关键字、构造方法、JavaBean-day06
  2. 基于Vue和SpringBoot的宾馆管理系统的设计和实现
  3. 关于 CouchDB 的一些知识
  4. 客似云来, 剪花布条
  5. revit插件:快速创建踢脚线连同墙体一起画,4步完成
  6. 仿 qq登录界面 php,js仿腾讯QQ的web登陆界面
  7. Revit 2016 笔记01------------2021-10-15
  8. Arduino开发板使用DHT11温湿度传感器的方法
  9. 实习:slam算法的学习整理
  10. Linux IPC总结(全)