从一道面试题了解Interge的原理实现

    技术2022-07-17  90

    目录

    举例

    值传递和引用传递

    源码

    拆箱和装箱

    IntegerCache


    主要考察你对Interge里面的缓存的实现机制,因为这里面很容易遇到一些坑。

    举例

    public static void main(String[] args) throws InterruptedException { Integer a=1,b=2; System.out.println("before=a"+a+"b="+b); swap(a,b); System.out.println("after=a"+a+"b="+b); } public static void swap(Integer i1,Integer i2){ Integer temp; temp=i1; i1=i2; i2=temp; } 执行结果: before=a1b=2 after=a1b=2

    我们发现我们通过这个方式去交换a和b值,没有成功,但是我们这种思路又是没有问题的,是正确的。

    这个就是我们这里 swap(Integer a,Integer b)传过来的这个值进行交换,但是他不会对原来的会有影响。

    那么我们可能会有一个疑问了。为什么我们传进来是一个引用对象的值,为什么改变这个值却不会对原来的值造成影响呢?

    那么这里就涉及到值传递和引用传递,这是java最基本的知识点。

    值传递和引用传递

    实际上呢,在java里面,他只有一种参数传递机制,是按值传递。至于为什么会有值传递和引用传递的说法,实际上这里取决于变量的类型,因为我们知道变量类型分为引用类型和基本类型。那么当我们把这两种的类型传递给一个方法的时候,那么处理两种类型的方式是相同的,对于我们jvm底层的处理方式是相同的。都是按照值去传递的,只是说,根据这两种类型的不同;那么我传递的是基本类型,那么函数接收的是原始值的一个副本,因此如果函数改变了这个值,那么函数改变的是副本的值,原始值不变。这是第一个。

    那么如果传递的是引用类型,那么函数接收的是原始引用的内存地址,而不是副本,因此这个函数去修改这个参数的话,他会修改这个值的一个地址去影响到我们原本传过来的值。

    但是也许还有疑问:

    为什么要取这么去设计?

    因为我们知道对象类型是存储在对堆里面的,就是对象引用指向的一个地址存在堆内存里面,引用地址是存在对栈里面的。

    那么通过对象类型的一个地址存在堆里面主要是为了提升,我们的一个速度。因为基本类型和对象类型,他们俩的内存副本的拷贝速度是不一样的。第二个是:对象类型它本身占用的内存空间比较大。如果从新去复制这样对象的一个方法,他会比较浪费内存。

    所以这是值传递和引用传递的区别。

    但是这里面还有第二个问题;

    就是我们这里传递的是一个Interge。他是一个引用类型,封装类型。为什么他不能改变?

    因为在java里面这种封装类型的传递,都是传递他的副本值。这是一个规定。

    那么你一定 要去探究他的原理的话,你要去看他的源码

    源码

    在源码中我们可以发现,Interge里面的他的value值他是一个final的一个东西。

    /** * The value of the {@code Integer}. * * @serial */ private final int value;

    并且这个value还没有对外的一个设置,因为他是final的嘛。他是没办法改变。所以我们在传递过来的时候,他是无法改变他的值,所以我们只能改变他的副本。所以他只能拷贝他的一个副本来传递。

    如果我们要来画图的话,我们先来画一个栈空间和堆空间

    原本我们定义了一个a和b,这两个对象,这两个封装类型。他们仍然是引用类型,所以他们都在堆内存中存在一个地址。比如说a=1;b=2。

    那么我们在刚刚的那个传递过程中,他是这样的,比如我们在另一个地方又定义了一个i1 ,i2。那么实际上我我们传递的是这个对象的副本。那么这个副本他就意味着他去在栈里面重现分配一个局部变量的定义i1,i2

    这是他在们栈针中的一个概念。那么i1 i2他原本的值,应该是指向之前的。那么此时我么去交换,没有问题,i1和i2的值进行交换了。但是他改变的是我们当前的副本的值。

    对于a和b来说他们是没有任何影响的。

    所以这一点验证了,java中的封装类型他传递的是副本不是原始值。这是一个固定的约定。

    了解这个之后,在老看,如果这种方法没有办法改变的话,那么还有什么办法去实现呢?

    我么看到源码中定义的一个value属性,他是用来保存我们a.b这两的值的一个定义。

    那么我们可以通过什么方式来改变一个私有的fianl的一个整型的成员变量呢?

    那么唯一的方法就是反射:

    因为反射可以改变我们原本定义的一些值,那么怎么实现呢

    public static void main(String[] args) throws InterruptedException, NoSuchFieldException, IllegalAccessException { Integer a=1,b=2; System.out.println("before a="+a+"b="+b); swap(a,b); System.out.println("after a="+a+"b="+b); } public static void swap(Integer i1,Integer i2) throws NoSuchFieldException, IllegalAccessException { //通过一个反射去拿到Integer里面的一个value的属性 Field fiel=Integer.class.getDeclaredField("value"); //拿到这属性怎么办呢?然后给这个属性去改变值 int temp=i1.intValue(); fiel.set(i1,i2.intValue()); fiel.set(i2,temp); }

    我们发现,其实交换思想还是一样的,只不过换了个方式,但是这里我们知道私有成员和fianl成员是不允许我们去访问的,所以运行他会报错。

    Class com.syz.designMode.ThreadDemo can not access a member of class java.lang.Integer with modifiers "private final"

    那么怎么去处理呢?

    Field的里面有一个field.setAccessible(true);

    我们可以通过这种方式绕过他的一个检查。

    所以这也是我们面试中经常被问到了,我们怎么去改变私有的成员变量的一个值,所以我们可以通过这个反射的机制去实现。

    但是上面这个还会有一个问题:我么看运行结果:

    before a=1b=2 after a=2b=2 Process finished with exit code 0

    我们发现a变过来了,但是b没有变过来。这是第一个问题

    那么第二个问题是:通过这个setAccessible(true);他是怎么绕过检查的?

    那么我们先去分析setAccessible的源码:

    public void setAccessible(boolean flag) throws SecurityException { SecurityManager sm = System.getSecurityManager(); if (sm != null) sm.checkPermission(ACCESS_PERMISSION); setAccessible0(this, flag); }

    我们看到他调用setAccessible0这个方法。而且传递了一个flag参数。那么这个flag是干嘛呢?

    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; }

    我们可以看到他是去设计一个obj.override = flag;这个操作。由此可以看到obi就是我们AccessibleObject对象,而他里面有一个成员属性override,那么他设置了这样一个属性,那么他的目的是干嘛的呢?

    我们去看当我们field.set(i1,i2.intValue());这个值的时候他会去判断这个属性。

    @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); }

    如果他设置为true的话,他就不会走这个检查逻辑。然后我们也能看到Field他实际上集成了AccessibleObject这个对象,所以他能够访问他的成员变量override。所以他通过这个绕过了安全检查。这就是setAccessible的核心原理。

    那么第二个问题,他为什么改变了第一个值,第二个值没有改变。也就是这个反射生效了,但是为什么他没有全部的去生效呢?

    这个就涉及到java中最核心的原理。拆箱和装箱这样一个概念。

    拆箱和装箱

    那么首先了解什么是拆箱和装箱。

    首先:我们定义了一个Integer a=1.而这个1他实际是基本类型,也就是这int他int类型。这又定义了一个Integer类型。为什么编译的时候他不会去报错。那么这里就涉及到了装箱操作。

    那么这个装箱操作他做了什么事情呢?

    如果想去看的话,可以去看他的字节码:可以通过命令:javap -c Swap.class

    public class com.syz.designMode.Swap { public com.syz.designMode.Swap(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]) throws java.lang.InterruptedException, java.lang.NoSuchFieldException, java.lang.IllegalAccessException; Code: 0: iconst_1 1: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 4: astore_1 5: iconst_2 6: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 9: astore_2 10: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 13: new #4 // class java/lang/StringBuilder 16: dup 17: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V 20: ldc #6 // String before a= 22: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 25: aload_1 26: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder; 29: ldc #9 // String b= 31: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 34: aload_2 35: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder; 38: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 41: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 44: aload_1 45: aload_2 46: invokestatic #12 // Method swap:(Ljava/lang/Integer;Ljava/lang/Integer;)V 49: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream; 52: new #4 // class java/lang/StringBuilder 55: dup 56: invokespecial #5 // Method java/lang/StringBuilder."<init>":()V 59: ldc #13 // String after a= 61: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 64: aload_1 65: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder; 68: ldc #9 // String b= 70: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 73: aload_2 74: invokevirtual #8 // Method java/lang/StringBuilder.append:(Ljava/lang/Object;)Ljava/lang/StringBuilder; 77: invokevirtual #10 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 80: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 83: return

    找到我们main方法里面的变量对应的字节码:

    我们发现我们定义的一个常量 iconst_1 这是他的字节码指令。但我们发现他去做:

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

    的时候,他会有一个Integer.valueOf,他实际上是int的value去做一个装箱。也就是意味着:

    Integer a=1 就相当于Integer a=Integer.valueOf(1); 他们是等价的。这个就是他的装箱操作。

    了解了这个之后,我们看valueOf他里面做了什么事情?

    IntegerCache

    我们去Integer.java类中去搜索一下这个方法:

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

    这里面他去判断了i >= IntegerCache.low && i <= IntegerCache.high

    这个操作。然后返回IntegerCache.cache[i + (-IntegerCache.low)。

    我们通过Cache这个名字知道他是一个缓存。意思是如果i在上面这范围内的话,他直接从缓存里面去拿值。

    那么我们看看缓存做了什么事情?

    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() {}

    这个缓存定义了low = -128 。int h = 127;

    所以就是在-128和127范围之内他会去做一件什么事呢?

    从源码看他会去初始化缓存:cache = new Integer[(high - low) + 1];

    然后把这个范围的值放入缓存里。

    这就意味着他在运行的时候,-128到127这个范围的值就应经被占用了。

    那么这么设计的好处是,因为他提升了效率。因为Integer我们用的最多的就是在这个范围之内的数据。所以他可以很好的去减少内存的分配,去提升他的一个效率。

    那么怎么去验证Integer的缓存的概念呢?如案例:

    Integer i1 = 1; Integer i2 = 1; System.out.println(i1 == i2);

    按照我们对于java里面的原理,就是两个对象去直接== 去相等的话,这是不可取的。因为他比较的是两个内存地址的值。所以对于对象去进行==对比的话,他们结果是不相等的。所以他这个地方应该打印的是false.。但是呢;我们上面说过Integer里面他去做了一个缓存的概念。所以发现运行的结果是:true。

    那么如果我们超过了他的范围之后呢?

    加入是i1,i2=129.那么他就会导致一个问题。他会走我们源码中的另一个方法:

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

    这里面的return new Integer(i);这个方法。

    凡事带new的操作,他都会去分配一个内存地址。所以这时候i1.i2他们两个的内存地址是不一样的。所以他的比较是不相等的。

    所以这就缓存的概念。

    那么为什么要了解缓存的概念和装箱拆箱的概念呢?

    因为这个就是产生之前数据交换为什么i2的值没有变的结果了。

    那么我们在来回顾那段代码:

    int temp = i1.intValue(); 这段代码是没有问题。他就是去拿到i1的整数值。

    field.set(i1, i2.intValue());但是这里他就会有问题了。

    因为这个set方法,他的两个参数值都是Object类型的

    public void set(Object obj, Object value){}

    我们这里传的i1是没问题的,他是Object类型;但是i2.intValue()这个就有问题了。我们给他传的是一个int类型的值了。所以这个他就涉及到装箱操作。引入你int类型怎能让我们Object去接受呢?所以他就行了一个自装箱操作。

    所以实际上i1的值的变化应该是 i1= Integer.valueOf(i2.intValue()).intValue()

    那么:;这地方也会涉及到装箱,那么i2=Integer.valueOf(temp).intValue()

    那么这个地方的问题就来了;valueOf()操作他涉及到了缓存;那么Integer.valueOf()他是根据temp的值去拿到下标的位置。而这个下标的位置i1的值变成了2.所以这里temp的值也变成了2.

    那么这里应该又有一个疑问:temp的值怎么会变成2呢?

    System.out.println(Integer.valueOf(temp)); 我们去输出一下temp。

    因为我们刚介绍到.valueOf()他是从缓存的下标去拿值。我们会发现,temp他本来的值应该是1.拿到的是基本的值。但是现在他经历了装箱,又变成了从缓存内存地址去拿,就是从下标位置去拿。所以运行这个输出值:发现temp=2。 就是:temp原本是数值数据1,但是现在进行装箱,通过valueOf(1) 就是取缓存中1下边对应的值了。这个下标1对应的值为2,所以Integer.valueof(temp)=2所以理解temp的值变成了2.

    所以现在temp=2.有设置给了i2(field.set(i2, temp)) (temp=Integer.valueOf(temp).intValue()=》temp=Integer.valueOf(1).intValue() 缓存下表1的值)。所以理所当然a等于2,b也等于2,他们是去替换,只是替换了结果。但是由于缓存的原因导致了最终出现了这样的结果。

    通过代码理解:

    public static void main(String[] args) throws InterruptedException, NoSuchFieldException, IllegalAccessException { Integer a=1,b=2; System.out.println("Integer获取缓存中的值 start"); System.out.println("000="+Integer.valueOf(0)); System.out.println("111="+Integer.valueOf(1)); System.out.println("222="+Integer.valueOf(2)); System.out.println("333="+Integer.valueOf(3)); System.out.println("444="+Integer.valueOf(4)); System.out.println("555="+Integer.valueOf(5)); System.out.println("Integer获取缓存中的值 end"); System.out.println("before a="+a+"b="+b); swap(a,b); System.out.println("after a="+a+"b="+b); } public static void swap(Integer i1, Integer i2) throws NoSuchFieldException, IllegalAccessException { //通过一个反射去拿到Integer里面的一个value的属性 Field field = Integer.class.getDeclaredField("value"); //拿到这属性怎么办呢?然后给这个属性去改变值 field.setAccessible(true); int temp = i1.intValue(); System.out.println("temp0="+temp); System.out.println("temp1="+Integer.valueOf(temp)); field.set(i1, i2.intValue()); // Integer.valueOf(i2.intValue()).intValue() System.out.println("temp2="+temp); System.out.println("temp3="+Integer.valueOf(temp)); field.set(i2, temp); } 输出值: Integer获取缓存中的值 start 000=0 111=1 222=2 333=3 444=4 555=5 Integer获取缓存中的值 end before a=1b=2 temp0=1 temp1=1 temp2=1 temp3=2 after a=2b=2

    那么我们该怎样改变这个值,避免他的装箱还拆箱操作。

    避免的方法比如:

    方法一:改变temp的值:Integer temp=new Integer(i1.intValue());

    这下我们temp的内存地址跟我们的i1就完全区分开。这时候我们就不因为temp的值产生影响了

    public static void swap(Integer i1, Integer i2) throws NoSuchFieldException, IllegalAccessException { //通过一个反射去拿到Integer里面的一个value的属性 Field field = Integer.class.getDeclaredField("value"); //拿到这属性怎么办呢?然后给这个属性去改变值 field.setAccessible(true); Integer temp=new Integer(i1.intValue()); // int temp = i1.intValue(); field.set(i1, i2.intValue()); field.set(i2, temp); System.out.println(Integer.valueOf(temp)); }

    这时候b的值就会变化了。

    因为这里我们走了内存空间,并没有走i1.intValue() 去走相同的内存地址。因为我们在这个位置Integer.valueOf(i2.intValue()).intValue()把i1的内存地址给改成2,所以我们再去拿相同地址i1的时候变成了2。

    方法二:set改成setInt

    field.setInt(i1, i2.intValue()); field.setInt(i2, temp);

    去避免装箱操作。

    Processed: 0.097, SQL: 10