1. ジェネリックの概念の導入 (なぜジェネリックが必要なのか)
まず、次の短いコードを見てみましょう:
public class GenericTest { public static void main(String[] args) { List list = new ArrayList(); list.add("qqyumidi"); list.add("corn"); list.add(100); for (int i = 0; i < list.size(); i++) { String name = (String) list.get(i); // 1 System.out.println("name:" + name); } } }
は List 型のコレクションを定義し、最初に 2 つの文字列型の値をそれに追加し、次に Integer 型の値を追加します。リストのデフォルトのタイプはオブジェクトであるため、これは完全に許可されます。後続のループでは、前に整数型の値をリストに追加するのを忘れたり、その他のエンコードの理由により、//1 と同様のエラーが発生しやすくなります。コンパイル段階は正常ですが、実行時に「java.lang.ClassCastException」例外が発生するためです。したがって、このようなエラーはコーディング中に検出することが困難です。
Java SE 1.5 より前では、ジェネリックスが存在しないため、「arbitrary」パラメータは型 Object への参照を通じて実装されていました。「arbitrary」の欠点は、明示的な強制的な型変換が必要であり、この変換について開発者が知っておく必要があることでした。実際のパラメータの型は事前に設定してください。強制的な型変換エラーの場合、コンパイラはエラーを表示しない可能性があり、実行時に例外が発生します。これはセキュリティ上のリスクです。ジェネリックの利点は、コンパイル中に型安全性がチェックされ、すべてのキャストが自動かつ暗黙的であるため、コードの再利用が向上することです。
2. ジェネリックとは何ですか?
ジェネリックは Java SE 1.5 の新機能です。ジェネリックの本質はパラメータ化された型であり、操作されるデータ型がパラメータとして指定されることを意味します。このパラメータ タイプは、それぞれジェネリック クラス、ジェネリック インターフェイス、ジェネリック メソッドと呼ばれるクラス、インターフェイス、およびメソッドの作成に使用できます。では、パラメーター化された型をどのように理解すればよいでしょうか?名前が示すように、型はメソッド内の変数パラメーターと同様に、元の特定の型からパラメーター化されます。このとき、型はパラメーター形式 (型パラメーターと呼ぶこともできます) としても定義されます。 type (type 引数) を使用または呼び出すときに、特定の型が渡されます。
少し複雑に思えますが、まず、上記の例の一般的な書き方を見てみましょう。
public class GenericTest { public static void main(String[] args) { /* List list = new ArrayList(); list.add("qqyumidi"); list.add("corn"); list.add(100); */ List<String> list = new ArrayList<String>(); list.add("qqyumidi"); list.add("corn"); //list.add(100); // 1 提示编译错误 for (int i = 0; i < list.size(); i++) { String name = list.get(i); // 2 System.out.println("name:" + name); } } }
汎用的な書き込み方法を採用した後、//1 に Integer 型のオブジェクトを追加しようとするとコンパイル エラーが発生します。Listf7e83be87db5cd2d9a8a0b8117b38cd4 により、リスト コレクションには String 型の要素のみを含めることが直接制限されるため、 /2 で強制的な型変換を実行する必要はありません。この時点で、コレクションは要素の型情報を記憶でき、コンパイラはそれが String 型であることをすでに確認できるからです。
上記の一般的な定義と組み合わせると、Listf7e83be87db5cd2d9a8a0b8117b38cd4 では、String が型の実パラメータであることがわかります。つまり、対応する List インターフェイスには型パラメータが含まれている必要があります。また、 get() メソッドの戻り結果も、この仮パラメータの型 (つまり、対応する受信型の実パラメータ) がそのまま返されます。 List インターフェイスの具体的な定義を見てみましょう:
public interface List<E> extends Collection<E> { int size(); boolean isEmpty(); boolean contains(Object o); Iterator<E> iterator(); Object[] toArray(); <T> T[] toArray(T[] a); boolean add(E e); boolean remove(Object o); boolean containsAll(Collection<?> c); boolean addAll(Collection<? extends E> c); boolean addAll(int index, Collection<? extends E> c); boolean removeAll(Collection<?> c); boolean retainAll(Collection<?> c); void clear(); boolean equals(Object o); int hashCode(); E get(int index); E set(int index, E element); void add(int index, E element); E remove(int index); int indexOf(Object o); int lastIndexOf(Object o); ListIterator<E> listIterator(); ListIterator<E> listIterator(int index); List<E> subList(int fromIndex, int toIndex); }
List インターフェイスで汎用定義を採用した後、1a4db2c2c2313771e5742b6debf617a1 の E が型パラメータを表し、特定の型の実パラメータを受け取ることができることがわかります。そして、このインターフェイス定義では、E が現れる場所はどこでも、外部から受け取った同じタイプの実際のパラメータを表します。
当然ですが、ArrayList は List インターフェースの実装クラスであり、その定義形式は次のとおりです。
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } public E get(int index) { rangeCheck(index); checkForComodification(); return ArrayList.this.elementData(offset + index); } //...省略掉其他具体的定义过程 }
このことから、//1 に Integer 型オブジェクトを追加するとコンパイルエラーが発生する理由がソースコードの観点から理解できます。 get( at //2 ) は直接 String 型です。
3. タプルクラスライブラリとは何ですか?そしてその使用方法は何ですか?
なぜタプルを使用するのですか?
タプルはリストと同様にデータの保存に使用でき、複数のデータを含めることができますが、リストとは異なります。リストは同じデータ型のみを保存できますが、タプルは異なり、int、string の保存など、異なるデータ型を保存できます。 、リストなどを同時に実行でき、ニーズに応じて無限に拡張できます。
たとえば、Web アプリケーションでよく発生する問題は、データ ページングの問題です。ページングのクエリには、現在のページ番号とページ サイズなどの情報が含まれている必要があります。現在のページ、ページサイズ、総ページ数およびその他の情報がフロントデスクに表示される場合、別の情報が必要です。データレコードの総数、次に総ページ数およびその他の情報です。上記の情報に基づいて計算されます。このとき、特定のページの情報をクエリする場合、list (現在のデータ レコード) と int (レコードの総数) の 2 つのデータ型を返す必要があります。もちろん、これら 2 つの値は 2 つの方法と 2 つのデータベース接続で取得できます。実際、リストをクエリするとき、レコードの総数は SQL クエリを通じて取得されていますが、別のメソッドを開いて別のデータベース接続を作成してレコードの総数をクエリすると、少し不必要になり、時間の無駄になります。コードの無駄、そして人生の無駄です。真面目な話 ~ この場合、タプルを使用して 1 つのデータベース接続内のレコードの総数と現在のページのレコードを取得し、そこに格納することができます。これは単純明快です。
4. カスタマイズされた汎用インターフェイス、汎用クラス、および汎用メソッド
从上面的内容中,大家已经明白了泛型的具体运作过程。也知道了接口、类和方法也都可以使用泛型去定义,以及相应的使用。是的,在具体使用时,可以分为泛型接口、泛型类和泛型方法。
自定义泛型接口、泛型类和泛型方法与上述Java源码中的List、ArrayList类似。如下,我们看一个最简单的泛型类和方法定义:
public class GenericTest { public static void main(String[] args) { Box<String> name = new Box<String>("corn"); System.out.println("name:" + name.getData()); } } class Box<T> { private T data; public Box() { } public Box(T data) { this.data = data; } public T getData() { return data; } }
在泛型接口、泛型类和泛型方法的定义过程中,我们常见的如T、E、K、V等形式的参数常用于表示泛型形参,由于接收来自外部使用时候传入的类型实参。那么对于不同传入的类型实参,生成的相应对象实例的类型是不是一样的呢?
public class GenericTest { public static void main(String[] args) { Box<String> name = new Box<String>("corn"); Box<Integer> age = new Box<Integer>(712); System.out.println("name class:" + name.getClass()); // com.qqyumidi.Box System.out.println("age class:" + age.getClass()); // com.qqyumidi.Box System.out.println(name.getClass() == age.getClass()); // true } }
由此,我们发现,在使用泛型类时,虽然传入了不同的泛型实参,但并没有真正意义上生成不同的类型,传入不同泛型实参的泛型类在内存上只有一个,即还是原来的最基本的类型(本实例中为Box),当然,在逻辑上我们可以理解成多个不同的泛型类型。
究其原因,在于Java中的泛型这一概念提出的目的,导致其只是作用于代码编译阶段,在编译过程中,对于正确检验泛型结果后,会将泛型的相关信息擦出,也就是说,成功编译过后的class文件中是不包含任何泛型信息的。泛型信息不会进入到运行时阶段。
五. 类型通配符
接着上面的结论,我们知道,Boxc8f01a3f8889dcf657849dd45bc0fc4c和Boxc0f559cc8d56b43654fcbe4aa9df7b4a实际上都是Box类型,现在需要继续探讨一个问题,那么在逻辑上,类似于Boxc8f01a3f8889dcf657849dd45bc0fc4c和Boxc0f559cc8d56b43654fcbe4aa9df7b4a是否可以看成具有父子关系的泛型类型呢?
为了弄清这个问题,我们继续看下下面这个例子:
public class GenericTest { public static void main(String[] args) { Box<Number> name = new Box<Number>(99); Box<Integer> age = new Box<Integer>(712); getData(name); //The method getData(Box<Number>) in the type GenericTest is //not applicable for the arguments (Box<Integer>) getData(age); // 1 } public static void getData(Box<Number> data){ System.out.println("data :" + data.getData()); } }
我们发现,在代码//1处出现了错误提示信息:The method getData(Boxc8f01a3f8889dcf657849dd45bc0fc4c) in the t ype GenericTest is not applicable for the arguments (Boxc0f559cc8d56b43654fcbe4aa9df7b4a)。显然,通过提示信息,我们知道Boxc8f01a3f8889dcf657849dd45bc0fc4c在逻辑上不能视为Boxc0f559cc8d56b43654fcbe4aa9df7b4a的父类。那么,原因何在呢?
public class GenericTest { public static void main(String[] args) { Box<Integer> a = new Box<Integer>(712); Box<Number> b = a; // 1 Box<Float> f = new Box<Float>(3.14f); b.setData(f); // 2 } public static void getData(Box<Number> data) { System.out.println("data :" + data.getData()); } } class Box<T> { private T data; public Box() { } public Box(T data) { setData(data); } public T getData() { return data; } public void setData(T data) { this.data = data; } }
这个例子中,显然//1和//2处肯定会出现错误提示的。在此我们可以使用反证法来进行说明。
假设Boxc8f01a3f8889dcf657849dd45bc0fc4c在逻辑上可以视为Boxc0f559cc8d56b43654fcbe4aa9df7b4a的父类,那么//1和//2处将不会有错误提示了,那么问题就出来了,通过getData()方法取出数据时到底是什么类型呢?Integer? Float? 还是Number?且由于在编程过程中的顺序不可控性,导致在必要的时候必须要进行类型判断,且进行强制类型转换。显然,这与泛型的理念矛盾,因此,在逻辑上Boxc8f01a3f8889dcf657849dd45bc0fc4c不能视为Boxc0f559cc8d56b43654fcbe4aa9df7b4a的父类。
好,那我们回过头来继续看“类型通配符”中的第一个例子,我们知道其具体的错误提示的深层次原因了。那么如何解决呢?总部能再定义一个新的函数吧。这和Java中的多态理念显然是违背的,因此,我们需要一个在逻辑上可以用来表示同时是Boxc0f559cc8d56b43654fcbe4aa9df7b4a和Boxc8f01a3f8889dcf657849dd45bc0fc4c的父类的一个引用类型,由此,类型通配符应运而生。
类型通配符一般是使用 ? 代替具体的类型实参。注意了,此处是类型实参,而不是类型形参!且Box6b3d0130bba23ae47fe2b8e8cddf0195在逻辑上是Boxc0f559cc8d56b43654fcbe4aa9df7b4a、Boxc8f01a3f8889dcf657849dd45bc0fc4c...等所有Boxf4669da5dd7ec4cbc793dba728d85fa1的父类。由此,我们依然可以定义泛型方法,来完成此类需求。
public class GenericTest { public static void main(String[] args) { Box<String> name = new Box<String>("corn"); Box<Integer> age = new Box<Integer>(712); Box<Number> number = new Box<Number>(314); getData(name); getData(age); getData(number); } public static void getData(Box<?> data) { System.out.println("data :" + data.getData()); } }
有时候,我们还可能听到类型通配符上限和类型通配符下限。具体有是怎么样的呢?
在上面的例子中,如果需要定义一个功能类似于getData()的方法,但对类型实参又有进一步的限制:只能是Number类及其子类。此时,需要用到类型通配符上限。
public class GenericTest { public static void main(String[] args) { Box<String> name = new Box<String>("corn"); Box<Integer> age = new Box<Integer>(712); Box<Number> number = new Box<Number>(314); getData(name); getData(age); getData(number); //getUpperNumberData(name); // 1 getUpperNumberData(age); // 2 getUpperNumberData(number); // 3 } public static void getData(Box<?> data) { System.out.println("data :" + data.getData()); } public static void getUpperNumberData(Box<? extends Number> data){ System.out.println("data :" + data.getData()); } }
此时,显然,在代码//1处调用将出现错误提示,而//2 //3处调用正常。
类型通配符上限通过形如Boxa2b037db85f4e1df0e812b9647ac55a8形式定义,相对应的,类型通配符下限为Boxda50108ad159903fabe211f1543600e8形式,其含义与类型通配符上限正好相反
六. 怎么构建复杂模型如list元组?
泛型的一个重要好处是能够简单而安全地创建复杂的模型。如List元组。
package Generics; import java.util.ArrayList; class ThreeTuple2<A,B,C>{ public final A first; public final B second; private final C three; public ThreeTuple2(A a,B b,C c){ first = a; second = b; three = c; } public String toString(){ return "(" + first + "," + second + "," + three + ")"; } } public class TupleList<A,B,C> extends ArrayList<ThreeTuple2<A,B,C>> { static ThreeTuple2<Integer,String,Character> h(){ return new ThreeTuple2<Integer,String,Character>(99,"掌上洪城",'a'); } public static void main(String[] args) { TupleList<Integer,String,Character> ts = new TupleList<Integer,String,Character>(); ts.add(h()); ts.add(h()); for(ThreeTuple2<Integer,String,Character> ttp:ts) System.out.println(ttp); } } package Generics; import java.util.ArrayList; class ThreeTuple2<A,B,C>{ public final A first; public final B second; private final C three; public ThreeTuple2(A a,B b,C c){ first = a; second = b; three = c; } public String toString(){ return "(" + first + "," + second + "," + three + ")"; } } public class TupleList<A,B,C> extends ArrayList<ThreeTuple2<A,B,C>> { static ThreeTuple2<Integer,String,Character> h(){ return new ThreeTuple2<Integer,String,Character>(99,"掌上洪城",'a'); } public static void main(String[] args) { TupleList<Integer,String,Character> ts = new TupleList<Integer,String,Character>(); ts.add(h()); ts.add(h()); for(ThreeTuple2<Integer,String,Character> ttp:ts) System.out.println(ttp); } } /* 输出结果为: (99,掌上洪城,a) (99,掌上洪城,a) */
七. 泛型的擦除
package generics; import java.util.*; public class ErasedTypeEquivalence { public static void main(String[] args) { Class c1 = new ArrayList<String>().getClass(); Class c2 = new ArrayList<Integer>().getClass(); System.out.println(c1 == c2); } } /* * Output: true */// :~
在泛型内部,无法获得任何有关泛型参数类型的信息。
ArrayListf7e83be87db5cd2d9a8a0b8117b38cd4和ArrayListc0f559cc8d56b43654fcbe4aa9df7b4a是相同的类型。
擦除的补偿
要想在表达式中使用类型,需要显式地传递类型的class对象。
package generics; class Building { } class House extends Building { } public class ClassTypeCapture<T> { Class<T> kind; public ClassTypeCapture(Class<T> kind) { this.kind = kind; } public boolean f(Object arg) { return kind.isInstance(arg); } public static void main(String[] args) { ClassTypeCapture<Building> ctt1 = new ClassTypeCapture<Building>(Building.class); System.out.println(ctt1.f(new Building())); System.out.println(ctt1.f(new House())); ClassTypeCapture<House> ctt2 = new ClassTypeCapture<House>(House.class); System.out.println(ctt2.f(new Building())); System.out.println(ctt2.f(new House())); } } /* * Output: true true false true */// :~
八. 可以创建泛型数组吗?相应的应用场景怎么处理?
正如你在下面示例Erased.java中所见,不能创建泛型数组。一般的解决方案是任何想要创建泛型数组的地方都使用ArrayList:
package generics; public class Erased<T> { private final int SIZE = 100; public static void f(Object arg) { if (arg instanceof T) { } // Cannot make a static reference to the non-static type T T var = new T(); // Error T[] array = new T[SIZE]; // Error T[] array = (T) new Object[SIZE]; // Unchecked warning } } /// :~
使用ArrayList示例
package generics; import java.util.*; public class ListOfGenerics<T> { private List<T> array = new ArrayList<T>(); public void add(T item) { array.add(item); } public T get(int index) { return array.get(index); } } /// :~
九. 泛型限定(上限和下限)的表达式是怎样的?
上限:?extends E:可以接收E类型或者E的子类型对象。
下限:?super E:可以接收E类型或者E的父类型对象。
上限什么时候用:往集合中添加元素时,既可以添加E类型对象,又可以添加E的子类型对象。为什么?因为取的时候,E类型既可以接收E类对象,又可以接收E的子类型对象。
下限什么时候用:当从集合中获取元素进行操作的时候,可以用当前元素的类型接收,也可以用当前元素的父类型接收。
十. 什么时候用泛型?
当接口、类及方法中的操作的引用数据类型不确定的时候,以前用的Object来进行扩展的,现在可以用泛型来表示。这样可以避免强转的麻烦,而且将运行问题转移到的编译时期。
泛型的细节:
1)、泛型到底代表什么类型取决于调用者传入的类型,如果没传,默认是Object类型;
2)、使用带泛型的类创建对象时,等式两边指定的泛型必须一致;
原因:编译器检查对象调用方法时只看变量,然而程序运行期间调用方法时就要考虑对象具体类型了;
3)、等式两边可以在任意一边使用泛型,在另一边不使用(考虑向后兼容);
ArrayListf7e83be87db5cd2d9a8a0b8117b38cd4al = new ArrayLista87fdacec66f0909fc0757c19f2d2b1d(); //错
//要保证左右两边的泛型具体类型一致就可以了,这样不容易出错。
ArrayListd876df6b879603b519fa77c3a7ff60d2 al = new ArrayListf7e83be87db5cd2d9a8a0b8117b38cd4();
al.add("aa"); //错
//因为集合具体对象中既可存储String,也可以存储Object的其他子类,所以添加具体的类型对象不合适,类型检查会出现安全问题。 ?extendsObject 代表Object的子类型不确定,怎么能添加具体类型的对象呢?
public static voidmethod(ArrayLista559c2fd4cc43ceac6831fa3de4b0d38 al) {
al.add("abc"); //错
//只能对al集合中的元素调用Object类中的方法,具体子类型的方法都不能用,因为子类型不确定。
十一. Java类库中的泛型有那些?
所有的标准集合接口都是泛型化的—— Collectiond94943c0b4933ad8cac500132f64757f、Listd94943c0b4933ad8cac500132f64757f、Setd94943c0b4933ad8cac500132f64757f 和 Mapb77a8d9c3c319e50d4b02a976b347910。类似地,集合接口的实现都是用相同类型参数泛型化的,所以HashMapb77a8d9c3c319e50d4b02a976b347910 实现 Mapb77a8d9c3c319e50d4b02a976b347910 等。
除了集合类之外,Java 类库中还有几个其他的类也充当值的容器。这些类包括 WeakReference、SoftReference 和 ThreadLocal。