java关键字final浅谈

上一篇我们提到关键finally,这一篇我们来看看final。这两者区别很大的。
final可以修饰:数据、方法和类

final数据
在我们程序设计中,往往会涉及到一些常量, 常量是程序运行时恒定不变的量,许多程序设计语言都有某种方法,向编译器告知一块数据是恒定不变的,例如C++中的const和Java中的final。而常量主要应用下述两个方面:
(1)编译期常数,它永远不会改变 (编译时常量)

public class FinalTest {
    //编译时常量
    final int i1 =9;
    static final int I2 = 99; //变量需大写
    public void method(){
    }
    public static void main(String[] args) {
        FinalTest fd1 = new FinalTest();
        //fd1.i1++; // 编译报错不能改变值
        //FinalTest.I2++;// 编译报错不能改变值
    }
}

对于编译器的常量,编译器可将常量值“封装”到需要的技术过程里。也就是说计算时间提前执行,从而节省运行时的一些开销。比如我们常用的常量类。在java中,这些形式的常数必须属于基本类型,而且要用final关键字进行表达。并且在定义时给出一个值。

注意:对于含有固定初始化值(即编译期常数)的 fianl static 基本数据类型,它们的名字根据规则要全部采用大写

(2)在运行期初始化的一个值,我们不希望它发生变化(运行时常量)

public class FinalTest {
    //运行期常量
    int i3 = (int)(Math.random()*20);
    final int i4 = (int)(Math.random()*20);
    static final int i5 = (int)(Math.random()*20); 

    public void print(String id){
        System.out.println(
                id + ": " + "i4 = " + i4 +
                ", i5 = " + i5);
    }
    public static void main(String[] args) {
        FinalTest fd1 = new FinalTest();
        fd1.print("fd1");
        FinalTest fd2 = new FinalTest();
        fd1.print("fd1");
        fd2.print("fd2");
        fd2.print("fd1");
    }
}
运行结果:
fd1: i4 = 13, i5 = 17
fd1: i4 = 13, i5 = 17
fd2: i4 = 6, i5 = 17
fd1: i4 = 13, i5 = 17

从上面的列子中可以看出,并非说我用final修饰了某样东西,那么它的值就一定能在编译期确定。i4和i5证明了这一点,它们是在运行期使用的随机数。例子的这一部分也向大家揭示出将final 值设为 static 和非 static 之间的差异。只有当值在运行期间初始化的前提下,这种差异才会揭示出来。
从结果我们可以知道,对于i4当同一个对象调用时,不管调用多少次,它的值会一直保持为13。而对于i5的值不会由于创建了另一个 FinalData 对象而发生改变。那是因为它的属性是 static,而且在载入时初始化,而非每创建一个对象时初始化。

注意:i5 在编译期间是未知的,所以它没有大写。

被final修饰的基本数据类型和对象的区别
对于基本数据类型,一旦被final修饰,那么它的值将不会再发送改变。对于final修饰的变量必须给定一个值。

而对于一个对象就没那么简单了,我们知道一旦我们new了一个对象,编译器会在堆空间中给这个对象分配一块空间,我们通过一个指引指向这个对象。一旦对象指引被final修饰,那么它的指引就会被固定不再改变(也就是说这个指引永远不能指向另一个对象),但需要注意的是对象本身是可以改变的。这点也是容易被许多人误解的地方。

class Value{
    int i =1;
}
public class FinalTest {
    //编译时常量,修饰的基本类型
    final int i1 =9;
    //一般对象
    Value v1 = new Value();
    //被final修饰的对象
    final Value v2 = new Value();
    //final Value v4;  // 编译报错,没有实例化

    //Arrays数组也是对象
    final int[] a = {1, 2, 3, 4, 5};

    public static void main(String[] args) {
        FinalTest fd1 = new FinalTest();
        //fd1.i1++; // 编译报错不能改变值
        fd1.v2.i++; // 对象不是常量,对象本身可以改变
        fd1.v1 = new Value(); // 可以new对象 ---没有被final修饰
        for(int i = 0; i < fd1.a.length; i++)
        fd1.a[i]++; // 对象不是常量,本身可以改变
        //! fd1.v2 = new Value(); // 无法改变对象的指引
        //! fd1.a = new int[3];  //
    }
}

从 v1 到 v4 的变量向我们揭示出 final 修饰对象指引的含义。正如大家在 main()中看到的那样,并不能认为由于 v2属于 final,所以就不能再改变它的值(对象本身可以改变)。然而,我们确实不能再将 v2 绑定到一个新对象(不能改变指引指向另一个对象),因为它的属性是final。这便是 final 对于一个句柄的确切含义。我们会发现同样的含义亦适用于数组,后者只不过是另一种类型的句柄而已。

空白 final
Java 1.1 允许我们创建“空白 final”,它们属于一些特殊的字段。尽管被声明成 final,但却未得到一个初始值。无论在哪种情况下,空白 final 都必须在实际使用前得到正确的初始化。而且编译器会主动保证这一规定得以贯彻。然而,对于 final 关键字的各种应用,空白 final 具有最大的灵活性。举个例子来说,位于类内部的一个 final 字段现在对每个对象都可以有所不同,同时依然保持其“不变”的本质。下面列出一个例子:

public class BlankFinal {
    final int i = 0; // 
    final int j; // 空白 final
    final Poppet p; // 空白 final指引

    // 空白 finals 必须被初始化
    BlankFinal() {
        j = 1; 
        p = new Poppet();
    }

    public static void main(String[] args) {
        BlankFinal bf = new BlankFinal();
    }

}

final 自变量
我们可以将自变量设成 final 属性,方法是在自变量列表中对它们进行适当的声明。这意味着在一
个方法的内部,我们不能改变自变量句柄指向的东西。如下所示:

public class FinalArguments {
    void with(final Gizmo g) {
        // ! g = new Gizmo(); // 参数被final修饰
        g.spin();
    }

    void without(Gizmo g) {
        g = new Gizmo(); // OK -- g not final
        g.spin();
    }

    // void f(final int i) { i++; } // 不能改变
    // 可以读取被自变量修饰的值:
    int g(final int i) {
        return i + 1;
    }

    public static void main(String[] args) {
        FinalArguments bf = new FinalArguments();
        bf.without(null);
        bf.with(null);
    }
}

注意:此时仍然能为 final 自变量分配一个 null(空)句柄,同时编译器不会捕获它。这与我们对非 final 自变量采取的操作是一样的。方法 f()和 g()向我们展示出基本类型的自变量为 final 时会发生什么情况:我们只能读取自变量,不可改变它。

final方法
之所以要使用 final 方法,可能是出于对两方面理由的考虑。
第一个是为方法“上锁”,防止任何继承类改变它的本来含义。设计程序时,若希望一个方法的行为在继承期间保持不变,而且不可被覆盖或改写,就可以采取这种做法(当然我们也可以采用private修饰父类方法达到同样的目的)。

第二个理由是程序执行的效率。将一个方法设成 final 后,编译器就可以把对那个方法的所有调用都置入“嵌入”调用里。只要编译器发现一个 final 方法调用,就会(根据它自己的判断)忽略为执行方法调用机制而采取的常规代码插入方法(将自变量压入堆栈;跳至方法代码并执行它;跳回来;清除堆栈自变量;最后对返回值进行处理)。相反,它会用方法主体内实际代码的一个副本来替换方法调用。这样做可避免方法调用时的系统开销。当然,若方法体积太大,那么程序也会变得雍肿,可能感觉不到嵌入代码所带来的任何性能提升。因为任何提升都被花在方法内部的时间抵消了。 Java 编译器能自动侦测这些情况,并颇为“明智”地决定是否嵌入一个 final 方法。然而,最好还是不要完全相信编译器能正确地作出所有判断。通常,只有在方法的代码量非常少,或者想明确禁止方法被覆盖的时候,才应考虑将一个方法设为final。

注意:类内所有 private 方法都自动成为 final。由于我们不能访问一个 private 方法,所以它绝对不会被其他方法覆盖(若强行这样做,编译器会给出错误提示)。我们也可手动为一个 private 方法添加 final 指示符,但这样多此一举。

final 类
假如我们的类肯定不需要进行任何改变;或者出于安全方面的理由,我们不希望进行子类化(被继承)。或者我们还考虑到执行效率的问题,并想确保涉及这个类各对象的所有行动都要尽可能地有效。我们就可以将这个类修饰成final。比如我常用的String类,Math类等等,都是final类。
当一个类被修饰为final,结果只是禁止进行继承—— 没有更多的限制。它的数据成员可以是final也可以不是,这个由我们自己选择。然而,由于它禁止了继承,所以一个 final 类中的所有方法都默认为 final。因为此时再也无法覆盖它们。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值