Java开发中通用的方法和准则

建议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接口类的writeObjectreadObject方法

    实现原理:利用序列化回调,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接口,重写getCharContentopenIputStreamopenOutStream
  • 实现JDK已经提供的两个SimpleJavaFileObjectForWardingJavaFileObject,具体代码参考上述例子

    使用动态编译时,需要注意以下几点:

  • 在框架中谨慎使用
  • 不要早要求高性能的项目使用
  • 动态编译要考虑安全问题
  • 记录动态编译过程

建议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包发布才是万全之策。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值