04. Java学习补充 – 值传递和引用传递、对象的拷贝
前言
值传递:对形参的修改不会影响到实参
引用传递:对实参的修改会影响到实参
一、值传递与引用传递
在了解值传递与引用传递前需要先明白两个概念:实参和形参。
实参和形参
- 实参:传递给方法的实际参数,在方法外面
- 形参:在方法定义时声明的参数,在方法里面
// 例子:
public class PassByValue {
public static void main(String[] args) {
int number = 10; // 实参
change(number);
}
public static void change(int number){
System.out.println(number); // 形参
}
}
接下来看两个例子:
// 例子1:
public class PassByValue {
public static void main(String[] args) {
int number = 10;
change(number); // 11
System.out.println(number); // 10
}
public static void change(int number){
number += 1;
System.out.println(number);
}
}
// 例子2:
public class PassByReference {
public static void main(String[] args) {
Cat cat = new Cat();
cat.setName("三花");
cat.setColor("黑白黄");
changeCat(cat); // Cat{name='大橘', color='橘色'}
System.out.println(cat); // Cat{name='大橘', color='橘色'}
}
public static void changeCat(Cat cat){
cat.setName("大橘");
cat.setColor("橘色");
System.out.println(cat);
}
}
public class Cat {
private String name;
private String color;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
@Override
public String toString() {
return "Cat{" +
"name='" + name + '\'' +
", color='" + color + '\'' +
'}';
}
}
两个结果的差异就涉及到了值传递和引用传递。
用最通俗易懂的说法,值传递和引用传递的区别就是,形参的修改会不会影响到实参。
值的传递(基本数据的传递 包括字符串)。它传递的并不是值的本身。传递的只是值的副本(拷贝)。
将这个副本传递给形式参数之后,修改的只是值的副本内容,并不是值的本身。
引用传递传递的是引用类型数据的内存地址。将内存地址传递给changeCar方法的形式参数里面之后,changeCar方法内部修改的数据,其实就是对当前内存地址指向的对象进行修改。所以修改之后,数据会发生变化。
再来看第一个例子。
在Java中基本数据类型和局部变量存放在栈中,在传参的时候将数据创建了一个副本传到了方法中,方法在修改的时候只修改了副本中的内容,并不是值的本身。
而第二个例子中。
Java中引用数据类型对象存储在堆中,栈中存储的是指向堆的地址。而传参时传递的副本其实是堆的地址,只不过副本和值本身都指向了同一个堆地址,changeCat方法内部修改的数据,其实就是对当前内存地址指向的对象进行修改。所以修改之后,数据会发生变化。
在学习这部分内容的时候我在网上看到了一个观点“Java没有引用传递,只有值传递”。但是,我并不是很理解这个观点。
用来证明的例子为下面这个:
public class Test {
public static void main(String[] args) {
A a1 = new A("hello", 22);
System.out.println(a1); // A{name='hello', age=22}
change(a1);
System.out.println(a1); // A{name='hello', age=22}
}
public static void change(A a2) {
a2 = new A("world", 23);
}
}
根据我查的资料,Java、C和C#对于引用传递的描述大致意思是相同的,都是:引用传递时,系统不是将实参本身的值复制后传递给形参,而是将其引用值(即地址值)传递给形参。因此,形参所引用的该地址上的变量与传递的实参相同,方法体内相应形参值得任何改变都将影响到作为引用传递的实参。
但是,在这个例子中,a1先将地址传递给了a2,而在change中又给a2重新new了一个新对象,这时a1和a2肯定就不指向同一个地址了,这样打印a1一定不会变。
我把同样的代码用C#尝试了一下,结果和Java相同,难道是因为C#也没有引用传递吗?我不太明白。
internal class Program
{
static void Main(string[] args)
{
A a1 = new A("hello", 22);
Console.WriteLine(a1); // name: hello age:22
change(a1);
Console.WriteLine(a1); // name: hello age:22
}
public static void change(A a2)
{
a2 = new A("world", 23);
}
}
二、对象的拷贝
**浅拷贝:**只复制指向某个对象的地址,而不是复制对象本身,新旧对象共享同一块内存
**深拷贝:**复制并创建一个一模一样的对象,不共享内存,修改新对象,旧对象保持不变
1. 浅拷贝
如果我们要使用浅拷贝,拷贝涉及到的对象,必须要实现Cloneable接口
1.1. 浅拷贝的实现
浅拷贝的实现方式:
1、在所涉及的类上面实现Cloneable接口
2、在需要被克隆的对象所属的类里面重写clone方法(访问修饰符必须使用public)
3、调用clone方法,实现对象的克隆
// 测试浅拷贝:
public class ShallowCopy {
public static void main(String[] args) throws CloneNotSupportedException {
Cat cat = new Cat("大橘", 2);
Person person = new Person(1001, "L", cat);
// 克隆Person对象
Person clonePerson = (Person) person.clone();
System.out.println("原对象:" + person); // 原对象:Person{id=1001, name='L', cat=Cat{name='大橘', age=2}}
System.out.println("克隆对象:" + clonePerson); // 克隆对象:Person{id=1001, name='L', cat=Cat{name='大橘', age=2}}
System.out.println(person == clonePerson); // false
// 修改克隆对象
cat.name = "三花";
cat.age = 1;
clonePerson.id = 1002;
clonePerson.name = "Z";
clonePerson.cat = cat;
System.out.println("原对象:" + person); // 原对象:Person{id=1001, name='L', cat=Cat{name='三花', age=1}}
System.out.println("克隆对象:" + clonePerson); // 克隆对象:Person{id=1002, name='Z', cat=Cat{name='三花', age=1}}
}
}
// Cat
public class Cat implements Cloneable {
public String name;
public int age;
public Cat(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Cat{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
// Person
public class Person implements Cloneable {
public int id;
public String name;
public Cat cat;
public Person(int id, String name, Cat cat) {
this.id = id;
this.name = name;
this.cat = cat;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return (Person) super.clone(); // super.clone() 调用父类的clone方法;
}
@Override
public String toString() {
return "Person{" +
"id=" + id +
", name='" + name + '\'' +
", cat=" + cat +
'}';
}
}
总结:
- 1、克隆出来的对象和原来的对象并不是同一个对象
- 2、如果我们对克隆对象的属性进行修改,如果修改的基本类型的属性和字符串类型的属性,对原对象的基本类型的属性和字符串类型的属性是没有影响的。如果我们修改克隆对象的引用对象类型的属性,会影响原对象的对象类型属性的属性值
2. 深拷贝
深拷贝所涉及到的类必须定义构造函数。除拷贝涉及到的对象需要实现 Cloneable 接口外,在对象中引用的其他对象也需要实现 Cloneable 接口,并重写 Object 类的 clone() 方法
2.1. 深拷贝的实现
// 测试深拷贝:
public class DeepCopy {
public static void main(String[] args) throws CloneNotSupportedException {
Cat cat = new Cat("大橘", 2);
Person person = new Person(1001, "L", cat);
// 通过深拷贝,创建克隆对象
Person clonePerson = (Person) person.clone();
System.out.println(person == clonePerson); // false
System.out.println(person); // Person{id=1001, name='L', cat=Cat{name='大橘', age=2}}
System.out.println(clonePerson); // Person{id=1001, name='L', cat=Cat{name='大橘', age=2}}
// 修改克隆对象
clonePerson.id = 1002;
clonePerson.name = "Z";
clonePerson.cat.name = "三花";
clonePerson.cat.age = 3;
System.out.println(person); // Person{id=1001, name='L', cat=Cat{name='大橘', age=2}}
System.out.println(clonePerson); // Person{id=1002, name='Z', cat=Cat{name='三花', age=3}}
}
}
// Cat
public class Cat implements Cloneable {
public String name;
public int age;
public Cat(String name, int age) {
this.name = name;
this.age = age;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public String toString() {
return "Cat{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
// Person
public class Person implements Cloneable {
public int id;
public String name;
public Cat cat;
public Person(int id, String name, Cat cat) {
this.id = id;
this.name = name;
this.cat = cat;
}
/*@Override // 浅拷贝
protected Object clone() throws CloneNotSupportedException {
return (Person) super.clone(); // super.clone() 调用父类的clone方法;
}*/
@Override // 重写clone方法
protected Object clone() throws CloneNotSupportedException {
Person clonePerson = (Person) super.clone();
clonePerson.cat=(Cat) this.cat.clone();
return clonePerson;
}
@Override
public String toString() {
return "Person{" +
"id=" + id +
", name='" + name + '\'' +
", cat=" + cat +
'}';
}
}
总结:
深拷贝克隆出来的对象,如果对象的属性发生变化(不管是基本类型数据的值还是引用类型数据的值),都不会影响原对象的属性值