`
Mysun
  • 浏览: 270510 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

Java final关键字详解

阅读更多
在java中,final关键字可以有如下的用处:
  1. final关键字可以被加到类的声明中,final类是不允许继承的;
  2. final关键字可以被加到方法声明中,final方法是不允许重写的(override),这个效果同私有方法一样;
  3. final关键字可以被家到属性或者变量的声明中,final属性或者变量一旦赋值之后就不允许再发生变化。对于基本类型(primitive type),比如int、double、long、byte等,一旦被生命为final,我们就可以将其当作常量来看待,但是对于引用类型或者数组(数组在java中也是对象)来说,则不是。虽然一个引用类型被赋值之后无法发生变化,但是我们仍然可以修改被引用的那个对象或者数组中的元素。因此在java中,常量的定义与其他语言相比可能会有点差异,在java中,常量的定义是:被声明为final的基本类型或者是通过编译时常量初始化的String类型;
  4. 方法的参数可以被声明为final,这些参数一旦初始化之后,在方法体中是不能改变其值的。基本上,在接口中将方法参数声明为final是没有什么意义的,因为java的编译器并没有强制要求在继承接口时,方法的参数也一定要带上final。也就是说,一个方法的参数是否为final并没有被当成是方法签名中的一部分,这个对于类的继承也是一样的。关于这一点,大家可以写个简单的程序测试一下;
  5. 本地类的方法中只能使用final类型的本地变量;
  6. 通常情况下,将方法或者变量生命为final类型有助于提高程序运行时的性能;

下面会对第5,第6点做一个详细的介绍,其他的几点都比较直观,容易理解,第5和第6点涉及到编译器如何产生字节码以及java中对堆区和栈区的使用,会稍微复杂一点。
1.本地类的方法中使用本地变量
public class FinalField {
public static void main(String[] args) {
	final int x = 0;
	final int y = 0;
	Foo foo = new Foo() {
		public void doBar() {
			int z = x + y;
			System.out.println(z);
		}
	};
	foo.doBar();
}
}

interface Foo {
	void doBar();
}

上面的代码中,定一个了一个Foo接口,在FinalField类中,在main方法中以匿名类的方式创建了一个Foo接口的实现,然后赋值给foo变量。在这里,我们创建的这个匿名的Foo接口的实现就是一个本地类。在这个本地类中,我们使用了在main方法中定义的两个变量x和y,将它们相加之后输出到控制台。
为了在本地类的doBar方法中使用x和y,我们必须将x和y声明成final,否则编译器是会报错的。其原因还要从Java是一个基于栈的语言说起。Java程序执行时,运行时环境会为每一个线程分配一个线程栈,一个线程在执行过程中的每次方法调用都会在这个栈中分配一个栈帧,而方法中使用到的参数、变量都会在这个栈帧中进行分配。我们可以通过配置JVM的参数来指定线程栈占用空间的最大值,由于每次方法调用都需要在线程栈中分配一个栈帧,因此线程栈的大小直接关系到我们可以执行几次方法调用。一般来说线程栈的大小默认为4K,足够一个线程正常地执行所有的方法调用。但是,对于需要递归调用的方法来说,由于受到线程栈大小的限制,其计算能力也会受到影响。比如,比较经典的斐波那契数的计算就是一个递归的算法,理论上是可以计算任何输入的参数的,但是由于受到线程栈大小的影响,真正可计算的数值的大小是有限制的。
通过下面这个简单的程序及其字节码,我们来体验一下Java程序是如何利用栈来执行操作的。
public class ThreadStack {
	public int run() {
		int x = 0;
		int y = 1;
		
		int z = x + y;
		
		return z;
	}
}

上面这段代码的字节码如下,这里为了简单起见只给出了run方法的字节码。
iconst_0
istore_1
iconst_1
istore_2
iload_1
iload_2
iadd
istore_3
iload_3
ireturn


字节码中的第0和1行对应源代码中的第3行,iconst指令的含义是将常数0压栈,istore指令的含义是从栈顶弹出一个值,然后赋值给变量x,字节码的第2和第3行是给变量y赋值,对应与源代码中的第4行,同样使用了iconst和istore指令。完成了对x和y变量的赋值之后,字节码的第4和第5行执行了两遍iload指令,这个指令的含义是将本地变量的值压入栈中,通过两次调用就是分别将x和y的值压入栈中。字节码第6行是一个加法指令,这个指令会从栈中弹出两个值,然后执行加法操作,然后将结果值再压入栈中。字节码的第7行是从栈顶弹出一个值然后赋值给变量z,字节码的第8行则是将变量z的值压入栈中,最后的ireturn指令则是从栈中弹出栈顶元素,然后压入调用这个方法的调用者的栈帧中。假设我们在main方法中调用了ThreadStack的run方法,那么这个返回值就会被压入main方法所在栈帧的顶部。一个方法结束之后,这个方法对应的栈帧也就消失了,留下的空间会分配给其他的方法调用所对应的栈帧。
回过头来再说本节开头的那个例子,main方法调用结束之后,它所对应的栈帧就被回收了,在main方法中声明的x和y变量也就消失了。而我们知道,在Java中,所有的对象都是在堆中被分配的,也就是说,foo所指向的那个对象是在堆中,而不是在栈中的。由于存在与堆中的对象的生命周期与存在与栈中的变量的生命周期不同(堆中对象的声明周期都是比栈中变量的声明周期要长的),因此Java是不允许堆中的对象直接使用栈中分配的变量的。碰到本节开头的例子中的情况,Java其实是将x和y复制了一份给foo所指向的那个对象使用的。这就要求x和y在后面的执行过程中不能够发生任何的变化,否则会就会造成执行上的错误。这就是为什么本地对象只能使用被声明成final的本地变量。
另外,在复制final类型的变量给本地方法使用的时候,Java针对引用类型和基本数值类型所采用的方法是不同的。我们在前面也提到过,本声明成final的基本数值类型可以被当作编译期常量来使用,因此java的编译器可以直接把这些数值放入到字节码中。而对于引用类型,编译器则是通过生成构造函数的形式来完成复制的。感兴趣的朋友可以通过改写本节开头的类,将x和y声明成String类型,然后用javap -verbose来看看生成的字节码有何不同。
2.为什么final有助与程序的性能
还是先来看一段程序,
public class FinalField {
	public static void main(String[] args) {
		ValueHolder vh = new ValueHolder();
		int v = vh.v;
		System.out.println(v);
	}
	
	public static class ValueHolder {
		private int v = 0;
	}
}

这个程序在FinalField类中定一个了一个子类,这样就可以在FinalField的任何方法中直接使用这个子类中的属性,代码会简单一些,同时也足够用来说明问题。
上面这个版本中,ValueHolder的v属性没有被声明成final,我们来看下编译器为我们生成的FinalField类的字节码中,是如何来访问ValueHolder中的v属性的。在源代码中是第4行。
invokestatic	#19; //Method com/taobao/tianxiao/FinalField2$ValueHolder.access$0:(Lcom/taobao/tianxiao/FinalField2$ValueHolder;)I
istore_2

我们会看到生成的字节码中有这么两条语句,第一条语句执行一个invokestatic指令,这个指令是调用静态方法的指令,而被调用的方法是FinalField2$ValueHolder的access$0方法,调用完成之后,将栈顶的值赋值给变量v。这就奇怪了,我们并没有在ValueHolder中定一个叫做access$0的方法,这是怎么会是呢?我们先来看下ValueHolder的字节码,打开之后可以发现果然有一个叫做access$0的方法定义存在,如下所示。那么既然这个方法不是我们自己定义的,那肯定就是编译器帮我们自动生成的。
static int access$0(com.taobao.tianxiao.FinalField2$ValueHolder);
  Code:
   Stack=1, Locals=1, Args_size=1
       aload_0
       getfield	#12; //Field v:I
       ireturn
  LineNumberTable: 
   line 11: 0

生成的access$0中的字节码很简单,就是去取传进来的ValueHolder对象中的v属性,然后返回。
从上面的介绍可以看到,虽然我们在源代码中只是简单的写了一句int v=vh.v,但是编译器生成的代码中,是执行了一次方法调用的。那么如果把ValueHolder中的v声明成final,会是什么情况呢?
iconst_0
istore_2

从生成的字节码来看,已经没有了之前对access$0方法的调用了,取而代之的是一条iconst_0指令,也就是直接将0压入栈顶了。通过检查ValueHolder的字节码,发现将v设置成声明成final之后,编译器也确实没有为我们生成access$0方法。从这里可以看出,将ValueHolder的v声明成final之后,会将原本需要方法调用的地方,替换成直接压常量入栈,由于减少了方法调用,程序的性能自然会提高一下。但是仔细观察FinalField的字节码会发现,在将ValueHolder的v声明成final之后,与原来相比却多了如下的两行代码,
invokevirtual	#19; //Method java/lang/Object.getClass:()Ljava/lang/Class;
pop
iconst_0
istore_2

着两行代码被放置在iconst_0指令之前,意思是调用一下vh这个变量所指向的ValueHolder对象的getClass()方法,之后又将返回值直接丢弃掉(pop的意思就是直接将栈顶元素弹出)。这两行代码似乎是没有什么任何意义的,因为不管怎么样,v的值都会被设置成0。想来想去,只有一个解释是正确的,那就是用来验证一下vh这个变量是不是null,由于后面直接用了常量,因此对vh变量的null检查就需要额外的步骤来完成。那么有没有办法去掉这个检查,真正地让编译器直接使用常量呢,答案是将ValueHolder中的v属性声明成static final。这里就不在列出字节码了,感兴趣的话可以自己试一下。
上面的讨论针对的是基本数值类型,对通过编译器常量初始化String对象也是适用的,那么引用类型又会是什么情况呢?让我们来改一下本节最开始的时候的那段程序,如下,
public class FinalField {
	public static void main(String[] args) {
		ValueHolder vh = new ValueHolder();
		String v = vh.v;
		System.out.println(v);
	}
	
	final public static class ValueHolder {
		public String v = new String();
	}
}

通过查看字节码,正如我们所预期的,main函数中对ValueHolder的v属性的访问是通过access$0这个由编译器自动为我们生成的函数来完成的。字节码如下:
invokestatic	#19; //Method com/taobao/tianxiao/FinalField2$ValueHolder.access$0:(Lcom/taobao/tianxiao/FinalField2$ValueHolder;)Ljava/lang/String;


那么将ValueHolder的v声明成final又会是什么情况呢?答案是没有任何变化,main函数对ValueHolder中的v属性的访问仍然是通过access$0来完成的。
综上所述,我们可以得出如下几点结论:
  1. 将类中的引用类型的属性声明成final不会对程序生成的字节码造成任何的改变,仅仅可以帮助编译器确定这个属性在被赋值之后不会被修改;
  2. 将类中的基本数值类型以及用编译器常量初始化的String类型的属性声明成final,确实会让编译器对访问这些属性的操作进行优化,直接使用常量值,而不是通过自动生成访问函数来完成,从而可以减少一次方法调用。但是,由于还是为需要判断引用是否为null而调用一次getClass()方法,因此性能上的提高有限;

除了final属性或者变量之外,很多资料上也会提到final方法对程序的性能也是由帮助的。但是本文没有谈到final方法,因为编译器对final方法能够做的优化很有限,可以说基本是干不了什么事情的。这是由继承引起的问题,由于子类在覆写父类的方法时,是可以将final关键字抹去的,因此编译器是没有足够多的信息来优化final方法的。final方法的优化是在运行期由虚拟机根据程序的执行情况来完成的,优化采用的方法本质同本文说的一样,就是减少方法调用,书面化一点也就是内联。
分享到:
评论
发表评论

文章已被作者锁定,不允许评论。

相关推荐

    【Java编程教程】详解Java final 关键字.pdf

    java中的final关键字是用来限制用户的。java final 关键字可以在许多上下文中使用

    Java中的final关键字详解及实例

    Java中的final关键字 1、修饰类的成员变量 这是final的主要用途之一,和C/C++的const,即该成员被修饰为常量,意味着不可修改。   上面的代码对age进行初始化后就不可再次赋值,否则编译时会报类似上图的错误。 ...

    Java中final关键字详解

     在Java中,final关键字可以用来修饰类、方法和变量(包括成员变量和局部变量)。下面就从这三个方面来了解一下final关键字的基本用法。  1.修饰类  当用final修饰一个类时,表明这个类不能被继承。也就是说,...

    java中的final关键字详解及实例

    主要介绍了 java中的final关键字详解及实例的相关资料,需要的朋友可以参考下

    Java中final关键字详解及实例

    主要介绍了Java中final关键字详解及实例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

    java中final关键字使用示例详解

    Java中的final关键字非常重要,它可以应用于类、方法以及变量。这篇文章中带你看看什么是final关键字?将变量,方法和类声明为final代表了什么?使用final的好处是什么?最后也有一些使用final关键字的实例

    JAVA中的final关键字用法实例详解

    主要介绍了JAVA中的final关键字用法,结合实例形式较为详细的分析了Java中的final关键字用于修饰数据,方法及类的具体使用技巧,需要的朋友可以参考下

    详解Java编程中static关键字和final关键字的使用

    主要介绍了详解Java编程中static关键字和final关键字的使用,是Java入门学习中的基础知识,需要的朋友可以参考下

    Java中的final关键字深入理解

    主要介绍了Java中的final关键字深入理解的相关资料,需要的朋友可以参考下

    Java-关键字final详解(public static final)

    B类继承A类,相当于对A类的功能进行扩展,如果不希望对A类进行扩展,可以给A类加final关键字,这样的话,A类就无法继承了。 源代码中String就是没有子孙的  结论:final修饰的类无法被继承 二、final修饰的方法 ...

    详解Java中的final关键字的使用

    主要介绍了详解Java中的final关键字的使用,是Java入门学习中的基础知识,需要的朋友可以参考下

    java关键字final使用方法详解

    在程序设计中,我们有时可能希望某些数据是不能够改变的,这个时候final就有用武之地了。final是java的关键字,本文就详细说明一下他的使用方法

    Java面向对象编程中final关键字的使用方法详解

    主要介绍了Java面向对象编程中final关键字的使用方法详解,包括对内部匿名类无法访问外面的非 final 的变量问题的解读,需要的朋友可以参考下

    Java 多线程与并发(6-26)-关键字- final详解.pdf

    Java 多线程与并发(6_26)-关键字_ final详解

    【Java面试+Java学习指南】 一份涵盖大部分Java程序员所需要掌握的核心知识

    final关键字特性 Java类和包 抽象类和接口 代码块和代码执行顺序 Java自动拆箱装箱里隐藏的秘密 Java中的Class类和Object类 Java异常 解读Java中的回调 反射 泛型 枚举类 Java注解和最佳实践 JavaIO流 多线程 深入...

    java多线程关键字final和static详解

    主要介绍了java多线程关键字final和static详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下

    Java开发详解.zip

    020604_【第6章:面向对象(高级)】_final关键字笔记.pdf 020605_【第6章:面向对象(高级)】_抽象类的基本概念笔记.pdf 020606_【第6章:面向对象(高级)】_接口的基本概念笔记.pdf 020607_【第6章:面向对象...

    final和static用法详解JAVA

    根据程序上下文环境,Java关键字final有“这是无法改变的”或者“终态的”含义,它可以修饰非抽象类、非抽象类成员方法和变量。你可能出于两种理解而需要阻止改变:设计或效率。

Global site tag (gtag.js) - Google Analytics