对Java泛型的理解

1 泛型的定义

  泛型的定义:参数化类型。将具体的数据类型参数化,在使用/调用时再传入具体的类型。
  如何理解呢?参考下面的例子:

ArrayList list = new ArrayList();
list.add("xx");
list.add(123);

for (int i = 0;i < list.size;i++) {
    String s = (String) list.get(i);
}

  我们定义了一个ArrayList,其中存储的是一个Object数组,故而可以存储任意类型,可以存字符串“xx”,也可以存整形123。但是当我们调用时,会发现报ClassCastException。
  如何解决?我们可以定义一个专门用来存储特定一种类型对象的ArrayList,但是不可能每当用到一个类型就去定义一个对应该类型的ArrayList。泛型就可以很好的解决这个问题。我们将ArrayList内部的数组类型参数化为T,通过在外部调用ArrayList时指定具体的类型。如此,我们希望ArrayList存储String,我们就指定泛型参数T类型为String,即ArrayList。这个时候在add(123)的话,编译器会提示错误。

public ArrayList {
    private T[] value;
    private int size;

    ......
}

ArrayList list = new ArrayList<>();
ArrayList list2 = new ArrayList<>();

  这里举的例子是泛型应用在类上,ArrayList称之为泛型类。除此之外,泛型还可以应用在接口和方法上。

2 使用泛型

2.1 泛型类

  使用泛型类时,把泛型参数指定为为需要的class类型,例如:ArrayList,ArrayList即可;
  可以省略编译器能自动推断出的类型,例如:List list = new ArrayList<>();;
  如果不指定泛型参数类型时,编译器会给出警告,并且将视为Object类型;

2.2 泛型接口

  泛型接口的定义与泛型类是类似的,如下:

public interface List {
    public T next();
}

  泛型接口的实现类分两种情况:

//1.实现未指定泛型参数具体类型的接口
//此种情况,实现类必须声明与接口一样的泛型参数
public ArrayList implements List {
    @Override
    public T next() {
        ......
    }
}

//2.实现指定泛型参数具体类型的接口
public ArrayList implements List {
    @Override
    public String next() {
        ......
    }
}
2.3 泛型方法
/**
 * 泛型方法的基本介绍
 * @param tClass 传入的泛型实参
 * @return T 返回值为T类型
 * 说明:
 *     1)public 与 返回值中间的非常重要,可以理解为声明此方法为泛型方法。
 *     2)只有声明了的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
 *     3)表明该方法将使用泛型参数T,此时才可以在方法中使用泛型参数T。
 *     4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
 */
public  T genericMethod(Class tClass) throws InstantiationException, IllegalAccessException {
        T instance = tClass.newInstance();
        return instance;
}

  需要注意:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。因为静态方法无法访问类声明的泛型参数。
  在使用泛型方法时,既可以指定泛型类型,也可以不指定。如下:

public Test {
    public static void main(String[] args) {
        //不指定泛型类型时
        //当不指定泛型类型时,泛型类型为传入参数类型的最小共同父类
        Integer a = Test.next(1, 2);//传入参数都是Integer,故泛型参数类型为Integer
        Number a = Test.next(1, 2.1);//传入了一个Integer,一个Double,它们的最小共同父类是Number,故泛型参数是Number
   
        //不指定泛型类型时
        //指定了泛型类型,那么传入参数只能为指定类型的本身类型及其子类,否则会报错
        Integer c = Test.next(1, 2);
        Number d = Test.next(1, 1.2);
    }
 

    //定义了一个泛型方法
    static  T next(T t1, T t2) {
        return t1;
    }
}

  在声明泛型类、泛型接口或泛型方法时,可以同时声明多个泛型参数。

3 类型擦除

  Java的泛型是伪泛型。Java的泛型基本上都是在编译器这个层次上实现的,在编译后生成的字节码中是不包含泛型的类型信息的,使用泛型的时候指定具体类型,在编译器编译的时候会去掉,这个过程称为类型擦除。对于JVM来说,它不知道泛型的具体信息。
  泛型参数经过类型擦除后会得到一个原始类型,通常情况下为其限定类型,如果泛型参数没有限定类型,则为Object类型。例如:ArrayList经过类型擦除之后的类型是ArrayList,ArrayList经过类型擦除后的类型为ArrayList

3.1 类型检查

  Java编译器是先对代码进行类型检查,在进行类型擦除,然后再编译。
  这也就是为什么ArrayList编译后是ArrayList,但是在代码里面不能add一个字符串。编译器检查到ArrayList指定了泛型类型为Integer,add字符串,会提示错误。

ArrayList list = new ArrayList<>();
list.add("xx");

ArrayList list2 = new ArrayList();
list2.add(12);

new ArrayList().add("xx");

ArrayList list3 = new ArrayList();

ArrayList list4 = new ArrayList();

  可以看到类型检查是针对引用的。对一个引用指定了泛型类型,那么只能add该类型的对象。但是我们看到 直接初始化一个对象,也会对其进行类型检查
  注意:编译器不允许最后两种用法

3.2 对类型擦除的个人理解

  实际上,类型擦除可以理解为,在“写”的时候,用Obejct类型(或其限定类型)的引用指向泛型类型的对象。而在“读”的时候,将Object类型(或其限定类型)引用向下转型为原来的泛型类型。

public class cp3 {
    public static void main(String[] args) {
        String[] a = asArray("xa", "xx");
        System.out.println(Arrays.toString(a));
        String[] b = cp3.pickTwo("xxx", "kkk", "123");
        System.out.println(Arrays.toString(b));

        ArrayList list = new ArrayList<>();
        list.add("12");
        System.out.println(list.get(0));
    }

    static  K[] pickTwo(K k1, K k2, K k3) {
        return asArray(k1, k2);
    }

    static  T[] asArray(T... objs) {
        return objs;
    }
}

//输出结果为:
[xa, xx]
Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
    at cp3.main(cp3.java:11)

  可以看到报错了,第11行,也就是代码中的第二个输出那里。那么为什么呢?报的错显示,强转错误,Object数组不能强转为String数组。我们先使用javac指令编译源码,再使用jad反编译得到字节码文件,最终得到反编译后的代码:

public class cp3
{

    public cp3()
    {
    }

    public static void main(String args[])
    {
        String a[] = (String[])asArray(new String[] {
            "xa", "xx"
        });
        System.out.println(Arrays.toString(a));
        String b[] = (String[])pickTwo("xxx", "kkk", "123");
        System.out.println(Arrays.toString(b));
        ArrayList list = new ArrayList();
        list.add("12");
        System.out.println((String)list.get(0));
        System.out.println(((String[])Cnm.createArray(java/lang/String)).getClass());
    }

    static Object[] pickTwo(Object k1, Object k2, Object k3)
    {
        return asArray(new Object[] {
            k1, k2
        });
    }

    static transient Object[] asArray(Object objs[])
    {
        return objs;
    }
}

  可以看到,对于pickTwo方法,被擦除为Object类型,相当于传入了三个Object类型的引用,这三个引用指向的是三个字符串,在其内部,调用asArray方法时,传入了一个Object类型的可变参数,实际上是初始化了一个Object数组,即new Object[] {},返回值即这个Object数组,pickTwo方法也返回这个Object数组,这个Object数组是无法强转为String数组的,所以报错。
  但是第一行输出为什么不报错呢?对于asArray方法,传入的是一个Object[]类型的引用,指向一个String数组,将这个数组直接返回,是可以强转为String数组的。
  原理很简单:一个父类引用指向子类对象,这是向上转型,我们可以将这个引用进行向下转型为该子类类型。

    String[] s = {"xx", "kk"};
    Object[] o = s;//向下转型
    String[] s1 = (String[]) o;//向上转型,强转成功

    Object[] o1 = new Object[] {"xx", "kk"};
    String[] s2 = (String[]) o1;//强转失败
3.3 类型擦除与多态的冲突及解决

  假设我们在泛型接口Pair里面写了一个方法,它的实现类DataPair(在声明类时指定了接口的泛型类型为Date,不是泛型类)实现了这个方法,编译后,接口里面的泛型类型被擦除为原始类型Obejct,但是实现类里面的类型仍为指定的类型,这使得该方法不再是被重写,而是发生了重载,实现类还应继承了一个参数类型为Object类型的方法。
  但是实际情况并非如此,Java编译器我们实现了一个桥方法。在反编译后的代码中,我们看到:桥方法重写了接口中的该方法,并且在桥方法中调用了我们自己重写的方法。桥方法对外是不可见的,所以呈现的效果是我们自己写的方法重写了原接口的方法。

interface Pair {
    T getValue();
    void setValue(T value);
}

class DataPair implements Pair {

    Date value;

    @Override
    public Date getValue() {
        return value;
    }

    @Override
    public void setValue(Date value) {
        this.value = value;
    }
}

//反编译后的代码
class DataPair
    implements Pair
{

    DataPair()
    {
    }

    public Date getValue()
    {
        return value;
    }

    public void setValue(Date value)
    {
        this.value = value;
    }

    public volatile void setValue(Object obj)
    {
        setValue((Date)obj);
    }

    public volatile Object getValue()
    {
        return getValue();
    }

    Date value;
}

参考

ttps://www.cnblogs.com/wuqinglong/p/9456193.html
https://www.cnblogs.com/coprince/p/8603492.html

你可能感兴趣的:(对Java泛型的理解)