String型別函數傳遞問題

2022-07-17 12:00:19

String型別函數傳遞問題

問題

  • 以前沒有注意過的一個問題, 最近在使用String型別作為函數入參的時候, 發現函數內對於String型別的改變並不會影響到外層呼叫物件本身;

結論 (先說結論)

  • 這個問題根本不存在 (屬於是自己把自己繞進去了);
  • String型別與普通的java物件一樣, 只不過是用final修飾的不可變物件 (具體看String型別的原始碼與相關介紹);

測試資料(為什麼會有這個問題, 來源於以下操作)

  • 發現String (其實Integer, Long... 等等這些型別也會這樣)函數傳遞修改後, 物件的值並沒有被改變;
  • 主要是因為String型別與Integer...等等這些型別的賦值方式迷惑了我們, 不需要通過"new"關鍵字和反射也可以構建物件;
  • 比如: Integer a = 123; 這種操作, 讓我們誤當作基本型別賦值(實際上這個問題比較基礎, 但有時候也會迷糊);
  • 以下是對上述的操作案例;
package timer;

/**
 * @author liwangcai E-mail:[email protected]
 */
public class StringDemo {
    public static void changeString(String tmp) {
        //此處操作具有一定的迷惑行為, 通常情況下只有基本資料型別才會這麼操作;
        //但是對與String型別, 相當於新建了一個物件或者是拿到了常數池中的物件;
        tmp = "new";
        //列印通過函數傳遞進來的tmp引數的地址
        System.out.println(System.identityHashCode(tmp));
    }

    public static void changeInteger(Integer tmp) {
        //此處操作和String型別一樣
        tmp = 199;
        //列印通過函數傳遞進來的tmp引數的地址
        System.out.println(System.identityHashCode(tmp));
    }

    public static void changeOther(StringDemo stringDemo) {
        System.out.println(System.identityHashCode(stringDemo));
    }


    public static void main(String[] args) {
        String tmp = "old";
        //列印原始的tmp
        System.out.println(tmp);      
        //列印原始的tmp物件地址
        System.out.println(System.identityHashCode(tmp));
        changeString(tmp);
        //列印函數呼叫後的tmp值
        System.out.println(tmp);


        //出於好奇也測試了一下Integer型別
        Integer iTmp = 1;
        //列印原始的iTmp的值
        System.out.println(iTmp);
        //列印原始的iTmp物件地址
        System.out.println(System.identityHashCode(iTmp));
        changeInteger(iTmp);
        //列印函數呼叫後iTmp的值
        System.out.println(iTmp);

        //對於普通的java物件
        StringDemo stringDemo = new StringDemo();
        //列印當前物件的地址
        System.out.println(System.identityHashCode(stringDemo));
        changeOther(stringDemo);
    }
}

//執行結果
old
685325104
685325104
460141958
old
1
1163157884
1163157884
1956725890
1
356573597
356573597

以上測試結果得到的結果分析

  • 上面的這個操作實際上就是迷惑所在,在這裡單獨把它列出來看一下
Integer a;
//此處實際上是java的自動裝箱, 相當於呼叫了valueOf函數
// 實際上是new了一個Integer物件出來,或者將另一個Integer物件直接賦值(可以去看一下Integer的原始碼)
a = 123;
//基礎型別的直接賦值
int b;
b = 123;
//與Integer類似,String型別也有常數池, 相當於快取, 此處不是重點;
// 相當於new了一個物件出來, 或將另一個String物件直接賦值;
String c;
c = "abc"

普通物件操作對比

/**
 * @author liwangcai E-mail:[email protected]
 */
public class StringTest {
    public static void changeUser(User user) {
        user = new User("zhang san", 24);
    }


    public static void main(String[] args) {
        User user = new User("li si", 25);
        System.out.println(user);
        changeUser(user);
        System.out.println(user);
    }

    @Data
    @AllArgsConstructor
    @ToString
    static class User {
        private String name;

        private Integer age;
        
    }
}

//執行結果, 可以發現物件的值並沒有改變
StringTest.User(name=li si, age=25)
StringTest.User(name=li si, age=25)
Process finished with exit code 0

為什麼new一個物件並不會改物件值?

  • 主要是因為在Java中函數傳遞只有值傳遞 (不是本部落格重點, 不展開描述)

如果想要改變String的值正確的操作姿勢?

  • String物件是普通的Java物件, 不過是被final修飾了;
  • 實際上String物件的值存在其內部的char value[]陣列中,如果想要改變String的值應該區修改這個陣列的資料;
  • 不過上述陣列是使用final修飾的, 所以如果使用jdk中的String類, 那麼String的值是無法被修改的;

如果要改變String的值應該怎麼做?

  • 實現自己的String型別, 內部儲存char[]陣列不設定為final型別就可以了;
/**
 * @author liwangcai E-mail:[email protected]
 */
public class MyStringTest {
    public static void main(String[] args) {
        MyString myString = new MyString(new char[]{'a', 'b'});
        System.out.println(myString);
        changeMyString(myString);
        System.out.println(myString);
    }

    private static void changeMyString(MyString myString) {
        myString.setValue(new char[]{'c', 'd'});
    }


    @ToString
    @AllArgsConstructor
    @Data
    static class MyString {
        char[] value;
    }
}

//測試結果
//可以發現值改變了
MyStringTest.MyString(value=[a, b])
MyStringTest.MyString(value=[c, d])

Process finished with exit code 0

總結

  • 對於String,Integer型別 "=" 操作符號迷惑了我們, 實際上一開始提出的問題並不存在;
  • 如果要修改同一個物件, 需要修改其內的成員;