专栏首页WindCoderjava漫谈-Java只有值传递

java漫谈-Java只有值传递

《Head First Java》中关于 Java 参数传递的说明:

Java 中所传递的所有东西都是值,但此值是变量所携带的值。引用对象的变量所携带的是远程控制而不是对象本身,若你对方法传入参数,实际上传入的是远程控制的拷贝

《Java编程思想 第四版》中第二章第一节“用引用操作对象”中写到:

尽管一切都看作对象,但操作的标识符实际上是一个对象的引用(reference)。

文中用遥控器(引用)操作电视(对象)为例形象的说明了该引用名词的含义,同时在对定义的“引用”该名词的注释中提到:

有人认为:“很明显,它是一个指针。”但是这种说法是基于底层实现的某种假设。并且,Java中的引用,在语法上更接近C++的引用而不是指针。 还是有很多人不同意用“引用”这个术语。我曾读到的一本书中这样说:“Java所支持的‘按址传递’是完全错误的”,因为Java对象标识符(按那位作者所说)实际上是“对象引用”。并且他接着说任何事物都是“按值传递”的。也许有人会赞成这种精准却让人费解的解释,但我认为我的这种方法可以简化概念上的理解并且不会伤害到任何事物。

对于基本类型(int等)没用争议,肯定是值传递。但String、基本类型的封装类(Integer等)、自定义类(如User等)传递的是一个地址,这就容易让人联想到C++中的指针传递和引用传递。以一个User类为例:

public class User {
    private String name;
    private int age;
    private Integer height;

    public User(String name, int age, Integer height) {
        this.name = name;
        this.age = age;
        this.height = height;
    }

    // ......

    // 省略了无参构造函数、所有set/get函数。

    // ......

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", height=" + height+'\'' +
                ", address=" + super.toString() +
                '}';
    }
}

示例1:

public class MainDemo {
    public static void main(String[] args) {
        User a = new User("a",10,10);
        User b = new User("b",11, 11);
        PrintUtill.println("交换前:");
        PrintUtill.println("a: "+a);
        PrintUtill.println("b: "+b);
        PrintUtill.printlnRule();
        swap(a,b);
        PrintUtill.println("交换最后:");
        PrintUtill.println("a: "+a);
        PrintUtill.println("b: "+b);
        PrintUtill.printlnRule();
        change(a);
        PrintUtill.println("修改最后:");
        PrintUtill.println("a: "+a);
    }

    public static void swap(Object sa, Object sb){
        Object sc = sa;
        sa = sb;
        sb = sc;
        PrintUtill.println("交换中:");
        PrintUtill.println("sa: " + sa);
        PrintUtill.println("sb: " + sb);
        PrintUtill.printlnRule();
    }


}

结果

交换前:
a: User{name='a', age=10, height=10', address=Others.base.common.User@2503dbd3}
b: User{name='b', age=11, height=11', address=Others.base.common.User@4b67cf4d}

--------windcoder.com----------

交换中:
sa: User{name='b', age=11, height=11', address=Others.base.common.User@4b67cf4d}
sb: User{name='a', age=10, height=10', address=Others.base.common.User@2503dbd3}

--------windcoder.com----------

交换最后:
a: User{name='a', age=10, height=10', address=Others.base.common.User@2503dbd3}
b: User{name='b', age=11, height=11', address=Others.base.common.User@4b67cf4d}

--------windcoder.com----------

不管是断点跟踪还是最终打印出的结果,swap方法中确实做了交换(sa和sb的地址做了交换),但并未对方法外有任何影响(a和b的地址指向依旧是原来的未变)。

每个方法的运行都会在Java虚拟机栈中创建一个栈帧,里面存放了局部变量表等内容,方法的运行就是一个栈帧进栈出栈的过程。

如方法swap:

public static void swap(Object sa, Object sb)

此时的sa,sb属于形参,就是形式参数,用于定义方法的时候使用的参数,用来接收调用者传递的参数。形参只有在方法被调用的时候,虚拟机才会分配内存单元,在方法调用结束之后便会释放所分配的内存单元。

相关知识: JVM-Java内存区域 JVM基础小结

换种说法,当调用swap方法时,sa类似C++中的引用,成为地址2503dbd3的一个别名,亦既上面关于《思想》中的引用说的更接近引用而不是指针,不同之处是一旦执行sa = sb;,改变的仅是是sa的指向,对原地址2503dbd3不会造成任何影响。一旦方法执行完,sa被分配到内存单元便会被释放。该实例执行如图:

示例2:

在示例1中追加代码后如下:

public class MainDemo {
    public static void main(String[] args) {
        User a = new User("a",10,10);
        User b = new User("b",11, 11);
        // ......
        // 省略示例1中的代码
        // ......
        change(a);
        PrintUtill.println("修改最后:");
        PrintUtill.println("a: "+a);
    }
    public static void swap(Object sa, Object sb){

        // ...省略

    }
    public static void change(User sa){
        sa.setName("a2");
        sa.setAge(11);
        sa.setHeight(12);
        PrintUtill.println("修改中:");
        PrintUtill.println("sa: " + sa);
    }
}

结果

// ......
// 省略之前的

交换最后:
a: User{name='a', age=10, height=10', address=Others.base.common.User@2503dbd3}
b: User{name='b', age=11, height=11', address=Others.base.common.User@4b67cf4d}

--------windcoder.com----------

修改中:
sa: User{name='a2', age=11, height=12', address=Others.base.common.User@2503dbd3}
修改最后:
a: User{name='a2', age=11, height=12', address=Others.base.common.User@2503dbd3}

此时函数内发生了变化影响到函数外的数据,change方法外的a和方法的参数sa指向的均是地址2503dbd3,指向未发生变化,但函数中的操作导致所指向的地址2503dbd3中的内容发生了变化,

示例3

public class ListDemo {
    public static void main(String[] args) {
        List<String> list = null;
        // List<String>  list = new ArrayList<String>();
        add(list);
        list.add("3");
        list.add("4");
        PrintUtill.println("list.size:" + list.size());
    }

    public static void add(List<String> list){
        if (list==null){
            list = new ArrayList<String>();
        }
        list.add("1");
        list.add("2");
    }
}

该示例中list最开始为null,意味着没有地址,当作为实参传进add方法中,add.list没有地址,从而执行了list = new ArrayList<String>();,add.list被分配了新的地址,当执行完add方法,add.list就会被释放,但main.list仍旧为空,从而导致NullPointerException(空指针异常)。

扩展

C++中函数参数的几种类型

参考地址

Java 中的参数传递和引用类型

C++ 函数

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Java8的List Object 去重

    假设Object为User,此处User类中省略getting/setting以及相关构造方法。

    汐楓
  • 数据加密

    /* 功能:数据加密 日期:2013-05-26 */ #include<stdio.h> #include<stdlib.h> #include<...

    汐楓
  • 《Linux内核分析》之分析system_call中断处理过程实验总结

    先占个位置,在实验楼做实验,刚做完一半忘了延续时间,结果之前写的代码神马的全没了。让我先去角落哭会,总结明天再写。2015-04-04

    汐楓
  • MyBatis之foreach

         foreach 元素是非常强大的,它允许你指定一个集合,声明集合项和索引变量,它们可以用在元素体内。它也允许你指定开放和关闭的字符串,在迭代之间放置分...

    Arebirth
  • AI助力短视频创作

    hi,大家好~我是shadow,一枚设计师/全栈工程师/算法研究员,目前主要研究方向是人工智能写作和人工智能设计,当然偶尔也会跨界到人工智能艺术及其他各种AI产...

    mixlab
  • 基于V6的中移动物联测试例子,当前测试还挺稳定

    链接:https://pan.baidu.com/s/1Gz8mEffDGXNSK8lIsAIUEg   提取码:2sur

    armfly
  • 用鱼竿、鱼钩、鱼饵和彩蛋模拟一次网络渗透

    *本文原创作者:flagellantX,本文属FreeBuf原创奖励计划,未经许可禁止转载

    FB客服
  • 原 Centos7 NodeJS安装记录

    霡霂
  • 程序员那些必须掌握的排序算法(上)

    现在的IT行业并不像以前那么好混了,从业人员过多,导致初级程序员过剩,这也间接导致了公司的招聘门槛越来越高,要求程序员掌握的知识也越来越多。 算法也是一个争论...

    wangweijun
  • 基于深度学习的视频内容识别

    好久未和老相好的您们面对面的知识交流过,不知道大家最近科研是否顺利,有没有新的想法和创新,我都会祝学术界的您科研硕果累累,祝工业界的您工程完善更多智能化功能,造...

    计算机视觉研究院

扫码关注云+社区

领取腾讯云代金券