九、Java 泛型

本文讲解了Java泛型的起源、设计原则,展示了如何消除ClassCastException,介绍了泛型类和类派生,以及类型通配符和泛型擦除的概念。通过实例说明了如何正确使用泛型限制集合元素类型,提升代码健壮性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

泛型入门

泛型的作用,很大程度上是为了让集合记住其元素的数据类型,而在没有泛型之前,集合是把所有对象都当成 Object 类型处理。而当取出集合的元素时,需要进行强制转换,此时就很可能会引发 ClassCastExeception 异常。

 public static void main(String[] args) {
        List list = new ArrayList();
        list.add("Java");
        list.add("泛型");
        list.add(5);
        for (Object str : list) {
            //将 str 变成 String 类型,并输出集合每个元素的长度
            System.out.println(((String) str).length());

        }
    }
/*res:
4
2
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
	at test.main(test.java:20)

上面的代码,只运行到把集合的前两个元素输出之后就引发异常了,是因为程序在遍历第三个元素时,想要吧 Integer 对象转换成 String 类型。所以引发了 ClassCastExeception 异常。而这就是泛型出现的意义!
Java泛型设计原则:只要在编译时期没有出现警告,那么运行时期就不会出现 ClassCastException 异常.
泛型:把类型明确的工作推迟到创建对象或调用方法的时候才去明确的特殊的类型
泛型的实质:允许在定义接口、类时声明类型形参,类型形参在整个接口、类体内可当成类型使用,几乎所有可使用普通类型的地方都可以使用这种类型形参。

在继续下文之前需要了解一些相关术语

  • ArrayList中的E称为类型参数变量
  • ArrayList中的Integer称为实际类型参数
  • 整个称为ArrayList泛型类型
  • 整个ArrayList称为参数化的类型(ParameterizedType)

那么接下来我们使用泛型来把上面的代码的异常给消除:

   public static void main(String[] args) {
        List<String> list = new ArrayList();		//1
        list.add("Java");
        list.add("泛型");
        //list.add(5);      //这里会自动报错		  //2
        for (String str : list) {					//3
            //将 str 变成 String 类型,并输出集合每个元素的长度
            System.out.println(str.length());		//4
        }
    }

只需要将 List 改写成 List 将指定了类型参数,如果向 list 集合添加的不是 String 类型的变量,则会进行报错;并且在进行遍历的时候,str 的类型也完全可以确定了下来。

定义泛型类

泛型类就是把泛型定义在类上,用户使用该类的时候,才把类型明确下来
在类定义的泛型,在类的方法中也可以使用

public class Apple<T>
{
    //使用T类型定义实例变量
    private T info;
    public Apple(){}
    //使用T类型形参定义构造器
    public Apple(T info){
        this.info = info;
    }
    public T getInfo(){
        return this.info;
    }

    public static void main(String[] args) {
        //由于传给T的形参是String,所以构造器的参数只能是String
        Apple<String> a1 = new Apple<>("苹果");
        System.out.println(a1.getInfo());
        //由于传给T的形参是Double,所以构造器的参数只能是Double或double
        Apple<Double> a2 = new Apple<>(3.1415926);
        System.out.println(a2.getInfo());
    }
}
/*res:
苹果
3.1415926

从泛型类派生子类

当创建爱你了带泛型声明的接口、父类后,可为该接口创建实现类,或者从该父类派生子类,需要指出的是,当使用这些接口、父类时不能再包含类型形参。

//定义类A继承Apple类,错误示范
public class A extends Apple<T>{}
//需要指定实际类型参数,正确示范
public class A extends Apple<String>{}
//或者不传入实际的类型参数,正确示范
public class A extends Apple{}

还有一点需要我们注意实现类的要是重写父类的方法,返回值的类型是要和父类一样的!
我们接着 Apple 类来写代码

   public class A extends Apple<String>{
        public String getInfo(){
            return "son" + super.getInfo();
        }
        /*
        //下面方法是错误的,重写父类方法时返回值类型和继承Apple<String>实际类型参数不一致
        public Object getInfo(){
            return "son";
        }
         */
    }

并不存在泛型类

为什么那么说呢?先来看一段代码

    public static void main(String[] args) {
        List<String> l1 = new ArrayList<>();
        List<Integer> l2 = new ArrayList<>();
        //来比较l1和l2的类是否相等
        System.out.println(l1.getClass() == l2.getClass());
    }
/*res:
true

可能会有很多人认为程序的结果是 false,但其实是 true。因为不管泛型的实际类型参数是什么,它们在运行时总有同样的类。因为对于 Java 来说,它们依然被当成同一个类来处理,在内存中也只占用一块内存空间,

  • 泛型在对象创建时才知道是什么类型,但是静态方法属于类而类在编译阶段就存在了,所以虚拟机根本不知道方法中引用的泛型是什么类型
  • 初始化时:对象创建的代码执行先后顺序是static的部分,然后才是构造函数等等,所以在对象初始化之前static的部分已经执行了,如果你在静态部分引用的泛型,那么毫无疑问虚拟机根本不知道是什么东西



所以在静态方法、静态初始化块或者静态变量的声明和初始化中不允许使用类型形参。

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

类型通配符

首先我们来模拟一个场景:现在我需要一个方法来接收一个结合参数,并把它打印出来,该怎么做?

    public void test1(List list) {
        for(int i = 0 ; i <list.size();i++){
            System.out.println(list.get(i));
        }
    }

方法很简单,直接遍历就行了,但细心的,应该会发现有一个警告。 上面的代码是正确的,**只不过在编译的时候会出现警告,说没有确定集合元素的类型。**原因也很简单,因为 List 是一个有泛型声明的接口,此处使用 List 接口但是没有传入实际类型参数,这会引起泛型警告。下面是将代码稍微改写一下:

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

这样做语法是没毛病的,但是这里十分值得注意的是:该test()方法只能遍历装载着Object的集合!!!
强调:泛型中的并不是像以前那样有继承关系的,也就是说List和List是毫无关系的!
我们是不清楚List集合装载的元素是什么类型的,List这样是行不通的………于是Java泛型提供了类型通配符 ?
接下来继续改写这个方法:

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

现在非常值得注意的是,当我们使用?号通配符的时候:就只能调对象与类型无关的方法,不能调用对象与类型有关的方法。
记住,只能调用与对象无关的方法,不能调用对象与类型有关的方法。因为直到外界使用才知道具体的类型是什么。也就是说,在上面的List集合,我是不能使用add()方法的。因为add()方法是把对象丢进集合中,(null 是唯一的例外,它是所有引用类型的实例)而现在我是不知道对象的类型是什么。即,下面代码是错误示范

List<?> c = new ArrayList<String>();
//下面程序会引起编译错误
c.add(new Object());

设置通配符或类型形参的上限

现在,我想接收一个List集合,它只能操作数字类型的元素(Float、Integer、Double、Byte等数字类型都行)
我们学习了通配符,但是如果直接使用通配符的话,该集合就不是只能操作数字了。因此我们需要用到设定通配符上限

//设置通配符的上限  
List<? extends Number>
//设置类型形参的上限
List<T extends Number>

上面的代码的含义是:List集合装载的元素只能是Number的子类或自身

设置通配符的下限

既然上面我们已经说了如何设定通配符的上限,那么设定通配符的下限也不是陌生的事了。直接来看语法吧

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

值得注意的是:无论是设定通配符上限还是下限,都是不能操作与对象有关的方法,只要涉及到了通配符,它的类型都是不确定的!

通配符和泛型方法

大多时候,我们都可以使用泛型方法来代替通配符的……

    //使用通配符
    public static void test(List<?> list) {

    }

    //使用泛型方法
    public <T> void  test2(List<T> t) {

    }

选择使用这两个方法的原则:

  • 如果参数之间的类型有依赖关系,或者返回值是与参数之间有依赖关系的。那么就使用泛型方法
  • 如果没有依赖关系的,就使用通配符,通配符会灵活一些.

泛型擦除

泛型是提供给javac编译器使用的,它用于限定集合的输入类型,让编译器在源代码级别上,即挡住向集合中插入非法数据。但编译器编译完带有泛形的java程序后,生成的class文件中将不再带有泛形信息,以此使程序运行效率不受到影响,这个过程称之为“擦除”。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值