typedef

    技术2024-02-20  101

    关于Java 5.0中新泛型功能的一个普遍抱怨是,它使代码过于冗长。 过去完全适合一行的变量声明不再有用,并且与声明参数化类型的变量相关联的重复操作可能会令人讨厌,尤其是在没有良好的IDE自动完成支持的情况下。 例如,如果要声明其键是Socket且值是Future<String>的Map , ,老方法:

    Map socketOwner = new HashMap();

    比新方法更紧凑:

    Map<Socket, Future<String>> socketOwner = new HashMap<Socket, Future<String>>();

    当然,新方法嵌入了更多的类型信息,减少了编程错误并提高了程序的可读性,但确实在声明变量和方法签名方面做了大量的前期工作。 在声明和初始化中重复使用类型参数似乎特别不必要; Socket和Future<String> 需要输入两次,这迫使我们违反了“ DRY”原则(请勿重复)。

    合成typedef。 有点

    泛型的添加为类型系统增加了一些复杂性。 在Java 5.0之前,“类型”和“类”几乎是同义的,而参数化类型(尤其是那些有界通配符类型的参数化类型)使子类型和子类的概念大为不同。 类型ArrayList<?> , ArrayList<? extends Number> ArrayList<? extends Number> ,以及ArrayList<Integer> 是不同的类型,即使它们都由相同的类ArrayList 。 这些类型形成层次结构。 ArrayList<?> 是ArrayList<? extends Number>的超类型ArrayList<? extends Number> ArrayList<? extends Number> 以及ArrayList<? extends Number> ArrayList<? extends Number> 是ArrayList<Integer>的超类型ArrayList<Integer> 。

    在最初的简单类型系统中,像C的typedef这样的功能毫无意义。 但是对于更复杂的类型系统,typedef工具可能会带来一些好处。 不管好坏,在泛型添加时,typedef并未添加到语言中。

    某些人用作“穷人的typedef”的惯用语是一个琐碎的扩展:创建一个类来扩展通用类型,但不添加任何功能,例如SocketUserMap类型,如清单1所示:

    清单1.伪typedef反模式-不要这样做
    public class SocketUserMap extends HashMap<Socket, Future<String>> { } SocketUserMap socketOwner = new SocketUserMap();

    我将这个技巧称为伪typedef反模式 ,它实现了将socketOwner定义重新返回一行的(可疑的)目标,但提供的内容很少,最终成为重用和维护的障碍。 (对于具有无参数构造函数以外的其他构造函数的类,派生类还需要声明每个构造函数,因为构造函数不会被继承。)

    伪类型问题

    在C语言中,使用typedef定义新类型更像是宏而不是类型声明。 定义等效类型的Typedef可以彼此自由交换,也可以与原始类型自由交换。 清单2显示了一个定义回调函数的示例,其中在签名中使用了typedef,但是调用方提供了等效类型的回调,并且编译器和运行时非常满意:

    清单2. C中的Typedef示例
    // Define a type called "callback" that is a function pointer typedef void (*Callback)(int); void doSomething(Callback callback) { } // This function conforms to the type defined by Callback void callbackFunction(int arg) { } // So a caller can pass the address of callbackFunction to doSomething void useCallback() { doSomething(&callbackFunction); }

    扩展名不是类型定义

    试图使用伪typedef反模式的Java语言中的等效程序会遇到麻烦。 清单3中的StringList和UserList类型都扩展了一个公共的超类,但是它们不是等效的类型。 这意味着任何要调用lookupAll代码都必须传递StringList ,而不是List<String> 或UserList 。

    清单3.伪类型如何将客户端锁定为使用伪类型
    class StringList extends ArrayList<String> { } class UserList extends ArrayList<String> { } ... class SomeClass { public void validateUsers(UserList users) { ... } public UserList lookupAll(StringList names) { ... } }

    此限制比可能最初出现的更为严格。 在一个小型程序中,它可能并没有多大区别,但是随着程序的变大,始终使用伪类型的要求可能会引起麻烦。 如果变量的类型为StringList ,则不能分配普通的List<String>变量List<String> 之所以这样,是因为List<String> 是StringList的超类型,因此不是StringList 。 正如无法将Object分配给String类型的变量一样,也无法分配List<String> 到StringList类型的变量。 (不过,您可以采用其他方法;例如,可以将StringList分配给List<String>类型的变量,因为List<String>是StringList的超类型。)

    方法参数也是如此。 如果方法参数的类型为StringList ,则不能传递普通的List<String> 对此。 这意味着您根本不能将伪类型用作方法参数,而不要求该方法的每次使用都使用伪类型,这实际上意味着您无法在库API中完全使用伪类型。 而且由于大多数库API都是从本来不想成为库代码的代码中长出来的,因此“此代码仅对我有用,没有其他人会使用它”的借口不是一个好借口(假设您的代码是好代码;如果发臭,您可能是对的)。

    伪型具有传染性

    这种“病毒”性质是使C代码重用成为问题的因素之一。 几乎每个C软件包都具有定义实用程序宏和类型的头文件,这些宏和类型如int32 , boolean , true , false等。 如果您尝试在应用程序中使用多个不对这些常见项目使用相同定义的软件包,则可能需要花相当长的时间在“头文件地狱”中,然后才能编译包含所有头文件的空程序。 编写使用来自不同作者的十几个不同程序包的C应用程序几乎肯定会涉及这种痛苦。 另一方面,对于Java应用程序来说,使用十几个或更多不同的程序包却没有任何麻烦是很常见的。 如果程序包在其API中使用伪类型,那么我们将重新发明一个问题,该问题应该只是痛苦的记忆。

    例如,假设有两个不同的包,每个包都使用伪typedef反模式定义StringList ,如清单4所示,每个包都定义用于对StringList进行操作的实用程序方法。 两个程序包都定义了相同的标识符这一事实已经为我们带来了不便。 客户端程序必须选择一个定义才能导入,并对另一个定义使用完全限定名称。 但是更大的问题是,现在这些包的客户端无法创建可以传递给sortList和reverseList的对象,因为两种不同的StringList类型是不同的类型并且彼此不兼容。 客户现在必须选择使用一个包还是选择另一个包,否则他们必须做很多工作才能在不同种类的StringList之间进行转换。 对于软件包编写者来说,原本是方便的做法已经成为在最有限的情况下以外的所有情况下使用软件包的重要障碍。

    清单4.伪类型的使用如何抑制重用
    package a; class StringList extends ArrayList<String> { } class ListUtilities { public static void sortList(StringList list) { } } package b; class StringList extends ArrayList<String> { } class SomeOtherUtilityClass { public static void reverseList(StringList list) { } } ... class Client { public void someMethod() { StringList list = ...; // Can't do this ListUtilities.sortList(list); SomeOtherUtilityClass.reverseList(list); } }

    伪类型通常太具体了

    伪typedef反模式的另一个问题是它倾向于忽略使用接口定义变量和方法参数类型的好处。 尽管可以将StringList定义为扩展List<String>的接口List<String> 以及扩展ArrayList<String>的具体类型StringArrayList ArrayList<String> 并实现StringList ,大多数伪typedef反模式的用户通常不会达到这个长度,因为此技术的目的主要是简化和缩短类型名称。 结果,API将变得不那么有用,而且更加脆弱,因为它们使用的是ArrayList类的具体类型,而不是诸如List类的抽象类型。

    一个更安全的把戏

    减少声明泛型集合所需的类型数量的更安全的技巧是使用类型推断 。 编译器非常聪明,可以使用程序中嵌入的类型信息来分配类型参数。 如果您定义这样的实用程序方法:

    public static <K,V> Map<K,V> newHashMap() { return new HashMap<K,V>(); }

    您可以使用它来避免两次输入类型参数:

    Map<Socket, Future<String>> socketOwner = Util.newHashMap();

    这种方法之所以有效,是因为编译器可以从调用通用方法newHashMap()的上下文中推断出K和V的值。

    结论

    伪typedef反模式的动机很简单-开发人员想要一种定义更紧凑的类型标识符的方法,尤其是当泛型使类型标识符更加冗长时。 问题在于,这种习语在使用它的代码与该代码的客户端之间产生了紧密的耦合,从而阻碍了重用。 您可能不喜欢泛型类型标识符的冗长,但这不是解决它的方法。


    翻译自: https://www.ibm.com/developerworks/java/library/j-jtp02216/index.html

    Processed: 0.028, SQL: 9