java 泛型学习笔记(一)

  • Post author:
  • Post category:java


泛型

JDK在1.5增加泛型。在没有泛型之前我们把对象“丢进”集合中,集合就会忘记对象的类型,把所有对象都当作object处理。当集合从程序中取出的时候还需要强制类型转换。这样有两个坏处:1,强制类型转换的代码使程序显得臃肿。2,容易引起ClassCastException异常。

1.5增加了泛型之后集合可以记住对象的类型,如果添加的对象不满足要求就会在编译时提示错误(在编译时做了类型检查)。而且在取出对象是也不用在做强制类型转换。使代码更加健壮和简洁。

那到底什么是泛型呢?泛型就是在定义类或接口是制定类型形参,在创建类和接口时传入类型实参。例如:

//定义类时E就是类型形参
public class AClass<E>{
    public void printInfo(E info){
        System.out.println(info);
    }
}
//在创建类时String就是类型实参,AClass所有E都将被替换成String类型
AClass ac = new AClass<String>();
ac.printInfo("ZXY...");

从泛型类派生子类

如果我们创建了带泛型声明的接口或父类,在用于派生子类时,父类不能再使用类型形参,应该明确指定类型实参。例如:

public class Apple<T>{}
这样是错误的:
```
public class A extends Apple<T>
```
这样是正确的:
```
public class A extends Apple<String>
```
也可以这样写:
```
public class A extends Apple //不指定类型实参,将默认是Object类型实参
```
也就是说我们在定义类、接口和方法是可以使用类型形参,但是在使用类、接口和方法时必须明确的传入一个类型实参。
如果派生的子类也是一个泛型声明的类和接口,那么可以不用指定父类的类型实参。示例如下:
public class A<T> extends Apple<T>{}
public class Apple<T>{
    private T info;

    public Apple(){}
    public Apple(T info){
        this.info = info;
    }

    public void setInfo(T info){
        this.info = info;
    }
    public T getInfo(){
        return info;
    }
}
public class A extends Apple<String>{
    public String getInfo(){
        return "子类:"+super.getInfo();
    }
}
public class A extends Apple{
    //注意系统会默认传入Object类型实参,所以返回值是Object
    @overrive
    public Object getInfo(){
        return "子类:"+super.getInfo();
    }
}

并不存在泛型类

我们创建了List strList = new ArrayList();,创建了一个Sting类型的ArrayList类。我们可以把ArrayList看做ArrayList的一个特殊类。但是实际上JDK并没有个为ArrayList创建.class文件,而且不会将Array当做是一个新的类型来处理。看下面示例:

    List<String> listStr1 = new ArrayList<String>();
    List<Integer> listInt1 = new ArrayList<Integer>();
    System.out.println(listStr1.getClass().getName()); //=>java.util.ArrayList
    System.out.println(listStr1.getClass() == listInt1.getClass()); //=>true

我们可能会认为listStr1.getClass() == listInt1.getClass()会输出false,认为listStr1和listInt1是两个不同类的实例。但是listStr1和listInt1的类都是java.util.ArrayList。也就说明了java中不存在泛型类。不管泛型类型的类型实参是什么,他们在运行时总有一样的类(在内存中占用一份内存)。那么我们也会联想到静态方法、静态变量、静态初始化中为什么不能使用类型形参的原因了。下面的代码将会引起编译时异常。

class A1<T>{
    private T info;
    public static T staticInfo; //会引发编译时异常
}

现在我们知道了不存在泛型类。那么下面代码也是会引发编译时错误的。

listStr1 instanceof ArrayList<String>

因为instanceof左边是一个对象,右边是一个类。ArrayList去不是一个类。listStr1 instanceof ArrayList 这样写就不会错了。

类型通配符

假如我们有这样一个需求,一个方法的形参是一个List类型的形参,方法体内打印出入的List类型的实参。实现代码如下:

    public void test(List<Object> c){
        for(int i=0;i<c.size();i++){
            System.out.println(c.get(i));
        }
    }

调用代码如下:

    public static void main(String [] args){
        Tpf t = new Tpf();
        List<String> listStr = new ArrayList<String>();
        listStr.add("Z");
        t.test(listStr); //此处会报编译时错误  The method test(List<Object>) in the type Tpf is not applicable for the arguments (List<String>)
    }

按照我们的惯性思维String是Object的子类,List接口是一个带泛型声明的接口,那么List应该是List的子类。一定要注意其实这是不成立的。

我们再来看一个数组和泛型对比的例子:

String [] arrayStr = new String[]{};
Object [] arrayObj = arrayStr;

List<String> listStr = new ArrayList<String>();
List<Object> listObj = listStr;//这里回报编译错误

通过观察上面的代码我们可以得出什么结论?

那就是String是Object的子类,String [] 依然是Object[]子类,但List却不是List的子类。

如果我们要上面报编译错误的代码通过编译并顺利执行,就需要有一个各种泛型List的父类。各种泛型List的父类用List

    public void test(List<?> c){
        for(int i=0;i<c.size();i++){
            System.out.println(c.get(i));//get(i)总是返回一个Object类型,虽然实际上放入的是"Z" String
        }
    }

这种带通配符的List仅仅代表所有泛型List的父类,并不能将元素加入其中。如下代码会引起编译异常

List<String> strList = new ArrayList<String>();
strList.add(A);
List<?> list = strList;
list.add("A"); //这段代码会引起异常,因为add方法类型形参E被替换成了?,不知道放入什么类型的元素。但是可以放入null,null是所有引用类型的实例

但是我们可以通过get()方法来返回list中指定索引的元素。

设定类型通配符的上限

当使用List

public class Test9_3_2 {

    public static void main(String [] args){
        List<Long> listLong = new ArrayList<Long>();
        listLong.add(100L);
        listLong.add(200L);

        Test9_3_2 test = new Test9_3_2();
        test.add(listLong);

        //不小心将String泛型的list传给了add()方法。会在运行时引起类型转换异常
        List<String> listStr =new ArrayList<String>();
        listStr.add("A");
        listStr.add("B");
        test.add(listStr);
    }

    public void add(List<?> listNum){
        for(Object n:listNum){
            Number number = (Number) n; //这里进行了强制类型转换
            //不管是long、int、short统统转换成double
            double d = number.doubleValue();
            System.out.println(d);
        }
    }
}

上面的代码,我们本意是想在调用时出入Number泛型List的子类。但是有时可能不小心传入了listStr,并且add()中的for循环还进行了强制类型转换,我们使用泛型的目的就是避免类型转换(强制类型转换可能会引发类型转换异常)。这样一来使我们的代码看起来变得臃肿和烦琐。这个时候有一种方法可以表示Number泛型List的父类,为了满足需求可以设定泛型通配符的上限。示例如下:

public class Test9_3_2 {

    public static void main(String [] args){
        List<Long> listLong = new ArrayList<Long>();
        listLong.add(100L);
        listLong.add(200L);

        Test9_3_2 test = new Test9_3_2();
        test.add(listLong);

        //不小心将String泛型的list传给了add()方法
        List<String> listStr =new ArrayList<String>();
        listStr.add("A");
        listStr.add("B");
        //test.add(listStr); //这里会在编译时报错(与调用方法的参数不匹配),在编译时就防止错误的发生
    }

    //List<? extends Number> 表示Number泛型List的父类,List<Long>、List<Integer>都是子类
    public void add(List<? extends Number> listNum){
        for(Number n:listNum){
            Number number = n; //这里也不在需要强制类型转换了
            //不管是long、int、short统统转换成double
            double d = number.doubleValue();
            System.out.println(d);
        }
    }
}

设定类型形参的上限

java泛型不仅允许在使用通配符时设定上限,而且可以在定义类型形参时也可以设定上限,表示传递给该类的类型实参那么是该上限类型,要不就是该上限类型的子类。示例如下:

public class Test9_3_3<T extends Number> {

    private T info;
    public static void main(String [] args){
        Test9_3_3<Long> test1 = new Test9_3_3<Long>();
        Test9_3_3<Integer> test2 = new Test9_3_3<Integer>();

        //编译时会报错 String不是Number的子类
        Test9_3_3<String> test3 = new Test9_3_3<String>();
    }

}

定义泛型方法

Java 5支持泛型类、泛型接口,也支持定义泛型方法。泛型类和泛型接口可以在类和接口内使用类型形参。泛型方法只能在方法内使用类型形参。

现在我们有这么一个需求,将一个Object数组中的所有元素添加到Collection集合中。我们用如下代码实现:

static void fromArrayToCollection(Oject [] a, Collection c){
    for(Object obj:a){
        c.add(obj);
    }
}

上面实现方法没有任何问题,关键在于c参数,我们说过Collection并不是Collection的子类,所以这个方法有局限性,只能讲数组中的元素添加到Collection中,不能添加到Collection、Collection等其它类型的Collection中。那么我是否可以使用泛型的通配符(Collection

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

把泛型方法的语法和普通方法比较,不能发现,比普通方法在修饰符和返回值类型之间多了类型形参声明。多个类型形参用逗号分隔。

下面我们对fromArrayToCollection()方法进行改进。

public class Test9_4_1 {
    public static void main(String [] args){
        String [] aStr = {"A","B"};
        Collection<String> collStr = new ArrayList<String>();
        fromArrayToCollection(aStr, collStr);
        System.out.print(collStr.toString());
    }

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

与类和接口中的泛型参数不同的是,泛型方法无需显示的传入类型实参,编译器可以根据传入的参数判断出类型形参。如上面示例所示,在调用fromArrayToCollection()方法时无需出入String类型,编译器可以根据传入的参数推断出类型实参是String类型。

为了让编译器准确的推断是泛型方法中的形参类型,不要制造迷惑,如果你制造了迷惑,编译器就不会让你通过编译。如下所示:

    public static void main(String [] args){
        Object [] aStr = {"A","B"};
        Collection<String> collStr = new ArrayList<String>();
        fromArrayToCollection(aStr, collStr); //将会报编译时错误
        System.out.print(collStr.toString());
    }

上面示例要求:数组实参的类型要和Collection实参的泛型类型要相同。要不然编译器也判断不出fromArrayToCollection(T [] a, Collection c)方法中的T代表Object还是String。

泛型方法和类型通配符的区别

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

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

上面的两个方法也可以用泛型方法来替代,示例如下:

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

上面的方法中使用了的形式,这是为类型形参T设定了上限,T类型必须是E类型的子类,在接口内部E可以当做普通类型来看待。

泛型方法允许类型形参被用来表示一个或多个参数类型形参之间的依赖关系,或者返回值与参数类型形参的依赖关系。如果没有这种依赖关系就不需要使用泛型方法,如果有,就可以使用泛型方法。例如下面的代码就体现了这种依赖关系:

public class Test9_4_1 {
    public static void main(String [] args){
        List<String> listStr = new ArrayList<String>();
        listStr.add("A");
        listStr.add("B");

        String [] reStr = toArray(listStr, new String[listStr.size()]);
        System.out.println(Arrays.toString(reStr));
    }

    //将集合中的元素添加到指定的数组中并返回
    public static <T> T[] toArray(List<T> c, T[] array){
        for(int i=0;i<c.size();i++){
            array[i] = c.get(i);
        }
        return array;
    }
}


类型通配符与泛型方法(在方法中显示声明的类型形参,就是方法修饰符和返回值之间的<>)还有一个显著的区别:类型通配符即可以在方法签名中定义类型形参,也可以用于定义变量的类型;但是泛型方法中的类型形参必须在对应方法中显示声明。

《=不太理解



版权声明:本文为fly_zxy原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。