十、泛型进阶——自定义泛型类,泛型接口,类型通配符,泛型方法

十、泛型进阶——自定义泛型类,泛型接口,类型通配符,泛型方法

为什么学习泛型?

在没有泛型时,将数据存储到集合,数据类型会被集合“遗忘”,当再次从集合取出数据时,就需要强制类型转换。这样会可能会导致ClassCastException类型转换错误异常,还会使代码更加臃肿。

增加泛型后,集合会记住数据类型,当往集合放入其他类型数据时编译器会给出错误提示。

注意:本文主要讲解泛型进阶用法

1.“菱形”语法

Java7以前使用带泛型的接口,类定义变量,调用构造器创建对象时,构造器后面也必须带泛型,显得很多余

Java7以后优化,允许构造器后面可以不需要带完整的泛型信息,在构造器后面只要给出一对尖括号(<>)就可以,这样对原有的泛型并没有改变,还更好的简化了泛型。同时,此处一对尖括号放在一起就形成一个菱形,所以也叫做“菱形语法”。

//Java7以前
List<String> stringList = new ArrayList<String>();
Map<String, Integer> scores = new HashMap<String, Integer>();
//Java7以后
List<String> stringList = new ArrayList<>();
Map<String, Integer> scores = new HashMap<>();

2.Java 9增强“菱形”语法(也叫“钻石”语法)

Java9以前创建匿名内部类时必须带泛型

Java9以后它允许在创建匿名内部类时使用菱形语法,Java根据上下文来推断匿名内部类中的泛型的类型

//Java9以前
interface Foo<T>{
    void test(T t);
}
public class AnnoymousTest {
    public static void main(String[] args) {
        //指定Foo类中泛型为String
        Foo<String> f = new Foo<String>() {
            //test()方法的参数类型为String
            @Override
            public void test(String s) {
                System.out.println(s);
            }
        };
        //使用泛型通配符,此时相当于通配符上限为Object
        Foo<?> fo = new Foo<Object>() {
            //test()方法的参数类型为Object
            @Override
            public void test(Object t) {
                System.out.println("test方法的Object参数为:"+t);
            }
        };
        //使用泛型通配符,通配符上限为Number
        Foo<? extends Number> fn = new Foo<Number>() {
            //test()方法的参数类型为Number
            @Override
            public void test(Number t) {
                System.out.println("test()方法的Number参数为:"+t);
            }
        };
    }
}
//=================================================================
//Java9以后,根据上下文配置匿名内部类中“菱形”的泛型类型
interface Foo<T>{
    void test(T t);
}
public class AnnoymousTest {
    public static void main(String[] args) {
        //指定Foo类中泛型为String
        Foo<String> f = new Foo<>() {
            //test()方法的参数类型为String
            @Override
            public void test(String s) {
                System.out.println(s);
            }
        };
        //使用泛型通配符,此时相当于通配符上限为Object
        Foo<?> fo = new Foo<>() {
            //test()方法的参数类型为Object
            @Override
            public void test(Object t) {
                System.out.println("test方法的Object参数为:"+t);
            }
        };
        //使用泛型通配符,通配符上限为Number
        Foo<? extends Number> fn = new Foo<>() {
            //test()方法的参数类型为Number
            @Override
            public void test(Number t) {
                System.out.println("test()方法的Number参数为:"+t);
            }
        };
    }
}

3.深入理解泛型

泛型:允在定义类、接口、方法是使用类型形参,这个类型形参(或叫泛型)将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型参数,也可以称为类型实参)。

①定义泛型接口

分析Java5改写后的List接口、Iterator接口、Map的源码片段。

//分析1.定义接口时指定一个 泛型形参,该形参名为E
public interface List<E>{
    //在该接口中,E可作为类型使用
    //下面方法可以使用E作为参数类型
    boolean add(E e);
    Iterator<E> iterator(); 
    ..
}
//分析2.定义接口时指定一个 泛型形参,该形参名为E
public interface Iterator<E> {
    //在该接口里E完全可以作为类型使用
    //下面方法可以使用E单独作为返回值类型
    E next();
    ...
}
//分析3.定义该接口时指定两个泛型形参,其形参名为K、V
public interface Map<K,V> {
	  //在该接口里K、V完全可以作为类型使用
      Set<K> keySet();
    ...
}

上述代码提到的返回值为Iterator、Set 。

例如List可以认为是List的子类数据类型。有:在使用时K形参传入String,则产生一个新的数据类型List,相当于把所有K替换成String

总结:使用泛型以后,虽然程序只定义了一个List接口,但实际使用时可以产生无数多个List接口,传入实参E不同,系统就会多出一个新的List子接口。注意,此处只是逻辑上增加,但是实际上这种子接口(类)物理上并不存在。

②定义泛型类

//定义Apple类时使用了泛型声明
public class Apple<T>{
    //使用T类型定义实例变量
    private T info;
    public Apple() {
    }
    public Apple(T info) {
        this.info = info;
    }
    public T getInfo() {
        return info;
    }
    public void setInfo(T info) {
        this.info = info;
    }
    public static void main(String[] args) {
        //由于传给T形参的是String,所以构造器参数只能是String
        Apple<String> apple1 = new Apple<>("苹果");
        System.out.println(apple1.getInfo());//苹果
        //由于传给T形参的是Double,所以构造器参数只能是Double或double
        Apple<Double> apple2 = new Apple<>(5.67);
        System.out.println(apple2.getInfo());
    }
}

泛型类总结:在定义时使用泛型形参,使用时传入实际类型参数,泛型就会自动匹配生成逻辑上的不同类类型。

举例:此处类定义格式Apple,使用了泛型形参T,在传入实际参数"苹果",5.67时,逻辑上生成Apple String类型的类和Apple Double类型的类。

这也是JDK种List,ArrayList等实现原理。

③从泛型派生子类

当创建了带泛型声明的接口,父类后,可以为该接口创建实现类,或从该父类派生子类。注意:在子类中引用接口和父类时不能再包含泛型形参,需要声明为实际参数。申明:此处案例承接上述②中案例实现。

//错误写法,编译器报错:不能解析T
class A extends Apple<T>{}
//正确写法1,表明T是具体的什么参数String/Integer/Double...
class A extends Apple<String>{}
//正确写法2,不传入泛型参数,此处会将T转为Object类
//此处不写泛型参数被称为  原始类型(raw type)  
class A extends Apple{}

案例

class A extends Apple<String>{
    //正确重写父类方法,返回值
    //注意此处需要与父类Apple<String>返回值一样为String,也就是被String转换了的T
    @Override
    public String getInfo() {
        return "子类"+super.getInfo();
    }
    //××错误写法此处并未被转成Object××
    public Object getInfo() {
        return "子类"+super.getInfo();
    }
}

主方法执行

public static void main(String[] args) {
      A a = new A();
      System.out.println(a.getInfo());//子类null   因为未调用setter方法
}

④并不存在的泛型子类

List<String> stringArrayList = new ArrayList<>();
List<Integer> integerArrayList = new ArrayList<>();
//通过getClass拿到原对象
System.out.println(stringArrayList.getClass() == integerArrayList.getClass());//true

分析上述源码,初学者可能会认为此处最后输出fasle,但是细心的读者在阅读前面文章有提到,泛型是逻辑上根据传入形参类型生成子类,物理上(即:内存上)是只有一个内存存放的当前泛型对象的,**他们仍然是被当成一个对象处理,**所以这里的两个ArrayList其实是一个对象,因此返回true。

由于上述特性,因此我们在 静态代码块,静态方法,静态变量 声明和初始化是不被允许使用泛型的。

public class R<T> {
    //错误:不能在静态代码块使用泛型
    static {
         T info;
    }
    //错误:不能在静态变量中使用泛型
    static T info;
    //错误:不能在静态方法声明中使用 泛型形参
    public static void bar(T msg){}
}

4.类型通配符

案例:

public class R {
    public static 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) {
        List<String> strList = new ArrayList<>();
        //报错无法将test中的java.util.List<java.lang.Object>应用于java.util.List<java.lang.String>,也就是说List<String>不是List<Object>的子类
        test(strList);
    }
}

注意:此处String是Object的子类型,但是List不是List的子类型,注意区分。

强化理解:String可以向上转型为Object的方式称为型变,但Java集合并不支持型变

①使用类型通配符

上述提到需求,集合中子类需要转换为父类的集合类型,不被允许,那么有没有其他的解决方案可以实现这样转换?

Java泛型中引出通配符“?”解决问题。为了表示各类泛型List的父类,使用问号通配符(?),将一个问号作为类型实参传给List集合,写作List<?> 此处“?”可以匹配任何类型。

更改上述代码

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

现在使用任何类型List来调用该方法,程序都可以访问集合c中的元素。注意此处List<?>中的元素只能被编译器当成Object类型处理。

加强理解通配符:这种通配符方式的List仅仅表示它是所有类型的List的父类,不能往该通配符集合中添加null以外元素

List<?> c = new ArrayList<String>();
//此处引起编译报错
c.add("test");

因为此处add()添加数据时,使用了通配符,找不到对应数据类型所以报错。

分析之前提到的List源码add(E e)方法中传入的必须是E类型的对象,此处将对象转换为了通配符,无法再匹配添加对象所以报错。

②设定类型通配符的上限

需求:上述写法List<?>将会作为所有集合的子类,如果说现在我们只需要生效于某个父类的全部子类如何实现?

案例复现:定义一个抽象父类Shape,两个子类Circle、Rectangle继承抽象父类,再定义个画布绘制数量不等的形状,最后编写测试类

//抽象父类 形状
public abstract class Shape {
    public abstract void draw(Canvas c);
}
//子类 画圆圈
public class Circle extends Shape{
    @Override
    public void draw(Canvas c) {
        System.out.println("在画布"+c+"上画了一个圆");
    }
}
//子类画矩形
public class Rectangle extends Shape{
    @Override
    public void draw(Canvas c) {
        System.out.println("在画布"+c+"上画了一个矩形");
    }
}
//画布类 进行画图
public class Canvas {
    public void drawAll(List<Shape> shapes){
        for (Shape shape : shapes) {
            shape.draw(this);
        }
    }
}
//测试类
public class CanvasTest {
    public static void main(String[] args) {
        List<Circle> circleList = new ArrayList<>();
        Canvas canvas = new Canvas();
        //根据上述分析,我们可以很清楚这里报错原因是因为集合不支持型变,即:具集合类型的泛型不会继承
        canvas.drawAll(circleList);
    }
}

上述案例出现编译错误。

通过通配符我们可以知道使用"?"通配符可以解决问题,但是注意前面提到的需求是若还有一个子泛型集合C,我们不想让子泛型集合C的父类也是List<?>。

解决方案:设定通配符上限,格式:List<? extends Shape>表示泛型形参必须是Shape子类的List

public void drawAll(List<? extends Shape> shapes){
//...
}

加强理解:此处未知类型一定是Shape的子类型(也可以是Shape本身),因此把Shape称为这个通配符的上限

注意:与通配符特性一致,不能往设定上限的集合中添加null以外的数据元素。

public void addRectangle(List<? extends Shape> shapes){
    //此处会报错,因为第二个元素为? extends Shape它不能确定它的元素类型,所以无法添加元素
    shapes.add(0,new Rectangle());
}
简化案例理解通配符上限

配符上限,即表示泛型形参必须是指定上限的子类,此处必须为Number的子类

public static void fun(List<? extends Number> list) {}
public static void main(String[] args) {
        fun(new ArrayList<Integer>());//Number 子类    ok
        fun(new ArrayList<Number>());//包含Number      ok
        fun(new ArrayList<Object>());//Number父类      编译出错
        fun(new ArrayList<String>());//与Number无关的类 编译出错
}

③设定通配符下限

格式:List<? super Integer>

与通配符上限相反,即表示泛型形参必须是指定下限的父类,此处必须为Integer的父类

public static void fun(List<? super Integer> list) {}
public static void main(String[] args) {
        fun(new ArrayList<Integer>());//ok
        fun(new ArrayList<Number>());//ok
        fun(new ArrayList<Object>());//ok
        fun(new ArrayList<String>());//编译出错
}

④设定泛型形参上限

Java允许通配符置顶上限时,也允许定义泛型形参时设定上限

public class Pear<T extends Number> {
    T col;
    public static void main(String[] args) {
        Pear<Integer> pear1 = new Pear<>();
        Pear<Double> pear2 = new Pear<>();
        //下面语句编译报错,因为String不是Number子类
        //Type parameter 'java.lang.String' is not within its bound; should extend 'java.lang.Number'
        Pear<String> stringPear = new Pear<>();
    }
}

极端情况:程序需要为泛型指定多个上限(至多一个父类上限,可以有多个接口上限)

//此处表示T类型必须是Number类或其子类,并必须实现java.io.Serializable接口
public class Pear<T extends Number & java.io.Serializable> {
    //...
}

⑤泛型 标记符 的区别

  • E Element 集合元素 或 方法中的参数类型
  • T Type Java类的参数类型
  • K Key 键
  • V Value 值
  • N Number 数值类型
  • ? 表示不确定的Java类型,通配符

除“?”通配符外,其余符号只是一种公认的规范,便于区分,实质填入其他参数不影响程序,日常开发为了规范代码,增强代码可读性建议遵守规范。

⑥协变 与 逆变

我们在主观上可能会认为泛型可以继承,其实是不能的。或者说换一个说法叫做逆变与协变。

定义:java中 协变 与 逆变 是对泛型类的继承关系的表述。

例:有两个类,关系如下

class Apple extends Fruit{}
class Fruit{}

案例1:

List<Fruit> fruits=new ArrayList<Apple>();//编译错误 java中泛型不可变

案例1修改:解决办法则是通过超类通配符?,再配置通配符上限,实现协变(小–>大)

List<? extends Fruit> fruits=new ArrayList<Apple>();

案例2:

List<Apple> apples=new ArrayList<Fruit>();//编译错误 java中泛型不可变

解决办法:逆变理解的写法(大–>小)

List<? super Apple> apples=new ArrayList<Fruit>()

这里则引出了一个原则:PECS原则

如果要从集合中读取类型T的数据,并且不能写入,可以使用 ? extends 通配符;(Producer Extends)
如果要从集合中写入类型T的数据,并且不需要读取,可以使用 ? super 通配符;(Consumer Super)
如果既要存又要取,那么就不要使用任何通配符。

5.泛型方法

问题:编写一个方法负责将一个Object数组的所有元素添加到Collection集合中。

public class MyUtils2 {
    static void fromArrayToCollection(Object[] a,Collection<Object> c){
         for (T o : a) {
            c.add(o);
        }
    }
    public static void main(String[] args) {
        String[] strArr = {"a","b"};
        List<String> strList = new ArrayList<>();
        //strList报错,不能把List<String>转换为Collection<Object>
        fromArrayToCollection(strArr,strList);
    }
}

上述报错,我们之前学了通配符“?”那么Collection<?>考虑是否可行?答案否定,因为Java不允许把对象放入一个未知的集合

如何解决该问题?

①定义泛型方法(Generic Method)

前面的介绍中在接口或者类中定义了泛型,在接口或类的方法、变量中用到该泛型。

若我们类或者接口没有泛型,只想方法上使用泛型也是可以的。

定义:泛型方法的方法签名比普通方法的方法签名多了泛型声明(<T , S>,泛型形参声明以尖括号括起来,多个形参之间以逗号分割,所有泛型形参声明放在 方法修饰符 与 返回值类型 之间。

//使用格式
修饰符 <T , S> 返回值类型 方法名(形参列标){
    //...
}
//有返回值的泛型方法
public<T> T test1(T t){    return t;}

修改上述案例:采用泛型方法实现

public class MyUtils2 {
    static <T> void fromArrayToCollection(T[] a,Collection<T> c){
        for (T o : a) {
            c.add(o);
        }
    }
    public static void main(String[] args) {
        Object[] ob = new Object[100];
        Collection<Object> co = new ArrayList<>();
        //1.下面代码T代表Object类型
        fromArrayToCollection(ob,co);
    }
}

完整用法

public class MyUtils2 {
    static <T> void fromArrayToCollection(T[] a,Collection<T> c){
         for (T o : a) {
            c.add(o);
        }
    }
    public static void main(String[] args) {
        Object[] ob = new Object[100];
        Collection<Object> co = new ArrayList<>();
        //1.下面一行代码中T代表Object类型(参数类型:Object,Object)
        fromArrayToCollection(ob,co);
        String[] strArr = {"a","b"};
        ArrayList<String> strList = new ArrayList<>();
        //2.下面一行代码中代码T代表String类型(参数类型:String,String)
        fromArrayToCollection(strArr,strList);
        //3.下面一行代码中代码T代表Object类型(参数类型:String,Object)
        fromArrayToCollection(strArr,co);
        Integer[] ia = new Integer[100];
        //4.编译出错,因为Number即不是String的类型,也不是它的子类所以编译报错(参数类型:Integer,String)
        fromArrayToCollection(ia,strList);
    }
}

强化理解:上面的泛型方法(fromArrayToCollection)中定义了一个T泛型形参( ),这个形参在方法中可以当作普通类型使用。注意类与接口中泛型的区别,方法声明中的泛型只能在该方法使用,类与接口中声明的泛型可以在整个接口和类中使用,作用范围不同

②特例讲解—形参为集合且只有一个泛型参数

public class MyUtils3 {
    static <T> void test(Collection<T> from, Collection<T> to){
        for (T f : from) {
            to.add(f);
        }
    }
    public static void main(String[] args) {
        List<Object> ob = new ArrayList<>();
        List<String> cs = new ArrayList<>();
        //下面代码编译报错
        test(ob,cs);
    }
}

由于此处形参from和to都是Collection类型,这要求调用该方法时的两个集合实参的泛型类型必须相同,否则编译器无法准确推断出泛型方法中泛型形参的类型。

此处编译出错原因,传入实参集合的泛型一个为Object一个为String,调用test方法时无法推断出T的类型。

解决方案:数据类型小的泛型参数使用<? extends T>

public class MyUtils3 {
    static <T> void test(Collection<? extends T> from, Collection<T> to){
        for (T f : from) {
            to.add(f);
        }
    }
    public static void main(String[] args) {
        List<Object> ao = new ArrayList<>();
        List<String> cs = new ArrayList<>();
        //1.下面代码编译报错
        test(cs,ao);
    }
}

此处采用 通配符上限 解决问题,只要实参传入的第一个参数类型 是第二个参数类型的子类型(即:form<=to类型;String<=Object)即可。

以上用到了泛型方法又用到了泛型通配符,在什么时候选择用哪一个更合适?

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

首先大多数时候都可以使用泛型方法来代替类型通配符。

分析Collection源码,addAll,containsAll方法使用了类型通配符

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

泛型方法修改

public interface Collection<E> extends Iterable<E> {  
    <T> boolean containsAll(Collection<T> c);
    //使用泛型形参上限
    <T extends E> boolean addAll(Collection<T> c);
}

注意:上面两个泛型形参T都只使用了一次,泛型形参T产生的唯一效果是可以在不同调用点传入不同的实际类型。这也是通配符创建出来的初衷,支持灵活子类化。

区分:泛型方法允许泛型形参用来表示方法的一个或者多个参数之间的类型依赖关系,或者方法返回值与参数之间的类型依赖关系。如果没有这样的类型依赖关系,就不应该使用泛型方法。

注意:若某个方法中一个形参a的类型或返回值类型依赖于形参b,此时形参b不应该使用通配符。使用通配符形参b是不确定的,程序无法定义形参a的类型。因此存在依赖关系应当在方法签名中声明泛型,使用泛型方法。

④泛型构造器 与 构造器中菱形语法

与泛型方法中声明泛型形参一样,Java允许在 构造器签名 中声明泛型形参,这样就产生了泛型构造器。

public class Foo1 {
    public <T> Foo1(T t) {
        System.out.println(t);
    }
    public static void main(String[] args) {
        //泛型构造器中的T为String
        new Foo1("B0");
        //泛型构造器中的T喂Integer
        new Foo1(500);
        //显示指定1泛型构造器中T为String
         new <String> Foo1("B0_2");
        //显示指定2泛型构造器中T为String,但是传入的实参为Double,编译出错
        new <String>Foo1(12.3);
    }
}

显示指定1处显示类型为String传入String类型数据,构造器可以正常匹配;显式指定2处显示指定为String传入Double类型不一致出错。

菱形语法:

public class MyClass<E> {
    public <T> MyClass(T t) {
        System.out.println(t);
    }
    public static void main(String[] args) {
        //类显示声明E形参是String类型,构造器隐式声明T形参为Integer类型,“菱形”语法调用构造器
        MyClass<String> mc1 = new MyClass<>(5);
        //类显示声明E形参为String,构造器显示声明T形参为Integer,显示调用构造器
        MyClass<String> mc2 = new <Integer>MyClass<String>(5);
        //类显示声明E形参为String,构造器显示声明T形参为Integer,“菱形”语法调用构造器,报错
        MyClass<String> mc3 = new <Integer>MyClass<>(5);
    }
}

注意:在显示调用泛型构造器时(eg:=new MyClass),“菱形”语法失效,构造器也需要显示调用(eg:new MyClass)

⑤泛型方法的重载

public class MyUtils4 {
    public static <T> void copy(Collection<T> dest,Collection<? extends T> src){...}//1
    public static <T> T copy(Collection<? super T> dest,Collection<T> src){...}//2
    public static void main(String[] args) {
        List<Number> ln = new ArrayList<>();
        List<Integer> li = new ArrayList<>();
        //由于上述方法重载,第1个方法T为Number,第2个方法T为Integer;编译器无法确定代码想调用那个方法。
        copy(ln,li);
    }
}

⑥Java8改进类型推断

Java8改进了泛型方法的类型推断能力,主要涉及两方面。

  • 通过调用方法的上下文推断 泛型的目标类型
  • 在方法调用链中,将推断得到的泛型传递到最后一个方法。
public class  MyUtils4<E>{
    //泛型方法nil,返回值为 MyUtils4<> 对象
    public static <Z> MyUtils4<Z> nil(){
        return null;
    }
    //泛型方法cons,返回值为 MyUtils4<> 对象
    public static <Z> MyUtils4<Z> cons(Z head,MyUtils4<Z> tail){
        return null;
    }
    E head(){
        return null;
    }
}
class InferenceTest{
    public static void main(String[] args) {
        //通过方法赋值自动推断数据类型:通过方法赋值的目标参数来推断泛型为String
        MyUtils4<String> ls = MyUtils4.nil();
        //无需如下调用方法时显示指定泛型
        MyUtils4<String> mu = MyUtils4.<String>nil();
        //通过方法传参自动推断数据类型:可调用cons方法所需的参数类型类推断泛型为Integer
        MyUtils4.cons(42,MyUtils4.nil());
        //无需如下调用方法时显示指定泛型
        MyUtils4.cons(42,MyUtils4.<Integer>nil());
    }
}

第一次调用nil时,程序将该方法返回值赋给 MyUtils4类型,系统自动推断此处泛型参数为String类型。

第一次调用cons时,程序根据第一个参数“42”自动推断出第二个参数泛型为Integer

注意类型推断失效情况:

//报错,因为此处无法识别head方法最终的返回值,前面未调用过MyUtils4给他赋泛型
String s =  MyUtils4.nil().head();
//修改,显示指定泛型方法的实际类型
String s =  MyUtils4.<String>nil().head();

6.擦除和转换

泛型存在一个情况:当我们将一个泛型类A,实例化并指定泛型类型后命名为a,再将a赋值给未初始化的泛型类A变量b,此时b会擦除掉a的泛型信息。

案例分析1:

public class A<T extends Number> {
    T size;
    public A() {
    }
    public A(T size) {
        this.size = size;
    }
    public T getSize() {
        return size;
    }
    public void setSize(T size) {
        this.size = size;
    }
}
class ErasureTest{
    public static void main(String[] args) {
        A<Integer> a = new A<>(6);
        //调用getSize返回Integer方法
        Integer as = a.getSize();
        //把a对象赋给A对象,丢失泛型信息
        A b = a;
        //泛型信息丢失,变为类中定义的泛型上限
        Number size1 = b.getSize();
        //编译报错,找到的数据类型为Number
        Integer size2 = b.getSize();
    }
}

案例分析2:

public class ErasureTest2 {
    public static void main(String[] args) {
        //泛型类型自动推断为Integer;“菱形”语法
        List<Integer> li = new ArrayList<>();
        li.add(6);
        li.add(9);
        //类型擦除测试:将Integer类型的list赋给未指定泛型的List
        List list = li;
        //访问list中的数据返回类型为Object,Integer被擦除
        Object o = list.get(0);
        //思考?下面两行代码为什么会抛出异常:ClassCastException
        List<String> ls = list;
        System.out.println(ls.get(0));
    }
}

在上述思考中,我们将被擦除了泛型类型的集合list(即:现在类型为Object)赋值给String类型的集合,由于Object类型不能自动转换为String类型(不能“向下转型”)因此抛出Java类转换异常。

7.泛型与数组

数组前置知识:

  • 数组创建后大小便固定

  • 数组能追踪它内部保存的元素的具体类型,插入的元素类型会在编译期得到检查

  • 数组可以持有原始类型 ( int,float等 ),不过有了自动装箱,容器类看上去也能持有原始类型了

那么当数组遇到泛型会怎样? 能否创建泛型数组呢?

Java泛型有一个很重要的设计原则:若一段代码在编译时没有提出“unchecked”警告(即:未经检查的转换)警告,则程序在运行时不会引发ClassCastException异常。

因此数组元素的类型不能包含泛型变量或泛型形参,除非是无上限的类型通配符。但可以声明元素类型包含泛型变量或泛型形参的数组

也就说可以声明List[]类型的数组对象(List[] lsa;),不能创建List[]这样的数组对象(即创建数组对象new List[];不被允许)

public class MyUtils5 {
    public static void main(String[] args) {
        //不被允许
        //List<String>[] lists = new ArrayList<String>[10];
        //有[unchecked]未经检查的转换警告
        List<String>[] lsa = new ArrayList[10];
        //向上转型为Object[]类型
        Object[] oa =lsa;
        List<Integer> li = new ArrayList<>();
        li.add(3);
        //将集合放入oa对象
        oa[1] = li;
        String s = lsa[1].get(0);//抛出ClassCastException
    }
}

注意此处 将ArrayList[10]对象 赋给 泛型数组对象List[]时会有编译警告“[unchecked]未经检查的转换警告”(即:编译器不保证这段代码是安全的)。

Java允许创建无上限的通配符泛型数组,new ArrayList<?>[10],在这种情况下,程序不得不进行强制类型转换。在强制类型转换之前应该通过instanceof运算符保证数据类型匹配。

public class MyUtils6 {
    public static void main(String[] args) {
        List<?>[] lsa = new ArrayList<?>[10];
        Object[] oa = lsa;
        List<Integer> li = new ArrayList<>();
        li.add(3);
        oa[1] = li;
        Object target = lsa[1].get(0);
        if (target instanceof String){
            //下面代码安全了
            String s = (String) target;
        }
    }
}

参考资料:《疯狂Java讲义》

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值