写在前面 你可能只需要看写在前面
这篇文章的内容就是讲清楚以下三句话,如果这三句话都理解透的同学,可绕过。
- Java的传参方式只有传值。
Java程序设计语言总是采用按值调用(call by value)。也就是说,方法得到的是所有参数值的一个副本。具体来讲,方法不能修改传递给它的任何参数变量的内容。(Java核心技术卷I) - Java的基本类型变量里保存的是数据本身的值
- Java的引用类型变量里保存了其引用的数据(可以是类类型、接口或数组等一切非基本类型数据)的地址。
形参复制了一份实参的值。不管参数是基本类型或引用类型,都是将实参变量的值复制一份给形参变量。注意:是复制变量的值。所以,基本类型形参复制的是其数据本身的值,引用类型的形参复制的是被引用数据的地址。
下面还有两问,如果觉得自己上面三句话都理解透了,但下面这两问又把自己搞蒙了的同学,对不起,你没透。
- 某方法的形参为数组,并在方法中修改了这个数组其中一个元素的值,且此方法没有返回值。为什么方法调用结束后,实参所引用数组的这个元素的值也改了?
- 某方法的一个形参为某个对象,并在方法中修改了这个对象的属性值,且此方法没有返回值。为什么方法调用结束后,实参所引用对象的属性值也变了?
有三有二,那再来一个一
- 下面代码两次打印的值分别是什么?
public static void main(String[] args) {
String ss="我在main函数中被赋值了";
stringTest(ss);
System.out.println("这是编号1打印ss="+ss);
}
public static void stringTest(String ss){
System.out.println("这是编号2打印ss="+ss);
ss="我在stringTest方法中被改写了";
}
如果以上三句话能理解,两个问题能回答,一段代码没疑惑,那么同学,请出列,后面没你啥事了。
注:有同学问,为什么不把答案写在这里?那个,如果对自己的答案不敢100%确定的,咱们还能再处处?
一、 问题引入
今天有一个不是太小朋友的小朋友问了我一个关于传参的问题。demo的代码如下:
注:如果下面的代码有小朋友看不懂,不要慌,后续会一层一层的慢慢解释让你看懂。
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
testList(list);
System.out.println("list.size:"+ list.size());
}
public static void testList(List<String> list) {
list = list.stream().filter(x -> {
if(x.equals("3"))
return false;
return true;
}).collect(Collectors.toList());
System.out.println("test.list size:"+list.size());
}
运行结果是这样子的:
test.list size:3
list.size:4
他问,为什么最后打印的结果,main方法中list集合里还是4个元素,不应该是3个元素吗?
关于这个问题,我们先讨论一下java的传参
二、 java的数据类型
大家都知道,java的数据类型分两种:基本数据类型和引用数据类型。
除了那8种基本数据类型,其它的数据类型都是引用类型。
基本数据类型变量声明并赋值后,其值直接保存在变量中。
而所有的声明为引用类型的变量里保存的值,其实都是它所引用数据的地址。重要的事情我加粗,就不说三遍了
三、 Java基本类型
3.1基本数据类型的变量声明及赋值
int a;
a=5;
上面两行代码的解析:
- 首先声明了int类型的变量a,则JVM给变量a开辟了一个内存空间。
- 其次给a赋初值,直接将字面值5写入变量a的内存空间中。
3.2 基本数据类型传参
- 所有的形参都是实参的复制。
- 形参的值的改变,不会影响到实参
示例代码如下:
public static void main(String[] args) {
//声明变量a,并赋初值 10
int a=10;
//调用方法 addIntSelf,a做为实参传入
addIntSelf(a);
//打印变量a的值
System.out.println("main a="+a);
}
public static void addIntSelf(int x){
// x自加 =>x=x+x
x+=x;
//打印变量x的值
System.out.println("addIntSelf x="+x);
}
代码解析:
片断一 :在main方法中的前两句代码解析
//声明变量a,并赋初值 10
int a=10;
//JVM会在栈中为a开辟内存空间,并存入10
//调用方法 addIntSelf,a做为实参传入
addIntSelf(a);
// jvm在栈中为addIntSelf的形参x开辟内存空间,
//并存入实参a 的值。此时实参a与形参x分别占据两个不同的空间地址
由上图可见,此时栈中有两个元素,分别是实参a和形参x,其值都是 10。
片断二 :在addIntSelf方法中的代码解析
//本次调用x的值为10
public static void addIntSelf(int x){
// x自加 =>x=x+x
x+=x; // x的值变为 20
//打印变量x的值
System.out.println("addIntSelf x="+x);
}
由上图可见,在addIntSelf方法中,形参x的值改为了20,但实参a的值没有变化,还是10.
addIntSelf中的代码运行完毕,返回main方法。此时addIntSelf方法中的变量出栈,销毁。此时栈中还保留 变量a,其值为10。
所以,以上代码最终运行结果如下:
addIntSelf x=20
main a=10
4.1 Java引用类型的声明和赋初值
所有不是8种基本类型的变量都是引用类型变量。
声明一个引用类型的变量,会在内存空间中为它开辟一个空间等待保存地址。赋值后,其引用的数据所在内存空间地址会做为变量的值保存在变量中。
下面用数组变量和自定义类的变量来举例说明一下。
4.1.1 数组变量的声明
int[] arrInt=new int[4];
代码解析:
首先给变量arrInt开辟一个内存空间(如果变量声明在方法中,则内存空间在栈中)。
其次,在堆中开辟一个数组的内存空间。这个内存空间由连续的四个int类型的内存空间组成。首元素的地址即这个数组内存空间的地址,保存在变量 arrInt中,做为arrInt的值。
4.1.2 自定义类的对象变量的声明
定义一个Student类,类中只有一个属性name。get和set方法分别对这个属性进行读写操作。
public class Student {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
创建一个学生对象
Student stu=new Student();
创建对象的代码解析:
首先,给Student类型的变量stu开辟一个内存空间。
其次,在堆中开辟一个内存空间,用来存放Student类型的对象。
最后,将对象的空间地址赋值给变量stu.
好了,至此,我们明确了,引用类型变量里保存的是地址,地址,地址
4.2 Java引用类型传参
重要的事情再说一遍
- 所有的形参都是实参的复制。
- 形参的值的改变,不会影响到实参
但是,引用类型变量里存的是地址。所以,形参复制了实参的值,得到的是地址。再去操作这个地址所指向的对象、数组、集合什么的,好象是合法的哇。是不是突然发现了什么大不了的事情? - 当实参是引用类型变量时,将值(实际是被引用数据的地址)复制给形参。
4.2.1形参得到地址后,去操作所引用类型的数据
上示例代码
public static void main(String[] args) {
Student stu=new Student();
stu.setName("张三");
System.out.println("学生姓名:"+stu.getName());
setStudentName(stu);
System.out.println("*****调用方法后*****");
System.out.println("学生姓名:"+stu.getName());
}
public static void setStudentName(Student s){
s.setName("李四");
}
运行结果:
学生姓名:张三
*****调用方法后*****
学生姓名:李四
代码解析:
1. 在main方法中创建一个学生对象,并将其属性name赋值为:张三
- 调用setStudentName方法,传入学生对象(其实质是传入的学生对象所在地址) 在栈中给setStudentName方法的参数s开辟一个内存空间,里面保存传入的学生对象的地址(即复制了实参stu的值)。
- 运行 setStudentName方法中的代码:
s.setName("李四");
将学生对象的name属性的值从张三更改为李四
4. setStudentName方法中的代码运行结束,返回main方法。形参s退栈。
而main方法中的stu变量所引用的学生对象其name属性已被更改为李四。有没有问题?
4.2.2形参指向另一个对象
上示例代码
public static void main(String[] args) {
Student stu=new Student();
stu.setName("张三");
System.out.println("学生姓名:"+stu.getName());
setStudentName(stu);
System.out.println("*****调用方法后*****");
System.out.println("学生姓名:"+stu.getName());
}
public static void setStudentName(Student s){
s=new Student();
s.setName("李四");
}
运行结果:
学生姓名:张三
*****调用方法后*****
学生姓名:张三
代码解析:
第1、2步与上例(4.1.1的示例)相同。
1. 在main方法中创建一个学生对象,并将其属性name赋值为:张三
- 调用setStudentName方法,传入学生对象(其实质是传入的学生对象所在地址) 在栈中给setStudentName方法的参数s开辟一个内存空间,里面保存传入的学生对象的地址(即复制了实参stu的值)。
3.运行 setStudentName方法中的代码:
s=new Student();
s.setName("李四");
- 新建一个学生对象,并将这个新的学生对象的地址赋值给s。
- 将s所指学生对象的name属性赋值为李四
4.setStudentName方法中的代码运行结束,返回main方法。形参s退栈。
而main方法中的stu变量所引用的学生对象其name属性还是张三,没有更改。而新的student对象没有引用变量指向它,呆在堆中等待被回收,此时只能想静静。此时没问题吧。
- 参数是基本类型,形参不能对实参造成任何影响。
- 参数是引用类型
- 形参和实参所引用的数据相同(同一个副本)。可以使用形参来修改实参所引用的数据。
- 如果给形参重新赋值,则形参所引用的数据与实参不再相同(不再是同一个副本)。此时修改形参所引用数据的值,实参所引用数据不会被改变。
5.1解决引入问题
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
testList(list);
System.out.println("list.size:"+ list.size());
}
public static void testList(List<String> list) {
list = list.stream().filter(x -> {
if(x.equals("3"))
return false;
return true;
}).collect(Collectors.toList());
System.out.println("test.list size:"+list.size());
}
运行结果是这样子的:
test.list size:3
list.size:4
他问,为什么最后打印的结果,main方法中list集合里还是4个元素,不应该是3个元素吗?
答案解析
list.stream()方法和list.stream().filter()都会新建一个对象,我们来简单看看源代码。
list.stream()
public static <T> Stream<T> stream(Spliterator<T> spliterator, boolean parallel) {
Objects.requireNonNull(spliterator);
return new ReferencePipeline.Head<>(
spliterator,
StreamOpFlag.fromCharacteristics(spliterator),
parallel);
}
list.stream().filter()
public final Stream<P_OUT> filter(Predicate<? super P_OUT> predicate) {
Objects.requireNonNull(predicate);
return new StatelessOp<P_OUT, P_OUT>(this, StreamShape.REFERENCE,
StreamOpFlag.NOT_SIZED) {
// ...
};
}
首先:list.stream()方法返回的就是一个全新的对象(我们给它命个名:S)。
其次:filter()方法接收到全新的对象S,对传入的数据过滤筛选后,将合条件的数据保存到更新的对象(此处它拥有姓名:F)传出。将这个新建的对象F重新赋值给list变量。则此时list变量与实参所指对象不再是同一个。而且整个过程中实参所指对象并没有任何更改。
所以调用testList(list) 结束后,在main方法中打印list,结果是4个元素。
5.2解决那个一
- 下面代码两次打印的值分别是什么?
public static void main(String[] args) {
String ss="我在main函数中被赋值了";
stringTest(ss);
System.out.println("这是编号1打印ss="+ss);
}
public static void stringTest(String ss){
System.out.println("这是编号2打印ss="+ss);
ss="我在stringTest方法中被改写了";
}
运行结果
这是编号2打印ss=我在main函数中被赋值了
这是编号1打印ss=我在main函数中被赋值了
答案解析
首先字符串是不能被更改的。只要更改了,就是另外一个对象了。
其次,形参ss在stringTest()方法中被重新赋值,指向了另一个字符串对象,不会影响到实参的值。
5.3解决最后的两个问题
- 某方法的形参为数组,并在方法中修改了这个数组其中一个元素的值,且此方法没有返回值。为什么方法调用结束后,实参所引用数组的这个元素的值也改了?
答案:因为形参和实参所引用的是同一个数组。 - 某方法的一个形参为某个对象,并在方法中修改了这个对象的属性值,且此方法没有返回值。为什么方法调用结束后,实参所引用对象的属性值也变了?
答案:因为形参和实参所引用的是同一个对象。---突然感觉这个答案写得好象有点不走心?涉嫌抄袭上一题答案?
打完收工