前几天在头条上看到一道经典面试题,引发了一些思考。也是写这篇文章的导火索。
请看题:
- public class Main {
- public static void main(String[] args) {
- Integer a = 1;
- Integer b = 2;
- System.out.println("a=" + a + ",b=" + b);
- swap(a, b);
- System.out.println("a=" + a + ",b=" + b);
- }
- private static void swap(Integer numa, Integer numb) {
- //请实现
- }
- }
看到这个题后 瞬间觉得有坑。也觉得为什么要书写一个
方法呢?如下实现不是更简单:
- swap
- public static void main(String[] args) {
- Integer a = 1;
- Integer b = 2;
- System.out.println("a=" + a + ",b=" + b);
- Integer tmp = a;
- a = b;
- b = tmp;
- System.out.println("a=" + a + ",b=" + b);
- }
输出:
- a=1,b=2
- a=2,b=1
完美实现交换。但是请注意,这是一道面试题,要的就是考验一些知识点。所以还是老老实实的实现
方法吧。有的同学可能会想,
- swap
是一个包装类型,是对Int的装箱和拆箱操作。其实也是一个对象。既然是对象,直接更改对象的引用不就行了?思路没问题,我们首先看看实现:
- Integer
- private static void swap(Integer numa, Integer numb) {
- Integer tmp = numa;
- numa = numb;
- numb = tmp;
- System.out.println("numa=" + numa + ",numb=" + numb);
- }
输出:
- a=1,b=2
- numa=2,numb=1
- a=1,b=2
不出意外,没有成功
这是什么原因呢?
技术老手一看就知道问题出在形参和实参
混淆了
形参 顾名思义:就是形式参数,用于定义方法的时候使用的参数,是用来接收调用者传递的参数的。
形参只有在方法被调用的时候,虚拟机才会分配内存单元,在方法调用结束之后便会释放所分配的内存单元。
因此,形参只在方法内部有效,所以针对引用对象的改动也无法影响到方法外。
实参 顾名思义:就是实际参数,用于调用时传递给方法的参数。实参在传递给别的方法之前是要被预先赋值的。
在本例中 swap 方法 的numa, numb 就是形参,传递给 swap 方法的 a,b 就是实参
注意:
在
调用过程中,只能把实参传递给形参,而不能把形参的值反向作用到实参上。在函数调用过程中,形参的值发生改变,而实参的值不会发生改变。而在
- 值传递
调用的机制中,实际上是将实参引用的地址传递给了形参,所以任何发生在形参上的改变也会发生在实参变量上。那么问题来了,什么是
- 引用传递
和
- 值传递
- 引用传递
在谈
和
- 值传递
之前先了解下 Java的数据类型有哪些
- 引用传递
Java 中的数据类型分为两大类,
和
- 基本类型
。相应的,变量也有两种类型:
- 对象类型
和
- 基本类型
- 引用类型
的变量保存
- 基本类型
,即它代表的值就是数值本身,
- 原始值
一般对应在内存上的
- 原始值
而
- 栈区
的变量保存
- 引用类型
,
- 引用值
指向内存空间的地址。代表了某个对象的引用,而不是对象本身。对象本身存放在这个引用值所表示的地址的位置。
- 引用值
对应内存上的
- 被引用的对象
。基本类型包括:
- 堆内存区
,
- byte
,
- short
,
- int
,
- long
,
- char
,
- float
,
- double
这八大基本数据类型引用类型包括:
- boolean
,
- 类类型
和
- 接口类型
- 数组
基本数据类型在声明时系统就给它分配空间
- int a;//虽然没有赋值,但声明的时候虚拟机就会 分配 4字节 的内存区域,而引用数据类型不同,它声明时只给变量分配了引用空间,而不分配数据空间:
- String str;//声明的时候没有分配数据空间,只有 4byte 的引用大小,在栈区,而在堆内存区域没有任何分配
- str.length(); //这个操作就会报错,因为堆内存上还没有分配内存区域,而 a = 1; 这个操作就不会报错。
好了,Java的数据类型说完了,继续我们的
和
- 值传递
的话题。先背住一个概念:
- 引用传递
的变量是
- 基本类型
;
- 值传递
的变量结合前面说的
- 引用类型
和
- 形参
。
- 实参
方法调用时,实际参数把它的值传递给对应的形式参数,函数接收的是原始值的一个copy,
此时内存中存在两个相等的基本类型,即实际参数和形式参数,后面方法中的操作都是对形参这个值的修改,不影响实际参数的值
也称为
,
- 地址传递
。方法调用时,实际参数的引用(地址,而不是参数的值)被传递给方法中相对应的形式参数,函数接收的是原始值的内存地址在方法执行中,形参和实参内容相同,指向同一块内存地址,方法执行中对引用的操作将会影响到实际对象通过例子来说话:
- 址传递
- static class Person {
- int age;
- Person(int age) {
- this.age = age;
- }
- }
- private static void test() {
- int a = 100;
- testValueT(a);
- System.out.println("a=" + a);
- Person person = new Person(20);
- testReference(person);
- System.out.println("person.age=" + person.age);
- }
- private static void testValueT(int a) {
- a = 200;
- System.out.println("int testValueT a=" + a);
- }
- private static void testReference(Person person) {
- person.age = 10;
- }
输出:
- int testValueT a=200
- a=100
- person.age=10
看见
a的值并没有改变,而
- 值传递
的 persion.age已经改变了有人说
- 引用传递
- private static void testReference(Person person) {
- person = new Person(100);
- }
为什么 输出的 person.age 还是20呢?
我想说 了解一下什么是
吧? 方法内把
- 引用类型
的地址引用换成了另一个对象,并没有改变这个对象,并不能影响 外边
- 形参
还引用原来的对象,因为 形参只在方法内有效哦。
- 实参
有人或许还有疑问,按照文章开头的例子,
也是
- Integer
该当如何呢?其实 类似的
- 引用类型
,
- String
,
- Integer
,
- Float
,
- Double
,
- Short
,
- Byte
,
- Long
等等基本包装类型类。因为他们本身没有提供方法去改变内部的值,例如
- Character
内部有一个
- Integer
来记录
- value
基本类型的值,但是没有提供修改它的方法,而且 也是
- int
类型的,无法通过
- final
更改。所以虽然他们是
- 常规手段
的,但是我们可以认为它是
- 引用类型
,这个也只是
- 值传递
,事实上还是
- 认为
,
- 引用传递
。
- 址传递
好了,基础知识补充完毕,然我们回到面试题吧
回归正题
- private static void swap(Integer numa, Integer numb) {
- Integer tmp = numa;
- numa = numb;
- numb = tmp;
- System.out.println("numa=" + numa + ",numb=" + numb);
- }
通过补习基础知识,我们很明显知道 上面这个方法实现替换 是不可行的。因为
虽然是
- Interger
但是上述操作只是改变了
- 引用类型
的引用,而没有改变
- 形参
对应的
- 实参
。
- 对象
那么思路来了,我们
改变
- 通过特殊手段
内部的
- Integer
属性
- value
- private static void swap(Integer numa, Integer numb) {
- Integer tmp = numa;
- try {
- Field field = Integer.class.getDeclaredField("value");
- field.setAccessible(true);
- field.set(numa, numb); //成功的将numa 引用的 1的对象 值改为 2
- field.set(numb, tmp); //由于 tmp 也是指向 numa 未改变前指向的堆 即对象1 ,经过前一步,已经将对象1的值改为了2,自然 numb 也是2,所以改动失效
- } catch(Exception e) {
- e.printStackTrace();
- }
- }
输出结果:
- a=1,b=2
- a=2,b=2
又来疑问了?为何
的值改变成功,而
- a
的改变失败呢?
- b
见代码注释
所以其实
是更改成功的,只是 tmp 经过前一行代码的执行,已经变成了 2。那么如何破呢?我们有了一个思路,既然是
- field.set(numb, tmp);
的引用的对象值变量,那么我让
- tmp
不引用
- tmp
了
- numa
- private static void swap(Integer numa, Integer numb) {
- int tmp = numa.intValue(); //tmp 定义为基本数据类型
- try {
- Field field = Integer.class.getDeclaredField("value");
- field.setAccessible(true);
- field.set(numa, numb); //这个时候并不改变 tmp 的值
- field.set(numb, tmp);
- } catch(Exception e) {
- e.printStackTrace();
- }
- }
这种情况下 对
这个对象的修改就不会导致
- numa
的值变化了,看一下运行结果
- tmp
- a=1,b=2
- a=2,b=2
这是为啥?有没有
啦?难道我们的思路错了?先别着急,我们看看这个例子:仅仅是将前面的例子
- 快疯
的值改为 129,
- a
的值改为130
- b
- public static void main(String[] args) {
- Integer a = 129;
- Integer b = 130;
- System.out.println("a=" + a + ",b=" + b);
- swap(a, b);
- System.out.println("a=" + a + ",b=" + b);
- }
- private static void swap(Integer numa, Integer numb) {
- int tmp = numa.intValue();
- try {
- Field field = Integer.class.getDeclaredField("value");
- field.setAccessible(true);
- field.set(numa, numb);
- field.set(numb, tmp);
- } catch(Exception e) {
- e.printStackTrace();
- }
- }
运行结果:
- a=129,b=130
- a=130,b=129
有没有
?我们的思路没有问题啊?为什么 换个数值就行了呢?我们稍微修改一下程序
- 怀疑人生
- public static void main(String[] args) {
- Integer a = new Integer(1);
- Integer b = new Integer(2);
- System.out.println("a=" + a + ",b=" + b);
- swap(a, b);
- System.out.println("a=" + a + ",b=" + b);
- }
- private static void swap(Integer numa, Integer numb) {
- int tmp = numa.intValue();
- try {
- Field field = Integer.class.getDeclaredField("value");
- field.setAccessible(true);
- field.set(numa, numb);
- field.set(numb, tmp);
- } catch(Exception e) {
- e.printStackTrace();
- }
- }
运行结果:
- a=1,b=2
- a=2,b=1
哎?为啥 1 和 2 也可以了?
我们这时肯定猜想和
的装箱 拆箱有关
- Integer
为什么
和
- Integer a = 1
效果不一样那就瞅瞅源码吧?
- Integer a = new Integer(1)
- public Integer(int value) {
- this.value = value;
- }
- /**
- * 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);
- }
通过注释知道,java推荐
方式初始化一个
- Integer.valueOf
因为有 缓存了
- Interger
的数字我们直接定义
- -128 - 127
具有这个功能,所以 Jvm 底层实现 是通过
- Integer a = 1
这个方法再看
- Integer.valueOf
我们打断点,发现通过反射设置
- field.set(numb, tmp);
时 竟然走了
- value
方法下面是 我们调用
- Integer.valueOf
前后的
- swap
值得变化
- IntegerCache.cache
在反射修改前
- IntegerCache.cache[128]=0
- IntegerCache.cache[129]=1
- IntegerCache.cache[130]=2
通过反射修改后
- IntegerCache.cache[128]=0
- IntegerCache.cache[129]=2
- IntegerCache.cache[130]=2
再调用
tmp这时等于1 对应的 角标 129 ,但是这个值已经变成了2所以出现了刚才
- field.set(numb, tmp)
原来都是
- 奇怪的结果
下面趁机再看个例子 加深理解
- 缓存的锅
- Integer testA = 1;
- Integer testB = 1;
- Integer testC = 128;
- Integer testD = 128;
- System.out.println("testA=testB " + (testA == testB) + ",\ntestC=testD " + (testC == testD));
输出结果:
- testA = testB true,
- testC = testD false
通过这小示例,在 -128 到 127的数字都走了缓存,这样
和
- testA
引用的是同一片内存区域的同一个对象。而
- testB
- testC
数值大于127 所以 没有走缓存,相当于两个
- testD
对象,在堆内存区域有两个对象。
- Integer
- Integer a = new Integer(1);
- Integer b = new Integer(2);
方式初始化
,
- a
我们的交换算法没有问题,也是这个原因。
- b
方法可以完善啦
- swap
- private static void swap(Integer numa, Integer numb) {
- int tmp = numa.intValue();
- try {
- Field field = Integer.class.getDeclaredField("value");
- field.setAccessible(true);
- field.set(numa, numb);
- field.set(numb, new Integer(tmp));
- } catch(Exception e) {
- e.printStackTrace();
- }
- }
只需将之前的
改为
- field.set(numb, tmp)
- field.set(numb, new Integer(tmp))
到此, 这个面试我们已经通过了,还有一个疑问我没有解答。
为什么
会执行
- field.set(numb, tmp)
而
- Integer.valueOf()
不会执行。这就是
- field.set(numb, new Integer(tmp))
操作,当 给
- Integer的装箱
赋值
- Integer.value
时,JVM 检测到
- int
,需要装箱,才执行了
- int不是Integer类型
方法。而
- Integer.valueOf()
设置的 是Integer类型了,就不会再拆箱后再装箱。
- field.set(numb, new Integer(tmp))
来源: https://juejin.im/entry/5a151897f265da43248016dc