一、引用

在 JDK 1.2 版本之前,Java 中的引用是很传统的定义:如果 reference 类型的数据中存储的数值代表的是另一块内存的起始地址,就称该 reference 数据是代表某块内存、某个对象的引用。

这种定义并没有什么不对,只是现在看来有些过于狭隘了,一个对象在这种定义下只有“被引用”或者“未被引用”两种状态,对于描述一些“食之无味,弃之可惜”的对象就显得无能为力。譬如下面这一类对象:当内存空间还足够时,能保留在内存之中,如果内存空间在进行垃圾收集后仍然非常紧张,那就可以抛弃这些对象——很多系统的缓存功能都符合这样的应用场景。

在 JDK 1.2 版本之后,Java 对引用的概念进行了扩充:

  • 强引用,Strongly Reference
  • 软引用,Soft Reference
  • 弱引用,Weak Reference
  • 虚引用,Phantom Reference

这四种引用强度依次减弱。

强引用,是最传统的引用定义,是指在程序代码中普遍存在的引用。无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。

软引用,用来描述一些还有用,但是非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,如果这次回收还没有足够的内存,才会抛出内存溢出异常。

弱引用,也用来描述那些非必须的对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

虚引用,是最弱的一种引用,也可称为”幽灵引用“或”幻影引用“。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。

Java 中声明了如下几个类分别对应上面的引用:

  • Reference,引用的抽象基类,此类定义了所有引用对象通用的操作。
  • SoftReference,软引用
  • WeakReference,弱引用
  • PhantomReference,虚引用
1
2
3
4
5
6
7
/**
* @since 1.2
*/
public abstract sealed class Reference<T>
permits PhantomReference, SoftReference, WeakReference, FinalReference {

}

另外 Reference 类还有一个子类 FinalReference

二、值调用和引用调用

值调用:方法接收的是调用者提供的值。

引用调用:方法接收的事调用者提供的变量地址。

“按……调用”​(call by)是一个标准的计算机科学术语,它用来描述各种程序设计语言(不只是 Java)中方法参数的传递方式(事实上,以前还有按名调用(call by name), Algol 程序设计语言是最古老的高级程序设计语言之一,它使用的就是这种参数传递方式。不过,对于今天,这种传递方式已经成为历史)​。

1、C++ 中的值调用和引用调用

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>

using namespace std;

void increment(int x) {
x++;
}

int main() {
int a = 5;
increment(a);
cout << a << endl;
}

上面执行 increment 方法之后,a 变量的值为 5,increment 方法修改的是副本 x,而不是 a 本身。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>

using namespace std;

void increment(int &x) {
x++;
}

int main() {
int a = 5;
increment(a);
cout << a << endl;
}

上面执行 increment 方法之后,a 变量的值变为 6,因为 increment 修改的是 a 本身的值。

注意上面 increment 方法参数前的 & 符号。

2、Java 的按值调用

Java 中总是采用按值调用。方法得到的是所有参数值的一个拷贝,方法不能修改传递给它的任何参数变量的内容。

但是,需要考虑的是,在 Java 中,方法参数共有两种类型:

  • 基本数据类型
  • 对象引用类型

针对这两种类型,Java 都采用的是按值传递。对于基本数据类型,方法参数传递的是值的拷贝;对于引用类型,方法参数传递的是对象引用的拷贝,原始引用和方法参数副本为两个变量,但都指向堆中的同一个对象。

关于对象引用类型的示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class ReferenceExample {  

public static void main(String[] args) {
MyClass obj = new MyClass();
obj.value = 10;

System.out.println(obj.value);
methodA(obj);
System.out.println(obj.value);

System.out.println();

System.out.println(obj.value);
methodB(obj);
System.out.println(obj.value);
}

public static void methodA(MyClass obj) {
obj.value = 20;
}

public static void methodB(MyClass obj) {
obj = new MyClass();
obj.value = 30;
System.out.println("in method B, obj.value = " + obj.value);
}

static class MyClass {
private int value;
}

}

上面代码的输出结果为:

1
2
3
4
5
6
10
20

20
in method B, obj.value = 30
20

三、基本数据和引用数据类型的区别

基础数据类型的变量中存储的是变量的值,而引用数据类型的变量中存储的是实际对象的引用地址。

在使用 = 时,对于基础数据类型,会直接修改变量的值,原有的值会被覆盖。而引用类型,会改变变量中所保存的地址,原来的地址会被覆盖。

最一开始,有一个地方有点没太注意。方法的参数传递。例如下面这个代码:

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
List<String> list = new ArrayList<>(List.of("hhh", "www"));
List<String> list2 = testList(list);
}

public static List<String> testList(List<String> list) {
list.add("aaa");
list = new ArrayList<>(List.of("111", "222"));
return list;
}

上面的两个方法,都是有自己的方法栈空间的,在两个方法中都声明了方法局部变量 list。在调用 testList() 方法时,是将 main 方法中的 list 变量中存储的对象地址,赋值给了 testList() 方法参数中声明的 list 变量。此时,两个变量指向的是同一个对象。

之后,调用 add 方法,会在 main 中声明的列表中添加元素。之后对 testList() 方法中声明的变量重新赋值为新创建的对象并返回,此时该方法返回的对象为 testList() 方法中新创建的对象,赋值给 main 中的 list2 变量。而 mainlist 变量指向的对象还为最开始创建的列表。

四、其他

我们都知道 JVM 内存模型中有,stack 和 heap 的存在,但是更准确的说,是每个线程都分配一个独享的 stack,所有线程共享一个 heap。对于每个方法的局部变量来说,是绝对无法被其他方法,甚至其他线程的同一方法所访问到的,更遑论修改。

相关链接

这一次,彻底解决Java的值传递和引用传递 - 个人文章 - SegmentFault 思否

Java中的值传递和引用传递的理解(带例子、通俗易懂)-CSDN博客

JAVA:值传递和引用传递-CSDN博客

Java中的值传递和引用传递(详解) - 知乎

OB tags

#Java