1 はじめに
Java 汎用プログラミングは、JDK1.5 バージョンの後に導入されました。ジェネリックを使用すると、プログラマは、多くの場合、コレクション内で型の抽象化を使用できます。以下はジェネリックスを使用しない例です:
List myIntList=new LinkedList(); //1 myIntList.add(newInteger(0)); //2 Integer x=(Integer)myIntList.iterator().next(); //3
コードの 3 行目に注目してください。ただし、これは非常に不快な点です。プログラマは、List に格納されているオブジェクトの型が Integer であることを知っている必要がありますが、 list 内の要素、または型をキャストする必要があります。これはなぜですか?その理由は、コンパイラがイテレータの next() メソッドが Object 型のオブジェクトを返すことのみを保証できるためです。 Integer 変数の型安全性を確保するには、強制的に変換する必要があります。
この種の変換は混乱を招くだけでなく、型変換例外 ClassCastException が発生する可能性もあり、多くの場合検出が困難です。リスト内の要素が特定のデータ型であることを確認して、型変換をキャンセルできるようにし、エラーの可能性を減らします。これはジェネリック設計の本来の目的でもあります。以下はジェネリックスの使用例です:
List<Integer> myIntList=newLinkedList<Integer>(); //1’ myIntList.add(newInteger(0)); //2’ Integerx=myIntList.iterator().next(); //3’
コードの最初の行で、List に格納されているオブジェクトの型が Integer であることを指定します。これにより、リスト内のオブジェクトを取得するときに型をキャストする必要がなくなります。
2 単純なジェネリックスを定義する
以下は、ジェネリック技術を使用する java.util パッケージのインターフェイス List および Iterator から引用した定義です。
public interface List<E> { <span style="white-space: pre;"> </span>void add(E x); <span style="white-space: pre;"> </span>Iterator<E> iterator(); } public interface Iterator<E> { <span style="white-space: pre;"> </span>E next(); <span style="white-space: pre;"> </span>boolean hasNext(); }
これは、インターフェイスの後に山括弧が追加され、山括弧内に型パラメータがあることを除いて、ネイティブ型と変わりません (定義時には書式設定された型パラメータであり、特定の型に置き換えられます) type を呼び出すとき)。
List
public interface IntegerList { <span style="white-space: pre;"> </span>void add(Integer x) <span style="white-space: pre;"> </span>Iterator<Integer> iterator(); }
型の消去とは、型パラメーターのマージを通じてジェネリック型のインスタンスを同じバイトコードに関連付けることを指します。コンパイラはジェネリック型に対して 1 つのバイトコードのみを生成し、そのインスタンスをこのバイトコードに関連付けます。そのため、ジェネリック型の静的変数はすべてのインスタンスで共有されます。さらに、クラスがインスタンス化されていないため、静的メソッドはジェネリック クラスの型パラメーターにアクセスできないため、静的メソッドでジェネリック機能を使用する必要がある場合は、ジェネリック メソッドにする必要があることに注意してください。型消去の鍵は、ジェネリック型から型パラメーターに関する情報を消去し、必要に応じて型チェックおよび型変換メソッドを追加することです。ジェネリックスを使用すると、具象型は消去され、オブジェクトを使用していることだけがわかります。例: List
public class Erasure{ public void test(List<String> ls){ System.out.println("Sting"); } public void test(List<Integer> li){ System.out.println("Integer"); } }
そこで問題が発生します。コンパイル中にメソッドやクラス内の実際の型情報が消去されるため、オブジェクトを返すときに特定の型をどのように知ることができるのでしょうか。たとえば、List
3. ジェネリックとサブタイプ
ジェネリックを完全に理解するために、ここに例を示します: (Apple は Fruit のサブクラスです
)List<Apple> apples = new ArrayList<Apple>(); //1 List<Fruit> fruits = apples; //2
第1行代码显然是对的,但是第2行是否对呢?我们知道Fruit fruit = new Apple(),这样肯定是对的,即苹果肯定是水果,但是第2行在编译的时候会出错。这会让人比较纳闷的是一个苹果是水果,为什么一箱苹果就不是一箱水果了呢?可以这样考虑,我们假定第2行代码没有问题,那么我们可以使用语句fruits.add(new Strawberry())(Strawberry为Fruit的子类)在fruits中加入草莓了,但是这样的话,一个List中装入了各种不同类型的子类水果,这显然是不可以的,因为我们在取出List中的水果对象时,就分不清楚到底该转型为苹果还是草莓了。
通常来说,如果Foo是Bar的子类型,G是一种带泛型的类型,则G
4.通配符
4.1通配符?
先看一个打印集合中所有元素的代码。
void printCollection(Collection c) { <span style="white-space: pre;"> </span>Iterator i=c.iterator(); <span style="white-space: pre;"> </span>for (k=0;k < c.size();k++) { <span style="white-space: pre;"> </span>System.out.println(i.next()); <span style="white-space: pre;"> </span>} }
void printCollection(Collection<Object> c) { for (Object e:c) { System.out.println(e); } }
很容易发现,使用泛型的版本只能接受元素类型为Object类型的集合如ArrayList
//使用通配符?,表示可以接收任何元素类型的集合作为参数 void printCollection(Collection<?> c) { <span style="white-space: pre;"> </span>for (Object e:c) { <span style="white-space: pre;"> </span>System.out.println(e); <span style="white-space: pre;"> </span>} }
这里使用了通配符?指定可以使用任何类型的集合作为参数。读取的元素使用了Object类型来表示,这是安全的,因为所有的类都是Object的子类。这里就又出现了另外一个问题,如下代码所示,如果试图往使用通配符?的集合中加入对象,就会在编译时出现错误。需要注意的是,这里不管加入什么类型的对象都会出错。这是因为通配符?表示该集合存储的元素类型未知,可以是任何类型。往集合中加入元素需要是一个未知元素类型的子类型,正因为该集合存储的元素类型未知,所以我们没法向该集合中添加任何元素。唯一的例外是null,因为null是所有类型的子类型,所以尽管元素类型不知道,但是null一定是它的子类型。
Collection<?> c=new ArrayList<String>(); c.add(newObject()); //compile time error,不管加入什么对象都出错,除了null外。 c.add(null); //OK
另一方面,我们可以从List> lists中获取对象,虽然不知道List中存储的是什么类型,但是可以肯定的是存储的类型一定是Object的子类型,所以可以用Object类型来获取值。如for(Object obj: lists),这是合法的。
4.2边界通配符
1)?extends通配符
假定有一个画图的应用,可以画各种形状的图形,如矩形和圆形等。为了在程序里面表示,定义如下的类层次:
public abstract class Shape { <span style="white-space: pre;"> </span>public abstract void draw(Canvas c); } public class Circle extends Shape { <span style="white-space: pre;"> </span>private int x,y,radius; <span style="white-space: pre;"> </span>public void draw(Canvas c) { ... } } public class Rectangle extends Shape <span style="white-space: pre;"> </span>private int x,y,width,height; <span style="white-space: pre;"> </span>public void draw(Canvasc) { ... } }
为了画出集合中所有的形状,我们可以定义一个函数,该函数接受带有泛型的集合类对象作为参数。但是不幸的是,我们只能接收元素类型为Shape的List对象,而不能接收类型为List
//原始版本 public void drawAll(List<Shape> shapes) { <span style="white-space: pre;"> </span>for (Shapes:shapes) { <span style="white-space: pre;"> </span>s.draw(this); <span style="white-space: pre;"> </span>} }
//使用边界通配符的版本 public void drawAll(List<?exends Shape> shapes) { <span style="white-space: pre;"> </span>for (Shapes:shapes) { <span style="white-space: pre;"> </span>s.draw(this); <span style="white-space: pre;"> </span>} }
这里就又有个问题要注意了,如果我们希望在List<?exends Shape> shapes中加入一个矩形对象,如下所示:
shapes.add(0, new Rectangle()); //compile-time error
那么这时会出现一个编译时错误,原因在于:我们只知道shapes中的元素时Shape类型的子类型,具体是什么子类型我们并不清楚,所以我们不能往shapes中加入任何类型的对象。不过我们在取出其中对象时,可以使用Shape类型来取值,因为虽然我们不知道列表中的元素类型具体是什么类型,但是我们肯定的是它一定是Shape类的子类型。
2)?super通配符
这里还有一种边界通配符为?super。比如下面的代码:
List<Shape> shapes = new ArrayList<Shape>(); List<? super Cicle> cicleSupers = shapes; cicleSupers.add(new Cicle()); //OK, subclass of Cicle also OK cicleSupers.add(new Shape()); //ERROR
这表示cicleSupers列表存储的元素为Cicle的超类,因此我们可以往其中加入Cicle对象或者Cicle的子类对象,但是不能加入Shape对象。这里的原因在于列表cicleSupers存储的元素类型为Cicle的超类,但是具体是Cicle的什么超类并不清楚。但是我们可以确定的是只要是Cicle或者Circle的子类,则一定是与该元素类别兼容。
3)边界通配符总结
l 如果你想从一个数据类型里获取数据,使用 ? extends 通配符
l 如果你想把对象写入一个数据结构里,使用 ? super 通配符
l 如果你既想存,又想取,那就别用通配符。
5.泛型方法
考虑实现一个方法,该方法拷贝一个数组中的所有对象到集合中。下面是初始的版本:
static void fromArrayToCollection(Object[]a, Collection<?> c) { <span style="white-space: pre;"> </span>for (Object o:a) { <span style="white-space: pre;"> </span>c.add(o); //compile time error <span style="white-space: pre;"> </span>} }
可以看到显然会出现编译错误,原因在之前有讲过,因为集合c中的类型未知,所以不能往其中加入任何的对象(当然,null除外)。解决该问题的一种比较好的办法是使用泛型方法,如下所示:
static <T> void fromArrayToCollection(T[] a, Collection<T>c){ <span style="white-space: pre;"> </span>for(T o : a) { <span style="white-space: pre;"> </span>c.add(o);// correct <span style="white-space: pre;"> </span>} }
注意泛型方法的格式,类型参数
Object[] oa = new Object[100]; Collection<Object>co = new ArrayList<Object>(); fromArrayToCollection(oa, co);// T inferred to be Object String[] sa = new String[100]; Collection<String>cs = new ArrayList<String>(); fromArrayToCollection(sa, cs);// T inferred to be String fromArrayToCollection(sa, co);// T inferred to be Object Integer[] ia = new Integer[100]; Float[] fa = new Float[100]; Number[] na = new Number[100]; Collection<Number>cn = new ArrayList<Number>(); fromArrayToCollection(ia, cn);// T inferred to be Number fromArrayToCollection(fa, cn);// T inferred to be Number fromArrayToCollection(na, cn);// T inferred to be Number fromArrayToCollection(na, co);// T inferred to be Object fromArrayToCollection(na, cs);// compile-time error
注意到我们调用方法时并不需要传递类型参数,系统会自动判断类型参数并调用合适的方法。当然在某些情况下需要指定传递类型参数,比如当存在与泛型方法相同的方法的时候(方法参数类型不一致),如下面的一个例子:
public <T> void go(T t) { System.out.println("generic function"); } public void go(String str) { System.out.println("normal function"); } public static void main(String[] args) { FuncGenric fg = new FuncGenric(); fg.go("haha");//打印normal function fg.<String>go("haha");//打印generic function fg.go(new Object());//打印generic function fg.<Object>go(new Object());//打印generic function }
如例子中所示,当不指定类型参数时,调用的是普通的方法,如果指定了类型参数,则调用泛型方法。可以这样理解,因为泛型方法编译后类型擦除,如果不指定类型参数,则泛型方法此时相当于是public void go(Object t)。而普通的方法接收参数为String类型,因此以String类型的实参调用函数,肯定会调用形参为String的普通方法了。如果是以Object类型的实参调用函数,则会调用泛型方法。
6.其他需要注意的小点
1)方法重载
在JAVA里面方法重载是不能通过返回值类型来区分的,比如代码一中一个类中定义两个如下的方法是不容许的。但是当参数为泛型类型时,却是可以的。如下面代码二中所示,虽然形参经过类型擦除后都为List类型,但是返回类型不同,这是可以的。
/*代码一:编译时错误*/ public class Erasure{ public void test(int i){ System.out.println("Sting"); } public int test(int i){ System.out.println("Integer"); } }
/*代码二:正确 */ public class Erasure{ public void test(List<String> ls){ System.out.println("Sting"); } public int test(List<Integer> li){ System.out.println("Integer"); } }
2)泛型类型是被所有调用共享的
所有泛型类的实例都共享同一个运行时类,类型参数信息会在编译时被擦除。因此考虑如下代码,虽然ArrayList
List<String>l1 = new ArrayList<String>(); List<Integer>l2 = new ArrayList<Integer>(); System.out.println(l1.getClass() == l2.getClass()); //True
3)instanceof
不能对确切的泛型类型使用instanceOf操作。如下面的操作是非法的,编译时会出错。
Collection cs = new ArrayList<String>(); if (cs instanceof Collection<String>){…}// compile error.如果改成instanceof Collection<?>则不会出错。
4)泛型数组问题
不能创建一个确切泛型类型的数组。如下面代码会出错。
List
因为如果可以这样,那么考虑如下代码,会导致运行时错误。
List<String>[] lsa = new ArrayList<String>[10]; // 实际上并不允许这样创建数组 Object o = lsa; Object[] oa = (Object[]) o; List<Integer>li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li;// unsound, but passes run time store check String s = lsa[1].get(0); //run-time error - ClassCastException
因此只能创建带通配符的泛型数组,如下面例子所示,这回可以通过编译,但是在倒数第二行代码中必须显式的转型才行,即便如此,最后还是会抛出类型转换异常,因为存储在lsa中的是List
ist<?>[] lsa = new List<?>[10]; // ok, array of unbounded wildcard type Object o = lsa; Object[] oa = (Object[]) o; List<Integer>li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; //correct String s = (String) lsa[1].get(0);// run time error, but cast is explicit Integer it = (Integer)lsa[1].get(0); // OK
更多Java泛型编程最全总结相关文章请关注PHP中文网!