读书笔记----《编写高质量代码:改善Java程序的151个建议》第二/三章

基本类型

21. 用偶判断,不用奇判断
String str=i+"->"+(i%2==1?"奇数""偶数");
//1->奇数
//0->偶数
//-1->偶数
//-2->偶数

//改进:
i%2==0?"偶数""奇数"

取余运算在正整数运算上并没有什么歧义但是在负整数取余运算时,各种语言并不一致:

语言算式结果
C++(G++ 编译)cout << (-7) % 3-1
Java(1.6)System.out.println((-7) % 3);-1
Python 2.6(-7) % 32
22. 用整数类型处理货币
public class Client{
public static void main(String[]args){
System.out.println(10.00-9.60);
}
}

打印出来的是0.40000000000000036
这是由浮点数的存储规则所决定的,我们先来看0.4这个十进制小数如何转换成二进制小数,使用“乘2取整,顺序排列”法,我们发现0.4不能使用二进制准确的表示,在二进制数世界里它是一个无限循环的小数。
可以这样理解,在十进制的世界里没有办法准确表示1/3,那在二进制世界里当然也无法准确表示1/5。
要解决此问题有两种方法:
(1)使用BigDecimal
金融行业应用较多。
(2)使用整型
把参与运算的值扩大100倍,并转变为整型,然后在展现时再缩小100倍,在非金融行业(如零售行业)应用较多。

23. 不要让类型默默转换
public static final int LIGHT_SPEED=30*10000*1000long dis2=LIGHT_SPEED*60*8;
System.out.println("太阳与地球的距离是:"+dis2+"米");
//输出:太阳与地球的距离是:-2028888064米

改正:
long dis2=LIGHT_SPEED*60L*8;

24. 边界,边界,还是边界

在单元测试中,有一项测试叫做边界测试(也有叫做临界测试),如果一个方法接收的是int类型的参数,那以下三个值是必测的:0、正最大、负最小,其中正最大和负最小是边界值,如果这三个值都没有问题,方法才是比较安全可靠的。
另外Web校验都是在页面上通过JavaScript实现的,只能限制普通用户,而对于高手,这些校验基本上就是摆设,HTTP是明文传输的,将其拦截几次,分析一下数据结构,然后再写一个模拟器,一切前端校验就都成了浮云!
所以:不要指望前端校验可以拦截非法输入,后端必须处理校验。

25. 不要让四舍五入亏了一方

由美国银行家对现有的四舍五入算法提出了修正算法,叫做银行家舍入(Banker’s Round)的近似算法,其规则如下:
四舍六入五考虑,五后非零就进一,五后为零看奇偶,五前为偶应舍去,五前为奇要进一。
Java 5以上的版本中使用银行家的舍入法则非常简单,直接使用RoundingMode类提供的Round模式即可,示例代码如下:

public class Client{
public static void main(String[]args){
//存款
BigDecimal d=new BigDecimal(888888);
//月利率,乘3计算季利率
BigDecimal r=new BigDecimal(0.001875*3);
//计算利息
BigDecimal i=d.multiply(r).setScale(2,RoundingMode.HALF_EVEN);
System.out.println("季利息是:"+i);
}
}

BigDecimal和RoundingMode是一个绝配,想要采用什么舍入模式使用RoundingMode设置即可,目前Java支持以下七种舍入方式:
ROUND_UP:远离零方向舍入。
ROUND_DOWN:趋向零方向舍入。
ROUND_CEILING:向正无穷方向舍入。
ROUND_FLOOR:向负无穷方向舍入。
HALF_UP:最近数字舍入(5进)。
HALF_DOWN:最近数字舍入(5舍)。
HALF_EVEN:银行家算法。

26. 提防包装类型的null值

包装类型参与运算时,要做null值校验。

27. 谨慎包装类型的大小比较

包装好的不要用< ,> , != 等

28. 优先使用整型池
public static Integer valueOf(int i){
final int offset=128if(i>=-128&&i<=127){//must cache
return IntegerCache.cache[i+offset];
}
return new Integer(i);
}

参数在-128和127之间,则直接从整型池中获得对象。

29. 优先选择基本类型

自动拆箱(装箱)只有在赋值时才会发生,和重载没有关系。

30. 不要随便设置随机种子

在同一台机器上,甭管运行多少次,所打印的随机数都是相同的,也就是说第一次运行,会打印出这三个随机数,第二次运行还是打印出这三个随机数,只要是在同一台硬件机器上,就永远都会打印出相同的随机数,似乎随机数不随机了,问题何在?
那是因为产生随机数的种子被固定了,在Java中,随机数的产生取决于种子,随机数和种子之间的关系遵从以下两个规则:
(1)种子不同,产生不同的随机数。
(2)种子相同,即使实例不同也产生相同的随机数。
Random类的默认种子(无参构造)是System.nanoTime()的返回值
注意:System.nanoTime不能用于计算日期,那是因为“固定”的时间点是不确定的,纳秒值甚至可能是负值,这点与System.currentTimeMillis不同。


类、对象及方法

31. 在接口中不要存在实现代码
32. 静态变量一定要先声明后赋值
33. 不要覆写静态方法

覆写父类的静态方法如果用父类表面类型调用只能调到父类方法。而得不到子类方法。
对于静态方法来说,首先静态方法不依赖实例对象,它是通过类名访问的;其次,可以通过对象访问静态方法,如果是通过对象调用静态方法,JVM则会通过对象的表面类型查找到静态方法的入口,继而执行之。

34. 构造函数尽量简化
public class Client{
public static void main(String[]args){
Server s=new SimpleServer(1000);
}
}
//定义一个服务
abstract class Server{
public final static int DEFAULT_PORT=40000public Server(){
//获得子类提供的端口号
int port=getPort();
System.out.println("端口号:"+port);
/*进行监听动作*/
}
//由子类提供端口号,并做可用性检查
protected abstract int getPort();
}
class SimpleServer extends Server{
private int port=100//初始化传递一个端口号
public SimpleServer(int_port){
port=_port;
}
//检查端口号是否有效,无效则使用默认端口,这里使用随机数模拟
@Override
protected int getPort(){
return Math.random()>0.5?port:DEFAULT_PORT;
}
}

多次运行看看,输出结果要么是“端口号:40000”,要么是“端口号:0”
解释:
子类实例化时,会首先初始化父类(注意这里是初始化,可不是生成父类对象),也就是初始化父类的变量,调用父类的构造函数,然后才会初始化子类的变量,调用子类自己的构造函数,最后生成一个实例对象。了解了相关知识,我们再来看上面的程序,其执行过程如下:
(1)子类SimpleServer的构造函数接收int类型的参数:1000。
(2)父类初始化常变量,也就是DEFAULT_PORT初始化,并设置为40000。
(3)执行父类无参构造函数,也就是子类的有参构造中默认包含了super()方法。
(4)父类无参构造函数执行到“int port=getPort()”方法,调用子类的getPort方法实现。
(5)子类的getPort方法返回port值(注意,此时port变量还没有赋值,是0)或DEFAULT_PORT(此时已经是40000)了。

35. 避免在构造函数中初始化其他类
36. 使用构造代码块精炼程序
public class Client{
    {
        //构造代码块
        System.out.println("执行构造代码块");
    }
    public Client(){
    }
}

构造代码块会在每个构造函数内首先执行,可以把构造代码块应用到如下场景中:
(1)初始化实例变量(Instance Variable)
(2)初始化实例环境

37. 构造代码块会想你所想

构造代码块在存在构造函数相互调用的情况下只会被执行一次。

38. 使用静态内部类提高封装性

只有在是静态内部类的情况下才能把static修复符放在类前,其他任何时候static都是不能修饰类的。
静态内部类与普通内部类的区别:
(1)静态内部类不持有外部类的引用
(2)静态内部类不依赖外部类
(3)普通内部类不能声明static的方法和变量

39. 使用匿名类的构造函数

(1)l2=new ArrayList(){}
l2代表的是一个匿名类的声明和赋值,它定义了一个继承于ArrayList的匿名类,只是没有任何的覆写方法而已,其代码类似于:

//定义一个继承ArrayList的内部类
class Sub extends ArrayList{
}
//声明和赋值
List l2=new Sub();

(2)l3=new ArrayList(){{}}
这个语句就有点怪了,还带了两对大括号,我们分开来解释就会明白了,这也是一个匿名类的定义,它的代码类似于:

//定义一个继承ArrayList的内部类
class Sub extends ArrayList{
{
//初始化块
}
}
//声明和赋值
List l3=new Sub();

就是多了一个初始化块而已,起到构造函数的功能。

40. 匿名类的构造函数很特殊
//定义一个枚举,限定操作符
enum Ops{ADD, SUB}
class Calculator{
private int i, j,result;
//无参构造
public Calculator(){}
//有参构造
public Calculator(int_i, int_j){
i=_i;
j=_j;
}
//设置符号,是加法运算还是减法运算
protected void setOperator(Ops_op){
result=_op.equals(Ops.ADD)?i+j:i-j;
}
//取得运算结果
public int getResult(){
return result;
}
}


public static void main(String[]args){
Calculator c1=new Calculator(12){
{
setOperator(Ops.ADD);
}
};
System.out.println(c1.getResult());
}
//输出结果:3

匿名类的构造函数特殊处理机制,一般类(也就是具有显式名字的类)的所有构造函数默认都是调用父类的无参构造的,而匿名类因为没有名字,只能由构造代码块代替,也就无所谓的有参和无参构造函数了,它在初始化时直接调用了父类的同参数构造,然后再调用了自己的构造代码块,也就是说上面的匿名类与下面的代码是等价的:

//加法计算
class Add extends Calculator{
{
setOperator(Ops.ADD);
}
//覆写父类的构造方法
public Add(int_i, int_j){
super(_i,_j);
//这里是关键
}
}

它首先会调用父类同参数的构造函数,而不是无参构造,这是匿名类的构造函数与普通类的差别。

41. 让多重继承成为现实

除了接口,内部类也可以完成多重继承

42. 让工具类不可实例化
43. 避免对象的浅拷贝

一个类实现了Cloneable接口就表示它具备了被拷贝的能力,如果再覆写clone()方法就会完全具备拷贝能力。
Object的clone方法提供的是一种浅拷贝方式,它的拷贝规则如下:
(1)基本类型 拷贝其值
(2)对象 拷贝地址引用
(3)String字符串 拷贝的也是一个地址,是个引用,但是在修改时,它会从字符串池(String Pool)中重新生成新的字符串,原有的字符串对象保持不变,在此处我们可以认为String是一个基本类型。

44. 推荐使用序列化实现对象的拷贝

如果一个项目中有大量的对象是通过拷贝生成的,那我们该如何处理?每个类都写一个clone方法?工作量巨大。其实,可以通过序列化方式来处理:

public class CloneUtils {  
    @SuppressWarnings("unchecked")  
    public static <T extends Serializable> T clone(T obj){  
        T cloneObj = null;  
        try {  
            //写入字节流  
            ByteArrayOutputStream out = new ByteArrayOutputStream();  
            ObjectOutputStream obs = new ObjectOutputStream(out);  
            obs.writeObject(obj);  
            obs.close();  

            //分配内存,写入原始对象,生成新对象  
            ByteArrayInputStream ios = new ByteArrayInputStream(out.toByteArray());  
            ObjectInputStream ois = new ObjectInputStream(ios);  
            //返回生成的新对象  
            cloneObj = (T) ois.readObject();  
            ois.close();  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
        return cloneObj;  
    }  
}

此工具类要求被拷贝的对象必须实现Serializable接口
用此方法进行对象拷贝时需要注意两点:
(1)对象的内部属性都是可序列化的
(2)注意方法和属性的特殊修饰符
通常情况下,用transient和static修饰的变量是不能被序列化的,但是通过在序列化的类中写writeObject(ObjectOutputStream stream)和readObject(ObjectInputStream stream)方法,可以实现序列化。
有人说static的变量为什么不能序列化,因为static的变量可能被改变。
static final的常量可以被序列化。
注:采用序列化方式拷贝时还有一个更简单的办法,即使用Apache下的commons工具包中的SerializationUtils类

45. 覆写equals方法时不要识别不出自己
46. equals应该考虑null值情景
47. 在equals中使用getClass进行类型判断

总结45-47
一个完美的equals示例:

    public boolean equals(Object otherObject) {  
            if(this == otherObject) {    
                return true;  
            }  

            if(null == otherObject ) {   
                return false;  
            }  

            if(!(getClass() == otherObject.getClass())){  
                return false;  
            }  

            if( ! (otherObject instanceof Apple)) {  
                return false;  
            }  
            Apple other = (Apple) otherObject; 
            return name.equals(other.name)&& color.equals(other.color);  
         }  
    }  
48. 覆写equals方法必须覆写hashCode方法
49. 推荐覆写toString方法
50. 使用package-info类为包服务

Java中有一个特殊的类:package-info类,它是专门为本包服务的
(1)它不能随便被创建
(2)它服务的对象很特殊,它是描述和记录本包信息的。
(3)package-info类不能有实现代码
它的作用,主要表现在以下三个方面:
(1)声明友好类和包内访问常量
(2)为在包上标注注解提供便利
(3)提供包的整体注释说明

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值