基于简单题目讨论JDK的Integer

讨论一道jdk里Integer的基础题

题目

public class IntegerTest {

    public static void main(String[] args) {
        Integer a = 1;
        Integer b = 2;
        System.out.println("before a = " + a + " b = " + b);
        exchange(a, b);
        System.out.println("after a = " + a + " b = " + b);
    }
    
    private static void exchange(Integer num1, Integer num2) {
    
    }
}

大概问题是:定义了两个Integer变量,通过exchange方法交换两个变量的数据?

其实这个问题看似真的挺简单的,我相信很多人都会有那么灵光一闪,交换数据不就用一个临时变量来保存,然后进行互换就可以了吗?
所以引发了第一种解法:

    private static void exchange(Integer num1, Integer num2) {
        Integer temp = num1;
        num1 = num2;
        num2 = temp;
    
    }

这种做法应该很多人都有遇到和见过,特别是在冒泡排序当中的数据互换也用到了这种临时变量互换数据的方法。
执行一下便知道结果是错的,为什么呢?

首先了解一下java的传值方式,有两种:
1、按值传递:顾名思义就是把参数传递给方法的时候,方法接收的变量实际上是变量的副本值,也就是说并不是把变量本身给你,而是先复制一份,把复制那份给你。
说到这里我想就能知道上面的第一种解法为什么会错了?因为你修改的和交换的数据只是一个副本值,并不是变量本身。

2、按引用传递:传递的是指向值的地址的指针。

而这第一种解法里,就是按值传递的。

有些人就会疑惑了?Integer不是对象吗?对象不是应该传递指针吗?为什么会是按值传递呢?这就可以去看一下Integer的源码了,我们可以看到是用final修饰的,所以final修饰的实例化对象是不可以修改的。

既然这里是按值传递,那是怎样一个过程呢?

基于简单题目讨论JDK的Integer_第1张图片
简单内存图

这又涉及到jvm的一些概念,我也只是最基础的了解而已,这涉及到内存中的栈和堆。栈是放变量本身,而堆是存储对象的值。
看上图就知道,实际上通过第一种解法是在栈当中复制了出来,所以num1和num2是独立的存在,已经并不是代表a和b了,所有的操作也是对复制出来的变量进行操作的,所以exchange后是num1和num2进行了交换。

既然想对Integer对象进行变量的交换,那我们首先要知道Integer内的变量是如何赋值的。我们进去看一下源码:

    private final int value;

从这里我们可以看出,Integer里面有个属性是vlaue,并且是用final修饰的,有属性的话就会让人联想到封装,如果有getter和setter方法的时候,我们是不是就可以直接通过方法去赋值了,而现实总是残酷的,并找不到setter方法。

不过有属性,我们就可以稍微深入的联想一下,是否可以用反射去获取改属性的值,然后去修改了它呢,这样了不就达到我们要的效果了吗?

那第二种解法我们便通过反射来实现吧:

    try {
            Field field = Integer.class.getDeclaredField("value");
            int temp = num1.intValue();
            field.set(num1, num2);
            field.set(num2, temp);
        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        }

首先可以告诉大家这样做肯定是会报错的,至于为什么呢?稍后给大家解释,现在先给大家解释一下反射里面用到的方法。
大家肯定有用过getField方法,那跟getDeclaredField有什么区别呢?这里给大家解释一下:
getField:这个是获取所有public修饰的字段的方法,记住,是只可以获取public修饰的字段的。
getDeclaredField:这个是获取一个类声明的所有字段的方法,不管是什么修饰的都是可以获取的。
set:这个方法可能比较简单,有两个参数,field.set(Object obj, Object obj2),这个方法作用就是向obj这个对象的Filed设置新值,新值就是第二个参数obj2,要切是filed去调用的方法,所以是对第一个参数对象的Field设置新值。

现在说一下上面为什么会报错,反射也不是说一用就无所不能的,关键就在于Integer的value变量的修饰符,回想上面说的,value是private final去修饰的,private修饰代表私有,final就是不可变,说白了就是没有权限。
报异常的关键错误如下:

    can not access a member of class java.lang.Integer with modifiers "private final"

从这句话可以看出,反射是不能访问私有的成员属性的。
那既然不能访问,我们要做的就是要做到可以访问,那有没有办法获取到访问权限呢?反射是可以做到的。
加上一句代码就可以:

    field.setAccessible(true);

field.setAccessible(true):这句代码其实就只有一个作用,那就是可以让我们在用反射时访问私有变量。

那加上去以后我们再执行一次看一下,会是我们想要的结果吗?

before a = 1 b = 2
after a = 2 b = 2

终于没有报错了,也终于完成一小步的迈进,但是为什么a和b都是等于2呢?

这就可以研究一下Integer的源码了,里面有一个静态内部类,叫做IntegerCache,这个类是起什么作用的呢?
我想有人遇到过一道题:

    public static void main(String[] args) {
        Integer a = 1;
        Integer b = 1;
        System.out.println(a == b); //true
        Integer c = 128;
        Integer d = 128;
        System.out.println(c == d); //false
    }

为什么这会一个true,一个false呢?是不是感觉真的是难以理解,都不认识java了。这里就是跟IntegerCache有关了,我们来看看源码:

    
    private static class 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;
        }

        private IntegerCache() {}
    }

我们可以看出IntegerCache里有一个cache数组,并且这个内部类是在加载的时候就执行了,而且cache的最小值是-128,最大值是127.
我们来分析一下这句代码 **Integer a = 1 **,类型是Integer封装类型,而值却是字面量1,那为什么可以赋值不会报错呢?这就牵涉到java的一个知识点了
java中的装箱和拆箱
那Integer中的装箱是怎么进行操作的呢?我们来看一下:

public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);
}

Integer在装箱的时候执行的就是这个方法,看到关键点没有,在IntegerCache的范围内,就是从cache当中拿的,而在IntegerCache之外,就是直接new出来的。那能够明白上面的为什么一个是true一个是false了吗?
a和b都是1的时候,是在cache之内,所以直接从cache里面拿,那对应的都是同一个对象。而128是在cache之外,都是new出来的,既然是直接new出来的,值是一样的,但是保存在内存中的地址是不一样的,而变量作为引用,自然保存的是指向内存当中的地址,地址不一样自然是为false。

回到我们的第二种解法:
为什么会a和b都是2呢?首先可以确定1和2都是从IntegerCache当中拿的是吧,
在IntegerCache中打断点就可以看到cache的数组,复制出来便是这样的:
[-128,-127,..........,-126,127]
那是怎么从cache里面拿值的呢?看回上面的装箱代码,就可以看到是通过
IntegerCache.cache[i + (-IntegerCache.low)]这句代码来拿值的。
也就是1对应的是IntegerCache.cache[129],而2对应的是IntegerCache.cache[130]。
看回这句代码

field.set(num1, num2);

这个方法在上面已经解释过了,是将第一个参数对象的Filed设置新值,那num1是1,而num2是2,也就是要将num1设置为2,而num1在IntegerCache当中是IntegerCache.cache[129],也就是这一句话将IntegerCache.cache[129]从1改为了2。
然后我们看第二句代码

field.set(num2, temp);

这一句就是将num2的值改为num1的值,num1是IntegerCache.cache[129],而IntegerCache.cache[129]在我们执行第一句的时候就已经改为2了,所以为num2赋值的时候就是2了。
所以得出的结果是a和b都是2。
分析了那么久,其实主要就是因为装箱操作而导致的,因为从cache当中拿,会直接修改了cache当中的值。
解决办法就是我们有没有办法不让它进行装箱拆箱操作呢?
1、第一个方法倒是挺简单的,从Integer的装箱代码就可以看出,如果不在范围内就直接new Integer出来,那第一个方法就是我们直接对临时变量temp进行new Integer,这样就不会进行装箱操作了。

private static void exchange(Integer num1, Integer num2) {
    try {
        Field field = Integer.class.getDeclaredField("value");
        field.setAccessible(true);
        int temp = num1.intValue();
        field.set(num1, num2);
        field.set(num2, new Integer(temp)); //new,不进行装箱
    } catch (Exception e) {
        // TODO: handle exception
        e.printStackTrace();
    }
    
}

自己new Integer防止装箱。

2、还是回归到反射,反射还是可以解决这个装箱拆箱操作的。
进入Field里,ctrl + 0可以看到有set方法和setInt方法,我们去看一下set方法上面的注释

Sets the field represented by this {@code Field} object on the
 *specified object argument to the specified new value. The new
  value is automatically unwrapped if the underlying field has a
 primitive type.

这里可以看出来是对某个对象的field进行设置一个new value(即新值),如果这个新值是一个原始类型即基础数据类型,那就会自动解包。
所以这里可以使用Field.setInt()这个方法,它有两个参数,第一个是object类型,第二个是int类型,因此它传进去的实际上是字面量,并不会进行解包操作。

private static void exchange(Integer num1, Integer num2) {
    try {
        Field field = Integer.class.getDeclaredField("value");
        field.setAccessible(true);
        int temp = num1.intValue();
        field.set(num1, num2);
        field.setInt(num2, temp);//关键代码
    } catch (Exception e) {
        // TODO: handle exception
        e.printStackTrace();
    }
    
}

这一道涉及到的知识点也就那么多,我也只能讲到这里,深入下去我也不会。

你可能感兴趣的:(基于简单题目讨论JDK的Integer)