Effective Java读书笔记——第八章 通用程序设计

本章主要讨论:

  • 局部变量的处理

  • 控制结构

  • 类库的用法

  • 各种数据类型的用法

  • 反射、本地方法

  • 优化、命名惯例


第45条:将局部变量的作用域最小化

将局部变量的作用域最小化,可以增强代码的可读性和可维护性,并降低出错的可能性。

要使局部变量的作用域最小化,最有力的方法就是在第一次使用它的地方声明。

过早地声明局部变量不仅会使它的作用域过早地扩展,而且结束的过于晚了——如果变量是在“使用它的块”之外被声明的,当程序退出该块之后,该变量仍是可见的。

每个局部变量都应该包含一个初始化表达式。

下面是一种遍历集合的首选做法:

for(Element e : c) {
    doSth() ...
}

或者:

for (int i = 0, n = expensiveComputation; i < n; ++i) {
    doSth() ...
}

第46条:for-each循环优于传统for循环

首先,for-each循环不会有性能损失,其次,可以减少出错的可能。

看下面的代码:

//代码会在执行第五次循环时抛出NoSuchElementException
enum Suit {CLUB, DIAMOND, HEART, SPADE}
enum Rank {ACE, DEUCE, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN, JACK, QUEEN, KING}

...

Collection<Suit> suits = Arrays.asList(Suit.values());
Collection<Rank> ranks = Arrays.asList(Rank.values());
List<Card> deck = new ArrayList<Card>();
for (Iterator<Suit> i = suits.iterator(); i.hasNext()) {
    for(Iterator<Rank> j = ranks.iterator(); j.hasNext()) {
        deck.add(new Card(i.next(),j.next()));
    }
}

而下面的代码将打印从“ONE ONE”到“SIX SIX”的6个重复的词,而不是36种组合:

enum Face {ONE, TWO, THREE, FOUR, FIVE, SIX }
...
Collection<Face> faces = Arrays.asList(Face.values());

for(Iteratoer<Face> i = faces.iterator; i.hasNext()) {
    for(Iterator<Face> j = faces.iterator;j.hasNext()) {
        System.out.println(i.next() + " " + j.next());
    }
}

下面的方式可以修复bug,但不美观:

for(Iterator<Suit> i = suits.iterator();i.hasNext();) {
    Suit suit = i.next();
    for (Iterator<>Rank) j = ranks.iterator();j.hasNext();){
        deck.add(new Card(suit,j.next()));
    }
}
//而如果用for-each循环就能很好的解决
for (Suit suit : suits)
    for(Rank rank : ranks)
        deck.add(new Card(suit,rank));

for-each循环的另一个好处是,它不仅可以遍历集合和数组,还可以遍历任何实现Iterable接口的对象:

//Iterable接口
public interface Iterable<E> {
    Iterator<E> iterator();
}

总之for-each循环的优势比for循环大很多。但是,以下三种情况没法使用for-each循环:

1、过滤——如果要在遍历过程中删除某些元素,就应当使用传统for循环,以便调用其remove方法。

2、转换——如果要在遍历过程中替换某些元素,就只能使用for循环。

3、平行迭代——需要并行遍历多个集合,需使用传统for循环。


第47条:了解和使用类库

考虑下面的方法:

//如希望产生位于0和某个上界之间的随机整数
static int random(int n) {
    return Math.abs(rnd.nextInt)) % n;
}

该方法存在几个问题:

1、若n是个较小的2的乘方,那么它产生的随机数将会重复。

2、若n不是2的乘方,那么有些数会比其他数出现的更加频繁。

3、如果nextInt()返回Integer.MIN_VALUE,那么Math.abs也返回Integer.MIN_VALUE,假设不是n不是2的乘方,那么取模操作符将返回一个负数。

产生上述问题的原因是未考虑伪随机数、数论和2的求补算法的相关知识

所以,应当使用Java API 1.2引入的方法Random.nextInt(),该方法已经把上述问题考虑进去。这就是使用标准类库的好处:产生的问题少,且不必把时间花在底层细节上,另外它们的性能会不断提高。

总结,不要重复发明轮子。


第48条:如果需要精确的答案,请避免使用float 和double

float和double类型主要为了科学计算和工程计算而设计的,它可以提供较为精确的快速而近似的计算。然而它们并没有提供完全精确的结果。所以在需要精确计算时,不应该使用float和double。而且 ,它们尤其不适用于货币计算

使用货币等精确计算应使用BigDecimal、int或者long。

public static void main(String[] args) {
    final BigDecimal TEN_CENTS = new BigDecimal(".10");
    int itemsBought = 0;
    BifDecimal funds = new BigDecimal("1.00");
    for(BigDecimal price = TEN_CENTS; funds.compareTo(price) >= 0; price = price(TEN_CENTS)) {
        itemBought++;
        funds = funds.subtract(price);
    }
    System.out.println(itemBought + " items bought.");
    System.out.println("Money left over: $" + funds);
}

然而BigDecimal有两个缺点:使用起来不便;效率低。

解决方式是使用int或是long

public static void main(String[] args) {
    int itemBought = 0;
    int funds = 100;
    for( int price = 10;funds >= price; price += 10) {
        itemBought++;
        funds -= price;
    }
    System.out,println(itemBought + " item bought.");
    System.out,println("Money left over: " + funds + " cents");
}

总而言之,对于精确的计算,请不要使用float或double。如果需要计算小数的计算,请使用BigDecimal,如果只是用整数,且数值不太大,就是用int,如果数值范围超过了9位十进制数,就是用long,如果超过了十八位,必须使用BigDecimal。

第49条:基本类型优于对应的装箱类型

Java1.5增加了自动装箱和自动拆箱功能。

基本类型和装箱类型有三个主要区别:

1、基本类型只有值,而装箱类型具有与他们值不同的同一性(同一性:是否为两个相同对象)。

2、基本类型只有功能完备的值,而装箱类型除了具备功能完备的值,还有一个非功能的值:null。

3、基本类型更节省空间和时间。

Comparator<Integer> naturalOrder = new Comparator<Integer>() {
    public int compare(Integer first,Integer second) {
        return first < second ? -1 : (first == second ? 0 : 1);

    }
};

当调用:

naturalOrder.Compare(new Integer(42), new Integer(42));

时,期望的输出时0,结果却是1。

原因是,当比较first < second的时候,编译器会先将Integer类型拆箱成基本类型,显然42 < 42为假,所以执行(first == second ? 0 : 1),而此时first和second将是对象的引用,即它们将比较的为 是不是同一个对象(这就是所谓的同一性问题),结果显然是假,所以返回1。所以,对装箱类型使用==几乎总是错误的

解决方式是加入两个局部变量,所有比较都在这两个局部变量上进行,从而避免了同一性的问题。

Comparator<Integer> naturalOrder = new Comparator<Integer>() {
    public int compare(Integer first,Integer second) {
        int f = first;
        int s = second;
        return f < s ? -1 : (f == s ? 0 : 1);
    }
}

考虑下面的程序:

public class Unbelievable {
    static Integer i;
    public static void main(String[] args) {
        if(i == 42){ 
            System.out.println("Unbelievable");

        }
    }
}

上面的程序会直接抛出NullPointerException异常,原因是i是一个Integer引用类型,而42是基本类型,当编译器遇到==操作符的两端是基本类型和装箱类型时,会先将装箱类型自动拆箱。但i还没有初始化,所以引用为null,引用为null的装箱类型自动拆箱会直接抛出NullPointerException异常。解决办法是把i的类型修改为int。

那么什么时候适合使用装箱类型呢?答案是1、当作为集合中的键或值的时候。2、当作为泛型参数的时候。ThreadLocal<Integer>2、在进行反射方法调用时必须使用装箱类型。


第50条:如果其他类型更适合,则尽量避免使用字符串

  • 字符串不适合代替其他的值类型。一些int、float、BigInteger类型不要用String类型表示;

  • 字符串不适合代替枚举类型;

  • 字符串不适合代替聚集类型:如果一个实体有多个组件,用一个字符串来表示这个实体是不恰当的:String compoundKey = className + "#" + i.next();,这会造成混乱。应当简单地编写一个类来描述这个数据集,通常是一个私有的静态成员类(见第22条)。

  • 字符串不适合代替能力表:下面以ThreadLocal类来说明。

ThreadLocal是线程局部变量,可以把它理解成一个Map,该对象在每个线程中都维护着自己的变量,通过“Key”就可以获得相应线程的值。吐过把Key设计成String类型的,就会造成如果有两个线程给“key”取得名字是一样的,那么这个key就变成了共享变量,这两个线程在通过key读取值的时候,会发生错误。

改进:

public final class ThreadLocal<T> {
    public ThreadLocal(){}
    public void set(T value);
    public T get();
}

总之,如果可以使用更加合适的数据类型,或者编写更加适当的数据类型,就应当避免用字符串来表示对象。

第51条:当心字符串链接的性能

字符串连接符(+)可以把多个字符串合并为一个字符串,这是个便利的方式。但它不是和连接多个字符串,因为String是不可变类,两个字符串连接时,都要被拷贝。

//不好的方式
public String statement() {
    String result = "";
    for (int i = 0;i < numItem(); ++i) {
        result += lineForItem(i); 
    }
    return result;
}

为了改善性能,应使用StringBuilder代替String,前者是非线程安全的:
(StringBuffer已经过时,它是线程安全的)

public String statement() {
    StringBuilder b = new StringBuilder(numItems() * LINE_WIDTH);
    for (int i = 0;i < numItems(); ++i) {
        b.append(lineForItem(i));
    }
    return b.toString();
}

第52条:通过接口引用对象

如果有合适的类型接口存在,那么对于参数、返回值、变量和域来说,就都应该使用接口类型进行声明。

考虑Vector,它是List接口的一个实现:

//应该这样声明,使用接口当做类型
List<Subscriber> subscriber = new Vector<Subscriber>();
//不应该这样声明
Vector<Subscriber> subscribers = new Vector<Subscriber>(); 

使用接口作为类型的好处是使得程序更加灵活,当决定改变实现时,只需要改变构造器的名称即可:

//将上面的第一句的Vector时间改为ArrayList
List<Subscriber> subscribers = new ArrayList<Subscriber>();

第53条:接口优先于反射机制

反射机制(java.lang.reflect)提供了“通过程序来访问关于已装载的类的信息”的能力。给定一个Class实例,亦可以获得Constructor、Method、Field实例,分别表示Class实例对应的类的Constructor、Method、Field。这些对象提供了“通过程序来访问类的成员名称、域类型、方法签名等信息”的能力。

例如,Method.invoke可以调用任何类的任何对象的任何方法。反射机制允许一个类使用另一个类。即使当前者按编译的时候后者还根本不存在。然而,反射也有缺点:

  • 丧失了编译时类型检查的好处:程序企图用反射调用不存在的或者不可访问的方法,在运行时它将会失败。

  • 执行反射访问所需的代码非常笨拙;

  • 性能损失:反射方法的调用比普通方法慢了许多。

所以反射机制应该只在设计时被用到。通常,普通应用程序在运行时不应该以反射的方式访问对象。

第54条:谨慎地使用本地方法

JNI(Java Native Interface)允许Java应用程序调用本地方法,所谓本地方法,就是本地应用程序需设计语言(如C或C++)来编写的特殊方法本地方法在本地语言中可以执行任意的计算任务,并返回Java程序设计语言。

本地方法主要有三个用途:

  • 本地方法提供了“访问特定于平台的机制”的能力,如访问注册表和文件锁。

  • 本地方法提供了访问遗留代码库的能力,从从而可以访问遗留数据。

  • 本地方法可以通过本地语言,编写应用程序中注重性能部分,以提高系统的性能。

然而,使用本地方法来提高性能的做法不值得提倡。因为如今的JVM越来越快了,对于大多数任务,现在即便不适用本地方法也可以获得相当的性能。

总之,使用本地方法之前务必三思,极少数情况下会需要使用本地方法来提高性能。


第55条:谨慎进行优化

路。。。

第56条:请遵守普遍接受的命名惯例

略。。。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值