Java学习笔记之对象和对象引用

基本概念

对象:

《Java编程思想》:按照通俗的说法,每个对象都是某个类(class)的一个实例(instance)。

引用:

《Java编程思想》: 每种编程语言都有自己的数据处理方式。有些时候,程序员必须注意将要处理的数据是什么类型。你是直接操纵元素,还是用某种基于特殊语法的间接表示(例如C/C++里的指针)来操作对象。所有这些在 Java 里都得到了简化,一切都被视为对象。因此,我们可采用一种统一的语法。尽管将一切都“看作”对象,但操纵的标识符实际是指向一个对象的“引用”(reference)。

上面两段是摘自《Java编程思想》中的两段话,下面我们通过例子进行进一步说明。
先定义一个简单类:

1
2
3
4
5
class People {
String name;
int age;
String sex;
}

通过这个类,我们可以创建对象

1
People tom = new People();

通常把这条语句的动作称之为创建一个对象,其实,它包含了四个动作。

  1. 右边的“new People”,是以People类为模板,在堆空间里创建一个People类对象(也简称为People对象)
  2. 末尾的()意味着,在对象创建后,立即调用People类的构造函数,对刚生成的对象进行初始化。构造函数是肯定有的。如果你没写,Java会给你补上一个默认的构造函数
  3. 左边的“People tom”创建了一个People类引用变量。所谓People类引用,就是以后可以用来指向People对象的对象引用,存储于堆栈中
  4. “=”操作符使对象引用指向刚创建的那个People对象

我们可以把这条语句拆成两部分:

1
2
People tom;
tom = new People();

这样就很清楚的看出,有两个实体:一是对象引用变量,二是对象本身。
在堆中创建的实体,与在数据段及栈空间里创建的实体不同,尽管它们是实实在在存在的实体,但是我们看不见摸不着。因为其没有具体的名字,一般都是通过对象引用指向某个对象,从而进行操作。
为了形象的说明对象与对象引用之间的关系,我们可以将对象比作一只气球,而把对象引用比作是一根绳子;一根绳子可以不系气球,也可以系一个气球;一个气球也可以有多个绳子系住

1
People tony;

上句定义了一个对象引用,但没有指向任何对象

1
tony = tom;

上句进行了一次对象引用的复制,而不是对象的复制,结果使得tonytom指向同一对象

1
tony = new People();

上句代码使得tony这个对象引用重新指向另外一个新的对象。
也就是说,
(1)一个对象引用可以指向0个或1个对象;
(2)一个对象可以有N个引用指向它
当然还有一种特殊情况,如下

1
new People();

该对象没有任何对象引用指向它,这样的对象已成为垃圾回收器处理的对象,至于什么时候被回收,具体就要看垃圾回收器了。

参数传递

把对象和对象引用搞清楚之后,我们再继续下一个比较有意思的话题。前面讲了那么多基础知识,都是为了这个话题准备的 —— 参数传值。

《thinking in Java》:When you’re passing primitives into a method,you get a distinct copy of the primitive. When you’re passing a reference into a method, you get a copy of the reference.

搞懂这段话,有关参数传递的疑惑将不是问题。
在讨论这个话题之前,我们先摆出以下几个事实:

  • 在 Java 中永远不会传递对象,而只传递对象引用
  • Java 有且仅有一种参数传递机制,即按值传递

什么是按值传递?

当将一个参数传递给一个函数时,函数接收的是原始值的一个副本。如果函数修改了该参数,仅改变副本,而原始值保持不变。如果函数修改了该参数,调用代码中的原始值也随之改变。

什么是按引用传递?

当将一个参数传递给一个函数时,函数接收的是原始值的内存地址,而不是值的副本

C++和Java中的参数传递有什么异同?

在 C++ 和 Java 中,当传递给函数的参数不是引用时,传递的都是该值的一个副本(按值传递)。区别在于引用:在 C++ 中当传递给函数的参数是引用时,您传递的就是这个引用,或者内存地址(按引用传递)。在 Java 应用程序中,当对象引用是传递给方法的一个参数时,您传递的是该引用的一个副本(按值传递),而不是引用本身。请注意,调用方法的对象引用和副本都指向同一个对象。这是一个重要区别。Java 应用程序在传递不同类型的参数时,其作法与 C++ 并无不同。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
class value{
public int i = 15;
}
public class dataType {
public static void main(String[] args){
dataType type = new dataType();
type.first();
}

public void first(){
int i = 5;
value v = new value();
v.i = 25;
second(v, i);
System.out.println(v.i);
}

public void second(value v, int i){
i = 0;
v.i = 20;
value val = new value();
v = val;
System.out.println(v.i + " " + i);
}
}

(1)在first内,首先程序在栈内存中开辟了一块地址编号为AD9500内存空间,用于存放v的引用地址,里边放的值是堆内存中的一个地址,示例中的值为BE2500

(2)调用函数second,程序在栈内开辟地址为AD9600内存空间存放v的副本,v的副本同样指向堆地址为BE2500的空间,然后将v的副本传入second,并且在second内,将v的副本所指对象i=25改为i=20

(3)在second内,程序新建一个对象放在地址为BE2600的堆内,并用新的引用val(栈中地址为AD9700)指向它,所以second中输出结果为:15 0

(4)但原v并未改变,改变的只是它传入second的副本,所以在first中仍然输出i=20

String类型和包装器类型的参数传递问题

String对象是不可变的。因为String对象具有只读特性,所以指向它的任何引用都不可能改变它的值,因此,也就不会对其他的引用有什么影响。

例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class StringAsParamOfMethodDemo {
public static void main(String[] srgs){
StringAsParamOfMethodDemo demo = new StringAsParamOfMethodDemo();
demo.test();
}

private void test(){
String originStr = "origin";
System.out.println("Before change:");
System.out.println("Outter String:" + originStr);
changeString(originStr);
System.out.println("After change:");
System.out.println("Outter String:" + originStr);
}

public void changeString(String str){
str = str + "is changed";
System.out.println("Inner String:" + str);
}
}

运行结果:

1
2
3
4
5
Before change:
Outter String:origin
Inner String:originis changed
After change:
Outter String:origin

从运行结果上看,String对象引用作为参数传递时,虽然复制了其引用并赋值给新引用,但对新引用的更改并没有影响到原引用,这表现出了String类型的“非对象特性”。归根到底,对String的存储实际上是通过char[]来实现的,即String相当于是char[]的包装类。包装类的特质之一就是对其值进行操作时会体现出其对应的基本类型的性质。下面再举一个包装器类的例子进行解释。

《Java编程思想》:基本类型具有的包装器类,使得可以在堆中创建一个非基本对象,用来表示对应的基本类型。

包装器类型对象跟传统java对象有着细微的差别,导致其作为参数传递时跟一般对象会不一样。
例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class IntegerAsParamOfMethodDemo {
public static void main(String[] srgs){
IntegerAsParamOfMethodDemo demo = new IntegerAsParamOfMethodDemo();
demo.test();
}

private void test(){
Integer a = new Integer(1);
System.out.println("Before change:");
System.out.println("Outter Integer:" + a);
changeInteger(a);
System.out.println("After change:");
System.out.println("Outter Integer:" + a);
}

public void changeInteger(Integer a){
a = 2;
System.out.println("Inner Integer:" + a);
}
}

运行结果:

1
2
3
4
5
Before change:
Outter Integer:1
Inner Integer:2
After change:
Outter Integer:1

从运行结果上来看,包装类对象引用在作为函数参数传递时表现出对应的基本类型的性质。

参考资料

  1. 对象引用与对象的区别
  2. Java 应用程序中的按值传递语义
  3. 对象和引用
  4. Java基础11 对象引用
  5. String类型的参数传递问题