从==和equals的区别,浅谈Java中的引用

7 篇文章 0 订阅

先上两个面试中经常遇见的问题:

1.给出以下代码执行后的打印输出:

public class lll {
    static void change(int i){
        i=10;
    }
    static int changeReturn(int i){
        i = 10;
        return i;
    }
    static class User{
        int age;

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }
    }
    static void change(User u){
        u.setAge(10);
    }
    static User changeReturn(User u){
        User user = new User();
        user.setAge(11);
        return u=user;
    }

    public static void main(String[] args) {
        int i=1;
        User u = new User();
        u.setAge(1);
        change(i);
        change(u);
        System.out.println(i);
        System.out.println(u.getAge());
        System.out.println(changeReturn(i));
        System.out.println(changeReturn(u).getAge());
        System.out.println(u.getAge());
    }
}

2.说出以下代码执行后的输出:

public class lll2 {
    static int print(){
        int i = 1;
        try {
            i=2;
            return i;
        }catch (Exception e){
            i=3;
        }finally {
            i=4;
        }
        return i;
    }
    static class User{
        int age;

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }
    }
    static User printUser(){
        User u = new User();
        try {
            u.setAge(1);
            return u;
        }catch (Exception e){
            u.setAge(2);
        }finally {
            u.setAge(3);
        }
        return u;
    }

    public static void main(String[] args) {
        System.out.println(print());
        System.out.println(printUser().getAge());
    }
}

上面这两种类型的题,乍一看跟标题没什么关系。一个考察java的参数传递,一个考察java的异常。但其实这两个问题的本质,包括标题要讲的==和equals的区别,都与java中的引用息息相关。

一、java中的引用

引用,是java中一个极其普通,但其实又最容易被人忽略的知识点。我们都知道java中没有指针的概念,取而代之的是封装好的引用。引用的本质,是一个指向对象存储内存地址的数据结构。我们知道java分两种数据类型,基本数据类型和引用数据类型,对于基本数据类型,其赋值一般都是在栈中直接分配,对于下面的语句:

int i=10;

 我们在程序中访问变量i,会直接访问10所在的内存空间。而对于下面的语句:

User user = new User();

首先我们要知道,这条语句并不是一个原子操作,其实这里分了三步:

1.新建一个引用user

2.内存中分配空间存储new出来的User对象实例

3.将引用user指向User对象实例的内存空间

不仅如此,java中new对象的操作还可能遇见指令重排序现象,对于虚拟机而言,1,2,3的执行顺序并不能保证,可以存在1,3,2现象,也就是新建引用后,直接指向要分配给User对象的内存空间,然后再执行User对象的实例化操作。这也是单例模式双重锁无法保证线程安全的原因。

java为何要多此一举,设计一个如此复杂的引用指向对象模式?java在设计之初便规定了8中基本数据类型,对于每种数据类型,其大小有固定的上下范围,便于jvm控制。而对于引用类型,由于在运行期会不断变化,很难在编译期去判定一个对象的大小。这也是java中数组分配在堆而不是栈的原因。引用本身只存储对象的内存地址,大小可控。基于此,jvm巧妙的设计出了堆内存区域,将基本数据类型和引用类型存放于栈中,无法预测的数组与对象存放于堆中。所以针对以上两个变量,i,user。我们在访问i的时候,是直接访问分配在栈中的10的内存空间,而访问user,是访问user所指向的,存储在堆中的User对象实例的内存地址。

二、java方法传递参数的方式

上面啰里啰嗦说的这些,可能或多或少每个java程序员都接触过。但是深入理解引用和基本数据类型的区别,非常重要。基于此,我们先解决一个java面试中的老大难问题,也就是题目一,java传递参数的方式。

编程语言传递参数通常有两种方式,值传递和引用传递。java采用的是值传递可能很多人都知道,但是如何理解这个值传递?如何解释change(int i)方法跟changeReturn(int i)方法对i变量的操作?

首先记住一点,所谓的值传递,就是把当前变量的值复制一份,传入到一个方法中。如果当前变量是一个基本变量,那么复制变量i的操作等价于复制10这个数字,然后传入复制出的10。对于引用类型也是一样,复制变量user的操作就是在内存中开辟一块空间,将user所指向的内存的地址复制一遍,然后传入复制后的引用。

所以对于方法change(int i)而言,传入的i,与之前main方法中的i,完全是在两块内存中,我们在change(int i)方法中对i进行操作后,改变的是当前i的值,与之前main方法中i的值毫无关系:

当然实际栈中执行方法的顺序不是这样的,这里只是简单的描述了一下两块内存中不同的i,这里的复制i也就是change(int i)中的参数i,可以看到两个操作完全是在不同的内存区域操作,所以change(int i)方法并不能改变main方法中i的值。

那么对于引用类型,情况有什么不同呢?与i的操作类似,都是要先复制然后操作:

区别非常明显,因为引用中只存储了对象的内存地址,所以change(User u)和main方法中的user,指向的是一个对象,对这两个引用的任何操作最终都是在一块内存区域中完成的,所以change(User u)方法可以改变main方法中user的age值。

回过头来,我们继续分析changeReturn(int i)方法,我们已经知道,由于java的采用值传递的传参方式,change(int i)方法无法改变main方法中的i值,那为何加一个返回值以后,i的值就可以被改变了呢?大家很容易想到,带有返回值后,原先i的值被覆盖掉了。这个覆盖操作是如何完成的?最终的答案,交给编译后的字节码输出解决:

  static void change(int);
    descriptor: (I)V
    flags: ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: bipush        10
         2: istore_0
         3: return
      LineNumberTable:
        line 6: 0
        line 7: 3

  static int changeReturn(int);
    descriptor: (I)I
    flags: ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: bipush        10
         2: istore_0
         3: iload_0
         4: ireturn
      LineNumberTable:
        line 9: 0
        line 10: 3

通过javap命令输出后的字节码指令,我们可以清晰的看到,changeReturn比change方法多出的iload_0这个指令,以及最终的返回指令,return与ireturn。iload_0用来将当前局部变量表的0位置元素入栈,也就是i,ireturn弹出栈顶元素也就是复制后的i,之后main方法再对i进行操作时,拿到的是新的pop出的内存区域,也就是数据1存放的内存区域:

讲到这里大家可以思考一下最后一个方法,changeReturn(User u),其实原理是一致的,changeReturn方法中新new出了一个User,那么直接调用changeReturn方法返回的user引用,会使用新的User,但是即便我们在方法中将参数u的引用更新为新地址,由于这个参数u与main方法中的u引用并不是一个引用,所以main方法中的u引用指向的依旧是以前的User对象:

可以看到最终方法执行结束后,main方法中指向的User对象依旧是之前的User对象。

这里插一句题外话,很多面试者不喜欢一些java基础的题目考察,尤其是笔试题。但通过这道题可以充分考察出一个程序员对java内存模型的熟悉程度。很多题目并不是面经能够给你解决的,只有深入了解其背后的原理,才能真的融会贯通。

三、java中的返回与异常

继续看第二道题,看起来考察的是java中的异常机制,但其实要真的理解发生异常后的返回值,只需要理解两点:

1.java中方法的退出机制。

2.java中引用和基本类型的区别。

首先我们要明白java中对方法的执行,归根结底是jvm对字节码的执行,我们手写的代码,与最终编译后的字节码往往差异很大,所以很多方法执行顺序问题只要我们去看一眼字节码便一目了然。针对第一点,java中方法的退出机制,其实这也是异常体系中一个核心知识点:

 

static int print();
    descriptor: ()I
    flags: ACC_STATIC
    Code:
      stack=1, locals=3, args_size=0//操作数栈深度只有1,局部变量表长度为3,
         0: iconst_1 //
         1: istore_0 //执行i=1,此时i保存在局部变量表下标为0的位置
         2: iconst_2 //
         3: istore_0 //执行i=2
         4: iload_0  //复制i的值入栈
         5: istore_1 //复制并且保存给当前返回值,注意这里是istore_1而不是istore_0
         6: iconst_4 //
         7: istore_0 //i=4
         8: iload_1  //复制当前位置1的值入栈,也就是上面istore_1保存的值2
         9: ireturn  //返回2
        10: astore_1 //保存一个异常对象到局部变量表1,下面是catch逻辑
        11: iconst_3 //
        12: istore_0 // i=3
        13: iconst_4 //
        14: istore_0 // i=4
        15: goto          23
        18: astore_2 //保存一个异常对象局部变量表2,下面执行的是如果catch中无法捕获的异常发生
        19: iconst_4 //
        20: istore_0 //i=4
        21: aload_2  //
        22: athrow   //抛出
        23: iload_0 //此时再加载0位置的值,最后一条istore的值是4
        24: ireturn //返回4
      Exception table:
         from    to  target type
             2     6    10   Class java/lang/Exception
             2     6    18   any
            10    13    18   any

关于字节码如何阅读,这里暂时不展开讲,可以翻看我之前转载的另一篇博文。通过字节码可以清晰的看到,遇见java中的return语句,jvm会先复制一份当前的返回值,然后进行finally中的操作。由于i代表的是int基本数据类型,这里复制操作复制的是真实的数值,所以finally中对i的操作不会影响当前方法的返回值。

如果这里不是很好理解,大家可以继续去想一下返回值是对象的情况,也就是printUser的字节码:

static com.guttv.bms.dao.lll2$User printUser();
    descriptor: ()Lcom/guttv/bms/dao/lll2$User;
    flags: ACC_STATIC
    Code:
      stack=2, locals=3, args_size=0
         0: new           #3                  // class com/guttv/bms/dao/lll2$User
         3: dup
         4: invokespecial #4                  // Method com/guttv/bms/dao/lll2$User."<init>":()V
         7: astore_0
         8: aload_0
         9: iconst_1
        10: invokevirtual #5                  // Method com/guttv/bms/dao/lll2$User.setAge:(I)V
        13: aload_0
        14: astore_1
        15: aload_0
        16: iconst_3
        17: invokevirtual #5                  // Method com/guttv/bms/dao/lll2$User.setAge:(I)V
        20: aload_1
        21: areturn
        22: astore_1
        23: aload_0
        24: iconst_2
        25: invokevirtual #5                  // Method com/guttv/bms/dao/lll2$User.setAge:(I)V
        28: aload_0
        29: iconst_3
        30: invokevirtual #5                  // Method com/guttv/bms/dao/lll2$User.setAge:(I)V
        33: goto          44
        36: astore_2
        37: aload_0
        38: iconst_3
        39: invokevirtual #5                  // Method com/guttv/bms/dao/lll2$User.setAge:(I)V
        42: aload_2
        43: athrow
        44: aload_0
        45: areturn
      Exception table:
         from    to  target type
             8    15    22   Class java/lang/Exception
             8    15    36   any
            22    28    36   any

整体流程与print方法类似,遇见return语句时,jvm同样是复制了当前的返回值,但注意这里的返回值是一个引用,jvm进行复制的时候,只是复制了引用的值,也就是对象的内存地址。复制前后的引用都指向一个对象,所以在finally中对user进行操作,依然会修改复制前的user对象,导致最终返回值的变化。

这里做一个简单总结:

1.java中return语句会触发程序复制当前的返回值

2.引用存储的永远都是对象的内存地址,对引用的复制只是对内存地址的复制,并不能复制一个新的对象产生。

四、java中的==与equals

java中判断两个对象是否相等常用的两个方法就是==与equals,注意hashcode方法由于碰撞冲突的存在,不是一个特别的好的判定方案。equals方法比较简单,他是Object类的一个方法,默认由==实现。java中自带的包装类以及String类等一般都重写了该方法的具体实现。

==比较的是值,如果是基本类型,比较的就是具体的数值。如果是引用类型,比较的是引用的值。由于引用中保存的是对象的内存地址,所以==对两个引用的比较,实际上比较的就是两个引用是否指向同一个对象。

值得注意的是,在重写equals方法的实现时,如果涉及类型比较,要注意getClass方法与instanceof关键字的区别。简单来说,instanceof用来比较类型,getClass用来比较类。假设有如下类关系:

class Father{}
class Son extends Father{}

Son与Girl调用getClass比较返回false,调用instanceof返回true。

五、浅拷贝与深拷贝

涉及对象引用还有一个常见的问题就是浅拷贝与深拷贝,所谓的浅拷贝,其实就是只拷贝了对象引用,拷贝前后的引用还是指向了同一个内存对象。深拷贝与之对应,是重新开辟了新的内存空间,所以如果想要实现深拷贝,就必须要手动执行new对象的过程。java中Object类的clone方法是一个浅拷贝方法,深拷贝需要自己实现。需要深拷贝的地方必须自行实现对象的创建复制过程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值