啃知識系列_泛型和泛型邊界
這兩天看Java程式設計思想,重新學習了一下泛型的知識。 以前很多不懂得地方也梳理了一下。
在沒有使用泛型之前,我們編寫一個類,想要持有其他型別的任何物件。
這個例子,我們先後存了兩種型別的物件,但是當我們從holder中取出物件的時候,需要強制型別轉換才可以。public class Holder { private Object a; public Holder(Object a){ this.a = a; } public Object get() { return a; } public void set(Object a) { this.a = a; } public static void main(String[] args) { Holder h = new Holder(new AutoMobile()); AutoMobile m = (AutoMobile) h.get(); h.set("Not AutoMobile"); String s = (String) h.get(); } } class AutoMobile{}
泛型的主要目的之一就是用來制定容器要持有什麼型別的物件,而且編譯器來保證型別的正確性。
因此,與其我們用Object,更喜歡暫時不指定型別,而是之後決定使用什麼型別,這時候我們需要使用型別引數,下面例子中T就是型別引數。
現在當建立HolderT的時候必須指明想要持有的物件,將其置於尖括號中。之後get和set的時候都已經制定了現在持有的物件。public class HolderT<T> { private T t; public HolderT(T t){ this.t = t; } public T get() { return t; } public void set(T t) { this.t = t; } public static void main(String[] args) { HolderT<AutoMobile> h = new HolderT<AutoMobile>(new AutoMobile()); AutoMobile a = h.get(); // h.set("Not AutoMobile"); 編譯器報錯,因為我們要制定儲存的事AutoMobile型別的物件 HolderT<String> h2 = new HolderT<String>("Not AutoMobile"); String s = h2.get(); } }
這裡初步講下泛型的基本用法,接下來會寫一些我覺的需要注意的泛型的例子和用法。
現在我們實現一個堆疊類。
這裡的Node也是泛型的,他有自己的型別引數U。public class LinedStack<T> { private static class Node<U>{ U item; Node next; Node(){ this.item = null; this.next = null; } Node(U item,Node next){ this.item = item; this.next = next; } boolean end(){ return item == null && next == null; } } private Node<T> top = new Node<T>(); public void push(T item){ top = new Node(item,top); } public T pop(){ T result = top.item; if(!top.end()){ top = top.next; } return result; } public static void main(String[] args) { LinedStack<Integer> stack = new LinedStack<Integer>(); stack.push(1); stack.push(2); System.out.println(stack.pop()); System.out.println(stack.pop()); } }
這裡我們使用了末端哨兵來判斷堆疊何時為空。這裡我關注的是一點,如果我不給他的內部類Node設定自己的型別引數,而是跟外部的型別引數一樣。
如果直接去掉U的型別引數,會報錯。這裡就需要先說一說型別擦除的概念。這個稍後再說。先讓我們看看泛型方法。
之前我們看到的泛型都是作用在類上,但他同樣可以作用在方法上。泛型方法使得該方法獨立於類而產生變化。以下是一些基本的指導建議:
無論何時,只要你能做到,就應該只使用泛型方法,因為他使得事情更清楚明白,另外,對於一個static的方法而言,無法訪問泛型類的泛型型別引數,所以,如果static方法需要使用泛型能力,就必須使其成為泛型方法。
要定義泛型方法,只需要將泛型引數列表置於返回型別之前就可以。
public class GenericMehtods {
public static <T> void f(T t){
System.out.println(t.getClass().getName());
}
public static void main(String[] args) {
f("");
f(1);
f(1.0d);
f(1.0f);
f(new GenericMehtods());
}
}
執行結果:
java.lang.String
java.lang.Integer
java.lang.Double
java.lang.Float
com.wyc.generics.GenericMehtods
對於為什麼傳入基本型別會輸出包裝型別的問題,這是由於自動裝箱導致的。這裡先不解釋這個問題。
這裡我們看到,方法f擁有型別引數。注意,當我們使用泛型類時我們必須在建立物件時指定型別引數的值,而使用泛型方法的時候,通常不必指明引數型別,因為編譯器會為我們找出具體的型別,這稱為型別引數推斷。
當我們平時使用容器的時候可能遇到過這種問題。
Map<String,Integer> map = new HashMap<String,Integer>();
我們會編寫多餘制定泛型引數的程式碼。我們可以使用型別引數推斷解決這種問題。(當然,JDK1.7之後已經支援直接去掉後面的指定型別的問題。只需要new HashMap<>()就可以)
public class New {
public static <K,V> Map<K,V> map(){
return new HashMap<K, V>();
}
public static void main(String[] args) {
Map<String,Integer> map = new HashMap<String, Integer>();
map = map();
}
}
當然,型別引數判斷會讓閱讀程式碼的人必須去分析New方法中的map()所隱含的功能。在泛型方法中,我們也可以顯示的指定型別,雖然這種方式不常用。
public class ExplicitTypeSpecification {
static void f(Map<String,List<String>> petPeople){}
public static void main(String[] args) {
New2 new2 = new New2();
f(new2.<String, List<String>>map());
}
}
class New2{
public <K,V> Map<K,V> map(){
return new HashMap<K, V>();
}
public static <K,V> Map<K,V> map2(){
return new HashMap<K, V>();
}
static void f(Map<String,List<String>> petPeople){}
public void test(){
f(this.<String,List<String>>map());
f(New2.<String, List<String>>map2());
// f(map());
// f(map2());
}
}
這種語法抵消了New類為我們帶來的好處。(既省去大量的型別說明),不過只有在編寫非賦值語句時才需要這樣額外的說明。
接下來我們說一說型別擦除。請考慮下面情況。
public class ErasedTypeEquivalence {
public static void main(String[] args) {
Class c1 = new ArrayList<Integer>().getClass();
Class c2 = new ArrayList<String>().getClass();
System.out.println(c1 == c2);
System.out.println(c1);
System.out.println(c2);
}
}
這種c1和c2很容易被認為是兩種型別,但上面認為兩種型別是相同。下面做下補充。
public class LostInfomation {
public static void main(String[] args) {
List<Frob> list = new ArrayList<Frob>();
Map<Frob,Fnorkle> map = new HashMap<Frob, Fnorkle>();
Quark<Fnorkle> quark = new Quark<Fnorkle>();
Particle<Long,Double> particle = new Particle<Long, Double>();
System.out.println(Arrays.toString(list.getClass().getTypeParameters()));
System.out.println(Arrays.toString(map.getClass().getTypeParameters()));
System.out.println(Arrays.toString(quark.getClass().getTypeParameters()));
System.out.println(Arrays.toString(particle.getClass().getTypeParameters()));
}
}
class Frob{}
class Fnorkle{}
class Quark<Q>{}
class Particle<POSITION,MOMENTUM>{}
Class.getTypeParameters()將返回TypeVariable物件陣列,表示有泛型宣告所宣告的型別引數,你會發現輸出的都是用作引數佔位符的識別符號,並不是有用的資訊。
在泛型程式碼內部,無法獲得任何有關泛型引數型別的資訊
因此,在你使用Java泛型的時候,這意味著任何具體的型別資訊都被擦除掉了,你唯一知道的就是你在使用的物件。
如果我們想編寫如下程式碼,是不行的。
public class HasF {
public void f(){
System.out.println("HasF.f()");
}
public static void main(String[] args) {
HasF hasF = new HasF();
Mainpulator<HasF> mainpulator = new Mainpulator<HasF>(hasF);
mainpulator.mainpulate();
}
}
class Mainpulator<T>{
private T obj;
public Mainpulator(T t){
obj = t;
}
public void mainpulate(){
obj.f(); //編譯器報錯
}
}
由於有了擦除,編譯器我們是不知道T的型別的,所以也沒有辦法呼叫obj的方法。這個時候我們必須協助泛型類,給定泛型類的邊界,以此告知編譯器只能接受遵循這一邊界的型別。這裡我們重用了extends關鍵字。由於有了邊界,下面程式碼可以編譯。
public class HasF {
public void f(){
System.out.println("HasF.f()");
}
public static void main(String[] args) {
HasF hasF = new HasF();
Mainpulator<HasF> mainpulator = new Mainpulator<HasF>(hasF);
mainpulator.mainpulate();
}
}
class Mainpulator<T extends HasF>{
private T obj;
public Mainpulator(T t){
obj = t;
}
public void mainpulate(){
obj.f();
}
}
邊界<T extends HasF>宣告T必須具有型別HasF或者從HasF匯出的型別。
泛型型別引數將擦除到它的第一個邊界。(他可能有很多邊界,之後說明),像上面的例子,他擦除到HashF,所以就好像類的宣告時候用HasF替換T一樣,可以直接使用T來呼叫相關的HasF的行為。
泛型型別只有在靜態型別檢查期間才出現,在此之後,程式中的所有的泛型型別都將被擦除,替換為他們的非泛型上界。例如,List<T>這樣的型別註解,將被擦除為List,二普通的型別變數在未指定邊界的情況下將被擦除為Object。
擦除的主要正當理由是從非泛化程式碼到泛化程式碼的轉變過程,以及在不破換現有類庫的情況下,將泛型融入Java語言的一種手段。
當我們想建立一個型別例項的時候new T(),我們是無法實現的,部分原因是擦除,另一部分則是因為我們不知道T是否具有預設的構造方法。我們可以通過工廠方法實現。
public class InstantiateGenericType {
public static void main(String[] args) {
ClassFactory<Employee> classFactory = new ClassFactory<Employee>(Employee.class);
System.out.println(classFactory.t);
ClassFactory<Employee1> classFactory1 = new ClassFactory<Employee1>(Employee1.class); //這裡會導致報錯,因為Employee1沒有預設的構造方法
System.out.println(classFactory1.t);
}
}
class Employee{}
class Employee1{
public Employee1(int i ){
}
}
class ClassFactory<T>{
T t;
public ClassFactory(Class<T> kind){
try {
t = kind.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}
}
所以上面這種方式是不可取的,我們最好建立一個顯示的工廠物件。
public class FactoryConstrain {
public static void main(String[] args) {
new Foo2<Integer>(new IntegerFacotry());
new Foo2<Widget>(new Widget.WidgetFacotry());
}
}
class Foo2<T>{
private T obj;
public <F extends Factory<T>> Foo2(F f){
obj = f.create();
}
}
interface Factory<T>{
T create();
}
class IntegerFacotry implements Factory<Integer>{
@Override
public Integer create() {
return new Integer(0);
}
}
class Widget {
public static class WidgetFacotry implements Factory<Widget>{
@Override
public Widget create() {
return new Widget();
}
}
}
這種方式是一種變體,這種方式能夠讓們在編譯期就檢查我們的物件建立。
還有一種是模板方法的設計模式。
public class CreatorGeneric {
public static void main(String[] args) {
Creator creator = new Creator();
creator.f();
}
}
abstract class GenericWithCreate<T>{
final T element;
GenericWithCreate(){
element = create();
}
abstract T create();
}
class X{}
class Creator extends GenericWithCreate<X>{
@Override
X create() {
return new X();
}
public void f(){
System.out.println(element.getClass().getName());
}
}
泛型陣列,我們一般是不能建立泛型陣列的,一般解決方案是在任何想要建立泛型陣列的地方都是用ArrayList來代替。如果建立泛型陣列我們看到如下情況。
public class GenericArray<T> {
private T[] array;
public GenericArray(int sz){
// array = new T[sz];
array = (T[]) new Object[sz];
}
public void put(int index,T item){
array[index] = item;
}
public T get(int index){
return array[index];
}
public T[] rep(){
return array;
}
public static void main(String[] args) {
GenericArray<Integer> gai = new GenericArray<Integer>(10);
gai.put(0,1);
Integer[] ia = gai.rep();
}
}
我們需要在構建的時候使用強制型別轉換從Object[]轉換。因為直接使用new T[]我們編譯器是不知道他是什麼型別的,所以無法直接例項化。下面的程式碼執行會報錯。錯誤資訊如下。Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.Integer;
at com.wyc.generics.GenericArray.main(GenericArray.java:25)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:147)
說無法從Object陣列轉換到Integer陣列的意思。那麼這裡我理解是,雖然編譯器是ok的,因為我們給T一個指定型別是Integer,但是執行時期,實際上是Object型別的。所以無法直接直接賦值給ia。我們debug模式看下就會發現。
這裡你會看到array是一個Object陣列,裡面存了一個之前我賦值的一個Integer物件。並且看程式碼中我試圖想用
(T[])array
將返回的rep強轉成Integer[].但是因為T在執行期其實是Object看待的話,那麼我們其實最後這句話也是相當於(Object[])array。那麼也不會起作用,這裡大家可能也看到了在
Integer[] ia = (Integer[])gai.rep();
這行程式碼 中我試圖將返回的Object[]陣列強轉成Integer[]陣列。但是最後也會報錯。
這是因為陣列不同型別之間轉換,基類無法向他的子類強轉型別轉換,但是子類可以直接賦值給基類的陣列。
這可能是一個常識性問題,但是我居然一直沒有注意到= =原諒我自己太不用心了。
好了,這裡我主要說明的是,對於編譯期,會給我們一種錯覺,這種程式碼是可以的,因為T的泛型我們已經指定了明確的型別,所以rep是認為是正確的,但是在執行期間,我們的泛型T其實都被當作Object來使用,所以這程式碼會報執行期錯誤。當我們看到泛型的時候,只要將型別擦除看成Object使用就更容易理解了。
但是有一種方式可以讓我們在執行期將T的型別的擦除恢復,如下程式碼。
public class GenericArrayWIthTypeToken<T> {
private T[] array;
public GenericArrayWIthTypeToken(Class<T> type,int sz){
array = (T[]) Array.newInstance(type,sz);
}
public void put(int index,T item){
array[index] = item;
}
public T[] req(){
return array;
}
public static void main(String[] args) {
GenericArrayWIthTypeToken<Integer> gai = new GenericArrayWIthTypeToken<Integer>(Integer.class,10);
Integer[] a = gai.req();
}
}
這裡我們看到。建立的時候我們使用Array.newInstance。雖然這個返回型別是Object[]我們需要強轉,但我理解這個強轉就是給編譯器看的,讓他知道編譯期型別的轉換是正確的。 現在程式碼執行是正確的,因為我們已經在執行期恢復了T的型別,通過Class<T> type來指定。
接下來我們詳細的說一下邊界。
看如下程式碼。
interface HasColor{
Color getColor();
}
class Colored<T extends HasColor>{
T item;
Colored(T item){
this.item = item;
}
T getItem(){
return item;
}
//我們會發現這裡我們可以直接呼叫HasColor的方法,因為我們使用邊界這種方式
//T的執行期已經擦除到第一個邊界,即是HasColor
Color color(){
return item.getColor();
}
}
class Dimension{
public int x,y,z;
}
//這裡會編譯不通過,因為 如果extends class和interface的話 , class必須是第一個
//class ColoredDimension<T extends HasColor & Dimension> {}
class ColoredDimension<T extends Dimension & HasColor > {
T item;
ColoredDimension(T item){
this.item = item;
}
Color color(){
return item.getColor();
}
int getX(){
return item.x;
}
int getY(){
return item.y;
}
int getZ(){
return item.z;
}
}
interface Weight{
int weight();
}
//邊界裡只能有一個類,多個介面
class Solid<T extends Dimension & HasColor & Weight>{
T item;
Solid(T item){
this.item = item;
}
Color color(){
return item.getColor();
}
int getX(){
return item.x;
}
int getY(){
return item.y;
}
int getZ(){
return item.z;
}
int weight(){
return item.weight();
}
}
class Bounded extends Dimension implements HasColor , Weight{
@Override
public Color getColor() {
return null;
}
@Override
public int weight() {
return 0;
}
}
public class BasicBounds {
public static void main(String[] args) {
Solid<Bounded> solid = new Solid<Bounded>(new Bounded());
solid.color();
solid.getX();
solid.weight();
}
}
這段程式碼可能有些長,但是希望大家能打一遍,有一個更好的理解。
這裡對於T extends Dimension & HasColor 可能會有些問題,為什麼裡面可以直接使用 item.color()到底最後執行時擦除成什麼型別。這裡我通過javap -c 反編譯看下。
看構造方法發現他確實擦除成第一個邊界,即是Dimension。在我們的color()方法中我們發現,他getfield的是Dimension但是會有一個checkcast,即是判斷下他是否是HasColor型別的,然後在直接呼叫HashColor.getColor。
所以這就是我們為何也能呼叫其他邊界方法的原因。
說到了這裡我對於泛型和邊界的理解是。
泛型,編譯期是帶著型別的,為了編譯器判斷我們寫的是否有問題,但是執行期間,我們的型別就被擦除掉了,而統一被看成了Object的,如果我們有了邊界,那麼我們的這個泛型型別就會被看成是第一個邊界的型別,如果有其他邊界,他也會有他們的行為。
由於這個泛型還有很大一部分沒有說,篇幅太長,分成兩部分來寫。,總結下的我們主要說了幾點,
1. 泛型會在執行期將型別擦除成為Object。
2.邊界可以讓我們將泛型的型別引數擦除為第一個邊界。
3.不能直接建立泛型陣列。