signed

QiShunwang

“诚信为本、客户至上”

深入解析Java编程中final关键字的使用

2021/6/24 18:20:50   来源:

在Java中声明属性、方法和类时,可使用关键字final来修饰。final变量即为常量,只能赋值一次;final方法不能被子类重写;final类不能被继承。
1.final成员
声明 final 字段有助于优化器作出更好的优化决定,因为如果编译器知道字段的值不会更改,那么它能安全地在寄存器中高速缓存该值。final 字段还通过让编译器强制该字段为只读来提供额外的安全级别。
 
1.1关于final成员赋值
1)在java中,普通变量可默认初始化。但是final类型的变量必须显式地初始化。
 
2)final 成员能且只能被初始化一次。
 
3)final成员必须在声明时(在final变量定义时直接给其赋值)或者在构造函数中被初始化,而不能在其它的地方被初始化。
示例1 Bat.java

public class Bat {
  final double PI = 3.14; // 在定义时赋值
  final int i; // 因为要在构造函数中进行初始化,所以此处便不可再赋值
  final List<Bat> list; // 因为要在构造函数中进行初始化,所以此处便不可再赋值
 
  Bat() {
    i = 100;
    list = new LinkedList<Bat>();
  }
 
  Bat(int ii, List<Bat> l) {
    i = ii;
    list = l;
  }
 
  public static void main(String[] args) {
    Bat b = new Bat();
    b.list.add(new Bat());
    // b.i=25;
    // b.list=new ArrayList<Bat>();
    System.out.println("I=" + b.i + " List Type:" + b.list.getClass());
    b = new Bat(23, new ArrayList<Bat>());
    b.list.add(new Bat());
    System.out.println("I=" + b.i + " List Type:" + b.list.getClass());
  }
}

 
结果:

I=100 List Type:class java.util.LinkedList
I=23 List Type:class java.util.ArrayList

 
在main方法中有两行语句注释掉了,如果你去掉注释,程序便无法通过编译,这便是说,不论是i的值或是list的类型,一旦初始化,确实无法再更改。然而b可以通过重新初始化来指定i的值或list的类型。
 
1.2 final引用字段的无效初始化
要正确使用final字段有点麻烦,对于其构造子能抛出异常的对象引用来说尤其如此。因为 final 字段在每个构造器中必须只初始化一次,如果 final 对象引用的构造器可能抛出异常,编译器可能会报错,说该字段没有被初始化。编译器一般比较智能化,足以发现在两个互斥代码分支(比如,if...else 块)的每个分支中的初始化恰好只进行了一次,但是它对 try...catch 块通常不会如此“宽容”。
下面这段代码通常会出现问题。

class Thingie {
  public static Thingie getDefaultThingie() {
    return new Thingie();
  }
}
 
public class Foo {
  private final Thingie thingie;
 
  public Foo() {
    try {
      thingie = new Thingie();
    } catch (Exception e) {
      thingie = Thingie.getDefaultThingie();//Error:The final field thingie may already have been assigned
    }
  }
}

 
你可以这样修改。

public class Foo {
  private final Thingie thingie;
 
  public Foo() {
    Thingie tempThingie;
    try {
      tempThingie = new Thingie();
    } catch (Exception e) {
      tempThingie = Thingie.getDefaultThingie();
    }
    thingie = tempThingie;
  }
}

 
1.3关于final成员使用
当你在类中定义变量时,在其前面加上final关键字,那便是说,这个变量一旦被初始化便不可改变,这里不可改变的意思对基本类型来说是其值不可变,而对于对象变量来说其引用不可再变。然而,对象其本身却是可以被修改的,Java并未提供使任何对象恒定不变的途径。这一限制同样适合数组,它也是对象。
示例2

private final int VAL_ONE=9;
private static final int VAL_TWO=99;
public static final int VAL_THREE=999;

由于VAL_ONE 和VAL_TOW 是带有编译期数值的final 原始类型,所以它们二者均可以用作编译期常量,并且没有重大区别。VAL_THREE是一种更加典型的对常量进行定义的方式:定义为 public,则可以被用于包之外;定义为 static 来强调只有一份;定义为 final 来说明它是一个常量。
final标记的变量即成为常量,但这个“常量”也只能在这个类的内部使用,不能在类的外部直接使用。但是当我们用public static final 共同标记常量时,这个常量就成为全局的常量(一个既是static又是final的字段只占据一段不能改变的存储空间)。而且这样定义的常量只能在定义时赋值,其他地方都不行。
示例3

class Value {
  int i;
 
  public Value(int i) {
    this.i = i;
  }
}
 
public class FinalData {
  private static Random rand = new Random();
 
  private String id;
 
  public FinalData(String id) {
    this.id = id;
  }
 
  private final int i4 = rand.nextInt(20);
 
  static final int i5 = rand.nextInt(20);
 
  public String toString() {
    return id + ":" + "i4:" + i4 + ", i5=" + i5;
  }
 
  public static void main(String[] args) {
    FinalData fd1 = new FinalData("fd1");
    System.out.println(fd1);
    System.out.println("Creating new FinalData");
    FinalData fd2 = new FinalData("fd2");
    System.out.println(fd1);
    System.out.println(fd2);
  }
}

 
结果

fd1:i4:6, i5=3
Creating new FinalData
fd1:i4:6, i5=3
fd2:i4:17, i5=3

示例部分展示了将final 数值定义为static(i5) 和非static(i4) 的区别。此区别只有在数值在运行期内被初始化时才会显现,这是因为编译器对编译期数值一视同仁。(并且它们可能因优化而消失。)当你运行程序时,就会看到这个区别。请注意,在fd1 和fd2 中, i5 的值是不可以通过创建第二个FinalData 对象而加以改变的。这是因为它是 static,在装载时已被初始化,而不是每次创建新对象时都初始化。
示例4

class Value {
  int i;
 
  public Value(int i) {
    this.i = i;
  }
}
 
public class … {
  private Value v1=new Value(11);
  private final Value v2=new Value(22);
  private static final Value v3=new Value(33);
  …
}
 
public static void main(String[] args) {
  …
  fd1.v2.i++;// OK--Object isn't constant!
  fd1.v1=new Value(9);//OK--not final
  fd1.v2=new Value(0);//Error:Can't change reference
  fd1.v3=new Value(1);//Error:Can't change reference
  …
}

从v1 到v3 的变量说明了final 引用的意义。正如你在main( )中所看到的,不能因为v2 是final 的,就认为你无法改变它的值。由于它是一个引用,final 意味着你无法将v2 再次指向另一个新的对象。
示例5

public class … {
  private final int[] a={1,2,3,4,5,6};
  …
}
 
public static void main(String[] args) {
  …
  for(int i=0;i<fd1.a.length;i++)
 fd1.a[i]++;// OK--Object isn't constant!
  fd1.a=new int[3];//Error:Can't change reference  …
}

对数组具有同样的意义(可以改变它的值,但不能指向一个新的对象),数组是另一种引用。
 
1.4解决final数组的局限性
尽管数组引用能被声明成 final,但是该数组的元素却不能。这意味着暴露 public final 数组字段的或者通过它们的方法将引用返回给这些字段的类都不是不可改变的。

// Not immutable -- the states array could be modified by a malicious
// callerpublic
class DangerousStates {
  private final String[] states = new String[] { "Alabama", "Alaska", "ect" };
 
  public String[] getStates() {
    return states;
  }
}

 
同样,尽管对象引用可以被声明成 final 字段,而它所引用的对象仍可能是可变的。如果想要使用 final 字段创建不变的对象,必须防止对数组或可变对象的引用“逃离”你的类。要不用重复克隆该数组做到这一点,一个简单的方法是将数组转变成 List。

// Immutable -- returns an unmodifiable List insteadpublic
class SafeStates {
  private final String[] states = new String[] { "Alabama", "Alaska", "ect" };
 
  private final List statesAsList = new AbstractList() {
    public Object get(int n) {
      return states[n];
    }
 
    public int size() {
      return states.length;
    }
  };
 
  public List getStates() {
    return statesAsList;
  }
}

 
1.5关于final参数使用
还有一种用法是定义方法中的参数为final,对于基本类型的变量,这样做并没有什么实际意义,因为基本类型的变量在调用方法时是传值的,也就是说你可以在方法中更改这个参数变量而不会影响到调用语句,然而对于对象变量,却显得很实用,因为对象变量在传递时是传递其引用,这样你在方法中对对象变量的修改也会影响到调用语句中的对象变量,当你在方法中不需要改变作为参数的对象变量时,明确使用final进行声明,会防止你无意的修改而影响到调用方法。
 
1.6关于内部类中的参数变量
另外方法中的内部类在用到方法中的参变量时,此参数变量必须声明为final才可使用。
示例6 INClass.java

public class INClass {
  void innerClass(final String str) {
    class IClass {
      IClass() {
       System.out.println(str);
      }
    }
    IClass ic = new IClass();
  }
 
  public static void main(String[] args) {
    INClass inc = new INClass();
    inc.innerClass("Hello");
  }
}

2.final方法
2.1final方法用途
1)为了确保某个函数的行为在继承过程中保持不变,并且不能被覆盖(overridding),可以使用final方法。
 
2)class中所有的private和static方法自然就是final。
 
2.2 final与private关键字
类中所有的private方法都隐式地指定是final的。由于无法取用private方法,所以也就无法覆盖它。
“覆盖”只有在某方法是基类的接口的一部分时才会出现。即,必须能将一个对象向上转型为它的基本类型并调用相同的方法。如果某方法为private,它就不是基类的接口的一部分。它仅是一些隐藏于类中的代码,只不过是具有相同的名称而已。但如果在导出类以相同的方法生成一个public、protected或包访问权限方法的话,该方法就不会产生在基类中出现的“仅具有相同名称”的情况。此时,你并没有覆盖该方法,仅是生成了一个新的方法。由于private方法无法触及而且能有效隐藏,所以除了把它看成是因为它所归属的类的组织结构的原因而存在外,其他任何事物都不需要考虑它。
3.final类
将某个类的整体定义为final 时,该类无法被继承。而且由于final类禁止继承,所以final类中所有的方法都隐式指定为final的,因为无法覆盖它们。
final 用于类或方法是为了防止方法间的链接被破坏。例如,假定类 X 的某个方法的实现假设了方法 M 将以某种方式工作。将 X 或 M 声明成 final 将防止派生类以这种方式重新定义 M,从而导致 X 的工作不正常。尽管不用这些内部相关性来实现 X 可能会更好,但这不总是可行的,而且使用 final 可以防止今后这类不兼容的更改。

PS:final,finally和finallize的区别

  1. final用于申明属性,方法和类,表示属性不可变,方法不可以被覆盖,类不可以被继承。
  2. finally是异常处理语句结构中,表示总是执行的部分。  
  3. finallize表示是object类一个方法,在垃圾回收机制中执行的时候会被调用被回收对象的方法。
您可能感兴趣的文章:
  • 详解Java编程中static关键字和final关键字的使用
  • Java中final关键字详解
  • java中final关键字使用示例详解
  • java关键字final使用方法详解
  • java final 和instanceof 关键字的区别
  • java 中的static关键字和final关键字的不同之处
  • 深入理解Java中的final关键字_动力节点Java学院整理