某某茶叶有限公司欢迎您!
金沙棋牌在线 > 金沙棋牌在线 > 改善Java程序建议9

改善Java程序建议9

时间:2019-12-29 06:38

我非常确定,作为开发人员我们都喜爱技术文档。我们喜欢阅读文档、写文档,更不用说维护文档了,我简直爱死它了!

建议 17: 慎用动态编译。

关于动态编译的代码:

public class Client {
    public static void main(String[] args) throws Exception {
            //Java源代码
            String sourceStr = "public class Hello{public String sayHello (String name)
                  {return "Hello," + name + "!";}}";
            //类名及文件名
            String clsName = "Hello";
            //方法名
            String methodName = "sayHello";
            //当前编译器
            JavaCompiler cmp = ToolProvider.getSystemJavaCompiler();
            //Java标准文件管理器
            StandardJavaFileManager fm = cmp.getStandardFileManager(null,null,null);
            //Java文件对象
            JavaFileObject jfo = new StringJavaObject(clsName,sourceStr);
            //编译参数,类似于javac <options>中的options
            List<String> optionsList = new ArrayList<String>();
            //编译文件的存放地方,注意:此处是为Eclipse工具特设的
            optionsList.addAll(Arrays.asList("-d","./bin"));
            //要编译的单元
            List<JavaFileObject> jfos = Arrays.asList(jfo);
            //设置编译环境
            JavaCompiler.CompilationTask task = cmp.getTask(null, fm, null, optionsList,null,jfos);
            //编译成功
            if(task.call()){
                    //生成对象
                    Object obj = Class.forName(clsName).newInstance();
                    Class<? extends Object> cls = obj.getClass();
                    //调用sayHello方法
                    Method m = cls.getMethod(methodName, String.class);
                    String str = (String) m.invoke(obj, "Dynamic Compilation");
                    System.out.println(str);
            }
   }
}
//文本中的Java对象
class StringJavaObject extends SimpleJavaFileObject{
    //源代码
    private String content = "";
    //遵循Java规范的类名及文件
    public StringJavaObject(String _javaFileName,String _content){
          super(_createStringJavaObjectUri(_javaFileName),Kind.SOURCE);
          content = _content;
    }
    //产生一个URL资源路径
    private static URI _createStringJavaObjectUri(String name){
          //注意此处没有设置包名
          return URI.create("String:///" + name + Kind.SOURCE.extension);
    }
    //文本文件代码
    @Override
    public CharSequence getCharContent(boolean ignoreEncodingErrors)
                    throws IOException {
          return content;
   }
}

只要静态编译能做的事情,动态编译就能实现。

动态编译时,需要注意以下几点:
(1)在框架中谨慎使用
比如要在Struts中使用动态编译,动态实现一个类,它若继承自ActionSupport就希望它成为一个Action。能做到,但是debug很困难;再比如在Spring中,写一个动态类,要让它动态注入到Spring容器中,这是需要花费老大功夫的。
(2)不要在要求高性能的项目使用
动态编译毕竟需要一个编译过程,与静态编译相比多了一个执行环节,因此在高性能项目中不要使用动态编译。不过,如果是在工具类项目中它则可以很好地发挥其优越性,比如在Eclipse工具中写一个插件,就可以很好地使用动态编译,不用重启即可实现运行、调试功能,非常方便。
(3)动态编译要考虑安全问题
如果你在Web界面上提供了一个功能,允许上传一个Java文件然后运行,那就等于说:“我的机器没有密码,大家都来看我的隐私吧”,这是非常典型的注入漏洞,只要上传一个恶意Java程序就可以让你所有的安全工作毁于一旦。
(4)记录动态编译过程
建议记录源文件、目标文件、编译过程、执行过程等日志,不仅仅是为了诊断,还是为了安全和审计,对Java项目来说,空中编译和运行是很不让人放心的,留下这些依据可以更好地优化程序。

建议13:避免为final变量复杂赋值

为final变量赋值可以通过方法赋值,即直接在声明时通过方法返回值赋值。

public class Person implements Serializable {
  private static final long serialVerisionUID = 91282334L;
  //通过方法返回值为final变量赋值
  public final String name = initName();
  //初始化方法名
  public String initName(){
    return "混世魔王";
    //  return "德天使";
  }
}

先序列化上面的代码,然后把initName的返回值改为注释的代码。然后在反序列化,name值是什么?

是“混世魔王”,虽然上一条建议说final变量会被重新赋值,其中的“值”指的是简单对象,简单对象包括:8个基本数据类型,以及数组,字符串(字符串情况很复杂,不通过new关键字生成String对象的情况下,final变量的赋值与基本类型相同),但是不能通过方法赋值。

其中的原理是这样的,保存到磁盘上(或网络传输)的对象文件包括两部分:
(1)类描述信息
包括包路径、继承关系、访问权限、变量描述、变量访问权限、方法签名、返回值,以及变量的关联类信息。要注意的一点是,它并不是class文件的翻版,它不记录方法、构造函数、static变量等的具体实现。之所以类描述会被保存,很简单,是因为能去也能回嘛,这保证反序列化的健壮运行。
(2)非瞬态(transient关键字)和非静态(static关键字)的实例变量值
注意,这里的值如果是一个基本类型,就是一个简单值保存下来;如果是复杂对象,连该对象和关联类信息一起保存,并且持续递归下去(关联类也必须实现Serializable接口,否则会出现序列化异常),也就是说递归到最后,其实还是基本数据类型的保存。
正是因为这两点原因,一个持久化后的对象文件会比一个class类文件大很多。
总结一下,反序列化时final变量在以下情况下不会被重新赋值:

  • 通过构造函数为final变量赋值。
  • 通过方法返回值为final变量赋值。
  • final修饰的属性不是基本类型。

建议9:少用静态导入

从Java 5开始引入了静态导入语法(import static),其目是为了减少字符输入量,提高代码的可阅读性,以便更好地理解程序。我们先来看一个不使用静态导入的例子,也就是一般导入:

public class MathUtils{
    //计算圆面积
    public static double calCircleArea(double r){
            return Math.PI * r * r;
    }
    //计算球面积
    public static double calBallArea(double r){
            return 4* Math.PI * r * r;
    }
}

这是很简单的数学工具类,我们在这两个计算面积的方法中都引入了java.lang.Math类(该类是默认导入的)中的PI(圆周率)常量,而Math这个类写在这里有点多余,特别是如果MathUtils中的方法比较多时,如果每次都要敲入Math这个类,繁琐且多余,静态导入可解决此类问题,使用静态导入后的程序如下:

import static java.lang.Math.PI;
public class MathUtils{
    //计算圆面积
    public static double calCircleArea(double r){
            return PI * r * r;
    }
    //计算球面积
    public static double calBallArea(double r){
            return 4 * PI * r * r;
    }
}

静态导入的作用是把Math类中的PI常量引入到本类中,这会使程序更简单,更容易阅读,只要看到PI就知道这是圆周率,不用每次都要把类名写全了。但是,滥用静态导入会使程序更难阅读,更难维护。静态导入后,代码中就不用再写类名了,但是我们知道类是“一类事物的描述”,缺少了类名的修饰,静态属性和静态方法的表象意义可以被无限放大,这会让阅读者很难弄清楚其属性或方法代表何意,甚至是哪一个类的属性(方法)都要思考一番(当然,IDE友好提示功能是另说),特别是在一个类中有多个静态导入语句时,若还使用了*(星号)通配符,把一个类的所有静态元素都导入进来了,那简直就是恶梦。我们来看一段例子:

import static java.lang.Double.*;
import static java.lang.Math.*;
import static java.lang.Integer.*;
import static java.text.NumberFormat.*;

public class Client {
  //输入半径和精度要求,计算面积
  public static void main(String[] args) {
            double s = PI * parseDouble(args[0]);
            NumberFormat nf = getInstance();
            nf.setMaximumFractionDigits(parseInt(args[1]));
            formatMessage(nf.format(s));
  }
  //格式化消息输出
  public static void formatMessage(String s){
            System.out.println("圆面积是:"+s);
  }
}

就这么一段程序,看着就让人火大:常量PI,这知道,是圆周率;parseDouble方法可能是Double类的一个转换方法,这看名称也能猜测到。那紧接着的getInstance方法是哪个类的?是Client本地类?不对呀,没有这个方法,哦,原来是NumberFormate类的方法,这和formateMessage本地方法没有任何区别了—这代码也太难阅读了,非机器不可阅读。
所以,对于静态导入,一定要遵循两个规则:

  • 不使用*(星号)通配符,除非是导入静态常量类(只包含常量的类或接口)。
  • 方法名是具有明确、清晰表象意义的工具类。

何为具有明确、清晰表象意义的工具类?我们来看看JUnit 4中使用的静态导入的例子,代码如下:

import static org.junit.Assert.*;
public class DaoTest {
  @Test
  public void testInsert(){
            //断言
            assertEquals("foo", "foo");
            assertFalse(Boolean.FALSE);
  }
}

我们从程序中很容易判断出assertEquals方法是用来断言两个值是否相等的,assertFalse方法则是断言表达式为假,如此确实减少了代码量,而且代码的可读性也提高了,这也是静态导入用到正确地方所带来的好处

我也知道,每次你创建一个类或者一个方法,你都会想到要为此写文档。我也很确定你很享受于写文档,就像你喜欢偶尔美味的汉堡一样。但是有时候,只是有时候,你会想要松懈一下,也许这次就跳过文档部分。不幸的是,这种行为会很快地失控。

所以在这篇文章中,我想聊聊这个开发者的生活中关键但是通常被忽视并遗忘的部分。希望你会从此爱上文档,明白你的代码为什么能工作,能帮助你、你的团队和使用你的软件的数不尽的用户。

为什么文档很重要

通常,开发者都不会忘记他们两个星期前写的代码。两个月以后甚至更长时间以后他们都会记得。即使我们保证我们从来不忘记我们写过的任何代码,写文档却有另一个理由并且更加重要。

在写代码前理清思路

我会举一个自己的例子:我有一个开发SlideshowFX里一个全新特性的想法,这时我就想直接开始写代码并实现它。但我知道我不是做这项工程的唯一一个有激情的开发者。所以我的典型行为是这样的:

1. 写出以下类主体
public class BurgersManager {
}
2. 思考:“那么,我应该在BurgersManager类中有些CRUD操作”
3. 写下:
public…
4. 思考:“我应该返回什么值?目前来说void就可以”
5. public void addBurger(Burger burger) {
// TODO implement that later
}
public …
6. 思考:“我应该返回被吃掉的汉堡的实例吗?还是void就可以?就像第4步那样。。。”
7. public void eat(Burger burger, boolean fast) {
// TODO …
8. 告诉自己:“糟糕,咖啡时间了,我的咖啡呢。。。”
9. 搜索,喝咖啡,和同事交谈
10. 然后告诉自己:“回去工作吧,我刚才在做什么来着?”

我知道,你在这个例子中看到了自己,对吧?在创造性工作刚开始的时候,我们的思路有些混乱,所以当你直接开始写代码,那么代码也会很混乱。在写代码之前就考虑文档能够帮你理清思路并清除列出你要用代码实现的事。所以第一步应该是写出以下代码:

/**
* 此类通过提供CRUD操作来管理汉堡
* 采用单件模式。可以使用{<a href='http://www.jobbole.com/members/57845349'>@link</a> #getInstance()}来获得这个管理器的实例。
* 之后可以用以下方法来调用CRUD操作:
*/

{<a href='http://www.jobbole.com/members/57845349'>@link</a> #addBurger(Burger)} 用来增加汉堡,并受管理于
* 单件实例 ;
* @作者 Thierry Wasylczenko
* @版本 0.1
* <a href='http://www.jobbole.com/members/chchxinxinjun'>@since</a> BurgerQueen 1.0
*/
public class BurgersManager {

}

这就是一个简短的例子,这个例子能够:

  • 强迫你思考你创建的类的目的是什么
  • 帮你确定你的需要
  • 即使是在你休息之后也能帮你想起来你在做什么
  • 帮助你预估还有什么是需要做的