建议1:不要在常量和变量中出现易混淆的字母
- 包名:全小写。
- 类名:首字母全大写。
- 常量:全部大写并用下划线分割。
- 变量:驼峰命名法。
package com.company;
/**
* 数字后跟小写字母l的问题
*/
public class Client {
public static void main(String[] args) {
long i = 1l;
System.out.println("i的两倍是:" + (i + i));
}
}
输出结果:i的两倍是:2
注意:字母"l"作为长整型标志时务必大写。
建议2:莫让常量蜕变成变量
package com.company;
import java.util.Random;
public class Client {
public static void main(String[] args) {
System.out.println("常量会变哦:" + Const.RAND_CONST);
}
}
/*接口常量*/
interface Const {
// 这还是常量吗?
public static final int RAND_CONST = new Random().nextInt();
}
注意:务必让常量的值在运行期间保持不变。
建议3:三元操作符的类型务必一致
package com.company;
public class Client {
public static void main(String[] args) {
int i = 80;
String s = String.valueOf(i < 100 ? 90 : 100);
String s1 = String.valueOf(i < 100 ? 90 : 100.0);
System.out.println("两者是否相等:" + s.equals(s1));
}
}
结果:两者是否相等:false
- 三元操作符类型的转换规则:
- 若两个操作数不可转换,则不做转换,返回值为Object类型。
- 若两个操作数市明确类型的表达式(比如变量),则按照正常的而进制数字来转换,int类型转换为long类型,long类型转换为float类型等。
- 若两个操作数中有一个市数字S,另外一个是表达式,且其类型标记为T,那么,若数字S在T的范围内,则转换为T类型;若S超出了T类型的范围,则T转换为S类型。
- 若两个操作数都是直接量数字,则返回值类型为范围较大者。
建议4:避免带有变长参数的方法重载
变长参数遵循的规则:变长参数必须是方法中的最后一个参数;一个方法不能定义多个变长参数。
package com.company;
import java.text.NumberFormat;
public class Client {
// 简单折扣计算
public void callPrice(int price, int discount) {
float knowdownPrice = price * discount / 100.0F;
System.out.println("简单折扣后的价格是:" + formateCurrency(knowdownPrice));
}
// 复杂多折扣计算
public void callPrice(int price, int... discounts) {
float knockdownPrice = price;
for (int discount : discounts) {
knockdownPrice = knockdownPrice * discount / 100;
}
System.out.println("复杂折扣后的价格是:" + formateCurrency(knockdownPrice));
}
// 格式化成本的货币形式
private String formateCurrency(float price) {
return NumberFormat.getCurrencyInstance().format(price / 100);
}
public static void main(String[] args) {
Client client = new Client();
// 499的货物,打75折
client.callPrice(49900, 75);
}
}
结果:简单折扣后的价格是:¥374.25
建议5:别让null值和空值威胁到变长方法
package com.company;
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);
}
}
结果:编译通不过。
public static void main(String[] args) {
Client client = new Client();
String[] strs = null;
client.methodA("China", strs);
}
这样修改后便可以编译通过。
建议6:覆写变长方法也循规蹈矩
覆写必须满足的条件:
- 重写方法不能缩小访问权限。
- 参数列表必须与被重写方法相同。
- 返回类型必须与被重写方法的相同或是其子类。
- 重写方法不能抛出新的异常,或者超出父类范围的异常,但是可以抛出更少、更有限的异常,或者不抛出异常。
package com.company;
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... discounts) {
System.out.println("Base......fun");
}
}
// 子类,覆写父类方法
class Sub extends Base {
@Override
void fun(int price, int[] discounts) {
System.out.println("Sub......fun");
}
}
}
结果:编译通不过。
注意:覆写的方法参数与父类相同,不仅仅是类型、数量,还包括显示形式。
建议7:小心自增的陷阱
package com.company;
/**
* 警惕自增的陷阱
*/
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++是一个表达式,是有返回值的,它的返回值就是count自加前的值,Java对自加是这样处理的:首先把count的值(注意是值,不是引用)拷贝到一个临时变量区,然后对count变量加1,最后返回临时变量区的值。
程序第一次循环时的详细处理步骤如下:
- 步骤1 JVM把count值(其值是0)拷贝到临时变量区。
- 步骤2 count值加1,这时候count的值是1。
- 步骤3 返回临时变量区的值,注意这个值是0,没修改过。
- 步骤4 返回值赋值给count,此时count值被重置为0。
“count=count++” 这条语句可以按照如下代码来解释:
public static int mockAdd(int count) {
//先保存初始值
int temp = count;
//做自增操作
count = count + 1;
//返回原始值
return temp;
}
建议8:不要让旧语法困扰你
package com.company;
/**
* 不用让旧语法困扰你
*/
public class Client {
public static void main(String[] args) {
//数据定义及初始化
int fee = 200;
//其他业务处理
saveDefault:
save(fee);
//其他业务处理
}
static void saveDefault() {
}
static void save(int fee) {
}
}
注意:goto语法。
建议9:少用静态导入
从java5开始引入了静态导入语法,目的是为了减少字符输入量,提高代码的可阅读性。
package com.company.section1;
/**
* 建议9:少用静态导入
*/
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;
}
}
使用静态导入后的程序如下:
package com.company.section2;
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常量引入到本类中。
如果在一个类中有多个静态导入语句时,若还使用了*通配符,简直是恶梦,看一段例子:
package com.company.section3;
import java.text.NumberFormat;
import static java.lang.Double.parseDouble;
import static java.lang.Integer.parseInt;
import static java.lang.Math.PI;
import static java.text.NumberFormat.getInstance;
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);
}
}
对于静态导入,一定要遵循两个规则:
- 不使用*通配符,除非是导入静态常量类(只包含常量的类或接口)。
- 方法名是具有明确、清晰表象意义的工具类。
来看看JUnit4中使用的静态导入的例子:
package com.company.section3;
import org.junit.Test;
import static org.junit.Assert.*;
/*
* 具有明确表象意义的方法
*/
public class DaoTest {
@Test
public void testInsert() {
//断言
assertEquals("foo", "foo");
assertFalse(Boolean.FALSE);
}
}
class Base {
public Object doSomething(String str) {
return null;
}
}
class Sub extends Base {
@Override
public String doSomething(String str) {
return null;
}
}
建议10:不要在本类中覆盖静态导入的变量和方法
package com.company;
/**
* 建议10:不用在本类中覆盖静态导入的变量和方法
*/
public class Client {
//常量名与静态导入的PI相同
public final static String PI = "祖冲之";
//方法名与静态导入的相同
public static int abs(int abs) {
return 0;
}
public static void main(String[] args) {
System.out.println("PI=" + PI);
System.out.println("abs(100)=" + abs(-100));
}
}
结果:
PI=祖冲之
abs(100)=0
注意: 编译器“最短路径”原则:如果能够在本类中查找到的变量、常量、方法,就不会到其他包或父类、接口中查找,以确保本类中的属性、方法优先。因此,如果要变更一个被静态导入的方法,最好的办法是在原始类中重构,而不是在本类中覆盖。
建议11:养成良好习惯,显示声明UID
来看一个简单的序列化类:
package com.company;
import java.io.Serializable;
public class Person implements Serializable {
private static final long serialVersionUID = 3424643650528555799L;
private String name;
private int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
protected void test() {
}
}
定义一个消息的生产者,代码如下:
package com.company;
/**
* 消息的生产者,也就是序列化类
*/
public class Producer {
public static void main(String[] args) throws Exception {
Person person = new Person();
person.setName("混世魔王");
//序列化,保存到磁盘上
SerializationUtils.writeObject(person);
}
}
引入一个工具类,作用是对一个类进行序列化和反序列化,并存储到硬盘上(模拟网络传输),其代码如下:
package com.company;
import java.io.*;
/**
* 序列化工具
*/
public class SerializationUtils {
private static String FILE_NAME = "c:/obj.bin";
// 序列化
public static void writeObject(Serializable s) {
try {
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_NAME));
oos.writeObject(s);
oos.close();
} catch (Exception e) {
//异常处理
}
}
public static Object readObject() {
Object obj = null;
// 反序列化化
try {
ObjectInput input = new ObjectInputStream(new FileInputStream(FILE_NAME));
obj = input.readObject();
input.close();
} catch (Exception e) {
//异常处理
}
return obj;
}
}
发送到消费者,并进行反序列化,生成实例对象,代码如下:
package com.company;
/**
* 养成良好习惯,显式声明UID
*/
public class Consumer {
public static void main(String[] args) throws Exception {
// 反序列化化
Person p = (Person) SerializationUtils.readObject();
System.out.println("name=" + p.getName());
}
}
JVM是根据什么来判断一个类版本的呢?
通过SerialVersionUID,也叫流标识符(Stream Unique Identifier),既类的版本定义的,可以显示声明也可以隐世声明。显示声明格式如下:
private static final long serialVersionUID = 3424643650528555799L;
生成的依据是通过包名、类名、继承关系、非私有的方法和属性,以及参数、返回值等诸多因子计算得出的,极度复杂,基本上计算出来的这个值是唯一的。
注意:显示声明serialVersionUID可以避免对象不一致,但尽量不要以这种方式向JVM“撒谎”。