pursue wind pursue wind
首页
Java
Python
数据库
框架
Linux
中间件
前端
计算机基础
DevOps
项目
面试
书
关于
归档
MacOS🤣 (opens new window)
GitHub (opens new window)
首页
Java
Python
数据库
框架
Linux
中间件
前端
计算机基础
DevOps
项目
面试
书
关于
归档
MacOS🤣 (opens new window)
GitHub (opens new window)
  • EffectiveJava

    • 考虑使用静态工厂方法替代构造方法
    • 当构造方法参数过多时使用builder模式
    • 使用私有构造方法或枚类实现Singleton属性
    • 使用私有构造方法执行非实例化
    • 依赖注入优于硬连接资源(hardwiring resources)
    • 避免创建不必要的对象
    • 消除过期的对象引用
    • 避免使用Finalizer和Cleaner机制
    • 使用try-with-resources语句替代try-finally语句
    • 重写equals方法时遵守通用约定
    • 重写equals方法时同时也要重写hashcode方法
    • 始终重写 toString 方法
    • 谨慎地重写 clone 方法
    • 考虑实现Comparable接口
    • 使类和成员的可访问性最小化
    • 在公共类中使用访问方法而不是公共属性
    • 最小化可变性
    • 组合优于继承
    • 要么设计继承并提供文档说明,要么禁用继承
    • 接口优于抽象类
    • 为后代设计接口
    • 接口仅用来定义类型
    • 类层次结构优于标签类
    • 支持使用静态成员类而不是非静态类
    • 将源文件限制为单个顶级类
    • 不要使用原始类型
    • 消除非检查警告
    • 列表优于数组
    • 优先考虑泛型
    • 优先使用泛型方法
    • 使用限定通配符来增加API的灵活性
    • 合理地结合泛型和可变参数
    • 优先考虑类型安全的异构容器
    • 使用枚举类型替代整型常量
    • 使用实例属性替代序数
    • 使用EnumSet替代位属性
    • 使用EnumMap替代序数索引
    • 使用接口模拟可扩展的枚举
    • 注解优于命名模式
    • 始终使用Override注解
    • 使用标记接口定义类型
    • lambda表达式优于匿名类
    • 方法引用优于lambda表达式
    • 优先使用标准的函数式接口
    • 明智审慎地使用Stream
    • 优先考虑流中无副作用的函数
    • 优先使用Collection而不是Stream来作为方法的返回类型
    • 谨慎使用流并行
    • 检查参数有效性
    • 必要时进行防御性拷贝
    • 仔细设计方法签名
    • 明智审慎地使用重载
    • 明智审慎地使用可变参数
    • 返回空的数组或集合,不要返回 null
    • 明智审慎地返回 Optional
    • 为所有已公开的 API 元素编写文档注释
    • 最小化局部变量的作用域
    • for-each 循环优于传统 for 循环
    • 了解并使用库
    • 若需要精确答案就应避免使用 float 和 double 类型
    • 基本数据类型优于包装类
    • 当使用其他类型更合适时应避免使用字符串
    • 当心字符串连接引起的性能问题
    • 通过接口引用对象
    • 接口优于反射
    • 明智审慎地本地方法
    • 明智审慎地进行优化
    • 遵守被广泛认可的命名约定
    • 只针对异常的情况下才使用异常
    • 对可恢复的情况使用受检异常,对编程错误使用运行时异常
    • 避免不必要的使用受检异常
    • 优先使用标准的异常
    • 抛出与抽象对应的异常
    • 每个方法抛出的异常都需要创建文档
    • 在细节消息中包含失败一捕获信息
    • 保持失败原子性
    • 不要忽略异常
    • 同步访问共享的可变数据
    • 避免过度同步
    • executor 、task 和 stream 优先于线程
    • 并发工具优于 wait 和 notify
    • 文档应包含线程安全属性
    • 明智审慎的使用延迟初始化
    • 不要依赖线程调度器
    • 优先选择 Java 序列化的替代方案
    • 非常谨慎地实现 Serializable
    • 考虑使用自定义的序列化形式
    • 保护性的编写 readObject 方法
    • 对于实例控制,枚举类型优于 readResolve
    • 考虑用序列化代理代替序列化实例
  • On Java 8

  • 书
  • EffectiveJava
pursuewind
2020-11-22

优先考虑类型安全的异构容器

# 33. 优先考虑类型安全的异构容器

泛型的常见用法包括集合,如 Set<E> 和 Map<K,V> 和单个元素容器,如 ThreadLocal<T> 和 AtomicReference<T>。 在所有这些用途中,它都是参数化的容器。 这限制了每个容器只能有固定数量的类型参数。 通常这正是你想要的。 一个 Set 有单一的类型参数,表示它的元素类型; 一个 Map 有两个,代表它的键和值的类型;等等。

然而有时候,你需要更多的灵活性。 例如,数据库一行记录可以具有任意多列,并且能够以类型安全的方式访问它们是很好的。 幸运的是,有一个简单的方法可以达到这个效果。 这个想法是参数化键(key)而不是容器。 然后将参数化的键提交给容器以插入或检索值。 泛型类型系统用于保证值的类型与其键一致。

作为这种方法的一个简单示例,请考虑一个 Favorites 类,它允许其客户端保存和检索任意多种类型的 favorite 实例。 该类型的 Class 对象将扮演参数化键的一部分。其原因是这 Class 类是泛型的。 类的类型从字面上来说不是简单的 Class,而是 Class<T>。 例如,String.class 的类型为 Class<String>,Integer.class 的类型为 Class<Integer>。 当在方法中传递字面类传递编译时和运行时类型信息时,它被称为类型令牌(type token)[Bracha04]。

Favorites 类的 API 很简单。 它看起来就像一个简单 Map 类,除了该键是参数化的以外。 客户端在设置和获取 favorites 实例时呈现一个 Class 对象。 如下是 API:

// Typesafe heterogeneous container pattern - API
public class Favorites {
    public <T> void putFavorite(Class<T> type, T instance);
    public <T> T getFavorite(Class<T> type);
}
1
2
3
4
5

下面是一个演示 Favorites 类,保存,检索和打印喜欢的 String,Integer 和 Class 实例:

// 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());
}
1
2
3
4
5
6
7
8
9
10
11
12
13

正如你所期望的,这个程序打印 Java cafebabe Favorites。 请注意,顺便说一下,Java 的 printf 方法与 C 语言的不同之处在于,你应该在 C 中使用 \n 的地方改用 %n。%n 用于生成适用于特定平台的行分隔符,在大多数平台上面的值为 \n,但并不是所有平台的分隔符都为 \n。

Favorites 实例是类型安全的:当你请求一个字符串时它永远不会返回一个整数。 它也是异构的:与普通 Map 不同,所有的键都是不同的类型。 因此,我们将 Favorites 称为类型安全异构容器(typesafe heterogeneous container)。

Favorites 的实现非常小巧。 这是完整的代码:

// 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));
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

这里有一些微妙的事情发生。 每个 Favorites 实例都由一个名为 favorites 私有的 Map<Class<?>, Object> 来支持。 你可能认为无法将任何内容放入此 Map 中,因为这是无限定的通配符类型,但事实恰恰相反。 需要注意的是通配符类型是嵌套的:它不是通配符类型的 Map 类型,而是键的类型。 这意味着每个键都可以有不同的参数化类型:一个可以是 Class<String>,下一个 Class<Integer> 等等。 这就是异构的由来。

接下来要注意的是,favorites 的 Map 的值类型只是 Object。 换句话说,Map 不保证键和值之间的类型关系,即每个值都是由其键表示的类型。 事实上,Java 的类型系统并不足以表达这一点。 但是我们知道这是真的,并在检索一个 favorite 时利用了这点。

putFavorite 实现很简单:只需将给定的 Class 对象映射到给定的 favorites 的实例即可。 如上所述,这丢弃了键和值之间的“类型联系(type linkage)”;无法知道这个值是不是键的一个实例。 但没关系,因为 getFavorites 方法可以并且确实重新建立这种关联。

getFavorite 的实现比 putFavorite 更复杂。 首先,它从 favorites Map 中获取与给定 Class 对象相对应的值。 这是返回的正确对象引用,但它具有错误的编译时类型:它是 Object(favorites map 的值类型),我们需要返回类型 T。因此,getFavorite 实现动态地将对象引用转换为 Class 对象表示的类型,使用 Class 的 cast 方法。

cast 方法是 Java 的 cast 操作符的动态模拟。它只是检查它的参数是否由 Class 对象表示的类型的实例。如果是,它返回参数;否则会抛出 ClassCastException 异常。我们知道,假设客户端代码能够干净地编译,getFavorite 中的强制转换不会抛出 ClassCastException 异常。 也就是说,favorites map 中的值始终与其键的类型相匹配。

那么这个 cast 方法为我们做了什么,因为它只是返回它的参数? cast 的签名充分利用了 Class 类是泛型的事实。 它的返回类型是 Class 对象的类型参数:

public class Class<T> {
    T cast(Object obj);
}
1
2
3

这正是 getFavorite 方法所需要的。 这正是确保 Favorites 类型安全,而不用求助一个未经检查的强制转换的 T 类型。

Favorites 类有两个限制值得注意。 首先,恶意客户可以通过使用原始形式的 Class 对象,轻松破坏 Favorites 实例的类型安全。 但生成的客户端代码在编译时会生成未经检查的警告。 这与正常的集合实现(如 HashSet 和 HashMap)没有什么不同。 通过使用原始类型 HashSet(条目 26),可以轻松地将字符串放入 HashSet<Integer> 中。 也就是说,如果你愿意为此付出一点代价,就可以拥有运行时类型安全性。 确保 Favorites 永远不违反类型不变的方法是,使 putFavorite 方法检查该实例是否由 type 表示类型的实例,并且我们已经知道如何执行此操作。只需使用动态转换:

// Achieving runtime type safety with a dynamic cast
public<T> void putFavorite(Class<T> type, T instance) {
    favorites.put(type, type.cast(instance));
}
1
2
3
4

java.util.Collections 中有一些集合包装类,可以发挥相同的诀窍。 它们被称为 checkedSet,checkedList,checkedMap 等等。 他们的静态工厂除了一个集合(或 Map)之外还有一个 Class 对象(或两个)。 静态工厂是泛型方法,确保 Class 对象和集合的编译时类型匹配。 包装类为它们包装的集合添加了具体化。 例如,如果有人试图将 Coin 放入你的 Collection<Stamp> 中,则包装类在运行时会抛出 ClassCastException。 这些包装类对于追踪在混合了泛型和原始类型的应用程序中添加不正确类型的元素到集合的客户端代码很有用。

Favorites 类的第二个限制是它不能用于不可具体化的(non-reifiable)类型(详见第 28 条)。 换句话说,你可以保存你最喜欢的 String 或 String[],但不能保存 List<String>。 如果你尝试保存你最喜欢的 List<String>,程序将不能编译。 原因是无法获取 List<String> 的 Class 对象。 List<String>.class 是语法错误,也是一件好事。 List<String> 和 List<Integer> 共享一个 Class 对象,即 List.class。 如果“字面类型(type literals)”List<String> .class 和 List<Integer>.class 合法并返回相同的对象引用,那么它会对 Favorites 对象的内部造成严重破坏。 对于这种限制,没有完全令人满意的解决方法。

Favorites 使用的类型令牌 type tokens) 是无限制的:getFavorite 和 putFavorite 接受任何 Class 对象。 有时你可能需要限制可传递给方法的类型。 这可以通过一个有限定的类型令牌来实现,该令牌只是一个类型令牌,它使用限定的类型参数(详见第 30 条)或限定的通配符(详见第 31 条)来放置可以表示的类型的边界。

注解 API(详见第 39 条)广泛使用限定类型的令牌。 例如,以下是在运行时读取注解的方法。 此方法来自 AnnotatedElement 接口,该接口由表示类,方法,属性和其他程序元素的反射类型实现:

public <T extends Annotation>
    T getAnnotation(Class<T> annotationType);
1
2

参数 annotationType 是表示注解类型的限定类型令牌。 该方法返回该类型的元素的注解(如果它有一个);如果没有,则返回 null。 本质上,注解元素是一个类型安全的异构容器,其键是注解类型。

假设有一个 Class<?> 类型的对象,并且想要将它传递给需要限定类型令牌(如 getAnnotation)的方法。 可以将对象转换为 Class<? extends Annotation>,但是这个转换没有被检查,所以它会产生一个编译时警告(详见第 52 条)。 幸运的是,Class 类提供了一种安全(动态)执行这种类型转换的实例方法。 该方法被称为 asSubclass,并且它转换所调用的 Class 对象来表示由其参数表示的类的子类。 如果转换成功,该方法返回它的参数;如果失败,则抛出 ClassCastException 异常。

以下是如何使用 asSubclass 方法在编译时读取类型未知的注解。 此方法编译时没有错误或警告:

// Use of asSubclass to safely cast to a bounded type token
static Annotation getAnnotation(AnnotatedElement element,
                                String annotationTypeName) {
    Class<?> annotationType = null; // Unbounded type token
    try {
        annotationType = Class.forName(annotationTypeName);
    } catch (Exception ex) {
        throw new IllegalArgumentException(ex);
    }
    return element.getAnnotation(
        annotationType.asSubclass(Annotation.class));
}
1
2
3
4
5
6
7
8
9
10
11
12

总之,泛型 API 的通常用法(以集合 API 为例)限制了每个容器的固定数量的类型参数。 你可以通过将类型参数放在键上而不是容器上来解决此限制。 可以使用 Class 对象作为此类型安全异构容器的键。 以这种方式使用的 Class 对象称为类型令牌。 也可以使用自定义键类型。 例如,可以有一个表示数据库行(容器)的 DatabaseRow 类型和一个泛型类型 Column<T> 作为其键。

Last Updated: 2023/01/30, 11:01:00
合理地结合泛型和可变参数
使用枚举类型替代整型常量

← 合理地结合泛型和可变参数 使用枚举类型替代整型常量→

Theme by Vdoing | Copyright © 2019-2023 pursue-wind | 粤ICP备2022093130号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
  • 飙升榜
  • 新歌榜
  • 云音乐民谣榜
  • 美国Billboard榜
  • UK排行榜周榜
  • 网络DJ