编写高质量代码:改善Java程序的151个建议(第1章:Java开发中通用的方法和准则___建议14~20)

感情迁移 提交于 2019-11-29 00:27:22

作为一个由影视圈转行做Java的菜鸟来说,读书是很关键的,本系列是用来记录《编写高质量代码 改善java程序的151个建议》这本书的读书笔记。方便自己查看,也方便大家查阅。

建议14:使用序列化类的私有方法巧妙解决部分属性持久化问题

建议15:break万万不可忘

建议16:易变业务使用脚本语言编写

建议17:慎用动态编译

建议18:浅谈Java instanceof

建议19:断言绝对不是鸡肋

建议20:不要只替换一个类

建议14:使用序列化类的私有方法巧妙解决部分属性持久化问题

例如:一个计税系统和一个HR系统,计税系统需要从HR系统获得人员的姓名和基本工资,而HR系统的工资分为两部分:基本工资和绩效工资,绩效工资是保密的,不能泄露到外系统。

public class Salary implements Serializable {
    private static final long serialVersionUID = 2706085398747859680L;
    // 基本工资
    private int basePay;
    // 绩效工资
    private int bonus;

    public Salary(int _basepay, int _bonus) {
        this.basePay = _basepay;
        this.bonus = _bonus;
    }
//Setter和Getter方法略
}
public class Person implements Serializable {

    private static final long serialVersionUID = 9146176880143026279L;

    private String name;

    private Salary salary;

    public Person(String _name, Salary _salary) {
        this.name = _name;
        this.salary = _salary;
    }

    //Setter和Getter方法略

}
public class Serialize {
    public static void main(String[] args) {
        // 基本工资1000元,绩效工资2500元
        Salary salary = new Salary(1000, 2500);
        // 记录人员信息
        Person person = new Person("张三", salary);
        // HR系统持久化,并传递到计税系统
        SerializationUtils.writeObject(person);
    }
}
public class Deserialize {
    public static void main(String[] args) {
        Person p = (Person) SerializationUtils.readObject();
        StringBuffer buf = new StringBuffer();
        buf.append("姓名: "+p.getName());
        buf.append("\t基本工资: "+p.getSalary().getBasePay());
        buf.append("\t绩效工资: "+p.getSalary().getBonus());
        System.out.println(buf);
    }
}

af478a1afde9b65ffcc8911a60f2900dc5d.jpg

但这个不符合需求,你可能会想到一下四种解决方案:

1、java 的transient关键字为我们提供了便利,你只需要实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中。

static修饰的变量也不能序列化。

在bonus前加上关键字transient,使用transient关键字就标志着salary失去了分布式部署的功能,一旦出现性能问题,再想分布式部署就不可能了,此方案否定。

注:分布式部署是将数据分散的存储于多台独立的机器设备上,采用可扩展的系统结构,利用多台存储服务器分担存储负担,利用未知服务器定位存储信息,提高了系统的可靠性、可用性和扩展性。

2、新增业务对象:增加一个Person4Tax类,完全为计税系统服务,就是说它只有两个属性:姓名和基本工资。符合开闭原则,而且对原系统也没有侵入性,只是增加了工作量而已。但是这个方法不是最优方法;

下面展示一个优秀的方案,其中实现了Serializable接口的类可以实现两个私有方法:writeObject和readObject,以影响和控制序列化和反序列化的过程。

public class Person implements Serializable {

    private static final long serialVersionUID = 9146176880143026279L;

    private String name;

    private transient Salary salary;

    public Person(String _name, Salary _salary) {
        this.name = _name;
        this.salary = _salary;
    }
    //序列化委托方法
    private void writeObject(ObjectOutputStream oos) throws IOException {
        oos.defaultWriteObject();
        oos.writeInt(salary.getBasePay());
    }
    //反序列化委托方法
    private void readObject(ObjectInputStream input)throws ClassNotFoundException, IOException {
        input.defaultReadObject();
        salary = new Salary(input.readInt(), 0);
    }
}

其它代码不做任何改动,运行之后结果为:

08661a2169c7a43566650ae1fac47f496a5.jpg

这里用到了序列化的独有机制:序列化回调。

Java调用ObjectOutputStream类把一个对象转换成数据流时,会通过反射(refection)检查被序列化的类是否有writeObject方法,并且检查其实否符合私有,无返回值的特性,若有,则会委托该方法进行对象序列化,若没有,则由ObjectOutputStream按照默认规则继续序列化。同样,从流数据恢复成实例对象时,也会检查是否有一个私有的readObject方法,如果有通过该方法读取属性值。

① oos.defaultWriteObject():告知JVM按照默认规则写入对象

② ois.defaultWriteObject():告知JVM按照默认规则读出对象

③ oos.writeXX和ois.readXX

分别是写入和对出响应的值,类似一个队列,先进先出,如果此处有复杂的数据逻辑,建议按封装Collection对象处理。

上面的方式也是Person失去了分布式部署的能了,确实是,但是HR系统的难点和重点是薪水的计算,特别是绩效工资,它所依赖的参数很复杂,计算公式也不简单(一般是引入脚本语言,个性化公式定制)而相对来说Person类基本上都是静态属性,计算的可能性不大,所以即使为性能考虑,Person类为分布式部署的意义也不大。

既然这样,为何不直接使用transient???

建议15:break万万不可忘

建议16:易变业务使用脚本语言编写

比如PHP、Ruby、groovy、JavaScript等

建议17:慎用动态编译

动态编译一直是Java的梦想,从Java6开始支持动态编译,可以在运行期直接编译.Java文件,执行.class文件,并且获得相关的输入输出,甚至还能监听相关的事件。

1、概念

静态编译:一次性编译,在编译的时候把你所有的模块都编译进去。

动态编译:按需编译,程序在运行的时候,用到哪个模块就编译哪个模块。

2、代码实例

public class Ay{
    public static void main(String[] args) throws Exception{
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        int flag = compiler.run(null, null, null,"D:\\HelloWorld.java");
        System.out.println(flag == 0 ? "编译成功" : "编译失败");
    }
}

/**
 * D盘放置的类的内容
 */
public class HelloWorld {

    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

解释一下:
第一个参数:为java编译器提供参数
第二个参数:得到java编译器的输出信息
第三个参数:接受编译器的错误信息
第四个参数:可变参数(是一个String数组)能传入一个或多个java源文件
返回值:0表示编译成功,非0表示编译失败

3、动态运行编译好的类

public class Ay{
    public static void main(String[] args) throws Exception{
        //获得系统的java编译器
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        //编译文件,编译成功返回 0 否则 返回 1
        int flag = compiler.run(null, null, null,"D:\\HelloWorld.java");
        System.out.println(flag == 0 ? "编译成功" : "编译失败");
        //指定class路径,默认和源代码路径一致,加载class
        URLClassLoader classLoader = new URLClassLoader(new URL[]{new URL("file:/d:/")});
        Object printer = classLoader.loadClass("HelloWorld").newInstance();
        System.out.println(printer.toString());

    }
}

运行结果:

编译成功
HelloWorld@4c583ecf

4、慎用动态编译

① 在框架中谨慎使用

比如要在struts中使用动态编译,动态实现一个类,它若继承自ActionSupport就希望它成为一个Action。能做到,但是debug很困难;再比如在Spring中,写一个动态类,要让它注入到Spring容器中,这是需要花费老大功夫的。

② 不要在要求高性能的项目中使用

动态编译毕竟需要一个编译过程,与静态编译相比多了一个执行环节,因此在高性能的项目中不要使用动态编译

③ 动态编译要考虑安全问题

它是非常典型的注入漏洞,只要上传一个恶意Java程序就可以让你所有的安全工作毁于一旦。

④ 记录动态编译过程

建议记录源文件、目标文件、编译过程、执行过程等日志。不仅仅为了诊断,还是为了安全和审计,对Java项目来说,动态编译和运行时很不让人放心的,留下这些依据可以更好地优化程序。

建议18:浅谈Java instanceof

1、instanceof是Java中的二元运算符,左边是对象,右边是类;当对象是右边类或子类所创建的对象时,返回true,否者,返回false。

注:

① 类的实例包含本身的实例,以及所有直接或间接子类的实例

② instanceof左边显示声明的类型与右边操作元必须是同种类或存在继承关系,也就是说需要位于同一个继承树,否者会编译报错

2、instanceof用法

① 左边的对象实例不能是基本数据类型

② 左边的对象和右边的类不在同一个继承树上

③ null用instanceof跟任何类型比较时都是false

建议19:断言绝对不是鸡肋

1、简介

断言也就是所谓的assert,是jdk1.4中加入的新功能。

他主要使用在代码开发和测试阶段,用于对某些关键数据的判断,如果这个关键数据不是你程序所预期的数据,程序就提出警告或退出。

当软件正式发布后,可以取消断言部分的代码。

2、语法:

assert<布尔表达式>

assert<布尔表达式> : <错误信息>

在布尔表达式为假时,跑出AssertionError错误,并附带了错误信息。

assert的语法比较简单,有以下两个特性:

① assert默认是不开启的

然后再VM栏里输入-enableassertions或者-ea即可

② assert跑出的异常AssertionError继承自Error

断言失败后,JVM会跑出一个AssertionError的错误,它继承自Error,这是一个错误,不可恢复。

3、assert的使用禁忌

① 对外的公开方法中不可使用

防御式编程最核心的部分就是:所有的外部因素(输入参数、环境变量、上下文)都是“邪恶”的,都存在着企图摧毁程序的罪恶本源,为了抵制它,我们要在程序处处设置合法性检查,不满足条件就不执行后续程序,以保护后续程序的正确性,但此时不能用断言做输入检查,特别是公开方法。

② 在执行逻辑代码的情况下

assert的支持是可选的,在开发时运行,生产环境下停止运行即可,因此在assert的布尔表达式中不能执行逻辑代码,否者会因为环境的不同产生不同的逻辑。

    public void doSomething(List list, Object element) {
        assert list.remove(element) : "删除元素" + element + "失败";
        /*业务处理*/
    }

这段代码在assert启用的环境下,没有任何问题,但是一旦投入生产环境,就不会启用断言了,这个方法就彻底完蛋,list的删除动作永远不会执行,永远不会报错或异常,因为根本没有执行!

4、assert的使用场景

按照正常的执行逻辑不可能到达的代码区域可以使用assert。

① 在私有方法中放置assert作为输入参数的校验

私有方法的使用者是自己,是自己可以控制的,因此加上assert可以更好地预防自己犯错或者无意的程序犯错。

② 流程控制中不可能到达的区域

程序执行到assert这里就是错误的

③ 建立程序探针

我们可能会在一段程序中定义两个变量,分别代两个不同的业务含义,但是两者有固定的关系,例如:var1=var2 * 2,那我们就可以在程序中到处设"桩"了,断言这两者的关系,如果不满足即表明程序已经出现了异常,业务也就没有必要运行下去了。

建议20:不要只替换一个类

注意:发布应用系统时禁止使用类文件替换方式,整体WAR包发布才是万全之策。

 

编写高质量代码:改善Java程序的151个建议@目录

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!