JAVA不可变类与可变类、值传递与引用传递深刻理解

  

  一个由try...catch...finally引出的思考,在前面已经初步了解过不可变与可变、值传递与引用传递,在这里再次深刻理解。html

1.先看下面一个try..catch..finally的例子:

Person.javajava

package cn.qlq.test;

public class Person {
    private int age;
    private String name;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

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

    @Override
    public String toString() {
        return "Person [age=" + age + ", name=" + name + "]";
    }

}

 

package cn.qlq.test;

public class FinallyTest {

    public static void main(String[] args) {
        System.out.println(test1());
        System.out.println(test2());
    }

    public static String test1() {
        String s = "s1";
        try {
            int i = 1 / 0;
            s = "s2";
            return s;
        } catch (Exception e) {
            s = "s3";
            return s;
        } finally {
            s = "s4";
        }
    }

    public static Person test2() {
        Person p = new Person();
        p.setName("old");
        try {
            int i = 1 / 0;
            return p;
        } catch (Exception e) {
            p.setName("exception");
            return p;
        } finally {
            p.setName("finally");
        }
    }
}

结果:面试

s3
Person [age=0, name=finally]数组

 

总结:安全

  finally块的语句在try或catch中的return语句执行以后返回以前执行且finally里的修改语句可能影响也可能不影响try或catch中return已经肯定的返回值,若是返回值类型为传址类型,则影响;传值类型(8种基本类型)与8种基本数据类型的包装类型与String(不可变类)不影响。若finally里也有return语句则覆盖try或catch中的return语句直接返回。app

 

面试宝典解释的缘由以下:ide

  程序在执行到return时首先会把返回值存到一个指定的位置(JVM中的slot),其次与执行finally块,最后再返回。若是finally中有return语句会以finally的return为主,至关于普通程序中的return结束函数。若是没有return语句,则会在finally执行完以后弹出slot存储的结果值而且返回,若是是引用类型则finally修改会影响结果,若是是基本数据类型或者不可变类不会影响返回结果。函数

补充:两个例子对上面很好的解释测试

(1)对不可变类不影响ui

package cn.xm.exam.test;

public class Test2 {
    public static void main(String[] args) {
        System.out.println(changeInteger());
    }

    private static Integer changeInteger() {
        Integer result = 1;
        try {
            int i = 1 / 0;
            return result;
        } catch (Exception e) {
            result = 2;
            return result;
        } finally {
            result = 3;
        }
    }

}

结果:  2

 

    public static void main(String[] args) {
        System.out.println(getValue());
    }

    @SuppressWarnings("finally")
    private static int getValue() {
        try {
            return 0;
        } finally {
            return 1;
        }
    }

结果:

1

 

(2)对引用类型可变类影响结果

package cn.xm.exam.test;

public class Test2 {
    public static void main(String[] args) {
        System.out.println(changeInteger());
    }

    private static StringBuilder changeInteger() {
        StringBuilder stringBuilder = new StringBuilder();
        try {
            int i = 1 / 0;
            stringBuilder.append("1");
            return stringBuilder;
        } catch (Exception e) {
            stringBuilder.append("2");
            return stringBuilder;
        } finally {
            stringBuilder.append("3");
        }
    }

}

结果:

23

 

2.值传递与引用传递

1)值传递:方法调用时,实际参数把它的值传递给对应的形式参数,形式参数只是用实际参数的值初始化本身的存储单元内容,是两个不一样的存储单元,因此方法执行中形式参数值的改变不影响实际参数的值。

2)引用传递(指针传递):也称为传地址。方法调用时,实际参数是对象(或数组),这时实际参数与形式参数指向同一个地址,在方法执行中,对形式参数的操做实际上就是对实际参数的操做,因此方法执行中形式参数的改变将会影响实际参数。引用类型若是另外一个采用new以后二者会指向不一样的对象,也就不会再关联。

 

注意:

在Java中,原始数据类型在传递参数时都是按值传递,而包装类型在传递参数是是按引用传递,但包装类型在进行计算的时候会自动拆箱。

对象在函数调用传参的时候是引用传递(基本数据类型值传递),"="赋值也是引用传递(基本数据类型值传递)。

 

1.Integer采用引用传递

  因为8种基本数据类型和String的不可变性,加大了引用传值的理解程度,误认为"8种包装类型是“值传递",下面进行实例:

    public static void main(String[] args) {
        Integer a = 5;
        Integer b = a;
        b++;
        System.out.println(a);//5
        
        String s1="s1";
        String s2 = s1;
        s2 = "s2";
        System.out.println(s1);//s1
    }

  解释:实际Integer和String是采用引用传递,=的时候a和b,s1和s2指向同一个对象。执行b++以后因为Integer的不可变性,b指向一个新的对象,b与a已经没有关系;s2="s2"以后s2指向一个新的对象,也与s1不要紧。

 

为了验证Integer是采用引用传递,我门作案例以下:

package cn.qlq.test;

import java.util.concurrent.TimeUnit;

public class IntegerSyn {
  
  public static void main(String[] args) throws InterruptedException {
    Integer index = 0;
    TestObject a = new TestObject(index);
    synchronized (index) {
      new Thread(a).start();
      index.wait();
    }
    System.out.println("end");
  }
}
 
class TestObject implements Runnable {
  private Integer index;
  
  public TestObject(Integer index){
    this.index = index;
  }
  
  public void run() {
    try {
        //线程休眠的另外一种方法
      TimeUnit.SECONDS.sleep(5);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    synchronized (index) {
      index.notify();
    }
  }
}

5s后打印end

解释:  在程序刚启动的时候把 Integer 的index 对象锁住 ,而且调用了 wait方法,释放了锁的资源,等待notify,最后过了5秒钟,等待testObject 调用notify 方法就继续执行了。你们都知道锁的对象和释放的对象必须是同一个,不然会抛出  java.lang.IllegalMonitorStateException 。由此能够证实 Integer做为参数传递的时候是地址传递,而非值传递。

 

2.数组采用引用传值

  其实数组也是对象类型,传递的时候也是采用引用传递,只是由于基本数据类型数的不可变性也增大了理解难度,例如:

 

package cn.qlq.test;

import java.util.Arrays;

public class ArrayTest {
    public static void main(String[] args) {
        int a[] = { 10, 20 };
        test(a);
        System.out.println(Arrays.toString(a));
    }

    public static void test(int arr[]) {
        arr[0] = 100;
    }
}

 

结果:

[100, 20]

 

  数组实际是引用传递。(这点必须理解,由于String的不可变是基于char[]与深复制实现)。实际上数组是基于引用传递,不论是基本数据类型数组仍是包装类型数组都是引用传递。测试代码以下:

package cn.qlq.test;

import java.util.concurrent.TimeUnit;

public class ArraySyn {
  
  public static void main(String[] args) throws InterruptedException {
    int index[] = {1,2};
    TestObject1 a = new TestObject1(index);
    synchronized (index) {
      new Thread(a).start();
      index.wait();
    }
    System.out.println("end");
  }
}
 
class TestObject1 implements Runnable {
  private int[] index;
  
  public TestObject1(int []index){
    this.index = index;
  }
  
  public void run() {
    try {
        //线程休眠的另外一种方法
      TimeUnit.SECONDS.sleep(5);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    synchronized (index) {
      index.notify();
    }
  }
}

5s后打印end,证实是引用传递。

 

另外一种测试方法:传递引用类型数组

package cn.qlq.test;

import java.util.Arrays;

public class ArrayTest {
    public static void main(String[] args) {
        Person[] p = new Person[2];
        Person p1 = new Person();
        p1.setName("p1");
        Person p2 = new Person();
        p2.setName("p2");
        p[0] = p1;
        p[1] = p2;
        testArr(p);
        System.out.println(Arrays.toString(p));
    }

    private static void testArr(Person[] p) {
        p[0].setName("p1p1");
    }
}

结果:

[Person [age=0, name=p1p1], Person [age=0, name=p2]]

 

总结一条:

  8种基本数据类型是值传递,8种基本数据类型与String与数组是引用传递,咱们程序中的类也是引用传递,可是因为String与8种基本数据类型的不可变性,因此每次赋予新值的时候都是新指向一个对象。若是是函数调用是形参和实参指向同一个对象,因此改变实参的时候至关于新创一个对象并赋给形参,对实参不会形成影响。

 

3.String引用传递图解

更进一步的理解:"引用传值也是按值传递,只不过传的是对象的地址"。

好比下面一段代码:

package cn.qlq.test;

import java.util.Arrays;

public class ArrayTest {
    public static void main(String[] args) {
        String s = "hello";
        System.out.println(s.hashCode());
        
        test(s);
        System.out.println(s);
    }

    public static void test(String s1) {
        System.out.println(s1.hashCode());
        s1 = "world";
        System.out.println(s1.hashCode());
    }
}

结果:

99162322
99162322
113318802
hello

解释:调用test方法的时候采用引用传递(将s的地址传下去),执行s1="world"是新创一个"world"并赋值给s1,也就是s1此时已经指向其余对象,再也不与s指向相同对象。

图解:

  

4.补充:

  引用类型若是另外一个采用 = 赋值 改变引用以后二者会指向不一样的对象(由于这个纠结了一下午,new至关于新创一个对象而且赋值给该变量,若是不想让其new能够采用final限制为引用不可变),并且将一个静态变量赋值给局部变量的时候改变局部变量的值也会影响static变量的值,因此若是要定义真正的不可变对象能够用final变量。

Person.java

class Person {
    public static Person person = new Person("1", 1);
    private String name = "zhangsan";
    private int age = 25;

    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + "]";
    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

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

 

(1)测试一:测试new不影响原对象

    /**
     * new以后是新建立一个对象并赋值给该变量,不会影响原来的变量
     */
    private static void test3() {
        Person p1 = Person.person;
        System.out.println("p1:"+p1);
        System.out.println("Person.person:"+Person.person);
        p1 = new Person("2", 2);
        System.out.println("--------p1从新new以后的值------");
        System.out.println("p1:"+p1);
        System.out.println("Person.person:"+Person.person);
    }

结果:

p1:Person [name=1, age=1]
Person.person:Person [name=1, age=1]
--------p1从新new以后的值------
p1:Person [name=2, age=2]
Person.person:Person [name=1, age=1]

 

(2)将static变量引用传递给局部变量,改变局部变量也能够影响static变量

    /**
     * 修改一个变量会影响static变量对应引用类型的值
     */
    private static void test4() {
        Person p1 = Person.person;
        System.out.println("p1:" + p1);
        System.out.println("Person.person:" + Person.person);
        p1.setName("20");
        p1.setAge(20);
        System.out.println("--------p1改变值以后------");
        System.out.println("p1:" + p1);
        System.out.println("Person.person:" + Person.person);
    }

结果:

p1:Person [name=1, age=1]
Person.person:Person [name=1, age=1]
--------p1改变值以后------
p1:Person [name=20, age=20]
Person.person:Person [name=20, age=20]

 

(3)测试2:(这个例子更加的能够理解)

public class PlainTest {
    private Person per = Person.person;

    public static void main(String[] args) {
        test1();
    }

    private static void test1() {
        PlainTest p1 = new PlainTest();
        Person.person = new Person("2", 2);
        System.out.println("Person.person:" + Person.person);
        System.out.println("p1的per:" + p1.per);

        PlainTest p2 = new PlainTest();
        System.out.println("p2的per:" + p2.per);
    }
}

结果:

Person.person:Person [name=2, age=2]
p1的per:Person [name=1, age=1]
p2的per:Person [name=2, age=2]

 

test1解释:

  PlainTest p1 = new PlainTest();此时其实例变量per与Person的person指向堆中同一个对象。

  Person.person = new Person("2", 2);  此时新出那个键一个对象而且Person的person会指向该对象。可是p1.per指向的地址仍是原来的地址。

  PlainTest p2 = new PlainTest();   此时新建一个PlainTest2,其成员变量与上面的Person.person指向同一对象(修改后的对象)

 

(4)测试3:(与上面例子3结合理解更好)

public class PlainTest {
    private Person per = Person.person;

    public static void main(String[] args) {
        test2();
    }private static void test2() {
        PlainTest p1 = new PlainTest();
        Person.person.setName("2");
        System.out.println("Person.person:" + Person.person);
        System.out.println("p1的per:" + p1.per);

        PlainTest p2 = new PlainTest();
        System.out.println("p2的per:" + p2.per);
    }
}

结果:

Person.person:Person [name=2, age=1]
p1的per:Person [name=2, age=1]
p2的per:Person [name=2, age=1]

 

  这个例子很好解释,三个person都指向同一对象,因此修改任何一个都会影响剩下两个。

 

5.若是一个对象被赋予null值,也至关于与原来的对象脱离关系,被赋予null会孤立原来堆中的对象,也就是会被GC,前提是原来堆中的对象没有被其余变量引用

    public static void main(String[] args) {
        Person p = new Person("张三", 0);
        Person p1 = p;
        p=null;
        System.out.println(p1+"\t"+p1.hashCode());
        System.out.println(p+"\t"+p.hashCode());
    }

结果:(被从新赋值为null不会影响与之指向同一对象的引用,只是本身再也不指向堆中的对象。)

Person [name=张三, age=0] 1605870897
Exception in thread "main" java.lang.NullPointerException
at cn.qlq.test.PlainTest.main(PlainTest.java:11)

  

3.可变类与不可变类

不可变类:所谓的不可变类是指这个类的实例一旦建立完成后,就不能改变其成员变量值。如JDK内部自带的不少不可变类:Interger、Long和String(8种基本数据类型的包装类和String都是不可变类)等。不可变类的意思是一旦这个对象建立以后其引用不会改变,每次从新赋值会新增一个对象。不可变类是实例建立后就不能够改变成员遍历的值。这种特性使得不可变类提供了线程安全的特性但同时也带来了对象建立的开销,每更改一个属性都是从新建立一 个新的对象。例如String s = "s1",s = "s2"其实是建立了两个对象,第二次将其值指向新的"s2".。

可变类:相对于不可变类,可变类建立实例后能够改变其成员变量值,开发中建立的大部分类都属于可变类。

关于更详细的介绍参考:http://www.javashuo.com/article/p-gxsdzueh-ha.html

 

在这里咱们只须要明白8种基本数据类型的包装类和String类型是不可变类,其他咱们程序中的大部分类都是可变类。

不可变类的设计原则:

1. 类添加final修饰符,保证类不被继承。
    若是类能够被继承会破坏类的不可变性机制,只要继承类覆盖父类的方法而且继承类能够改变成员变量值,那么一旦子类以父类的形式出现时,不能保证当前类是否可变。

2. 保证全部成员变量必须私有,而且加上final修饰(不可变指的是引用不可变,也就是不能够从新指向其余对象)
    经过这种方式保证成员变量不可改变。但只作到这一步还不够,由于若是是对象成员变量有可能再外部改变其值。因此第4点弥补这个不足。

3. 不提供改变成员变量的方法,包括setter
    避免经过其余接口改变成员变量的值,破坏不可变特性。

4.经过构造器初始化全部成员,进行深拷贝(deep copy)

若是构造器传入的对象直接赋值给成员变量,仍是能够经过对传入对象的修改进而致使改变内部变量的值。例如:

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();   
    }   
}
5. 在getter方法中,不要直接返回对象自己,而是克隆对象,并返回对象的拷贝
    这种作法也是防止对象外泄,防止经过getter得到内部可变成员对象后对成员变量直接操做,致使成员变量发生改变

 

string对象在内存建立后就不可改变,不可变对象的建立通常知足以上5个原则,咱们看看String代码是如何实现的。

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence
{
    /** The value is used for character storage. */
    private final char value[];//数组是引用传递 /** The offset is the first index of the storage that is used. */
    private final int offset;
    /** The count is the number of characters in the String. */
    private final int count;
    /** Cache the hash code for the string */
    private int hash; // Default to 0
    ....
    public String(char value[]) {
         this.value = Arrays.copyOf(value, value.length); // deep copy操做
     }
    ...
     public char[] toCharArray() {
     // Cannot use Arrays.copyOf because of class initialization order issues
        char result[] = new char[value.length];
        System.arraycopy(value, 0, result, 0, value.length);
        return result;
    }
    ...
}

 

如上代码所示,能够观察到如下设计细节:

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

这都符合上面总结的不变类型的特性,也保证了String类型是不可变的类。

 

补充:深复制与浅赋值区别:

浅复制:被赋值的对象与原对象都含有相同的值,而全部对其余对象的引用仍然指向原来的对象。换言之,浅复制仅仅赋值所考虑的对象,而不复制它所引用的对象。

深赋值:被复制的对象的全部变量都有与原对象相同的值,除去那些引用其余对象的变量。那些引用其余对象的变量将指向被赋值的新对象,而再也不是原来的那些被引用的对象。换言之,深复制把复制的对象所引用的对象都复制了一遍。

以下图:

 

  总结:

  (1)关于finally:

  finally块的语句在try或catch中的return语句执行以后返回以前执行且finally里的修改语句可能影响也可能不影响try或catch中return已经肯定的返回值,若是返回值类型为传址类型,则影响;传值类型(8种基本类型)与8种基本数据类型的包装类型与String(不可变类)不影响。若finally里也有return语句则覆盖try或catch中的return语句直接返回,至关于普通流程中的return语句。

 

面试宝典解释的缘由以下:

  程序在执行到return时首先会把返回值存到一个指定的位置(JVM中的slot),其次与执行finally块,最后再返回。若是finally中有return语句会以finally的return为主,至关于普通程序中的return结束函数。若是没有return语句,则会在finally执行完以后弹出slot存储的结果值而且返回,若是是引用类型则finally修改会影响结果,若是是基本数据类型或者不可变类不会影响返回结果。

 

  (2)值传递与引用传递:

    =与函数调用是引用传递,8种基本数据类型采用值传递,其包装类型与String与其余咱们手写的类都是引用传递。只是因为String和8种包装类型都是不可变类,因此每次操做都是新创一个对象并从新赋给引用;在函数调用的时候,若是形参是String或者8种包装类型,操做形参不会影响实参,操做形参至关于从新建立对象不会影响原实参。

  

  (3)可变与不可变

    String与8种包装类型、BigInteger、BigDecimal是不可变类,不可变的意思是每次更换值都会从新生成对象并赋给引用。不用考虑线程安全。咱们也能够设计本身的不可变类。

    其余咱们手写的通常是可变类。

相关文章
相关标签/搜索