final 修饰符

final 修饰符表示不可变。类似 C 中的 constant。用于修饰变量表示不可变的变量。用于修饰方法表示不可被重写。用于修饰类表示不可被继承。

final 的成员变量

成员变量随着类或者实例的初始化而初始化。在类初始化时,静态变量就会被分配内存并初始化。对于实例变量,系统会在实例初始化的时候初始化这些变量。

由于成员变量会被系统隐式的初始化。如果程序员不显式的初始化它们,那他们会变成 0,false,null 这样的值。失去了意义。

所以 final 修饰的成员变量必须显式的初始化。

  • 类变量:必须在静态初始化块,或者在声明的时候初始化
  • 实例变量:必须在非静态初始化块,声明时,或者构造函数中初始化。

final 的局部变量

对于形参:在方法内部无法对其进行赋值。 方法体内的 final 局部变量:只能赋值一次。

final 的基本类型变量和引用类型变量

  • 基本类型变量:值不可变。
  • 引用类型变量:指针不可变,但是指向的内存区域可变。
public class FinalVariableTest {
 public static void main(String[] agrs){
  final int[] arr = {1,23,4,5};
  System.out.println(Arrays.toString(arr));
  arr[2] = 20;
        System.out.print(Arrays.toString(arr));
   }
}

//Console
//[1, 23, 4, 5]
//[1, 23, 20, 5]

final 的宏替换

对于 final 的变量来说,不管是类变量,实例变量还是局部变量。只要在编译的时候可以确定,那么编译器就会把它看成一个直接量而不是变量。 比如:

public class FinalVar{
   public static void main(String[] args){
       final int a =5;
       System.out.println(a);
   }
}

实际编译器在执行 System.out.println(a);等同于执行 System.out.println(5); 什么叫做编译时可以确定 下面这个例子。 str1 在编译的时候就可以确定。而 str2 需要调用 valueOf() 方法才能得到后面一个字符串。在编译的时候无法确定。从结果也可以看出来。

final String str1 = "hello world"+2016;
final String str2 = "hello world"+String.valueOf(2016);

System.out.println(str1=="hello world2016");
System.out.println(str2=="hello world2016");

再看这个

String str1 = "hello world";
String str2 = "hello " + "world";

String s1 = "hello ";
String s2 = "world";

String str3 = s1 + s2;

System.out.println(str1 == str2);
System.out.println(str1 == str3);

str1 == str3 返回是 false。因为 str3 在编译是无法确定。所以不会被当作直接量。但是如 s1 和 s2 都加上 final。编译器会进行宏替换。str3 就等同于"hello" + “world”。

final 的方法

final 的方法不会被重写。

final 的类

final 的类不能被继承。

不可变类

Java 提供的基础变量类型的包装类和 String 类都是不可变类。如果要自己创建不可变的类需要注意几点:

  • 使用 private 和 final 修饰成员变量。
  • 提供带参数的构造器,用于根据需要传入参数来初始化类里的成员变量。
  • 仅提供 getter 方法,不提供 setter 方法。
  • 如果有必要重写 hashCode() 和 equals() 方法。还要保证两个用 equals 方法判断相等的对象 hasdCode() 也相等。
public class Address {
   private final String detail;
   private final String postCode;
   public Address(){
      this.detail="";
      this.postCode="";
   }
   public Address(String detailString postCode){
       this.detail=detail;
       this.postCode=postCode;
   }
   public String getDetail(){
       return this.detail;
   }
   public String getPoetCode(){
       return this.postCode;
   }
   public boolean equals(Object obj){
       if(this == obj){
           return true;
       }
       if(obj!=null&&obj.getClass()==Address.class){
           Address ad = (Address)obj;
           if(this.getClass().equals(ad.getDetail())&&this.getPoetCode().equals(ad.getPoetCode())){
               return true;
           }
       }
       return false;
   }
   public int hashCode(){
       return this.detail.hashCode()+this.postCode.hashCode();
   }
}

编写不可变类的时候,只需要提供 getter 方法,不提供 setter 方法就可以了。但是对引用传递一定要注意。因为引用传递传的是尼玛指针。下面就是一个失败的例子。

public class Person {
   public final Name name;
   public Person(Name name){
       this.name = name;
   }
   public Name getName(){
       return this.name;
   }
   public static void main(String[] agrs){
       Name n = new Name("Brady""Gao");
       Person p = new Person(n);
       System.out.println(p.getName().getFirstName());
       n.setFirstName("heihei");
       System.out.println(p.getName().getFirstName());
   }
}
class Name {
   private String firstName;
   private String lastName;
   public Name(){}
   public Name(String firstNameString lastName){
       this.firstName = firstName;
       this.lastName = lastName;
   }
   public String getFirstName(){
       return this.firstName;
   }
   public void setFirstName(String firstName){
       this.firstName = firstName;
   }
   public String getLastName(){
       return this.lastName;
   }
   public void setLastName(String lastName){
       this.lastName = lastName;
   }
}

要想解决这个,就要做到,避免引用传递带来的指针的直接传递。 重写 Person 的构造方法。

public Person (Name name){
  this.name = new Name(name.getFirstName()name.getLastName());
}

References

  • 番茄博客-final 修饰符(原链接失效)