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 修饰的变量一定是初始化过了的。

你可能感兴趣的:(Java 中的 final 关键字)