阅读 32

Effective Java理解(二)

使用限定通配符来增加灵活性

  1. PECS ( producer-extends,consumer-super),如果一个参数化类型代表一个 T 生产者,使用 <? extends T>;如果它代表 T 消费者,则使用 <? super T>。 在Stack 示例中,pushAll 方法的 src 参数生成栈使用的 E 实例,因此 src 的合适类型为 Iterable<? extends E>;popAll 方法的 dst 参数消费 Stack 中的 E 实例,因此 dst 的合适类型是 Collection <? super E>。 PECS 助记符抓住了使用通配符类型的基本原则。 Naftalin 和 Wadler 称之为获取和放置原则。
  2. 如果编写一个将被广泛使用的类库,正确使用通配符类型应该是强制性的。

泛型结合可变参数

  1. 可变参数和泛型不能很好地交互,因为可变参数机制是在数组上面构建的脆弱的抽象,并且数组具有与泛型不同的类型规则。 虽然泛型可变参数不是类型安全的,但它们是合法的。 如果选择使用泛型(或参数化)可变参数编写方法,请首先确保该方法是类型安全的,然后使用 @SafeVarargs 注解对其进行标注
  2. 比如List<? extends T>... lists 因为这里规定了必须为T类型的子类,所以这里是类型安全的
  3. 在 Java 8 中,注解仅在静态方法和 final 实例方法上合法; 在 Java 9 中,它在私有实例方法中也变为合法。

枚举类型

许多枚举不需要显式构造方法或成员,但其他人则可以通过将数据与每个常量关联并提供行为受此数据影响的方法而受益。 使用单一方法关联多个行为可以减少枚举。 在这种相对罕见的情况下,更喜欢使用常量特定的方法来枚举自己的值。 如果一些(但不是全部)枚举常量共享共同行为,请考虑策略枚举模式。

EnumMap

  1. 有一组植物代表一个花园,想要列出这些由生命周期组织的植物 (一年生,多年生,或双年生)。为此,需要构建三个集合,每个生命周期作为一个,并遍历整个花园,将每个植物放置在适当的集合中。
// Using an EnumMap to associate data with an enum
Map<Plant.LifeCycle, Set<Plant>>  plantsByLifeCycle =
    new EnumMap<>(Plant.LifeCycle.class);

for (Plant.LifeCycle lc : Plant.LifeCycle.values())
    plantsByLifeCycle.put(lc, new HashSet<>());

for (Plant p : garden)
    plantsByLifeCycle.get(p.lifeCycle).add(p);

System.out.println(plantsByLifeCycle);

//lamba 使用 Collectors.groupingBy 的三个参数形式的方法,它允许调用者使用 mapFactory 参数指定 map 的实现
//在大量使用 Map 的程序中可能是至关重要的性能可以得到提升
System.out.println(Arrays.stream(garden)
        .collect(groupingBy(p -> p.lifeCycle,
() -> new EnumMap<>(LifeCycle.class), toSet())));

//除此之外,如果所代表的关系是多维的,使用 EnumMap <...,EnumMap <... >>
复制代码

标记注解与标记接口

  1. 如果标记是应用于除类或接口以外的任何程序元素,则必须使用注解,因为只能使用类和接口来实现或扩展接口。如果标记仅适用于类和接口,那么问自己问题:「可能我想编写一个或多个只接受具有此标记的对象的方法呢?」如果是这样,则应该优先使用标记接口而不是注解。这将使你可以将接口用作所讨论方法的参数类型,这将带来编译时类型检查的好处。
  2. 如果标记是大量使用注解的框架的一部分,则标记注解是明确的选择。
  3. 如果你想定义一个没有任何关联的新方法的类型,一个标记接口是一种可行的方法。 如果要标记除类和接口以外的程序元素,或者将标记符合到已经大量使用注解类型的框架中,那么标记注解是正确的选择。 如果发现自己正在编写目标为 ElementType.TYPE 的标记注解类型,那么请花时间弄清楚究竟应该用注解类型,还是标记接口更合适。

lamba -- 除非必须创建非函数式接口类型的实例,否则不要使用匿名类作为函数对象

  1. lambda 没有名称和文档; 如果计算不是自解释的,或者超过几行,则不要将其放入 lambda 表达式中。 一行代码对于 lambda 说是理想的,三行代码是合理的最大值。 如果违反这一规定,可能会严重损害程序的可读性。 如果一个 lambda 很长或很难阅读,要么找到一种方法来简化它或重构你的程序来消除它。
  2. Lambda 仅限于函数式接口。 如果你想创建一个抽象类的实例,你可以使用匿名类来实现,但不能使用 lambda。
  3. lambda 不能获得对自身的引用。 在 lambda 中,this 关键字引用封闭实例,在匿名类中,this 关键字引用匿名类实例。 如果你需要从其内部访问函数对象,则必须使用匿名类。

明智审慎地使用 Stream

  1. 过度使用流使程序难于阅读和维护,使用流而不过度使用它们。其结果是一个比原来更短更清晰的程序

优先考虑流中无副作用的函数

  1. forEach 操作除了表示由一个流执行的计算结果外,什么都不做,它应仅用于报告流计算的结果,而不是用于执行计算。

谨慎使用流并行

  1. 通常,并行性带来的性能收益在 ArrayList、HashMap、HashSet 和 ConcurrentHashMap 实例、数组、int 类型范围和 long 类型的范围的流上最好。这些数据结构的共同之处在于,它们都可以精确而廉价地分割成任意大小的子程序,这使得在并行线程之间划分工作变得很容易。
  2. 不要尝试并行化流管道,除非你有充分的理由相信它将保持计算的正确性并提高其速度。

给参数添加必要的限制

  1. 每次编写方法或构造方法时,都应该考虑对其参数存在哪些限制。 应该记在这些限制,并在方法体的开头使用显式检查来强制执行这些限制。 养成这样做的习惯很重要。 在第一次有效性检查失败时,它所需要的少量工作将会得到对应的回报。
  2. 在日常开发接口的时候,会有很多插入更新等操作,这个时候使用 validation 来对参数做出限制。

必要的时候进行防御性拷贝

  1. 如果一个类有从它的客户端获取或返回的可变组件,那么这个类必须防御性地拷贝这些组件。如果拷贝的成本太高,并且类信任它的客户端不会不适当地修改组件,则可以用文档替换防御性拷贝,该文档概述了客户端不得修改受影响组件的责任。
  2. 其实就是深克隆和钱克隆的问题

慎用重载

  1. 重载(overloaded)方法之间的选择是静态的,如果在子类中重载实例方法并且在子类的实例上调用,它在编译时就要选择要调用哪个重载方法,运行时类型在每次迭代中都不同,但这不会影响对重载方法的选择,都会执行父类的重载方法
  2. 重写(overridden)方法之间的选择是动态的,如果在子类中重写实例方法并且在子类的实例上调用,则无论子类实例的编译时类型如何,都会执行子类的重写方法
//全打印  --  Unknown Collection
public class CollectionClassifier {

    public static String classify(Set<?> s) {
        return "Set";
    }

    public static String classify(List<?> lst) {
        return "List";
    }

    public static String classify(Collection<?> c) {
        return "Unknown Collection";
    }

    public static void main(String[] args) {
        Collection<?>[] collections = {
            new HashSet<String>(),
            new ArrayList<BigInteger>(),
            new HashMap<String, String>().values()
        };

        for (Collection<?> c : collections)
            System.out.println(classify(c));
    }
}

//打印各个子类
class Wine {
    String name() { return "wine"; }
}

class SparklingWine extends Wine {
    @Override String name() { return "sparkling wine"; }
}

class Champagne extends SparklingWine {
    @Override String name() { return "champagne"; }
}

public class Overriding {
    public static void main(String[] args) {
        List<Wine> wineList = List.of(
            new Wine(), new SparklingWine(), new Champagne());

        for (Wine wine : wineList)
            System.out.println(wine.name());
    }
}
复制代码
  1. 安全和保守的策略是永远不要导出两个具有相同参数数量的重载。 => 可以为方法赋予不同的名称,而不是重载它们。ObjectOutputStream类。对于每个基本类型和几个引用类型,它都有其write方法的变体。这些变体都有不同的名称,例如writeBoolean(boolean)、writeInt(int)和writeLong(long),而不是重载write方法。与重载相比,这种命名模式的另一个好处是,可以为read方法提供相应的名称,例如readBoolean()、readInt()和readLong()。ObjectInputStream类实际上提供了这样的读取方法。
参考:
  1. 《effective java》3rd -- Joshua Bloch
关注下面的标签,发现更多相似文章
评论