写在前面 你可能只需要看写在前面
这篇文章的内容就是讲清楚以下三句话,如果这三句话都理解透的同学,可绕过。
- Java的传参方式只有传值。Java程序设计语言总是采用按值调用(call by value)。也就是说,方法得到的是所有参数值的一个副本。具体来讲,方法不能修改传递给它的任何参数变量的内容。(Java核心技术卷I)
- Java的基本类型变量里保存的是数据本身的值
- Java的引用类型变量里保存了其引用的数据(可以是类类型、接口或数组等一切非基本类型数据)的地址。
形参复制了一份实参的值。不管参数是基本类型或引用类型,都是将实参变量的值复制一份给形参变量。注意:是复制变量的值。所以,基本类型形参复制的是其数据本身的值,引用类型的形参复制的是被引用数据的地址。
下面还有两问,如果觉得自己上面三句话都理解透了,但下面这两问又把自己搞蒙了的同学,对不起,你没透。
- 某方法的形参为数组,并在方法中修改了这个数组其中一个元素的值,且此方法没有返回值。为什么方法调用结束后,实参所引用数组的这个元素的值也改了?
- 某方法的一个形参为某个对象,并在方法中修改了这个对象的属性值,且此方法没有返回值。为什么方法调用结束后,实参所引用对象的属性值也变了?
有三有二,那再来一个一
- 下面代码两次打印的值分别是什么?
如果以上三句话能理解,两个问题能回答,一段代码没疑惑,那么同学,请出列,后面没你啥事了。注:有同学问,为什么不把答案写在这里?那个,如果对自己的答案不敢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 基本数据类型传参
- 所有的形参都是实参的复制。
- 形参的值的改变,不会影响到实参示例代码如下:
代码解析:片断一 :在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=20main a=10四、Java引用类型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赋值为:张三
将学生对象的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赋值为:张三
3.运行 setStudentName方法中的代码:
- 新建一个学生对象,并将这个新的学生对象的地址赋值给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解决那个一
- 下面代码两次打印的值分别是什么?
运行结果
这是编号2打印ss=我在main函数中被赋值了这是编号1打印ss=我在main函数中被赋值了答案解析首先字符串是不能被更改的。只要更改了,就是另外一个对象了。其次,形参ss在stringTest()方法中被重新赋值,指向了另一个字符串对象,不会影响到实参的值。
5.3解决最后的两个问题
- 某方法的形参为数组,并在方法中修改了这个数组其中一个元素的值,且此方法没有返回值。为什么方法调用结束后,实参所引用数组的这个元素的值也改了?答案:因为形参和实参所引用的是同一个数组。
- 某方法的一个形参为某个对象,并在方法中修改了这个对象的属性值,且此方法没有返回值。为什么方法调用结束后,实参所引用对象的属性值也变了?答案:因为形参和实参所引用的是同一个对象。---突然感觉这个答案写得好象有点不走心?涉嫌抄袭上一题答案?
打完收工