【編碼技巧】Java-爲什麼要給方法參數或一些對象的引用加final?

 

 

 

 

 

 

閱讀源碼時常常會看到一些例如被這樣修飾的方法參數

public void print(final Object obj){
    System.out.println(obj);
}

你可能會疑問這個final有什麼用,那麼請看下面代碼

食用final前

Object o = "Hello";
foo(o)
System.out.println(o);//打印結果依舊是"Hello"

void foo(Object o) { o = "Goodbye"; } //此時已經進入棧區!o變量的修改只在此方法內有效

食用final後

Object o = "Hello";
foo(o);
System.out.println(o);//打印結果是"Hello"

void foo(final Object o) { 
    o = "Goodbye"; //oh shit,編譯報錯
} 

在一些不會出現被修改情況的變量前加final,這樣就可以有效防止我們在通宵加班的日子裏寫出各種奇異的bug了。

但這時候你可能會問,既然加final是個好習慣,爲什麼Java不給方法參數直接強制加上final呢???

因爲有時你會遇到如下圖這種操作

void print(String msg, int count) {
    msg = msg != null ? msg : "DefaultValue";
    while(--count >= 0) {
        System.out.println(msg);
    }
}

如果不直接修改方法參數我們就要聲明更多變量去實現這種操作,如果參數不是對象而是一個巨大無比的數組,那我豈不是要開闢一倍的空間?

 

要知道在Java中不存在C#的ref out這種類似指針的操作

那麼問題來了,如果我需要讓這個方法來修改我傳入的參數再返回給我,又不想用return的方式,那該怎麼辦?

public static void main(String[] args) {
	String[] arg = new String[2];
	set(arg);
	System.out.println(arg[0]);//輸出結果是"233"
}

public static void set(String[] args){
	args[0] = "233";
}

上圖通過數組的特性實現了你想要的操作,而且有了這種操作我們就可以在lambda裏修改方法體外部的變量了。

關於類加載的話題:

A類

public class A {

    static {
        System.out.println("A類被初始化");
    }
    public static final int X = 100;

}

B類

public class B {
    public static void main(String[] args) {

        System.out.println(A.X);
    }
}

如上這種使用static加final修飾的變量在如下這種情況中不會觸發父類的初始化,因爲B類被編譯時把對A.X這個變量的引用的值直接寫到B類中了,來javap對B.class一探究竟

E:\PersonalProject\Algorithm\target\classes\mairuis\algorithm>javap -verbose B.class
Classfile /E:/PersonalProject/Algorithm/target/classes/mairuis/algorithm/B.class
  Last modified 2019-7-18; size 533 bytes
  MD5 checksum 8b4bcec45904f68b2bd63c0212a80a9f
  Compiled from "B.java"
public class mairuis.algorithm.B
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #6.#20         // java/lang/Object."<init>":()V
   #2 = Fieldref           #21.#22        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Class              #23            // mairuis/algorithm/A
   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(I)V
   #5 = Class              #26            // mairuis/algorithm/B
   #6 = Class              #27            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lmairuis/algorithm/B;
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               args
  #17 = Utf8               [Ljava/lang/String;
  #18 = Utf8               SourceFile
  #19 = Utf8               B.java
  #20 = NameAndType        #7:#8          // "<init>":()V
  #21 = Class              #28            // java/lang/System
  #22 = NameAndType        #29:#30        // out:Ljava/io/PrintStream;
  #23 = Utf8               mairuis/algorithm/A
  #24 = Class              #31            // java/io/PrintStream
  #25 = NameAndType        #32:#33        // println:(I)V
  #26 = Utf8               mairuis/algorithm/B
  #27 = Utf8               java/lang/Object
  #28 = Utf8               java/lang/System
  #29 = Utf8               out
  #30 = Utf8               Ljava/io/PrintStream;
  #31 = Utf8               java/io/PrintStream
  #32 = Utf8               println
  #33 = Utf8               (I)V
{
  public mairuis.algorithm.B();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lmairuis/algorithm/B;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: bipush        100  //喔,在這裏!這是A類中的常量X的值
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(I)V
         8: return
      LineNumberTable:
        line 12: 0
        line 13: 8
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       9     0  args   [Ljava/lang/String;
}
SourceFile: "B.java"

可以看到常量池中的FieldRef只有一個System.out並沒有A.X這個變量,然後我們在後面的main方法字節碼中發現了A.X的值100。

聰明的你看到這裏一定會想到如下問題:

(1)如果我用反射修改了A.X,B類對A.X引用的值會跟着改變嗎?

(2)如果我寫了像是這樣的語句

public static void main(String[] a){
    if (A.X == 100) {
        System.out.println("是100");
    } else {
        System.out.println("不是100");
    }
}

那最終編譯結果會是什麼樣?如果我在這種情況下反射修改了A.X呢?

相信有了答案之後你對Java該如何實現c++/c/c#那樣的條件編譯已經有了概念~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章