Java基础(12)——泛型

本文深入探讨了Java中的泛型,包括其作用、使用方式如泛型类、泛型接口和泛型方法,以及泛型擦除和通配符的概念。泛型提供类型安全,防止运行时类型转换异常,而泛型擦除则意味着在运行时没有泛型信息。此外,通配符允许更灵活地处理不同类型的集合。
摘要由CSDN通过智能技术生成

泛型,可以理解为参数化类型。对于参数,我们比较熟悉的是形参和实参。方法定义时,用的是形参,在调用方法时,实际传的数值或对象叫实参。参数化类型,可以理解为,在定义类或者方法时,将用到的类型参数化(类型形参),不传具体的类型,只是在使用时,才将具体的类型传进来(类型实参)。

泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。比较常见的用法是容器类,例如list,set,map。

作用

当不使用泛型,可能会在运行时产生转换异常。

// 当不限定List中存储的类型,则任意类型对象都可以加入List中
List list = new ArrayList();
list.add("abc");    // 向list中加入String
list.add(111);      // 向list中加入Integer,容器类中无法储存基本类型,int会装箱后存入

for (int i = 0; i < list.size(); i++) {
    // 此处在i=1时,会抛出转换异常
    // ClassCastException:java.lang.Integer cannot be cast to java.lang.String
	String str = (String) list.get(i);
	System.out.println(str);
}

当我们不限定ArrayList中存储的类型,那么任意类型的数据都可以存入。上例中,先存入一个String类型,后存入一个Integer类型,然后都以String类型使用,程序运行时抛出转换异常。

大部分情况下,我们都会将相同类型的数据放在一起,而不是简单地将所有数据放在一个list中,所以可以通过限定类型的方式来尽快发现这种问题,在编译期间就报错,而不是在运行时才抛出异常,所以需要引入泛型来限定类型。

List<String> list = new ArrayList();
list.add("abc");
//list.add(111);  编译器此处报错

如果容器中的数据类型不确定,那么在使用前,应该先进行类型判断再使用,从而避免运行时异常。

if (list.get(i) instanceof String){
	String str = (String) list.get(i);
}

主要有三种使用泛型的方式,分别是泛型类、泛型接口、泛型方法

泛型类

泛型类:类的定义中使用泛型。常见的就是各种容器类。

泛型写法

class 类名<泛型标识> {
    
}

泛型类的例子:

// 此处T可写为任意标识,常见T、E、K、V等大写字母;
// 在实例化泛型类时,必须明确指定T的类型
// 只有在此处声明过的泛型标识才能在类中使用
class Generic<T> {
    
    // 成员变量field的类型是T,由实例化泛型类时确定
    private T field;
    
    // 报错,Cannot resolve symbol 'E'
    // 泛型标识E没有被声明过,不能被使用
    // private E value;
    
    // 方法返回值的类型是T,由实例化泛型类时确定
    public T getField() {
        return field;
    }
    
    // 方法的形参field的类型是T,由实例化泛型类时确定
    public void setField(T field) {
        this.field = field;
    }
}

普通的字符串也可以用作为一个标识,还可以用Integer\String等,都会被识别为泛型标识,但是不建议使用除大写字母之外的作为泛型标识,降低代码可读性。

实例化上述的泛型类:

Generic<Integer> genericInt = new Generic<>();
Generic<String> genericStr = new Generic<>();
genericInt.setField(11);
genericStr.setField("abc");
System.out.println("genericInt:"+genericInt.getField());
System.out.println("genericStr:"+genericStr.getField());

// 运行结果:
// genericInt:11
// genericStr:abc

若在实例化泛型类时,不指定确定的类型,那么此实例泛型类可以存入任何类型的对象,就失去了类型限制的作用。

Generic generic = new Generic();
generic.setField(22);
System.out.println("generic:" + generic.getField());
generic.setField("def");
System.out.println("generic:" + generic.getField());

// 运行结果:
// generic:22
// generic:def

需要注意:

  1. 在实例化类时,传入的类型不能是基本类型。
// 报错,Type argument cannot be of primitive type
Generic<int> ge = new Generic<int>();
  1. 不能对具体的泛型类使用instanceof
Generic<Integer> genericInt = new Generic<>();
// 报错,Illegal generic type for instanceof
// if (genericInt instanceof Generic<Integer>) {
// }
if (genericInt instanceof Generic) {
    System.out.println("is Generic");
}
// 运行结果:
// is Generic
  1. 泛型类中可以有多个泛型标识
public class Main {
    public static void main(String[] args) {
        Generic<Integer, String> generic = new Generic<>();
        generic.setField(33);
        generic.setKey("aaa");
    }
}

// T与K可以是不同类型,也可以是相同类型
class Generic<T, K>{
    private T field;
    private K key;
    public void setField(T field) {
        this.field = field;
    }
    public void setKey(K key){
        key = key;
    }
}

泛型接口

泛型接口的定义类似泛型类的格式。

interface Generic<T> {
    T next();
}

实现泛型接口的类,可以传入具体类型,此时类就是一个普通的类;也可以不传入具体类型,那么这个类就是个泛型类。

传入具体类型:虽然只创建了一个泛型接口Generic<T>,但是可以为T传入各种具体类型,从而形成各种类型的Generic接口。在传入具体类型时,实现类中所有用到泛型的地方都需要替换为传入的具体类型。

class StudentGeneric implements Generic<String> {
    private String[] students = new String[]{"zhangsan", "lisi", "wangwu"};
    
    @Override
    public String next() {
        return students[new Random().nextInt(3)];
    }
}

不传入具体类型:实现类与泛型类定义相同,在定义类时,需要将泛型声明一起加入。

class TeacherGeneric<T> implements Generic<T> {
    @Override
    public T next() {
        return null;
    }
}

// 不在实现类中声明泛型标识会报错,Cannot resolve symbol 'T'
//class TeacherGeneric implements Generic<T> {
//    @Override
//    public T next() {
//        return null;
//    }
//}

泛型方法

相比于泛型类,泛型方法稍微复杂一些。

泛型类在实例化时,需要指定具体类型;在调用泛型方法时,需要指定泛型方法的具体类型。

泛型方法的格式如下:

访问修饰符 <泛型标识> 返回值 泛型方法名(泛型标识 形参)

普通类中的泛型方法

class Generic {
    public <T> T showKey(T key){
        return key;
    }
}

public static void main(String[] args) {
    Generic generic = new Generic();
    // 与普通方法区别不大,只是参数类型变为泛型
    System.out.println(generic.show(111));
    System.out.println(generic.show("abc"));
}

泛型方法需要注意以下几点:

  • 泛型方法可以声明在普通类中
  • 此泛型方法返回值为T
  • public与返回值T之间的<T>必须存在,此方法才会被声明为泛型方法
  • <T>表示此方法声明了泛型类型T,方法中才可以使用T

泛型类中的泛型方法

在前面的泛型类例子中,用到泛型标识的方法并不是泛型方法。

class Generic<T>{
    private T field;
    
    // 此方法虽然将泛型标识T作为返回值,但只是一个使用了泛型标识的普通方法,不是泛型方法
    // 泛型标识在泛型类中已经声明过,所以可以使用
    public T getField() {
        return field;
    }
    
    // 此方法也不是泛型方法
    public void setField(T field) {
        this.field = field;
    }
    
    // 泛型类中声明泛型方法,泛型标识E可以与泛型类中的T相同,也可以不同
    public <E> E show(E key){
        return key;
    }

    // 泛型方法中的泛型标识T可以与泛型类中的T相同,也可以不同
    public <T> T showkey(T key){
        return key;
    }
    
    public <T> T setKey(T key){
        // 报错,Incompatible types. Found: 'T', required: 'T'
        // this.field的类型T是类中声明的,key的类型T是泛型方法中声明的,
        // 并不一定是同一种类型,所以不能相互赋值
        // this.field = key;
        return key;
    }
}

public static void main(String[] args) {
    Generic<Integer> generic = new Generic<>();
    generic.setField(111);
    // 报错,泛型类的泛型确定类型后,类中使用此泛型的地方就确定类型了
    // generic.setField("abc");
    Integer newInt = generic.getField();
    // 泛型类中的泛型方法,不受泛型类中的泛型定义的影响
    String str = generic.show("def");
}

静态泛型方法

普通类中定义静态泛型方法

class Generic {
    // 普通类中的静态泛型方法
    public static <T> T show(T var){
        return var;
    }
}

public static void main(String[] args) {
    // 与使用普通的静态方法差别不大,只是参数类型变为泛型
    System.out.println(Generic.show("abc"));
    System.out.println(Generic.show(111));
}

泛型类中的静态泛型方法,泛型类中静态泛型方法无法使用泛型类中定义的泛型。这意味着,静态方法若是想要使用泛型,必须将静态方法定义为泛型方法

class GenericTest<T> {
    // 泛型方法中的泛型与泛型类中定义的泛型不存在关系,可以类型相同,也可以不同
    public static <E> E show(E var){
        return var;
    }
}

public static void main(String[] args) {
    System.out.println(GenericTest.show(33));
    GenericTest<Integer> genericTest = new GenericTest<>();
    System.out.println(genericTest.show("def"));
}

泛型擦除

public static void main(String[] args) {
    List<Integer> intList = new ArrayList<>();
    List<String> strList = new ArrayList<>();

   Class intClass = intList.getClass();
   Class strClass = strList.getClass();

   System.out.println(intClass == strClass);
}

// 运行结果:
// true

从上面的例子可以看到,尽管在使用时,List<Integer>与List<String>类型不同,分别只能添加Integer与String类型的数据,但是在运行时,两者有属于同一种类型。这是因为Java在编译后,会将所有的泛型信息擦除,在字节码层面是没有泛型的类型信息的。也就是Java的泛型只在编译期间有效,这个过程叫做泛型擦除

泛型是后来(SE5)才加入到Java语言特性的,Java让编译器擦除掉关于泛型类型的信息,这样使得Java可以兼容之前没有使用泛型的类库和代码,在字节码层面是没有泛型概念的。

泛型(T) --> 编译器(type erasure) --> 原始类型(T被Object替换)
泛型(? extends XXX) --> 编译器(type erasure) --> 原始类型(T被XXX替换)
原始类型指被编译器擦除了泛型信息后,类型变量在字节码中的具体类型。

存在泛型擦除,无法通过不同的List类型来重载方法,因为在运行时,List<String>与List<Integer>实际上就是同一种类型。

// 报错,both methods have same erasure
public void show(List<String> list) {
    System.out.println(list);
}

public void show(List<Integer> list) {
    System.out.println(list);
}

既然List<String>与List<Integer>在运行时,是同一种类型,那么通过反射,List<Integer>中也可以添加String类型的数据。

public static void main(String[] args) {
    List<Integer> intList = new ArrayList<>();
    intList.add(111);
    // 报错,List<Integer>不能直接添加String类型对象
    // intList.add("def");

    intList.getClass().getMethod("add", Object.class).invoke(intList, "abc");
    System.out.println(intList);
}
// 运行结果:
// [111, abc]

可以看到,List<Integer>通过反射成功添加了String类型对象,这是因为在运行时,List<Integer>的泛型信息被擦除,只保留了原始类型(在此处是Object),所以String类型的对象也可以被添加。

通配符

对于泛型类,有一点需要注意,当确定了泛型的类型后,不只可以使用此类型,还可以使用此类型的子类。对于List<A>,可以添加A及所有A的子类对象。

List<Number> numberList = new ArrayList<>();
numberList.add(new Integer(11));
numberList.add(new Double(2.22));
// 报错
// numberList.add(new Object());
System.out.println(numberList);

// 运行结果:
// [11, 2.22]

但是当List<A>作为方法的参数类型时,只有List<A>类型的数据可以使用,List<A的子类>不可以。IntegerNumber的子类,但是List<Integer>并不是List<Number>的子类。

class GenericTest {
    public void show(List<Number> numList) {
        System.out.println(numList);
    }
}
public static void main(String[] args) {
    List<Number> numList = new ArrayList<>();
     intList = new ArrayList<>();
    GenericTest genericTest = new GenericTest();
    genericTest.show(numList);
    // 报错,List<Integer>类型的参数不能调用List<Number>的方法
    // genericTest.show(intList);
}

为了实现同样的功能,我总不能再写一个参数为List<Integer>的方法吧。List<?>就派上用场了。List<?>可以视为List<Integer>List<Number>的共同父类,此时,?就是通配符,因为它不会对类型进行限制,也被称为无界通配符。

class GenericTest {
    public void show(List<?> list) {
        System.out.println(list);
    }
}
public static void main(String[] args) {
    List<Object> objList = new ArrayList<>();
    List<Number> numList = new ArrayList<>();
    List<Integer> intList = new ArrayList<>();
    GenericTest genericTest = new GenericTest();
    genericTest.show(objList);
    genericTest.show(numList);
    genericTest.show(intList);
}

?作为通配符存在一些问题,它可以接受任何类型的参数。当我们想在一定范围内限定参数类型,但是同时又能接受多种类型的参数呢?这时候可以使用? extends A? super A? extends A限定为A及A的子类,? super A限定为A及A的父类。

List<Object> objectList = new ArrayList<>();
List<Number> numberList = new ArrayList<>();
List<Integer> integerList = new ArrayList<>();

GenericTest genericTest = new GenericTest();
// showExtendsNumber方法的参数类型是List<? extends Number>
// 只有List<Number>及List<Number的子类>可以作为实参传入
// genericTest.showExtendsNumber(objectList);  // 此行报错
genericTest.showExtendsNumber(numberList);
genericTest.showExtendsNumber(integerList);

// showSuperNumber方法的参数类型是List<? super Number>
// 只有List<Number>及List<Number的父类>可以作为实参传入
genericTest.showSuperNumber(objectList);
genericTest.showSuperNumber(numberList);
// genericTest.showSuperNumber(integerList);   // 此行报错

List、List<Object>与List<?>区别

    public static void main(String[] args) {

        List list = new ArrayList();
        list.add(new Object());
        list.add(new Integer(11));
        list.add("abc");

        List<Object> objectList = new ArrayList<>();
        objectList.add(new Object());
        objectList.add(new Integer(22));
        objectList.add("def");

        List<?> newList = new ArrayList<>();
        newList.add(null);
        newList.remove(0);
        newList.clear();
        // 报错,List<?>不可添加元素(null除外)
//        newList.add(new Object());

        //
        List<Object> objectList01 = list;
        List<Integer> intList01 = list;
        List<?> newList01 = list;

        // 报错,不同类型List不可互相赋值
//        List<Integer> intList02 = objectList;
//        List<Integer> intList02 = newList;
//        List<Object> objectList02 = newList;
    }
  1. List、List<Object>可以添加任意类型的对象,List<?>除null外,不可添加任何类型的对象,主要作为通配符用于方法参数;List<Object>是存在类型约束的,只能添加Object及其子类,但因为Java中的类都直接或者间接继承自Object,所以在添加对象方面,List与List<Object>没什么区别。
  2. List没有泛型约束,可以向任意类型的List赋值,也可以接受任意类型的List向它赋值
  3. List<Object>存在泛型约束,只能赋值同类型泛型,也只能接受同类型泛型赋值
  4. List<?>一般作为参数来接收外部集合,可以接受任意类型的List赋值,但是只能给同类型泛型赋值。List<?>在赋值后,除null外,不可添加任何元素,但是可以remove与clear。

参考资料:

  1. Java泛型详解
  2. 《码出高效》
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值