java 泛型使用详解 + 总结

一. 泛型概念的提出(为什么需要泛型)?

首先,我们看下下面这段简短的代码:

复制代码
 1 public class GenericTest {
 2 
 3     public static void main(String[] args) {
 4         List list = new ArrayList();
 5         list.add("qqyumidi");
 6         list.add("corn");
 7         list.add(100);
 8 
 9         for (int i = 0; i < list.size(); i++) {
10             String name = (String) list.get(i); // 1
11             System.out.println("name:" + name);
12         }
13     }
14 }
复制代码

定义了一个List类型的集合,先向其中加入了两个字符串类型的值,随后加入一个Integer类型的值。这是完全允许的,因为此时list默认的类型为Object类型。在之后的循环中,由于忘记了之前在list中也加入了Integer类型的值或其他编码原因,很容易出现类似于//1中的错误。因为编译阶段正常,而运行时会出现“java.lang.ClassCastException”异常。因此,导致此类错误编码过程中不易发现。

 在如上的编码过程中,我们发现主要存在两个问题:

1.当我们将一个对象放入集合中,集合不会记住此对象的类型,当再次从集合中取出此对象时,改对象的编译类型变成了Object类型,但其运行时类型任然为其本身类型。

2.因此,//1处取出集合元素时需要人为的强制类型转化到具体的目标类型,否则很容易出现“java.lang.ClassCastException”异常

那么有没有什么办法可以使集合能够记住集合内元素各类型,且能够达到只要编译时不出现问题,运行时就不会出现“java.lang.ClassCastException”异常呢?答案就是使用泛型

 

二.什么是泛型?

泛型定义

泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

 看着好像有点复杂,首先我们看下上面那个例子采用泛型的写法。

复制代码
 1 public class GenericTest {
 2 
 3     public static void main(String[] args) {
 4         /*
 5         List list = new ArrayList();
 6         list.add("qqyumidi");
 7         list.add("corn");
 8         list.add(100);
 9         */
10 
11         List<String> list = new ArrayList<String>();
12         list.add("qqyumidi");
13         list.add("corn");
14         //list.add(100);   // 1  提示编译错误
15 
16         for (int i = 0; i < list.size(); i++) {
17             String name = list.get(i); // 2
18             System.out.println("name:" + name);
19         }
20     }
21 }
复制代码

采用泛型写法后,在//1处想加入一个Integer类型的对象时会出现编译错误,通过List<String>,直接限定了list集合中只能含有String类型的元素,从而在//2处无须进行强制类型转换,因为此时,集合能够记住元素的类型信息,编译器已经能够确认它是String类型了。

结合上面的泛型定义,我们知道在List<String>中,String是类型实参,也就是说,相应的List接口中肯定含有类型形参。且get()方法的返回结果也直接是此形参类型(也就是对应的传入的类型实参)。下面就来看看List接口的的具体定义:

复制代码
 1 public interface List<E> extends Collection<E> {
 2 
 3     int size();
 4 
 5     boolean isEmpty();
 6 
 7     boolean contains(Object o);
 8 
 9     Iterator<E> iterator();
10 
11     Object[] toArray();
12 
13     <T> T[] toArray(T[] a);
14 
15     boolean add(E e);
16 
17     boolean remove(Object o);
18 
19     boolean containsAll(Collection<?> c);
20 
21     boolean addAll(Collection<? extends E> c);
22 
23     boolean addAll(int index, Collection<? extends E> c);
24 
25     boolean removeAll(Collection<?> c);
26 
27     boolean retainAll(Collection<?> c);
28 
29     void clear();
30 
31     boolean equals(Object o);
32 
33     int hashCode();
34 
35     E get(int index);
36 
37     E set(int index, E element);
38 
39     void add(int index, E element);
40 
41     E remove(int index);
42 
43     int indexOf(Object o);
44 
45     int lastIndexOf(Object o);
46 
47     ListIterator<E> listIterator();
48 
49     ListIterator<E> listIterator(int index);
50 
51     List<E> subList(int fromIndex, int toIndex);
52 }
复制代码

我们可以看到,在List接口中采用泛型化定义之后<E>中的E表示类型形参,可以接收具体的类型实参,并且此接口定义中,凡是出现E的地方均表示相同的接受自外部的类型实参。

自然的,ArrayList作为List接口的实现类,其定义形式是:

复制代码
 1 public class ArrayList<E> extends AbstractList<E> 
 2         implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
 3     
 4     public boolean add(E e) {
 5         ensureCapacityInternal(size + 1);  // Increments modCount!!
 6         elementData[size++] = e;
 7         return true;
 8     }
 9     
10     public E get(int index) {
11         rangeCheck(index);
12         checkForComodification();
13         return ArrayList.this.elementData(offset + index);
14     }
15     
16     //...省略掉其他具体的定义过程
17 
18 }
复制代码

由此,我们从源代码角度明白了为什么//1处加入Integer类型对象编译错误,且//2处get()到的类型直接就是String类型了。

类型擦除

正确理解泛型概念的首要前提是理解类型擦除(type erasure) Java中的泛型基本上都是在编译器这个层次来实现的。在生成的Java字节代码不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会被编译器在编译的时候去掉。这个过程就称为类型擦除。如在代码中定义的List<Object>和List<String>等类型,在编译之后都会变成List。JVM看到的只是List,而由泛型附加的类型信息对JVM来说是不可见的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。类型擦除也是Java的泛型实现方式与C++模板机制实现方式之间的重要区别。
很多泛型的奇怪特性都与这个类型擦除的存在有关,包括:

  • 泛型类并没有自己独有的Class类对象比如并不存在List<String>.class或是List<Integer>.class,而只有List.class。
  • 静态变量是被泛型类的所有实例所共享的对于声明为MyClass<T>的类,访问其中的静态变量的方法仍然是 MyClass.myStaticVar。不管是通过new MyClass<String>还是new MyClass<Integer>创建的对象,都是共享一个静态变量。
  • 泛型的类型参数不能用在Java异常处理的catch语句中。因为异常处理是由JVM在运行时刻来进行的。由于类型信息被擦除,JVM是无法区分两个异常类型MyException<String>和MyException<Integer>的。对于JVM来说,它们都是 MyException类型的。也就无法执行与异常对应的catch语句。

类型擦除的基本过程也比较简单,首先是找到用来替换类型参数的具体类。这个具体类一般是Object。如果指定了类型参数的上界的话,则使用这个上界。把代码中的类型参数都替换成具体的类。同时去掉出现的类型声明,即去掉<>的内容。比如T get()方法声明就变成了Object get();List<String>就变成了List。接下来就可能需要生成一些桥接方法(bridge method)。这是由于擦除了类型之后的类可能缺少某些必须的方法。


泛型的好处

使用泛型的好处:

  • 强类型检查。在编译时就可以得到类型错误信息。
  • 避免显式强制转换。
  • 方便实现通用算法。

泛型的规则

使用泛型的规则:

  • 泛型的类型参数只能是类类型(类型的封转类Integer,Char,Double等,也包括自定义类),不能是简单类型(int, char,double)。
  • 同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的,不存在父子类关系,不同版本的泛型实际上都是同一种基本类型。  <public class Example {    public void print(List<Integer> integers) {}      public void print(List<Double> doubles) {} }>两个print方法会被认为同一类中的相同方法,因此编译错误:Erasure of method print(List<Integer>) is the same as another method in type Example。
  • 泛型的类型参数可以有多个,比如 public class Pair<T, V> { ....} 。
  • 静态字段的类型不能为类型参数,public class Box<T> {   private static T object; // compile-time error}。
  • 不能直接创建类型参数变量的数组,只能通过反射创建,因此 List<Integer>[] arrayOfLists = new List<Integer>[2];  // compile-time error 。
  • 泛型的参数类型可以使用extends和super关键字,例如<T extends superclass>、<T extends B1 & B2 & B3> 、<T super Number> 习惯上称为“有界类型”。
  • 泛型的参数类型还可以是通配符类型。例如Class<?> classType = Class.forName("java.lang.String")。

三.自定义泛型接口、泛型类、泛型方法、自定义泛型数组

从上面的内容中,大家已经明白了泛型的具体运作过程。也知道了接口、类和方法也都可以使用泛型去定义,以及相应的使用。是的,在具体使用时,可以分为泛型接口、泛型类和泛型方法。

自定义泛型接口、泛型类和泛型方法与上述Java源码中的List、ArrayList类似。如下,我们看一个最简单的泛型类和方法定义:

复制代码
 1 public class GenericTest {
 2 
 3     public static void main(String[] args) {
 4 
 5         Box<String> name = new Box<String>("corn");
 6         System.out.println("name:" + name.getData());
 7     }
 8 
 9 }
10 
11 class Box<T> {
12 
13     private T data;
14 
15     public Box() {
16 
17     }
18 
19     public Box(T data) {
20         this.data = data;
21     }
22 
23     public T getData() {
24         return data;
25     }
26 
27 } 
复制代码

在泛型接口、泛型类和泛型方法的定义过程中,我们常见的如T、E、K、V等形式的参数常用于表示泛型形参,由于接收来自外部使用时候传入的类型实参。那么对于不同传入的类型实参,生成的相应对象实例的类型是不是一样的呢?

复制代码
 1 public class GenericTest {
 2 
 3     public static void main(String[] args) {
 4 
 5         Box<String> name = new Box<String>("corn");
 6         Box<Integer> age = new Box<Integer>(712);
 7 
 8         System.out.println("name class:" + name.getClass());      // com.qqyumidi.Box
 9         System.out.println("age class:" + age.getClass());        // com.qqyumidi.Box
10         System.out.println(name.getClass() == age.getClass());    // true
11 
12     }
13 
14 }
复制代码

由此,我们发现,在使用泛型类时,虽然传入了不同的泛型实参,但并没有真正意义上生成不同的类型,传入不同泛型实参的泛型类在内存上只有一个,即还是原来的最基本的类型(本实例中为Box),当然,在逻辑上我们可以理解成多个不同的泛型类型。

究其原因,在于Java中的泛型这一概念提出的目的,导致其只是作用于代码编译阶段,在编译过程中,对于正确检验泛型结果后,会将泛型的相关信息擦出,也就是说,成功编译过后的class文件中是不包含任何泛型信息的。泛型信息不会进入到运行时阶段。

对此总结成一句话:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型

泛型接口

     1.在Java中除了可以定义类和方法,还可以定义泛型接口。泛型接口的作用和普通接口一样,只是它的实用性更强。对于很多具体类型通用的方法,可以将其提取到一个泛型接口中,再编写一个泛型类实现这个接口即可。    

     2.定义泛型接口和定义泛型类是相似的,直接在接口名称后面加上<T>即可。T就是泛型类型参数,可以是多个。     

     3.在实现此接口时要注意,实现类的泛型参数和接口的泛型参数要相匹配。


复制代码
package com.xhj.generics.used.ginterface;

/**
 * 定义一个泛型接口
 * 
 * @author XIEHEJUN
 * 
 */
public interface GenericComparableInterface {
    public <T extends Comparable<T>> T getMax(T[] array);
}
实现泛型接口
package com.xhj.generics.used.ginterface;

public class GenericComparableImp implements GenericComparableInterface {

    @Override
    public <T extends Comparable<T>> T getMax(T[] array) {
        if(array==null||array.length==0){
            return null;
        }else{
            T max = array[0];
            for (int i = 0; i < array.length; i++) {
                if(max.compareTo(array[i])<0){
                    max = array[i];
                }
            }
            return max;
        }
    }
    
    public static void main(String[] args) {
        GenericComparableImp gci = new GenericComparableImp();
        String[] strs = { "您好!我是和佑b,来自和佑博客园", "您好!我是和佑a,来自和佑博客园",
                "您好!我是和佑c,来自和佑博客园" };
        System.out.println("最小的类对象实例为:" +gci.getMax(strs));
    }
}
复制代码

 

注:泛型接口的应用 
    一个大型网站的后台往往使用多个数据表,可以将一些公共的操作如数据的增删改以及保存等放在一个泛型的DAO接口中定义, 
    在针对使用的持久层技术,编写此DAO的实现类,这些对于每一个持久化的对象,直接继承这个实现类,再去实现特有
方法即可。

 泛型类

原有的普通类定义

public class Box {
    public Object getObject() {
        return object;
    }

    public void setObject(Object object) {
        this.object = object;
    }

    private Object object;

   该类可以传给它任何你想要的对象,比如对象String,Integer等,也可以传入自定义的一些对象。但是 调用getObject方法返回的对象需要显式的强转为传入的类型,才能使用原来类型的一些方法。


泛型类定义

public class Box<T> {
    public T getObject() {
        return object;
    }

    private T object;

    public void setObject(T object) {
        this.object = object;
    }
}

因此使用泛型构建该类可以方便在实例化Box对象时,必须要给其指定一种类型,String,Integer或者自定义的类,并且调用getObject方法并不需要进行强转就可以使用该类型的方法。


同样也可以一个类中传入多个类型参数。例如下面的Pair对象

public class Pair<T, V> {
    private T key;
    private V value;

    public Pair(T key, V value) {
        this.key = key;
        this.value = value;
    }

    public T getKey() {
        return key;
    }

    public V getValue() {
        return value;
    }

使用方法如下:

Pair<Integer, String> one = new Pair<Integer, String>(1, "one");

Pair<String, String> hello = new Pair<String, String>("hello", "world");

 泛型方法

   在Java中,不仅可以声明泛型类,还可以声明泛型方法:         

   1.使用<T>格式来表示泛型类型参数,参数个数可多个;          

   2.类型参数列表要放在访问权限修饰符、static和final之后;          

   3.类型参数列表要放在返回值类型、方法名称、方法参数之前。



package com.xhj.generics.used.entity;

/**
 * 用户实体类
 * 
 * @author XIEHEJUN
 * 
 */
public class User {
    private String userName;
    private String userId;
    private int userAge;
    private String userAddress;
    private String gende;
    private long userTell;

    public User() {
        super();
    }

    public User(String userName, String userId, int userAge,
            String userAddress, String gende, long userTell) {
        this.userName = userName;
        this.userId = userId;
        this.userAge = userAge;
        this.userAddress = userAddress;
        this.gende = gende;
        this.userTell = userTell;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getUserId() {
        return userId;
    }

    public int getUserAge() {
        return userAge;
    }

    public void setUserAge(int userAge) {
        this.userAge = userAge;
    }

    public String getUserAddress() {
        return userAddress;
    }

    public void setUserAddress(String userAddress) {
        this.userAddress = userAddress;
    }

    public String getGende() {
        return gende;
    }

    public void setGende(String gende) {
        this.gende = gende;
    }

    public long getUserTell() {
        return userTell;
    }

    public void setUserTell(long userTell) {
        this.userTell = userTell;
    }

    @Override
    public String toString() {
        return "User{" + "\n\tuserId =" +userId+ "\n\tuserName =" + userName
                + "\n\tuserAge =" + userAge + "\n\tgende =" + gende
                + "\n\tuserAddress =" + userAddress + "\n\tuserTell =" + userTell
                + "\n\t}";
    }
}
复制代码

泛型数据访问操作类

复制代码
package com.xhj.generics.used.dao;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.List;

import org.apache.commons.dbutils.DbUtils;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.BeanListHandler;

/**
 * 数据库操作类,定义增删改查等操作方法
 * 
 * @author XIEHEJUN
 * 
 */
public class GenericQuery {
    private static String URL = "jdbc:oracle:thin:@192.168.100.13:1521:SIGMA";
    private static String DRIVRR = "ojdbc6";
    private static String USER = "PCD_Online_V2";
    private static String PASSWORD = "password";
    private static Connection con;

    /**
     * 获取数据库连接
     * 
     * @return
     */
    public static Connection getConnecton() {
        DbUtils.loadDriver(DRIVRR);
        try {
            con = DriverManager.getConnection(URL, USER, PASSWORD);
        } catch (SQLException e) {
            System.out.println("连接失败");
        }
        return con;
    }

    /**
     * 查询数据
     * 
     * @param sql
     *            SQL语句
     * @param type
     *            实体类类型
     * @return
     */
    @SuppressWarnings("unchecked")
    public static <T> List<T> query(String sql, Class<T> type) {
        QueryRunner qr = new QueryRunner();
        List<T> list = null;
        try {
            list = (List<T>) qr.query(getConnecton(), sql, new BeanListHandler(
                    type));
        } catch (SQLException e) {
            System.out.println("SQL语句不正确");
            e.printStackTrace();
        }finally{
            DbUtils.closeQuietly(con);
        }
        return list;
    }

    /**
     * 更新数据--增/删/改
     */
    public static void queryUpdate(String sql) {
        QueryRunner qr = new QueryRunner();
        try {
            qr.update(getConnecton(), sql);
        } catch (SQLException e) {
            e.printStackTrace();
        }finally{
            DbUtils.closeQuietly(con);
        }
    }
}
复制代码


业务操作类

 

复制代码
package com.xhj.generics.used.service;

import java.util.List;
import com.xhj.generics.used.dao.GenericQuery;
import com.xhj.generics.used.entity.User;

/**
 * 调用数据库操作方法,对数据进行增删改查等操作
 * 
 * @author XIEHEJUN
 * 
 */
public class Service {
    /**
     * 插入数据
     * 
     * @param user
     */
    public static void update(User user) {
        String sql = "insert into XHJUSER values('" + user.getUserId() + "','"
                + user.getUserName() + "','" + user.getUserAge() + "','"
                + user.getGende() + "','" + user.getUserAddress() + "','"
                + user.getUserTell() + "')";
        GenericQuery.queryUpdate(sql);
    }

    /**
     * 修改数据
     * 
     * @param sql
     */
    public static void update(String sql) {
        GenericQuery.queryUpdate(sql);
    }

    /**
     * 查询数据
     * 
     * @param user
     * @param sql
     */
    public static void select(User user, String sql) {
        List<User> list = GenericQuery.query(sql, User.class);
        System.out.println("表中数据有:");
        for (int i = 0; i < list.size(); i++) {
            System.out.println(i + "号对象属性值为:" + list.get(i));
        }
    }

    /**
     * 删除数据
     * 
     * @param user
     */
    public static void delete(String sql) {
        GenericQuery.queryUpdate(sql);
    }
}
复制代码

 

测试类:

复制代码
package com.xhj.generics.used.main;

import com.xhj.generics.used.entity.User;
import com.xhj.generics.used.service.Service;

/**
 * 测试类
 * 
 * @author XIEHEJUN
 * 
 */
public class Test {

    public static void main(String[] args) {
        User user = new User("B", java.util.UUID.randomUUID().toString(), 12,
                "湖南", "女", 1213344455);
        Service.update(user);

        String sql = "select * from XHJUser where username = 'B'";
        Service.select(user, sql);

        sql = "update XHJuser set username = 'D' where userid ='1ab3ee1b-1c52-43d2-8df5-ffefb92c9c5c'";
        Service.update(sql);

        sql = "delete from XHJuser where username = 'C'";
        Service.delete(sql);
    }

}
复制代码


注:泛型类与泛型方法的重要区别 
    1.在使用泛型类时,需要注意不能将泛型参数类型用于静态域和静态方法中,而对于泛型方法则可以是静态的。 
    2.这种区别主要是"擦除"产生的。由于在泛型方法中已经指明了参数的具体类型,故即使发生擦除,也不会丢失。

    
泛型化方法与最小值

    1.在Java中除了数值可以比较大小外,任何实现了Comparable接口的类的实例,都可以比较大小。 
    2.在比较类的对象是,需要限制比较的对象实现Comparable接口即:<T extends Comparable> 
    3.当泛型参数类型被限制为接口的子类型时,也使用extends关键字。 
   

代码实例:

复制代码
package com.xhj.generics.used;

/**
 * 利用泛型比较类对象实例大小
 * 
 * @author XIEHEJUN
 * 
 */
public class GenericComparable {
    /**
     * 比较并获取最小类对象实例
     * 
     * @param array
     * @return
     */
    public static <T extends Comparable<T>> T getMin(T[] array) {
        if (array.length == 0 || array == null) {
            return null;
        } else {
            T min = array[0];
            for (int i = 0; i < array.length; i++) {
                if (min.compareTo(array[i]) > 0) {
                    min = array[i];
                }
            }
            return min;
        }
    }

    public static void main(String[] args) {
        String[] strs = { "您好!我是和佑b,来自和佑博客园", "您好!我是和佑a,来自和佑博客园",
                "您好!我是和佑c,来自和佑博客园" };
        System.out.println("最小的类对象实例为:" + getMin(strs));
    }

}
复制代码


注:1.compareTo()方法先是逐步比较ASCII码,若是此时仍无法得出结果,再比较其长度 
     2.泛型类型参数的限定一般有两种情况: 
        a.小于某一个"范围" 
        b.大于某一个"范围" 
        范围即可以是一个类,也可以是一个接口,还可以是类和接口的组合,对于组合来说,需要将类放在第一位,并且用&分隔。 



自定义泛型化数组类

       1.在Java虚拟机中并没有泛型类型的对象,所有有关泛型的信息都被擦除了。这虽然可以避免C++语言的模版代码膨胀问题,但是也引起了其他问题。如:不能直接创建泛型数组等。      

       2.Java中的泛型不支持实例化类型变量。      

       3.通过Java的反射机制创建一个泛型化数组newInstance(Class<?> componentType,int length)


package com.xhj.generics.used;

import java.lang.reflect.Array;

/**
 * 利用Java反射机制泛型化数组
 * 
 * @author XIEHEJUN
 * 
 * @param <T>数组类型
 */
public class GenericsArray<T> {
    private T[] array;
    private int size;

    /**
     * 泛型化数组构造函数
     * 
     * @param type
     *            数组类型
     * @param size
     *            数组长度
     */
    @SuppressWarnings("unchecked")
    public GenericsArray(Class<T> type, int size) {
        array = (T[]) Array.newInstance(type, size);
        this.size = size;
    }

    /**
     * 向泛型化数组添加元素
     * 
     * @param index
     * @param item
     */
    public void put(int index, T item) {
        if (index >= 0 && index < size) {
            array[index] = item;
        }
    }

    /**
     * 根据数组下标获取相应值
     * 
     * @param index
     * @return
     */
    public T get(int index) {
        if (index >= 0 && index < size) {
            return array[index];
        }
        return null;
    }

    /**
     * 将泛型化数组打印输出
     * 
     * @param t
     */
    public void printService(T[] t) {
        put(0, t[0]);
        System.out.println("添加的元素为:" + get(0));
        put(1, t[1]);
        System.out.println("添加的元素为:" + get(1));
        put(2, t[2]);
        System.out.println("添加的元素为:" + get(2));
    }

    public static void main(String[] args) {
        System.out.println("向泛型化数组添加String元素");
        GenericsArray<String> gStrArray = new GenericsArray<String>(
                String.class, 3);
        String[] strs = { "您好!", "我叫和佑!", "我喜欢Java!" };
        gStrArray.printService(strs);

        System.out.println("\n向泛型化数组添加Integer元素");
        GenericsArray<Integer> gIntArray = new GenericsArray<Integer>(
                Integer.class, 3);
        Integer[] arrays = { 10, 52, 32 };
        gIntArray.printService(arrays);
    }

}
复制代码


总结: 
    Java泛型的局限性 
        1.不能使用基本类型作为其类型参数; 
        2.不能抛出或捕获泛型类型的实例、 
        3.不能直接使用泛型数组、 
        4.不能实例化类型变量 
        5.对于某些不足,可以通过Java的反射机制进行弥补。



四.类型通配符

接着上面的结论,我们知道,Box<Number>和Box<Integer>实际上都是Box类型,现在需要继续探讨一个问题,那么在逻辑上,类似于Box<Number>和Box<Integer>是否可以看成具有父子关系的泛型类型呢?

为了弄清这个问题,我们继续看下下面这个例子:

复制代码
 1 public class GenericTest {
 2 
 3     public static void main(String[] args) {
 4 
 5         Box<Number> name = new Box<Number>(99);
 6         Box<Integer> age = new Box<Integer>(712);
 7 
 8         getData(name);
 9         
10         //The method getData(Box<Number>) in the type GenericTest is 
11         //not applicable for the arguments (Box<Integer>)
12         getData(age);   // 1
13 
14     }
15     
16     public static void getData(Box<Number> data){
17         System.out.println("data :" + data.getData());
18     }
19 
20 }
复制代码

我们发现,在代码//1处出现了错误提示信息:The method getData(Box<Number>) in the t ype GenericTest is not applicable for the arguments (Box<Integer>)。显然,通过提示信息,我们知道Box<Number>在逻辑上不能视为Box<Integer>的父类。那么,原因何在呢?

复制代码
 1 public class GenericTest {
 2 
 3     public static void main(String[] args) {
 4 
 5         Box<Integer> a = new Box<Integer>(712);
 6         Box<Number> b = a;  // 1
 7         Box<Float> f = new Box<Float>(3.14f);
 8         b.setData(f);        // 2
 9 
10     }
11 
12     public static void getData(Box<Number> data) {
13         System.out.println("data :" + data.getData());
14     }
15 
16 }
17 
18 class Box<T> {
19 
20     private T data;
21 
22     public Box() {
23 
24     }
25 
26     public Box(T data) {
27         setData(data);
28     }
29 
30     public T getData() {
31         return data;
32     }
33 
34     public void setData(T data) {
35         this.data = data;
36     }
37 
38 }
复制代码

这个例子中,显然//1和//2处肯定会出现错误提示的。在此我们可以使用反证法来进行说明。

假设Box<Number>在逻辑上可以视为Box<Integer>的父类,那么//1和//2处将不会有错误提示了,那么问题就出来了,通过getData()方法取出数据时到底是什么类型呢?Integer? Float? 还是Number?且由于在编程过程中的顺序不可控性,导致在必要的时候必须要进行类型判断,且进行强制类型转换。显然,这与泛型的理念矛盾,因此,在逻辑上Box<Number>不能视为Box<Integer>的父类。

好,那我们回过头来继续看“类型通配符”中的第一个例子,我们知道其具体的错误提示的深层次原因了。那么如何解决呢?总部能再定义一个新的函数吧。这和Java中的多态理念显然是违背的,因此,我们需要一个在逻辑上可以用来表示同时是Box<Integer>和Box<Number>的父类的一个引用类型,由此,类型通配符应运而生。

类型通配符一般是使用 ? 代替具体的类型实参。注意了,此处是类型实参,而不是类型形参!且Box<?>在逻辑上是Box<Integer>、Box<Number>...等所有Box<具体类型实参>的父类。由此,我们依然可以定义泛型方法,来完成此类需求。

复制代码
 1 public class GenericTest {
 2 
 3     public static void main(String[] args) {
 4 
 5         Box<String> name = new Box<String>("corn");
 6         Box<Integer> age = new Box<Integer>(712);
 7         Box<Number> number = new Box<Number>(314);
 8 
 9         getData(name);
10         getData(age);
11         getData(number);
12     }
13 
14     public static void getData(Box<?> data) {
15         System.out.println("data :" + data.getData());
16     }
17 
18 }
复制代码

有时候,我们还可能听到类型通配符上限和类型通配符下限。具体有是怎么样的呢?

在上面的例子中,如果需要定义一个功能类似于getData()的方法,但对类型实参又有进一步的限制:只能是Number类及其子类。此时,需要用到类型通配符上限。

复制代码
 1 public class GenericTest {
 2 
 3     public static void main(String[] args) {
 4 
 5         Box<String> name = new Box<String>("corn");
 6         Box<Integer> age = new Box<Integer>(712);
 7         Box<Number> number = new Box<Number>(314);
 8 
 9         getData(name);
10         getData(age);
11         getData(number);
12         
13         //getUpperNumberData(name); // 1
14         getUpperNumberData(age);    // 2
15         getUpperNumberData(number); // 3
16     }
17 
18     public static void getData(Box<?> data) {
19         System.out.println("data :" + data.getData());
20     }
21     
22     public static void getUpperNumberData(Box<? extends Number> data){
23         System.out.println("data :" + data.getData());
24     }
25 
26 }
复制代码

此时,显然,在代码//1处调用将出现错误提示,而//2 //3处调用正常。

类型通配符上限通过形如Box<? extends Number>形式定义,相对应的,类型通配符下限为Box<? super Number>形式,其含义与类型通配符上限正好相反,在此不作过多阐述了。

    1.Java中的数组支持协变类型,即如果方法参数是数组T,而S是T的子类,则方法也可以使用参数S。对于泛型类则没有这个特性。 
      为了弥补这个不足,Java推出了通配符类型参数。 
    2.使用通配符"?"可以让泛型在实际应用中更加的灵活,如果只指定了<?>,而没有extends,则默认是允许Object及其下的任何Java类,也就是任意类 
    3.通配符可以利用"extends"关键字来设置取值上限,如:<? extends Number>,参数类型要求继承Number 
    4.通配符可以设置取值下限,如:<?super Number>,参数类型要求是Number的父类 
    5.通配符可有多个"界限",如:实现多个接口,在接口间用&分隔。

 

五.话外篇

虽然Java泛型简单的用 extends 统一的表示了原有的 extends 和 implements 的概念,但仍要遵循应用的体系,Java 只能继承一个类,但可以实现多个接口,所以你的某个类型需要用 extends 限定,且有多种类型的时候,只能存在一个是类,并且类写在第一位,接口列在后面,也就是:<T extends SomeClass & interface1 & interface2 & interface3>

这里的例子仅演示了泛型方法的类型限定,对于泛型类中类型参数的限制用完全一样的规则,只是加在类声明的头部,如:

public class Demo<T extends Comparable & Serializable>{
        //T类型就可以用Comparable声明的方法和Seriablizable所拥有的特性了
    }


最后再强调一点,就是泛型最重要的作用就是提高了代码的安全性,因为它能够在编译期对代码进行检查,从而避免了很多在运行期强转类型发生的异常。了解了泛型出现的目的,相信你也就知道该怎么使用泛型了吧!

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值