第9章:通用编程

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

  1. 将局部变量作用域最小化,可以增强代码可读性和可维护性,并降低出错的可能性
57.1 具体做法
  1. 在第一次使用它的地方进行声明
    1. 过早声明变量会导致代码难以读懂,等使用变量时,读者可能已经记不起该变量的类型和初始值了
    2. 过早声明局部变量还会导致扩大整个作用域,如果变量在"应该使用它的块"外声明,会导致这个变量在这个目标区域之前或之后使用,后果将是灾难的
  2. 每一个局部变量声明都应该包含一个初始化表达式
    1. 如果没有足够信息对变量进行有意义的初始化,应该推迟声明的位置,直到可以初始化
    2. 此规则有例外情况,就是如果一个变量被try块内的代码初始化(因为初始化的过程可能产生受检异常),且这个变量需要在try块外使用,那么这个变量就必须在try块前声明,但很明显,在try块之前,该变量并不能被有意义地初始化
  3. for循环和for-each循环允许声明循环变量,它们的作用域被限制在了循环体+for()内,保证了变量的作用域最小化,因此如果在循环终止后,不再需要循环变量的内容,且代码需要更强的可读性时,for循环优先于while循环
//1. 遍历集合的首选做法
for (Element e : c) {
... // Do Something with e
}

//2. 如果需要访问迭代器,调用其remove方法时的做法,for循环代替for-each循环
for (Iterator<Element> i = c.iterator(); i.hasNext(); ) {
	Element e = i.next();
... // Do something with e and i
}

//3. 使用while循环的问题,在于下面这种情况,由于第二段代码是复制的第一段代码,如果忘记修改while中的i成i2,也能正常编译,但结果却有问题
//这个问题本质上讲,是因为while导致循环变量作用域扩大而产生的
Iterator<Element> i = c.iterator();
while (i.hasNext()) {
	doSomething(i.next());
}
...
Iterator<Element> i2 = c2.iterator();
while (i.hasNext()) { // BUG!
	doSomethingElse(i2.next());
}

//4. 使用for循环改造后,第二段代码,使用了i.hasNext,根本无法通过编译,因为在下面for循环中,根本无法读取到i这个变量
for (Iterator<Element> i = c.iterator(); i.hasNext(); ) {
	Element e = i.next();
	... // Do something with e and i
}
...
for (Iterator<Element> i2 = c2.iterator(); i.hasNext(); ) {
	Element e2 = i2.next();
	... // Do something with e2 and i2
}
  1. for循环的最佳实践
//如果将n放在for循环之上,导致n作用域变大
//如果将n放在for循环内,每次都需要重新计算一次n的值,浪费系统性能
//将n直接放在for的括号内,其作用域只在for的括号内+循环体内,且只执行一次
//适用于循环内涉及方法调用,且n对于每次循环,该方法调用返回值固定不变的情况
for (int i = 0, n = expensiveComputation(); i < n; i++) {
... // Do something with i;
}
  1. 使方法小而集中
    1. 如果将两个操作合并到一个方法中,那么第二个操作就可以读到第一个操作的局部变量,可能会产生问题
    2. 只需将这一个方法分开成为两个,每个操作用一个方法完成,就能保证将局部变量的作用域最小化

第58条 for-each循环优先于传统的for循环

58.1 传统for循环的缺点
  1. 容易写错,难以不改动客户端前提下,修改容器类型
    1. 对于第一个方法,表示iterator对象的i出现了3次,第二个方法表示元素索引的i出现4次,很容易出现写错变量的情况,如果写错了很有可能编译器还无法发现
    2. 两个循环完全不同,容器的类型的不同,转移了注意力,并且一旦容器类型变化,客户端代码难以修改,需要将整个循环方式都改动
//循环集合
for (Iterator<Element> i = c.iterator(); i.hasNext(); ) {
Element e = i.next();
... // Do something with e
}
//循环数组
for (int i = 0; i < a.length; i++) {
... // Do something with a[i]
}
  1. 平行迭代问题
//这段代码会造成平行迭代,不是我们想象的先由suits提供一个固定值,然后由这个固定值和ranks中所有元素进行组合
//由于每次都使用了i的next方法和j的next方法,导致每次都从suits和ranks中分别取一个新值出来,组成一个Card
//当Suit用尽,循环就会抛出NoSuchElementException
enum Suit { CLUB, DIAMOND, HEART, SPADE }
enum Rank { ACE, DEUCE, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT,
NINE, TEN, JACK, QUEEN, KING }
...
static Collection<Suit> suits = Arrays.asList(Suit.values());
static Collection<Rank> ranks = Arrays.asList(Rank.values());
List<Card> deck = new ArrayList<>();
for (Iterator<Suit> i = suits.iterator(); i.hasNext(); )
	for (Iterator<Rank> j = ranks.iterator(); j.hasNext(); )
		deck.add(new Card(i.next(), j.next()));
  1. 平行迭代时,如果外部集合大小为内部集合大小的整数倍(比如外部集合和内部集合是同一个,那么就是1倍的情况),问题更严重,循环会正常结束,但返回的结果和你预期的不同
//1. 该方法会正常结束,但会打印ONE ONE,TWO TWO,一直到SIX SIX
//2. 当外部集合是内部集合的整数倍时,比如外部集合30个元素,内部集合5个元素,那么会外部集合每五个元素陪内部集合的五个元素进行一次循环
enum Face { ONE, TWO, THREE, FOUR, FIVE, SIX }
...
Collection<Face> faces = EnumSet.allOf(Face.class);
for (Iterator<Face> i = faces.iterator(); i.hasNext(); )
	for (Iterator<Face> j = faces.iterator(); j.hasNext(); )
		System.out.println(i.next() + " " + j.next());
  1. 传统for循环中修正平行迭代
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()));
}
  1. for-each修正平行迭代
for (Suit suit : suits)
	for (Rank rank : ranks)
		deck.add(new Card(suit, rank));
58.2 无法使用for-each循环的情况
  1. 解构过滤(破坏性过滤,所谓破坏可能就是指会破坏原有的集合、解除结构):需要使用显示迭代器的remove方法删除集合中元素。但这种情况在Java8中可以通过使用Collection接口新提供的removeIf(Predicate filter)方法完成同样的功能,从而避免使用传统for循环
  2. 转换:需要遍历集合或数组,并且修改其中的某些值,需要list的iterator或数组的索引,来完成这个工作
  3. 平行迭代
58.3 for-each循环可以处理的元素类型
  1. 可以使用for-each来遍历集合和数组
  2. 也可以遍历实现Iterable接口的对象
  3. 因此当你正在写一个表示一组元素的类,且需要遍历其内所有元素时,让他实现Iterable接口比实现Collection接口更重要,这样就可以对这个类的对象,使用for-each进行遍历
58.4 最佳实践
  1. for-each更简洁、灵活、不容易出错,且没有性能问题
  2. 除了58.2中说明的几种情况,都应该使用for-each替代传统for循环

59 了解并使用类库

59.1 使用标准类库的优点
59.1.1 通过使用标准类库,可以充分利用编写标准类库的专家的知识,以及在你之前其他人的使用经验
  1. 如果你只了解Random的nextInt()这一个方法,那么当你想设计一个方法(random),可以返回0到传入的int类型值(n)之间的随机整数,可能会编写如下方法
static Random rnd = new Random();

static int random(int n) {
    return Math.abs(rnd.nextInt()) % n;
}
  1. 如果n为一个比较小的2的乘方,那么随机数就是那么几个,多次取随机数结果会重复,虽然这可能算不上一个毛病
  2. 如果n不是2的整数次方,那么该方法结果中某些数会比其他数出现的更频繁,尤其n越大,这个问题越严重
//该结果产生的数字,有2/3落在0到n的前半部分
int n = 2 * (Integer.MAX_VALUE / 3);
int low = 0;
for (int i = 0; i < 1000000; i++)
    if (random(n) < n / 2)
        low++;
System.out.println(low);
  1. 由于Math.abs正常应该返回传入的int型数的绝对值,但该方法有一个问题,如果这个方法其内传入的值如果恰好为Integer.MIN_VALUE,而Integer.MIN_VALUE的绝对值已经超过了int的范围,所以该方法无法返回Integer.MIN_VALUE的正整数,而是会返回Integer.MIN_VALUE这个负数本身,此时如果n不是2的整数次方,无法整除,取模操作会返回一个负数
  2. 如果自己想编写一个修正这三个缺点的random方法,非常复杂,API中实际上已经提供了Random.nextInt(int)方法,完成该功能
  3. Java7开始后,选择随机数生成器时,大多使用ThreadLocalRandom,在作者机器上,是Random效率的3.6倍。对于Fork Join Pool和并行Stream,使用SplittableRandom
59.1.2 不必浪费时间为那些与工作不太相关的问题提供特别的解决方案

应该把时间花费在应用程序上,而不是底层的细节上

59.1.3 标准类库提供的方法,会随时间推移,性能不断提高

提供这些标准类库的组织,会不断完善这些方法的实现细节,从而提升性能,比如当jdk推出新版本,你去使用,相当于就是用了新的实现,而你的客户端不需要变动,性能就会提升

59.1.4 会随着时间的推移增加新的功能

类库中如果漏掉了某些功能,开发者社区会把这些缺点公示出来,漏掉的功能就会添加到后续的发行版本中

59.1.5 可以使自己编写的代码融入主流,可读性、可维护性、可重用性更强
59.2 应如何学习标准类库
  1. 每个重要发行版本,都会有很多新的特性被加入到类库,应该同步地了解这些新特性
  2. 可以通过阅读新特性的说明网页(Java8-feat、Java9-feat),来了解这些新特性
  3. 标准类库太庞大,无法学完所有的文档,但一般程序员应该熟悉java.lang、java.util、java.io及其子包下的所有内容,其他类库的知识可以根据需要随时学习
  4. 尤其是Collections Framework、Stream类库、java.util.concurrent包,这些内容尤其重要
  5. 如果标准类库中没有功能满足你的需求,也不要着急自己实现,而是应该考虑使用比较高级的第三方类库,比如Google的Guava类库
59.3 最佳实践
  1. 不要重复发明轮子(reinvent the wheel)
    1. 所谓发明轮子,而不是制造轮子,因为轮子形状应该就是圆的,已经公认没有更好的形状替代它,尝试使用发明新的形状的轮子,就叫发明轮子
    2. 而制造轮子是被赞成的,制造轮子是指制造不同种类的轮子,比如雪地轮胎、越野轮胎
  2. 实现功能时,优先使用标准类库,如果不存在是否有满足功能的类,就去查一查
  3. 标准类库中代码比起自己写的代码质量更高,并且可以随着时间推移,不断改进

60 如果需要精确的答案,避免使用float和double

60.1 不同方式的计算
  1. float和double不适用于货币计算,因为他们无法准确的表示0.1
//打印0.6100000000000001,而不是0.61
System.out.println(1.03-0.42);
//打印0.09999999999999998,而不是0.1
System.out.println(1.00 - 9 * 0.10);
public static void main(String[] args) {
    double funds = 1.00;
    int itemsBought = 0;
    for (double price = 0.10; funds >= price; price += 0.10) {
        funds -= price;
        itemsBought++;
    }
    System.out.println(itemsBought + " items bought.");
    System.out.println("Change: $" + funds);
}
  1. 使用BigDecimal:BigDecimal与基本类型运算相比,编程复杂,且效率变低
public static void main(String[] args) {
    //使用BigDecimal的String构造器,而不是double构造器,避免不正确的值引入到计算
    final BigDecimal TEN_CENTS = new BigDecimal(".10");
    int itemsBought = 0;
    BigDecimal funds = new BigDecimal("1.00");
    for (BigDecimal price = TEN_CENTS;
         funds.compareTo(price) >= 0;
         price = price.add(TEN_CENTS)) {
        funds = funds.subtract(price);
        itemsBought++;
    }
    System.out.println(itemsBought + " items bought.");
    System.out.println("Money left over: $" + funds);
}
  1. 使用int、long进行货币的计算:需要自己处理十进制小数点,比如本例中,不再使用元作为单位计算,而是使用分为单位
public static void main(String[] args) {
    final BigDecimal TEN_CENTS = new BigDecimal(".10");
    int itemsBought = 0;
    BigDecimal funds = new BigDecimal("1.00");
    for (BigDecimal price = TEN_CENTS;
         funds.compareTo(price) >= 0;
         price = price.add(TEN_CENTS)) {
        funds = funds.subtract(price);
        itemsBought++;
    }
    System.out.println(itemsBought + " items bought.");
    System.out.println("Money left over: $" + funds);
}
60.2 最佳实践
  1. 对于需要精确答案的计算任务,不要使用float和double
  2. 如果想让系统处理十进制小数点,且不介意因为不使用基本类型而带来的不变,就可以使用BigDecimal,而且BigDecimal API提供不同的舍入方式,方便使用
  3. 如果不介意自己处理十进制小数点,且涉及的数值不太大,可以使用int和long,long最多18位,如果数值超过18位,就必须使用BigDecimal

61 基本类型优于装箱的基本类型

61.1 基本类型与装箱基本类型区别

基本类型:primitive
装箱基本类型:boxed primitive

  1. 两个基本类型值相等,就完全相同,而两个装箱基本类型,可能值相等,但对象本身不相等,因此对于装箱基本类型使用==判断是否相等,基本都是有问题的
//false
System.out.println(new Float(123)==new Float(123));
//true
System.out.println(123f==123f);
  1. 装箱基本类型可能为null,而基本类型不能
  2. 基本类型比装箱基本类型更节省时间和空间
61.2 装箱基本类型引发的问题
  1. 问题一:装箱基本类型间,不应使用==比较相等
Comparator<Integer> naturalOrder =
        (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);
int value = naturalOrder.compare(new Integer(1),new Integer(1));
//值为1,因为虽然两个装箱基本类型值相同,但==是判断对象地址,是不同的
System.out.println(value);
  1. 如果只想实现自然排序,可以使用Comparator.naturalOrder()来构造Comparator对象,这个由Comparator提供的方法,已经实现了自然排序
  2. 如果想自己实现该功能
//方案一
Comparator<Integer> naturalOrder = (iBoxed, jBoxed) -> {
    int i = iBoxed, j = jBoxed; // Auto-unboxing
    return i < j ? -1 : (i == j ? 0 : 1);
};	
//方案二
Comparator<Integer> naturalOrder = (iBoxed, jBoxed) -> {
    return Integer.compare(iBoxed,jBoxed);
};
  1. 问题二:装箱基本类型与基本类型比较时,会自动拆箱
public class Unbelievable {
    static Integer i;
    public static void main(String[] args) {
        //会抛出NullPointerException,因为i为Integer,Integer与int比较时,会自动拆箱
        //自动拆箱相当于调用了Integer的intValue()方法,由于Integer对象为null,因此抛出异常
        if (i == 42)
            System.out.println("Unbelievable");
    }
}
  1. 问题三:循环中使用装箱基本类型,引发性能问题
//sum为Long型,循环中反复的装箱与拆箱,引发性能问题
public static void main(String[] args) {
    Long sum = 0L;
    for (long i = 0; i < Integer.MAX_VALUE; i++) {
        sum += i;
    }
    System.out.println(sum);
}
61.3 使用装箱基本类型的场景
  1. 对象需要作为集合中的元素、键、值的情况
    1. 因为无法将基本类型放在集合中
  2. 无法用基本类型作为泛型
    1. 例如ThreadLocal<int>无法编译通过,需要写为ThreadLocal<Integer>
  3. 利用反射调用对象的方法时,无法反射生成int类型的对象,只能生成Integer的
61.4 最佳实践
  1. 基本类型优先于装箱基本类型
  2. 自动装箱功能降低了使用装箱基本类型的繁琐性,但提升了使用它的风险
  3. 不要使用==判断两个装箱基本类型
  4. 混用基本类型和装箱基本类型时,会涉及到装箱基本类型的拆箱操作,可能会抛出NullPointerException
  5. 使用装箱基本类型会导致较高的资源消耗,和不必要的对象创建

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

62.1 不适合用字符串的场景
  1. 字符串不适合代替其他的值类型
    1. 当一段数据从文件、网络、键盘设备,进入程序后,它通常以字符串的形式存在
    2. 因此很多人认为应该让这段数据以String存在,但如果这段数据本质上并不是文本信息时,应该以其适当的值类型对其进行存放
    3. 如果是数值,应转为int、float、BigInteger,如果是yes-or-no问题的答案,应以boolean存放,如果没有适合的类型表示这段数据,就应编写一个新类型表示
  2. 字符串不适合替代枚举类型(item 34)
  3. 字符串不适合替代聚合类型
    1. 如果用来表示分隔的字符#,如果需要表示属性的值,例如className叫做Test#Wu,那么会出现混乱,不知道到底哪段字符串对应哪个属性,例如是Test是类名,还是Test#Wu是类名
    2. 如果想访问单独某一个属性,必须解析整个compoundKey字符串,非常繁琐和易错
    3. 没法为compoundKey属性提供符合逻辑的equals、toString、compareTo方法,只能使用String类默认的
    4. 更好的做法是简单的写一个类描述这个数据集,通常是一个静态成员类(item 24)
//compoundKey就是一个聚合类型,因为它用一个属性表示了两个不同的属性,className与i.next(),不同属性间使用#分隔
String compoundKey = className + "#" + i.next();

//修改方案
private static class CompoundKey{
	String className;
	String iNext;
}
  1. 字符串不适合替代capabilities
    1. capabilities(能力的复数):这里指根据不同String对象来表示具有不同的能力的做法,并不好
    2. 例如自定义ThreadLocal,保证不同线程,使用同一个ThreadLocal对象(或类,也就是static的get方法)的get方法时,可以获取不同的值。使用方案如果是,对于每个客户端持有不同的String,根据这个String来获取客户端独有的值,就叫作用String当做能力表
    3. Java1.2之前,还没有ThreadLocal,需要自定义
//使用String当做能力表,ThreadLocal的get方法,对于同一个字符串,可以返回唯一值,此时只要让不同线程持有不同的字符串,就能实现不同线程通过ThreadLocal获取不同变量值的功能
//如果不同线程,决定使用同一个字符串,就会导致第二个线程获取到第一个线程中的thread-local variable,可能会有恶意的客户端伪造键,从而访问其他线程中的那个thread-local variable
public class ThreadLocal {
	private ThreadLocal() { }
	public static void set(String key, Object value);
	public static Object get(String key);
}
//改造一:用一个不可伪造的键来替代字符串
public class ThreadLocal {
    private ThreadLocal() { } // Noninstantiable
    public static class Key { // (Capability)
        Key() { }
    }
    // Generates a unique, unforgeable key
    public static Key getKey() {
        return new Key();
    }
    public static void set(Key key, Object value);
    public static Object get(Key key);
}
//改造二:可以去掉静态方法,使用key的实例方法替代这个构造方法,此时key不再是key,而是完全可以替代ThreadLocal的功能
public class ThreadLocal {
    private ThreadLocal() { } // Noninstantiable
    public static class Key { // (Capability)
        Key() { }
        //可以使用如下两个Key类中的实例方法,替代原有的两个静态方法,由于实例方法本身一定持有当前Key的引用
        //在方法内使用this关键字就可以获取Key对象,因此Key不再需要作为参数传入
        public void set(Object value);
        public Object get();
    }
    // Generates a unique, unforgeable key
    public static Key getKey() {
        return new Key();
    }
}
//改造三:外层ThreadLocal已经没有任何作用,因为既没有对外的构造器对其初始化,又不提供具体方法,所以可以去掉,同时原来的Key类,完全具备了ThreadLocal的功能,所以将原有Key类改名为ThreadLocal
//此处使用final修饰,可能是为了防止通过继承修改其功能
public final class ThreadLocal {
    public ThreadLocal();
    //可以使用如下两个Key类中的实例方法,替代原有的两个静态方法,由于实例方法本身一定持有当前Key的引用
    //在方法内使用this关键字就可以获取Key对象,因此Key不再需要作为参数传入
    public void set(Object value);
    public Object get();
}
//改造四:改造三种的方法类型不安全,因为使用get方法的客户端,只能获取到一个Object对象,必须从Object转换为其真正值,转换过程中可能转错(转错时,编译通过,执行报错),所以叫做类型不安全,通过将ThreadLocal泛型化,来解决类型不安全问题
//1. 一定要注意,对于之前String和Key的例子,是没办法做到通过将ThreadLocal泛型化,来解决类型不安全问题的,因为类上的泛型信息,没法用在static方法内,比如如下写法编译是无法通过的
//public class ThreadLocal<T>  {
//    编译报错,找不到T
//    public static T get(String key){
//        return null;
//    }
//}
//2. 这就是简略版的java提供的ThreadLocal的API,不同的线程持有不同的Key(已改为ThreadLocal),从而获取不同的值
//此处省略了具体的set、get实现,同时也省略了为每个线程,持有不同的Key的做法
public final class ThreadLocal<T> {
	public ThreadLocal();
	public void set(T value);
	public T get();
}
62.2 最佳实践
  1. 当可以使用更加适合的数据类型,或可以编写恰当的数据类型时,就不用String对象表示该数据
  2. 如果使用不当,字符串会比其他类型更笨拙、更不灵活、速度更慢、更容易出错
  3. 基本类型、枚举类型、聚合类型一般都不应该使用String表示

63 了解字符串连接的性能

63.1 String的+的性能问题
  1. 字符串连接符(+)不适用于大规模字符串连接,使用字符串连接符连接n个字符串,时间复杂度为n的平方
  2. 这是由于字符串的不可变性导致的,两个字符串连接在一起时,它们的内容都需要拷贝一份
//使用字符串连接符
public String statement() {
	String result = "";
	for (int i = 0; i < numItems(); i++)
	result += lineForItem(i); // String concatenation
	return result;
}
//使用StringBuilder替代String,同时用其append方法进行字符序列的连接
//当进行100次(numItems)连接,每次连接的字符串长度为80(lineForItem(i)),在作者机器上,StringBuilder方法比String的+,要快6.5倍
public String statement() {
	//此处为StringBuilder预先分配了长度,这个长度足以容纳最后的结果,所以连接过程中,StringBuilder不需要自动扩展,性能更快,即使不预先分配长度,StringBuilder仍是String效率的5.5倍
	StringBuilder b = new StringBuilder(numItems() * LINE_WIDTH);
	for (int i = 0; i < numItems(); i++)
	b.append(lineForItem(i));
	return b.toString();
}
63.2 最佳实践
  1. 应该使用StringBuilder的append替代String的+,对多个字符串进行连接,除非不在乎性能
  2. 也可以使用字符数组来提升性能,甚至就不将String进行连接,而是每次处理一个String

64 通过接口引用对象

64.1 接口引用对象的好处
  1. item 54中说明了应该使用接口,而不是具体的类,来作为参数的类型,实际上无论是参数、返回值、变量、成员,只要有合适的接口类型存在,都应使用接口类型进行声明,而不使用具体类型
  2. 只有在构造方法中,才使用具体类型,来返回创建的对象
//正确做法
Set<Son> sonSet = new LinkedHashSet<>();
//错误做法
LinkedHashSet<Son> sonSet = new LinkedHashSet<>();
  1. 用接口作为类型,使程序更加灵活,具体实现修改,不影响其他客户端代码
//当决定更换实现时,只需修改调用的构造器(如果用的是静态工厂方法,那就直接换一个返回新的类型的工厂方法就行)
Set<Son> sonSet = new HashSet<>();
  1. 需要注意更换实现时,如果原来的实现提供了某种特殊的功能(不是提供了新的方法,而是功能),那么用于替换的新的实现,也必须提供这个功能
    1. 例如如果原来使用的是LinkedHashSet,同时客户端又依赖于它的排序功能,才能正确的执行,那么就不用用一个HashSet替换它
  2. 改变具体实现带来的好处:新实现拥有更好的性能和新的功能
    1. 例如使用EnumMap替换HashMap,性能提升,且保证其内元素有序。但注意,只有key是Enum类型时,才能用EnumMap
    2. 又比如LinkedHashMap替代HashMap,可以提供可预见的迭代顺序,同时也没对key值做出什么特殊要求(不像EnumMap)
  3. 使用具体类型声明变量的缺点
    1. 虽然可以同时修改声明的类型和具体实现类型,从而改为新的实现
    2. 但如果新类型不具有原类型中的某些方法,会导致编译报错
64.2 不必使用接口替代具体类型的情况
  1. 没有合适的接口存在:比如值类(value class),值类指的是仅仅用于表示值的类,例如String、BigInteger,值类很少用多个实现进行编写,而且是final的,所以值类可以直接用于引用对象(引用对象就是指定义一个值类的引用,指向值类的对象)
  2. 对象属于一个框架,而框架的基本类型时类,这种框架叫做基于类的框架(class-based framework),就应该使用相关的基类(base class 往往是抽象类),来引用这个对象,也不是使用具体实现。比如OutputStream就属于这种情形
  3. 具体的实现类提供了接口额外的方法,而且应用程序依赖于这个额外的方法。例如PriorityQueue相比Queue提供了一个compare方法,如果客户端还用了这个方法,那么就不应该用Queue来引用具体实现

65 接口优先于反射机制

65.1 反射的功能
  1. 核心的反射工具,java.lang.reflect,提供了编程访问任意类的能力
  2. 反射机制允许一个类使用另一个编译时还不存在的类
65.2 反射的问题
  1. 损失了编译时,类型检查、异常检查的优势
  2. 执行反射访问所需要的代码笨拙且冗长,可读性差
  3. 性能损失:在作者机器上,使用反射调用一个以int作为返回值,无参的方法,比普通的方法调用慢11倍
65.3 需要使用反射的情况
  1. 如果需要用到的类在编译时并不存在,就可以用反射的方式创建其实例,然后通过它们的接口或超类,以正常的方式访问这些实例
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;
import java.util.Set;
//1. 本例中,产生了6个运行时异常,需要在代码中try catch人工处理,如果不使用反射,这些异常实际上都是编译时异常(编译能通过,运行时就不会有问题,也就不用人为try catch了)
//2. 为了创建出Set实例,花费了大量的代码
//3. 程序的长度可以通过直接捕捉Java7引入的ReflectiveOperationException异常来减少,因为它是所有反射产生的异常的父类
//4. 限定只有用到的类在编译时并不存在,才使用反射,除了实例化Set对象的代码受到反射影响,其他代码都和原来一样
public class TestNew {
    public static void main(String[] args) {
        Class<? extends Set<String>> cl = null;
        try {
            //5. 此处会产生一个unchecked cast警告
            cl = (Class<? extends Set<String>>)
                    Class.forName(args[0]);
        } catch (ClassNotFoundException e) {
            fatalError("Class not found.");
        }
// Get the constructor
        Constructor<? extends Set<String>> cons = null;
        try {
            cons = cl.getDeclaredConstructor();
        } catch (NoSuchMethodException e) {
            fatalError("No parameterless constructor");
        }
// Instantiate the set
        Set<String> s = null;
        try {
            s = cons.newInstance();
        } catch (IllegalAccessException e) {
            fatalError("Constructor not accessible");
        } catch (InstantiationException e) {
            fatalError("Class not instantiable.");
        } catch (InvocationTargetException e) {
            fatalError("Constructor threw " + e.getCause());
        } catch (ClassCastException e) {
            fatalError("Class doesn't implement Set");
        }
// Exercise the set
        s.addAll(Arrays.asList(args).subList(1, args.length));
        System.out.println(s);
    }

    private static void fatalError(String msg) {
        System.err.println(msg);
        System.exit(1);
    }
}

65.4 最佳实践
  1. 如果要使用编译时不存在的类,就应该使用反射创建类对象,再通过这个类的、编译时存在的父类或接口,来访问(引用)这个对象
//通过反射,newInstance来创建对象,但后续使用已存在的Set接口,来引用这个对象
Set<String> s = cons.newInstance();

66 谨慎地使用本地方法

66.1 本地方法的缺陷
  1. 本地方法不安全,所以使用本机方法的应用程序不再对内存损坏错误免疫
  2. 本地方法与平台相关,所以使用本地方法的应用程序,可移植性会变差
  3. 本地方法很难调试(debug)
  4. 使用本地方法时,如果不够小心,还会降低系统性能,因为本地方法的垃圾收集不是自动的,甚至无法追踪本地内存的使用情况,而且在进入和退出本地代码时,需要额外的开销
  5. 本地方法需要"glue code"(将代码拼在一起),写起来很乏味,读起来也困难
66.2 最佳实践
  1. 使用本地代码时要三思,很少有必须使用本地方法来提升性能的地方
  2. 如果必须使用本地方法访问底层资源、当地库(native libraries),也要尽可能缩小使用本地方法的代码,并且进行全面的测试
  3. 本地代码中只要有一个bug,就能破坏整个应用程序

67 谨慎地进行优化

67.1 性能相关问题
  1. 要努力编写好的程序,而不是快的
  2. 必须在设计过程中,就考虑到性能问题
    1. 使公有的类可变,导致大量不必要的保护性拷贝
    2. 不使用组合而使用继承,会把子类和超类绑定,限制了子类的性能
    3. 在API中使用实现类型,而不是接口类型,将客户端与具体实现绑定,将来出现更快的实现也无法使用
  3. 要努力避免那些限制性能的设计
  4. 不要为了提升性能而对API进行包装,因为在平台未来发行版本中,或在将来底层软件中不复存在,但被包装的API以及由它引起的问题,会永远困扰着你
  5. 尽量不要优化
  6. 每次试图做优化的前后,要对性能进行测量,因为性能可能变得更差了,因为很难猜出系统把性能花费在哪些地方
  7. 性能剖析工具有助于决定将优化的重心放在哪,通常可以使用JMH来定位性能问题位置
  8. Java没有很强的性能模型,即各种操作基本开销没有明确定义,因此很难可靠地预测优化后的性能,大量关于性能的说法最终都被证明为半真半假
  9. 不同JVM实现不同,性能优化时,还要考虑不同JVM、不同硬件平台上的性能间的平衡
67.2 最佳实践
  1. 不要费力编写快的程序,应该努力编写好的程序,速度会随之而来
  2. 设计系统时,尤其设计API、交互协议、永久数据格式时,一定要考虑性能问题
  3. 构建完整个系统后,应测量其性能,如果不够快,使用性能剖析器来定位性能问题所在,然后只对指定部分进行优化
    1. 先检查使用的算法,再多的底层优化,也无法弥补算法的选择不当
    2. 每次优化后,重新测量性能,直到满意为止

68 遵守普遍接受的命名规范

很多命名规范写在了The Java Language Specification中,可以大致将命名规范分为两类,字面规范和语法规范

68.1 字面规范
  1. API如果违反字面规范,会导致使用困难。具体实现违反字面规范,会导致难以维护
  2. 包和模块的名称
    1. 层次状,用句号分隔每个部分
    2. 每个部分都应该由小写字母组成,很少情况下可能还会有数字夹杂其中
    3. 如果组织编写的包,最后要在组织外使用,那么包名的开头应该是组织的Internet域名,并且顶级域名应该放在前面,例如edu.com、com.google、org.eff
    4. 标准类库一般不遵守上面的这个规定,一般以java或javax开头
    5. 用户自己创建包时不应该使用java和javax作为包的开头
    6. 包的其余部分的命名,一般由一个或多个组件组成,它们能够描述这个包,每一组件一般是一个不超过8个字母的单词或缩写
      1. 一般鼓励使用有意义的缩写,比如util,而不是utilities
      2. 或者取首字母的缩写,例如awt
    7. 大部分包只需要域名+一个组件来命名就可以了,如果包的功能非常大,大到可能需要将功能按包名分层,那么就需要多个组件一同命名
    8. 例如javax.util.concurrent.atomic是javax.util的子包
  3. 类、接口、枚举、注解的名称
    1. 由一个或多个单词组成,每个单词首字母大写
    2. 尽量避免缩写,除非单词本身是首字母缩略词(例如AIDS就表示艾滋),或者通用缩写(MIN、MAX)
    3. 对于使用首字母缩略词时,到底是首字母缩写,还是全部缩写,一直有争议,不过只首字母缩写,可以在连续出现多个首字母缩略词时,方便地分辨出一个单词的起始处与结束处,比如HTTPURL和HttpUrl,明显后者可读性更强,马上能知道是Http+Url
  4. 方法和属性的命名
    1. 与类的命名规范相同,只不过第一个字母应小写
    2. 如果首字母缩略词作为方法或属性名中的第一个词,那么这个首字母缩略词,整体都应该小写
    3. “常量域"应该包含一个或多个大写单词,中间由”_"隔开,例如VALUES、NEGATIVE_INFINITY
  5. 局部变量
    1. 和方法、属性相似,也允许缩写,具体如何命名和上下文有关,例如i、denom、houseNum
  6. 类型参数
    1. 一般由一个字母组成
    2. T:表示任意类型
    3. E:表示集合中的一个元素
    4. K和V:表示map中的key和value
    5. X:表示异常
    6. R:表示方法的返回值
    7. 表示几个连续的类型参数,可以用T, U, V或T1, T2, T3
68.2 语法规范
  1. 包:无
  2. 可被实例化的类、枚举类
    1. 名词或名词短语命名,例如Thread、PriorityQueue、ChessPiece
  3. 不可实例化的工具类
    1. 使用复数名词命名,例如Collectors、Collections
  4. 接口
    1. 名词或名词短语
    2. 或以able、ible结尾的形容词,例如Runnable、Iterable、Accessible
  5. 注解
    1. 由于注解的用处特别多,没要求必须使用哪种词性命名
  6. 执行某个动作的方法
    1. 动词或动词短语,例如append、drawImage
  7. 返回boolean的方法
    1. is开头,少数has开头+名词/名词短语/具有形容词功能的单词或短语,例如isDigit,、isProbablePrime、isEmpty,、isEnabled、hasSiblings
  8. 返回非boolean或调用者某个属性的方法
    1. 名词/名词短语,例如size、hashCode
    2. get开头的动词短语,例如getTime
  9. 转换对象类型到另一个类型的方法
    1. toType,例如toString、toArray
  10. 转换对象的类型成一个视图类型的方法
    1. asType,例如asList
  11. 返回包装类型对应的java基本类型的方法
    1. typeValue,例如intValue
  12. 静态工厂方法
    1. item1中已经介绍
  13. 属性名
    1. 没有很完善的语法规范,因为设计良好的类,很少会将属性暴露出来
    2. boolean类型的属性的命名和返回值为boolean的方法命名相似,但省略了开头的is,例如initialized、composite
    3. 其它类型的属性,一般用名词或名词短语命名,例如height、digits、bodyStyle
    4. 局部变量与属性语法规范相似,但更弱
68.3 最佳实践
  1. 应遵守命名规范
  2. 但如果长期以来养成的习惯用法不符合命名规范,也不要盲目尊崇这些命名规范,还是使用大家公认的做法即可
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值