背景
最近在项目里经常看到有的小伙伴喜欢在参数里面加上final关键字,平常也没怎么注意,偶然几天有空仔细看了一下,觉得十分有意思。所以记录下来,一方面是给自己多一些加深回忆的素材,另一方面也是希望能给与我有相同疑惑的同学有个参考。当然这个本身可能是比较基础的问题,但是多换个角度看也蛮有意思的
final关键字在Java中的应用场景其实也还是蛮多的,其中最出名的地方,应该就是继承的时候不允许继承吧。然后还有很多的场景就是final static来定义一些常量吧。当然,还有一个比较有意思的场景,就是今天我们的主角,在传参时使用final关键字。
比如这样:
public void doThings(final Object inputParam)
这里不由得联想到了当年刚学C++中,一个很有意思又很容易混淆的概念,参数时按址传递还是按值传递的呢?当然在Java中因为取消了 * 和 & 这些容易混淆的关键字,但是如果在传递参数时不注意的话,还是很容易造成一些不容易发现的错误的。那么就想从使用final关键词上来在看看Java有趣的传参机制。
为什么在传参中加上final
首先,我觉得还是给出一个大概的结论吧:
-
int、char这些基础类型来说,可以认为就是按值传递的(在函数中对其的修改不会影响到参数本身来源的值得改变)。当然String这个类型会比较特殊,在于虽然本身是一个基础类型,但是它同时又是一个对象,有一些列Object中继承的方法。(这里和String在内存中具体处理有关系,因为我们实际操作的是String其实是一个对象,而真实的字串值是存在于静态区,并非是String本身,这一点和装箱类型的Integer之类的有点相似。具体有兴趣的同学可以参考一下Java中内存结构,如内存模型的一个图
谜之音:凑不要脸的! 我:是啊,我就是,怎么了(捂脸)) -
对于对象来说,按照按值传递来处理,基本上也是没有问题的,而且能够简化问题本身。
-
但是(终于等到这个词了,据说是中国人最讨厌的两个字之一(捂脸)),在Java中其实也是可以直接改变参数对象的值的,只要你加上final关键字之后编译器不报错的话,而且这很好的弥补了有时我们返回值只有一个时的尴尬。
那么,这是为啥呢,我们就来看看final关键字吧~其实就final本身来说,还是比较好理解的,简单的可以认为final就代表着不能改变的意思。可以发现,不管是final在修饰变量,或者修饰方法,或者修饰类的时候,其实都最最终意味着不可改变的意思。
乍一听,感觉有点玄乎,不可改变为啥修饰了参数之后反而使得参数值本身可以改变了呢?其实这里和Java本身的机制有关,因为传递的是参数并非参数本身,而是参数本身的一个引用的复制。这也就是很多文章将这里的传递和赋值号联系起来的原因。具体可以参考这些文章,写的挺好的:
所以参考下图,我们可以发现,其实参数值的变化,是因为我们在内部可以通过引用来修改到参数值本身,但是其本质仍然是一个引用,而final定义的不可改变,是引用的不可改变,也就是引用本身是不能改变的(即不能指向其它对象!)
![](https://img.haomeiwen.com/i2198180/3d9c2f04b3a5411c.png)
所以引用的不可改变就表示了我们始终只能修改最初开始所指向的值本身,而不会因为修改成中间可能指向了其它值得引用本身。这里可能有点难以理解,我们可以通过以下代码来加强理解一下:
public class AnPoJo {
public int a = 1;
@Override
public String toString() {
return String.valueOf(a);
}
}
public class Application {
public static void main(String[] args) {
AnPoJo pojo = new AnPoJo();
modify1(pojo);
System.out.println("modify1: "+ pojo);
modify2(pojo);
System.out.println("modify2: " + pojo);
}
private static AnPoJo modify1(AnPoJo pojo){
pojo = new AnPoJo();
pojo.a = 2;
return pojo;
}
private static AnPoJo modify2(AnPoJo pojo){
pojo.a = 2;
return pojo;
}
}
打印结果如下:
modify1: 1
modify2: 2
由此可见,其实只要引用本身不变化,是可以成功达到想要修改参数值的效果的。因此这里如果使用final关键字来标记想要直接改变参数值的方法是一个非常好的编程习惯,杜绝了疏忽可能导致的错误。例如可以这样来修改方法modify1,通过编译器就能很容易的检测到错误点:
private static AnPoJo modify1(final AnPoJo pojo){
pojo = new AnPoJo(); // compile error
pojo.a = 2;
return pojo;
}
结语
其实有很多非常好的编程技巧,看似平淡无奇,但是仔细思考起来还是蛮有意思的一件事,能从中体会到优秀代码中对语言本身的理解和把握。除了本例之外,还有诸如其它一些例子,如: if(true==isValid())
当然由于知识结构所限,也有一个学习的过程,如果有所错误或者遗漏,欢迎大家指正补充~
并在此给出一个简单的结论:
- 如果不清楚到底是传值还是传址,Java中就全部当做传值处理就ok(除了部分特殊场景外,也不会有什么不足)
- 如果一定要改变参数值本身,一定要加final关键字!
enjoy~
网友评论
1、先说一个模型:函数调用的时候传递参数的方式有:栈、寄存器和公共内存空间(一般就是静态变量空间和常量空间),大多数编译型语言编译成机器码对应的汇编代码都可以看出栈传递参数是最常见的。
2、Java 中的基本类型在函数调用过程中均是按照 `值传递` 的。对象均是按照 `地址传递`的。为什呢?因为 基本类型和 地址类型 都是固定字节的,能确定大小的。所以直接通过栈分配空间的(这也就是为什么JAVA中每个线程会有一个单独的栈空间和栈指针)。对象的大小不确定,而且一般的对象的空间会比较大,所以直接进行值传递,栈的空间会被占用很多,开销也大。所以JAVA采用对象地址传递(大多数语言的对象都是地址传递,C/C++两种方式都可以)。
3、JAVA中的final关键字,我个人理解是把变量定义到了常量空间,这样的好处是,很多函数中定义的匿名类可以取到值,因为不加final就放到了栈空间了,栈空间在函数调用完就回收了,有些匿名类可能是异步调用,这样就访问不到那个参数了,这是匿名类访问不到不加final参数的原因之一。加上final的参数,如果是对象,那么这个值就是对象地址(这么说不准确,因为java中的引用并不是直接指向对象内存的,而是指向一个固定大小且包含有 对象内存地址和对象其他信息的内存空间的地址),java32位中一个地址的大小4字节。例如 final obj = new Object(),obj这个变量只有4字节,且!!且!! obj的值不可变,就是说obj不能再指向其他的对象了!!。
soga,欢迎交流,互相学习 :)
我只是说下自己的理解。。。。交流下而已。
“ final Object obj = new Object(),obj这个变量只有4字节” 这句话,其实是说 obj这个变量,这变量只是一个指针,obj只是存放内存地址的变量,存放内存地址的变量其实很小,一般32位4字节,64位8字节,就可以存放地址了。而且obj的空间分配在栈里,栈中分配一个空间存放真正对象的堆地址。真正的对象数据 new Object() 放在堆里。而对象的那些头信息也放在堆里。并不放到obj 这个变量里。这个变量只是个指针类型。我其实是这个意思= =、
对于final修饰的参数和变量,存放在 常量区,也只是我自己的理解。。。并没有找资料证实。有空再翻翻书。。
不过感谢你的分享~
不过个人理解“不加final就放到了栈空间了”这一个和我所理解的在参数中加final关键字的情况有出入。因为final修饰的对象引用,仍然是在栈中的,在函数返回时就可以回收了(也就是文中描述的方法参数中的引用是源引用的一个复制),当然可能我的理解也不一定准确,还是得看下JVM的实际处理才知道了。
还有就是“ final obj = new Object(),obj这个变量只有4字节”,我觉得这个不一定准确,因为如果单纯从c++的引用来说,可能是这样的,但是java里面这样的一个对象是不止那么大的,还和object中包含的东西有关,可以参考下http://www.cnblogs.com/zhanjindong/p/3757767.html