五十一、final的三种用法

1、final的作用

final 关键字一共有三种用法,它可以用来修饰变量、方法或者类。

1.1 final 修饰变量

作用
关键字 final 修饰变量的作用,就是意味着这个变量一旦被赋值就不能被修改了。如果尝试给其赋值,会报编译错误。

目的
(1)第一个目的是出于设计角度去考虑的,比如希望创建一个一旦被赋值就不能改变的量,就可以使用 final 关键字。比如声明常量的时候。
(2)第二个目的是从线程安全的角度去考虑的。不可变的对象天生就是线程安全的,不需要额外进行同步等处理。如果 final 修饰的是基本数据类型,那么它自然就具备了不可变这个性质,所以自动保证了线程安全,去使用它也就非常放心。

赋值时机
被 final 修饰的变量的赋值时机,变量可以分为以下三种:

  • 成员变量,类中的非 static 修饰的属性;
  • 静态变量,类中的被 static 修饰的属性;
  • 局部变量,方法中的变量。

(1)成员变量
成员变量指的是一个类中的非 static 属性,对于这种成员变量而言,被 final 修饰后,它有三种赋值时机(或者叫作赋值途径)。

  • 第一种是在声明变量的等号右边直接赋值,例如:
public class FinalFieldAssignment1 {
    private final int finalVar = 0;
}
  • 第二种是在构造函数中赋值,例如:
class FinalFieldAssignment2 {
    private final int finalVar;

    public FinalFieldAssignment2() {
        finalVar = 0;
    }
}
  • 第三种就是在类的构造代码块中赋值(不常用),例如:
class FinalFieldAssignment3 {
    private final int finalVar;

    {
        finalVar = 0;
    }
}

对于 final 修饰的成员变量而言,必须从中挑一种来完成对 final 变量的赋值,而不能一种都不挑,这是 final 语法所规定的。

  • 空白 final
    如果声明了 final 变量之后,并没有立刻在等号右侧对它赋值,这种情况就被称为“空白 final”。这样做的好处在于增加了 final 变量的灵活性,比如可以在构造函数中根据不同的情况,对 final 变量进行不同的赋值,这样的话,被 final 修饰的变量就不会变得死板,同时又能保证在赋值后保持不变。用下面这个代码来说明:
/**
 * 描述:     空白final提供了灵活性
 */
public class BlankFinal {

    //空白final
    private final int a;

    //不传参则把a赋值为默认值0
    public BlankFinal() {
        this.a = 0;
    }

    //传参则把a赋值为传入的参数
    public BlankFinal(int a) {
        this.a = a;
    }
}

(2)静态变量
静态变量是类中的 static 属性,被 final 修饰后,只有两种赋值时机。

  • 第一种同样是在声明变量的等号右边直接赋值,例如:
/**
 * 描述:     演示final的static类变量的赋值时机
 */
public class StaticFieldAssignment1 {
    private static final int a = 0;
}
  • 第二种赋值时机就是它可以在一个静态的 static 初始代码块中赋值,这种用法不是很多,例如:
class StaticFieldAssignment2 {

    private static final int a;

    static {
        a = 0;
    }
}

需要注意的是,不能用普通的非静态初始代码块来给静态的 final 变量赋值。同样有一点比较特殊的是,static 的 final 变量不能在构造函数中进行赋值。

(3)局部变量
局部变量指的是方法中的变量,如果把它修饰为了 final,它的含义依然是一旦赋值就不能改变。对于 final 的局部变量而言,它是不限定具体赋值时机的,只要求在使用之前必须对它进行赋值即可。

这个要求和方法中的非 final 变量的要求也是一样的,对于方法中的一个非 final 修饰的普通变量而言,它其实也是要求在使用这个变量之前对它赋值。

(4)特殊用法:final 修饰参数
关键字 final 还可以用于修饰方法中的参数。在方法的参数列表中是可以把参数声明为 final 的,这意味着没有办法在方法内部对这个参数进行修改。例如:

/**
 * 描述:     final参数
 */
public class FinalPara {
    public void withFinal(final int a) {
        System.out.println(a);//可以读取final参数的值
//        a = 9; //编译错误,不允许修改final参数的值
    }
}

1.2 final 修饰方法

早期选择用 final 修饰方法的原因之一是为了提高效率,因为在早期的 Java 版本中,会把 final 方法转为内嵌调用,可以消除方法调用的开销,以提高程序的运行效率。不过在后期的 Java 版本中,JVM 会对此自动进行优化,所以不需要我们去使用 final 修饰方法来进行这些优化了。

目前使用 final 去修饰方法的唯一原因,就是锁定这个方法,就是说,被 final 修饰的方法不可以被重写,不能被 override。举一个代码的例子:

/**
 * 描述:     final的方法不允许被重写
 */
public class FinalMethod {

    public void drink() {
    }

    public final void eat() {
    }
}

class SubClass extends FinalMethod {
    @Override
    public void drink() {
        //非final方法允许被重写
    }

//    public void eat() {}//编译错误,不允许重写final方法

//    public final SubClass() {} //编译错误,构造方法不允许被final修饰
}

同时这里还有一个注意点,在下方写了一个 public final SubClass () {},这是一个构造函数,也是编译不通过的,因为构造方法不允许被 final 修饰。

特例:final 的 private方法
这里有一个特例,那就是用 final 去修饰 private 方法。先来看看下面这个看起来可能不太符合规律的代码例子:

/**
 * 描述:     private方法隐式指定为final
 */
public class PrivateFinalMethod {

    private final void privateEat() {
    }
}

class SubClass2 extends PrivateFinalMethod {

    private final void privateEat() {//编译通过,但这并不是真正的重写
    }
}

类中的所有 private 方法都是隐式的指定为自动被 final 修饰的,额外的给它加上 final 关键字并不能起到任何效果。由于这个方法是 private 类型的,所以对于子类而言,根本就获取不到父类的这个方法,就更别说重写了。在上面这个代码例子中,其实子类并没有真正意义上的去重写父类的 privateEat 方法,子类和父类的这两个 privateEat 方法彼此之间是独立的,只是方法名碰巧一样。

为了证明这一点,可以尝试在子类的 privateEat 方法上加个 Override 注解,这个时候就会提示“Method does not override method from its superclass”,意思是“该方法没有重写父类的方法”,就证明了这不是一次真正的重写。

1.3 final 修饰类

final 修饰类的含义很明确,就是这个类不可被继承。举个代码例子:

/**
 * 描述:     测试final class的效果
 */
public final class FinalClassDemo {
    //code
}

//class A extends FinalClassDemo {}//编译错误,无法继承final的类

这样设计,就代表不但我们自己不会继承这个类,也不允许其他人来继承,它就不可能有子类的出现,这在一定程度上可以保证线程安全。

比如非常经典的 String 类就是被 final 修饰的,所以我们自始至终也没有看到过哪个类是继承自 String 类的,这对于保证 String 的不可变性是很重要的

注意:假设给某个类加上了 final 关键字,这并不代表里面的成员变量自动被加上 final。事实上,这两者之间不存在相互影响的关系。

不过我们也记得,final 修饰方法的含义就是这个方法不允许被重写,而现在如果给这个类都加了 final,那这个类连子类都不会有,就更不可能发生重写方法的情况。所以,其实在 final 的类里面,所有的方法,不论是 public、private 还是其他权限修饰符修饰的,都会自动的、隐式的被指定为是 final 修饰的。

如果真的要使用 final 类或者方法的话,为了防止后续维护者有困惑,有必要或者说有义务说明原因,这样也不至于发生后续维护上的一些问题。

2、为什么加了 final 却依然无法拥有“不变性”?

2.1 什么是不变性?

如果对象在被创建之后,其状态就不能修改了,那么它就具备“不变性”。举个例子,比如下面这个 Person 类:

public class Person {
    final int id = 1;
    final int age = 18;
}

final 修饰对象时,只是引用不可变
这里有个非常重要的注意点,当用 final 去修饰一个指向对象类型(而不是指向 8 种基本数据类型)的变量时候,那么 final 起到的作用只是保证这个变量的引用不可变,而对象本身的内容依然是可以变化的。这一点同样适用于数组,因为在 Java 中数组也是对象。举个例子:

class Test {
    public static void main(String args[]) {
       final int arr[] = {1, 2, 3, 4, 5};  //  注意,数组 arr 是 final 的
       for (int i = 0; i < arr.length; i++) {
           arr[i] = arr[i]*10;
           System.out.println(arr[i]);
       }
    }
}

最后打印出来的是 10 20 30 40 50,而不是最开始的 1 2 3 4 5,这就证明了,虽然数组 arr 被 final 修饰了,它的引用不能被修改,但是里面的内容依然是可以被修改的。

2.2 final 和不可变的关系

关键字 final 可以确保变量的引用保持不变,但是不变性意味着对象一旦创建完毕就不能改变其状态,它强调的是对象内容本身,而不是引用,所以 final 和不变性这两者是很不一样的。

对于一个类的对象而言,必须要保证它创建之后所有内部状态(包括它的成员变量的内部属性等)永远不变,才是具有不变性的,这就要求所有成员变量的状态都不允许发生变化。

只有当一个类的成员变量全是基本数据类型时,将类中的属性都声明为 final 可保证对象具有不变性。所以不变性并不意味着,简单地使用 final 修饰所有类的属性,这个类的对象就具备不变性了。

3、为什么 String 被设计为是不可变的?

在 Java 中,字符串是一个常量,一旦创建了一个 String 对象,就无法改变它的值,它的内容也就不可能发生变化(不考虑反射这种特殊行为)。

调用 String 的 subString() 或 replace() 等方法,同时把 s 的引用指向这个新创建出来的字符串,这样都没有改变原有字符串对象的内容,因为这些方法只不过是建了一个新的字符串而已。

3.1 String 具备不变性背后的原因是什么呢?

来看下 String 类的部分重要源码:

public final class String
    implements Java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
    //...
}

这里面有个非常重要的属性,即 private final 的 char 数组,数组名字叫 value。它存储着字符串的每一位字符,同时 value 数组是被 final 修饰的,也就是说,这个 value 一旦被赋值,引用就不能修改了;并且在 String 的源码中可以发现,除了构造函数之外,并没有任何其他方法会修改 value 数组里面的内容,而且 value 的权限是 private,外部的类也访问不到,所以最终使得 value 是不可变的。

因为 String 类是被 final 修饰的,所以这个 String 类是不会被继承的,因此没有任何人可以通过扩展或者覆盖行为来破坏 String 类的不变性。这就是 String 具备不变性的原因。

3.2 String 不可变的好处

如果把 String 设计为不可变的,会带来以下这四个好处:
(1)字符串常量池
String 不可变的第一个好处是可以使用字符串常量池。在 Java 中有字符串常量池的概念,比如两个字符串变量的内容一样,那么就会指向同一个对象,而不需创建第二个同样内容的新对象。正是因为这样的机制,再加上 String 在程序中的应用是如此广泛,就可以节省大量的内存空间。

(2)用作 HashMap 的 key
String 不可变的第二个好处就是它可以很方便地用作 HashMap (或者 HashSet) 的 key。通常建议把不可变对象作为 HashMap的 key,比如 String 就很合适作为 HashMap 的 key。

对于 key 来说,最重要的要求就是它是不可变的,这样才能利用它去检索存储在 HashMap 里面的 value。由于 HashMap 的工作原理是 Hash,也就是散列,所以需要对象始终拥有相同的 Hash 值才能正常运行。如果 String 是可变的,这会带来很大的风险,因为一旦 String 对象里面的内容变了,那么 Hash 码自然就应该跟着变了,若再用这个 key 去查找的话,就找不回之前那个 value 了。

(3)缓存 HashCode
String 不可变的第三个好处就是缓存 HashCode。在 Java 中经常会用到字符串的 HashCode,在 String 类中有一个 hash 属性,代码如下:

/** Cache the hash code for the String */
private int hash;

这是一个成员变量,保存的是 String 对象的 HashCode。因为 String 是不可变的,所以对象一旦被创建之后,HashCode 的值也就不可能变化了,就可以把 HashCode 缓存起来。这样的话,以后每次想要用到 HashCode 的时候,不需要重新计算,直接返回缓存过的 hash 的值就可以了,因为它不会变,这样可以提高效率,所以这就使得字符串非常适合用作 HashMap 的 key。

而对于其他的不具备不变性的普通类的对象而言,如果想要去获取它的 HashCode ,就必须每次都重新算一遍,相比之下,效率就低了。

(4)线程安全
String 不可变的第四个好处就是线程安全,因为具备不变性的对象一定是线程安全的,不需要对其采取任何额外的措施,就可以天然保证线程安全。

由于 String 是不可变的,所以它就可以非常安全地被多个线程所共享,这对于多线程编程而言非常重要,避免了很多不必要的同步操作。

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