美文网首页浅尝Java
Java引用传递解析

Java引用传递解析

作者: SYFHEHE | 来源:发表于2018-01-26 22:07 被阅读11次

    0.前言

    因为之前有同学刚好问我一个关于Java引用传递的问题,为什么将ArrayList中拿到的一个元素,赋给一个对象,对象修改了属性,ArrayList中的对应的元素属性也会修改,这就牵扯出Java中一直讨论的一个问题:Java传参时是引用传递还是值传递。 在这篇文章里我们主要会对一下几个问题进行讨论。

    • 1.基本类型和引用类型在内存中的保存
    • 2.变量的基本类型和引用类型的区别
    • 3.引用传递和值传递

    1.基本类型和引用类型在内存中的保存

    Java中数据类型分为两大类,

    • 基本类型:`byte, short, int, long, char, float, double, Boolean,不过,Java字节码中还有额外的两种基础数据类型,分别是reference type与returnAddress type;
    • 对象类型:类,接口和数组。
      相应的,变量也有两种类型:
    • 基本类型:基本类型的变量保存原始值,即它代表的值就是数值本身;
    • 引用类型:引用类型的变量保存引用值,"引用值"指向内存空间的地址,代表了某个对象的引用,而不是对象本身。

    2.变量的基本类型和引用类型的区别

    2.1 基本类型的初始化

    基本数据类型是存储在栈内存中,当在一段代码块中定义一个变量时,java就在栈中为这个变量分配内存空间,当超过变量的作用域后,java会自动释放掉为该变量分配的内存空间,该内存空间可以立刻被另作他用。

    举个例子:

    int a = 3;
    

    首先它会在栈中创建一个变量为a的引用,然后查找有没有字面值为3的地址,没找到,就开辟一个存放3这个字面值的地址,然后将a指向3的地址。

    题外话:不是所有的基本数据类型都存在java栈里,为了反驳观点" Java的基本数据类型都是存储在栈的 ",我们也可以随便举出一个反例,例如:

    int[] array=new int[]{1,2};
    

    由于new了一个对象,所以new int[]{1,2}这个对象时存储在堆中的,也就是说1,2这两个基本数据类型是存储在堆中,这也就很有效的反驳了基本数据类型一定是存储在栈中~~

    2.2 引用类型的初始化

    image.png

    上图表示的是在JVM中,Java对象的访问方式。从上图可以看出,对象的引用变量是存储在栈内存中的。我们通过一个例子来分析下这个过程。

    Object obj = new Object();
    
    • 第一步:new Object() :JVM将在GC堆中开辟一定大小的空间用以存放此Object,此Object是引用数据类型(注意不是reference类型,reference类型保存在Java栈中的本地变量表里)。另外,在JAVA堆中还必须包含能查找到此对象类型数据(如对象类型、父类、实现接口、方法等)的地址信息,这些类型数据存储在方法区中。
    • 第二步: Object obj:JVM将在虚拟机栈中此线程栈开辟出一个slot用以存放obj,注意此处obj为reference type。(slot可以理解为用以存放基础数据类型的线程私有的空间)
    • 第三步:=:reference type 的obj被赋值为先前new Object() 指令所产生的引用数据类型的一个对象。

    分析完不同类型的变量在内存中的储存形式之后,我们就可以来分析下到底什么时候是值传递,什么时候是引用传递。

    3.引用传递和值传递

    • 值传递:
      方法调用时,实际参数把它的值传递给对应的形式参数,函数接收的是原始值的一个copy,此时内存中存在两个相等的基本类型,即实际参数和形式参数,后面方法中的操作都是对形参这个值的修改,不影响实际参数的值。

    • 引用传递:
      也称为传地址。方法调用时,实际参数的引用(地址,而不是参数的值)被传递给方法中相对应的形式参数,函数接收的是原始值的内存地址;在方法执行中,形参和实参内容相同,指向同一块内存地址,方法执行中对引用的操作将会影响到实际对象

    举例:

    public class Demo0 {
      
      private User user = new User("Jack", 1l);
      
      private int num;
      
      private String string ="";
    
      //提供改变自身方法的引用类型
      void fun0(User user){
        user.setName("Lucy");
      }
    
      //基本类型作为参数传递时,是传递值的拷贝,无论你怎么改变这个拷贝,原值是不会改变的
      void fun1(int value) {
        value = 100;
      }
      
      //没有提供改变自身方法的引用类型
      void fun2(String text) {
        text = "windows";
      }
    
      //在Java中对象作为参数传递时,是把对象在内存中的地址拷贝了一份传给了参数。
      public static void main(String[] args) {
        Demo0 demo = new Demo0();
        demo.num = 10;
        demo.string = "Linux";
        
        System.out.println("before name: " + demo.user.getName());
        demo.fun0(demo.user);
        System.out.println("after name: " + demo.user.getName());
    
        System.out.println("before num: " + demo.num);
        demo.fun1(demo.num);
        System.out.println("after num: " + demo.num);
        
        System.out.println("before string: " + demo.string);
        demo.fun2(demo.string);
        System.out.println("after string: " + demo.string);
        
      }
    
    }
    
    
    • 函数fun0运行结果:
    before name: Jack
    after name: Lucy
    

    user的name属性被修改,原因是:fun0调用时,函数接收的是原始值的内存地址;方法执行setName操作,修改了堆中改内存地址上的User对象的属性。通过这个例子我们可以知道:引用类型传递的是对象的引用,形参和实参指向同一个内存地址(同一个对象),所以对参数的修改会影响到实际的对象。

    • 函数fun1运行结果:
    before num: 10
    after num: 10
    

    很显然输出的 是10,10。传递的是值得一份拷贝,这份拷贝与原来的值没什么关系。基本数据类型传值,对形参的修改不会影响实参。

    • 函fun2运行结果:
    before string: Linux
    after string: Linux
    

    这就很有意思了,为什么String是引用类型,为什么不会修改其值呢?原因是:String,以及Integer、Double等几个基本类型包装类,它们都是immutable类型,因为没有提供自身修改的函数,每次操作都是新生成一个对象,所以要特殊对待,可以认为是和基本数据类型相似,传值操作。

    现在来看一开始提出的那个问题:为什么从ArrayList中get第一个元素,赋给一个对象,对象修改了属性,ArrayList中的元素属性也会修改?

    public class Demo2 {
      public static void main(String[] args) {
    
        List<User> list = new ArrayList<User>();
        User user1 = new User("name1", 1111l);
        User user2 = new User("name2", 2222l);
        User user3 = new User("name3", 3333l);
        list.add(user1);
        list.add(user2);
        list.add(user3);
        
        System.out.println("--------------Before---------------");
        for (User user : list) {
          System.out.println("name = " + user.getName());
        }
        
        User userTemp = list.get(0);
        userTemp.setName("xxxxxx");
        
        System.out.println("--------------After---------------");
        for (User user : list) {
          System.out.println("name = " + user.getName());
        }
        
      }
    }
    

    输出:

    --------------Before---------------
    name = name1
    name = name2
    name = name3
    --------------After---------------
    name = xxxxxx
    name = name2
    name = name3
    

    原因很简单:userTemp 和list的第一个对象都指向同一块内存地址,方法执行中对引用的操作将会影响到实际对象

    4.总结

    java中方法参数传递方式是按值传递。

    • 如果参数是基本类型,传递的是基本类型的字面量值的拷贝。
    • 如果参数是引用类型,传递的是该参量所引用的对象在堆中地址值的拷贝
    • String,以及Integer、Double等几个基本类型包装类,它们都是immutable类型,因为没有提供自身修改的函数,每次操作都是新生成一个对象,所以要特殊对待,可以认为是和基本数据类型相似,传值操作。

    相关文章

      网友评论

        本文标题:Java引用传递解析

        本文链接:https://www.haomeiwen.com/subject/rsnpaxtx.html