您的位置 首页 java

为什么说函数只存在值传递 & 引用类型是什么

1. 例子一

我们先来看几个例子. 先来 Java 的吧. Java最假了. 一般人都认为是引用传递(基本类型值传递). 所以我们又拿C++做例子.

写 a , b 是为了好区分开. 其实 写俩a都行. 不好书面表达

 public class Demo {

    public  static   void  main(String[] args) {
        A a = new A();
        System.out.println("初始化 : " + a.name);
        a.name = "main";
        System.out.println("init函数调用前 : " + a.name);
        // change 函数传递
        init(a);
        // 结果 ?
        System.out.println("init函数调用后 : "+a.name);
    }

    static void init(A b) {
        // a被初始化了..
        b = new A();
    }

     private  static class A {
        String name;
    }
}  

输出 :

 初始化 : null
init函数调用前 : main
init函数调用后 : main  

结果是啥. 如果真的是引用传递. 那么为啥a.name为啥不是空呢.

我用C++ 给大家写一下.

“`c++

include

include

using namespace std;

class A { public: string name; };

void init(A *b) { cout << “未改变的地址 : ” << b << endl; *b = A(); cout << “改变后的地址 : ” << b << endl; } int main(int argc, char const *argv[]) { A *a = new A(); cout << “初始化 : ” << a->name << endl; a->name = “main”; cout << “init函数调用前 : ” << a->name << endl; init(a); cout << “init函数调用后 : ” << a->name << endl; return 0; } “`

输出

 初始化 : 
init函数调用前 : main
未改变的地址 : 0xe699d0
改变后的地址 : 0xe699d0
init函数调用后 :   

显然C++和Java的表现是不一样的. 为什么C++ 赋值可以改变值. 而Java却不是呢.

我们先解释C++的做法.

我们 init(A *b) 做了啥

首先是我们定义了一个变量 A *a=new A(“main”) , 我没有写 构造函数 , 假设的.

此时传递给函数 init(A *b) , 此时 b=a ,a= 0xe699d0 . 所以呢 b= 0xe699d0 .

后面我们的操作就是基于这个 b 的. 此时我们将 *b =A(); , 是不是将 0xe699d0指针 指向了 A() 对象呢(其实就是块内存,指向的是内存的首地址) . 此时是不是修改了 0xe699d0指针 的指向呢. 那么a也等于 0xe699d0 .所以a的值也被修改了 .

然后我们看看Java的做法.

 static void init(A b) {
    System.out.println("未改变的地址: 0x"+Integer.toHexString(a. hashcode ()));
    // a被初始化了..
    b = new A();
    System.out.println("改变后的地址: 0x"+Integer.toHexString(a.hashCode()));
}  

我们打印一下 hashcode. 因为Java的hashcode其实就是确定一个对象的唯一标识, 如果你想深入了解hashcode的话介意看看 JVM 的源码,C++和Java对象的映射关系 (如果是对象地址的话,那么内存回收会改变大量的内存地址,难道还是地址吗,我们这里不考虑这个.只要知道这个是Java对象的唯一值类似于hash码 ).

输出结果:

 初始化 : null
init函数调用前 : main
未改变的地址: 0x6e8cf4c6
改变后的地址: 0x12edcd21
init函数调用后 : main  

此时我们发现地址发生了改变. 其实理解了上面C++那部分聪明的就明白了.

由于Java引用类型传递如果是Hotspot虚拟机则实现的就是简单的指针传递. 这个变量指向Java对象的数据区域.

由于一开始 a=0x6e8cf4c6(Java引用对象) , 然后函数赋值 b=a , 此时 b= 0x6e8cf4c6.

关键点在于new A()地方; 执行的是,实例化一个A对象,在栈顶开辟一块空间, 将A对象保存在栈顶中, 此时栈顶值=0x12edcd21, 然后将栈顶值存入到变量b中. 此时b=0x12edcd21 . 那么改变原来0x6e8cf4c6指向的内容了吗,并没有.

我们再看看 javac 编译后的结果 :

 static void init(com.jvm.reference.Demo$A);
descriptor: (Lcom/jvm/reference/Demo$A;)V
flags: ACC_STATIC
Code:
// 栈的深度需要3 , 变量表需要一个,参数一个
  stack=3, locals=1, args_size=1
      // 实例化一个对象
     0: new           #2                  // class com/jvm/reference/Demo$A
     3: dup
     4: aconst_null
     // 调用 构造方法 
     5: invokespecial #3                  // Method com/jvm/reference/Demo$A."<init>":(Lcom/jvm/reference/Demo$1;)V
     // 将栈顶值存入变量0中
     8: astore_0
     // 返回
     9: return  

准确点说Java的做法是 : 如下操作.

 void init(A *b)
{
    cout << "未改变的地址 : " << b << endl;
    A a = A();
    b = &a
    cout << "改变后的地址 : " << b << endl;
}  

2. 例子二

​ 其实你理解上面这个例子. 你就明白了为啥值传递了. 我们继续拿C++说话. Java查看地址不方便.

 #include <iostream>

using namespace std;

void swap(int *a, int *b)
{
    int *temp = a;
    a = b;
    b = temp;
    cout << "a : " << *a << " , b : " << *b << endl;
}

int main(int argc, char const *argv[])
{
    int x = 1;
    int y = 2;

    cout << "x : " << x << " , y : " << y << endl;
    // swap 方法.
    swap(&x, &y);

    cout << "x : " << x << " , y : " << y << endl;
    /* code */    return 0;
}  

输出

 x : 1 , y : 2
a : 2 , b : 1
x : 1 , y : 2  

我们发现为啥函数内部 . a 和 b 成功交换了地址 . 所以 a 和 b的值就互换了 .

但是为啥呢.

我们再次打印一下地址

 cout << "&x  : " << &x << " , &y : " << &y << endl;
// 输出 : 
&x : 0x61fefc , &y : 0x61fef8  

&x = 0x61fefc , &y=0x61fef8 ,

执行swap函数. 此时 a=0x61fefc , b=0x61fef8 ,

然后经过一番操作, 此时 a=0x61fef8 , b=0x61fefc . 然后输出 *a=2, *b=1, 所以成功了.

但是为啥 x 和 y 没变呢. 是不是发现 &x 和 &y依旧着原来的地址呢.

正确的swap操作 , 必须修改指针指向的值.

 void swap(int *a, int *b)
{
    int temp = *a;
    *a = *b;
    *b = temp;
}  

3. Java的引用类型

​ 创建对象自然是为了后续使用该对象,我们的Java程序会通过栈上的reference数据来操作堆上的具体对象。由于 reference类型 在《 Java虚拟机规范 》里面只规定了它是一个 指向对象的引用 ,并没有定义这个引用应该通过什么方式去定位、访问到堆中对象的具体位置,所以对象访问方式也是由 虚拟机 实现而定的,主流的访问方式主要有使用句柄和直接指针两种:

  • 如果使用 句柄 访问的话,Java堆中将可能会划分出一块内存来作为句柄池,reference中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息,其结构如图2-2所示。
  • 如果使用直接指针访问的话,Java堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息 ,reference中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销,如图2-3所示。

本文第三节引用自 <<深入理解 Java虚拟机 >> .

​ 这两种对象访问方式各有优势 :

使用句柄来访问的最大好处就是reference中存储的是稳定句柄地址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而reference本身不需要被修改。

使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销,由于对象访问在Java中非常频繁,因此这类开销积少成多也是一项极为可观的执行成本,就本书讨论的主要虚拟机HotSpot而言,它主要使用第二种方式进行对象访问(有例外情况,如果使用了Shenandoah收集器的话也会有一次额外的转发,具体可参见第3章),但从整个 软件开发 的范围来看,在各种语言、框架中使用句柄来访问的情况也十分常见。

所以句柄访问方便管理, 直接指针访问效率高, 但是不方便管理.

文章来源:智云一二三科技

文章标题:为什么说函数只存在值传递 & 引用类型是什么

文章地址:https://www.zhihuclub.com/190020.shtml

关于作者: 智云科技

热门文章

发表回复

您的电子邮箱地址不会被公开。

网站地图