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是不可變類,不可變的意思是每次更換值都會從新生成對象並賦給引用。不用考慮線程安全。咱們也能夠設計本身的不可變類。

    其餘咱們手寫的通常是可變類。

相關文章
相關標籤/搜索