echo编辑整理



在我们平常的开发当中,我们一定碰到过基本类型和引用类型需要在方法之间进行值传递。那么什么是基本类型、哪种才是引用类型值的传递?会出现什么问题呢?


基本类型、引用类型值传递现状

首先我们来看一段代码

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

/**
* @author echo wechat:t2421499075
* @date 2020-10-28 17:15
*/
public class Test {

public static void main(String[] args) {
List<BigDecimal> list = new ArrayList<>();
list.add(BigDecimal.ZERO);
System.out.println(list);
test(list);
System.out.println(list);
}

private static void test(List<BigDecimal> list) {
list.clear();
list.add(new BigDecimal("2"));
}

private static void test1(BigDecimal bigDecimal) {
bigDecimal.add(new BigDecimal("2"));
}

}

代码中我们可以看到,我们在调用test的时候,我们传递了一个list。在list当中我们新增了一个值,最终调用之后,我们得到的结果如下:

基本类型和引用类型值传递问题解析_基本类型

但是当我们调整main方法调用test1的时候得到的结果和调用test却不相同

// main调整代码
public static void main(String[] args) {
BigDecimal bigDecimal = BigDecimal.ZERO;
System.out.println(bigDecimal);
test1(bigDecimal);
System.out.println(bigDecimal);
}

基本类型和引用类型值传递问题解析_引用传递_02


为什么不一样呢?为什么会出现一个值没被改变,一个被改变了?这就是我们基本类型在方法之间值的传递和引用类型在方法之间值传递的问题了


调用test我们传递的是引用类型,调用test1我们传递的是基本类型的包装类。这就造就了最终的结果不一样。(注意:这里是直接用的包装类型当做基本类型,原因后面会讲)

基本类型、引用类型基础

java中八大基本类型


  • byte:8位
  • short:16位
  • int:32位
  • long:64位
  • float:32位
  • double:64位
  • boolean:只有true和false两个取值。
  • char:16位

java中除基本类型以外,都可以直接判定为引用类型。Java是面向对象的语言,所有的对象都需要有引用,然后通过引用指向对象内存。


到这里之后,其实我们可以根据Java的数据类型来判断,我们的代码到底哪些是基本类型的值传递,哪些是引用类型的值传递


基本类型和引用类型值传递的结果和原因分析

我们从调用test中看到,引用类型传递值的结果。当我们在另外一个方法当中修改当前传递进入引用类型的值的时候,引用类型的值进行了改变!但是当我们调用test1的时候,我们发现,基本类型的包装类的值没有改变!

这个中间的原因其实比较简单我们调用方法的时候,不管你传递的是基本类型还是引用类型,都会被​​当前方法copy一份当前传入的值​​。引起不同的原因就是copy引起的。假若我们传入基本类型的值,这个时候,值被复制,并不是原来的值,所以我们使用加法,并不会改变main方法当中的基本类型的值。但是引用类型就不一样了,它传递的是一个地址,地址不管怎么复制,是不会变的,而且不改变地址的指向的时候,最终还是修改的那一个对象,当你复制当前地址,更改当前地址指向的值的时候,最终引用的指针也会跟着偏移。

进阶分析

思考:包装类型的值传递最终结果为什么是一样的?

从代码上我们可以看到,我们使用的代替基本类型的就是包装类型BigDecimal,这也是我们这篇文章的知识点提升。如果传递基本类型估计大家都清楚。但是这里是传递的包装类型(引用类型),我们最终看到的结果是,方法的计算并没有改变我们main方法里面的值。

为什么?

首先有一点是确定的,那就是包装类型都属于引用类型,传递值的时候,都是传递的引用。但是值没有因为引用而被更改的原因,很简单:包装类型的值都被使用final进行修饰,final修饰的值不能被修改,这一点我们可以从源码看到

private final BigInteger intVal;
  • BigDecimal只是包装类型中的一种,其他的包装类型也是一样的。

拓展思考,引用传递进阶

我们传递引用类型,假若我对调用test的方法做如下修改,会出现什么样的结果呢?会不会由于我们引用传递,然后对值进行修改,最终main方法中的值被修改了呢?我们看下面的结果截图。

public static void main(String[] args) {
List<BigDecimal> list = new ArrayList<>();
list.add(BigDecimal.ZERO);
System.out.println(list);
test(list);
System.out.println(list);
}

private static void test(List<BigDecimal> list) {
list = new ArrayList<>();
list.add(new BigDecimal("2"));
}

代码调整之后,我们可以看到,原本的list.clear被更改为了list = new ArrayList<>(); 最终的输出结果如下:

基本类型和引用类型值传递问题解析_引用类型_03

  • 根据前面我们传递引用类型值会被更改的思路,我们这里看到的结果却是两个都是0,证明引用传递,值没被更改!

为什么引用传递,值没有被更改?

这个结果相对来说要费点时间来理解,因为这里不仅有值的传递的问题,还有list对值处理的问题。最终的结果没有被更改的原因,其实我们前面已经埋下了伏笔。我们所有方法的参数不管你是基本类型还是引用类型,在被调用的时候,这些参数的值都会被copy一份。相当于在被调用的方法里面使用了副本。如果是基本类型,副本的值无法影响原有的值,但是有可能会影响引用类型的值。


  • 这里没有被影响的原因就是副本的地址,看似跟main方法当中的地址是一模一样的,但是当我们使用list = new ArrayList<>() 之后,副本的地址指向了新的对象,并不是原有的对象。所以我们在list.add的时候,这个值最终加入到了新的对象当中。当然这还不足以解释为什么main方法当中的list为什么没有被影响。
  • 核心原因是,我们System.out.println(list);的时候,list的地址值跟副本是一样的没错,但是main方法当中的list引用指向的是main中间创建的那个list引用。而不是副本里面那个引用。副本里面的引用在new ArrayList的时候就已经和main中的引用不再是同一个了。

扩展思考

为什么用list.clear()可以呢?先来看看list.clear的源码

/**
* Removes all of the elements from this list. The list will
* be empty after this call returns.
*/
public void clear() {
modCount++;

// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;

size = 0;
}

从源码当中我们可以看到clear并没有去新建对象,而是做了两件事,一个是置空了list当中所以的数据。一个是让当前的size等于了0

在之前的list = newArrayList当中,看似相同的两个地址,指向的却不是一个对象,所以打印的时候,获取到的引用却成了关键。这里则不一样。但是也有一样的地方,那就是同样有两个引用,但是指向的地址对象却是一样的,所以最终的操作影响了main方法当中的list


总结:基本类型的值在方法间传递,原有的值不会被影响。引用类型的值在方法间传递,原有的引用类型的值有可能被影响!