`
mingyang2013
  • 浏览: 4888 次
  • 性别: Icon_minigender_1
  • 来自: 成都
社区版块
存档分类
最新评论

深入剖析Java关键字static 和 final

阅读更多

【原创博文,我会不断修改整理的...】

 

 

    两个Java关键字static 和 final,平常也经常用,或许是因为用得多了,成了一种习惯了,反而只知其然,而不知其所以然了。下面我就来结合自己的经验做一个总结,也算作一种备忘吧。

    2.3.3小节中,对方法内部类使用的局部变量为什么要用final声明,进行了详细深入的探讨。

1.static

(待写。。。)

 

2.final

    final可用于修饰非抽象类、方法、数据,其中,方法包括类方法(static方法)、成员方法,数据包括类变量、成员变量、局部变量。

2.1修饰类

    final修饰类,其表示的意思是,被修饰的这个类不能被继承,并且这个类里所有的方法都隐含的被声明为final。final方法和非final方法有什么区别2.2会谈到。

2.2修饰方法

    final可以修饰静态方法和成员方法。final修饰方法通常表示这个方法不能在子类中重写,当然,前提是子类能够继承这个方法。

    final修饰的方法,可以由编译器根据“实际情况”进行内联操作,从而提高性能。什么是内联呢?就是A方法内调用了B方法,如果对B方法进行内联操作,那么在A中执行到调用B时,不会对B新启一个压栈操作,从而可以减小B方法出入栈的开销。那么怎么内联的呢?简单说就是编译器根据“实际情况”确定要内联某个方法时,就会把这个方法的代码语句直接插在调用这个方法的地方,和原来的方法融合在一起成为一个整块,这样在调用这个被内联的方法时,就不重新开辟栈了。当然,内联有好处也有弊端,如果被内联的方法体太大,很多地方都内联这个方法,就显得很臃肿,增加了内存空间的开销。虽然编译器会对此有一定的“权衡”,并不会内联所有的final方法,但是这种“权衡”并非万无一失的,所以需要内联的方法,我们最好让它们精炼简洁一些。

2.2.1静态方法

    说到静态方法,我就想起一个问题,静态方法可以被继承,可以被覆盖(override),被重写(overwrite)吗?带着问题,我们来看例子。

(1)

public class Parent {
	public static void foo() {
		System.out.println("Parent.foo()");
	}
	
	public static void main(String[] args) {
		Parent.foo();
		Son.foo();
	}
}

class Son extends Parent {
}

执行结果:
Parent.foo()
Parent.foo()

    可见,静态方法被继承是没问题的。

(2)

 

public class Parent {
	public static void foo() {
		System.out.println("Parent.foo()");
	}
	
	public static void main(String[] args) {
		//case 1
		Parent.foo();
		Son.foo();
		//case 2
		Parent parent2 = new Parent();
		Parent son2 = new Son();
		parent2.foo();
		son2.foo();
		//case 3
		Parent parent3 = new Parent();
		Son son3 = new Son();
		parent3.foo();
		son3.foo();
	}
}

class Son extends Parent {
	public static void foo() {
		System.out.println("Son.foo()");
	}
}

执行结果:
Parent.foo()
Son.foo()
Parent.foo()
Parent.foo()
Parent.foo()
Son.foo()

    例子中case2说明static方法调用不是动态绑定的,只是根据类名和引用的类型(引用声明时类型)来静态决定的。那到底Son中的foo()方法是不是对Parent中foo()方法的重写(overwrite)?不是,Eclipse里也不会显示重写的标记(Eclipse会对重写的方法进行一个三角的标记,并显示为“overrides 某某”,其实个人觉得这有些概念不清,显示为“overwrites 某某”更好)。其实这是一种“覆盖”(override),就是在Son中重新定义一个相同签名的方法把继承来的相同签名方法“覆盖”了,继承来的方法没有被放弃,只是对子类不可见了。

(3)

 

public class Parent {
	public static final void foo() {
		System.out.println("Parent.foo()");
	}
}

class Son extends Parent {
	public static void foo() {
		System.out.println("Son.foo()");
	}
}

执行结果:
编译错误!
Eclipse错误提示信息:Cannot override the final method from Parent

    例子说明,final修饰的方法,子类不能覆盖(override)这个方法,Eclipse信息也这么提示的。

    综上,static方法是可以被继承的,可以被覆盖(override)的,不叫重写(overwrite)。但是被final修饰的static方法是不可以被覆盖的。

 

2.2.3成员方法

    成员方法加上final理解起来就很简单了,就是不能被子类重写(overwrite)。重写指:重写继承到的那个方法的代码,继承来的原方法被放弃。

    另外,可以被内联,只是可以,最终还需要编译器的做一个“权衡”。

2.3修饰数据

    final修饰数据包括类变量(static声明的变量),成员变量,局部变量。其中,局部变量包括方法参数变量,方法体内定义的变量。

    final修饰的数据变量,通常表示该变量不可一旦初始化就不可更改。

    final修饰的数据还可分为基本变量、引用变量。其中,若修饰基本变量,则表示这个基本变量一旦被初始赋值了,这个变量就再不能被赋其它值了。若修饰引用变量,则表示这个引用变量一旦被初始赋值,指向某个对象,就不能再被赋予另外一个引用值,不能再指向另外一个对象了,但是对象里面的内容仍是可以改变的。

2.3.1类变量

    final修饰的类变量,必须在声明变量时就做显示赋值。显示复制包括编译期赋值和运行期赋值,且一旦赋值就不可更改,也就成了我们常说的“常量”。

 

public class FinalData {
	private static final int I1 = 1;
	private static final int I2 = getData();
	private static int getData() {
		return 2;
	}
}

    其中,I1的值是编译期就确定的。I2值是运行时确定的。两者的值一旦确定,都不可更改。

2.3.2成员变量

    final修饰成员变量,该变量可以在声明时赋值,也可以在声明时不赋值(即blank final),但必须在构造方法返回之前赋值。且一旦赋值就不可更改。

public class FinalData {
	private final int i1 = 1;
	private final int i2 = getData();
	private final int i3 = getStaticData();
	private final int i4;
	private final int i5;
	//private final int i6; //编译错误:The blank final field i6 may not have been initialized
	FinalData() {
		i4 = 4;
		i5 = getData();
	}
	private int getData() {
		return 2;
	}
	private static int getStaticData() {
		return 3;
	}
}

 2.3.3局部变量

    final修饰局部变量,包括方法参数变量,方法体内变量。

    有个比较重要的点就是,如果一个方法内声明创建了一个方法内部类,且该内部类使用了该方法的局部变量,即参数定义的变量以及方法体内定义的变量,则该变量必须用final修饰。这是为什么呢?

(2.3.3清单1)

 

public class World {
	int foo() {return 0;}
	private static World getWorld(final int input) {
		World w = new World() {
			int foo() {
				return input;
			}
		};
		return w;
	}
	public static void main(String[] args) {
		World w = getWorld(100);
		System.out.println(w.foo());
	}
}

执行结果:
100

     World类中有一个成员方法foo(),有一个静态方法getWorld(final int),调用getWorld(final int)会返回一个匿名方法内部类,该内部类中重写的一个方法将返回传给getWorld()的参数100。

    疑问来了,传给方法getWorld()的的参数只存在于栈中,一旦getWorld()方法返回,传入的input就不存在了,那么在main方法中调用w.foo()怎么还能正确返回传入的那个值100呢?

    其实,对于方法内部类访问方法内的局部变量时,在new内部类实例时,会将内部类用到的变量拷贝一份作为该内部类的成员变量存起来,所以栈中变量销毁对其并无影响,这中操作是编译器做的,对程序员不可见。上例中方法内部类其实经过编译器处理就是下面这个样:

(2.3.3清单2)

 

public class World {
	int foo() {return 0;}
	private static World getWorld(final int input) {
		
		class WorldInner extends World {
			private final int data;
			WorldInner(final int input) {
				this.data = input;
			}
			int foo() {
				return data;
			}
		}
		World w = new WorldInner(input);
		return w;
	}
	public static void main(String[] args) {
		World w = getWorld(100);
		System.out.println(w.foo());
	}
}

执行结果:
100

     一目了然了吧?豁然开朗了吧?

    那为什么要把input定义成final的(2.3.3清单1),不这样还不让编译通过呢?其实如果我们按照编译器处理后的代码(2.3.3清单2)写,是可以不把input定义为final的。但是按照前面那种写法,为什么就非要把input弄成final的呢?

    答案是:Java语言设计者为了不给程序员增加麻烦,尽量从语言的角度给程序员们消除语义歧义。

    因为使用方法内部类,会进行数据拷贝,一旦拷贝结束,就有两份数据,如“2.3.3清单2”中WorldInner中的成员变量data和传给getWorld()方法的参数input。但是从代码表面上看来,foo()方法就是用的外面传进来的input的值嘛,如果不给input加final,input在方法内部随时可以改动,设想一下,如果给WorldInner的data赋了值后,再把input的值修改成99,我们在外面调用WorldInner的foo()方法,感觉是操作的input值,也就是修改后的99,实际上是操作的拷贝的data,即100,要是让不了解其中原理的程序员看到明明修改成了99的input参数,却输出的是100, 还不纳闷死!

    所以,为了避免这种语义歧义,Java设计者干脆就让这个方法内部类使用到的局部变量为final,不让修改得了。

    那有人可能想问,可不可以不让这个设计成必须是final的呢?反正我觉得可以,只要理解了其中的原理,明白什么情况下是对什么变量值进行操作,“2.3.3清单2”不就突破这种限制了嘛。当然,至于哪种更好,反正我是给不了定论。

 

 

    好吧,熬夜写了这么多,够辛苦的。。。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics