Java泛型理解

java泛型

通俗的讲,泛型就是操作类型的占位符

 

一、通常泛型的写法

 

1.1定义泛型类

public class ApiResult<T>{

    int resultCode;

    String resultMsg;

    T resultObject;

}

 

1.2定义泛型方法

 

public JsonUtil{

    public <T> T str2Json(String jsonText, Class target){

        T result = null;

        ...

 

        return result;

    }

}

 

1.3 K,V类型

public class ResultMap<K,V>{

    private K key;

    private V value;

 

    // 省略set,get方法

 

 

    public void put(K key, V value){

        this.key = key;

        this.value = value;

    }

}

 

 

二、使用泛型类和方法

ApiResult<User> result = new ApiResult<User>();

result.setResultCode(0);

result.setResultMsg("success");

result.setResultObject(new User());

 

String userJsonText = "userJsonText";

User u = JsonUtil.str2Json(jsonText, User.class);

 

 

ResultMap<String,User> resultMap = new ResultMap<String,User>();

resultMap.put("currentUserKey", new User());

 

 

三、类型擦除

public class Operate{

    public static void main(String[] args){

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

        names.add("Jack");

        names.add("Tom");

        for(String name:names){

            System.out.println(name);

        }

    }

}

其对应的class文件反汇编以后,我们使用java-gui反编译.exe  查看编译之后的代码如下:

 

发现没有,根本没有<String> 这一部分了。这个限制为String类型的泛型被“擦除”了。写代码的时候,泛型会做校验,类型不对应的,无法add,但是编译之后边去掉了泛型类型。

 

 

四、为什么要使用java泛型??

这里的泛型就相当于“约法三章”,先给你定好“规矩”,我这个List<String> 就是用来操作String类型的,你插入Person对象就不行。说白了就是为了类型安全。所以其好处有:

4.1类型安全

通过知道使用泛型定义的变量的类型限制,编译器可以在一个高得多的程度上验证类型假设。没有泛型,这些假设就只存在于程序员的头脑中(或者如果幸运的话,还存在于代码注释中)。

4.2消除强制类型转换

//该代码不使用泛型:

List li = new ArrayList();

li.put(new Integer(3));

Integer i = (Integer) li.get(0);

 

 

//该代码使用泛型:

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

li.put(new Integer(3));

Integer i = li.get(0);

 


 

 

<? extends T><? super T>

 

public class Fruit {

 

}

 

public class Apple extends Fruit {

 

}

 

public class Plate<T> {

       private T item;

       public Plate(T t){

              item = t;

       }

       public T getItem() {

              return item;

       }

       public void setItem(T item) {

              this.item = item;

       }

      

}

 

public class TestGenericParadigm {

      

       public static void main(String[] args){

             

             

              Plate<Fruit> p = new Plate<Apple>(new Apple()); // 编译不通过

              Plate<Fruit> p = new Plate<Fruit>(new Fruit());

       }

}

为什么上面这个会出现编译不通过??

解释:苹果是水果,但是装苹果的盘子不是装水果的盘子

所以,就算容器里装的东西之间有继承关系,但容器之间是没有继承关系的

 

为了让泛型用起来更舒服,Sun的大脑袋们就想出了<? extends T>和<? super T>的办法,来让”水果盘子“和”苹果盘子“之间发生关系

 

 

 

什么是上界?

 

Plate<? extends Fruit>

翻译成人话就是:一个能放水果以及一切是水果派生类的盘子。再直白点就是:啥水果都能放的盘子。这和我们人类的逻辑就比较接近了。Plate<? extends Fruit>和Plate<Apple>最大的区别就是:Plate<? extends Fruit>是Plate<Fruit>以及Plate<Apple>的基类。直接的好处就是,我们可以用“苹果盘子”给“水果盘子”赋值了。

Plate<? extends Fruit> p=new Plate<Apple>(new Apple());

 

如果把Fruit和Apple的例子再扩展一下

//Lev 1

class Food{}

 

//Lev 2

class Fruit extends Food{}

class Meat extends Food{}

 

//Lev 3

class Apple extends Fruit{}

class Banana extends Fruit{}

class Pork extends Meat{}

class Beef extends Meat{}

 

//Lev 4

class RedApple extends Apple{}

class GreenApple extends Apple{}

在这个体系中,上界通配符 Plate<? extends Fruit> 覆盖下图中蓝色的区域

 

 

 

 

什么是下界?

 

Plate<? super Fruit>

表达的就是相反的概念:一个能放水果以及一切是水果基类的盘子。Plate<? super Fruit>是Plate<Fruit>的基类,但不是Plate<Apple>的基类。对应刚才那个例子,Plate<? super Fruit>覆盖下图中红色的区域。

 

 

 

 

上下界通配符的副作用

 

边界让Java不同泛型之间的转换更容易了。但不要忘记,这样的转换也有一定的副作用。那就是容器的部分功能可能失效。

还是以刚才的Plate为例。我们可以对盘子做两件事,往盘子里set()新东西,以及从盘子里get()东西。

 

public class TestGenericParadigm {

      

       public static void main(String[] args){

              /*

               * 上界通配符

               * 不能存入任何元素

               * 读出来的东西只能放在Fruit或它的基类中

               */

             

              Plate<? extends Fruit> p = new Plate<Apple>(new Apple());

 

              p.setItem(new Fruit()); //Error

              p.setItem(new Apple()); //Error

             

              Fruit newFruit1 = p.getItem();

              Object newFruit2 = p.getItem();

              Apple new Fruit3 = p.getItem();//Error

             

       }

}

 

原因是编译器只知道容器内是Fruit或者它的派生类,但具体是什么类型不知道

所以通配符<?>和类型参数的区别就在于,对编译器来说所有的T都代表同一种类型。比如下面这个泛型方法里,三个T都指代同一个类型,要么都是String,要么都是Integer。

但通配符<?>没有这种约束,Plate<?>单纯的就表示:盘子里放了一个东西,是什么我不知道

 

下界<? super T>不影响往里存,但往外取只能放在Object对象里

使用下界<? super Fruit>会使从盘子里取东西的get( )方法部分失效,只能存放到Object对象里。set( )方法正常。

              /*

               * 下界通配符

               * 存入元素正常

               * 读取的东西只能存放在Object中

               */

              Plate<? super Fruit> p1 = new Plate<Fruit>(new Fruit());

             

              p1.setItem(new Fruit());

              p1.setItem(new Apple());

             

              Apple newFruit4 = p1.getItem();//Error

              Fruit newFruit5 = p1.getItem();//Error

              Object newFruit6 = p1.getItem();

因为下界规定了元素的最小粒度的下限,实际上是放松了容器元素的类型控制。既然元素是Fruit的基类,那往里存粒度比Fruit小的都可以。但往外读取元素就费劲了,只有所有类的基类Object对象才能装下。但这样的话,元素的类型信息就全部丢失

 

PECS原则

最后看一下什么是PECS(Producer Extends Consumer Super)原则,已经很好理解了:

频繁往外读取内容的,适合用上界Extends。

经常往里插入的,适合用下界Super。

 

 

posted @ 2019-01-07 16:38  林木声  阅读(1631)  评论(0编辑  收藏  举报