5.java基础-不可变对象与值传递

★不可变类

不可变类的状态不可以改变

顾名思义,一个类实例化一个对象后,对象的属性无法被改变,可称之为不可变类。

不存在并发修改:线程安全

​ JDK中的八大包装类、String类等都是不可变类,因为其内部的状态不可以改变。

如果一个对象在不能够修改其内部状态(属性),那么它就是线程安全的,因为不存在并发修改啊!

​ 不可变类,实际是另一种避免竞争的方式,因此它们的方法都是线程安全的。

​ String、Integer 等都是不可变类,所谓的不可变类是指这个类的实例一旦创建完成后,就不能改变其成员变量值。

​ 而String类作为我们最常用的类之一,通过字符串常量池大大提升了性能。不可变类因为是不可变的,所以天然具有线程安全性。

​ 有同学或许有疑问,String 有 replace,substring 等方法【可以】改变值啊,那么这些方法又是如何保证线程安全的呢?

但有同学会说,使用字符串时,也有一些跟修改相关的方法啊,比如 substring 等,那么下面就看一看这些方法是如何实现的,就以 substring 为例:

public String substring(int beginIndex) {
if (beginIndex < 0) {
throw new StringIndexOutOfBoundsException(beginIndex);
}
int subLen = value.length - beginIndex;
if (subLen < 0) {
throw new StringIndexOutOfBoundsException(subLen);
}
return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}

//发现其内部是调用 String 的构造方法创建了一个新字符串。
//再进入这个构造看看,是否对 final char[] value 做出了修改:
public String(char value[], int offset, int count) {
    if (offset < 0) {
    	throw new StringIndexOutOfBoundsException(offset);
    }
    if (count <= 0) {
    	throw new StringIndexOutOfBoundsException(count);
    }
    if (offset <= value.length) {
   	 	this.value = "".value;
    	return;
    }
}
if (offset > value.length - count) {
	throw new StringIndexOutOfBoundsException(offset + count);
}
	this.value = Arrays.copyOfRange(value, offset, offset+count);
}
//结果发现也没有,构造新字符串对象时,会生成新的 char[] value,对内容进行复制 。
//这种通过创建副本对象来避免共享的手段称之为【保护性拷贝(defensive copy)】

查看源码发现有以下几个特点:

1. String类被final修饰,不可继承
2. string内部所有成员都设置为私有变量
3. 并将value和offset设置为final。
4. 不存在value的setter
5. 当传入可变数组value[]时,进行copy而不是直接将value[]复制给内部变量.
6. 获取value时不是直接返回对象引用,而是返回对象的copy.

string对象在内存创建后就不可改变,不可变对象的创建一般满足以下5个原则:

1.不提供任何会修改类状态的方法,包括set方法,避免通过set方法改变成员变量的值,破坏不可变特性。

2.使用final修饰符修饰类,保证类不被继承。如果类可以被继承会破坏类的不可变性机制。如果子类覆盖父类的方法并且子类可以改变成员变量值,那么不能保证当前类是不可变。

3.保证所有成员变量必须私有被private修饰,并且加上final。 这种方式保证成员变量不可改变。但只做到这一步还不够,因为如果是对象成员变量,其值有可能在外部被改变。
如果类具有指向可变对象的域,则必须确保该类的使用者无法获得指向这些对象的引用。如构造器传入的对象直接赋值给成员变量,还是可以通过对传入对象的修改进而导致改变内部变量的值。所以还需要保护性拷贝弥补这个不足。

4、保护性拷贝弥补

5、一个类的private方法会隐式被指定为final方法。
public final class ImmutableDemo {
    private final int[] myArray; 
    public ImmutableDemo(int[] array) { 
        this.myArray = array; // wrong 
    } 
}
//这种方式不能保证不可变性,myArray和array指向同一块内存地址。
//用户可以在ImmutableDemo之外通过修改array对象的值来影响myArray内部的值。

保护性拷贝

为了保证内部的值不被修改,可以采用深度copy来创建一个新内存保存传入的值。正确做法:

public final class MyImmutableDemo {  
    private final int[] myArray;  
    public MyImmutableDemo(int[] array) {  
        this.myArray = array.clone();   
    }   
}

​ 这种做法是为了防止对象外泄。防止通过获得内部可变成员对象后对成员变量直接操作,导致成员变量发生改变。

​ 在get方法中,不要直接返回对象本身,而是返回对象的拷贝。这种做法也是防止对象外泄,防止通过get获得内部可变成员对象后对成员变量直接操作,导致成员变量发生改变。

首先,关于类的成员变量(也就是属性),如果它们都不是不可变类,那么它们应该是私有的、final的。通过私有的封装来让属性外部无法修改,而final的作用是让属性初始化后就不能再改变(第3点);

如果类可以被继承会破坏类的不可变性机制,只要继承类覆盖父类的方法并且继承类可以改变成员变量值,那么一旦子类以父类的形式出现时,不能保证当前类是否可变。(第2点);

其次,关于类的方法,我们不能提供任何修改属性的方法(第1点),比如常见的setXXX方法就不能再出现了。

如果类成员变量本身不是不可变的,那么需要注意两点:

1、在初始化(比如构造器里)给该成员变量赋值时,应该取的是外部对象引用的克隆(第5点);

2、如果不可变类提供给外部用于获取该成员变量的方法时,应该使用该成员变量的克隆,而非直接返回成员变量本身。这两点的目的都是避免外部通过对象引用修改不可变类的内部成员变量(第5点)。

具体参考:

https://www.cnblogs.com/wuxun1997/p/10970717.html

https://www.cnblogs.com/jaylon/p/5721571.html

创建不可变类的五个条件

那么如何定义一个类为不可变类呢?要使类成为不可变类,遵循以下5条规则:

1.不要提供任何会修改类属性的方法;

2.保证类不会被继承;

3.使所有值域都为final;

4.使所有值域都成为private私有;

5.如果类具有指向可变对象的属性。则必须确保该类的使用者无法获得指向这些对象的引用。

​ 接下来我们从另一个角度来仔细过一下上面的规则:

首先,关于类的成员变量(也就是属性),如果它们都是可变类,那么它们应该是私有的、final的。通过私有的封装来让属性外部无法修改,而final的作用是让属性初始化后就不能再改变(第3、4点);

其次,关于类的方法,我们不能提供任何修改属性的方法(第1点),比如常见的setXXX方法就不能再出现了。

​ 如果类成员变量本身是可变的,那么需要注意两点:

​ 1.在初始化(比如构造器里)给该成员变量赋值时,应该取的是外部对象引用的克隆;

​ 2.如果不可变类提供给外部用于获取该成员变量的方法时,应该使用该成员变量的克隆,而非直接返回成员变量本身。

这两点的目的都是避免外部通过对象引用修改不可变类的内部成员变量(第5点)。

1.final修饰类,保证类不能继承,类中方法不能重写
2.private final修饰成员变量

1.为什么要用final修饰?

final修饰成员变量,可以被继承。但是不能被修改。不能被修改包含2个方面:

1.基本类型

值不能被修改。

2.引用类型

引用不能被修改。但是引用对象的值是可以被修改。

属性用 final 修饰保证了

1.属性在构造方法完成之前完成初始化,并且该属性只能被初始化1次

2.不能进行第2次显式的赋值。

举个例子

package base01;

import java.util.Date;

public class Immutable {
    private String name;
  	 public Immutable(String name, Date date) {
        this.name = name;
    }
    public static void main(String[] args) {
        String name = "a";
        Immutable t = new Immutable(name);
        //修改成功! 如果使用final修饰 在成员变量被初始化以后就无法被显式赋值
        t.name = "c";
    }
}

2.为什么还要用private修饰呢?

final的作用是让属性初始化后就不能再改变,而private通过私有的封装来让属性在外部无法修改

其实这里解释的有些勉强,只能说属性被私有是一般化的常态化的。这是一种规范:不能通过对象直接操作对象的属性,而是通过对象的方法获取对象属性的引用再去操作。

3.不提供改变成员变量的方法,包括set

防止通过set方法修改不可变类的成员变量

4.通过构造器初始化所有成员,进行深拷贝

防止通过构造方法传递引用构造对象,然后通过引用修改不可变类的成员变量。

这里的date引用是外部传进来的,外部可以在构造对象完成后,继续改变date的属性值。

public class Immutable {
    private  String name;
    private final Date date;
    public Immutable(String name, Date date) {
        this.name = name;
        this.date = date;
    }

    public String getName() {
        return name;
    }

    public Date getDate() {
        return date;
    }

    @Override
    public String toString() {
        return "Immutable{" +
                "name='" + name + '\'' +
                ", date=" + date +
                '}';
    }
}
    public static void main(String[] args) {
        String name = "a";
        Date today = new Date();

        Immutable t = new Immutable(name, today);
        System.out.println("Start Change: " + t.toString());

        //构造器初始化方法漏洞
        //String是不可变类,所以没有影响
        //Date是可变类所以需要注意。
        name = t.getName();
        name = "b";
        today.setTime(today.getTime() + 1000 * 60 * 60);
        System.out.println("After Change: " + t.toString());

        //设置值方法漏洞
        //String和Date都受到了影响
        today = t.getDate();
        t.name = "c";
        today.setTime(today.getTime() + 1000 * 60 * 60);
        System.out.println("After Change: " + t.toString());
    }
Start Change: Immutable{name='a', date=Tue May 10 09:20:15 CST 2022}
After Change: Immutable{name='a', date=Tue May 10 10:20:15 CST 2022}
After Change: Immutable{name='c', date=Tue May 10 11:20:15 CST 2022}

但是对于String来说String是不可变对象,是不会发生这种问题的。对于不可变对象来说,你是无法改变它的属性值的。所以这里是不需要担心的。但是最好还是一起改下:

public Immutable3(String name, Date date) {
        //这里注意String 是不可变的,所以是安全的
        //但是还是建议使用:
        this.name = new String(name);
        this.date = (Date) date.clone();
    }
5.在getter方法中,不返回对象本身,返回对象拷贝
    public String getName() {
        return name;
    }

    public Date getDate() {
        return date;
    }
  public String getName() {
        return new String(name);
    }

    public Date getDate() {
        return (Date) date.clone();
    }

创建不可变类

详见下面例子:

package base01;

import java.util.Date;

public class Immutable {
    private String name;
    private final Date date;

    public Immutable(String name, Date date) {
        this.name = name;
        this.date = date;
    }

    public String getName() {
        return name;
    }

    public Date getDate() {
        return date;
    }

    @Override
    public String toString() {
        return "Immutable{" +
                "name='" + name + '\'' +
                ", date=" + date +
                '}';
    }

    public static void main(String[] args) {
        String name = "a";
        Date today = new Date();

        Immutable t = new Immutable(name, today);
        System.out.println("Start Change: " + t.toString());

        //构造器初始化方法漏洞
        //String是不可变类,所以没有影响
        //Date是可变类所以需要注意。
        name = t.getName();
        name = "b";
        today.setTime(today.getTime() + 1000*60*60);
        System.out.println("After Change: " + t.toString());

        //设置值方法漏洞
        //String和Date都受到了影响
        today = t.getDate();
        t.name = "c";
        today.setTime(today.getTime() + 1000*60*60);
        System.out.println("After Change: " + t.toString());
    }
}

运行结果:

Start Change: Immutable{name='a', date=Tue May 10 08:46:19 CST 2022}
After Change: Immutable{name='a', date=Tue May 10 09:46:19 CST 2022}
After Change: Immutable{name='c', date=Tue May 10 10:46:19 CST 2022}

我们看到String是不可变类,所以我们很放心。而Date并非不可变类,所以它变了。

但是事实并非如此,String对象由于我们可以直接拿到对象的t.name的引用。

所以还是可以操作对象的name属性。

我们通过克隆来让对象的引用不被外部操纵:

package base01;

import java.util.Date;

public class Immutable2 {
    private String name;
    private final Date date;

    public Immutable2(String name, Date date) {
        this.name = name;
        this.date = (Date) date.clone();
    }

    public String getName() {
        return name;
    }

    public Date getDate() {
        return (Date) date.clone();
    }

    @Override
    public String toString() {
        return "Immutable2{" +
                "name='" + name + '\'' +
                ", date=" + date +
                '}';
    }

    public static void main(String[] args) {
        String name = "a";
        Date today = new Date();

        Immutable2 t = new Immutable2(name, today);
        System.out.println("Start Change: " + t.toString());

        //构造器初始化方法漏洞
        //String是不可变类,所以没有影响
        //Date是可变类所以需要注意。
        name = t.getName();
        name = "b";
        today.setTime(today.getTime() + 1000 * 60 * 60);
        System.out.println("After Change: " + t.toString());

        //设置值方法漏洞
        //String和Date都受到了影响
        today = t.getDate();
        t.name = "c";
        today.setTime(today.getTime() + 1000 * 60 * 60);
        System.out.println("After Change: " + t.toString());
    }
}
Start Change: Immutable2{name='a', date=Tue May 10 08:53:30 CST 2022}
After Change: Immutable2{name='a', date=Tue May 10 08:53:30 CST 2022}
After Change: Immutable2{name='c', date=Tue May 10 08:53:30 CST 2022}

发现String类型的name还是被修改了。参考Date类型的数据,我们也可以返回1个新的对象。

package base01;

import com.sun.xml.internal.stream.events.NamedEvent;

import java.util.Date;

public class Immutable3 {
    private final String name;
    private final Date date;

    public Immutable3(String name, Date date) {
        //这里注意String 是不可变的,所以是安全的
        //但是还是建议使用:
        this.name = new String(name);
        this.date = (Date) date.clone();
    }

    public String getName() {
        return new String(name);
    }

    public Date getDate() {
        return (Date) date.clone();
    }

    @Override
    public String toString() {
        return "Immutable3{" +
                "name='" + name + '\'' +
                ", date=" + date +
                '}';
    }

    public static void main(String[] args) {
        String name = "a";
        Date today = new Date();

        Immutable3 t = new Immutable3(name, today);
        System.out.println("Start Change: " + t.toString());

        //构造器初始化方法漏洞
        //String是不可变类,所以没有影响
        //Date是可变类所以需要注意。
        name = t.getName();
        name = "b";
        today.setTime(today.getTime() + 1000 * 60 * 60);
        System.out.println("After Change: " + t.toString());

        //设置值方法漏洞
        //String和Date都受到了影响
        today = t.getDate();
        //加了final 就不能再显式初始化赋值了
        //t.name = "c";
        today.setTime(today.getTime() + 1000 * 60 * 60);
        System.out.println("After Change: " + t.toString());
    }
}
Start Change: Immutable3{name='a', date=Tue May 10 09:00:05 CST 2022}
After Change: Immutable3{name='a', date=Tue May 10 09:00:05 CST 2022}
After Change: Immutable3{name='a', date=Tue May 10 09:00:05 CST 2022}

String对象的不可变性的优点

从上一节分析,String数据不可变类,那设置这样的特性有什么好处呢?我总结为以下几点:

1.字符串常量池的需要。

字符串常量池可以将一些字符常量放在常量池中重复使用,避免每次都重新创建相同的对象、节省存储空间。但如果字符串是可变的,此时相同内容的String还指向常量池的同一个内存空间,当某个变量改变了该内存的值时,其他遍历的值也会发生改变。所以不符合常量池设计的初衷。

2.线程安全考虑。

同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。

3.类加载器使用字符串,不可变保证正确加载。

类加载器使用字符串,提供安全性,不可变保证正确加载。

譬如你想加载java.sql.Connection类,而这个值被改成了myhacked.Connection,那么会对你的数据库造成不可知的破坏。

4.支持hash映射和缓存。

因为字符串是不可变的,所以在它创建的时候hashcode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串。

String对象的不可变性的缺点

如果有对String对象值改变的需求,那么会创建大量的String对象。

String对象不是真的不可变

虽然String对象将value设置为final,并且还通过各种机制保证其成员变量不可改变。但是还是可以通过反射机制的手段改变其值。例如:

//创建字符串"Hello World", 并赋给引用s

  String s = "Hello World"; 

  System.out.println("s = " + s); //Hello World
//获取String类中的value字段

  Field valueFieldOfString = String.class.getDeclaredField("value");

  //改变value属性的访问权限

  valueFieldOfString.setAccessible(true);

  //获取s对象上的value属性的值

  char[] value = (char[]) valueFieldOfString.get(s);

  //改变value所引用的数组中的第5个字符

  value[5] = '_';

  System.out.println("s = " + s);  //Hello_World

打印结果为:

s = Hello World

s = Hello_World

发现String的值已经发生了改变。也就是说,通过反射是可以修改所谓的“不可变”对象的

总结

不可变类是实例创建后就不可以改变成员遍历的值。这种特性使得不可变类提供了线程安全的特性.

但同时也带来了对象创建的开销,每更改一个属性都是重新创建一个新的对象。

JDK内部也提供了很多不可变类如Integer、Double、String等。

String的不可变特性主要为了满足常量池、线程安全、类加载的需求。

合理使用不可变类可以带来极大的好处。

★JAVA是值传递

值传递:会创建副本

引用传递:不会创建副本

值传递和引用传递的误区

错误理解一:值传递和引用传递,区分的条件是传递的内容,如果是个值,就是值传递。如果是个引用,就是引用传递。

错误理解二:Java是引用传递。

错误理解三:传递的参数如果是普通类型,那就是值传递,如果是对象,那就是引用传递。

我们都知道,在Java中定义方法的时候是可以定义参数的。比如Java中的main方法:

public static void main(String[] args)

这里面的args就是参数。参数在程序语言中分为形式参数和实际参数。

形式参数:是在定义函数名和函数体的时候使用的参数,目的是用来接收调用该函数时传入的参数。

实际参数:在调用有参函数时,主调函数和被调函数之间有数据传递关系。

在主调函数中调用一个函数时,函数名后面括号中的参数称为“实际参数”。

public static void main(String[] args) {
    ParamTest pt = new ParamTest();
    String parameter = "Hollis";
    //实际参数为 parameter
    pt.sout(parameter);
}

//形式参数为 name
public void sout(String name) { 
    System.out.println(name);
}

实际参数是调用有参方法的时候真正传递的内容,而形式参数是用于接收实参内容的参数。

值传递(pass by value)是指在调用函数时将实际参数parameter复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。

**引用传递(pass by reference)是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。** 

img

值传递:你有一把钥匙,当你的朋友想要去你家的时候,你复刻了一把新钥匙给他,自己的还在自己手里,这就是值传递。这种情况下,他对这把新钥匙做什么都不会影响你手里的这把钥匙。

引用传递你有一把钥匙,当你的朋友想要去你家的时候,如果你直接把你的钥匙给他了,这就是引用传递。这种情况下,如果他对这把钥匙做了什么事情,比如他在钥匙上刻下了自己名字,那么这把钥匙还给你的时候,你自己的钥匙上也会多出他刻的名字。

钥匙就是栈内地址,房子就是堆中的对象。

但是,不管上面那种情况,你的朋友拿着你给他的钥匙,进到你的家里,把你家的电视砸了。

那你说你会不会受到影响?

而我们在pass方法中,改变user对象的name属性的值的时候,不就是在“砸电视”么。

什么是形参?什么是实参?

package base03_值传递;

public class ValueTransfer {

    private String name;
    private int age;

    public ValueTransfer(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "ValueTransfer{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    public static void main(String[] args) {
        String name1 = "a";
        pass(name1);
        System.out.println(name2);
        ValueTransfer v = new ValueTransfer("a", 1);
        pass(v);
        System.out.println(v);
        ValueTransfer v2 = new ValueTransfer("a", 1);
        pass2(v2);
        System.out.println(v2);
    }

 
    public static void pass(String name2) {
        //相当于 new String("b");
        //这个可以通过pass2验证
        //和这个方法是同样的结果,不会影响实际参数。
        //然后再把这个新创建的字符串的引用交给name。
        //这个时候 main方法中name字符串指向的是 a
        //这个时候 main方法中name字符串指向的是 b
        name2 = "b";
        //方法执行完毕name被回收
    }

    public static void pass(ValueTransfer v) {
        v.name = "b";
        v.age = 2;
    }

    public static void pass2(ValueTransfer v) {
        v = new ValueTransfer("c", 3);
         v.age=4;
    }
}
/***
结果:
a
ValueTransfer{name='b', age=2}
ValueTransfer{name='a', age=1}
***/



/***
如果是值传递
假设name2的地址值是 0x123456
假设name1的地址值是 0x456789
那么形参和实参的地址值是不是指向了同一个对象?
答案:是,因为形参是实参地址的拷贝 形参和实参的地址值可能不一样,但是指向的对象是同1个。

对于方法pass(String name2)
String类的存储是通过final修饰的char[]数组来存放结果的,不可更改。一旦定义就是最终形态,任何试图改变String值的操作都只能重新开辟地址。
故当外部一个String类型的引用name1传递到方法内部时,只是把“外部String实例对象”的引用传递给了方法参数变量name2,使得外部String类型变量name1和方法参数变量name2都是实际char[]数组的引用而已。
当我们在方法体中改变name2时,因为char[]数组是不可变的,故每次修改都会导致创建一个新的String实例对象,而方法体中的方法参数name2就会指向这个新创建的String实例对象,而非指向原来外部的String实例对象了。故从方法执行前到方法执行后,外部String类型的引用name1始终指向原String实例对象。

但是对于方法pass(ValueTransfer v),对象的引用也有2个,同时指向了同一个内存地址的堆内存里的对象,当堆内存中的数据发生改变,
也就意味着这2个引用指向的堆内存里的对象发生了改变。

但是对于方法pass2(ValueTransfer v),对象的引用也有2个,同时指向了同一个内存地址的堆内存里的对象,当执行代码:
v = new ValueTransfer("c", 3);
意味着这2个引用指向的堆内存里的对象已经不是同1个对象了。
所以即使我们执行
 v.age=4;
也不会影响到外部的实际参数对象。

***/
package study;

public class User {
    String gender;
    String name;

    public void setGender(String gender) {
        this.gender = gender;
    }

    public String getGender() {
        return gender;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    @Override
    public String toString() {
        return "User{" +
                "gender='" + gender + '\'' +
                ", name='" + name + '\'' +
                '}';
    }
}

package study;

/***
 * 值传递(pass by value)是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
 * 引用传递(pass by reference)是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
 */
public class 值传递还是引用传递 {
    public static void main(String[] args) {
        值传递还是引用传递 pt = new 值传递还是引用传递();

        System.out.println(" ------------基本类型------------------ ");
        int i =20;
        System.out.println("print in main , i is " + i);
        pt.pass(i);
        System.out.println("print in main , i is " + i);

        System.out.println(" -------------引用类型1----------------- ");
        String name = "Hollis";
        System.out.println("print in main , name is " + name);
        pt.pass(name);
        System.out.println("print in main , name is " + name);

        System.out.println(" --------------引用类型2---------------- ");
        User hollis = new User();
        hollis.setName("Hollis");
        hollis.setGender("Male");
        System.out.println("print in main , hollis is " + hollis);
        pt.pass(hollis);
        System.out.println("print in main , hollis is " + hollis);


        System.out.println(" --------------引用类型3---------------- ");
        User hollis2 = new User();
        hollis2.setName("Hollis");
        hollis2.setGender("Male");
        System.out.println("print in main , tyrant is " + hollis2);
        pt.passAndNew(hollis2);
        System.out.println("print in main , tyrant is " + hollis2);

        System.out.println(" --------------引用类型4---------------- ");
        User hollis3 = new User();
        hollis3.setName("Hollis");
        hollis3.setGender("Male");
        System.out.println("print in main , tyrant is " + hollis3);
        pt.pass(hollis3);
        System.out.println("print in main , tyrant is " + hollis3);
    }

    public void pass(int j) {
        j=100;
        System.out.println("print in pass , j is " + j);
    }

    public void pass(String name) {
        //因为String是不可变对象
        //这里相当于new了一个String赋值给了形参
        //形参和实参指向的不是一个对象
        //修改不会影响实参属性
        
        //可以理解为name这个形参 和 实参指向了同1个对象 a
        //但是给name赋值的时候 ,由于String是不可变对象
        //因此 name = ("hollischuang"); 相当于  name = new String("hollischuang");
        //此时 name 相当于指向了 1个新对象 b
        name = ("hollischuang");
        System.out.println("print in pass , name is " + name);
    }



    public void passAndNew(User user) {
        //这个和String一样
        //形参 实参指向的不是同1个对象的引用
        //不会影响实参的值
        user = new User();
        user.setName("hollischuang");
        user.setGender("female");
        System.out.println("print in pass , user is " + user);
    }

    public void pass(User user) {
        //形参 实参指向1个对象的引用
        //修改会影响实参属性
        user.setName("hollischuang");
        System.out.println("print in pass , user is " + user);
    }

}

 ------------基本类型------------------ 
print in main , i is 20
print in pass , j is 100
print in main , i is 20
 -------------引用类型1----------------- 
print in main , name is Hollis
print in pass , name is hollischuang
print in main , name is Hollis
 --------------引用类型2---------------- 
print in main , hollis is User{gender='Male', name='Hollis'}
print in pass , user is User{gender='Male', name='hollischuang'}
print in main , hollis is User{gender='Male', name='hollischuang'}
 --------------引用类型3---------------- 
print in main , tyrant is User{gender='Male', name='Hollis'}
print in pass , user is User{gender='female', name='hollischuang'}
print in main , tyrant is User{gender='Male', name='Hollis'}
 --------------引用类型4---------------- 
print in main , tyrant is User{gender='Male', name='Hollis'}
print in pass , user is User{gender='Male', name='hollischuang'}
print in main , tyrant is User{gender='Male', name='hollischuang'}

以引用类型2为例

img

在参数传递的过程中,实际参数的地址0X1213456被拷贝给了形参,在这个方法中,并没有对形参本身进行修改,而是修改的形参持有的地址中存储的内容。即堆内存中的内容。

所以,值传递和引用传递的区别并不是传递的内容。而是实参到底有没有被复制一份给形参。在判断实参内容有没有受影响的时候,要看传的的是什么,**如果你传递的是个地址,那么就看这个地址的变化会不会有影响,而不是看地址指向的对象的变化。**就像钥匙和房子的关系。钥匙就是栈内地址,房子就是堆中的对象。

以引用类型1为例

既然这样,为啥上面同样是传递对象,引用类型1和引用类型2的结果不一样呢?即传递的String对象和User对象的表现结果不一样呢?

一开始形参和实参指向的是同一个对象。

String name = "Hollis";

在调用方法时,因为String是不可变对象

name = ("hollischuang");

这里相当于new了一个String赋值给了形参

这时形参和实参指向的不是一个对象,所以实参对象的值还是Hollis

我们在引用类型1中的pass方法中使用name = “hollischuang”;

试着去更改name的值,阴差阳错的直接改变了形参name的引用的地址。

因为这段代码,会new一个String,在把引用交给name。

name = “hollischuang”;等价于name = new String(“hollischuang”);

而原来的那个”Hollis”字符串还是由实参持有着的,所以,并没有修改到实际参数的值。

img

以引用类型3为例

img

稍微解释下这张图,当我们在main中创建一个User对象的时候,在堆中开辟一块内存,其中保存了name和gender等数据。然后hollis持有该内存的地址0x123456(图1)。当尝试调用pass方法,并且hollis作为实际参数传递给形式参数user的时候,会把这个地址0x123456交给user,这时,user也指向了这个地址(图2)。然后在pass方法内对参数进行修改的时候,即user = new User();,会重新开辟一块0X456789的内存,赋值给user。后面对user的任何修改都不会改变内存0X123456的内容(图3)。

上面这种传递是什么传递?肯定不是引用传递,如果是引用传递的话,在user=new User()的时候,实际参数的引用也应该改为指向0X456789,但是实际上并没有

通过概念我们也能知道,这里是把实际参数的引用的地址复制了一份,传递给了形式参数。所以上面的参数其实是值传递,把实参对象引用的地址当做值传递给了形式参数。

所以说,Java中其实还是值传递的,只不过对于对象参数,值的内容是对象的引用。

还有一个问题就是等号左边的内容是栈内存中的内容。也就是说 实参和形参指向了同一个内存的地址。

在引用类型1中,String被重新赋值,相当于new String 赋值给了形参。但是实际参数的指针还是指向原来的String。所以经过pass方法不会改变。

在引用类型2中,User的name属性被重新赋值。实参和形参同时指向的是同一个内存的地址。因此影响到了实参的内容。

在引用类型3中,新创建了一个User。实参和形参同时指向的不是同一个内存的地址。因此影响不到实参的内容。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值