JAVA基礎之-引數傳遞

正在战斗中發表於2024-08-19

準備整理一個系列,這是系列的第一篇。

這是一個經典的問題,也是JAVA程式設計師所必須掌握的。

一、小結論和例子

1.1結論

內容沒有多少,可以先說結論:

變數的表示和引數傳遞

  • 變數是如何表示,尤其是引數是如何表示的
    • 儲存則具體看變數是什麼型別:類靜態、例項變數、方法
    • 變數表示-基本型別直接儲存,類型別則儲存地址
  • 值是如何傳遞的
    • 如果是基本型別-則是值的副本
    • 如果是類型別-則是指向具體資料的地址的副本

變數透過方法加工後,對原來變數的影響

  1. 方法對基礎型別引數做任何處理,都不會影響到引數關聯的變數的值
  2. 如果方法中對物件型別引數不做重新賦值,那麼方法會影響引數關聯的變數的值
  3. 如果方法中對物件型別引數重新賦值,那麼方法不會影響引數關聯的變數的值

約定

前文提到變數 、引數,透過下文解釋:

public class Warrior{
    
     public void test(){
           String firstPart="喊";
           String secondPart="山";
           String combinStr=concat(firstPart,secondPart);
           System.out.println(combinStr);
     }

     public String concat(String a,String b){
          a=a+b;
          return a;
     }

}

這段示例程式碼中,firstPart,secondPart稱為變數(方法test的);a,b則稱為引數(方法concat)。

引數a關聯變數firstPart,引數b關聯變數secondPart。

1.2示例程式碼

本例子是基於JDK17編寫:

package study.base.param;

import java.lang.reflect.InvocationTargetException;
import java.util.Random;

import com.alibaba.fastjson2.JSON;

/**
 * 本類主要演示以下內容:
 * <pre>
 *    1.在方法中傳遞物件
 *    2.如何在方法中交換兩個物件的值
 *    3.透過物件地址驗證方法引數被重新賦值後,會指向另外一個物件的地址
 * </pre>
 */
public class TestPassObjectParam {
    
    public void testPassint(int x) {
        x=x+10;
        System.out.println("x=x+10="+x);
    }
    /**
     * 測試-傳遞字串,但是對引數整體調整,不會影響外部的變數,
     * 因為這會給引數重新賦值,即重新指向另外一個物件的地址,已經不指向原來的物件
     * @param s1
     * @param s2
     */
    public  void testPassString(String s1,String s2) {
        System.out.println("引數s1,s2在方法中被重新賦值,但不會影響到相關的變數");
        s1=s1+"**";
        s2=s2.substring(2);
    }
    
    /**
     * 改變引數的區域性值,會改變數本身
     * @param dog
     */
    public void  testPassObject(Dog dog) {
        dog.www();
        dog.eat();
        System.out.printf("引數dog的邏輯地址=%s \n",System.identityHashCode(dog));
    }
    
    /**
     * 為物件型別引數重新賦值,不會改變變數
     * @param dog
     */
    public void  testPassObjectAndchange(Dog dog) {
        dog=new Dog("等級很高", "白", 24);
        System.out.printf("引數dog被重新賦值後的邏輯地址=%s\n",System.identityHashCode(dog));
    }
    
    public Dog createDog(String name,String color,Integer weight){
        Dog dog=new Dog(name,color,weight);
        return dog;
    }
    
    /**
     * 經典的字串交換例子--這是不可能的,這是因為字元型別是不可變的。 
     * @param a  
     * @param b
     */
    public void swap(String a,String b) {
        String tmp=a;
        a=b;
        b=tmp;
    }
    
    public void swapDog(Dog a,Dog b) {
        Dog c=a;
        a=b;
        b=c;
    }
    
    public void swapDog2(Dog a,Dog b) {
        //Dog c=a;
        String tsa=JSON.toJSONString(a);
        String tsb=JSON.toJSONString(b);
        a=JSON.to(Dog.class, tsb);
        b=JSON.to(Dog.class, tsa);
    }
    
    public static void main(String[] args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        TestPassObjectParam test=new TestPassObjectParam();
        
        // 演示基本型別和物件型別傳遞
        // a.1  傳遞基本型別。基本型別 的值不會被改變
        int x=10;
        test.testPassint(x);
        System.out.println(x); // 基本型別不會被改變,因為傳遞的是x的副本
        
        //a.2  傳遞物件。可能改變變數的值,也可能不會。   這裡需要格外小心,尤其是傳遞String型別的時候。
        //在方法中對物件型別進行處理,是否會修改物件,需要格外小心,有時候會修改變數,有時候不會
        //大體上可以有3個基本對的結論:
        // 1.如果只是對引數的區域性屬性進行修改,那麼變數也會被改變 
        // 2.如果對引數整體進行處理,或者重新賦值,那麼變數不會被改變
        // 3.以上2點用String不好理解,最好使用更加複雜的一些類進行測試
        
        String name="lzf";
        String address="宇宙銀河系太陽系地球中國";
        System.out.println("變數name,address被傳入方法前的值");
        System.out.printf("name=%s,address=%s",name,address);
        test.testPassString(name, address);
        System.out.println("變數name,address被傳入方法後,檢視它們的值是否改變");
        System.out.printf("name=%s,address=%s \n\r",name,address);
        System.out.println("...現在交換字串變數name,address");
        test.swap(name, address);
        System.out.println("...name,address交換後的值(事實證明挖法在簡單傳參的情況下交換兩個物件,包括字串)");
        System.out.printf("name=%s,address=%s \n\r",name,address);        
        
        System.out.println("---------------------------------------------------------");
        
        // 透過物件的雜湊編碼驗證在方法中對引數的賦值影響=》會被賦予另外一個物件的地址
        //a.3  只是修改引數的屬性,會影響變數。    演示一個Dog型別的屬性變換會影響到變數,因為沒有為引數重新賦值
        Dog dog=test.createDog(name, "黃",15);
        System.out.printf("變數dog傳入方法前的邏輯地址=%s \n",System.identityHashCode(dog));
        test.testPassObject(dog);
        dog.www();
        
        //a.4  引數被重新賦值,不會改變變數
        test.testPassObjectAndchange(dog);
        dog.www();
        
        
        //b:簡單傳參交換兩個物件也是不行的
        System.out.println("------------------------------------------------------------");
        Dog a=test.createDog("ss", "red", 10);
        Dog b=test.createDog("ww", "black", 20);
        System.out.printf("Dog a,b在交換前的顏色:%s,%s \n",a.getColor(),b.getColor());
        String cb=a.getColor();
        test.swapDog(a, b);
        String ca=a.getColor();
        System.out.printf("Dog a,b在交換後的顏色:%s,%s \n",a.getColor(),b.getColor());
        
        if (cb.equals(ca)) {
            System.out.println("Dog a,b交換失敗(無法透過簡單傳遞來交換兩個物件)");
        }
        
        System.out.println("Dog a,b透過嘗試透過json序列和反序列進行交換");
        test.swapDog2(a, b);
        System.out.printf("Dog a,b在交換後的顏色:%s,%s \n",a.getColor(),b.getColor());
        
        cb=a.getColor();
        if (cb.equals(ca)) {
            System.out.println("Dog a,b交換失敗(無法透過簡單傳遞來交換兩個物件)");
        }
        System.out.println("透過簡單的論證,可以得出結論:兩個物件透過一個函式來進行簡單的交換屬性,是不可行");
        System.out.println("在沒有特殊的情況下,java不可能再調整為引數複製/變數賦值的方法:先建立值,然後把值的地址賦予類變數/引數");
        
    }
    
    class Dog{
        private String name;
        private String color;
        private Integer weight;
        
        
        Dog(String name,String color,Integer weight){
            this.name=name;
            this.color=color;
            this.weight=weight;
        }
        
        public void eat() {
            Random rand=new Random();
            int randValue=rand.nextInt(1,10);
            int rd=rand.nextInt(10,100);
            if (rd>50) {
                this.weight+=randValue;
            }
            else {
                this.weight-=randValue;
            }
        }
        
        public void www() {
            System.out.println("有一隻"+color+"色,重"+weight.toString()+"斤,它正在吠叫:"+name);
        }

        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;
        }

        public Integer getWeight() {
            return weight;
        }

        public void setWeight(Integer weight) {
            this.weight = weight;
        }
        
        
    }
}

注:所謂的簡單交換,即經典的交換方法,透過一個臨時變數過度。

二、注意事項和其它一些問題

大部分情況下,引數的傳遞並不是一個問題,這裡的注意事項,其實主要就是和字元(String)型別有關。

我們都知道,由於某些原因String本身是final儲存的:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence,
               Constable, ConstantDesc {

    /**
     * The value is used for character storage.
     *
     * @implNote This field is trusted by the VM, and is a subject to
     * constant folding if String instance is constant. Overwriting this
     * field after construction will cause problems.
     *
     * Additionally, it is marked with {@link Stable} to trust the contents
     * of the array. No other facility in JDK provides this functionality (yet).
     * {@link Stable} is safe here, because value is never null.
     */
    @Stable
    private final byte[] value;

這就意味著字元對一個字串變數重新賦值,則必須重新建立一個字串物件。而一個新的物件必然指向一個新的地址。

當變數/引數被指向新的地址的時候,對原來的物件自然無法產生影響。

對字串做變更的操作都會導致為建立一個新的物件,併為變數重新賦予新物件的地址。

例如常見的substring,concat,replace都是這樣的,如果僅僅是訪問字元變數的屬性,是不會改變字元的。

所以,如果希望透過一個函式修改一個字串,那麼必須只有兩種途徑可以影響原來的字元變數:

1.函式返回新的字串,並把這個新的字串賦值給原來的變數

2.把字串包裝在某個物件內部,然後在方法中為物件的字元屬性重新賦值

相關文章