Java泛型

Java泛型学习

此篇博客用sout代替System.out.pringln();

概述

Java集合有个缺点——当我们把一个对象“丢进”集合里后,集合就会“忘记”这个对象的数据类型,当再次取出该对象时,该对象的编译类型就变成了Object类型(其运行时类型没变)。

Java集合之所以被设计成这样,是因为设计集合的程序员不会知道我们用它来保存什么类型的对象,所以他们把集合设计成能保存任何类型的对象,只要求具有很好的通用性。但这样做带来如下两个问题:

集合对元素类型没有任何限制,这样可能引发一些问题。例如,想创建一个只能保存Dog对象的集合,但程序也可以轻易地将Cat对象“丢”进去,所以可能引发异常。

由于把对象“丢进”集合时,集合丢失了对象的状态信息,集合只知道它盛装的是Object,因此取出集合元素后通常还需要进行强制类型转换。这种强制类型转换既增加了编程的复杂度,也可能引发ClassCastException异常。

编译时不检查类型可能引发的异常

main(){
    List strList=new ArrayList();
    strList.add("aaa");strList.add("aaa");strList.add("aaa");
    strList.add(5);//1
    for(int i=0;i<strList.size();i++){
        String str =(String)strList.get(i);//2
    }
}

上面程序创建了一个List集合,而且只希望该List集合保存字符串对象——但我们没有办法进行任何限制,如果程序在①处“不小心”把一个Integer对象“丢进”了List集合中,这将导致程序在②处引发ClassCastException异常,因为程序试图把一个Integer对象转换为String类型。

手动实现编译时检查类型

class StrList{
    private List strList=new ArrayList();
    public boolean add(String ele){
        return  strList.add(ele);
    }
    public String get(int index){
        return (String)strList.get(index);
    }
    public int size(){
        return strList.size();
    }
}
public static void main(String[] args) {
        StrList strList=new StrList();
        strList.add("aaa");
        strList.add("bbb");
        strList.add("ccc");
        strList.add(5);
        System.out.println(strList);
        for (int i = 0; i <strList.size() ; i++) {
            String str=strList.get(i);
        }
    }

这种做法虽然有效,但局限性非常明显——程序员需要定义大量的List子类,这是一件让人沮丧的事情。从Java 5以后,Java引入了“参数化类型(parameterized type)”的概念,允许我们在创建集合时指定集合元素的类型,正如List< String>,这表明该List只能保存字符串类型的对象。Java的参数化类型被称为泛型(Generic)。

使用泛型

List<String> stringList =new ArrayList<String>();//1

上面程序成功创建了一个特殊的List集合:strList,这个List集合只能保存字符串对象,不能保存其他类型的对象。创建这种特殊集合的方法是:在集合接口、类后增加尖括号,尖括号里放一个数据类型,即表明这个集合接口、集合类只能保存特定类型的对象。注意①处的类型声明,它指定strList不是一个任意的List,而是一个String类型的List,写作:List

泛型的菱形语法

在Java 7以前,如果使用带泛型的接口、类定义变量,那么调用构造器创建对象时构造器的后面也必须带泛型,这显得有些多余了。例如如下两条语句:

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

上面两条语句中后面的方括号里的字完全是多余的,在Java 7以前这是必需的,不能省略。从Java7开始,Java允许在构造器后不需要带完整的泛型信息,只要给出一对尖括号(<>)即可,Java可以推断尖括号里应该是什么泛型信息。即上面两条语句可以改写为如下形式:

List strList=new ArrayList<>();

Map<String , Integer> scores=new HashMap<>();

深入泛型

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

定义泛型接口、类

Java 5改写后List接口、Iterator接口、Map的代码片段:

public interface List<E>{
    void add(E,x);
    Iterator<E> iterator();//1
}
public interface Iterator<E>{
    E next();
    boolean hasNest();
}
public interface Map<K,V>{
    Set<K> keySet();//2
    V put(K key,V value);
}

允许在定义接口、类时声明类型形参,类型形参在整个接口、类体内可当成类型使用,几乎所有可使用普通类型的地方都可以使用这种类型形参。除此之外,我们发现①②处方法声明返回值类型是Iterator< E>、Set< K>,这表明Set形式是一种特殊的数据类型,是一种与Set不同的数据类型——可以认为是Set类型的子类

通过上面介绍可以发现,我们可以为任何类、接口增加泛型声明(并不是只有集合类才可以使用泛型声明,虽然集合类是泛型的重要使用场所)。下面自定义一个Apple类,这个Apple类就可以包含一个泛型声明。

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 this.info;
    }
}
public static void main(String[] args) {
       Apple<String> apple=new Apple<>("苹果");
        System.out.println(apple.getInfo());
        Apple<Double> apple1=new Apple<>(5.67);
        System.out.println(apple.getInfo());

    }

上面程序定义了一个带泛型声明的Apple< T>类(不要理会这个类型形参是否具有实际意义),使用Apple类时就可为T类型形参传入实际类型,这样就可以生成如Apple< String>、Apple< Double>…形式的多个逻辑子类(物理上并不存在)。

当创建带泛型声明的自定义类,为该类定义构造器时,构造器名还是原来的类名,不要增加泛型声明。例如,为Apple< T>类定义构造器,其构造器名依然是Apple,而不是Apple< T>!调用该构造器时却可以使用Apple< T>的形式,当然应该为T形参传入实际的类型参数。Java 7提供了菱形语法,允许省略<>中的类型实参。

从泛型类派生子类

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

public class A extends Apple<T>{}

如果想从Apple类派生一个子类,则可以改为如下代码:

public class A extends Apple<String>

并不存在泛型类

可以把ArrayList< String>类当成ArrayList的子类,事实上,ArrayList< String>类也确实像一种特殊的ArrayList类,这个ArrayList< String>对象只能添加String对象作为集合元素。但实际上,系统并没有为ArrayList< String>生成新的class文件,而且也不会把ArrayList< String>当成新类来处理。

List<String> l1 =new ArrayList<>();
List<Integer l2=nwq ArrayList<>();
sout(l1.getClass()==l2.getClass());

结果为true。因为不管泛型的实际类型参数是什么,他在运行时总有同样的类。

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

public class R<T>{
    //下列代码错误,不能再静态Field(成员变量)生命中使用类型形参。
    static T info;
    T age;
    public void foo(T msg){}
    //下面代码错误,不能再静态方法声明中使用类型形参
    public static void bar(T msg){}
}

由于系统中并不会真正生成泛型类,所以instanceof运算符后不能使用泛型类。(instanceof主要用来判断一个类是否实现了某个接口,或者判断一个实例对象是否属于一个类。)

类型通配符

当使用一个泛型类时(包括声明变量和创建对象两种情况),都应该为这个泛型类传入一个类型实参。如果没有传入类型实际参数,编译器就会提出泛型警告。例如下列代码

public void test(List c){//泛型警告
    for(int i=0;i<c.size();i++){
        System.out.println(c.get(i));
    }
}

此处使用List接口时没有传入实际类型参数,这将引起泛型警告。修改后:

public void test(List<Object> c){
    for(int i=0;i<c.size();i++){
        System.out.println(c.get(i));
    }
}
List<String> str=new ArrayList<>();
test(str);//编译错误,List<String>对象不能被当成List<Object>对象使用,也就是说, List<String>类并不是List<Object>类的子类。

如果Foo是Bar的一个子类型(子类或者子接口),而G是具有泛型声明的类或接口,G< Foo>并不是G< Bar>的子类型!它与我们的习惯看法不同。

为了表示各种泛型List的父类,我们需要使用类型通配符,类型通配符是一个问号(?),将一个问号作为类型实参传给List集合,写作:List<?>(意思是未知类型元素的List)。这个问号(?)被称为通配符,它的元素类型可以匹配任何类型。所以改写后的代码如下:

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

List< ?>,这种写法可以适应于任何支持泛型声明的接口和类,比如写成Set< ?>、Collection< ?>、Map< ? , ?>等。

但这种带通配符的List仅表示它是各种泛型List的父类,并不能把元素加入到其中。

List<?> c=new ArrayList<String>():
c.add(new Object());//编译错误

因为我们不知道上面程序中c集合里元素的类型,所以不能向其中添加对象。根据前面的List接口定义的代码可以发现:add方法有类型参数E作为集合的元素类型,所以传给add的参数必须是E类的对象或者其子类的对象。但因为在该例中不知道E是什么类型,所以程序无法将任何对象“丢进”该集合。唯一的例外是null,它是所有引用类型的实例。

通配符上限

实际上,我们需要一种泛型表示方法,它可以表示所有Shape泛型List的父类。为了满足这种需求, Java泛型提供了被限制的泛型通配符。被限制的泛型通配符表示如下:

List<? extends Shape>

List<? extends Shape>是受限制通配符的例子,此处的问号(?)代表一个未知的类型,就像前面看到的通配符一样。但是此处的这个未知类型一定是Shape的子类型(也可以是Shape本身),因此我们把Shape称为这个通配符的上限(upper bound)。

形参上限

public class Apple<T extends Number>{
	T col;
}

上面程序定义了一个Apple泛型类,该Apple类的类型形参的上限是Number类,这表明使用Apple类时为T形参传入的实际类型参数只能是Number或Number类的子类。

泛型方法

格式:

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

​ //方法体。。。

}

示例:

public class GenericMethodTest{
    static <T> void fromArrayToCollection(T[] a,Collection<T> c){
        for(T t:a){
            c.add(t);
        }
    }
    main(){
        Object [] object=new Object[100];
        Collection<Object> collection =new ArrayList<>();
        fromArrayToCollection(object,collection);
    }
}

上面程序定义了一个泛型方法,该泛型方法中定义了一个T类型形参,这个T类型形参就可以在该方法内当成普通类型使用。与接口、类声明中定义的类型形参不同的是,方法声明中定义的形参只能在该方法里使用,而接口、类声明中定义的类型形参则可以在整个接口、类中使用。

作者:Loserfromlazy
本文版权归作者和博客园共有,欢迎转载,但必须给出原文链接,并保留此段声明,否则保留追究法律责任的权利。
原文地址:https://www.cnblogs.com/yhr520/p/12665096.html