Fork me on GitHub

泛型

注意:所有文章除特别说明外,转载请注明出处.

泛型

泛型表示将集合中的一个元素限定为一个特定的类型。参数化类型,将类型由原来的具体的类型参数化,类似于方法中变量参数。在使用泛型类时,虽然传入了不同的泛型参数,但是并没有真正意义上生成不同的类型,传入不同的泛型实参的泛型类在内存上只有一个,没有真正意义上生成不同的类型,传入不同参数的泛型类在内存上只有一个,即还是原来的基本类型,在逻辑上可以理解成多个不同的泛型类型。

提示:foreach()方法作为遍历集合中的所有元素的方法。

总结:泛型类型在逻辑上可以看成多个不同的类型,但是在实际上都是相同的基本类型。

ArrayList<E> -- 泛型类型
ArrayList -- 原始类型
E -- 类型参数
<> -- 读作"typeof"
ArrayList<Integer> -- 参数化的类型
Integer -- 实际类型参数

注意:1.参数类类型与原始类型相互兼容。

ArrayList  collection1 = new ArrayList<Integer>();//通过,无warning
ArrayList<Integer> collection2 = new ArrayList();//通过,有warning

注意:2.参数类型不考虑类型的继承关系。

ArrayList<String> collection3 = new ArrayList<Object>();//编译不通过
ArrayList<Object> collection4 = new ArrayList<String>();//编译不通过

ArrayList collection5 = new ArrayList<Integer>();
ArrayList<String> collection6 = collection5;//编译通过

注意:ArrayList像一种新的特殊的ArrayList类,但是实际上系统并没有为ArrayList生成新的class文件,并且不会把ArrayList当做一种新的类型来处理,所以不存在泛型类这一说。不管泛型的类型形参传入哪一种类型实参,对于Java而言,他们依然被当成同一个类型来处理,在内存中也占用一块内存空间,故而在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用类型形参。

//下面程序错误,不能再静态变量声明中使用类型形参
public class R<T>{
    static T info;
    //不能再静态方法声明中使用类型形参
    public static void bar(T msg){

    }
}

1.”?”通配符

“?”通配符表示任意类型,使用”?”通配符可以引用各种参数化的类型,可以调用与参数化无关的方法(如:size()方法),不能调用与参数化有关的方法(如:add()方法)。

通配符的扩展:

1.限定通配符的上边界
ArrayList<? extends Number > collection1= new ArrayList<Integer >();//编译通过
ArrayList<? extends Number > collection2= new ArrayList<String>();//编译不通过

2.限定通配符的下边界
ArrayList<? super Integer > collection3= new ArrayList<Number>();//编译通过
ArrayList<? super Integer > collection4= new ArrayList<String>();//编译不通过

2.自定义泛型方法

Java的泛型基本上完全在编译器中实现,用于编译器执行类型检查和类型判断,然后生成普通的非泛型的字节码,这种实现技术为“擦除”(erasure)。

注意:因为泛型是提供给javac编译器使用的,限定集合的输入类型,编译器编译带类型说明的集合时会去掉“类型”信息。

擦除实例:

public class GenericTest {
    public static void main(String[] args) {
        new GenericTest().testType();
    }

    public void testType(){
        ArrayList<Integer> collection1 = new ArrayList<Integer>();
        ArrayList<String> collection2= new ArrayList<String>();

        System.out.println(collection1.getClass()==collection2.getClass());
        //两者class类型一样,即字节码一致

        System.out.println(collection2.getClass().getName());
        //class均为java.util.ArrayList,并无实际类型参数信息
    }
}

输出:

true
java.util.ArrayList

注意:使用反射可以跳过编译器,向某个泛型集合中注入其他类型数据。

注意:只有引用类型才能作为泛型方法的实际参数。如:

public class GenericTest {
    public static void main(String[] args) {
        swap(new String[]{"111","222"},0,1);//编译通过

        //swap(new int[]{1,2},0,1);
        //编译不通过,因为int不是引用类型

        swap(new Integer[]{1,2},0,1);//编译通过
    }

    /*交换数组a 的第i个和第j个元素*/
    public static <T> void swap(T[]a,int i,int j){
        T temp = a[i];
        a[i] = a[j];
        a[j] = temp;
    }
}

提示:基本类型有些时候可以作为实参,因为自动装箱与拆箱。如:

//该实例还表明当实参不一致时,T取交集,即第一个共同的父类。
public class GenericTest {
    public static void main(String[] args) {
        new GenericTest().testType();
        int a = biggerOne(3,5);
        //int 和 double,取交为Number
        Number b = biggerOne(3,5.5);
        //String和int 取交为Object
        Object c = biggerOne("1",2);
    }
    //从x,y中返回y
    public static <T> T biggerOne(T x,T y){
        return y;
    }
}

3.类型参数推断

编译器在判断泛型方法的实际类型参数的过程称之为类型推断。

1.当某个 类型变量 只在整个 参数列表 的 所有参数 和 返回值 中的一处被应用,那么根据调用方法时该处的 实际应用类型 来确定。即 直接根据调用方法 时 传递的参数类型 或 返回值 来决定 泛型参数 的类型。

如:
    swap(new String[3],1,2) -> static <E> void swap(E[]a,int i,int j);

2.当某个 类型变量 在整个 参数列表 的 所有参数 和 返回值 中的 多处被应用 ,如 调用方法 时这么多处的 实际应用类型 都对应 同一种类型 ,则 泛型参数的类型 就是 该类型 。

如:
    add(3,5) -> static <T> T add(T a,T b);

3.当 某个类型变量 在整个 参数列表 的 所有参数 和 返回值 中的*多处被应用了,如果 调用方法 时这么多处的 实际应用类型 对应 不同的类型 ,且没有 返回值 ,则取多个参数中的 最大交集类型 ,即第一个公共父类。

如:
    fill(new Integer[3],3.5) -> static <T> void fill(T a[],T v);

4.当某个 类型变量 在整个 参数列表 的 所有参数 和 返回值 中的 多处被应用 ,如果 调用方法 时这么多处的 实际应用类型 对应不同的 类型 ,且使用有 返回值 ,则优先考虑 返回值的类型 。

如:
    int x = add(3,3.5) -> static <T> T add(T a,T b);

5.参数类型的类型推断具有传递性。

如:
    //该例推断的实际类型为Object,编译通过
    copy(new Integer[5],new String[5]) -> static <T> void copy(T []a,T []b);

    //该例则根据参数化的ArrayList类实例将类型变量直接确定为String类型,编译报错
    copy(new ArrayList<String>,new Integer[5]) -> static <T> void copy(Collection<T>a,T[]b);

4.自定义泛型

如:
    public class GenericDao<T>{
        public void add(T x){
        }

        public T findById(int id){
            return null;
        }

        public void delete(T obj){
        }

        public void delete(int id){
        }

        public void update(T obj){
        }

        public T findByUserName(String name){
            return null;
        }

        public <T> Set<T> findByConditions(String where){
            return null;
        }

    }

注意:当一个变量被声明为泛型时,只能被实例变量和方法调用(还有内嵌类型),而不能被静态变量和静态方法调用。因为静态成员是被所参数化的类所共享的,所以静态成员不应该有类级别的类型参数。

5.泛型方法与泛型类的比较

如:
    public class A<T>(){
        //泛型类的成员方法,该T受A后面的T的限制
        public T memberFunc(){
            return null;
        }
        //泛型方法,这里的T和和类A的T是不同的
        public static <T> T genericFunc(T a){
            return null;
        }
        public static void main(String[] args) {
            //编译不通过
            //Integer i = A<String>().findByUserName("s");

            //编译通过
            Set<Integer> set=  A<String>().findByConditions("s");
        }
    }

总结:从这个例子可以得到泛型方法中的T与类中的T是不相同的。

6.泛型与反射

1.通过反射获取泛型的实际类型参数

将泛型变量当成方法的参数,利用method类的getGenericParameterTypes()方法来获取泛型的实际类型参数。

如:
    public class GenericTest {
        public static void main(String[] args) throws Exception {
            getParamType();
        }

         /*利用反射获取方法参数的实际参数类型*/
        public static void getParamType() throws NoSuchMethodException{
            Method method = GenericTest.class.getMethod("applyMap",Map.class);
            //获取方法的泛型参数的类型
            Type[] types = method.getGenericParameterTypes();
            System.out.println(types[0]);
            //参数化的类型
            ParameterizedType pType  = (ParameterizedType)types[0];
            //原始类型
            System.out.println(pType.getRawType());
            //实际类型参数
            System.out.println(pType.getActualTypeArguments()[0]);
            System.out.println(pType.getActualTypeArguments()[1]);
        }

        /*供测试参数类型的方法*/
        public static void applyMap(Map<Integer,String> map){

        }

    }

output:

java.util.Map<java.lang.Integer, java.lang.String>
interface java.util.Map
class java.lang.Integer
class java.lang.String

7.泛型派生类

当创建带泛型类型声明的接口、父类时,可以为该接口创建实现类,或从该父类派生子类,当使用这些接口和父类时,不能再包含类型形参。

//定义类A继承Apple类,Apple类不能再跟类型形参
pulic class A extends Apple<T>{};//这样的定义方式是错误的

//如果想从Apple类派生一个子类,使用Apple类时为T形参传入一个String类型
public class A extends Apple<String>


扩展:泛型

泛型的好处:
    1.将运行时的异常提前到编译阶段.
    2.避免了无谓的强制类型转换

注意:泛型是jdk1.5加的特性

泛型在集合中的常见应用:

注意:泛型没有多态的概念,左右两边的数据类型必须要一致,或者只写一边的泛型类型。

推荐使用:两边都写泛型

自定义泛型:自定义泛型就是一个数据类型的占位符或者是一个数据类型的常量。

在方法上自定义泛型:

修饰符 <声明自定义的泛型>返回值类型 函数名(使用自定义的泛型){

    }

方法泛型注意事项:
1.在方法上自定义泛型,这个自定义泛型的具体数据类型是在调用该方法时传入实参时确定具体的数据类型的。
2.自定义泛型只要符合标识符的命名规则即可,但自定义泛型我们一般都习惯使用一个大写字母(E T)表示。

注意:在泛型中不能使用基本数据类型,如果需要使用基本数据类型,那么就使用基本数据类型对应的包装类型
如:

char(基本类型)----->Character(包装类型)
int------>Integer
boolean-->Boolean

泛型类

泛型类的自定义格式:

class 类名<声明自定义类型>{

    }

泛型类要注意的事项:

1.在类上自定义泛型的具体数据类型是在使用该类的时候创建对象时确定的。
2.如果一个类在类上已经声明了自定义类型,且如果使用该类创建对象的时候没有指定泛型的具体数据类型,那么默认为Object类型。
3.在类上自定义泛型不能作用于静态的方法,如果静态的方法需要使用自定义泛型,那么需要在方法上自己声明使用。

泛型接口:

泛型接口的定义格式:

interface 接口名<声明自定义类型>{

}

泛型接口要注意的事项:
1.接口上自定义的泛型的具体数据类型是在实现一个接口的时候指定的。
2.在接口上自定义的泛型如果在实现接口的时候没有指定具体的数据类型,那么默认为Object类型

需求:目前实现一个接口的时候,不明确目前要操作的数据类型,需要等待创建接口实现类对象的时候时候才能指定泛型的具体数据类型。

如果要延长接口自定义泛型的具体数据类型,格式如下:

public class Demo4<T> implements Dao<T>{

        }

泛型的上下限:

需求1:定义一个函数可以接收任意类型的集合对象,要求接收的集合对象只能存储Interger或者是Integer的父类类型数据。
需求2:定义一个函数可以接收任意类型的集合对象,要求接收的集合对象只能存储Number或者是Number子类类型数据。

泛型中的通配符:?

例:? super Integer:表示只能存储Integer或者Integer父类元素 泛型的下限
    ? extends Number:表示只能存储Number或者Number类型的子类数据 泛型的上限

本文标题:泛型

文章作者:Bangjin-Hu

发布时间:2019年10月15日 - 09:22:26

最后更新:2020年03月30日 - 08:10:17

原始链接:http://bangjinhu.github.io/undefined/Java 泛型/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

Bangjin-Hu wechat
欢迎扫码关注微信公众号,订阅我的微信公众号.
坚持原创技术分享,您的支持是我创作的动力.