首页 > 技术文章 > java泛型

ant-xu 2019-08-18 20:23 原文

Java泛型来源于JDK1.5。包括三个方面泛型类、泛型接口,泛型方法。泛型即Generic

泛型就是,参数化类型,就是将类型也作为参数。

泛型类:

只需要在类名后添加类型参数声明。如下类。

public class Test <T>{
    T one;

    @Override
    public String toString() {
        return "Test [one=" + one + "]";
    }

    public T getOne() { //返回值可以是List<T>,在内部T就是实际的类型
        return one;
    }

    public void setOne(T one) {
        this.one = one;
    }
    
    public static void main(String[] args) {
        Test<Integer> t1 = new Test<Integer>();
        t1.setOne(123);
        System.out.println(t1);
    }
}

我们可以看到,泛型类声明的类型T可以作为泛型类方法的参数,返回值,还可以声明变量,与Integer, double一样。这样只需要在编写时传入类型参数,就能使得该类的使用范围更广。但是我们需要在使用时赋予类型的参数,当然这不是强制的。

泛型接口:

声明泛型接口和声明泛型类相似。在接口名后加类型参数声明。

interface Dao <T, K>{
    //T x;    //报错,因为是public static final,泛型不能用static修饰
    T findById(K id);
    List<T> findAll();
}

其中泛型不能使用static修饰可以理解,因为泛型类或泛型接口都是在实例化时才能确定其类型,用static修饰后,就说明不需要实例化的过程。有了接口自然需要实现,在继承或实现泛型类或接口的时候,可以先确定类型,如下。

class MyDao implements Dao<Integer, String>{

    @Override
    public Integer findById(String id) {
        return null;
    }

    @Override
    public List<Integer> findAll() {
        return null;
    }
    
}

为什么要这样写呢,我认为需要这样写是因为,我们需要实现的是一个,接口中的<T, K>都有参数的类型。即我们想要实现的如下。

interface Dao {
    Integer findById(String id);
    List<Integer> findAll();
}

而在实现该接口之前,将需要的参数传入就是为了使接口变成上面的样子(个人这样理解的,是否有别的深意,我还不知道)。当让然也可以继续使用泛型,而不传实际的类型,甚至在此基础上扩展

class MyDao1<T, K, V> implements Dao<T, K>{
    V x;
    
    @Override
    public T findById(K id) {
        return null;
    }
    
    @Override
    public List<T> findAll() {
        return null;
    }
}

泛型方法:

泛型方法很容易让人疑惑,首先上面的例子中没有泛型方法。上面的方法都是泛型类中的普通方法,因为这些方法在使用时根本不需要传递类型参数,这些方法使用的不过是类实例化时就确定了的泛型。而真正的泛型方法,有自己的泛型类型,在使用泛型方法时可以使用尖括号给类型参数。还有就是泛型类和泛型方法没有任何的关联。因为泛型方法作用域更小,在方法中声明的T在方法的作用域中会覆盖类中声明的T。

public class Generic <T>{
    T value;    //泛型类声明的类型参数,只能声明
    T [] values;   //泛型数组的声明,注意只能声明
    public T getValue() {
        System.out.println(value);
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }
    
    public <T> void printT(T t) {    //函数自己的类型参数,屏蔽了类的泛型参数
        T x = t;                    //当然,不是泛型类也可以有泛型方法,他俩无关联
        System.out.println(t);
    }
    
    public <E> void setT(E t) {    //函数自己的类型参数E
        this.value = (T) t;        //需要强制转换,不然报错。
    }
    
    public static <E> void sayHi(E name) {
        System.out.println("Hi "+name);
    }
    
    public static void main(String[] args) {
        Generic.<String>sayHi("name");    //静态的泛型方法
        Generic.sayHi("123");
        Generic<Double> one = new Generic<Double>();
        one.<String>setValue(3.5);    //不是泛型方法,非要指定类型,会警告。
        one.<String>getValue();        //也能看出,这个类型指定毫无作用
        one.setValue(3.14);
        one.getValue();        //输出3.14
        one.<Integer>printT(555);    //输出555,说明函数的泛型与类的泛型无关
        one.printT(666);    //直接输入,不指定类型,编译器能够推断出来,
                            //因为一个类型对应一个参数,类型可以由参数确定
                            //泛型类型参数不能是基本类型,所以666被装箱为Integer类型
    }
}

从上面可以看出,似乎泛型类和泛型方法有一些重复的地方,既然定义了泛型类,泛型方法似乎不太需要专门定义了。原则是:无论何时,只要你能做到,你就应该尽量使用泛型方法。即如果使用泛型方法就可以完成任务,那就不要再将类泛型化了。这样保证不放大作用空间。

所有的方法调用one.xxx()都可以写成one.<XX>xxx(),这种类似泛型的格式
但是只有实际的泛型方法才有效果,可能需要这样写的情况有
1当泛型方法中的泛型不能从其方法参数中获取时,
2从参数中获取到的与实际需要传递给泛型的不相同时,
但是一般没用,并且第二种情况中
public <E> void getPrint(E e)中需要符合
this.<XX>getPrint(XX或XX的子类)的格式

限定通配符:

泛型、通配符出现的契机:再java的泛型使用中,我们有时候会想将泛型之间有父子关系的类进行赋值运算,如下,首先定义了一个泛型类

class TestGeneric<T extends Number>{}    //空实现

我们定义了一个泛型T,这个泛型将泛型参数限定再Number类及其子类的范围内,于是我们有了下面再main方法中的测试。

TestGeneric<Object> objTest = new TestGeneric<Object>();     //报错
TestGeneric<Integer> intTest = new TestGeneric<Number>();    //报错
TestGeneric<Number> numTest = new TestGeneric<Integer>();    //报错

三行代码都报错,第一行,我们可以理解,因为一开始的限制就是Number及其子类。第二行父类型的泛型赋值给子类的引用,普通继承的情况下也是不行的。第三行,这一行传达了一个意思,泛型中的参数化类型没有继承关系。为什么呢,在泛型出现之前,编写的java时如果需要用到如今泛型的功能,是通过Object来实现,比如工具类List,里面可能存储各种类型时就是用Object来存,然后通过强制类型转换,但是这样可容易出错,没有好用的参照,不能一眼就看出List中存储的类型。于是出现了泛型。也就是说泛型的出现就是为了解决类型间的转换,一眼就知道里面时Object还是Integer。所以java的决策者就决定,不能让泛型类型间的有这种父子关系,这也是不违背泛型出现的初衷。但是严格限制了类型之间的这种转换就一定好用么,显然不是。第三行想表达的是放数字的地方不能放整数。但整数却属于数字。这明显不符合我们的逻辑。于是就出现了通配符来解决这种父子关系。注意泛型之间的父子关系不好处理,但泛型外还是有父子关系的。List<String> list = ArrayList<String>(); 外部的List和ArrayList是不受影响的。

java中有三种通配符

1、无限定通配符<?>,匹配任意类

2、上界限定符<? extends Number>,匹配Number和其子类

3、下界通配符<? super Number>,匹配Number和其父类。

使用通配符意味着你需要显示的强制转换,另外<T>和<?>的区别。

1、<T>相当于<T extends Object>与之对应的是<?>相当于<? extends Object>。就是说可以使用Object中的方法,extends有取到父类方法的效果

2、应用的场景不同,<T>用于声明一个泛型类或泛型方法,说明类型以后再给,再定义时的返回值List<T>这样,将自定义的T传入List,作为实参,但是实参是泛型。就是说在定义时T是代表实际类的假的类,而定义时需要的就是假的类。而<?>主要用于使用泛型类和泛型方法,在左边等待赋值,这也就意味着<?>是实参与String是一样的。对于这第二条区别的例子如下。

/*声明时*/
class Pen1<T>{}
class Pen2<?>{}        //报错,声明不能使用<?>

class Pen3<T extends Number>{}
class Pen4<? extends Number>{ //报错,不能使用?作为泛型类型,但可以作为限制,像下面这样

@SuppressWarnings("unchecked")    //这是Collections的源码,?可以像下面这样作为限制
public static <T extends Comparable<? super T>> void sort(List<T> list) {
    list.sort(null);
} //泛型T必须实现Comparable接口,并且这个接口中的类型为T或T的父类,声明T,但T要满足?的限制

/*main中使用时*/
Pen1<?> penx = new Pen1<Integer>();
Pen1<? extends Number> peny = new Pen1<Integer>(); //上文中解决父子关系的方法

其实也能理解,在声明时用T这样就可以创建引用,方便类中对于泛型类型的操作,比如类型转换,保存引用等等。既然已经存在了T那么就没有必要让<?>也有创建引用的功能。相当于各自的分工。注意泛型类型只能声明引用而不能创建对象,也可以声明泛型数组的引用。原因下面再说。

第一种,无限定通配符<?>,首先通配符是用在已经定义好的类型中的。我们可以像下面这样使用。

ArrayList<String> ls = new ArrayList<String>();
ls.add("123");
ls.add("456");
        
List<?> list = ls; //可以赋值,可以匹配任意类,添加的方法受限
Object get1 = list.get(0); //获取的是一个Object,限制get方法 System.out.println(list.get(
0)); //输出调用的是实际内存中对象的方法,所以一定是123
//list.add("123");//报错,限制set方法,The method add(capture#2-of ?) in the type //List<capture#2-of ?> is not applicable for the arguments (String),参数不匹配
//list.add(list.get(0));//同样报错,The method add(capture#2-of ?) in the type //List<capture#2-of ?> is not applicable for the arguments (capture#3-of ?)

由上面的提示可以看出,String类型变成了capture#2,而从ls中获取的参数capture#3。这是使用<?>作为参数类型的后遗症。造成这样的原因是使用<?>时,编译器根本就不知道这是一个什么类型,它知道的是,里面已经放好了对象。因为不知道里面的对象是什么类型,所以为了安全,拒绝添加任何的对象。实际上由于获取也只能得到Object,相当于同时限制了get和set。实现list.add(list.get(0));的方法如下

//定义辅助方法
public static <T> void add1(List<T> list, T str) {  //这样不行
    list.add(list.get(0));
    list.add(str);
}

public static <T> void add2(List<T> list) {  
    list.add(list.get(0));
}    //方法定义的泛型T,放入了泛型类List<T>中

//调用
add1(list, new String("098"));    //报错
add2(list);    //编译成功,利用泛型函数的参数,保留类型信息

对于<?>的使用,体现在接受参数上。从<?>中取得的数据是Object类型。

第二种,上界限定符<? extends Number>,匹配Number和其子类。这个好理解,如下使用

List<? extends Number> list = new ArrayList<Integer>();

这样我们就能将泛型参数的子类赋值给父类。另外有单继承多接口的限制,如下

class TestGeneric<T extends Abc&Serializable>{}    //空实现,在定义时的限定

class Abc{}

//调用时
TestGeneric<Abc> test = new TestGeneric<Abc>();  //报错,因为Abc没有实现Serizlizable接口

出现在extends后的类只能有一个,且在第一个,可以有多个接口,&表示的是多重的限制,每一个都要满足。<? extends Number>的使用也有限制,如下

List<Integer> l = new ArrayList<Integer>();
l.add(new Integer(1));
l.add(new Integer(200));
        
List<? extends Number> list = l;    //可以放泛型的子类
System.out.println(list.get(0));
//list.add(new Integer(300));  报错,和<?>一样的原因,参数中有泛型的方法不能使用
Number number = new Integer(300);
//list.add(number);    //即便是Number也报错

 <? extends Number>也是用在接受参数上。在从list中获取值的时候,类型为Number即上界的类型。注意不是用取出值然后getClass()判断的,而是赋值给别的变量时,编译器会提示将变量类型转换为Number。getClass()或取得真实的类型。即便赋值给了Number类型的引用,其getClass()也是原本的真实类型。在限定上界的情况下,其实达到了只能取不能存的目的,相当于一个生产者。

第三种、下界通配符<? super Number>,匹配Number和其父类。

List<? super Number> list1 = new ArrayList<Object>();
//List<? super Number> list2 = new ArrayList<Integer>();    //报错Integer不是Number的父类

<? super Number>的使用限制如下,因为Number是抽象类所以在下面使用A、B、C来代替

class A{
    @Override
    public String toString() {
        return "A []";
    }
}
class B extends A {
    @Override
    public String toString() {
        return "B []";
    }
}
class C extends B {
    @Override
    public String toString() {
        return "C []";
    }
}

//调用时
List<? super B> list = new ArrayList<B>();    
//list.add(new A());    //报错
list.add(new B());
list.add(new C());
Object x = list.get(0);    //返回类型为Object

可以看出,从list中获取的类只能是Objcet。<? super B> 限制的只是ArrayList<>的尖括号中的内容,至于list中放置的内容还不进行限制,或者说,上例中list中的类型一定是B或者B以上的类,这样向list中存C的对象是安全的。因为返回的值都是Object,相当于限制了获取,但add方法却不受限制,即对存入开放,对获取关闭,相当于消费者。

总的来说,上界不存,下界不取。或者说只读使用extends,只写使用super。

一些相关博客

泛型概述或详解:

https://www.cnblogs.com/shijiaqi1066/p/3441445.html

https://www.cnblogs.com/penghuwan/p/8420791.html

https://www.cnblogs.com/xiaomiganfan/p/5390252.html

https://blog.csdn.net/s10461/article/details/53941091

类型擦除:

https://blog.csdn.net/lonelyroamer/article/details/7868820

https://blog.csdn.net/briblue/article/details/76736356

不明觉厉:

https://waltyou.github.io/Effective-Java-32-Combine-Generics-And-Varargs-Judiciously/

https://waltyou.github.io/Effective-Java-31-Use-Bounded-Wildcards/

https://www.rabbitwfly.com/articles/2019/05/07/1557234596180.html

不知道:

https://www.cnblogs.com/penghuwan/p/8458269.html

https://segmentfault.com/a/1190000008423240

推荐阅读