5天带你读完《Effective Java》(二)

Effective Java第三版1 《Effective Java》第三版中文版目录 第一章 介绍 1 第二章 创建和销毁对象 4 1  考虑用静态工厂方法替换构造器 4 2  当遇到多个构造器参 立即下载

《Effective Java》是Java开发领域无可争议的经典之作,连Java之父James Gosling都说: “如果说我需要一本Java编程的书,那就是它了”。它为Java程序员提供了90个富有价值的编程准则,适合对Java开发有一定经验想要继续深入的程序员。

本系列文章便是这本著作的精华浓缩,通过阅读,读者可以在5天时间内快速掌握书中要点。为了方便读者理解,笔者用通俗易懂的语言对全书做了重新阐述,避免了如翻译版本中生硬难懂的直译,同时对原作讲得不够详细的地方做了进一步解释和引证。

本文是系列的第二部分,包含对第四、五章的19个准则的解读,约1.9万字。

第四章 类和接口

Chapter 4. Classes and Interfaces

第15条:尽量减少类和成员的可访问性

item15: Minimize the accessibility of classes and members

封装和信息隐藏是软件设计的基本原则。 Java的访问控制机制指定了类、接口和成员的可访问性。它们的可访问性由其声明的位置以及声明中出现的访问修饰符(private、protected 和 public)决定。

控制可访问级别的最佳实践是:在不影响软件功能的前提下,让每个类或成员尽可能不可访问。

对于顶级(非嵌套)类和接口,只有两个可能的访问级别:包级和公共。使用哪个级别,取决于是否需要将API对外导出。

如果包级顶级类或接口只被一个类使用,那么可以考虑在使用它的这个类中,将顶级类设置为私有静态嵌套类。

对于成员(字段、方法、嵌套类和嵌套接口),存在四种访问级别,按可访问性从低到高分别是:

  1. 私有(private):成员只能从声明它的顶级类访问。
  2. 包级(package-private):成员可以从声明它的类所在的包访问。不加修饰符时默认的访问级别。
  3. 保护(protected):成员可以从声明它的类的子类和声明它的类所在的包访问。
  4. 公共(public):成员可以从任意地方访问。

如果一个方法覆盖了超类方法,那么它在子类中的访问级别就不能比超类更严格。

公共类的实例字段很少用public修饰,除非是静态final常量。带有公共可变字段的类通常不是线程安全的。

Java 9的模块系统引入了两个额外的隐式访问级别。模块是包的分组,它可以显式导出一些包,未导出包的公共类的公共和保护成员在模块外不可访问,它们产生了两个隐式访问级别,即模块内公共和模块内保护。

第16条:在公共类中,使用访问器方法,而不是公共字段

item16: In public classes use accessor methods not public fields

对于公共类中的可变字段,不应该将它们设为公共,因为这样破坏了类的封装性。应该通过setter、getter方法访问。对于公共类中的不可变字段,设为公共的危害要小一些。

对于包级类或私有嵌套类,公开它们的字段是允许的。因为这两种类的访问受到限制,所以对它们字段的更改也能限制在一定范围内。

第17条:减少可变性

Item 17: Minimize mutability

应该尽量降低类的可变性。

不可变类是实例不能被修改的类。Java中有很多不可变类,如String、Integer等。不可变类的优点是:简单、线程安全,可作为缓存共享。

不可变类需满足以下5条规则:

  1. 不要提供修改状态的方法。
  2. 确保类不能被继承。可以通过为类加上final修饰符,或者通过静态工厂方法对外提供创建对象的唯一方法。
  3. 所有字段用final修饰。
  4. 所有字段设为私有。
  5. 确保对可变对象引用的独占访问。不要给用户提供访问这些引用的方法。

不可变类的缺点是每个不同的值都需要一个单独的对象。这样会产生很多对象创建和回收的开销。解决办法是提供一个公共可变伴随类。例如String的公共可变伴随类就是StringBuilder,用后者处理多个字符串的拼接时可以减少对象创建数量。

对于那么无法做到不可变的类,应尽量限制它的可变性。这样可以减少出错的可能性。每个字段如果可以,尽量设为私有final的。

第18条:优先选择组合而不是继承

Item 18: Favor composition over inheritance

继承并不适合于所有场合。在同一个包中使用继承是安全的,对专为扩展而设计的类使用继承也是安全的。但对普通的具体类做跨包的继承是危险的。

继承的危险在于它破坏了封装。因为子类的功能是否正确依赖于超类的实现细节。

下面例子说明了这个问题,它统计一个HashSet自创建以来添加了多少元素:

// Broken - Inappropriate use of inheritance!
public class InstrumentedHashSet<E> extends HashSet<E> {

    // The number of attempted element insertions
    private int addCount = 0;

    public InstrumentedHashSet() {
    }

    public InstrumentedHashSet(int initCap, float loadFactor) {
        super(initCap, loadFactor);
    }

    @Override
    public boolean add(E e) {
        addCount++;
        return super.add(e);
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }

    public int getAddCount() {
        return addCount;
    }
}

当我们用addAll方法添加3个元素时,问题出现了:getAddCount返回的计数是6,而不是3。这是因为super.addAll会调用覆盖后的add方法,导致每个元素被重复算了一遍。

解决办法是通过组合,而非继承。下面是用组合和转发重新实现的类,它使用了装饰者模式:

// Wrapper class - uses composition in place of inheritance
public class InstrumentedSet<E> extends ForwardingSet<E> {

    private int addCount = 0;

    public InstrumentedSet(Set<E> s) {
        super(s);
    }

    @Override
    public boolean add(E e) {
        addCount++;
        return super.add(e);
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }

    public int getAddCount() {
        return addCount;
    }
}

// Reusable forwarding class
public class ForwardingSet<E> implements Set<E> {
    private final Set<E> s;
    public ForwardingSet(Set<E> s) { this.s = s; }
    public void clear() { s.clear(); }
    public boolean contains(Object o) { return s.contains(o); }
    public boolean isEmpty() { return s.isEmpty(); }
    public int size() { return s.size(); }
    public Iterator<E> iterator() { return s.iterator(); }
    public boolean add(E e) { return s.add(e); }
    public boolean remove(Object o) { return s.remove(o); }
    public boolean containsAll(Collection<?> c)
    { return s.containsAll(c); }
    public boolean addAll(Collection<? extends E> c)
    { return s.addAll(c); }
    public boolean removeAll(Collection<?> c)
    { return s.removeAll(c); }
    public boolean retainAll(Collection<?> c)
    { return s.retainAll(c); }
    public Object[] toArray() { return s.toArray(); }
    public <T> T[] toArray(T[] a) { return s.toArray(a); }

    @Override
    public boolean equals(Object o){ return s.equals(o); }

    @Override
    public int hashCode() { return s.hashCode(); }

    @Override
    public String toString() { return s.toString(); }
}

两个类只有满足is-a关系时才应该建立继承关系。Java库中有很多违反这一原则的地方,如Stack不是Vector却继承了后者,Properties不是HashTable也继承了后者。这些情况本应该使用组合。

笔者注:鉴于继承的上述缺点,Go语言在设计时去除了继承的概念,转而用组合和接口实现类似的效果,但是更加灵活。Go的规则是:只要一个结构实现了接口的所有函数,那么就认为这个结构属于接口对应的类型,无需像Java一样显式声明继承或实现关系。即只要满足act-like-a,那么就能推导出is-a。

第19条:继承要设计良好并且具有文档,否则禁止使用

Item 19: Design and document for inheritance or else prohibit it

为了避免继承影响子类实现的正确性,需要为可覆盖方法提供详细的文档,说明它的实现细节,以及覆盖它产生的影响。

这看上去违反了一条准则:好的API文档应该描述一个方法做什么,而不是如何做。确实,这是继承违反封装导致的后果。

第20条:接口优于抽象类

Item 20: Prefer interfaces to abstract classes

接口相对抽象类的优点是:

  1. 一个类只能继承单个抽象类,却能实现多个接口。
  2. 接口的使用更加灵活,可以很容易对现有类进行改造,实现新的接口。
  3. 接口允许构造非层次化类型结构。示例如下,有一个歌手接口和一个歌曲作者接口:
public interface Singer {
   AudioClip sing(Song s);
}

public interface Songwriter {
   Song compose(int chartPosition);
}

如果有人既是歌手,又是歌曲作者,那么我们通过创建一个新的接口就能很容易实现这个需求:

public interface SingerSongwriter extends Singer, Songwriter {
    AudioClip strum();
    void actSensitive();
}

但如果是使用类继承来实现这个功能,就容易显得臃肿。特别是,如果不同属性有n个,那么就有2n种组合,这就是所谓的类型爆炸。

为了代码复用,可以为接口提供默认方法。但是默认方法有不少限制,例如编译器会阻止你提供一个与Object类中的方法重复的默认方法,而且接口不允许包含实例字段或非公共静态成员(私有静态方法除外)。

这时可以实现一个抽象骨架类来结合抽象类和接口的优点。例如Java类库中的AbstractList就是典型的抽象骨架类。抽象骨架类使用了设计模式中的模板模式。

第21条:为后代设计接口

Item 21: Design interfaces for posterity

在Java 8之前,向接口添加方法会导致现有的实现出现编译错误,影响版本兼容性。为了解决这个问题,在Java 8中添加了默认方法的功能,允许向现有接口添加方法,但是这个添加过程存在很大风险。

由于默认方法被注入到已有实现的过程对实现者是透明的,实现者无需对此做任何反应。但是有时很难为所有的实现提供一个通用的默认方法,提供的默认方法很可能在某些场合是错误的。

下面的例子是Java 8 中被添加到集合接口中的 removeIf 方法:

// Default method added to the Collection interface in Java 8
default boolean removeif(predicate<? super e> filter) {
    objects.requirenonnull(filter);
    boolean result = false;
    for (iterator<e> it = iterator(); it.hasnext(); ) {
        if (filter.test(it.next())) {
            it.remove();
            result = true;
        }
    }
    return result;
}

很遗憾,它在实际使用的一些 Collection 实现中导致了问题。例如,考虑 org.apache.commons.collections4.collection.SynchronizedCollection。这个类提供了Java Collection的同步实现版本,但由于继承了removeIf的默认实现,新增了一个非同步方法,与这个类的设计初衷不符。

总结下来,设计接口前应该三思,应仔细考虑接口应包含的方法集合。虽然默认方法可以做到接口发布后新增方法,但是你不能依赖这种有很大风险的方式。

笔者注:这个观点需要辩证理解。本书作者作为Java类库的设计者之一,他的很多观点都来自他在设计这些类库时的经验总结。对于像基础Java类库或是使用广泛的开源项目,同时需要保证严格的版本兼容性的,接口发布后新增默认方法确实会带来很大的风险。但是对于广大业务开发者来说,如果接口只是局限在自己的业务代码中,所有引用都能用IDE反向查找到,那么风险是可控的。

第22条:接口只用于定义类型

Item 22: Use interfaces only to define types

接口只应该用来定义类型,不要用来导出常量。

想要导出常量,可以把它们放在相关的类中,如Integer类中的MAX_VALUE;或者定义一个XXXConstants类来存放一组相关的常量。

第23条:类层次结构优于带标签的类

Item 23: Prefer class hierarchies to tagged classes

对于有两种或两种以上的样式的类,我们有时会定义一个标签字段来表示不同的样式。例如,下面的类能够表示一个圆或一个矩形:

// Tagged class - vastly inferior to a class hierarchy!
class Figure {
    enum Shape {RECTANGLE, CIRCLE};

    // Tag field - the shape of this figure
    final Shape shape;

    // These fields are used only if shape is RECTANGLE
    double length;

    double width;

    // This field is used only if shape is CIRCLE
    double radius;

    // Constructor for circle
    Figure(double radius) {
        shape = Shape.CIRCLE;
        this.radius = radius;
    }

    // Constructor for rectangle
    Figure(double length, double width) {
        shape = Shape.RECTANGLE;
        this.length = length;
        this.width = width;
    }

    double area() {
        switch (shape) {
            case RECTANGLE:
                return length * width;
            case CIRCLE:
                return Math.PI * (radius * radius);
            default:
                throw new AssertionError(shape);
        }
    }
}

标签类的缺点是:不同的实现逻辑混杂在一起,可读性较低,容易出错。

标签类本质上是对类层次结构的模仿。所以还不如直接用类层次结构来编写,得到更加简单明了的代码:

// Class hierarchy replacement for a tagged class
abstract class Figure {
    abstract double area();
}

class Circle extends Figure {
    final double radius;

    Circle(double radius) {
        this.radius = radius;
    }

    @Override
    double area() {
        return Math.PI * (radius * radius);
    }
}

class Rectangle extends Figure {
    final double length;
    final double width;

    Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }

    @Override
    double area() {
        return length * width;
    }
}

类层次结构的另一个优点是,可以反映类型之间的自然层次关系,而这是标签类做不到的。下面例子表示正方形是一种特殊的矩形:

class Square extends Rectangle {
  Square(double side) {
    super(side, side);
  }
}

第24条:静态成员类优于非静态成员类

Item 24: Favor static member classes over nonstatic

嵌套类共有四种:静态成员类、非静态成员类、匿名类和局部类。它们各自有不同的适用场合。判断方法见如下流程图:

在这里插入图片描述
每种嵌套类的常见例子总结如下:

  1. 静态成员类:作为公共的辅助类,如Map中的Entry类。
  2. 非静态成员类:Map的entrySet、keySet方法返回的视图,List、Set中的迭代器。
  3. 匿名类:新建Comparable、Runnable接口实现类,可用lambda表达式替代。
  4. 局部类:与匿名类的区别仅为有名字,可重复使用。

下面重点谈谈静态成员类和非静态成员类的取舍。如果成员类不需要访问外部实例,那么始终应该设置其为静态的。因为非静态的成员类会持有对外部类的引用,增加时间空间代价,而且会影响对外部类的垃圾回收。

第25条:源文件仅限有单个顶层类

Item 25: Limit source files to a single top level class

虽然Java编译器允许在单个源文件中定义多个顶层类,但是这样做没有任何好处,反而存在重大风险。请看下面的例子,这个源文件引用了另外两个顶层类(Utensil 和 Dessert):

public class Main {
    public static void main(String[] args) {
        System.out.println(Utensil.NAME + Dessert.NAME);
    }
}

然后你在一个名为 Utensil.java 的源文件中提供了 Utensil 类和 Dessert 类的实现:

// Two classes defined in one file. Don't ever do this!
class Utensil {
    static final String NAME = "pan";
}

class Dessert {
    static final String NAME = "cake";
}

不料在你不知情的情况下,另一个人在名为 Dessert.java的源文件中定义了相同的两个类:

// Two classes defined in one file. Don't ever do this!
class Utensil {
    static final String NAME = "pot";
}

class Dessert {
    static final String NAME = "pie";
}

如果你使用 javac Main.java Dessert.java 命令编译程序,编译将失败,编译器将告诉你多重定义了 Utensil 和 Dessert。这是因为编译器将首先编译 Main.java,当它看到对 Utensil 的引用(在对 Dessert 的引用之前)时,它将在 Utensil.java 中查找这个类,并找到餐具和甜点。当编译器在命令行上遇到 Dessert.java 时,(编译器)也会载入该文件,导致(编译器)同时遇到 Utensil 和 Dessert 的定义。

如果你使用命令 javac Main.java 或 javac Main.java Utensil.java 编译程序,它将按我们的预期打印出pancake。但是如果你使用命令 javac Dessert.java Main.java 编译程序,它将按别人的实现打印出 potpie。因此,程序的行为受到源文件传递给编译器的顺序的影响,这显然是不可接受的。

要避免这种问题,只需将顶层类(Utensil和Dessert)分割到单独的源文件中,或者放弃将它们作为顶层类,转而使用静态成员类。如下所示:

// Static member classes instead of multiple top-level classes
public class Test {

    public static void main(String[] args) {
        System.out.println(Utensil.NAME + Dessert.NAME);
    }

    private static class Utensil {
        static final String NAME = "pan";
    }

    private static class Dessert {
        static final String NAME = "cake";
    }
}

第五章 泛型

Chapter 5. Generics

第26条:不要使用原始类型

Item 26: Do not use raw types

每个泛型都定义了一个原始类型,它是没有任何相关类型参数的泛型的名称。例如,List<E> 对应的原始类型是 List。原始类型的行为就好像所有泛型信息都从类型声明中删除了一样。它们的存在主要是为了与之前的泛型代码兼容。

使用原始类型是类型不安全的,编译器会发出警告,而且容易出现类型相关的运行时异常。
如下面的程序:

// Fails at runtime - unsafeAdd method uses a raw type (List)!

public static void main(String[] args) {
    List<String> strings = new ArrayList<>();
    unsafeAdd(strings, Integer.valueOf(42));
    String s = strings.get(0); // Has compiler-generated cast
}

private static void unsafeAdd(List list, Object o) {
    list.add(o);
}

因为使用了原始类型List,编译器会给出一个警告:

Test.java:10: warning: [unchecked] unchecked call to add(E) as a
member of the raw type List
list.add(o);
^

然后运行程序时,会在将strings.get(0)` 的结果强制转换为字符串的地方抛出一个 ClassCastException。

如果你想使用泛型,但不知道或不关心实际的类型参数是什么,那么可以使用问号代替。例如,泛型集合 Set<E> 的无界通配符类型是 Set<?>。它是最通用的参数化集合类型,能够容纳任何集合:

// Uses unbounded wildcard type - typesafe and flexible
static int numElementsInCommon(Set<?> s1, Set<?> s2) { ... }

无界通配符类型和原始类型之间的区别是:前者中不能放入任何元素(除了 null)。如果你这样做,会得到一个编译报错:

WildCard.java:13: error: incompatible types: String cannot be converted to CAP#1
c.add("verboten");
^ where CAP#1
is a fresh type-variable:
CAP#1 extends Object from capture of ?

为便于参考,本条目中介绍的术语(以及后面将要介绍的一些术语)总结如下:

术语例子条目
参数化类型List<String>第26条
实际的类型参数String第26条
泛型类型List<E>第26条、第29条
形式化类型参数E第26条
无界泛型表达式List<?>第26条
原始类型List第26条
有界类型参数<E extends Number>第29条
递归类型限制<T extends Comparable<T>>第30条
有界泛型表达式List<? extends Number>第31条
泛型方法static <E> List<E> asList(E[] a)第30条
类型记号String.class第33条

第27条:消除 unchecked 警告

Item 27: Eliminate unchecked warnings

使用泛型编程时,很容易看到unchecked编译器警告。我们应该尽可能消除这些警告。消除所有这些警告后,我们就能确保代码是类型安全的。

有时unchecked警告很容易消除,例如下面不规范的代码会导致编译器警告:

Set<Lark> exaltation = new HashSet();

我们可以修改一下,取消警告:

Set<Lark> exaltation = new HashSet<>();

但有时警告无法消除,如果我们可以证明代码是类型安全的,可以通过SuppressWarnings(“unchecked”) 注解来抑制警告。

SuppressWarnings应该在尽可能小的范围内使用。如下例在一个变量上使用这个注解:

// Adding local variable to reduce scope of @SuppressWarnings
public <T> T[] toArray(T[] a) {
    if (a.length < size) {
        // This cast is correct because the array we're creating
        // is of the same type as the one passed in, which is T[].
        @SuppressWarnings("unchecked") T[] result = (T[]) Arrays.copyOf(elements, size, a.getClass());
        return result;
    }
    System.arraycopy(elements, 0, a, 0, size);
    if (a.length > size)
        a[size] = null;
    return a;
}

每次使用注解时,要添加一条注释,说明这样做是安全的,以帮助他人理解代码。

第28条:list 优于数组

Item 28: Prefer lists to arrays

使用泛型时,优先考虑用list,而非数组。

数组和泛型有两个重要区别,这让它们在一起工作不那么协调。

区别一:数组是协变的,而泛型不是。如果 Sub 是 Super 的子类型,那么类型 Sub[] 就是类型 Super[] 的子类型,而List<Sub>并非List<Super>的子类型。

例如,下面这段代码是合法的:

// Fails at runtime!
Object[] objectArray = new Long[1];
objectArray[0] = "I don't fit in"; // Throws ArrayStoreException

但这一段代码就不是:

// Won't compile!
List<Object> ol = new ArrayList<Long>(); // Incompatible types
ol.add("I don't fit in");

两种方法都不能将 String 放入 Long 容器,但使用数组,你会得到一个运行时错误;使用 list,你可以在编译时发现问题。后者当然是更加安全的。

区别二:数组是具体化的,而泛型通过擦除来实现。这意味着,数组在运行时知道并强制执行他们的元素类型,而泛型只在编译时执行类型约束,在运行时丢弃类型信息,这样做是为了与不使用泛型的老代码兼容。

由于这些差异,数组和泛型不能很好地混合。例如,创建泛型、参数化类型或类型参数的数组是非法的。因此,这些数组创建表达式都是非法的:new List<E>[]、new List<String>[]、new E[]。所有这些行为都会在编译时报错,原因是它们并非类型安全。如果合法,那么类型错误可能延迟到运行时才出现,这违反了泛型系统的基本保证。

例如下面代码:

// Why generic array creation is illegal - won't compile!
List<String>[] stringLists = new List<String>[1]; // (1)
List<Integer> intList = List.of(42); // (2)
Object[] objects = stringLists; // (3)
objects[0] = intList; // (4)
String s = stringLists[0].get(0); // (5)

如果第一行是合法的,那么会运行时到第5行才抛出运行时异常。

第29条:优先使用泛型类型

Item 29: Favor generic types

应该尽量在自己编写的类型中使用泛型,这会保证类型安全,并使代码更易使用。

下面我们通过例子来看下如何对一个现有类做泛型化改造。

首先是一个简单的堆栈实现:

// Object-based collection - a prime candidate for generics
public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new Object[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(Object e) {
        ensureCapacity();
        elements[size++] = e;
    }

    public Object pop() {
        if (size == 0)
            throw new EmptyStackException();
        Object result = elements[--size];
        elements[size] = null; // Eliminate obsolete reference
        return result;
    }

    public boolean isEmpty() {
        return size == 0;
    }

    private void ensureCapacity() {
        if (elements.length == size)
            elements = Arrays.copyOf(elements, 2 * size + 1);
    }
}

我们用适当的类型参数替换所有的 Object 类型,然后尝试编译修改后的程序:

// Initial attempt to generify Stack - won't compile!
public class Stack<E> {
    private E[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        elements = new E[DEFAULT_INITIAL_CAPACITY];
    }

    public void push(E e) {
        ensureCapacity();
        elements[size++] = e;
    }

    public E pop() {
        if (size == 0)
            throw new EmptyStackException();
        E result = elements[--size];
        elements[size] = null; // Eliminate obsolete reference
        return result;
    } ... // no changes in isEmpty or ensureCapacity
}

这时生成一个错误:

Stack.java:8: generic array creation
elements = new E[DEFAULT_INITIAL_CAPACITY];
^

上一条目中讲到不能创建一个非具体化类型的数组。因此我们修改为:

// The elements array will contain only E instances from push(E).
// This is sufficient to ensure type safety, but the runtime
// type of the array won't be E[]; it will always be Object[]!
@SuppressWarnings("unchecked")
public Stack() {
    elements = (E[]) new Object[DEFAULT_INITIAL_CAPACITY];
}

另一种解决编译错误的方法是将字段元素的类型从 E[] 更改为 Object[]。这时会得到一个不同的错误:

Stack.java:19: incompatible types
found: Object, required: E
E result = elements[--size];
^

You can change this error into a warning by casting the element retrieved from the array to E, but you will get a warning:

通过将从数组中检索到的元素转换为 E,可以将此错误转换为警告。我们对警告做抑制:

// Appropriate suppression of unchecked warning
public E pop() {
    if (size == 0)
        throw new EmptyStackException();
    // push requires elements to be of type E, so cast is correct
    @SuppressWarnings("unchecked")
    E result =(E) elements[--size];
    elements[size] = null; // Eliminate obsolete reference
    return result;
}

第30条:优先使用泛型方法

Item 30: Favor generic methods

应尽量使方法支持泛型,这样可以保证类型安全,并让代码更容易使用。例如下面代码:

// Generic method
public static <E> Set<E> union(Set<E> s1, Set<E> s2) {
    Set<E> result = new HashSet<>(s1);
    result.addAll(s2);
    return result;
}

有时,你需要创建一个对象,该对象是不可变的,但适用于许多不同类型,这时可以用泛型单例工厂模式来实现,如 Collections.emptySet。

下面的例子实现了一个恒等函数分发器:

// Generic singleton factory pattern
private static UnaryOperator<Object> IDENTITY_FN = (t) -> t;

@SuppressWarnings("unchecked")
public static <T> UnaryOperator<T> identityFunction() {
    return (UnaryOperator<T>) IDENTITY_FN;
}

然后是对这个恒等函数分发器的使用:

// Sample program to exercise generic singleton
public static void main(String[] args) {
    String[] strings = { "jute", "hemp", "nylon" };
    UnaryOperator<String> sameString = identityFunction();

    for (String s : strings)
        System.out.println(sameString.apply(s));

    Number[] numbers = { 1, 2.0, 3L };
    UnaryOperator<Number> sameNumber = identityFunction();

    for (Number n : numbers)
        System.out.println(sameNumber.apply(n));
}

递归类型限定允许类型参数被包含该类型参数本身的表达式限制,常见的场合是用在Comparable接口上。例如:

// Using a recursive type bound to express mutual comparability
public static <E extends Comparable<E>> E max(Collection<E> c);

第31条:使用有界通配符增加 API 的灵活性

Item 31: Use bounded wildcards to increase API flexibility

在泛型中使用有界通配符,可以让API更加灵活。

考虑第29条中的堆栈类。我们创建一个Stack<Number>类型的堆栈,并在其中插入integer。

Stack<Number> numberStack = new Stack<>();
Iterable<Integer> integers = ... ;
numberStack.pushAll(integers);

这个例子在直觉上似乎是没问题的。然而实际执行的时候会报错:

StackTest.java:7: error: incompatible types: Iterable<Integer>
cannot be converted to Iterable<Number>
        numberStack.pushAll(integers);
                    ^

解决办法是使用带extends的有界通配符类型。下面代码表示泛型参数为E的子类型(包括E类型本身):

// Wildcard type for a parameter that serves as an E producer
public void pushAll(Iterable<? extends E> src) {
    for (E e : src)
        push(e);
}

作为堆栈,还需要对外提供弹出元素的方法,以下是基础实现:

// popAll method without wildcard type - deficient!
public void popAll(Collection<E> dst) {
    while (!isEmpty())
        dst.add(pop());
}

但是这个实现在遇到下面场景时也会出现运行时报错,错误信息与前面的非常类似:Collection<Object>不是 Collection<Number> 的子类型。

Stack<Number> numberStack = new Stack<Number>();
Collection<Object> objects = ... ;
numberStack.popAll(objects);

解决办法是使用带super的有界通配符类型。下面例子表示泛型参数为E的超类(包括E类型本身)。

// Wildcard type for parameter that serves as an E consumer
public void popAll(Collection<? super E> dst) {
  while (!isEmpty()
    dst.add(pop());
}

总结上面例子的经验,就是生产者用extends通配符,消费者用super通配符。可以简记为PECS原则:producer-extends, consumer-super。

第32条:明智地合用泛型和可变参数

Item 32: Combine generics and varargs judiciously

可变参数方法和泛型在一起工作时不那么协调,因此需要特别注意。

可变参数方法的设计属于一个抽象泄漏,即当你调用可变参数方法时,将创建一个数组来保存参数;该数组本应是实现细节,却是可见的。因此会出现编译器警告。

下面是一个可变参数和泛型混用,造成类型错误的例子:

// Mixing generics and varargs can violate type safety!
static void dangerous(List<String>... stringLists) {
    List<Integer> intList = List.of(42);
    Object[] objects = stringLists;
    objects[0] = intList; // Heap pollution
    String s = stringLists[0].get(0); // ClassCastException
}

有人可能会问:为什么使用泛型可变参数声明方法是合法的,而显式创建泛型数组是非法的?因为带有泛型或参数化类型的可变参数的方法在实际开发中非常有用,因此语言设计人员选择忍受这种不一致性。

要保证可变参数和泛型混用时的类型安全,有以下两种方式:

  1. 为方法添加 SafeVarargs标记,这代表方法的编写者承诺这个方法是类型安全的,需要方法编写者自己保证。这时编译器警告会被消除。
  2. 如果方法内部保证可变参数只读,不做任何修改,那么这个方法也是类型安全的。

将数组传递给另一个使用 @SafeVarargs 正确注释的可变参数方法是安全的,将数组传递给仅计算数组内容的某个函数的非可变方法也是安全的。

第33条:考虑类型安全的异构容器

Item 33: Consider typesafe heterogeneous containers

在前面的例子中,类型参数都是固定数量,例如Map<K, V>就只有两个类型参数。如果我们需要无限数量的类型参数,可以通过将类型参数放置在键上而不是容器上。例如,可以使用 DatabaseRow 类型表示数据库行,并使用泛型类型 Column<T> 作为它的键。

下面的例子实现了以类型作为键的缓存,它是类型安全的,例如以String类型为键读取时,读到的对象肯定也是String类型,而不是Integer类型。

// Typesafe heterogeneous container pattern - implementation
public class Favorites {
  private Map<Class<?>, Object> favorites = new HashMap<>();

  public <T> void putFavorite(Class<T> type, T instance) {
    favorites.put(Objects.requireNonNull(type), instance);
  }

  public <T> T getFavorite(Class<T> type) {
    return type.cast(favorites.get(type));
  }
	// Typesafe heterogeneous container pattern - client
  public static void main(String[] args) {
	    Favorites f = new Favorites();
	    f.putFavorite(String.class, "Java");
	    f.putFavorite(Integer.class, 0xcafebabe);
	    f.putFavorite(Class.class, Favorites.class);
	    String favoriteString = f.getFavorite(String.class);
	    int favoriteInteger = f.getFavorite(Integer.class);
	    Class<?> favoriteClass = f.getFavorite(Class.class);
	    System.out.printf("%s %x %s%n", favoriteString,favoriteInteger, favoriteClass.getName());
  }
}
Ubuntu中snap简单介绍 通过Snap可以安装众多的软件包。需要注意的是,snap是一种全新的软件包管理方式,它类似一个容器拥有一个应用程序所有的文件和库,各个应用程序之间完全独立。所以使用snap包的好处就是它解决了应用程序之间的依赖问题,使应用程序之间更容易管理。但是由此带来的问题就是它占用更多的磁盘空间 介绍一些常用命令,有点类似docker 1、查看已经通过snap安装的snap包 snap list可以查看 2、可以搜索想要的snap包,例如tomcat snap search tomcat 阅读详情

相关推荐

AI for Science:GNN预测分子性质全流程详解与工业实践

从分子表示到工业落地,一文掌握图神经网络在药物研发与材料设计中的核心实战近年来,的浪潮正深刻变革着传统科研范式。在药物研发与材料设计领域,作为核心环节,已从传统的实验试错转向基于图神经网络(GNN)的智能计算。本文将深入解析GNN预测分子性质的,涵盖数据准备、模型设计、训练优化到工业部署,并揭秘前沿技术与实战案例。

datacanvas2426的博客 1674

5天带你读完《Effective Java》(一)

Effective Java》是Java开发领域无可争议的经典之作,连Java之父都说:“如果说我需要一本Java编程的书,那就是它了”。它为Java程序员提供了90个富有价值的编程准则,适合对Java开发有一定经验想要继续深入的程序员。本系列文章便是这本著作的精华浓缩,通过阅读,读者可以在5天时间内快速掌握书中要点。为了方便读者理解,笔者用通俗易懂的语言对全书做了重新阐述,避免了如翻译版本中生硬难懂的直译,同时对原作讲得不够详细的地方做了进一步解释和引证。

AlbenXie的博客 65

Vienna整流器SVPWM调制:7扇区划分与中点电位平衡的MATLAB/Simulink实现解析

本文深入解析Vienna整流器的SVPWM调制技术,重点介绍7扇区划分与中点电位平衡的MATLAB/Simulink实现方法。通过详细的算法推导和仿真模型构建,帮助工程师掌握整流器闭环控制策略,优化功率转换效率。内容涵盖扇区判断逻辑、矢量作用时间计算及中点电位平衡的PID调节等核心技术要点。

weixin_33937499的博客 387

《Effective Java》真的是一本值得一直去钻研的好书

文章目录《Effective Java》读书笔记第一章 引言第二章 创建和销毁对象1 考虑使用静态工厂方法替代构造方法**2 当构造方法参数过多时使用builder(建造者) 模式**3 使用私有构造方法或枚类实现Singleton 属性*4 使用私有构造方法执行非实例化*5 依赖注入优于硬连接资源(hardwiring resources)**6 避免创建不必要的对象*7 消除过期的对象引用*8 避免使用Finalizer 和Cleaner 机制**9 使用try-with-resources 语句替代t

追求幸福,探索未知的博客 8642

Redmi K30 5G刷 LineageOS17.1

前言 买手机前就观望了Xdr论坛,看Redmi K30 5G有LineageOS17.1的Rom并且还挺热,应该还不错,就入手了。晚上就刷机了。买了小米就是为了折腾,为发烧而生。 在开始前还请先安装adb工具,因为我自己是做Android开发的,自带adb,不会安装的可以百度“Android安装adb工具”。还有下载刷机文件:百度云 密码:2a0f 刷机的系统是刚买来的低版本,没有升级过Rom,如果高版本慎刷。 还有这个Rom是5G版本的,4G版本的不要刷。 建议没有刷过机器的最好不要尝试,变砖后果自负。

u010436867的博客 1万+

Kali Linux下Havoc框架编译避坑指南:从环境配置到成功运行的全流程

本文提供了在Kali Linux系统上成功编译和运行Havoc开源框架的详细指南。文章重点解析了从Go语言环境配置、系统依赖库安装到交叉编译环境搭建的全流程,并针对编译过程中常见的Qt、Boost及mingw-w64等依赖问题给出了具体的解决方案,帮助安全研究人员绕过常见陷阱,顺利完成环境部署。

weixin_29230865的博客 96

一文简单理解《Effective Java》建议

​一种失败的接口就是所谓的常量接口这样的接口不包含任何方法;它只包含静态 final 属 性,每个输出一个常量。使用这些常量的类实现接口,以避免需要用类名限定常量名。​常量接口模式是对接口的糟糕使用。类在内部使用一些常量,完全属于实现细节。实现一个常量接口会导致这 个实现细节泄漏到类的导出 API 中。对类的用户来说,类实现一个常量接口是没有意义的。事实上,它甚至可能使他 们感到困惑。更糟糕的是,它代表了一个承诺:如果在将来的版本中修改了类,不再需要使用常量,那么它仍然必须 实现接口,以确保二进制兼容性。

weixin_40709965的博客 2627

解锁SPWM电压利用率:三次谐波注入幅值的黄金分割点

本文深入探讨了SPWM电压利用率的提升方法,重点介绍了三次谐波注入技术及其黄金分割点(1/6比例)。通过数学推导和工程实践,揭示了如何通过特定幅值的三次谐波注入将电压利用率从86.6%提升至115%,与SVPWM相当。文章还提供了实验室调试流程和DSP实现代码,帮助工程师优化逆变器设计。

weixin_30729609的博客 915

带你快速看完9.8分神作《Effective Java》—— 并发篇(工作里的这些坑你都遇到过吗?)

???? Java学习:Java从入门到精通总结 ???? Spring系列推荐:Spring源码解析 ???? 最近更新:2021年12月16日 ???? 个人简介:通信工程本硕????、阿里新晋猿同学????。我的故事充满机遇、挑战与翻盘,欢迎关注作者来共饮一杯鸡汤 ???? 点赞 ???? 收藏 ⭐留言 ???? 都是我最大的动力! 豆瓣评分9.8的图书《Effective Java》,是当今世界顶尖高手Josh Bloch的著作,在我之前的文章里我也提到过,编程就像练武,既需要外在的武功招

如切如磋,如琢如磨 1万+

【ESP32使用MAX98357播放音频】

在物联网和智能家居领域,音频播放功能越来越受到重视。ESP32作为一款功能强大的微控制器,结合MAX98357音频放大器模块,可以实现高品质的音频播放。本文将详细介绍如何使用ESP32的I2S接口通过MAX98357进行音频播放。

vor234的博客 7529

080、多轴运动控制:插补器设计(螺旋线插补)

本文分享了螺旋线插补器设计的实战经验。针对五轴点胶机Z轴抖动问题,作者分析了螺旋线数学本质,提出基于归一化时间参数的速度规划方法,强调各轴严格同步的重要性。文章详细介绍了速度分解、加减速处理、嵌入式优化技巧(查表法、定点化),并总结了起点闭合、速度波动、数值处理等常见问题解决方案。最后给出5条实用建议:注重速度规划而非纯数学推导、预先仿真、保留调试接口、考虑机械谐振、防范编码器丢脉冲。通过优化插补周期、电子齿轮比和S形速度曲线,成功将设备良率从60%提升至95%。

m0_50546716的博客 86

Effective Java 经典学习(一)

  (尊重劳动成果,转载请注明出处:https://blog.youkuaiyun.com/qq_25827845/article/details/85016496冷血之心的博客) 系列文章: Effective Java经典学习(一) 目录 第二章:创建和销毁对象 (1)使用静态工厂方法代替构造器 (2)遇到多个构造器参数时要考虑用构建器 (3)用私有构造器或者枚举类型强化Singleton...

冷血之心的博客 8618

华为OD机试 - 小火车最多人时所在园区站点(Java 2024 E卷 100分)

刷的越多,抽中的概率越大,私信哪吒,备注华为OD,加入华为OD刷题交流群,每一题都有详细的答题思路、详细的代码注释、发现新题目,随时更新,全天优快云在线答疑。

学Java,找哪吒 996

5天带你读完《Effective Java》(五)

在没有考虑默认序列化形式是否合适之前,不要接受它。如果对象的物理表示与其逻辑内容相同,则默认的序列化形式可能是合适的。/*** @serial/*** @serial/*** @serial即使你认为默认的序列化形式是合适的,你通常也必须提供readObject方法来确保不变性和安全性。Entry next;将导出的 API 永久地绑定到当前的内部实现。占用过多的空间。消耗过多的时间。可能导致堆栈溢出。

长三月的游戏开发 2538

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

文档:亲测可用解决vnc灰屏,黑屏,鼠标大黑叉情况

主要解决vnc-viewer灰屏,黑屏,鼠标大黑叉等现象,详细见文档。

5天带你读完《Effective Java》(三)

标记接口是一种不包含任何方法声明的接口,它只是标记它的实现类具有某种特性。如Serializable 接口,实现此接口的类可以写入ObjectOutputStream(被序列化)。如果要定义类型,那么使用标记接口优于使用标记注解。标记接口定义的类型由标记类的实例实现;标记注解不会。前者在编译时捕获错误,后者在运行时才能捕捉错误。可以更精确地定位。标记注解可以应用于任何类或接口,而标记接口限定于它的实现类。标记注解的优点是:它们可以是其他注解功能的一部分。

AlbenXie的博客 116

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

5天带你读完《Effective Java》(四)

Effective Java》是Java开发领域无可争议的经典之作,连Java之父James Gosling都说: “如果说我需要一本Java编程的书,那就是它了”。它为Java程序员提供了90个富有价值的编程准则,适合对Java开发有一定经验想要继续深入的程序员。本系列文章便是这本著作的精华浓缩,通过阅读,读者可以在5天时间内快速掌握书中要点。为了方便读者理解,笔者用通俗易懂的语言对全书做了重新阐述,避免了如翻译版本中生硬难懂的直译,同时对原作讲得不够详细的地方做了进一步解释和引证。

长三月的游戏开发 1677

OV9734 DATASHEET,完整规格书

OV9734完整规格书,包括电气参数,尺寸规格,寄存器配置表等

读完 Effective Java,我整理这 59 条技巧!

作者:Dong GuoChao链接:https://blog.dogchao.cn/?p=70 Effective Java,Java名著,必读。 如果能严格遵从本文的原则,以编写API的质量来苛求自己的代码,会大大提升编码素质。 以下内容只记录了我自己整理的东西,还是建议读原文。为了聚焦知识点,一些说明故意忽略掉了。相当于是一篇摘要。 1、考虑用静态工厂方法替代构造函数 例子: Integer.valueOf(“1”)、Boolean.valueOf(“true”)等。 优势: 可读性高(方法名) 性

Java技术栈,分享最主流的Java技术 438

读完《Effective Java》: 我整理这 50 条技巧

点击上方Java后端,选择设为星标优质文章,及时送达《Effective Java》Java名著,必读。如果能严格遵从本文的原则,以编写API的质量来苛求自己的代码,会大大提升编码素...

公众号:Java后端 317

读完《Effective Java》后我淦了 50 条开发技巧

《Effective Java》Java名著,必读。如果能严格遵从本文的原则,以编写API的质量来苛求自己的代码,会大大提升编码素质。以下内容只记录了我自己整理的东西,还是建议读原文。为...

emprere的博客 156

读完《Effective Java》后,我总结了 50 条开发技巧

作者 | Dong GuoChao来源 |https://blog.dogchao.cn/?p=70《Effective Java》,Java经典必读名著。如果能严格遵从本文的原则,以...

华章IT官方博客 251

读完 Effective Java,我整理了 59 条技巧!(含pdf)

点击????方“逆锋起笔”,公众号回复视频教程领取大佬们推荐的学习资料上一篇:CTO 写低级 Bug,致公司 70 GB 数据泄露!作者:Dong GuoChao链接:https://blog...

weixin_38754349的博客 908
上一篇: 5天带你读完《Effective Java》(一)
下一篇: 手把手教你写一个极简版Netty
长三月
博客等级 码龄9年 228粉丝 80原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值