记得有次面试,面试官问我:java
如何写一个方法交换两个 Integer 类型的值?程序员
当时内心一惊,这是把我当小白了呀!交换两个数的值还不容易么,最简单的直接搞一个中间变量,而后就能够交换了... ... 面试
面试官随即拿出一张雪白雪白的 A4 纸缓存
工具用多了,有没有体验过白纸写代码?来吧,开始你的表演,小伙子。bash
此时稍微有点心虚,但仍是要装腔做势,把本身想象成大佬才行。ide
有的人可能会问,你不是说很简单么,还心虚个啥?写过代码的都知道,工具写代码是有自动补全提示的,这白板写代码纯粹就是考察你对代码的熟练度,其实至关考验代码功底。工具
因而乎,提起笔,奋笔疾书,唰唰唰不到两分钟,我就写完了。性能
代码以下:优化
public static void main(String[] args) { Integer a = 1, b = 2; System.out.println("交换前:a = " + a + ", b = " + b); swap(a,b); System.out.println("交换后:a = " + a + ", b = " + b);}public static void swap(Integer i, Integer j) { int temp = i; i = j; j = temp;}复制代码
当我成竹在胸的把纸递过去的时候,我仿佛看见面试官嘴角哪不经意间的微笑。ui
这一笑没关系,要紧的是一个大男人对着我笑干吗?
难道个人代码感动到他了?
明人不说暗话,这明显不可能。
难道是他要对我... ...
想到此处,我不由赶忙回忆了下来时的路,怎么样能够快速冲出去... ...
喂,醒醒,想啥呢
面试官瞄了一眼代码以后开始发问呢。
你肯定你这段代码真的能够交换两个 Integer 的值吗?(居然在 Integer 上加了重音)
个人天呐,难道有问题,多年面试经验告诉我,面试重音提问要不就是在故意混淆,要不就是在善意提醒你,看能不能挖掘出点其余技术深度出来。
因此根据面试官的意思确定是使用这段代码不能交换呢,哪么不能交换的缘由在哪里?
首先,想了下,要交换两个变量的值,利用中间变量这个思路是不会错的。既然思路没错,哪就要往具体实现上想,问题出在哪里。
咱们都知道,Java 中有两种参数传递
值传递
方法调用时,实际参数把它的值传递给对应的形式参数,方法执行中形式参数值的改变不影响实际参数的值。
引用传递
也称为传地址。方法调用时,实际参数的引用(地址,而不是参数的值)被传递给方法中相对应的形式参数,在方法执行中,对形式参数的操做实际上就是对实际参数的操做,方法执行中形式参数值的改变将会影响实际参数的值。
简单总结一下就是:
也就是说 对象类型(地址空间)的变量存在于堆中,其引用存在于栈中。
至于为何这么设计:主要仍是为了考虑访问效率和提高代码性能上考虑的。
难道问题出在这个地方?
但是 Integer 不就是 引用类型?
为何不能改变呢?
难道 Integer 的实现有什么特殊之处?
你别说,还真是 Integer 有他本身的独特之处。
/** * The value of the {@code Integer}. * * @serial */private final int value;复制代码
这个属性也是表示这个 Integer 实际的值,可是他是 private final 的,Integer 的 API 也没有提供给外部任何能够修改它的值接口,也就是说这个值改变不了。
简单理解就是上面的 swap 方法其实真实交换的是 两个形参 i 和 j 的值,而没有去改变 a 和 b 的值
画个图简单理解一下:
哪如何去改变这个 value 值呢 ?
赶忙给面试官陪着笑脸说刚才激动了,代码我能不能再改改?
面试官:能够,代码原本就是一个不断优化的过程,你改吧!
而后又是一顿奋笔疾书,再次唰唰唰写了以下代码:
public static void swap(Integer i, Integer j) throws NoSuchFieldException, IllegalAccessException { /*int temp = i; i = j; j = temp;*/ Field value = Integer.class.getDeclaredField("value"); int temp = i.intValue(); value.set(i,j.intValue()); value.set(j,temp);}复制代码
此次长脑子呢,我又回过头检查了一遍代码,没办法,很蛋疼,这要是有电脑先跑一遍再说。
白纸只能靠你本身脑子想,脑子编译,脑子运行(固然运行很差可能就烧坏了)
果真,查出问题来了(还好够机智)
前边不是说了这个 value 是私有属性么,既然是 private 的 ,final 的,在 Java 中是不容许的,再访问的时候会报
java.lang.IllegalAccessException
异常,
在反射的时候还须要加value.setAccessible(true)
,设置代码执行时绕过对私有属性的检查,哪么代码就变成了以下:
public static void swap(Integer i, Integer j) throws NoSuchFieldException, IllegalAccessException { /*int temp = i; i = j; j = temp;*/ Field value = Integer.class.getDeclaredField("value"); value.setAccessible(true); int temp = i.intValue(); value.set(i,j.intValue()); value.set(j,temp);}复制代码
另外多提几句:设置了 setAccessible(true)
就能访问到私有属性是由于他的源码是这样的
public void setAccessible(boolean flag) throws SecurityException { SecurityManager sm = System.getSecurityManager(); if (sm != null) sm.checkPermission(ACCESS_PERMISSION); setAccessible0(this, flag);}复制代码
能够看到,他调用了 setAccessible0()
这个方法,继续看下这个:
private static void setAccessible0(AccessibleObject obj, boolean flag) throws SecurityException { if (obj instanceof Constructor && flag == true) { Constructor<?> c = (Constructor<?>)obj; if (c.getDeclaringClass() == Class.class) { throw new SecurityException("Cannot make a java.lang.Class" + " constructor accessible"); } } obj.override = flag; }复制代码
这段代码咱们须要关注有两点:
参数是 boolean flag
而这个 flag 实际的值刚好是咱们设置进去的setAccessible()
中的参数
这个参数真正的做用是把一个 AccessibleObject
对象的 override
属性进行了赋值
哪么这个 override
属性的做用又是什么呢?
咱们一块儿来看下value.set()
这个方法的源码
@CallerSensitive public void set(Object obj, Object value) throws IllegalArgumentException, IllegalAccessException { if (!override) { if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { Class<?> caller = Reflection.getCallerClass(); checkAccess(caller, clazz, obj, modifiers); } } getFieldAccessor(obj).set(obj, value); }复制代码
看着这段代码是不瞬间就明白了,原来这个 overried
属性就比如一个开关,负责控制在 set
值得时候是否须要检查访问权限(不少时候,一直说要阅读源码阅读源码,由于源码就比如火眼金睛,在源码面前,不少妖魔鬼怪都是无所遁形的)
看着这段代码乐开了花,内心想着这下应该总能交换了吧,我又很是自信的把代码递给了面试官
面试官:为何要这么改?为何要使用反射?为何要加这行 setAccessible(true) ?
哇 此时正和我意啊,写了这半天,就等你问我这几个点,因而我很利索的把上边描述的给面试官讲了一遍,他听完以后继续微微一笑,这个笑很迷,也很渗人。
难道这还不对?
他又开始发问:
面试官:这段代码仍是会有问题,最终输出结果会是 a = 2, b = 2。能够提示你一下,你知道拆箱装箱吗?
呃,这还涉及到拆箱装箱了... ...
咱们在上面的代码中
Integer a = 1, b = 2;复制代码
a 和 b 是 Integer 类型,可是 1 和 2 是 int 类型,为何把 int 赋值给 Integer 不报错?
由于 Java 中有自动装箱(若是感兴趣的话可使用 javap 命令去查看一下这行代码执行的字节码)
实际上 Integer a = 1 就至关于执行了 Integer a = Integer.valueOf(1);
复制代码
哪么,valueOf()
方法的实现又是什么样的呢?
public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); }复制代码
这个方法的代码说明,若是你的值是在某个范围以内,会从 IntegerCache
这个缓存中获取值,而不是去 new 一个新的 Integer
对象。继续研究 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; }复制代码
根据以上代码能够获得:在范围在 -128 - 127 之间的数字,会被直接初始化好以后直接加入获得缓存中,以后处于这个范围中的全部Integer 会直接从缓存获取值,这样提升了访问效率
为了验证这一点,你能够直接试一试,写一段代码:
Integer a = 100,b = 100;System.out.println(a == b);复制代码
根据咱们在 Java 领域的理解,对于引用类型,使用 == 比较的是他们在内存中的地址,哪么,对于Integer这个引用类型,直接使用 == 结果应该是false
。
但是,你若是实际调试试一试的话,会发现这是 true
, 是否是有点难以想象?
哪么为何是ture
,就回到了上边说的缓存问题,由于 100 处于 -128-127 这份范围
若是你定义的变量是
Integer a = 200,b = 200;System.out.println(a == b);复制代码
这个结果输出确定是 false
,由于根据前边Integer。valueOf()
实现的源码能够获得:超过-128-127的值须要从新 new Integer(i)
,但凡是 new
出来的,使用 ==
比确定是 false
继续深究下去你会发现面试官说的 a = 2, b = 2 是对的,具体缘由是:
public static void swap(Integer i, Integer j) throws NoSuchFieldException, IllegalAccessException { Field value = Integer.class.getDeclaredField("value"); value.setAccessible(true); int temp = i.intValue(); // 此处 咱们使用 j.intValue 返回结果是个 int 类型数据 // 而 value.set()方法须要的是一个 Object 对象 此处就涉及到了装箱 // 因此 i 值的实际变化过程为:i = Integer.valueOf(j.intValue()).intValue() value.set(i,j.intValue()); // 同理 j 值得实际变化过程为:j = Integer.valueOf(temp).intValue() // 由于 valueOf() 要从缓存获取值 也就是此时须要根据 temp 的下标来获取值 // 但是在上一步中 i 的值已经被自动装箱以后变成了 2 // 因此此处会把 j 的值设置成 2 value.set(j,temp); }复制代码
综上:咱们就搞清楚了为何面试官会说结果是 a = 2, b = 2 .
既然分析出了为何会变成 a = 2 b = 2,哪就好办呢。
发现问题,解决问题,永远是程序员最优秀的品质,对了,还要脸皮厚(小声哔哔)
我又厚着脸把代码要过来了(面试官仍是一如既往的微笑,一如既往很迷的笑)
把 set 改成 setInt 避免装箱操做
public static void swap(Integer i, Integer j) throws NoSuchFieldException, IllegalAccessException { Field value = Integer.class.getDeclaredField("value"); value.setAccessible(true); int temp = i.intValue(); value.setInt(i,j.intValue()); value.setInt(j,temp); }}复制代码
把 temp 从新建立一个对象进行赋值,这样就不会和 i 的值产生相互影响
public static void swap(Integer i, Integer j) throws NoSuchFieldException, IllegalAccessException { Field value = Integer.class.getDeclaredField("value"); value.setAccessible(true); int temp = new Integer(i.intValue()); value.setInt(i,j.intValue()); value.setInt(j,temp); }复制代码
靠着脸厚,我第三次把代码交给了面试官,没办法,厚度不是你所能想象的... ...
这一次,他终于再也不笑了,再也不很迷的笑了
看来这场面试要迎来终结了 ... ...
面试官:嗯,你总算答对了,如今来总结一下这道题涉及到的知识点(这是要考察表达能力啊)
值传递和引用传递
Integer 实现缓存细节
使用反射修改私有属性的值
拆箱和装箱
有没有不总结不知道,一总结吓一跳的感受,这么一道看似简单的题,居然考察到了这么多东西
面试官:好了,技术问题咱们今天就先面到这里,接下来可否说一说你有什么长处?
我:我是一个思想积极乐观向上的人。
面试官:可否举个例子。
我:何时开始上班?
欢迎关注公众号: