Integer类型的值交换、IntegerCache缓存

Integer类型的值交换、IntegerCache缓存

一个很经典的面试题:

private static void swap(Integer x, Integer y)
{
    //请实现
}

public static void main(String[] args) {
    Integer a = 1;
    Integer b = 2;
    swap(a, b);
    System.out.println("a=" + a + ",b=" + b);
}

题目意思是交换a和b两个Integer类型变量的值,在swap方法中实现

乍一看好像很简单,Integer是int的包装类型,直接通过修改对象的引用实现:

private static void swap(Integer x, Integer y)
{
    //请实现
    Integer temp = x;        
    x = y;
    y = temp;
}

public static void main(String[] args) {
    Integer a = 1;
    Integer b = 2;
    swap(a, b);
    System.out.println("a=" + a + ",b=" + b);
}

输出:

a=1,b=2

显然没有达到预期的效果

这就需要提到形参与实参的区别了:

形参 顾名思义:就是形式参数,用于定义方法的时候使用的参数,是用来接收调用者传递的参数的。 形参只有在方法被调用的时候,虚拟机才会分配内存单元,在方法调用结束之后便会释放所分配的内存单元。 因此,形参只在方法内部有效,所以针对引用对象的改动也无法影响到方法外。

实参 顾名思义:就是实际参数,用于调用时传递给方法的参数。实参在传递给别的方法之前是要被预先赋值的。

在本例中 swap 方法 的x,y就是形参,传递给 swap 方法的 a,b 就是实参

我们可以用图来演示下上面解决方案的过程:

Integer a = 1;

Integer b = 2;

调用swap方法,创建两个局部变量x和y(形参),分别指向堆中1和2的内存区域,这个过程并不会改变a和b(实参)的引用。

Integer temp = x; 定义temp临时变量,temp的引用和x指向同一内存区域。

x = y; 改变了x的引用使其指向2的内存

y = temp; 改变y的引用指向temp指向的内存。

方法swap执行结束,我们可以到实参a和b指向的内存始终没有改变,所以并没有达到交换值的目的。

再看下面一种实现:

public static void main(String[] args) {
    Integer a = 1;
    Integer b = 2;
    Integer temp = a;
    a = b;
    b = temp;
    System.out.println("a=" + a + ",b=" + b);
}

输出结果:

a=2,b=1

这样虽然可以达到目的,但改变题目的做法显然不太行

看一个示例:

public class Person {
    int age;
    public Person(int age) {
        this.age = age;
    }

    static void testPersion(Person person) {
        person.age = 40;
    }

    public static void main(String[] args) {
        Person person = new Person(30);
        person.testPersion(person);
        System.out.println(person.age);
    }
}

输出:

40

Integer和Person一样都是引用类型,能否参照Person类改变值的方式来实现swap方法呢?很遗憾不能,我们查看Integer的源码可以看到:

private final int value;

public Integer(int value) {
	this.value = value;  
}

Integer内部有一个value来记录int基本类型的值,但是没有提供修改它的方法,而且它也是final类型的,类似的还有 String, Float, Double, Short, Byte, Long, Character等,我们均无法访问到其内部的值,因此无法通过常规手段实现来改变它们。

使用反射的方式是可以做到改变私有value的值的。

在讲反射方式之前,先了解一下Java中基本数据类型的装箱和拆箱操作

当我们使用Integer x = 2 的时候,在JVM运行前的编译阶段,该Integer x= 2 将会被编译为Integer x= new Integer(2),此时编译后的这样一个语法是符合JDK运行时的规则的,这种操作就是所谓的装箱操作

接着Integer x = 2; 写上int y = x; 这里就完成了 一个拆箱的操作,即把包装类型转换为基本数据类型。

一般当我们进行对比的时候,编译器会优先把包装类进行自动拆箱:如Integer x1= 2; 和 int x2 = 2; 当执行if(x1 == x2) 时,编译器便会自动的将包装类的x1自动拆箱为int类型进行对比等操作,此处返回true

聊完了拆装箱,我们看一下反射方式实现的几个示例:

示例一:

private static void swap(Integer x, Integer y) throws Exception
{
    //请实现
    Integer temp = x;
    Field field = Integer.class.getDeclaredField("value");//获取成员变量value
	field.setAccessible(true);//暴力反射,用于私有成员
	field.set(x,y);
	field.set(y,temp);//若此处传的temp是int类型(即将第一行Integer temp = x; 改为int temp = x;),会先调用valueOf方法对temp进行装箱操作(public void set(Object obj, Object value))
}

public static void main(String[] args) {
    Integer a = 1;
    Integer b = 2;
    swap(a, b);
    System.out.println("a=" + a + ",b=" + b);
}

输出:

a=2,b=2		

示例二:

private static void swap(Integer x, Integer y) throws Exception
{
    //请实现
    Integer temp = x.intValue();
    Field field = Integer.class.getDeclaredField("value");//获取成员变量value
    field.setAccessible(true);//暴力反射,用于私有成员
    field.set(x,y);
    field.set(y,temp);
}

public static void main(String[] args) {
    Integer a = 1;
    Integer b = 2;
    swap(a, b);
    System.out.println("a=" + a + ",b=" + b);
}

输出:

a=2,b=2

在示例二的基础上,仅仅修改实参a和b的定义,

示例三:

public static void main(String[] args) {
    Integer a = 200;
    Integer b = 300;
    swap(a, b);
    System.out.println("a=" + a + ",b=" + b);
}

输出:

a=300,b=200

示例四:

public static void main(String[] args) {
	Integer a = new Integer(1);
    Integer b = new Integer(2);
    swap(a, b);
    System.out.println("a=" + a + ",b=" + b);
}

输出:

a=2,b=1

可以看到:示例三和示例四得到了正确的结果,很奇妙!

为什么会出现这种情况呢?终于开始得讲IntegerCache缓存了

上面我们提到过,在自动装箱时,实际上是把 Integer x=2; 在编译时变更成了Integer x= new Integer(2);而在这个装箱的过程中实际上是调用的Integer的valueOf(int i)的方法。
看一下源码实现:

public static Integer valueOf(int i) {//返回的类型是Integer
    //自动装箱时,当传入的i值在范围[IntegerCache.low, IntegerCache.high]内,会从缓存中取出对象返回
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    //i超出范围,new一个新的Integer对象返回
    return new Integer(i);
}

接下来看一下缓存的实现:

public final class Integer extends Number implements Comparable<Integer> {
    private static class IntegerCache {//private修饰的静态内部类
        static final int low = -128;//IntegerCache.low被固定为-128
        static final int high;
        static final Integer cache[];//设置Integer类型的缓存数组,可以看到缓存是存放在常量池中(static修饰)

        static {
            // high value may be configured by property
            int h = 127;
            //可以通过设置JVM参数来改变IntegerCache.high,使用integerCacheHighPropValue接收
            //-Djava.lang.Integer.IntegerCache.high=xxx 或 -XX:AutoBoxCacheMax=xxx
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);//IntegerCache.high最小应为127
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);//避免Integer最大值溢出,因为是先从负数开始的,看下面a处的cache初始化代码
                } 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;
            //a    IntegerCache.cache初始化,存储了值在[IntegerCache.low, IntegerCache.high]内的Integer对象
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);
            /*
            	cache[0] = new Integer(-128)
            	cache[1] = new Integer(-127)
            		...
            	cache[128] = new Integer(0)
            	cache[129] = new Integer(1)
            		...
            */

            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;//IntegerCache.high必须大于等于127
        }

        private IntegerCache() {}
    }
}

而自动拆箱时调用的Integer的intValue()方法:

private final int value;

public int intValue() {
    return value;
}

可以看到返回的是基本数据类型。

从上面的源码分析中可以总结几点:

  1. 在代码中进行Integer的对比时尽可能的使用 .equals()来进行对比,看下面的例子:

    Integer x1 = 3;
    Integer x2 = 3;
    Integer x3 = new Integer(3);
    
    Integer x4 = 200;
    Integer x5 = 200;
    
    System.out.println(x1 == x2);  //true
    System.out.println(x4 == x5);  //false
    System.out.println(x1 == x3);  //false
    

    可以看到还是很奇妙,而用equals方法来进行对比则会很保险

  2. Integer对象既可能存放在堆中也可能存放在常量池中,由于IntegerCache.cache是位于常量池的,而除缓存里的外新new的Integer对象都是放在堆中的

  3. 补充一点:

    在方法中定义的局部变量:int a = 3 ; a和其值3都是放在线程栈中

    对于在类中定义的成员属性来说,比如:static int a =3; 此时是在常量池中(有static修饰),而类的非static成员属性如:int a=3; 则在对象所在的堆中。

介绍完了IntegerCache,就可以解答上面示例一到示例四的问题了,用图解的方式来演示:

示例一:

Integer temp = x; temp指向x所指向的对象

field.set(x, y); 将x指向对象的value值改为2,temp和x指向的是同一个对象

field.set(y,temp);

最终:a = 2,b = 2

示例二:

Integer temp = x.intValue(); 该行首先会判断 x.intValue()返回的int值是否在-128~127之间,是则直接从缓存中取相应对象赋给temp,否则才会在堆中分配一块内存。

后面的过程和示例一相同

示例三:

Integer temp = x.intValue(); 由于 x.intValue()返回的int值未在-128~127之间,于是在堆中新分配一块内存,其值是200

field.set(x, y);

field.set(y, temp);

示例四与示例三类似,只不过temp指向的是缓存中对象

正确的答案:

private static void swap(Integer x, Integer y) throws Exception
{
    //请实现
    Integer temp = new Integer(x.intValue());
    Field field = Integer.class.getDeclaredField("value");//获取成员变量value
    field.setAccessible(true);//暴力反射,用于私有成员
    field.set(x,y);
    field.set(y,temp);
}

public static void main(String[] args) {
    Integer a = 1;
    Integer b = 2;
    swap(a, b);
    System.out.println("a=" + a + ",b=" + b);
}

关键在于不能让temp和x指向堆中的同一个对象

过程与示例三类似,这里不再演示

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值