0%

final修饰符

一、含义

final修饰符能够修饰“类,变量,方法”。

1.1、类

final修饰符修饰“类”,表示该类不允许被继承,因此“类”只能是“具体类,枚举”,而不能是“抽象类,接口,注解”。

1.2、变量

final修饰符修饰“变量”,表示变量在被使用前必须“被显式初始化,且只能被显式初始化一次”。
“只能被显式初始化一次”的含义:如果是基本类型变量,只能被显式初始化一次,不能进行第二次显式初始化,即第一次显式初始化后变量值保持不变;如果是引用类型变量,只能被显式初始化一次,不能进行第二次显式初始化,即第一次显式初始化后变量值保持不变,但是需要注意的是,引用类型变量所指对象的内容是允许被改变的。
“变量”分为3种:“字段,方法形式参数变量,局部变量”。

1.2.1、字段

字段的“默认初始化”不被认为是“显式初始化”,只有“定义初始化,实例初始化语句初始化,构造器初始化”(针对普通字段)和“定义初始化,静态初始化语句初始化”(针对静态字段)才被认为是“显式初始化”。

1.2.2、方法形式参数变量

方法形式参数变量的初始化天然的是“显式初始化”,因为不存在“默认初始化”。

1.2.3、局部变量

局部变量的初始化天然的是“显式初始化”,因为不存在“默认初始化”。

1.3、方法

final修饰符修饰“方法”,表示该方法不允许被覆盖,因此显而易见的是:final修饰符不能跟abstract修饰符共用。

二、编译期常量

字段或者局部变量(方法形式参数变量不能进行定义初始化,因此天然不满足成为编译期常量的基本条件)只有满足以下两个条件时才能被称为“编译期常量”:

  1. 由final修饰符修饰
  2. 字段或者局部变量为“基本类型或者String类型”,进行“定义初始化”,且在定义初始化时使用“确切的值”进行赋值,而不需要通过间接的运算(虚拟机会对满足条件的+,-,*,/等运算进行编译期优化,具体的条件描述笔者不知,可认为这些经过编译期优化的+,-,*,/等运算不属于上述“间接的运算”)

本质上,“类”只包括“具体类和接口”,为进行完备介绍,分别以具体类和接口中的编译期常量进行举例说明。

备注
接口中的字段默认由“public static final”修饰符组合修饰。

2.1、编译期常量识别和混淆

以下示例代码给出了几个“编译期常量”和“易被混淆为编译期常量”的例子。

具体类中的编译期常量:

package modifier;

import java.util.Random;

class CompileConstantExampleClass {

  private static final Random rand = new Random();

  // 编译期常量
  final int a = 10;

  // 编译期常量
  static final boolean b = true;

  // 编译期常量
  final String c = "hello";

  // 编译期常量
  static final String d = "world";


  // 非编译期常量
  final int e = new Integer(10);

  // 非编译期常量
  static final int f = rand.nextInt(10);

  // 非编译期常量
  final String g = new String("hello");

  // 编译期常量,编译期优化:编译时完成拼接
  static final String h = "wor" + "ld";

  // 编译期常量
  static final int p = 10 + 20;

  // 编译期常量
  static final String q = d + " beautiful";

  // 编译期常量
  static final int r = 10 - 20;

  // 编译期常量
  static final int s = 10 * 20;

  // 编译期常量
  static final int t = 20 / 10;

  // 非编译期常量
  static final String i = "world" + rand.nextInt(10) + "ld";

  // 非编译期常量
  final int j;

  {
    j = 20;
  }

  // 非编译期常量
  static final String k;

  static {
    k = "world";
  }

  public static void main(String[] args)
  {
    System.out.println(CompileConstantExampleClass.b);
    System.out.println(CompileConstantExampleClass.d);
    System.out.println(CompileConstantExampleClass.f);
    System.out.println(CompileConstantExampleClass.h);
    System.out.println(CompileConstantExampleClass.i);
    System.out.println(CompileConstantExampleClass.k);
    System.out.println(CompileConstantExampleClass.p);
    System.out.println(CompileConstantExampleClass.q);
    System.out.println(CompileConstantExampleClass.r);
    System.out.println(CompileConstantExampleClass.s);
    System.out.println(CompileConstantExampleClass.t);

    CompileConstantExampleClass compileConstantExampleClass = new CompileConstantExampleClass();
    System.out.println(compileConstantExampleClass.a);
    System.out.println(compileConstantExampleClass.c);
    System.out.println(compileConstantExampleClass.e);
    System.out.println(compileConstantExampleClass.g);
    System.out.println(compileConstantExampleClass.j);

    // 编译期常量
    final int l = 10;

    // 编译期常量
    final String m = "world";

    // 非编译期常量
    final int n = rand.nextInt(10);

    // 非编译期常量
    final String o = "hell" + rand.nextInt(10) + "o";

    System.out.println(l);
    System.out.println(m);
    System.out.println(n);
    System.out.println(o);

  }

}

接口中的编译期常量:

package modifier;

import java.util.Random;

public interface CompileConstantExampleInterface {

  int a = 10;

  String b = "hello";

  Random rand = new Random();

  int c = rand.nextInt(10);

  String d = "he" + rand.nextInt(10) + "llo";

}

class VisitClass {

  public static void main(String[] args) {
    System.out.println(CompileConstantExampleInterface.a);
    System.out.println(CompileConstantExampleInterface.b);
    System.out.println(CompileConstantExampleInterface.c);
    System.out.println(CompileConstantExampleInterface.d);
  }

}

2.2、编译期常量访问

根据Java编程思想#复用类可知,第一次访问类的“静态字段”会触发执行“类对象初始化”,但是有个例外:如果“静态字段”是“编译期常量”,则第一次访问该“静态字段”并不会触发执行“类对象初始化”。
具体原因:虚拟机会对访问“编译期常量”的源代码进行编译期优化,优化后等效于“直接配定相应常量值,而不再引用原字段或者局部变量”,因此自然不会触发执行“类对象初始化”。
以下示例代码给出了几个例子。

具体类中的编译期常量:

package modifier;

class A {

  static {
    System.out.println("触发类对象初始化的标志");
  }

  static int a = 10;

  static final int b = 20;

  static String c = "hello";

  static final String d = "world";

  static final String e = "wor" + "ld";
}

public class CompileConstantInClass {

  public static void main(String[] args) {

    //每次只访问A中一个字段,进行验证

//    System.out.println(A.a);
//    System.out.println(A.b);
//    System.out.println(A.c);
//    System.out.println(A.d);
    System.out.println(A.e);
  }

}

接口中的编译期常量:

package modifier;

import java.util.Random;

interface B {

  Random rand = new Random();

  int a = 10;

  int b = C.f();

  int c = rand.nextInt(10);

  String d = "hel" + "lo";
}

class C {

  public static int f() {
    System.out.println("调用C类的f()方法");
    return 10;
  }

}

public class CompileConstantInInterface {

  public static void main(String[] args) {

    //每次只访问B中一个字段,进行验证

//    System.out.println(B.a);
//    System.out.println(B.b);
//    System.out.println(B.c);
    System.out.println(B.d);

  }
}

**备注:** 可借助`javap -c`命令进行编译期常量的相关验证。
您的支持将鼓励我继续分享!