方法中的参数传递

基本类型和引用类型

  • Java中的数据类型分为两种为基本类型和引用类型。

    • 基本类型的变量保存原始值,所以变量就是数据本身。
      • 常见的基本类型:byte,short,int,long,char,float,double,Boolean。
    • 引用类型的变量保存引用值,所谓的引用值就是对象所在内存空间的“首地址值”,通过对这个引用值来操作对象。
      • 常见的引用类型:类类型,接口类型和数组。
  • 基本类型(primitive types)

    • primitive types 包括boolean类型以及数值类型(numeric types)。numeric types又分为整型(integer types)和浮点型(floating-point type)。整型有5种:byte short int long char(char本质上是一种特殊的int)。浮点类型有float和double。
  • 引用类型(reference types)

    • ①接口 ②类 ③数组
  • 基本数据类型和引用数据类型传递区别

    • 程序设计语言中有关参数传递给方法的两个专业术语是:

      • 按值调用:表示方法接收的是调用者提供的值。
      • 按引用调用:表示方法接收的是调用者提供的变量的地址。
    • 在Java中 不存在按引用调用,也就是说,假如方法传递的是一个引用数据类型,那么可以修改引用所指向的对象的属性,但不能让引用指向其它的对象。

    • 传递基本数据类型

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      public static void methodBasic() {
      int lNum = 3;
      methodRunInner(lNum);
      Log.d("CopySample", "After methodRunInner, lNum=" + lNum);
      }

      private static void methodRunInner(int lNum) {
      lNum++;
      Log.d("CopySample", "methodRunInner, lNum=" + lNum);
      }
    • 传递引用数据类型

      • 当传递的是引用数据类型,可以在函数中修改该引用所指向的对象的成员变量的值,如下所示:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      public static void methodRef() {
      NumHolder holder = new NumHolder();
      holder.num = 3;
      methodRunInner(holder);
      Log.d("CopySample", "After methodRunInner, holder.num=" + holder.num);
      }

      private static void methodRunInner(NumHolder holder) {
      holder.num++;
      Log.d("CopySample", "methodRunInner, holder.num=" + holder.num);
      }

      private static class NumHolder {
      int num;
      }

值传递

  • 在方法的调用过程中,实参把它的实际值传递给形参,此传递过程就是将实参的值复制一份传递到函数中,这样如果在函数中对该值(形参的值)进行了操作将不会影响实参的值。因为是直接复制,所以这种方式在传递大量数据时,运行效率会特别低下。
  • 比如String类,设计成不可变的,所以每次赋值都是重新创建一个新的对象,因此是值传递!

引用传递

  • 引用传递弥补了值传递的不足,如果传递的数据量很大,直接复过去的话,会占用大量的内存空间。
  • 引用传递就是将对象的地址值传递过去,函数接收的是原始值的首地址值。
  • 在方法的执行过程中,形参和实参的内容相同,指向同一块内存地址,也就是说操作的其实都是源数据,所以方法的执行将会影响到实际对象

案例分析

  • 看下面代码案例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    private void test1(){
    Demo demo = new Demo();
    demo.change(demo.str, demo.ch);
    Log.d("yc---",demo.str);
    Log.d("yc---", Arrays.toString(demo.ch));
    //打印值
    //yc---: hello
    //yc---: [c, b]
    }

    public class Demo {
    String str = new String("hello");
    char[] ch = {'a', 'b'};
    public void change(String str2, char[] ch2) {
    str = "ok";
    ch[0] = 'c';
    }
    }
  • 案例过程分析

    1. new Demo后,为对象分配空间,栈中有sts 和 ch 分别指向堆中的 hello和 数组{‘a’, ‘b’}。
    2. 执行change()方法,方法的二个形参(str2和ch2)定义在栈中。
      • 因为String是不可变类且为值传递,这里str是直接复制了值,相当于重新创建一个对象并没有改变实参str的值。
      • 而数组ch[]是引用传递,形参ch2指向同一块内存地址,这里修改了直接改变实参。

小结

  • 通过上面的分析我们可以得出以下结论:
    • 基本数据类型传值,对形参的修改不会影响实参;
    • 引用类型传引用,形参和实参指向同一个内存地址(同一个对象),所以对参数的修改会影响到实际的对象。
    • String, Integer, Double等immutable的类型特殊处理,可以理解为传值,最后的操作不会修改实参对象。
  • 如何理解引用类型的按值传递?
    • 引用类型的按值传递,传递的是对象的地址。只是得到元素的地址值,并没有复制元素。比如数组,就是引用传递,假如说是值传递,那么在方法调用赋值中,将实参的值复制一份传递到函数中将会非常影响效率。

绑定机制

什么是绑定

  • 把一个方法与其所在的类/对象关联起来叫做方法的绑定。绑定分为静态绑定(前期绑定)和动态绑定(后期绑定)。

静态和动态绑定

  • 静态绑定(前期绑定)是指:
    • 在程序运行前就已经知道方法是属于那个类的,在编译的时候就可以连接到类的中,定位到这个方法。
    • 在Java中,final、private、static修饰的方法以及构造函数都是静态绑定的,不需程序运行,不需具体的实例对象就可以知道这个方法的具体内容。
  • 动态绑定(后期绑定)是指:
    • 在程序运行过程中,根据具体的实例对象才能具体确定是哪个方法。
    • 动态绑定是多态性得以实现的重要因素,它通过方法表来实现:每个类被加载到虚拟机时,在方法区保存元数据,其中,包括一个叫做 方法表(method table)的东西,表中记录了这个类定义的方法的指针,每个表项指向一个具体的方法代码。如果这个类重写了父类中的某个方法,则对应表项指向新的代码实现处。从父类继承来的方法位于子类定义的方法的前面。

动态绑定编译原理

  • 我们假设 Father ft=new Son(); ft.say(); Son继承自Father,重写了say()。
  • 编译:我们知道,向上转型时,用父类引用执行子类对象,并可以用父类引用调用子类中重写了的同名方法。但是不能调用子类中新增的方法,为什么呢?
  • 因为在代码的编译阶段,编译器通过 声明对象的类型(即引用本身的类型) 在方法区中该类型的方法表中查找匹配的方法(最佳匹配法:参数类型最接近的被调用),如果有则编译通过。(这里是根据声明的对象类型来查找的,所以此处是查找 Father类的方法表,而Father类方法表中是没有子类新增的方法的,所以不能调用。)
  • 编译阶段是确保方法的存在性,保证程序能顺利、安全运行。

动态绑定运行原理

  • 运行:我们又知道,ft.say()调用的是Son中的say(),这不就与上面说的,查找Father类的方法表的匹配方法矛盾了吗?不,这里就是动态绑定机制的真正体现。
  • 上面编译阶段在 声明对象类型 的方法表中查找方法,只是为了安全地通过编译(也为了检验方法是否是存在的)。而在实际运行这条语句时,在执行 Father ft=new Son(); 这一句时创建了一个Son实例对象,然后在 ft.say() 调用方法时,JVM会把刚才的son对象压入操作数栈,用它来进行调用。而用实例对象进行方法调用的过程就是动态绑定:根据实例对象所属的类型去查找它的方法表,找到匹配的方法进行调用。我们知道,子类中如果重写了父类的方法,则方法表中同名表项会指向子类的方法代码;若无重写,则按照父类中的方法表顺序保存在子类方法表中。故此:动态绑定根据对象的类型的方法表查找方法是一定会匹配(因为编译时在父类方法表中以及查找并匹配成功了,说明方法是存在的。这也解释了为何向上转型时父类引用不能调用子类新增的方法:在父类方法表中必须先对这个方法的存在性进行检验,如果在运行时才检验就容易出危险——可能子类中也没有这个方法)。