今天,被一位先生问到了一个Java的问题,下面的代码应该输出什么:
Class Demo{ public static void main(String[] args){ String s = "0"; change(s); System.out.println(s); } static void change(String s){ s = "123"; } }
说实话,当这位先生问我问题的时候,我还是比较紧张的,毕竟他曾是某牛逼公司的牛逼架构师。
而且,往往人们遇到自认为简单的问题,往往就觉得这个问题不简单,举个例子就是:
当被问到一加一等于几的时候,人们往往会想:这个问题肯定不像我想象的这么简单,说不定有个坑正等着我去跳。
所以对于上面这道题,我脑子里面经历了下面几个过程:
1.原始想法
s是一个对象,而且不是基础类型数据,所以参数因该传递的是引用,那么在函数中改变了引用中某个属性的值(比如),所以main函数中s的值肯定也就改变了。
2.转折
但是,不会这么简单吧,他为什么要问我一个类似于一加一等于几的问题?很大可能是打印“0”,但这与我的推断结果并不相符。
3.转折
难道这道题就打印的是“123”?万一这道题就是考验人的心理素质呢?
所以:
我就坚定不移的回答了“123”。当这位先生问我两次“你确定?”之后,我依然认为他是在对我进行心理上的考验。
结果证明:我错大发了!
当这位先生把结果给我展现的时候,我觉得他一定在想:哥们儿,你当时面试怎么过的呀,进这公司不是走后门儿的吧?!(哈哈。。。)
所以我当时就更紧张了,同时:我的大脑进行飞速的运转,思考着各种各样会导致这种输出的原因。
一.第一步进阶
现在我需要研究一下Java的函数调用到底是传值还是传引用?
之前我一直认为:除了Java的基础数据类型(例如int,long等等),其余的数据类型(例如String,自定义对象等)都是引用传递。结果是:
通过查阅各种资料获知,Java中只存在值传递,值传递的时候会创建副本。比如下面这个例子:
class Demo{ public static void main(String[] args){ User u = new User(); u.name = "123"; changeName(u); System.out.println(u.name); } void changeName(User u){ u = new User(); u.name = "0"; } } class User{ public Strin name; }
结果肯定是打印123,而不是打印0.现在引用就相当于房卡,对象就相当于房间。这个例子中传递的就是房卡,在changeName方法中,对房卡进行了修改,本来该房卡是打开1001这间房的,但是现在这个房卡已经执行1002号房间了。
那么main方法中的房卡是不是也就应该指向1002号房间了呢?答案是否定的,因为main方法调用changeName方法的时候,只不过是将房卡复制了一份,交给changeName(注意这里不是讲1001号房间复制一份交给changeName)。
所以这里传递的是引用的值,引用同样是一种数据结构。like that:
因为Java中没有指针,对内存的操作也很难下手,所以通过C语言相似的例子似乎更能够说明问题,如下所示:
#include<stdio.h> #include<malloc.h> #include<string.h> void getSpace(char* str) { str = (char*)malloc(sizeof(char)*10); } int main(int argc,char** argv) { char* str; getSpace(str); strcpy(str,"nihao"); printf("%s\n",str); }
这个小程序就是main函数中声明一个字符串指针,然后通过将该指针以参数的形式传入到函数中进行空间分配。
空间分配完成后将一个字符串copy到该空间。并且打印该空间中的内容。
该程序会在运行期间异常终止。这里的函数形参是一个指针,就相似于Java中的引用。
虽然我们在getSpace函数中开辟了一块空间,并且让该指针指向开辟的新的空间,但是main函数中的指针是没有变的,main函数传递的只是指针值的一个副本。同样是一个copy后的房卡,main函数中的房卡仍然是指向原来的房间。
二.第二步进阶
回到最初的例子:
Class Demo{ public static void main(String[] args){ String s = "0"; change(s); System.out.println(s); } static void change(String s){ s = "123"; } }
通过第一步进阶得到的结论:main函数中保留的是String s的引用,传递到change函数中的是s的引用副本,但他们都指向堆中的同一片区域:“123”。
所以如果我们在change函数中改变了s所指向的堆中“123”的值,那么按道理来说,main函数中s所指向的值也就会发生改变。
但是问题来了,String并没有提供改变自身value的方法,所以s = “123” 就相当于 s = new String("123");!!!!!!!!!!!!!!!!!!!!!!!!,所以s所指向的堆中“123”这片区域没有发生改变。
这里发生改变的同样是让房卡副本指向了1002号房间,main函数中的房卡仍然指向1001号房间。
所以对于之前提到的那位先生问我的问题,应该输出0,而非123.
三.第三步进阶
第二步进阶的说明是错误的!俗话说:温故而知新,当我再一次查看第二步进阶的时候我想到了以前在C语言中遇到的一个问题,如下所示:
#include<stdio.h> int main(int argc,char** argv) { char* str="hello"; str[1] = 'z'; printf("%s\n",str); }
这个程序很简单,就是先让一个指针指向一个字符串“hello”,再将“hello”中的‘e‘替换为‘z’。但是在运行时,该程序会异常退出,原因在于:
对于指针str我们并没有分配空间,而是让其直接指向了常量池(方法区)中的“Hello”字符串,常量池中的内容是不能够被改变的,就相当于4=5这样的赋值操作是不被允许的。
通过下面这个例子更加可以说明问题:
#include<stdio.h> int main(int argc,char** argv) { char* str1="hello"; char* str2="hello"; }
C调试的时候相对于Java的好处就在于:可以很容易的看到变量所在的内存地址。如下图所示:
嗦噶,上面这些话都是为了引入下面的主题:对于Java来说是不是也是这样呢?String s = “123” 与 String s = new String("123")是否有区别呢? 答案是肯定的!
我们通过下面两个程序示例可以看到效果:
示例代码一:
String a = "123"; String b = "123"; a==b; //true a.equals(b);//true
示例代码二:
String a = new String("123"); String b = new String("123"); a==b; //false a.equals(b);//true
如下图所示:
所以对于第二步进阶中说明的main中的s与change中的s指向堆中同一片内容的说法是错误的,他们应该是指向方法区中的同一片内容。
后记:
我在这里非常诚恳的感谢这位先生对我的教导。自从学Java以来,整天都是各种框架满天飞,各种新技术不断地尝试,却忽略了根本。
多看,多想,多写!