泛型怎么玩

泛型怎么玩


1.什么是泛型

参数化类型,把类型当做参数传递给类或者方法,创建对象或者调用方法才明确类型;

Java泛型的设计原则:只要在编译期没有错误或警告,在运行期就不回出现ClassCastException异常。

2.泛型的好处

如集合类,如果没有泛型之前,我们不清楚集合类的元素类型,很有可能会发生ClassCastException异常或者需要类型装置转换,如:

List list = new ArrayList();
list.add("123");
list.add(new Integer(123));
Object obj = list.get(0);

List对内部元素是没有任何限制的,装载String或者Integer都是没有任何语法错误的,只知道是Object类型,当get的时候只能返回Object,如要返回String则需要进行类型向下强制转换,这样程序就不安全,使用不当就会发生异常。

支持过泛型类之后,可以把类型传给List(指定List内部元素类型),在编译期就确定了List的元素类型,使得程序可以更安全、健壮,如:

List<String> list = new ArrayList<String>();
list.add("123");
list.add(new Integer(123));//编译期会发出错误警告
String str = list.get(0);

有了泛型后,get获取元素可以直接返回String,避免类型的强制转换,提高了:

  • 代码可读性 (可以更清晰的读出包装类型限制)
  • 代码的健壮性[避免了集合类型的强制转换]
  • 代码更简洁 (可以更优雅的实现需要动态设定参数类型功能等)

3.如何使用泛型

但出于规范的目的,Java 还是建议我们用单个大写字母来代表类型参数。常见的如:

泛型类

泛型类就是把泛型定义在类上,在使用此类的时候需要关联泛型参数时才明确下来,这样的话可以动态的使用一类抽象类(需要使用才明确什么类型),不需要担心转换问题。
定义在类上,该类的所有方法、属性都可以使用这一类型参数

public class Test<T>  {
    private T obj;
    public Test(T Obj){
        this.obj = obj;
    }

    public static void main(String[] args) {
        Test<String> t = new Test<String>("123");
        String a = t.obj;

        Test<Integer> t1 = new Test<Integer>(123);
        Integer b = t1.obj;
    }
}

用户想要使用哪种类型,就在创建的时候指定类型。使用的时候,该类就会自动转换成用户想要使用的类型了。

**上例代码的T泛型参数,是JAVA的常用写法,你也可以换成任意的字符;但出于规范的目的,Java还是建议我们用单个大写字母来代表类型参数。**常见的如:

  • T 代表一般的任何类。
  • E 代表 Element 的意思,或者 Exception 异常的意思。
  • K 代表 Key 的意思。
  • V 代表 Value 的意思,通常与 K 一起配合使用。
  • S 代表 Subtype 的意思。
泛型类派生出的子类

上述泛型类本质上还是一个JAVA类,只不过多了泛型类型这个参数,所以泛型类也是可以继承的。
但子类可以明确泛型类型参数变量,也可以不明确:
子类明确泛型类型参数

public class Test<T>  {
    private T obj;
    public Test(T Obj){
        this.obj = obj;
    }
}
/**
* 子类明确泛型参数类型
*/
class TestA extends Test<String>{
    public TestA(String Obj) {
        super(Obj);
    }
}

子类不明确泛型类型参数

public class Test<T>  {
    private T obj;
    public Test(T Obj){
        this.obj = obj;
    }
}
/**
* 子类不明确泛型参数类型
*/
class TestA<T> extends Test<T>{
    public TestA(T Obj) {
        super(Obj);
    }
}

当子类不明确泛型类的类型参数变量时,外界使用子类的时候,也需要传递类型参数变量进来,在实现类上需要定义出类型参数变量

泛型方法

在某一个方法上使用泛型,有时外界只关心某个方法,而不关心整个类,就可以使用泛型方法
定义泛型方法:

public class Test {
    public <T> void print(T t){
        System.out.println(t);
    }

    public static void main(String[] args) {
        Test t = new Test();
        //用方法,传入的参数是什么类型,返回值就是什么类型
        t.print("123");

        t.print(123);
    }
}

泛型通配符

类型通配符一般是使用?代替具体的类型参数,例如 List<?> 在逻辑上是List,List 等所有List<具体类型实参>的父类。

public class Test{

    public void test(List<?> list){
        //只读,不能使用add一类的操作
        list.get(1);
    }

    public static void main(String[] args) {
        Test t = new Test();
        //用方法,传入的参数是什么类型,返回值就是什么类型
        t.test(new ArrayList<String>());

        t.test(new ArrayList<Integer>());
    }
}

注意:?泛型对象是只读的,不可修改,因为?类型是不确定的,可以代表范围内任意类型;

类型通配符上限
可以设定?类型通配符可接受类型的上限,只能装载上限的子类或自身

? extends Number

类型通配符下限
顾名思义,可以设定?类型通配符可接受类型的下限,只能装载下限的父类或自身

//传递进来的只能是Type或Type的父类
? super Number

通配符和泛型方法

所有能用类型通配符(?)解决的问题都能用泛型方法解决,并且泛型方法可以解决的更好:

a. 类型通配符:void func(List<? extends A> list);
b. 完全可以用泛型方法解决:<T extends A> void func(List<T> list);

上面两种方法可以达到相同的效果(?可以代表范围内任意类型,而T也可以传入范围内的任意类型实参),并且泛型方法更进一步,?泛型对象是只读的,而泛型方法里的泛型对象是可修改的,即List list中的list是可修改的!
两者区别:

通配符可以使用超类(super)限定而类型参数不行
?和 T 都表示不确定的类型,区别在于我们可以对 T 进行操作,但是对 ?不行
类型参数可以多重限定而通配符不行(T extends A,B)

?相对来说更灵活,代码更简洁,二者各有各的应用场景:

  • 一般只读就用?,要修改就用泛型方法
  • 在多个参数、返回值之间存在类型依赖关系就应该使用泛型方法,否则就应该是通配符?

4.类型擦除

Java的类库是Java生态中非常宝贵的财富,必须保证向后兼容(即现有的代码和类文件依旧合法)和迁移兼容(泛化的代码和非泛化的代码可互相调用)基于上面这两个背景和考虑,Java设计者采取了“类型擦除”这种折中的实现方式。

Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除。

原始类型

类型擦除之后为什么,我们需要进行类型转换呢?原因是类型擦除之后,最后在字节码中的类型变量会转译成原始类型。

在无限定泛型类型,会被转译成Object的原始类型。
有限定泛型类型,会被其限定类型替换

带来的问题

多态的冲突
子类重写父类方法,如父类方法使用了泛型,由于类型擦除,父类泛型参数都会转译成Object类型,子类重写方法明确泛型类型,就会导致参数类型不一致,重载@Override不成立。
如何处理呢?
桥方法:在子类变异后,会生成桥方法来实现重👟父亲
反射调用问题
既然泛型类型在编译期都会被擦除,转移成上限(Object),那通过反射的方式调用机会导致,类型不匹配胃问题有可能导致ClassCastException异常:

public class Test{
    public <T> void test(List<T> list,T e){
        //只读,不能使用add一类的操作
        list.add(e);
    }
    public <T> void print(List<T> list){
        if(list!=null){
            for(T t: list){
                System.out.println(t);
            }
        }
    }
    public static void main(String[] args) throws Exception{
        List<String> list = new ArrayList<String>();
        Test t = new Test();
        t.test(list,"123");
        //一定写object.class 表示什么类型都可以加
        Method method = Test.class.getMethod("test", List.class,Object.class);
        method.invoke(t, list,new Integer(456));
        t.print(list);
    }
}

上例,list限定泛型类型为String,但是class字节码把String参数类型擦除,通过反射调用,可以传入Object对象,从而把Integer对象加入List。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章