2 Java语言中的保留字
任何一种语言都有自己的保留字,这些保留字是不能单独出现在程序中的,除非你赋予了其应有的意义。下表是Java语言中全部的保留字。
abstract |
boolean |
break |
byte |
case |
catch |
char |
class |
const * |
continue |
default |
do |
double |
else |
extends |
final |
finally |
float |
for |
goto * |
if |
implements |
import |
instanceof |
int |
interface |
long |
native |
new |
null |
package |
private |
protected |
public |
return |
short |
static |
super |
switch |
synchronized |
this |
throw |
throws |
transient |
try |
void |
volatile |
while |
限于篇幅关系,我们只讲解其中几个较为常用、重要的保留字,而且这些保留字有时候如果运用不当,容易产生错误。
2.1 静态的(static)
关于静态变量的知识在前面已经做过一些讲解,这里再介绍一下有关静态代码块、静态变量引用顺序及引用规则的相关知识。
静态代码块
有时候为了能在引用一个类的时候做一些必要的初始化工作,经常利用static 关键字加入一些静态的代码块,完成这个超前功能。下面给出一个处理配置信息的类的例子。
public class FileDirectoryConfig { static boolean isLoaded = false; static String fileDirectory = null; static { if ( ! isLoaded) { Properties props = new Properties(); try { java.io.InputStream is = FileDirectoryConfig.class.getResourceAsStream("file.properties"); System.out.println(is); props.load(is); } catch (IOException ex) { System.out.println("配置文件读取失败!"); ex.printStackTrace(); } fileDirectory = props.getProperty("fileDir"); if (fileDirectory == null) { System.out.println("read config file failed"); } else { props = null; isLoaded = true; } } } }
这个类的意图很明显,就是通过配置文件获取文件目录的配置信息,这种技术经常被应用在当你想在不修改应用程序代码的情况下,通过配置文件获取配置信息,并且调整或增加应用程序的功能的情景下。这样在其他类中引用这个类的属性fileDirectory 时就不用实例化这个类,而且也不需要调用其任何方法,只需下面一行代码即可达到预期的目的:
String fileDirectory = FileDirectoryConfig.fileDirectory;
这样书写代码就十分简便,节省编码时间,提高效率。你可能已经注意到了,为了保证该类只初始化一次,又引入了静态变量isLoaded,当其为假时,我们知道该类还没有做过初始化操作即执行。如果其为真,说明该类已经做过初始化操作,就不用再做初始化操作了。
静态变量的引用顺序
静态代码块对静态变量的应用在声明上存在先后顺序的限制,这与方法中引用变量的情况是不同的,在方法体中引用的变量在类文件前后声明没有限制,例如:
public class A { private String userName = null; public String getUserName() { return userName; } }
或者
public class A { public String getUserName() { return userName; } private String userName = null; }
都是没有问题的,程序都会正常运行,但是如果你试图在静态代码块中引用静态变量,就要考虑其声明的先后顺序的限定,例如下面的代码是不对的:
public class A { Static { userName = "张三"; } private static String userName = null; }
而下面的代码是正确的:
public class A { private static String userName = null; Static { userName = "张三"; } }
这是因为在静态代码块中引用的静态变量必须在静态代码块之前声明。
静态变量引用规则
对静态变量的引用有下面的规则:
(1) 可以在非静态方法体中引用静态变量。例如:
public class A { private static String userName = null; public void getMessage () { return userName; } }
(2) 在静态方法体中不可以引用非静态变量。
public class A { private String userName = null; public static void main(String args[]) { // 这是不允许的 System.out.println("userName = " + userName); } }
(3) 可以在静态方法体中创建非静态变量。
public class A { private String userName = null; public static void main(String args[]) { A a = new A(); } }
2.2 超类(super)
超类(super)在当类试图引用其父类对象时使用,当在类的构造器中试图引用父类的构造器时必须将其放置在子类构造器中的第一行,这是由类的初始化顺序规则决定的,否则编译器会提示你系统错误信息,你是无法完成对该类的编译工作的。例如下面的代码是不正确的:
public class B extends A { public B() { otherMethod(); super(initialName); } }
正确的书写方式为:
public class B extends A { public B() { super(initialName); otherMethod(); } }
2.3 最终的(final)
在日常的程序开发中final 保留字的运用较为广泛,下面是保留字final 的应用范围:
下面对final 保留字的这四个使用范围分别给予详细的讲解。
用来声明类的常量
在Java程序设计中常量的声明经常采用下面的方式:
public static final String USER_NAME = "张三";
在上面的语句中加入了static 与 final 两个保留字,这是常量具备的基本特征——静态的、不可变的。
用来声明方法的常量参数
在对一些方法声明的时候为了防止其参数被方法体中的语句更改,经常将参数声明为final 参数,例如:
class A { public void showMessage(final String userName) { ... System.out.println("userName = " + userName); } }
如果你企图在方法体中改变userName 参数的值,如:
... ... public void showMessage(final String userName) { ... userName = "李四"; System.out.println("userName = " + userName); } ... ...
该类在编译时,编译器就会提示在加粗代码行处有错误,提示你不能更改userName变量的值,这样做的好处是当你在方法中使用类的全局变量,而且不希望在该方法中对这个变量的值做修改,或者你不想在方法中改变某一个参数的值的时候,都可以采用final 关键字声明这个变量,这样做可以对该变量起到相应的保护作用,防止变量被无意赋值、破坏。
用来声明不可覆盖的方法
在面向对象程序的设计中,子类可以覆盖父类中的方法,即子类中的方法的名称与参数个数、参数类型及排放顺序完全相同,这时我们就说在子类中覆盖了父类中的方法。但有时我们并不希望一个类的子类覆盖某个方法,因为这些方法只能属于父类,只有父类才有条件,有资格完成这个行为,而这个方法子类又是子类无法完成的行为。此时我们就将这个方法声明为final,如下所示。
类A: public class A { protected void showMessage() {} } 类B: public class B extends A { protected void showMessage() {} }
上面的代码中,我们在类B中覆盖了类A中的方法showMessage() ,如果将类A中的showMessage()方法声明为final 方法,在类B中就不能覆盖这个方法了,如下所示。
类A: public class A { protected final void showMessage() {} } 类B: public class B extends A { protected void showMessage() {} }
上面的代码在编译时就会产生错误,其实采用final 保留字不但可以防止父类中的方法不被子类覆盖,而且还可以加快应用的运行速度,提高系统性能。在介绍之前我们先引入C++语言中内联函数(inline)的概念。
内联函数是C++中的概念,指将代码插入到调用者代码处的函数。如同C++语言中通过关键字#define 所定义的宏,内联函数通过避免被调用的开销来提高执行效率,尤其是它能够通过调用(“过程化集成”)被编译器优化。内联函数和宏很类似,其区别在于,宏是由预处理器对宏进行替代,而内联函数是通过编译器控制来实现的。而且内联函数是真正的函数,只是在需要用到的时候,内联函数像宏一样地展开,所以取消了函数的参数压栈,减少了调用的开销。你可以像调用函数一样调用内联函数,而不必担心会产生处理宏的一些问题。
当然,内联函数也有一定的局限性,就是函数中的执行代码不能太多了,如果内联函数的函数体过大,一般的编译器会放弃内联方式,而采用普通的方式调用函数。这样,内联函数就和普通函数执行效率一样了。
简单而言,内联函数就是当你编译应用程序时就可以确定该函数的代码,并且可以将函数的代码展开插入到调用者代码处的函数。在Java语言中,你可能也经常用到过类似于内联函数的引用,只是不自知罢了,例如你可以在某个方法中直接书写处理代码,也可以通过调用某个方法来封装处理代码段,然后再应用该方法。而前者的调用形式就是内联的调用方式:
public class A { public static int max(int a, int b) { return (a > b ? a : b); } public static void main(String args[]) { final int N = 10000000; int a = 5; int b = 17; int c; // 调用方法 long startTime = System.currentTimeMillis(); for (int i = 1; i <= N; i++) { c = max(a, b); } long endTime = System.currentTimeMillis(); System.out.println("调用方法:" + (endTime - startTime)); // 使用与max 方法相同的内联代码 startTime = System.currentTimeMillis(); for (int i = 1; i <= N; i++) { c = (a > b ? a : b); } endTime = System.currentTimeMillis(); System.out.println("使用内联代码:" + (endTime - startTime)); } }
编译并运行上面的代码,得到下面的输出:
调用方法:63
使用内联代码:15
由此可见,在编译时就可以确定展开的内联代码比方法调用将近快4倍,上面程序的运行硬件环境为CPU(奔腾4,2.4GZH),内存(DDR 1GB)的PC电脑上。随着硬件配置的提高,这个差别会更加明显。但是上面的例子中我们只能使用一些简单的表达式来达到内联的目的,但是如果代码量较大,并且在应用程序的多个地方引用相同的代码,这种方式就显得有些力不从心了,因为过多拷贝相同的代码块必将使类变得臃肿,怎样解决这个问题呢?通过将方法声明为final 方法来达到这个效果,例如将上面的代码修改成下面的样子:
public class A { public final static int max(int a, int b) { return (a > b ? a : b); } public static void main(String args[]) { final int N = 10000000; int a = 5; int b = 17; int c; // 调用方法 long startTime = System.currentTimeMillis(); for (int i = 1; i <= N; i++) { c = max(a, b); } long endTime = System.currentTimeMillis(); System.out.println("调用方法:" + (endTime - startTime)); // 使用与max 方法相同的内联代码 startTime = System.currentTimeMillis(); for (int i = 1; i <= N; i++) { c = (a > b ? a : b); } endTime = System.currentTimeMillis(); System.out.println("使用内联代码:" + (endTime - startTime)); } }
编译并运行上面的代码,得到下面的输出:
调用方法:16
使用内联代码:16
与上面的输出有些出入,但是两种方式的运行时间却是相同的或接近的。这是我们将max()方法声明为final 方法的原因。一个方法被声明成final 方法后,当你编译应用程序时就可以确定该方法的代码,并且编译器可以将该方法的代码展开插入到调用者代码处,因此提高了应用的运行速度,提高了系统效率。
用来声明不可继承的类
将上面的知识推而广之,我们也可以将一个类声明成final 类,使其不可以被其他类继承,说通俗点就是有点让其“断子绝孙”的味道,如下所示。
类A: public final class A { ... } 类B: public class B extends A { ... }
类B是无法继承类A的,上面的代码是不正确的,当你在编译应用时编译器将会报错,提示你类B不能继承类A,因为类A是final 类。同理,将类A声明成final 类也可以起到内联的作用,加快应用速度,提高系统性能,如:
... ... A ref = new A(); ref.showMessage(); ... ...
上面的这段代码就将整个类A看做内联代码。
2.4 同步(synchronized)
同步技术是在多线程的运行环境下提出的,该技术用来保护多线程环境下的共享资源。使用多线程时,由于线程之间可以共享资源,同时访问共享资源有时就会发生冲突。举一个简单的例子,有两个线程:thread1 负责写共享资源;thread2 负责读共享资源。当它们操作同一个对象时,会发现由于thread1 与thread2 是同时执行的,因此,可能thread1 已经修改了数据,而thread2 读出的仍为旧数据,此时就无法获得预期的结果。问题之所以产生,主要是由于在多线程环境下,资源使用的协调不当(不同步)。
Java语言提供了同步(synchronized)方法和同步状态来协调资源。在Java程序设计中,被宣布为同步(Synchronized)的方法,对象或类数据。在任何一个时刻只能被一个线程访问。通过这种方式,可以保证多线程环境下的共享资源能够被合理的使用,达到线程同步的目的。
下面给出一个线程同步的具体的例子:
import java.lang.*; // 线程同步测试类SynchronizedTest.java public class SynchronizedTest { private int i = 0; public static void main(String args[]) { SynchronizedTest st = new SynchronizedTest(); st.startThread(); } // 启动线程方法 private void startThread() { // 同时启动3个线程 for (int j = 0; j < 3; j++) { new Thread(new ThreadWorker()).start(); } } // 内部线程类ThreadWorker class ThreadWorker implements Runnable { public void run() { showMessage(); } } // 方法showMessage()被宣布为同步的方法,因此每次只有一个线程能调用这个方法 private synchronized void showMessage() { Thread currentThread = Thread.currentThread(); System.out.println("当前线程为: " + currentThread.getName()); ++i; } }
运行结果为:
当前线程为: Thread-1
当前线程为: Thread-2
当前线程为: Thread-3
另外,利用Synchronized 可以锁定对象。
例如:
synchronized (某个对象A) { // 程序块 }
在此程序块中,对于相同的对象A,在任何时候只可以有一个线程在此代码中执行,但对于不同的对象还是有很多个线程同时执行的。用同样的方法也可以协调类数据,例如:
synchronized (new 欲锁定的类().getClassData()) { // 程序块 }
方法getClassData() 是用来获取类数据的。这样通过利用Synchronized 这一关键字可以自由协调对象实体的各种数据。
关于多线程的相关知识,这里就不再介绍了,想要了解更多的信息,请参考与之相关的技术资料。
2.5 实例识别 (instanceof)
实例识别(instanceof)是用来判断一个对象的引用是否为某一类型。比如:
A a = new A(); System.out.println(a instanceof A);
返回为true,因为a是一个类A的对象的引用。
A a = new A(); System.out.println(a instanceof B);
则返回为false,因为a不是一个类B的对象的引用。
但如果
A a = null; System.out.println(a instanceof A);
则返回值为false。这是因为a的值为null,null不是任何对象的引用。这是需要特别注意的。在识别区分子类实例的场合是非常有用的,例如:
基类 class BasicClass { ... } 子类A class A extends BasicClass { ... } 子类B class B extends BasicClass { ... }
在上面的代码中,类A与类B都继承了共同的基类BasicClass,因此可以将类A或类B的对象引用赋给基类BasicClass的对象引用,但是当无法确定基类引用到底是类A的对象引用还是类B的对象引用时,就可以通过instanceof 保留字进行识别和区分,例如:
... ... BasicClass basicClass = (BasicClass)obj.getObjectOfAOrB(); if (basicClass instanceof A) { ... } else if (basicClass instanceof B) { ... } else { ... } ... ...
这样就可以确定在运行时刻,基类BasicClass的引用中所指向的对象实例,这在相关的应用程序设计中是非常有用的。