Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

依值类型是一个很有用的概念,它可以使函数的输入或者输出,根据输入的参数性质来产生变化,让强类型系统能类型安全地使用一些动态类型性质。其中一种用法是类型安全的属性列表。我是在 Ktor 和 Netty 的代码中学习到了这种用法。

例子

在以前,Java 的世界里,像 ServletContext 或者一些 PropertyMap,它都是基于两个繁星参数的 Map 来作为存储表(或者说,注册表)。

1
2
3
4
5
6
7
8
9
10
11
12
13

final Map<String, Object> attr = new HashMap<>();

attr.put("key1", new ArrayList<String>());
attr.put("key2", "This is some word");
attr.put("key3", 1234);
//...


// 问题:取出需要 cast
final ArrayList<String> list = (AttaryList<String>) attr.get("key1");
final String str = (String) attr.get("key2");
final Integer num = (Integer) attr.get("key3");

以上的例子,使用了一个 key 为 String 的表来存储各样类型的数值,在取出的时候,就需要显式地作类型转换。
这时候潜在的危险就来了:如果我不知道该 key 对应的含义,使用了错误的类型来转换,那么就会出现 ClassCaseException。而在有 IDE 的编程环境下,单纯一个字符串,无法在智能提示下显现其对应值的类型。在编译器看来,这完全是合法的 ── 显式转换意味着编译器相信编码者的决定,忽略对这句表达式的类型判定,这导致错误要在运行时才能看出来,这是明显的执行错误隐患。

分析

编码者已经作了忽略 Map 的值类型的选择:Map<String, Object>,这告诉编译器“我不在乎 Value 的值,我当它们是 Object”。
而后在取出的时候,值都是 Object,而我们却要求编译器将它看成是我们期望的类型,这是毫无根据的。
要编译器这么做,就需要明确地给出根据,最简单的根据,便是显式的指令:强制转换。

强制转换带来的问题是,它仅仅是告诉编译器不要去理会类型错误,运行时的类型错误是无法避免的。而如果运行时也不在乎,就会出现逻辑冲突,程序运行便不正确,导致非法操作,这在程序执行中是不被允许的。

Key 的作用是从表中索引出对应的数据,Key 不同,Value 也将不同,而在强类型系统中,Value 都有其实际的类型。通过简单的 String 取出的 Object 无类型信息,产生了代码编写问题而违背运行时逻辑的可能性,而这种问题在客观上很难被发现的。针对这情况,可以设计一个机制,在编写时就解决这个问题,并在编译时确保无问题。

实现

我们可以通过创建一种 Key 类型,在编译时有一个类型的参照。并创建一种新的容器,管理对一个 Map 取值时的类型转换操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

public final class AttributeKey<T> {

// 增加一个 label 方便标识这个 key 代表的含义
public final String label;

public AttributeKey(String label) {
this.label = label;
}
}

public final class Attributes {

// Attributes 不在乎 Map 的实现
// map 仅仅是拿来存储数据用的
private final Map<Key<?>, Object> map;

public Attributes(Map<Key<?>, Object> map) {
this.map = map;
}

//
// 将不安全的类型转换操作都包装在这些方法里,避免
// 它们到处都是
//

@SuppressWarnings("unchecked")
public <T> T put(Key<T> key, T value) {
return (T) this.map.put(key, value);
}

@SuppressWarnings("unchecked")
public <T> T get(Key<T> key) {
return (T) this.map.get(key);
}
}

这样,在使用的时候,就可以避免到处转换类型的危险操作了。

1
2
3
4
5
6
7
8
9
10
11
12

// Attributes 的 Map 可使用其他实现,在使用场景上更灵活。
final var attr = new Attributes(new ConcurrentHashMap<>());

final var listKey = new AttributeKey<List<String>>("List");

// 存的时候用 key 固定类型
attr.put(listKey, new ArrayList<>());

// 取的时候,就再也无需类型转换了
final List<String> list = attr.get(listKey);

这里使用的基本技巧就是泛型。

限制

可以看得出来,它是在属性存储无需被序列化的情况下使用的,并不适用于数据传输。
这种用法大多出现在一个程序中需要中央地存储状态的时候使用,像 Netty、Ktor 等等面向过程的场景下就大量出现这种用法。

评论