美文网首页Java 杂谈
Effective-java 3 中文翻译系列 (Item 26

Effective-java 3 中文翻译系列 (Item 26

作者: TryEnough | 来源:发表于2018-08-09 22:02 被阅读81次

文章也上传到

github

(欢迎关注,欢迎大神提点。)


ITEM26 不要使用原始类型

从Java5开始引入范型。
在没有范型的时候,如果有人不小心将错误的类型加入到collection中,就会造成运行时的错误。
有了范型之后,你能告诉编译器,哪种类型被允许加入到collection 中,而且能在编译期间就发现错误。这个系列的文章会告诉你如何更优雅的使用范型。

首先,有几个术语解释一下。一个类或者接口如果声明了一个或多个类型参数,就可以被叫做范型类或范型接口。例如,List接口有一个类型参数:E,代表元素的类型,接口的全名是:List<E>.读作E列表。但是大多数人简单读作列表。范型类和范型接口被统称为范型类型。
每个范型类型的组成是在类名或者接口名后面跟着一对尖括号,里面是真实参数类型对应范型类型的列表。例如List<String>(读作string列表)代表的是每一个元素都是String类型的list(String是对应形参E的真实参数类型)。
每一个范型都定义了一个原始类型,如List<E>中的List(它们的存在主要是为了兼容范型之前的代码)。


// 邮票集合. 只能存储邮票实例.
private final Collection stamps = ... ;

如果你这样声明而且错误的放了一个不是邮票的实例进入到这个集合中,编译和运行期间并不会报错:

//错误的放一个coin类型实例进入邮票集合中
stamps.add(new Coin( ... )); // Emits "unchecked call" warning

直到你尝试从集合中取出这个coin时才会报错:

for (Iterator i = stamps.iterator(); i.hasNext();)
    Stamp stamp = (Stamp) i.next(); // 抛出异常ClassCastException
stamp.cancel();

但是我们的原则是越早发现问题越好,最好是在编译期就能发现问题。在上面这种情况下,在运行时也只能到执行到上面代码时才发生错误。而且编译器不会告诉你是因为添加了coin进入stamps导致的,它仅能告诉你“Contains only Stamp instances.”。

如果使用范型的话,可以指定类型:

private final Collection<Stamp> stamps = ... ;

编译期就知道stamps只能包含stamp类型的实例并且保证这个规则是被满足的。如果插入其他类型的实例(比如coin),编译器会告诉你发生了错误:

Test.java:9: error: incompatible types: Coin cannot be converted
to Stamp
c.add(new Coin());
       ^

编译器会隐含的添加强转的逻辑,并保证它们不会失败。把coin加入stamp集合的例子虽然看起来不太恰当,但这也确实会发生:例如很容易会把BitInteger对象放到BitDecimal集合中。

即使使用原始类型是合法的,但是你也尽量不要这样做。因为你是使用原始类型会失去使用范型的安全性和表达的便利性。 既然你不应该使用它们,那么为什么语言还要设计允许你使用呢?答案是为了兼容性。java出现范型的时候是它被发明出来的十年后,当时已经存在大量的代码没有使用范型,所以老的代码应该是合法的,并且老代码也应该是可以和范型正常交互的,方法可以正常的传递真实类型参数,反之亦然。这被叫做迁移兼容性

即使你不使用像List一样的具体真实类型,你也可以很方便的使用参数化类型来允许插入任何实例类型,像List<Object>。那么原始类型List和List<Object>有什么不同呢?简单来说,后者明确的告诉编译器它能添加对象类型的参数。你能将List<String>传递到List,但你不能将其传递到List<Object>。范型是有子类型规则的,List<String>是List的子类型,但是不是List<Object>的子类(Item28)。总结,使用像List的原始类型,就会失去类型安全性。具体说明请看例子:

// Fails at runtime - unsafeAdd method uses a raw type
(List)!
public static void main(String[] args) {
    List<String> strings = new ArrayList<>();
    unsafeAdd(strings, Integer.valueOf(42));
    String s = strings.get(0); // Has compiler-generated cast
}

private static void unsafeAdd(List list, Object o) {
    list.add(o);
}

程序可以编译,但是会收到警告:


Test.java:10: warning: [unchecked] unchecked call to add(E) as a
member of the raw type List
list.add(o);
       ^

实际上,运行时当程序尝试执行strings.get(0)时,你会得到ClassCastException异常。因为要将Integer转换成String报错。如果你把List替换成List<Object>然后重新编译程序,你将会发现不能编译通过而是发生错误。

Test.java:5: error: incompatible types: List<String> cannot be
converted to List<Object>
unsafeAdd(strings, Integer.valueOf(42));
    ^

你可能想使用原始类型作为集合,包含一些未知类型的对象。例如,假如你想写一个方法返回两个sets中所共有的元素个数,代码如下:


// Use of raw type for unknown element type - don't do
this!
static int numElementsInCommon(Set s1, Set s2) {
    int result = 0;
    for (Object o1 : s1)
        if (s2.contains(o1))
            result++;
        
    return result;
}   

这样的方法可以正常工作,但是是危险的。更安全的方法是使用无限制通配符类型
如果当你想使用范型,但是你又不知道或者不确定真实类型是什么,你可以使用一个问号标志代替。例如,Set<E>的无限制通配符号类型是Set<?>,读作某种类型的set。这是一种可用范围更广的Set类型,可以包含任何set。将numElementsInCommon方法声明称无限制通配符类型是:

// Uses unbounded wildcard type - typesafe and flexible
static int numElementsInCommon(Set<?> s1, Set<?> s2) { ... }

Set<?>和Set类型的区别是什么呢?
通配符是更加安全的。因为你可以将任何原始类型元素放入到一个集合中,这样会很容易的破坏集合类型的不变性(就像上面的unsafeAdd一样)。但是你不能添加任何元素(除了null)外到Collection<?>。尝试这么做会在编译期间报错:

WildCard.java:13: error: incompatible types: String cannot be
converted to CAP#1
c.add("verboten");
    ^
where CAP#1 is a fresh type-variable:
CAP#1 extends Object from capture of ?

很明显这个错误已经提示了一些期望的信息,而且编译器也完成了它的使命,防止了你去破坏集合的不可变性。不但防止了你放入一个除了null之外的元素进入Collection<?>,而且不能确定你从集合中取出的是何种类型。如果这些限制对你来说不可接受,那么你就可以使用范型方法(Item30)或者无限制的通配符号类型(Item31)。

但是对于这条限制也有一些特殊的例外情况你必须使用原始类型。

  • 字面值类型
  • 使用instanceof
if (o instanceof Set) { // Raw type
    Set<?> s = (Set<?>) o; // Wildcard type
    ...
}

快速回顾一下:

  • Set<Object>代表可以存放任何对象类型的set;
  • Set<?>表示可以存放任何未知对象类型的set;
  • Set是原始类型,根据范型系统输出。
    前两个是安全的,最后一个不是。

最后介绍下术语:

术语 例子
参数化类型 List<String)
真实类型 String
范型 List<E>
正式类型参数 E
无限制通配符类型 List<?>
原始类型 List
有限制类型参数 <E extends Number>
递归类型边界 <T extends Comparable<T>>
有限制通配符类型 List<? extends Number>
范型方法 static <E> List<E> asList(E[] a)

相关文章

网友评论

    本文标题:Effective-java 3 中文翻译系列 (Item 26

    本文链接:https://www.haomeiwen.com/subject/xjvsvftx.html