23423

来源:互联网 发布:mac出现了客人用户 编辑:程序博客网 时间:2024/05/04 16:42

第二章 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有很大的区别的,一般别拿这两者比较,要比就比较的彻底一些,否则半懂不懂,后患不穷啊:-)(夸张了)。后面还有两章谈到关于‘常量’这个问题,那时会对引用有更深的认识。

原创粉丝点击