119 Star 1K Fork 296

lin-mt / effective-java-third-edition

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
第62项:如果其他类型更适合,则尽量避免使用字符串.md 6.35 KB
一键复制 编辑 原始数据 按行查看 历史
lin-mt 提交于 2020-01-05 10:05 . 修订61、62项

如果其他类型更适合,则尽量避免使用字符串

  字符串被用来表示文本,它在这方面也确实做得很好。因为在很多地方都可以用字符串,并且 Java 语言【对字符串】也支持得很好,因此将字符串用于除设计字符串之外的其他目的的自然倾向【也就是说,设计字符串本来是用来做某些事情的,但是除了这些事情之外,人们也会使用字符串】。本项就是讨论一些不应该使用字符串的情形。

  字符串不适合代替其他的值类型 。当一段数据从文件、网络、或者键盘设备,输入到程序的时候,它通常以字符串的形式存在。有一种自然的倾向是让它继续保留这种形式,但是,只有当这段数据本质上确实是文本信息时,这种想法才是合理的。如果它是数值,就应该被转换为适当的数值类型,比如 int、float 或者 BigInteger 类型。如果它是一个“是-或-否”这种问题的答案,就应该被转换为适当的枚举类型或者 boolean 类型。如果存在适当的值类型,不管是基本类型还是对象引用,大多数应该使用这种类型。如果不存在这样的类型,就应该编写一个类型。虽然这条建议是显而易见的,但【这条建议】却经常被违反。

  字符串不适合代替枚举类型 。正如在 34 项中讨论的,枚举类型比字符串更加适合用来表示枚举类型的常量。

  字符串不适合代替聚集类型 。如果一个实体有多个组件,用一个字符串来表示这个实体通常是很不恰当的,例如,下面这行代码来自真实的系统————标识符的名称已经被修改了,一边发生纠纷:

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

  这种方法有许多缺点。如果用来分隔域的字符也出现在某个域中,就会出现混乱。为了访问单独的域,必须解析该字符串,这个过程非常慢,也很繁琐,还容易出错。你无法提供 equals、toString 或者 compareTo 方法,但是却被迫接受 String 提供的行为。更好的做法是,简单地编写一个类来描述这个数据集,通常是一个私有的静态成员类(第 24 项)。

  字符串也不适合代替能力(capabilities) 。有时候,字符串被用于对某种功能进行授权访问。例如,考虑设计一个线程局部变量(thread-local variable)的机制。这个机制提供的变量在每个线程中都有自己的值。自从 Java 1.2 发行版本以来,Java 类库就有提供线程局部变量的机制,但在那之前,程序猿必须自己完成。几年前面对这样的设计任务时,有些人自己提出了同样的设计方案:利用客户端提供的字符串键,对每个线程局部变量的内容进行访问授权:

// 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 并不难,只要用一个不可伪造的键(unforgeable key,有时被称为能力(capability))来代替字符串即可:

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 的两个问题,但是你还可以做得更好。实际上不再需要静态方法,它们可以被代之以键(Key)中的实例方法,这样这个键就不再是键,而是线程局部变量了。此时,这个不可被实例化的顶层类也不再做任何实质性的工作,因此可以删除这个顶层类,并将内层的嵌套类命名为 ThreadLocal:

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

  这个 API 不是类型安全的,因为当你从线程局部变量得到它时,必须将值从 Object 转换成它实际的值。不可能使原始的基于 String 的 API 是类型安全的,要使基于 Key 的 API 是类型安全的也是很困难,但是,通过将 ThreadLocal 类泛型化(第 29 项),使这个 API 变成类型安全的就是很简单的事情了:

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

  大致地提一下,这正是 java.util.ThreadLocal 提供的 API。除了解决了基于字符串的 API 的问题之外,与前面的两个基于键的 API 相比,它还更快速、更优雅。

  总而言之,如果可以使用更加合适的数据类型,或者可以编写更加适当的数据类型,就应该避免使用字符串来表示对象。若使用不当,字符串会比其他的类型更加笨拙、更加不灵活、速度更慢,也更容易出错。经常被错误地用字符串来代替的类型包括基本类型、枚举类型和聚合类型。

其他
1
https://gitee.com/lin-mt/effective-java-third-edition.git
git@gitee.com:lin-mt/effective-java-third-edition.git
lin-mt
effective-java-third-edition
effective-java-third-edition
master

搜索帮助