作为一个由影视圈转行做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);
}
}
但这个不符合需求,你可能会想到一下四种解决方案:
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);
}
}
其它代码不做任何改动,运行之后结果为:
这里用到了序列化的独有机制:序列化回调。
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包发布才是万全之策。
来源:oschina
链接:https://my.oschina.net/u/4006148/blog/3072740