【原创博文,我会不断修改整理的...】
两个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”不就突破这种限制了嘛。当然,至于哪种更好,反正我是给不了定论。
好吧,熬夜写了这么多,够辛苦的。。。
分享到:
相关推荐
- Java关键字和修饰符:例如final、static、abstract等,它们用于声明Java中的类、方法和变量的属性。 2. Java高级特性 - Java接口、抽象类和继承:解释了Java中接口的继承、类的实现以及多个继承的实现方式。 -...
`abstract`, `interface`, `extends`, `implements`, `public`, `protected`, `private`, `final`, `static`, `finalize`等,为面向对象编程提供了丰富的表达工具,使得开发者能够轻松地实现封装、继承和多态性。...
- **关键字详解**:this关键字用于指代当前对象,static关键字用于表示静态成员,final关键字用于表示不可修改的对象或方法。 - **方法参数传递机制**:区分值传递和引用传递的区别,并深入探讨常见误区。 - **Java...
- **Java Collection API**:深入分析集合框架中的主要接口和实现类,如`ICollection`、`List`、`Set`、`Map`及其具体实现如`ArrayList`、`LinkedList`、`Vector`、`HashSet`、`TreeSet`、`HashMap`、`TreeMap`等。...
《深入剖析J2EE面试题集锦:从基础到进阶》 J2EE(Java 2 Enterprise Edition)是Sun Microsystems公司(现已被Oracle收购)为简化企业级应用开发而设计的一套标准和规范,其核心是利用Java平台构建可扩展、高性能...