`
Yinny
  • 浏览: 296349 次
  • 性别: Icon_minigender_2
  • 来自: 杭州
社区版块
存档分类
最新评论

泛型的几个注意点!

阅读更多
[size=medium]上周代码里碰到了泛型,使用到了类型参数通配符,平时使用到不多,但是一用到还是会有些模糊,于是想再学习下泛型,网上的文章一抓一大把,但都是你抄我我抄你的,好不容易找到两篇好文,于是整理了些过来。

未经处理的类型和不被检查的警告

即使被重写的Java集合类带来了泛型的好处,在使用他们的时候你也不被要求说明类型变量。一个不带类型变量的泛型类型被认为是一个未经处理的类型(raw type)。这样,5.0版本以前的java代码仍然能够运行:显式的编写所有类型转换就像已经写的一样,你可能会被一些来自编译器的麻烦所困扰。查看下列存储不同类型的对象到一个未经处理的List:
List l = new ArrayList();
l.add("hello"); 
l.add(new Integer(123));
Object o = l.get(0);

这段代码在java1.4下运行得很好。如果您用java5.0来编译它,javac编译了,但是会打印出这样的“抱怨”:
Note: Test.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
如果我们加入-Xlint参数后重新编译,我们会看到这些警告:
Test.java:6: warning: [unchecked]
unchecked call to add(E) as a member of the raw type java.util.List
l.add("hello");
Test.java:7: warning: [unchecked]
unchecked call to add(E) as a member of the raw type java.util.List
l.add(new Integer(123));
编译在add()方法的调用上给出了警告,因为它不能够确信加入到list中的值具有正确的类型。它告诉我们说我们使用了一个未经处理的类型,它不能验证我们的代码是类型安全的。注意,get()方法的调用是没有问题的,因为能够被获得的元素已经安全的存在于list中了。
如果您不想使用任何的java5.0的新特性,您可以简单的通过带-source1.4标记来编译他们,这样编译器就不会再“抱怨”了。如果您不能这样做,您可以忽略这些警告,通过使用一个“@SuppressWarnings("unchecked")”注解(查看本章的4.3节)隐瞒这些警告信息或者升级您的代码,加入类型变量描述。[2]下列示例代码,编译的时候不再会有警告但仍然允许您往list中放入不同的类型的对象。
List<Object> l = new ArrayList<Object>();
l.add("hello"); 
l.add(123);              // autoboxing
Object o = l.get(0);



参数化类型的体系

参数化类型有类型体系,就像一般的类型一样。这个体系基于对象的类型,而不是变量的类型。这里有些例子您可以尝试:
ArrayList<Integer> l = new ArrayList<Integer>();
List<Integer> m = l;                            // okay
Collection<Integer> n = l;                      // okay
ArrayList<Number> o = l;                        // error
Collection<Object> p = (Collection<Object>)l;   // error, even with cast

一个List<Integer>是一个Collection<Integer>,但不是一个List<Object>。这句话不容易理解,如果您想理解为什么泛型这样做,这段值得看一下。考察这段代码:
List<Integer> li = new ArrayList<Integer>();
li.add(123);
// The line below will not compile.  But for the purposes of this
// thought-experiment, assume that it does compile and see how much
// trouble we get ourselves into.
List<Object> lo = li; 
// Now we can retrieve elements of the list as Object instead of Integer
Object number = lo.get(0);
// But what about this?
lo.add("hello world");
// If the line above is allowed then the line below throws ClassCastException
Integer i = li.get(1);  // Can't cast a String to Integer!

这就是为什么List<Integer>不是一个List<Object>的原因,虽然List<Integer>中所有的元素事实上是一个Object的实例。如果允许转换成List<Object>,那么转换后,理论上非整型的对象也将被允许添加到list中。


运行时类型安全

就像我们所见到的,一个List<X>不允许被转换为一个List<Y>,即使这个X能够被转换为Y。然而,一个List<X>能够被转换为一个List,这样您就可以通过继承的方法来做这样的事情。
这种将参数化类型转换为非参数化类型的能力对于向下兼容是必要的,但是它会在泛型所带来的类型安全体系上凿个漏洞:
Java容器的插入方法并不检查类型,所以很可能会插入错误的类型:
List l_raw = new ArrayList<Integer>();
l_raw.add(new Double(3)); // success

Collections提供了检查类型的方法,比如checkedList返回一个在插入时检查类型的List:
List<Integer> l = Collections.checkedList(new ArrayList<Integer>(), Integer.class);
List l_raw = l;
l_raw.add(new Double(3)); // throws exception!

在checked容器里试图插入不合法的类型会抛出异常。
运行了下,结果如下:
Exception in thread "main" java.lang.ClassCastException: Attempt to insert class java.lang.Double element into collection with element type class java.lang.Integer
at java.util.Collections$CheckedCollection.typeCheck(Collections.java:2202)
at java.util.Collections$CheckedCollection.add(Collections.java:2243)
at generic.TestType.main(TestType.java:12)





参数化类型的数组

在使用泛型类型的时候,数组需要特别的考虑。回忆一下,如果T是S的父类(或者接口),那么类型为S的数组S[],同时又是类型为T的数组T[]。正因为如此,每次您存放一个对象到数组中时,Java解释器都必须进行检查以确保您放入的对象类型与要存放的数组所允许的类型是匹对的。例如,下列代码在运行期会检查失败,抛出一个ArrayStoreException异常:
String[] words = new String[10];
Object[] objs = words;
objs[0] = 1;  // 1 autoboxed to an Integer, throws ArrayStoreException

虽然编译时obj是一个Object[],但是在运行时它是一个String[],它不允许被用于存放一个Integer。
当我们使用泛型类型的时候,仅仅依靠运行时的数组存放异常检查是不够的,因为一个运行时进行的检查并不能够获取编译时的类型参数信息。查看下列代码:
List<String>[] wordlists = new ArrayList<String>[10];
ArrayList<Integer> ali = new ArrayList<Integer>();
ali.add(123);
Object[] objs = wordlists;
objs[0] = ali;                       // No ArrayStoreException
String s = wordlists[0].get(0);      // ClassCastException!

如果上面的代码被允许,那么运行时的数组存储检查将会成功:没有编译时的类型参数,代码简单地存储一个ArrayList到一个ArrayList[]数组,非常正确。既然编译器不能阻止您通过这个方法来战胜类型安全,那么它转而阻止您创建一个参数化类型的数组。所以上述情节永远不会发生,编译器在第一行就开始拒绝编译了。
注意这并不是一个在使用数组时使用泛型的全部的约束,这仅仅是一个创建一个参数化类型数组的约束。我们将在学习如何写泛型方法时再来讨论这个话题。


类型参数通配符

假设我们需要写一个方法来显示一个List中的元素。[3]在以前,我们只需要象这样写段代码:
public static void printList(PrintWriter out, List list) {
for(int i=0, n=list.size(); i < n; i++) {
     if (i > 0)
         out.print(", ");
     out.print(list.get(i).toString());
 }
}

在Java5.0中,List是一个泛型类型,如果我们试图编译这个方法,我们将会得到unchecked警告。为了解决这些警告,您可能需要这样来修改这个方法:
public static void printList(PrintWriter out, List<Object> list) {
for(int i=0, n=list.size(); i < n; i++) {
   if (i > 0)
       out.print(", ");
   out.print(list.get(i).toString());
    }
}

这段代码能够编译通过同时不会有警告,但是它并不是非常地有效,因为只有那些被声明为List<Object>的list才会被允许使用这个方法。还记得么,类似于List<String>和List<Integer>这样的List并不能被转型为List<Object>。事实上我们需要一个类型安全的printList()方法,它能够接受我们传入的任何List,而不关心它被参数化为什么。解决办法是使用类型参数通配符。方法可以被修改成这样:
public static void printList(PrintWriter out, List<?> list) {
for(int i=0, n=list.size(); i < n; i++) {
    if (i > 0)
        out.print(", ");
        Object o = list.get(i);
        out.print(o.toString());
}
}

这个版本的方法能够被编译过,没有警告,而且能够在任何我们希望使用的地方使用。通配符“?”表示一个未知类型,类型List<?>被读作“List of unknown”
作为一般原则,如果类型是泛型的,同时您并不知道或者并不关心值的类型,您应该使用“?”通配符来代替一个未经处理的类型。未经处理的类型被允许仅是为了向下兼容,而且应该只能够被允许出现在老的代码中。注意,无论如何,您不能在调用构造器时使用通配符。下面的代码是非法的:
List<?> l = new ArrayList<?>();
创建一个不知道类型的List是毫无道理的。如果您创建了它,那么您必须知道它将保持的元素是什么类型的。您可以在随后的方法中不关心元素类型而去遍历这里list,但是您需要在您创建它的时候描述元素的类型。如果你确实需要一个List来保持任何类型,那么您只能这么写:
List<Object> l = new ArrayList<Object>();
从上面的printList()例子中,必须要搞清楚List<?>既不是List<Object>也不是一个未经处理的List。一个使用通配符的List<?>有两个重要的特性。第一,考察类似于get()的方法,他们被声明返回一个值,这个值的类型是类型参数中指定的。在这个例子中,类型是“unknown”,所以这些方法返回一个Object。既然我们期望的是调用这个object的toString()方法,程序能够很好的满足我们的意愿。
第二,考察List的类似add()的方法,他们被声明为接受一个参数,这个参数被类型参数所定义。出人意料的是,当类型参数是未确定的,编译器不允许您调用任何有不确定参数类型的方法——因为它不能确认您传入了一个恰当的值。一个List(?)实际上是只读的——既然编译器不允许我们调用类似于add(),set(),addAll()这类的方法。


界定通配符

让我们在我们原来的例子上作些小小的稍微复杂一点的改动。假设我们希望写一个sumList()方法来计算list中Number类型的值的合计。在以前,我们使用未经处理的List,但是我们不想放弃类型安全,同时不得不处理来自编译器的unchecked警告。或者我们可以使用List<Number>,那样的话我们就不能调用List<Integer>、List<Double>中的方法了,而事实上我们需要调用。如果我们使用通配符,那么我们实际上不能得到我们期望的类型安全,我们不能确定我们的方法被什么样的List所调用,Number?还是Number的子类?甚至,String?这样的一个方法也许会被写成这样:
public static double sumList(List<?> list) {
double total = 0.0;
for(Object o : list) {
    Number n = (Number) o;  // A cast is required and may fail
    total += n.doubleValue();
}
return total;
}

要修改这个方法让它变得真正的类型安全,我们需要使用界定通配符(bounded wildcard),能够确保List的类型参数是未知的,但又是Number或者Number的子类。下面的代码才是我们想要的:
public static double sumList(List<? extends Number> list) {
double total = 0.0;
for(Number n : list) total += n.doubleValue();
return total;
}

类型List<? extends Number>可以被理解为“Number未知子类的List”。理解这点非常重要,在这段文字中,Number被认为是其自身的子类。
注意,这样的话,那些类型转换已经不再需要了。我们并不知道list中元素的具体类型,但是我们知道他们能够向上转型为Number,因此我们可以把他们从list中把他们当作一个Number对象取出。使用一个for/in循环能够稍微封装一下从list中取出元素的过程。普遍性的原则是当您使用一个界定通配符时,类似于List中的get()方法的那些方法将返回一个类型为上界的值。因此如果我们在for/in循环中调用list.get(),我们将得到一个Number。在前一节说到使用通配符时类似于list.add()这种方法中的限制依然有效:举个例子来说,如果编译器允许我们调用这类方法,我们就可以将一个Integer放到一个声明为仅保持Short值的list中去。
[/size]
分享到:
评论

相关推荐

    泛型集合泛型集合泛型集合

    创建泛型类时,需要注意以下几个方面: 1. **确定类型参数**:你需要决定哪些类型应被抽象为类型参数。越多的类型参数可以使代码更灵活,但也可能增加理解难度。 2. **约束类型参数**:你可以对类型参数施加约束,...

    xe7结构体泛型

    泛型结构体的使用有以下几个优点: 1. **类型安全性**:泛型确保了在编译时就能检测到类型错误,避免了运行时的类型转换。 2. **性能优化**:由于泛型结构体是值类型,它们在内存中直接存储其值,避免了引用类型的...

    java 继承泛型类示例

    在实际开发中,继承泛型类有以下几个关键点需要注意: 1. **类型擦除**:Java的泛型在编译后会被擦除,也就是说在运行时,泛型信息不会存在。因此,泛型主要是在编译时提供类型检查,而非运行时。 2. **类型约束**...

    Java1.5泛型指南中文版

    本文将详细介绍以下几个方面: ### 1. 引言 #### 泛型简介 Java 1.5引入了泛型(Generics)的概念,这是一个重要的语言特性,它允许开发者在编译时期指定集合或其他数据结构中的元素类型,从而避免了运行时期的...

    泛型实际应用(简介)

    这段代码存在几个问题: - `Push` 和 `Pop` 方法使用 `object` 类型作为参数和返回值。 - 强制类型转换时可能出现异常(如果类型不匹配)。 ##### 2. 使用泛型改进栈实现 通过引入泛型,我们可以改进这个栈的实现...

    java泛型的使用

    在Java中,泛型主要体现在以下几个方面: 1. 类:你可以创建泛型类,如`ArrayList&lt;T&gt;`,其中`T`代表一个类型参数。这个`T`可以是任何引用类型,如`String`、`Integer`等。这样,你就可以创建一个特定类型的列表,...

    JVM如何理解Java泛型类.doc

    在创建泛型对象时,需要注意以下几点: 1. **显式指定类型**:当创建泛型对象时,最好明确指定类型参数`T`的具体类型,以便编译器能够进行类型检查。例如: ```java Pair&lt;String&gt; pair = new Pair("a", "b"); ``...

    Java 理论和实践 了解泛型

    在使用泛型时,有以下几点需要注意: 1. 类型参数的声明:在定义泛型类或接口时,使用尖括号`&lt;&gt;`来声明类型参数,例如`class Box&lt;T&gt;`,其中`T`是类型参数,代表某种未知类型。 2. 泛型约束:可以为类型参数设定...

    自定义泛型集合

    下面我们将详细探讨自定义泛型集合的相关知识点。 首先,理解泛型的基本概念。泛型是Java中的模板类或模板方法,它允许我们在定义类、接口或方法时指定一种或多种类型参数。这样,我们就可以创建一个通用的类或方法...

    Java泛型通配符

    Java泛型通配符是Java编程语言中一个重要的特性,它允许我们在定义泛型类型时使用问号(?)作为占位符,表示任意类型的参数。这种通配符的使用大大提高了代码的灵活性和可复用性,同时也帮助开发者遵循强类型检查的...

    JdbcTemplate通用泛型Dao实现

    在实现过程中,我们还需要注意以下几点: 1. **参数绑定**:`JdbcTemplate`支持使用占位符`?`来代替SQL语句中的具体值,通过`PreparedStatement`防止SQL注入。 2. **结果映射**:查询结果通常需要映射到Java对象。...

    Java泛型编程快速入门

    在使用泛型时需要注意以下几点: 1. **定义泛型类**:在定义泛型类时,需要在类名后面的尖括号 `和 `&gt;` 之间定义类型参数。例如 `class JavaGenerics, V&gt;`,这里的 `K` 和 `V` 代表类型而不是具体的值。 2. **...

    清华大学C++进阶讲义:第10章 泛型程序设计与C++标准模板库.pdf

    STL的设计哲学强调泛型程序设计,并围绕几个核心组件构建: 1. **容器**:容器是用来存储数据的对象。STL提供了多种容器类型,如`vector`、`list`、`set`、`map`等,每种容器都有其独特的特性和应用场景。例如,`...

    泛型方法的使用.rar

    在使用泛型方法时,有几个关键点需要注意: - **约束**:你可以对类型参数施加约束,限制它可以是什么类型。例如,可以要求类型参数必须实现某个接口或具有无参构造函数。 - **类型推断**:在调用泛型方法时,如果...

    java泛型md,学习代码

    在使用泛型时,有几点需要注意: 1. **类型擦除**:Java的泛型在编译后会被擦除,因此泛型不会在运行时存在。这意味着无法在运行时检查泛型类型,所有有关泛型的检查都在编译时完成。 2. **通配符**:在需要与多种...

    深入解析C#中的泛型类与泛型接口

    创建泛型类时,应考虑以下几点: 1. **确定类型参数**:选择哪些类成员应使用类型参数,以提高代码的灵活性和可重用性。但过多的泛型化可能导致代码复杂,不易理解。 2. **约束类型参数**:根据实际需求,可以对...

    三十分钟掌握STL(泛型编程)

    - **迭代器类型**:STL定义了几种不同类型的迭代器,包括输入迭代器、输出迭代器、前向迭代器、双向迭代器和随机访问迭代器。每种迭代器支持不同的操作,如递增(`++`)、解引用(`*`)等。 - **迭代器操作**:迭代...

    C#泛型设计需要注意的一个小陷阱

    泛型的出现就是专门解决这个问题的。 但泛型就简单吗?当然不是,继续往下看.. 背景 最近一直在对于公司一个网络通信服务程序使用.net core 进行重构.重构的目的有两个:一是让程序能够跨平台运

    java5泛型

    下面我们分别探讨这几个方面。 ##### 1. 泛化 泛型的基本思想是使用一个类型参数(通常称为泛型参数或类型变量)来代表任意类型的实例。例如,在集合框架中使用泛型可以声明一个可以存储任何类型对象的列表: ```...

Global site tag (gtag.js) - Google Analytics