建议1:不要在常量和变量中出现易混淆的字母
包名全小写,类名首字母大写,常量全部大写并用下划线分隔,变量采用驼峰命名法(Camel Case)命名等,这些都是最基本的Java编码规范。但是在变量的声明中注意不要引入容易混淆的字母。如下所示:
public class Client {
public static void main(String[] args) {
long i = 1l;
System.out.println("i的两倍是:" + (i + i));
}
}
输出结果为:2,而不是22
注意:
- 混淆字母数字区分大小写或者采用注释手段
- 字母
l
作为长整型标志时务必大写
建议2:莫让常量蜕变成变量
代码如下:
public class Client {
public static void main(String[] args) {
System.out.println("常量会变哦:" + Const.RAMD_CONST);
}
}
常量类:
public class Const {
//这还是常量吗?
public static final int RAMD_CONST = new Random().nextInt();
}
注意:
- 线程运行期间该值不会发生变化,即使多个线程同时读取。如果主线程销毁,重启主线程,则该值会重新生成
- 务必让常量的值在运行期保持不变
建议3:三元操作符的类型务必一致
三元操作符时if-else
的简化写法,在项目中使用它的地方很多,也非常好用,但是好用又简单的东西并不表示就可以随便用。代码如下:
public class Client {
public static void main(String[] args) {
long i = 80;
String s1 = String.valueOf(i < 100 ? 90 : 100);
String s2 = String.valueOf(i < 100 ? 90 : 100.0);
System.out.println("两者是否相等:" + s1.equals(s2));
}
}
运行结果是:两者是否相等:false。
注意:三元操作符的两个操作数类型不一致时,需要转换成统一的数据类型,保证三元操作符中的两个操作数类型一致,即可减少肯错误的发生
三元操作符类型的转换规则:
- 若两个操作数不可转换,则不做转换,返回值为
Object
类型 - 若两个操作数是明确类型的表达式(比如常量),则按照正常的二进制数字来转换,int类型转换为long类型,long类型转换为float类型等。
- 若两个操作数中有一个是数字S,另一个是表达式,且其类型标示为T,那么,若数字S在T的范围内,则转换为T类型;若S超出了T类型的范围,则T类型转换为S类型
- 若两个操作数都是字面量数字,则返回值类型为范围较大者
建议4:避免带有变长参数的方法重载
在项目和系统的开发中,为了提高方法的灵活度和可复用性,我们经常要传递不确定数量的参数到方法中,在Java 5之前常用的设计技巧就是把形参定义成Collection类型或其子类类型,或者是数组类型,这种方法的缺点就是需要对空参数进行判断和筛选,比如实参为null值或长度为0的Collection或数组。而Java 5引入了变长参数就是为了更好地提高方法的复用性,让方法调用者可以“随心所欲”地传递实参数量,当然变长参数也是要遵循一定规则的,比如变长参数必须是方法中的最后一个参数;一个方法不能定义多个变长参数等,这些基本规则需要牢记,但是即使这样,仍然可能出现错误,如下代码所示:
import java.text.NumberFormat;
public class Client {
//简单折扣后的价格
public void callPrice(int price, int discount) {
float knockdownPrice = price * discount / 100.0F;
System.out.println("简单折扣后的价格是:" + formatCurrency(knockdownPrice));
}
// 复杂折扣后的价格
public void callPrice(int price, int... discounts) {
float knockdownPrice = price;
for (int discount : discounts) {
knockdownPrice = knockdownPrice * discount / 100;
}
System.out.println("复杂折扣后的价格是:" + formatCurrency(knockdownPrice));
}
//格式化成本的货币形式
private String formatCurrency(float price) {
return NumberFormat.getCurrencyInstance().format(price / 100);
}
public static void main(String[] args) {
Client client = new Client();
client.callPrice(10000, 75);
}
}
结果显示:client.callPrice(10000, 75);
调用的是callPrice(int price, int discount)
方法。
JVM进行重载的方法调用时会遵循四个原则:
- 在不考虑装箱拆箱、可变参数的情况下选择
- 在不考虑可变参,考虑装箱拆箱的情况下选择
- 在考虑装箱拆箱、可变参数的情况下选择
- 如果在某个条件下选出多个方法依靠类型亲疏原则:如果一个方法形参是Object,重载方法是String,实参时String,两个方法在第一个筛选条件下都会被选择,这时候就会选择重载方法
建议5:别让null值和空值威胁到变长方法
import java.text.NumberFormat;
public class Client {
public void methodA(String str, Integer... is) {}
public void methodA(String str, String... strs) {}
public static void main(String[] args) {
Client client = new Client();
client.methodA("china",0);
client.methodA("china","people");
//编译报错
client.methodA("china");
client.methodA("china", null);
}
}
结果:有两处编译不通过,client.methodA("china");
,client.methodA("china", null);
。这样设计违反了KISS原则(Keep It Simple,Stupid
,即懒人原则),编译器不知道调用哪个方法,null值也没有实际意义。
public static void main(String[] args) {
Client client = new Client();
String[] strs = null;
//编译通过
client.methodA("china", strs);
}
建议6:重写变长方法也循规蹈矩
在Java中,子类重写父类方法既可以修正Bug也可以提供扩展的业务功能支持,同时还符合开闭原则(Open-closed Principle
)。重写方法必须满足的条件如下:
- 重写方法不能缩小访问权限
- 参数列表必须与被重写方法相同:参数数量相同、参数类型相同、顺序相同
- 返回类型必须与被重写方法的相同或是其子类
- 重写方法不能抛出新的异常,或者超出父类访问的异常,但是可以抛出更少、更有限的异常,或者不抛出异常
import java.text.NumberFormat;
public class Client {
public static void main(String[] args) {
Base base = new Sub();
base.fun(100, 50);
//不转型
Sub sub = new Sub();
//编译报错
sub.fun(100,50);
}
}
class Base {
void fun(int price, int... discount) {
System.out.println("base....fun");
}
}
class Sub extends Base {
@Override
void fun(int price, int[] discount) {
System.out.println("sub....fun");
}
}
base.fun(100, 50);
执行时,base对象把子类对象Sub做了向上转型,形参是由父类决定的,由于是变长参数,在编译时,base.fun(100, 50);
中的“50”会被编译成{50}
,再由子类Sub执行。
sub.fun(100,50);
执行时,编译器不会把50
做类型转换,因为数组本身也是一个对象。
结论:重写的方法参数与父类相同,不仅仅是类型、数量,还包括显示形式。
建议7:警惕自增的陷阱
public class Client {
public static void main(String[] args) {
int count = 0;
for (int i = 0; i < 10; i++) {
count = count++;
}
System.out.println("count=" + count);
}
输出结果:count=0
count++
是一个表达式,它的返回值是count自加前的值,Java是这样处理的:
- JVM把count值拷贝到临时变量区
- count值加1,这时候count的值是1
- 返回临时变量区的值,注意这个值是0,没修改过
- 返回值赋值给count,此时count值被重置为0
建议8:不要让旧语法困扰你
建议9:少用静态导入
对于静态导入,一定要遵循两个规则:
- 不使用
*
(星号)通配符,除非是导入静态常量类(只包含常量的类或接口) - 方法名是具有明确、清晰表象意义的工具类
建议10:不要在本类中覆盖静态导入的变量和方法
编译器有一个最短路劲
原则:如果能够在本类中查找到的变量、常量、方法,就不会到其他包或父类、接口中查找,以确保本类中的属性、方法优先。
建议11:养成良好习惯,显示声明UID
类实现Serializable
接口的目的是为了可持久化,比如网络传输或本地存储,为系统的分布和异构部署提供先决支持条件。
JVM是通过Serializable
,也叫做流标识符,即类的版本定义的,它可以显式声明也可以隐式声明。显式声明格式如下:private static final long serialVersionUID = XXXXXL
。而隐式声明则是编译器在编译的时候自动生成。生成的依据是通过包名、类名、继承关系、非私有的方法和属性,以及参数、返回值等诸多因子计算得出的。
serialVersionUID
的作用:JVM在反序列化时,会比较数据流中的serialVersionUID
是否相同,如果相同,则认为类没有发生改变,可以把数据流load为实例对象;如果不相同,JVM会抛出InvalidClassException
。
注意:显式声明可以避免对象不一致,但尽量不要以这种发生向JVM“撒谎”。
建议12:避免用序列化类在构造函数中为不变量赋值
反序列化时构造函数不会执行。
反序列化的执行过程是这样的:JVM从数据流中获取一个Object对象,然后根据数据流中的类文件描述信息(在序列化时,保存到磁盘的对象文件中包含了类描述信息),发现是final变量,需要重新计算,于是引用Person类中的name值,而此时JVM又发现name竟然没有赋值,不能引用,于是JVM就不再初始化,保持原值状态。
注意:在序列化类中,不使用构造函数为final变量赋值。
建议13:避免为final变量复杂赋值
反序列化时final变量在以下情况下不会被重新赋值:
- 通过构造函数为final变量赋值
- 通过方法返回值为final变量赋值
- final修饰的属性不是基本类型
建议14:使用序列化类的私有方法巧妙解决部分属性持久化问题
例如:一个计税系统和一个HR系统,计税系统需要从HR系统获得人员的姓名和基本工资,而HR系统的工资分为两部分:基本工资和绩效工资,其中绩效工资是保密的,不能泄漏到外系统中。
实现方式:实现Serializable
接口类的writeObject
和readObject
方法
实现原理:利用序列化回调,Java调用ObjectOutputStream
类把一个对象转换成流数据时,会通过反射检查被序列化的类是否有writeObject
方法,并且检查其是否符合私有、无返回值的特性。若有,则会委托该方法进行对象序列化,若没有,则由ObjectOutputStream
按照默认规则继续序列化。同样,在从流数据恢复成实例对象时,也会检查是否有一个私有的readObject
方法,如果有,则会通过该方法读取属性值。
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
public class Person implements Serializable {
private static final long serialVersionUID = -6150221270390154035L;
//姓名
private String name;
// 薪水
private Salary salary;
public Person(String name, Salary salary) {
this.name = name;
this.salary = salary;
}
//省略get\set方法
//序列化委托方法
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
out.writeInt(salary.getBasePay());
}
//反序列化委托方法
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
salary = new Salary(in.readInt(),0);
}
}
建议15:case语句后面一定记得随手写break
建议16:易变业务使用脚本语言编写
public static void main(String[] args) throws FileNotFoundException, ScriptException, NoSuchMethodException {
//获得一个JavaScript的执行引擎
ScriptEngine engine = new ScriptEngineManager().getEngineByName("javascript");
// 建立上下文变量
Bindings bind = engine.createBindings();
//绑定上下文,作用域是当前引擎范围
bind.put("factor", 1);
engine.setBindings(bind, ScriptContext.ENGINE_SCOPE);
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextInt()) {
int first = scanner.nextInt();
int sec = scanner.nextInt();
System.out.println("输入参数是:" + first + "," + sec);
//执行js代码
engine.eval(new FileReader("E:\\Code\\Java8\\src\\main\\java\\script.js"));
//是否可调用方法
if (engine instanceof Invocable) {
Invocable in = (Invocable) engine;
//执行js中的函数
Double result = (Double) in.invokeFunction("formula", first, sec);
System.out.println("运算结果:" + result.intValue());
}
}
}
function formula(val1, val2) {
return val1 + val2 * factor;
}
建议17:慎用动态编译
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
public class Client {
public static void main(String[] args) throws InvocationTargetException, IllegalAccessException,
NoSuchMethodException, ClassNotFoundException, InstantiationException {
//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> optionList = new ArrayList<>();
//编译文件的存放地址,注意:此处是为Eclipse工具特设的
optionList.addAll(Arrays.asList("-d", "/bin"));
//要编译的单元
List<JavaFileObject> jfos = Arrays.asList(jfo);
//设置编译环境
JavaCompiler.CompilationTask task = cmp.getTask(null, fm, null, optionList, 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(methodName, String.class);
System.out.println(str);
}
}
}
import java.io.IOException;
import java.net.URI;
import javax.tools.SimpleJavaFileObject;
//文本中的Java对象
public class StringJavaObject extends SimpleJavaFileObject {
//源代码
private String context = "";
//遵循Java规范的类名及文件
public StringJavaObject(String _javaFileName, String _content) {
super(_createStringJavaObjectUri(_javaFileName), kind.SOURCE);
context = _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 context;
}
}
这是一个动态编译的模板程序,Java的动态编译对源提供了多个渠道。可以是字符串、文本文件或编译过的字节码文件,以及存放在数据库中的明文代码或是字节码。
实现方式:
- 实现
javaFileObject
接口,重写getCharContent
、openIputStream
、openOutStream
- 实现JDK已经提供的两个
SimpleJavaFileObject
、ForWardingJavaFileObject
,具体代码参考上述例子
使用动态编译时,需要注意以下几点:
- 在框架中谨慎使用
- 不要早要求高性能的项目使用
- 动态编译要考虑安全问题
- 记录动态编译过程
建议18:避免instanceof非预期结果
instanceof是一个简单的二元操作符,它是用来判断一个对象是否是一个类实例的,其操作类似于>=
、==
。
//true:"String"是字符串,字符串继承Object
boolean b1 = "String" instanceof Object;
//true:一个类的对象当然是它的实例了
boolean b2 = new String() instanceof String;
//false:可以编译通过,但Object是父类型
boolean b3 = new Object() instanceof String;
//编译失败,'A'是char类型,还是基本类型,instanceof只能用于对象的判断,不能用于基本类型的判断
boolean b4 = 'A' instanceof Character;
//false:instanceof特有规则-若左操作数是null,结果就直接返回false,不再运算右操作数是什么类
boolean b5 = null instanceof String;
//false:null没有类型,即使做类型转换还是null
boolean b6 = (String) null instanceof String;
//编译不通过,date和String没有继承或实现关系
boolean b7 = new Date() instanceof String;
//false:T是String类型,与Date之间没有继承或实现关系
//Java泛型是为编码服务的,在编译成字节码时,T已经是Object类型,传递的实参是String类型,即T的表面类型是Object,实际类型是String,也就是说,"t instanceof Date"等价于"Object instanceof Date"
boolean b8 = new GenericClass<String>().isDateInstance("");
建议19:断言绝对不是鸡肋
断言语法特点
- 默认不启用
- 抛出的异常
AssertionError
是继承自Error
的
在以下两种情况下不可使用
- 在对外公开的方法中
- 在执行逻辑代码的情况下
可以使用断言的情况
- 在私有方法中设置assert作为输入参数的校验
- 流程控制中不可能达到的区域
- 建立程序探针
建议20:不要只替换一个类
常量接口(类)中的常量值修改后,直接把该类的class文件替换后发布项目时,可能会导致取不到修改后的常量值。
原因是:对于final修饰的基本类型和String类型,编译器会认为它是稳定态,所以在编译期就直接把值编译到字节码中了,避免了在运行期引用,以提高代码的执行效率。
注意:发布应用系统时禁止使用类文件替换方式,整体WAR包发布才是万全之策。