Effective Java 第三版——62. 当有其他更合适的类型时就不用字符串

Tips
书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code
注意,书中的有些代码里方法是基于Java 9 API中的,所以JDK 最好下载 JDK 9以上的版本。

Effective Java, Third Edition

62. 当有其他更合适的类型时就不用字符串

字符串被设计用来表示文本,它们在这方面做得很好。因为字符串是如此常见,并且受到开发语言的良好支持,所以很自然地倾向会将字符串用于其他目的,而不是它们设计的原本目的。这一条目讨论了一些不应该使用字符串做的事情。

字符串是其他值类型的不良替代品。当一段数据从文件、网络或键盘输入进入程序时,它通常是字符串形式的。有一种自然的倾向是这样的,但是这种倾向只有在数据本质上是文本的情况下才合理。如果是数值类型,则应将其转换为适当的数值类型,如int、float或BigInteger。如果是“是”或“否”问题的答案,则应将其转换为适当的枚举类型或boolean值。更通常地说,如果有合适的值类型,无论是基本值还是对象引用,都应该使用它;如果没有,你应该编写一个。虽然这条建议似乎很明显,但经常被违反。

字符串是枚举类型的不良替代品。 正如条目 34中所讨论的,枚举使得枚举类型常量比字符串好得多。

字符串是聚合类型的不良替代品。 如果实体具有多个组件,则将其表示为单个字符串通常是个坏主意。 例如,这里是来自真实系统的一行代码——标识符名称已被更改:

// Inappropriate use of string as aggregate type
String compoundKey = className + "#" + i.next();

这种方法有许多缺点。 如果用于分隔属性的字符出现在某个属性中,结果可能会产生混乱。 要访问单个属性,必须解析字符串,这很慢,很乏味且容易出错。 不能提供equals,toString或compareTo方法,但必须接受String类提供的行为。 更好的方法是编写一个类来表示聚合,通常是私有静态成员类(条目 24)。

字符串是功能的不良替代品。 有时,字符串用于授予对某些功能的访问权限。 例如,考虑ThreadLocal的设计。 这样的工具提供了每个线程都有自己值的变量。 从版本1.2开始,Javal类库就有了一个ThreadLocal工具,但在此之前,程序员必须自己动手来实现。 当多年前遇到设计这样一个工具的任务时,几个人独立地想出了相同的设计,其中客户提供的字符串键用于识别每个线程局部变量:

// Broken - inappropriate use of string as capability!
public class ThreadLocal {
    private ThreadLocal() { } // Noninstantiable

    // Sets the current thread's value for the named variable.
    public static void set(String key, Object value);

    // Returns the current thread's value for the named variable.
    public static Object get(String key);
}

这种方法的问题是,字符串键表示线程本地变量的共享全局命名空间。为了使这种方法有效,客户端提供的字符串键必须是惟一的;如果两个客户端各自决定为它们的线程本地变量使用相同的名称,它们无意中共享一个变量,这通常会导致两个客户端都失败。而且,安全性很差。恶意客户端可以故意使用与另一个客户端相同的字符串密钥来非法访问另一个客户机端数据。

可以通过用一个不可伪造的键(有时称为功能)替换字符串来修复这个API:

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);
}

虽然这解决了基于字符串的API的这两个问题,但是可以做得更好。不再真正需要静态方法。它们可以变成键上的实例方法,此时不再是线程局部变量的键:而是线程局部变量。此时,顶层类不再做任何事情,可以删除它,并将嵌套类重命名为ThreadLocal:

public final class ThreadLocal {
    public ThreadLocal();
    public void set(Object value);
    public Object get();
}

此API不是类型安全的,因为当从线程局部变量中检索它时,必须将值从Object转换为其实际类型。原始的基于字符串的API类型安全是不可能实现的,基于键的API类型安全也是很难实现的,但通过使ThreadLocal成为参数化类(第29项)来使这种API类型安全是一件简单的事情:

public final class ThreadLocal<T> {
    public ThreadLocal();
    public void set(T value);
    public T get();
}

粗略地说,这是java.lang.ThreadLocal提供的API。 除了解决基于字符串的API的问题之外,它还比任何基于键的API更快,更优雅。

总而言之,当存在或可以编写更好的数据类型时,避免将对象表示为字符串的自然倾向。 使用不当,字符串比其他类型更麻烦,更灵活更差,速度更慢,更容易出错。 字符串通常被滥用的类型包括基本类型,枚举类型和聚合类型。

posted @ 2019-03-23 20:40  林本托  阅读(669)  评论(0编辑  收藏  举报