`
Mysun
  • 浏览: 273436 次
  • 性别: 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中的final关键字详解及实例

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

    java final关键字

    ### Java Final 关键字详解 #### 一、引言 在Java编程语言中,`final`关键字具有重要的地位。它能够用于限定类、方法以及变量的行为,并赋予它们特定的属性。本文将深入探讨`final`关键字在不同场景下的具体用法及...

    Java中final关键字详解

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

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

    Java中的`final`关键字是一个非常重要的概念,它用于在编程中实现不可变性。下面是对`final`关键字的详细解释: ## Java最终变量 当一个变量被声明为`final`时,这意味着它的值一旦被赋值后就无法再改变。这在Java...

    stati、thi、supe、final关键字详解

    四、final关键字 final在Java中有多种用途: 1. **修饰变量**:声明为final的变量一旦赋值后就不能再改变。 2. **修饰方法**:final方法不能被子类重写,确保了行为的一致性。 3. **修饰类**:声明为final的类不能被...

    Java中final关键字详解及实例

    Java中final关键字详解及实例 在Java编程语言中,final关键字扮演着非常重要的角色,它可以用来声明成员变量、方法、类以及本地变量。final关键字的主要作用是确保变量、方法或类不可以被修改或继承。 final变量 ...

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

    在Java编程语言中,`final`关键字是一个非常重要的概念,它具有多种用途,可以用于修饰类、方法和变量。下面将详细介绍`final`关键字在不同情况下的应用和含义。 1. **final修饰类中的属性或变量** 当`final`...

    final关键字和static_用法

    ### Java中的final关键字详解 #### 一、final的概述与用途 `final`关键字在Java中扮演着重要的角色,它通常被用来表示“不可变”的特性。`final`可以用来修饰类、方法以及变量,其核心目的是为了保护数据的安全性...

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

    final关键字是Java语言中的一个关键字,用于修饰类、方法、变量等。final关键字的主要作用是限制程序员的某些行为,以免出现不必要的错误。 * final修饰类:当某个类的整体定义为final时,就表明了你不能打算继承该...

    java51个关键字详解

    Java编程语言中有51个关键字,它们在程序中扮演着至关重要的角色,用来定义类、接口、变量、方法以及控制程序流程。以下是一些主要的关键字及其解释: 1. `abstract`:抽象关键字,用于声明抽象类和抽象方法。抽象...

    java final变量详解

    Java final 变量详解 Java 中的 final 变量是指不能被改变的变量,它有三个方面的作用:修饰变量、修饰方法和修饰类。在 Java 中,final 变量是一种常量,它只能被赋值一次,赋值后值不再改变。 final 变量的使用...

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

    4. **final关键字的好处** - 提高性能:JVM可以对`final`变量进行优化,因为它们的值是不可变的,这有助于提升程序执行速度。 - 多线程安全:`final`变量在多线程环境中可以直接共享,因为它们的值不会改变,从而...

    final关键字的使用

    ### Final关键字详解 #### 一、Final的基本概念与作用 `final`关键字是Java语言中的一个重要的修饰符,它的含义非常直接——“最终”的意思。它可以在不同的上下文中发挥不同的作用,主要体现在以下三个方面: 1....

    Java中的final关键字深入理解

    Java中的`final`关键字是一个非常重要的概念,它用于在编程中实现不同的限制和特性。`final`关键字可以应用于以下几个方面: 1. **final变量**:当一个变量被声明为`final`时,它成为了一个不可修改的常量。这意味...

    Java中final的深度剖析

    【Java中的final关键字详解】 Java中的final关键字是一个非常重要的修饰符,它用于声明变量、方法和类,确保它们在程序执行期间保持不变。final关键字在Java编程中的应用广泛,可以帮助提高代码的稳定性和可维护性...

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

    在Java编程语言中,`final`关键字扮演着重要的角色,它是用来声明不可变性的一种机制。下面我们将详细探讨`final`关键字在修饰数据、方法和类时的不同用法。 首先,我们来看`final`关键字如何修饰数据。在Java中,`...

    Java关键字详解

    Java关键字详解 在Java编程语言中,关键字是具有特殊含义的保留词汇,它们不能作为变量名、类名或方法名。这些关键字对于理解和编写有效的Java代码至关重要。下面将详细解释Java中的各个关键字。 一、关键字总览:...

    Java关键字final、static使用总结

    #### 一、final关键字详解与应用 在Java语言中,`final`关键字被广泛应用于各种场景,如定义不可变的变量、禁止类的继承等,具有重要的作用。 1. **final修饰变量** - `final`用于修饰变量时,该变量将变为常量...

Global site tag (gtag.js) - Google Analytics