众所周知,在 Java 方法内将局部变量作为参数传递到方法后,如果该参数是基本数据类型,那内部的赋值修改不会同步到外部,如果是引用数据类型,对于对象的赋值也是不会同步到外部的,但是对于对象的属性修改是会同步的,这是因为 Java 是值传递,修改的是副本,但是副本指向的属性地址是相同的。
Java 基本数据类型的局部变量作为参数传递到方法
public class Test {
public static void main(String[] args) {
int num = 5;
changeParams(num);
}
private static void changeParams(int num){
println(num);
num = 10;
println(num);
}
private static void println(int msg){
System.out.println(msg);
}
}
执行结果如下
5
10
Java 引用数据类型的局部变量作为参数传递到方法
public class Test {
static class Data{
private int age;
private String name;
public Data(int age, String name) {
this.age = age;
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Data{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
}
public static void main(String[] args) {
Data data = new Data(15, "小李");
test01(data);
test02(data);
// 虽然 data 在 test02 方法内部被重新赋值了,但是赋值的是副本,因为 Java 是值传递
println(data.toString());
}
private static void test01(Data data){
println(data.toString());
// 传递进来的是 data 的副本,但是对象的属性是指向同一块内存
data.setAge(18);
data.setName("小黑");
}
private static void test02(Data data){
println(data.toString());
// 相当于把 data 指向一块新的堆内存
data = new Data(20, "小张");
println(data.toString());
}
private static void println(String msg){
System.out.println(msg);
}
}
执行结果如下
Data{age=15, name='小李'}
Data{age=18, name='小黑'}
Data{age=20, name='小张'}
Data{age=18, name='小黑'}
当我们在匿名内部类中使用局部变量时却发现必须将局部变量申明为 final ,这是因为 Java 并不支持闭包,Java 的匿名内部类是将该局部变量的副本传递进去,当一个匿名内部类对局部变量重新赋值(PS:这是一个假设,Java 并不支持这样做),外部的局部变量是不会被修改的,为了避免看似修改但是又没有修改的理解歧义,所以 Java 的设计者在语法就规避风险强制用 final 修饰。
Java 基本数据类型及引用数据类型的局部变量被匿名内部类使用
public class Test {
static class Data{
private int value;
public Data(int value) {
this.value = value;
}
public int getValue() {
return value;
}
public void setValue(int value) {
this.value = value;
}
@Override
public String toString() {
return "Data{" +
"value=" + value +
'}';
}
}
interface CallBack{
void onCall();
}
public static void main(String[] args) {
final int num = 5;
CallBack call1 = new CallBack(){
@Override
public void onCall() {
println(String.valueOf(num+1));
}
};
call1.onCall();
final Data data = new Data(20);
CallBack call2 = new CallBack(){
@Override
public void onCall() {
println(data.toString());
data.setValue(8);
println(data.toString());
}
};
call2.onCall();
}
private static void println(String msg){
System.out.println(msg);
}
}
执行结果如下
6
Data{value=20}
Data{value=8}
Kotlin 基本数据类型及引用数据类型的局部变量作为参数传递到方法
data class Data(var value: Int)
fun main() {
var num = 5
test01(num)
num = 10
println(num)
test02(Data(10))
}
fun test01(num: Int){
println(num)
}
fun test02(data: Data){
println(data.toString())
data.value = 20
println(data.toString())
}
执行结果如下
5
10
Data(value=10)
Data(value=20)
这里值得一提的是 Kotlin 对于函数的参数是 val 不可修改的,这在早期并非如此,以下为谷歌官方的修改提交说明
The main reason is that this was confusing: people tend to think that
this means passing a parameter by reference, which we do not support
(it is costly at runtime). Another source of confusion is primary
constructors: “val” or “var” in a constructor declaration means
something different from the same thing if a function declarations
(namely, it creates a property). Also, we all know that mutating
parameters is no good style, so writing “val” or “var” in front of a
parameter in a function, catch block of for-loop is no longer allowed.
中文翻译如下:
主要原因是这令人困惑:人们倾向于认为
这意味着通过引用传递参数,我们不支持
(这在运行时成本很高)。 造成混乱的另一个原因是主要的
构造函数:构造函数声明中的“ val”或“ var”
如果函数声明,则与同一事物有所不同
(即,它创建一个属性)。 另外,我们都知道变异
参数不是很好的样式,因此在a前面写“ val”或“ var”
参数在函数中,不再允许for循环的catch块。
其实不难看到,对于局部变量作为参数传递到方法,Java 和 Kotlin 的输出结果都是一样的,不同的是 Java 允许修改局部变量,而 Kotlin 不可以。最后我们再看一下 Kotlin 的局部变量在匿名内部类中的表现如何。
Kotlin 基本数据类型及引用数据类型的局部变量被匿名内部类使用
data class Data(var value: Int)
interface CallBack{
fun onCall()
}
fun main() {
var num = 5
val callBack01 = object : CallBack{
override fun onCall() {
println("num=$num")
num = 10
println("num=$num")
}
}
callBack01.onCall()
println("num=$num")
var data02 = Data(10)
val callBack02 = object : CallBack{
override fun onCall() {
println("data02=${data02}")
data02 = Data(20)
println("data02=${data02}")
}
}
callBack02.onCall()
println("data02=${data02}")
val data03 = Data(10)
val callBack03 = object : CallBack{
override fun onCall() {
println("data03=${data03}")
data03.value = 66
println("data03=${data03}")
}
}
callBack03.onCall()
println("data03=${data03}")
}
执行结果如下
num=5
num=10
num=10
data02=Data(value=10)
data02=Data(value=20)
data02=Data(value=20)
data03=Data(value=10)
data03=Data(value=66)
data03=Data(value=66)
咦,非常奇怪,对于引用数据类型的属性修改 Kotlin 和 Java 的表现是一样的,值会同步,但是和 Java 不同的是在匿名内部类中 Kotlin 支持对局部变量重新赋值并且属性会同步到外部,这是因为 Kotlin 支持闭包,那么闭包到底是什么呢。闭包就是能够读取其他函数内部变量的函数。例如在 javascript 中,只有函数内部的子函数才能读取局部变量,所以闭包可以理解成“定义在一个函数内部的函数“。在本质上,闭包是将函数内部和函数外部连接起来的桥梁。
以上为百度百科的解释,函数在 Kotlin 中分别为普通具名函数、匿名函数及 lambda 表达式,简而言之就是当这三种类型的函数处于其他函数的内部时是可以访问该函数的局部变量的,那么同样作为 JVM 语言,为什么 Java 不支持 Kotlin 却支持呢,其实把代码编译后可以发现 Kotlin 是把变量作为一个对象的属性来处理了,这样来达到修改的目的。
以上为个人之言,如有误,请留言评论,谢谢。