Java基础篇笔记(六):Java泛型基础

一、引用泛型

在Java集合中,有个缺点就是把一个对象“丢进”集合里之后,集合就会“忘记”这个对象的数据类型,当再次取出该对象时,该对象的编译类型就变成了Object类型(其运行时类型没有变)。
Java集合这个设计也并无不妥,因为集合的设计者并不知道我们会用集合来保存什么类型的对象,所以他们把几个设计成能保存任何类型的对象。这种设计有很明显的缺陷:

集合对元素类型没有任何类型,这样可能引发一些问题。比如像创建一个只能保存String对象的集合,但程序也可以轻易的将Integer对象丢进去。
把对象“丢进”集合时,集合丢失了对象的状态信息,集合只知道它盛装的时Object,因此取出集合元素后通常还需要进行强制类型转换。这种强制类型转换既增加了编程的复杂度,也可能引发ClassCastException异常。

代码例:

public class ListErr {
    public static void main(String[] args){
        List strList=new ArrayList();
        strList.add("QAQ");
        strList.add("QWQ");
        //不小心把一个Integer对象丢进了集合
        strList.add(666);
        //将会发生ClassCastException异常
        strList.forEach(str->System.out.println(((String)str)));//此处强制转换,限定了为String类型。
    }
}

二、使用泛型

我们改进一下上述代码的例子:

public class GenericList {
    public static void main(String[] args){
        List<String> strList=new ArrayList<String>();
        strList.add("QAQ");
        strList.add("QWQ");
        //这行代码直接引发编译异常,因为前面强制了String类型。
        strList.add(666);
        //此处不用强制转换类型,因为strList已经记住了所有集合元素为String类型。
        strList.forEach(str->System.out.println(str));
    }
}

Java7以前,如果使用带泛型的接口、类定义变量,那么调用构造器创建对象时构造器的后面也必须带泛型,这就显得有些多余了。

List<String> strList=new ArrayList<String>();
Map<String,Integer> map=new HashMap<String,Integer>();

Java7之后,允许在构造器后不需要带完整的泛型信息,直接写一个<>即可:

List<String> strList=new ArratList<>();
Map<String,Integer> map=new HashMap<>();

三、深入泛型

所谓泛型,就是允许在定义类、接口、方法时使用类型形参,这个类型形参(或叫泛型)将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型参数,也可称为类型实参)。在Java5中,改写了集合框架中的全部接口和类,为这些接口、类增加了泛型支持,从而可以在声明集合变量、创建几个对象时传入类型实参,这就是在前面程序中看到的List<String>ArrayList<String>两种类型。

如下代码定义了一个带泛型声明的Huang<H>类,使用该类时就可以为H形参传入实际类型。

public class HuangHE<H> {
    private H info;
    //下面方法用H类型来定义构造器:
    public Huang(H info){
        this.info=info;
    }
    public void setInfo(H info){
        this.info=info;
    }
    public H getInfo(){
        return this.info;
    }
    public static void main(String[] args){
        //下面传给H形参的是String,所以构造器的参数只能是String:
        Huang<String> h1=new Huang<>("黄鹤");
        System.out.println(h1.getInfo());
        //传给H形参的是Double,所以构造器的参数只能是Double
        Huang<Double> h2=new Huang<>(11.11);
        System.out.println(h2.getInfo());
    }
}

当创建带泛型声明的自定义类,为该类定义构造器时,构造器名还是原来的类名。比如上述为Huang<H>类定义构造器,其构造器名依然是Huang,而不是Huang<H>。但是调用该构造器时却可以使用Huang<H>的形式,为H形参传入实际的类型参数。

当创建了带泛型声明的接口、父类之后,可以为该接口创建实现类,或从该父类派生子类,需要指出的是,当时用这些接口、父类时不能再包含泛型形参,例如如下代码就是错误的:

//定义类Hu继承Huang类,Huang类不能跟泛型形参。
public class Hu extends Huang<H>{}

定义方法时可以声明数据形参,调用方法(使用方法)时必须为这些数据形参传入实际的数据;与此类似的是,定义类、接口、方法时可以声明泛型形参,使用类、接口、方法时应该为泛型形参传入实际的类型。
如果想从Huang类派生出一个子类,则可以这么改:

//使用Huang类时为H形参传入String类型
public class Hu extends Huang<String>{}

调用方法时必须为所有的数据形参传入参数值,与调用方法不同的是,使用类、接口时也可以不为泛型形参传入实际的类型参数,即如下的代码也是正确的:

public class Hu extends Huang{}

像这种使用Huang类时省略泛型的形式被称为原始类型
如果从Huang<String>类派生子类,则在Huang类中所有使用H类型的地方都会被替换成String类型。如果子类要重写父类的方法,就必须注意这一点。

public H1 extends Huang<String>{
    public String getInfo(){
        return "子类"+super.getInfo();
    }
    /*这么写是错误的,重写父类方法时返回值类型不一致
    public Object getInfo(){
        return "子类";
    }
    */

如果使用Huang类时没有传入实际的类型(即使用原始类型),Java编译器可能会发出警告:使用了未经检查或不安全的操作。

泛型形参不一样,但实际上,如果类都是一样的,则它们就是一样的,例如如下代码:

List<String> l1=new ArrayList<>();
List<Integer> l2=new ArrayList<>();
//调用getClass()方法来比较l1和l2是否相等
System.out.println(l1.getClass()==l2.getClass());
//输出结果为true,因为不管泛型的实际类型参数是什么,它们在运行时总有同样的类(class)。

不管为泛型形参传入哪一种类型实参,对于Java来说,它们依然被当成同一个类处理,在内存中也只占用一块内存空间,因此在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用泛型形参

为了表示各种泛型的父类,我们可以使用类型通配符,类型通配符是一个问号(?),即可以写作List<?>,Set<?>,Collection<?>,Map<?,?>等等。但是这种带通配符的Type仅仅表示它是各种泛型Type的父类,并不能把元素加入其中,因为程序无法确定其中元素的类型,所以不能向其中添加对象。

设置通配符的上限,一般用<? extends Name>来指定通配符的上限为Name。
设置通配符的下限,一般用<? super Name来指定通配符的下限为Name。
设置泛型形参的上限,一般用<? extends typeName来指定泛型形参的上限为什么类型

四、泛型方法

如果现在需要实现这样一个方法–该方法负责将一个Object数组的所有元素添加到一个Collection集合中。可以考虑这么实现:

static void fromArrayToCollection(Object[] a,Collection<Object> c){
    for(Object o:a){
        c.add(o);
    }

定义方法无任何问题,关键在于方法中的c形参,它的数据类型是Collection< Object >,Collection< String >不是Collection< Object >的子类型–所以这个方法功能十分有限,只能将Object[] 数组的元素复制到元素为Object(Object的子类也不行)的Collection集合中。所以下面的代码出现错误:

String[] strArr={"a","b"};
List<String> strList=new ArrayList<>();
//Collection<String>对象不能当成Collection<Object>使用,下面代码会出现编译错误
fromArrayToCollection(strArr,strList);

上面方法的参数类型不可以使用Collection< String >,使用Collection< ? >也不可行,因为Java不允许把对象放进一个未知类型的集合中。
为了解决这种问题,Java 5提供了泛型方法,即在声明方法时定义一个或多个泛型形参:

修饰符<T,S> 返回值类型 方法名(形参列表){方法体...}

我们可以把上面定义方法的代码改成如下形式:

static <T> void fromArrayToCollection(T[] a,Collection<T> c){
    for(T o:a){
        c.add(o);
    }
} 

以下为完整的用法:

public class GenericMethodTest {
    static <T> void fromArrayToCollection(T[] a, Collection<T> c){
        for(T o:a){
            c.add(o);
        }
    }
    public static void main(String[] args){
        Object[] oa=new Object[100];
        Collection<Object> co=new ArrayList<>();
        //下面代码中T表示Object类型
        fromArrayToCollection(oa,co);
        String[] sa=new String[100];
        Collection<String> cs=new ArrayList<>();
        //下面代码中T表示String类型
        fromArrayToCollection(sa,cs);
        fromArrayToCollection(sa,co);
        Integer[] ia=new Integer[100];
        Float[] fa=new Float[100];
        Number[] na=new Number[100];
        Collection<Number> cn=new ArrayList<>();
        //下面代码中T表示Number类型
        fromArrayToCollection(ia,cn);
        //下面代码中T表示Number类型
        fromArrayToCollection(fa,cn);
        //下面代码中T表示Number类型
        fromArrayToCollection(na,cn);
        //下面代码中T表示Object类型
        fromArrayToCollection(na,co);
        //下面代码中T表示String类型,但是na是一个Number数组,Number既不是String类型
        //也不是它的子类,所以会出现编译错误
        fromArrayToCollection(na,sa);
    }
}

泛型方法和类型通配符的区别:
大多数的时候都可以使用泛型方法来代替类型通配符,例如对于Java的Collection接口中两个方法的定义:

public interface Collection<E>{
    boolean containsAll(Collection<?> c);
    boolean addAll(Collection<? extends E> c);
}

上面集合中两个方法的形参都采用了类型通配符的形式,也可以采用泛型方法的形式:

public interface Collection<E>{
    <T> boolean containsAll (Collection<T> c);
    <T extends E> boolean addAll(Collection<T> c);
}

上面的方法使用了<T extends E>泛型形式,定义了泛型形参的上限E(E是Collection接口里定义的泛型,在该接口里E可当成普通类型使用)。这两种方法泛型形参T只使用了一次,泛型形参T产生的唯一效果是可以在不同的调用点传入不同的实际类型。对于这种情况,应该使用通配符:通配符是被设计用来支持灵活的子类化的。
泛型方法允许泛型形参被用来表示方法的一个或多个参数之间的类型依赖关系,或者方法返回值与参数之间的类型依赖关系。如果没有这样类型的依赖关系,就不应该使用泛型方法。
如果某个方法中一个形参(a)的类型或返回值的类型依赖于另一个形参(b)的类型,则形参(b)的类型声明不应该使用通配符。因为(a)依赖(b),如果形参(b)的类型无法确定,程序就无法定义形参(a)的类型。这种情况下就只能考虑使用在方法签名中声明泛型,即泛型方法。

五、擦除和转换

在严格的泛型代码里,带泛型声明的类总应该带着类型参数。但为了与老的Java代码保持一致,也允许在使用带泛型声明的类时不指定实际的类型。如果没有为这个泛型类指定实际的类型,此时被称为raw type(原始类型),默认是声明该泛型形参时指定的第一个上限类型。当把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,所有在尖括号之间的类型信息都将被扔掉。比如一个List<String>类型被转换为List,则该List对集合元素的类型检查变成了泛型参数的上限(即Object)。Java又允许直接把List对象赋给一个List<Type>(Type可以是任何类型)类型的变量,只是会发出“未经检查的转换”警告,这就是转换。

猜你喜欢

转载自blog.csdn.net/laobanhuanghe/article/details/98207371