首页 > 技术文章 > 简单聊聊java中的final关键字

bedlimate 2018-02-07 00:04 原文

 

简单聊聊java中的final关键字
日常代码中,final关键字也算常用的。其主要应用在三个方面:

1)修饰类(暂时见过,但是还没用过);
2)修饰方法(见过,没写过);
3)修饰数据

那么,我们主要也是从一下几个方面探讨一下,主要是第三点。

一、final修饰类和方法
final修饰的类不可被继承(例如: String, Integer, Double, ....);
final修饰的方法不可被重写(例如: AtomicInteger中的大部分方法)

二、final修饰数据
1. final修饰变量
   分为局部变量和全局变量而言.如果是全局变量,不管你用不用,都必须而且只能赋值一次;
  例如:

public class FinalTest {
  private final int i;
}

  如果不赋值,就会报编译错误: Variable "xxx" might not have been initialized。

  如果是局部变量,如果不使用,可以不赋值。(当然,你得保证你的项目经理不会打死你)。

 

  1.1 final修饰的基本数据类型和不变对象(例如: String, 包装类以及jdk8新的日期时间类库)那就是真的什么都不能该变了(引用与对象之间的引用关系,对象的内容(基本类型就是值)的值都不能改变). 例如:

1   public class FinalTest {
2     
3         public static void main(String[] args) {
4     
5             final int a = 10;
6             final LocalDate localDate = LocalDate.now();
7         }
8     }

 

  如果再对a或者localDate进行赋值:就会报编译错误: cannot assign a value to final variable 'xxx'.

  可以说,final对不可变对象的引用的修饰和对基础类型的引用的修饰含义几乎是一样的。

  1.2 对于可变对象而言(常用的StringBuilder, 各种常见(List, Set, Map)的集合实现类),final仅仅只能保证引用和对象之前的引用关系不变,无法确保对象的内容(例如字段的值,容器内元素的个数)不变.

 

 public class FinalTest {
    
    public static void main(String[] args) {
       final StringBuilder sb = new StringBuilder("sb ");
       System.out.println("sb: " + sb);
sb.append("changed"); System.out.println("sb: " + sb); } }

  

-------------------------------------------------------------------------------
输出:
sb: sb
sb: sb changed


  3. final修饰的值类型(8种基本数据类型 + String)将会优化为编译期常量

  此处我们利用String的特性来测验一下:

public class FinalTest {

    public static void main(String[] args) {
        String a = "a";
        String ab = a + "b";
        System.out.println(ab == "ab");


        final String finalA = "a";
        String finalAB = finalA + "b";
        System.out.println(finalAB == "ab");
    }
} 

-------------------------------------------------------------------------------
输出:
false
true

  此处,我们可以对反编译FinalTest.class文件:

1 public class FinalTest {
2    public FinalTest() {
3   }
4
5    public static void main(String[] args) {
6        String a = "a";
7        String ab = a + "b";
8        System.out.println(ab == "ab");
9        String finalA = "a";
10       String finalAB = "ab";
11       System.out.println(finalAB == "ab");
12    }
13 }

对比第7行和第10行发现,第7行变量ab的值在编译期还是未知的(实际上在运行期,第7行的代码是这样执行的:

  String ab = new StringBuilder("").append(a).append("b").toString;

而在StringBuilder#toString()方法内,new了一个新的String实例,因此ab 和 "ab"不是一个实例,所以第8行输出false.),而第10行变量finalAB的值在编译期就已知了,由于String常量池的缓存特性,使得finalAB和"ab"是同一个实例,所以第11行输出true. 

这一点,有时候会带来一些问题。例如下面的例子:

 1 class A{
 2     public static final int A = 10;
 3 }
 4 
 5 
 6 class B{
 7     public B(){
 8         System.out.println(A.A);
 9     }
10 }

 

从源代码中,可以看到class B和class A有些关系。但是实际上,编译之后的class字节码,类A 和类B没有任何关系。编译之后,第8行A.A已经被替换为10了。如果这个时候,修改了这个A中常量的值,然后仅仅对A重新编译,就会导致类B的class文件中依然是10.这可能给程序运行代码一些问题。


final修饰方法参数
  `1这个在jdk中少见,但是在框架代码中常常见到.基本作用也就是防止方法调用者对参数在做赋值(其实这也是一种约定吧). 例如这样的场景: 现在有一组任务需要执行(任务可以并行),在这组任务全部执行完成之后,需要做一次清理缓存的操作; 可能的代码是这样的

    List<Task> tasks = ...;
        CountDownLatch countDoenLatch = new CountDownLatch(tasks.size());
        for(Task task : tasks){
            //线程池异步执行
            WORKER.submit(task);
        }
        cleanCache(countDownLatch);
-----------------------------------------------------------------------------------------------------        
        public void cleanCache(final CountDownLatch countDownLatch){
           countDownLatch.await();
           ...
        } 


此处cleanCache()方法中的参数CountDownLatch就需要使用final修饰。(如果对countDownLatch重新赋值,后续调用countDownLatch.await()会导致无限期等待)

小结:
final在修饰引用的时候,仅仅只能确保引用能且只能和某个对象建立引用关系(基本类型的值), 至于引用所指向的对象的内容是否可以改变,和这里的final没有任何关系,而是和这个对象是否是不变对象有关。 (通俗的将,使用final修饰某个引用的时候,这个final能够管得着的仅仅只是这个引用, 至于这个引起所指向的对象可不可变,它管不着。)

推荐阅读