【编码技巧】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#那样的条件编译已经有了概念~

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