Java 中的 final 关键字

final 关键字的含义

final 在 Java 中是一个保留的关键字,可以声明成员变量、方法、类以及本地变量。一旦你将引用声明作 final,你将不能改变这个引用了,编译器会检查代码,如果你试图将变量再次初始化的话,编译器会报编译错误。

什么是 final 变量

凡是对成员变量或者本地变量(在方法中的或者代码块中的变量称为本地变量)声明为 final 的都叫作 final 变量。final 变量经常和 static 关键字一起使用,作为常量。下面是 final 变量的例子:

public static final String LOAN = "loan";
LOAN = new String("loan");    // invalid compilation error`

对于一个 final 变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。

什么是 final 方法

使用 final 方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。final 方法比非 final 方法要快,因为在编译的时候已经静态绑定了,不需要在运行时再动态绑定。在早期的 Java 实现版本中,会将 final 方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升。在最近的 Java 版本中,不需要使用 final 方法进行这些优化了。

因此,如果只有在想明确禁止该方法在子类中被覆盖的情况下才将方法设置为 final 的。

class PersonalLoan {
    public final String getName() {
        return "personal loan";
    }
}
 
class CheapPersonalLoan extends PersonalLoan {
    @Override
    public final String getName() {
        return "cheap personal loan";   // compilation error: overridden method is final
    }
}

注:类的 private 方法会隐式地被指定为 final 方法。

什么是 final 类

使用 final 来修饰的类叫作 final 类。final 类通常功能是完整的,它们不能被继承。Java 中有许多类是 final 的,譬如 String,Interger 以及其他包装类。下面是 final 类的实例:

final class PersonalLoan {
 
}
 
class CheapPersonalLoan extends PersonalLoan {  // compilation error: cannot inherit from final class
 
}

注:final 类中的所有成员方法都会被隐式地指定为 final 方法。

final 关键字的好处

  1. final 关键字提高了性能。JVM 和 Java 应用都会缓存 final 变量。

  2. final 变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销。

  3. 使用 final 关键字,JVM 会对方法、变量及类进行优化。

不可变类

创建不可变类要使用 final 关键字。不可变类是指它的对象一旦被创建了就不能被更改了。String 是不可变类的代表。不可变类有很多好处,譬如它们的对象是只读的,可以在多线程环境下安全的共享,不用额外的同步开销等等。

类的 final 变量和普通变量有什么区别

当用 final 作用于类的成员变量时,成员变量(注意是类的成员变量,局部变量只需要保证在使用之前被初始化赋值即可)必须在定义时或者构造器中进行初始化赋值,而且 final 变量一旦被初始化赋值之后,就不能再被赋值了。

下面就是 final 变量和普通变量的区别了,当 final 变量是基本数据类型以及 String 类型时,如果在编译期间能知道它的确切值,则编译器会把它当做编译期常量使用。也就是说在用到该 final 变量的地方,相当于直接访问的这个常量,不需要在运行时确定。这种和 C 语言中的宏替换有点像。因此在下面的一段代码中,由于变量 b 被 final 修饰,因此会被当做编译器常量,所以在使用到 b 的地方会直接将变量 b 替换为它的值。而对于变量 d 的访问却需要在运行时通过链接来进行。不过要注意,只有在编译期间能确切知道 final 变量值的情况下,编译器才会进行这样的优化:

public static void main(String[] args) throws Exception {

    String a = "hello2";
    final String b = "hello";
    String d = "hello";
    String c = b + 2;
    String e = d + 2;
    int i = 2;
    String f = "hello" + i;
    System.out.println(a == c);    // true
    System.out.println(a == e);    // false
    System.out.println(a == f);     // false 
    System.out.println(a.equals(c));    // true
    System.out.println(a.equals(e));    // true
    System.out.println(a.equals(f));    // true

    String aa = "hello" + 2;
    System.out.println(aa == a);    // true

    final String bb = getHello();
    System.out.println(bb == a);    // false
        
}

private static String getHello() {
    return "hello";
}

关于 final 参数的问题

关于网上流传的 “当你在方法中不需要改变作为参数的对象变量时,明确使用 final 进行声明,会防止你无意的修改而影响到调用方法外的变量” ,这句话其实是不恰当的
因为无论参数是基本数据类型的变量还是引用类型的变量,使用 final 声明都不会达到上面所说的效果

public class Test {
    public static void main(String[] args)  {
        MyClass myClass = new MyClass();
        StringBuffer buffer = new StringBuffer("hello");
        myClass.changeValue(buffer);
        System.out.println(buffer.toString());
    }
}
 
class MyClass {
    void changeValue(final StringBuffer buffer) {
        buffer.append("world");
    }
}

运行这段代码就会发现输出结果为 helloworld。很显然,用 final 进行修饰并没有阻止在 changeValue 中改变 buffer 指向的对象的内容。有人说假如把 final 去掉了,万一在 changeValue 中让 buffer 指向了其他对象怎么办。有这种想法的朋友可以自己动手写代码试一下这样的结果是什么,如果把 final 去掉了,然后在 changeValue 中让 buffer 指向了其他对象,也不会影响到 main 方法中的 buffer,原因在于 java 采用的是值传递,对于引用变量,传递的是引用的值,也就是说让实参和形参同时指向了同一个对象,因此让形参重新指向另一个对象对实参并没有任何影响

所以 final 参数的作用仅仅是:

  1. 修饰基本类型(非引用类型)。这时参数的值在方法体内是不能被修改的,即不能被重新赋值。否则编译就通不过
  2. 修饰引用类型。这时参数变量所引用的对象是不能被改变的。作为引用的拷贝,参数在方法体里面不能再引用新的对象。否则编译通不过

匿名内部类中使用的外部局部变量为什么只能是 final 变量

abstract class Bird {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public abstract int fly();
}

public class OuterClass {

    public void test(Bird bird){
        System.out.println(bird.getName() + "能够飞 " + bird.fly() + "米");
    }

    public static void main(String[] args) {
        OuterClass test = new OuterClass();
        int x = 1000;
        test.test(new Bird() {

            public int fly() {
                // x = 1;      Error:从内部类引用的本地变量必须是最终变量或实际上的最终变量
                return x;
            }

            public String getName() {
                return "大雁";
            }
        });
    }
}

Java 内部类与外部持有的是值相同的不同的变量,所以他们两者是可以任意变化的,也就是说在内部类中对属性的改变并不会影响到外部的形参,然而这从程序员的角度来看这是不可行的,毕竟站在程序的角度来看这两个根本就是同一个,如果内部类变了,而外部方法的形参却没有改变这是难以理解和不可接受的,所以为了保持参数的一致性,就规定使用 final 来避免形参的不改变。

简单理解就是,拷贝引用,为了避免引用值发生改变,例如被外部类的方法修改等,而导致内部类得到的值不一致,于是用 final 来让该引用不可改变。

故如果定义了一个匿名内部类,并且希望它使用一个其外部定义的参数,那么编译器会要求该参数引用是 final 的。

最后,Java 8 更加智能:如果局部变量被匿名内部类访问,那么该局部变量相当于自动使用了 final 修饰。

内存模型的作用 – 防止变量从构造方法中逸出

除非使用锁或 volatile 修饰符,否则无法从多个线程安全地读取一个域。还有一种情况可以安全地访问一个共享域,即这个域声明为 final 时,虚拟机会有禁止指令重排的保证。

在多线程环境下,域变量是有可能从构造方法中逸出的,也就是说线程有可能读到还没有被构造方法初始化的域变量的值。比如:

class Foo {
    int a;

    Foo(int v) {
        a = v;
    }
}

如果是在多线程环境下,一个线程 A 在创建 Foo 的对象,另一个线程 B 在读对象的 a 的值,则 B 是有可能读到未正确初始化 a 的值(默认初始值 0)。这就是域变量从构造方法中逸出。当然对 a 的操作并不是线程安全的,如果多个线程在读写这个值,仍然需要进行同步。

关键字 final 可以禁止虚拟机指令重排,从而保证了构造方法执行完毕前 final 修饰的变量一定是初始化过了的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容

  • 谈到final关键字,想必很多人都不陌生,在使用匿名内部类的时候可能会经常用到final关键字。另外,Java中的...
    WilsonMing阅读 616评论 0 5
  • 在Java中,final关键字可以用来修饰类、方法和变量(包括成员变量和局部变量)。下面就从这三个方面来了解一下f...
    indexImprov阅读 229评论 0 1
  • 一、基本用法 ** 1. 修饰类 **当final修饰一个类时,表明这个类不能被继承。final类中的成员变量可以...
    薛晨阅读 535评论 0 0
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,137评论 11 349
  • 今天小可爱兔兔就已经跨入十八岁的门槛了。很感激。很幸运。能够与你们。结实你们这些可爱的人。真的很幸运。三生...
    俊阿兔阅读 242评论 0 0