Java泛型可行與不可行

泛型基礎

理解

​ 通常狀況,一個類的屬性,或者一個方法的參數/返回值都須要在編寫代碼時聲明基本類型或者自定義類型,但有時候沒法在編寫代碼時使用現有的類來表達參數類型或者返回值類型,這時候就需有一種方式能夠表達下面的意思:這裏須要一個類,它知足這些要求就能夠了,具體是什麼類能夠在使用這個類或方法時指定。Java中這種方式就是泛型。可是java泛型在使用上有不少限制,使用時要注意,同時注意泛型主義上的理解,Java中泛型的聲明使用更多java

做用

​ 必定程序上繼承與接口就能夠完成上面的功能,但泛型有不少額外的做用數組

  1. 泛型能夠更安全安全

    使用泛型就是告訴編譯器想使用什麼類型,在使用泛型時編譯器會對代碼進行類型檢查,讓錯誤暴露在編譯期,而不是運行期,更安全app

  2. 能夠快速建立複雜的類型jvm

    由於在編寫時沒有指定具體類型,因此在使用時就能夠更隨意的指定類型,這個功能能夠完成相似js中對象的功能,對象的屬性規定好,具體是什麼類型你隨便,可是沒能像js那樣隨意添加屬性ide

    public class TupleTest<T, R> {
    
        public final T t;
        public final R r;
    
        public TupleTest(T t, R r) {
            this.t = t;
            this.r = r;
        }
    
        public static <A, B> TupleTest<A, B> make(A a, B b) {
            return new TupleTest<>(a, b);
        }
    
        /**
         * 若是返回值聲明裏聲明瞭泛型,那麼在方法返回值 new 時就要有尖括號,否則會警告
         * jdk1.5中返回值聲明時的泛型去掉,也會有編譯警告
         *
         * @return tupleTest
         */
        public TupleTest<String, String> make() {
            return new TupleTest<>("a", "b");
        }
    
         public R getR() {
            return r;
        }
    }
  3. 能夠自動完成類型的轉換ui

在泛型出現以前,若是一個方法不能肯定方法的返回值類型,或者根據入參能夠肯定多種類型返回值類型,那麼這個方法就只能返回Object ,有了泛型以後,在方法返回正確的值後,會自動轉爲具體的類型,而這在代碼上沒有額外的代碼,並且這種轉換很安全this

上面例子編譯以後再反編譯回來 make 方法是這樣的code

public com.zoro.thinkinginjava.four.TupleTest<String, String> make() {
    return new com.zoro.thinkinginjava.four.TupleTest((T)"a", (R)"b");
  }

再看一個調用時的代碼對象

public class TupleMain {

    public static void main(String[] args) {
        TupleTest<Apple, Orange> tuple = new TupleTest<>(new Apple(), new Orange());

        Orange orange = tuple.getR();
    }
}

反編譯以後

public class TupleMain {
  public static void main(String[] args) {
    TupleTest<Apple, Orange> tuple = new TupleTest(new Apple(), new Orange());
    Orange orange = (Orange)tuple.getR();
  }
}

能夠看到自動對參數進行了轉型,因此編譯器不會產生轉型警告

  1. 還有一些更高級的用法,好比 泛型自限定

困難之處

書寫泛型代碼的主要困難是由於泛型在運行時被擦除,因此在運行期沒有泛型類的具體信息,這意味着泛型參數看上去就借一個Object類,什麼都幹不了,須要注意如下方法

  1. 一樣的類型,不一樣的泛型參數在編譯期表明着不一樣類型,在運行期就沒有差異了

    public class EraseMain {
    
        public static void main(String[] args) {
            List<String> list1 = new ArrayList<>();
            List<Integer> list2 = new ArrayList<>();
    //        list1 = list2; // 編譯期是不一樣的
            System.out.println(list1.getClass().getName());
            System.out.println(list2.getClass().getName());
            // 運行期類型是相同的
            System.out.println(list1.getClass() == list2.getClass());
        }
    
    }
  2. 不能使用 new 來建立泛型類型的具體對象,最好的方案是使用 Class.newInstance()或者使用工場模式

    public T getNewInstance() {
            // return new T();
            // Error:(12, 20) java: 意外的類型
            //  須要: 類
            //  找到:    類型參數T
            try {
                return t.newInstance();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
  3. 不能使用 instanceof 操做符了,但能夠用 Class.isInstance(Object)方法

    public class EraseEntity<T> {
    
        Class<T> tClass;
    
        public EraseEntity(Class<T> tClass) {
            this.tClass = tClass;
        }
    
        public boolean instanceOf(Object t) {
    //        return t instanceof T; // 這樣就不能夠了
            return tClass.isInstance(t); // 這樣是能夠的
        }
    
    }
  4. 不能new 一個泛型數組,並且要產生泛型數組很是麻煩,可使用Array.newInstance(Class<?>,int)

    public T[] createArray(){
            return (T[]) Array.newInstance(t,5);
        }

    可是這樣也會有警告,須要壓制

  5. 除非設定邊界,不然不能調用任何自定義的方法

  6. 基本類型不能做爲泛型參數,可是其包裝類型能夠,並能夠自動包裝

    //        List<int> list2 = new ArrayList<>();
            List<Integer> list2 = new ArrayList<>();
  7. 一個類不能實現同一個泛型接口的兩種變體,但去掉泛型實現能夠;

    public class ImplTest extends AbstractA implements InterfaceA<Integer> {
    //public class ImplTest extends AbstractA implements InterfaceA<String> {
    // ImplTest類實現InterfaceA接口時聲明的泛型參數是String,AbstractA實現InterfaceA時聲明的泛型參數是 Integer,這時就不能夠了,
    // 若是能夠會致使類型衝突,好比 get方法,在AbstractA中返回值是Integer,可是在ImplTest中就變成了String,不管重載或重寫都不能解決這個問題
    }
    
    interface InterfaceA<T> {
        T get(T t);
    
    }
    
    abstract class AbstractA implements InterfaceA<Integer> {
        public Integer get(Integer integer) {
            return 0;
        }
    }
  8. 不能經過不一樣的泛型參數進行方法重載,可是可使用 <R extends List<?>> 給泛型參數添加邊界重載方法

    public class OverLoadTest {
    
        public <T> void test(T t) { }
    
        // 由於T與R沒有設置邊界在運行時 T與R 都是相似Object,因此不能經過方法簽名區分這兩個方法
    //    public <R> void test(R r) { }
    
        // 這樣是能夠的 由於R必定會是一個List的子類,List與Object(T)是有區別的,就能夠經過方法簽名區分了
        public <R extends List<?>> void test(R r) {    }
    }

泛型邊界

可使用 extends 限定泛型類型的邊界,能夠是多個(&鏈接),類寫在前面,限定邊界以後在泛型方法或者類的內部就可使用邊界類上的方法了

public class WildCardTest<T extends List<String> & Iterable<String> & InterfaceA<?>> {


    public void test(T t) {
        t.add("");  // List接口的方法
        t.iterator(); // Iterable接口的方法
        t.testMethod(); // InterfaceA方法
    }

}

interface InterfaceA<T>{

    //    void add(T t); // List接口也有一樣方法簽名的方法,因此在 同時將 List與InterfaceA設置爲上邊界時List與InterfaceA的泛型參數要兼容,不然也會出錯
    void testMethod();

}

通配符

通配符在泛型中的應用是爲了解決下面的問題:有一個容器的泛型是基類的變量,想要將一個泛型是子類的容器賦值給這個變量,編譯器是不容許的;由於運行時會將泛型擦除,一旦將一個泛型是子類的容器賦值給泛型是基類的容器變量,在運行時就能夠將一個這個基類的其餘子類對象放入這個窗口,形成在取出對象時的類型不安全,因此編譯期不容許這樣賦值;

public class WildCardTest<T extends List<String> & Iterable<String> & InterfaceA<?>> {

    public static void main(String[] args) {
        List<InterfaceA<String>> list ;
        List<Impl> impls = new ArrayList<>();
//        list = impls;
        // 將 impls賦值給 list是不能夠的,緣由:
        // 1. 編譯期 List<InterfaceA<String>> 與 List<Impl>是不一樣的且不能向上轉型
        // 2. 一旦容許這樣賦值,那麼以後 的操做會出現類型問題,好比此例,將一個ArrayList<Impl> 賦值給 List<InterfaceA>變量list,
        // 那麼以後能夠向list 中add 一個 Impl2對象,Impl2與Impl不兼容
    }
}
interface InterfaceA<T>{}

class Impl implements InterfaceA<String> {}

class Impl2 implements InterfaceA<String> {}

容器的這一特色與數組不一樣,子類數組對象能夠賦值給基類數組變量(相似向上轉型),可是在運行期jvm 能夠知道數組元素中的對象類型是哪一個具體子類,因此若是將數組中元素賦值時,若是不是原數組中的類型,會報錯(ArrayStoreException)

public class WildCardTest2 {
    public static void main(String[] args) {
        InterfaceA<?>[] arr1 = new Impl[3];
        arr1[0] = new Impl();
        //會報錯
        //arr1[2] = new Impl2();

        // 兼容的類型能夠
        InterfaceA<?>[] arr2 = new InterfaceA[4];
        arr2[0] = new Impl();
        arr2[0] = new Impl2();
    }

}

爲了保證類型安全,又能夠將子類泛型容器賦值給基類泛型變量,可使用通配符(單一邊界,extends 後面只能有一個類型)

通配符的困難之處

當一個類在聲明時使用了<? extends Fruit> 這種泛型,而這個類的寫法如同下面這樣

class TestClass<T>{
	public void test(T t){
		// somecode
	}
    public void test2(Object o){
        // somecode
    }
}

在使用時

TestClass<? extends Fruit> f = new TestClass<Apple>;

這樣寫會出現的問題是不能調用test(T)方法了,由於test 須要的是一個具體的Fruit 的子類,例子中應該是Applie,但 ? extends Fruit 表明的不只僅是 Apple 這一種子類,也多是 orange 。若是調用時真的用orange 類型實例作爲能數,類型就不安全,因此 test(T) 方法不能用了;可是 test2(Object) 還能夠用

逆變

逆變指的是 < ? super Apple> 這種寫法,這種寫法的特性與 <? extends Apple> 的寫法的特性是相反的。上面的例子,泛型入參方法不能用了,而逆變的特性是入參能夠是任何Apple 的子類,注意是子類,不是基類,由於Apple 的基類有多種,若是編譯器容許傳入基類,就會存在風險,可是傳入子類就不會有風險,由於子類能夠轉型爲Apple 類,Apple 類能夠算是Apple的基類;

public class WildcardTest4 {

    public static void main(String[] args) {
        List<? super Apple> appleList = new ArrayList<Fruit>();
        List<? super Apple> appleList2 = new ArrayList<Apple>();
        List<? super Apple> appleList3 = new ArrayList<>();
        // 前三種狀況均可以,可是這種不能夠
//    List<? super Apple> appleList4 = new ArrayList<BigApple>();

        // 不能夠
        //appleList3.add(new Orange());
        appleList3.add(new Apple());
        appleList3.add(new BigApple());
        // 雖然字面上是 任何 Apple 的父類,可是Apple父類不少,不能肯定類型,因此實際上任何Apple 的父類都不行
        //appleList3.add(new Fruit());
        
        // 只能Object 接
        Object a = appleList3.get(1);
    }


}

class Fruit {}

class Orange extends Fruit {}

class Apple extends Fruit {}

class BigApple extends Apple implements Runnable {
    @Override
    public void run() {
    }
}

class SmallApple extends Apple {}

逆變的困難之處在於方法的返回值,它的返回值只能用Object 類型的變量接受

無界通配符

兩個功能

  • 這裏想用泛型代碼來編寫,這裏並非要用原生的類型,可是當前狀況下,泛型參數能夠持有任何類型
  • 當有個地方須要多個泛型參數,但你只能肯定一部分時可使用無界通配符 例:Map<String, ?>
  • 當一個地方要求泛型,若是你沒有給出泛型,會有警告,但使用無界通配符會消除警告

無界通配符與原生類型是不同的,以ListList<?> 爲例,List 表明持有任何Object類型的List,List<?>表明具備某種特定類型的的非原生List,但目前不肯定是什麼類型;

下面例子顯示這種區別

public class WildcardTest5 {

    public static void main(String[] args) {
        List list = new ArrayList();
        list.add(new Apple());// 有警告,可是不會編譯報錯
        Object o = list.get(0);

        List<?> list1 = new ArrayList<>();
//        list1.add(new Apple());// 不可這樣寫,編譯報錯
    }
}

總結

在使用泛型時,時刻都要想着,我這樣定義泛型,編譯器爲了保證泛型安全,這裏我只能接受什麼樣的類型; 方法的返回值會是什麼樣的;同時要想着這裏是否會發生轉型

相關文章
相關標籤/搜索