第二章 The Final Story
编程的一条基本法则就是:尽可能的将逻辑错误转换为编译错误。因为逻辑错误很难发现甚至无法发现,而编译错误则很快就能发现(尤其是现在的java IDE,基本都能即时编译),也比较容易修改。java中的关键字final就可以帮助我们把大量的逻辑错误转为编译错误
1.public primitives and substitution
所谓的原始类型(primitives)是指如:int,float,double等简单类型,在Java中这些类型并不是继承自Object。public final修饰的原始类型变量是在编译时刻替换而不是运行时刻。除了原始类型外还有String也是编译时替换
如下:
Constant.java文件
public class Constant{
public static final int CONST1 = 6;
public static final String NAME="HELLO!";
}
Some.java文件
public class Some{
public void test(){
System.out.println("the const String value is : "+Constant.NAME);
System.out.println("the const int value is : "+Constant.CONST1*4);
}
public static void main(String[] args){
Some s = new Some();
s.test();
}
}
编译,运行Some后,得到结果:
E:/program>javac Some.java
E:/program>java Some
the const String value is : HELLO!
the const int value is : 24
修改Constant.java文件
public class Constant{
public static final int CONST1 = 8;
public static final String NAME="HELLO,changed!";
}
编译Constant.java文件
E:/program>javac Constant.java
再次运行Some
E:/program>java Some
the const String value is : HELLO!
the const int value is : 24
可以看到结果并没有改变。只有从新编译Some.java(虽然它并没有变化):
E:/program>javac Some.java
再次运行Some
E:/program>java Some
the const String value is : HELLO,changed!
the const int value is : 32
当然现在的一些集成开发环境可能会发现这个问题,并自动编译Some.java文件,但不应该依赖这些IDE,尤其是现在很多都在使用ANT进行自动编译等,就更应该注意这个问题,最好是在编译前清理掉上一次的编译结果,因为ANT是不会编译没有改变的文件的。
2.Final Variables
2.1 Method-Scoped Final variables
在方法中声明为final的变量看起来可能有些奇怪,但这确实有用,可以防止意外的改变这个变量,尤其对那些超过100行的方法更是要注意,否则这个bug也很难找的。
2.2 Final Parameters
final参数可以防止对参数的无意的赋值。注意final并不能组织你调用它所修饰的对象的方法来修改对象本身,如:
public void nothing(final SomeObject s){
s.setName("not what i want");//这是合法的,所以一定要注意。
}
所以final于C++中的Const有很大的不不同的,当修饰一个对象是,final相当于C++中Const修饰一个对象的指针,如
final SomeObject s;
const SomeObject * s;
而不是const SomeObject* const s;(个人的看法)
3. Final Collections
当我们需要一个不能改变的集合时,第一感觉可能会这样写:
public final Collection list;
认为这样就可以了,其实这是完全错误的,这里的final只能阻止你在为list赋初值之后再次赋值,但不能阻止你修改list中的内容。至于为什么,仍然象上面所说的
final SomeObject s;等价于const SomeObject * s;
而不是const SomeObject* const s;(个人的看法)
如果你真的想得到一个不可变的集合应该象这样:
这样得到的list才是真正的不可变。
public final Collection list;
List temp = new LinkedList();
temp.add(something);
.....
list = Collections.unmodifiableList(temp);
4.final class和final method
作者的观点是尽量不要这么做,除非真的有这个必要。
final class可以用带有protected的构造函数来代替,这样别人可以扩展你的类。
final method作者给了一个例子:
很明显这个类是用来被继承的,并且不希望它的子类修改getName()方法。
public class FinalMethod
{
private final String name;
protected FinalMethod(final String name){
this.name = name;
}
public final String getName(){
return this.name;
}
};
5.Conditional Compalition
‘条件编译’就是根据一定的条件决定是否把一段代码编译到class文件中。最常见的例子就是我们经常使用的日志,如apache的log4j。一般的日志系统都会提供几个输出基本,然后在配置文件中指定一个级别,这样小于这个级别的输出就不会显示在日志的输出文件中,但其实程序仍然会执行一定的代码来进行比较,如果一个大的系统给的DEBUG信息比较多,虽然在虽然发布的系统指定输出级别为ERROR,但那些DEBUG仍然会耗费大量的CPU时间。如下:
public class ConditionCompile
{
public void test()
{
logger.debug("debug");
logger.info("info");
logger.warn("warn");
logger.error("error");
logger.fatal("fatal");
}
}
虽然在配置文件中指定输出级别为ERROR,但debug和info方法仍然会执行,只不过在比较后发现级别太低而没有输出。
想要省去这些代码的执行就要用到Conditional Compalition了。上面的代码可修改如下:
public class ConditionCompile
{
private final static boolean DEBUG = true;
public void test()
{
if(DEBUG){
logger.debug("debug");
}
if(DEBUG){
logger.info("info");
}
if(DEBUG){
logger.warn("warn");
}
if(DEBUG){
logger.error("error");
}
if(DEBUG){
logger.fatal("fatal");
}
}
}
这样如果把DEBUG改为false,如果编译器做得比较好就可以优化掉这些代码。那么这些代码就不会编译到class文件中去(Sun公司的jdk1.5就可以优化,其他的版本就没有实验了,不能保证)。
下面是我的实验例子:
public class Condition
{
public static final boolean DEBUG = false;
public void test(){
if(DEBUG){
System.out.println("debug");
}
.......//上面的if语句重复19遍。
}
}
编译出来的class文件大小为287bytes,如果把DEBUG该为true
public class Condition
{
public static final boolean DEBUG = true;
public void test(){
if(DEBUG){
System.out.println("debug");
}
.......//上面的if语句重复19遍。
}
}
编译出来的class文件大小为669bytes,可见编译器确实做了优化(这样推理应该是对的吧)。
但这样做会不会增加进不必要的开销呢?下面把if语句去掉再看一下编译文件的大小
public class Condition
{
public void test(){
System.out.println("debug");
.......//上面的语句重复19遍。
}
}
编译出来的class文件大小为620bytes,可见虽然有一定的开销,但这种开销还是值得的(这个开销是由于定义变量DEBUG,如果在没有if语句的例子里加上DEBUG变量的声明,class文件仍然是669bytes)。
最后作者建议把DEBUG变量的定义放在一个单独的包的java文件中。
我在现在写的一个程序中就使用了作者的建议,不过有所扩展,我针对log4j的五个级别,为每个包定义了五个常量,如我有一个client包,定义了如下五个常量:CLIENT_DEBUG,CLIENT_INFO,CLIENT_WARN,CLIENT_ERROR,CLIENT_FATAL。不知道这样做是不是有点过头。
个人的看法:
java虽然为C++程序员解除了指针的烦恼,在java中一切都为引用,但任何事物都有去两面性,‘引用’也是一把双刃剑,用好了所向披靡,用不好也会伤到自己。java虽然为我们减轻了内存泄漏的痛苦(但并没有消灭,后面的章节会谈到),但也因‘引用’而带来了一系列的问题,在编程中要格外的注意。另外final于C++中的Const有很大的区别的,一般别拿这两者比较,要比就比较的彻底一些,否则半懂不懂,后患不穷啊:-)(夸张了)。后面还有两章谈到关于‘常量’这个问题,那时会对引用有更深的认识。