所有栏目 | 云社区 美国云服务器[国内云主机商]
你的位置:首页 > 云社区 » 正文

如何正确使用Java泛型?

发布时间:2020-04-12 08:49:22

资讯分类:泛型  java  正确  类型  参数  代码
如何正确使用Java泛型?

一个泛型类型通过使用一个或多个类型变量来定义,并拥有一个或多个使用一个类型变量作为一个参数或者返回值的占位符。例如,类型java.util.List<E>是一个泛型类型:一个list,其元素的类型被占位符E描述。这个类型有一个名为add()的方法,被声明为有一个类型为E的参数,同时,有一个get()方法,返回值被声明为E类型。

为了使用泛型类型,你应该为类型变量详细指明实际的类型,形成一个就像List<String>类似的参数化类型。[1]指明这些额外的类型信息的原因是编译器据此能够在编译期为您提供很强的类型检查,增强您的程序的类型安全性。举个例子来说,您有一个只能保持String对象的List,那么这种类型检查就能够阻止您往里面加入String[]对象。同样的,增加的类型信息使编译器能够为您做一些类型转换的事情。比如,编译器知道了一个List<String>有个get()方法,其返回值是一个String对象,因此您不再需要去将返回值由一个Object强制转换为String。

Java.util包中的集合类在java5.0中已经被做成了泛型,也许您将会在您的程序中频繁的使用到他们。类型安全的集合类就是一个泛型类型的典型案例。即便您从没有定义过您自己的泛型类型甚至从未用过除了java.util中的集合类以外的泛型类型,类型安全的集合类的好处也是极有意义的一个标志——他们证明了这个主要的新语言特性的复杂性。

我们从探索类型安全的集合类中的基本的泛型用法开始,进而研究更多使用泛型类型的复杂细节。然后我们讨论类型参数通配符和有界通配符。描绘了如何使用泛型以后,我们阐明如何编写自己的泛型类型和泛型方法。我们对于泛型的讨论将结束于一趟对于JavaAPI的核心中重要的泛型类型的旅行。这趟旅程将探索这些类型以及他们的用法,旅程的目的是为了让您对泛型如何工作这个问题有个深入的理解。

类型安全集合类

Java.util类包包含了Java集合框架(Java Collections Framework),这是一批包含对象的set、对象的list以及基于key-value的map。第五章将谈到集合类。这里,我们讨论的是在java5.0中集合类使用类型参数来界定集合中的对象的类型。这个讨论并不适合java1.4或更早期版本。如果没有泛型,对于集合类的使用需要程序员记住每个集合中元素的类型。当您在java1.4种创建了一个集合,您知道您放入到集合中的对象的类型,但是编译器不知道。您必须小心地往其中加入一个合适类型的元素,当需要从集合中获取元素时,您必须显式的写强制类型转换以将他们从Object转换为他们真是的类型。考察下边的java1.4的代码。

public static void main(String[] args) {

// This list is intended to hold only strings.

// The compiler doesn't know that so we have to remember ourselves.

List wordlist = new ArrayList();

// Oops! We added a String[] instead of a String.

// The compiler doesn't know that this is an error.

wordlist.add(args);

// Since List can hold arbitrary objects, the get() method returns

// Object. Since the list is intended to hold strings, we cast the

// return value to String but get a ClassCastException because of

// the error above.

String word = (String)wordlist.get(0);

}

泛型类型解决了这段代码中的显示的类型安全问题。Java.util中的List或是其他集合类已经使用泛型重写过了。就像前面提到的, List被重新定义为一个list,它中间的元素类型被一个类型可变的名称为E的占位符描述。Add()方法被重新定义为期望一个类型为E的参数,用于替换以前的Object,get()方法被重新定义为返回一个E,替换了以前的Object。

在java5.0中,当我们申明一个List或者创建一个ArrayList的实例的时候,我们需要在泛型类型的名字后面紧跟一对“<>”,尖括号中写入我们需要的实际的类型。比如,一个保持String的List应该写成“List<String>”。需要注意的是,这非常象给一个方法传一个参数,区别是我们使用类型而不是值,同时使用尖括号而不是圆括号

Java.util的集合类中的元素必须是对象化的,他们不能是基本类型。泛型的引入并没有改变这点。泛型不能使用基本类型:我们不能这样来申明——Set<char>或者List<int>。记住,无论如何,java5.0中的自动打包和自动解包特性使得使用Set<Character>或者List<Integer>和直接使用char和int值一样方便。(查看第二章以了解更多关于自动打包和自动解包的细节)。

在Java5.0中,上面的例子将被重写为如下方式:

public static void main(String[] args) {

// This list can only hold String objects

List<String> wordlist = new ArrayList<String>();

// args is a String[], not String, so the compiler won't let us do this

wordlist.add(args); // Compilation error!

// We can do this, though.

// Notice the use of the new for/in looping statement

for(String arg : args) wordlist.add(arg);

// No cast is required. List<String>.get() returns a String.

String word = wordlist.get(0);

}

值得注意的是代码量其实并没有比原来那个没有泛型的例子少多少。使用“(String)”这样的类型转换被替换成了类型参数“<String>”。 不同的是类型参数需要且仅需要声明一次,而list能够被使用任何多次,不需要类型转换。在更长点的例子代码中,这一点将更加明显。即使在那些看上去泛型语法比非泛型语法要冗长的例子里,使用泛型依然是非常有价值的——额外的类型信息允许编译器在您的代码里执行更强的错误检查。以前只能在运行起才能发现的错误现在能够在编译时就被发现。此外,以前为了处理类型转换的异常,我们需要添加额外的代码行。如果没有泛型,那么当发生类型转换异常的时候,一个ClassCastException异常就会被从实际代码中抛出。

就像一个方法可以使用任意数量的参数一样,类允许使用多个类型变量。接口Java.util.Map就是一个例子。一个Map体现了从一个key的对象到一个value的对象的映射关系。接口Map申明了一个类型变量来描述key的类型而另一个类型变量来描述value的类型。举个例子来说,假设您希望做一个String对象到Integer对象的映射关系:

public static void main(String[] args) {

// A map from strings to their position in the args[] array

Map<String,Integer> map = new HashMap<String,Integer>();

// Note that we use autoboxing to wrap i in an Integer object.

for(int i=0; i < args.length; i++) map.put(args[i], i);

// Find the array index of a word. Note no cast is required!

Integer position = map.get("hello");

// We can also rely on autounboxing to convert directly to an int,

// but this throws a NullPointerException if the key does not exist

// in the map

int pos = map.get("world");

}

象List<String>这个一个参数类型其本身也是也一个类型,也能够被用于当作其他类型的一个类型变量值。您可能会看到这样的代码:

// Look at all those nested angle brackets!

Map<String, List<List<int[]>>> map = getWeirdMap();

// The compiler knows all the types and we can write expressions

// like this without casting. We might still get NullPointerException

// or ArrayIndexOutOfBounds at runtime, of course.

int value = map.get(key).get(0).get(0)[0];

// Here's how we break that expression down step by step.

List<List<int[]>> listOfLists = map.get(key);

List<int[]> listOfIntArrays = listOfLists.get(0);

int[] array = listOfIntArrays.get(0);

int element = array[0];

在上面的代码里,java.util.List<E>和java.util.Map<K,V>的get()方法返回一个类型为E的list元素或者一个类型为V的map元素。注意,无论如何,泛型类型能够更精密的使用他们的变量。在本书中的参考章节查看List<E>,您将会看到它的iterator( )方法被声明为返回一个Iterator<E>。这意味着,这个方法返回一个跟list的实际的参数类型一样的一个参数类型的实例。为了具体的说明这点,下面的例子提供了不使用get(0)方法来获取一个List<String>的第一个元素的方法。

List<String> words = // ...initialized elsewhere...

Iterator<String> iterator = words.iterator();

String firstword = iterator.next();

理解泛型类型

本段将对泛型类型的使用细节做进一步的探讨,以尝试说明下列问题:

不带类型参数的使用泛型的后果

参数化类型的体系

一个关于编译期泛型类型的类型安全的漏洞和一个用于确保运行期类型安全的补丁

为什么参数化类型的数组不是类型安全的

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

即使被重写的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,这样您就可以通过继承的方法来做这样的事情。

这种将参数化类型转换为非参数化类型的能力对于向下兼容是必要的,但是它会在泛型所带来的类型安全体系上凿个漏洞:

// Here's a basic parameterized list.

List<Integer> li = new ArrayList<Integer>();

// It is legal to assign a parameterized type to a nonparameterized variable

List l = li;

// This line is a bug, but it compiles and runs.

// The Java 5.0 compiler will issue an unchecked warning about it.

// If it appeared as part of a legacy class compiled with Java 1.4, however,

// then we'd never even get the warning.

l.add("hello");

// This line compiles without warning but throws ClassCastException at runtime.

// Note that the failure can occur far away from the actual bug.

Integer i = li.get(0);

泛型仅提供了编译期的类型安全。如果您使用java5.0的编译器来编译您的代码并且没有得到任何警告,这些编译器的检查能够确保您的代码在运行期也是类型安全的。如果您获得了警告或者使用了像未经处理的类型那样修改您的集合的代码,那么您需要增加一些步骤来确保运行期的类型安全。您可以通过使用java.util.Collections中的checkedList()和checkedMap( )方法来做到这一步。这些方法将把您的集合打包成一个wrapper集合,从而在运行时检查确认只有正确类型的值能够被置入集合众。下面是一个能够补上类型安全漏洞的一个例子:

// Here's a basic parameterized list.

List<Integer> li = new ArrayList<Integer>();

// Wrap it for runtime type safety

List<Integer> cli = Collections.checkedList(li, Integer.class);

// Now widen the checked list to the raw type

List l = cli;

// This line compiles but fails at runtime with a ClassCastException.

// The exception occurs exactly where the bug is, rather than far away

l.add("hello");

参数化类型的数组

在使用泛型类型的时候,数组需要特别的考虑。回忆一下,如果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(

留言与评论(共有 0 条评论)
   
验证码:
Top