Java范型优缺点?与Object区别

本文深入探讨了编程语言中范型的高效编码原理,通过实例解析了范型在避免冗余代码、增强类型安全及提高代码复用性方面的关键作用。重点讲解了范型在方法、类及接口上的应用,以及其在类型擦除、占位符理解和泛型限制等方面的实际操作。

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

  • 范型是高级编程语言一般都具有的特性,在现在框架中应用广泛,但是对于一些细微的使用上的区别需要做一个记录。
    最初我打算通过其他网站找到想要的答案,但是网上的文章鱼龙混杂,我一般都会对比多家进行参考,但是最有说服力的还是
    talk is cheap,show me your code.
  • 关于范型有几个问题也并没找到想要的答案,大多数都是简单的介绍范型的使用。极少数相关的文章也大多是互相copy。
  • 本文不打算介绍范型的基础知识,所以希望学习范型基本使用的同学可以绕行了。
  1. 范型产生的原因?
    范型的产生根本原因就是为了编码的高效。废话,什么语言特性不是为了高效。
    好吧,我们可以show code了。假如说我们没有范型,我们要写一个整数的排序算法。
class Sort {
    void sort(Integer[] sort) {
        // 排序
    }
}

如果后面需要浮点数的排序.

class Sort {
    void sort(Integer[] sort) {
        // 排序
    }

    void sort(Double[] sort) {
        // 排序
    }
}

发现排序的逻辑是一样的,就因为数据类型不同,后面还要添加各种数据类型,添加各种方法太冗余了。
我们可以传入一个通用的对象进行排序。

class Sort {
    void sort(Object[] sort) {
        Number[] numbers = (Number[]) sort;
        // 排序
    }
}

这样个问题,强转无法在编译阶段发现传入参数类型.

范型的引入一个重要原因就是解决强转的问题,采用范型写法的话。

class Sort {
    <T> void sort(T[] sort) {
        Number[] numbers = (Number[]) sort;
        // 排序
    }
}

这与前面一个示范其实是等价的,因为JVM会在编译后生成的字节码进行一个所谓的“范型擦除”,
void sort(T[] sort) 。前面的是范型定义,没有任何限制,因此方法签名的入参T[]会在字节码里显示为Object[].
简单来说就是范型的占位符号被真实的类给替换掉,因此范型可以看作是对JVM执行引擎透明的存在。
我们要对其进行限制,例如此处排序算法我们的设计只是限定为对Number类型的数组进行排序。因此需要
范型对我们的入参数进行校验,一定要满足Number数组才允许调用,否则编译出错。
可以如下

class Sort {
    <T extends Number> void sort(T[] sort) {
        Number[] numbers = (Number[]) sort;
        // 排序
    }
}

这样如果调用试图传入一个String[],编译就会出错。因为限制了该方法只能传入一个Number类型的数组.

从此处看范型做了一个隐形的类型校验的功能。

其实范型不仅可以修饰方法签名,也可以修饰类,此处又有什么用途呢?
其实主要是看该范型的限制范围,如果是整个类的多个方法限制,定义在类,否则定义在方法签名上。
定义类的时候 class [类名]<范型定义>.多个方法可以直接使用,
例如方法可能不只排序,还有比较大小呢,这样我们定义

class Sort<T extends Number> {
    void sort(T[] sort) {
        Number[] numbers = (Number[]) sort;
        // 排序
    }

    boolean equals(T number1, T number2) {

        // 比较
        return false;
    }
}

其实同价:

class Sort {
    <T extends Number> void sort(T[] sort) {
        Number[] numbers = (Number[]) sort;
        // 排序
    }

    <T extends Number> boolean equals(T number1, T number2) {

        // 比较
        return false;
    }
}

但是在调用的地方,第一种可以指定明确的子类。

Sort<Integer> sort = new Sort<>();
sort.sort(new Integer[]{1,2,3});

这样在不同的地方调用复用了该方法,又避免了类型问题。

这是对于参数的限制,范型也对返回参数也可以限制。但是返回值的限制

class NumberService<T extends Number> {
    T getRandom() {
        return (T) new Integer(1);
    }
}

这里遇到一个问题,返回的类型必须强制转换成限制的类型,这里在调用的地方如果指定的范型类型无法强转将报错。
所以范型一般用在接口或则抽象类上。 子类去实现限制具体类型.
可以参考Java集合框架代码
public interface List extends Collection
一般我们使用都是,数字集合就
List list = new ArrayList<>();
如果是字符串
List list = new ArrayList<>();
List与Collection等接口的定义行为规范,这个行为规范是解耦于具体指定类型的. 可以理解成,集合是支持Object类型的。
如果没有范型,集合的使用大概如下:

List stringList = new ArrayList();
stringList.add("1");
stringList.add(1);
stringList.add(1D);

String s = (String)stringList.get(0);
这个集合什么都可以保存,强转的error风险延迟到了运行阶段。
因此范型的指定保证了代码的健壮性,避免了强转的风险.


  • 占位符的理解
    范型里面的?与限制符号有什么区别,show code
    Person有两个子类Student和Teacher。代码略。
class PersonService {
    <T extends Person> List<T> queryList() {
        List<Person> personList = Lists.newArrayList();
        personList.add(new Student());
        personList.add(new Teacher());
        return (List<T>) personList;
    }

    List<? extends Person> queryList2() {
        List<Person> personList = Lists.newArrayList();
        personList.add(new Student());
        personList.add(new Teacher());
        return personList;
    }

    List<Person> queryList3() {
        List<Person> personList = Lists.newArrayList();
        personList.add(new Student());
        personList.add(new Teacher());
        return personList;
    }
}

调用如下:

PersonService personService = new PersonService();
List<Teacher> personList = personService.queryList();
System.out.println(personList);

List<? extends Person> personList2 = personService.queryList2();
System.out.println(personList2);

这并不会报错,注意personList添加了Student对象,返回值声明List居然程序没报错。
不要怀疑,还记得范型的类型擦除和帮我们做了强转的过程么。为什么没报错。因为其没有强转。
执行以下代码:

Teacher teacher = personList.get(0);

xception in thread "main" java.lang.ClassCastException: Student cannot be cast to Teacher。

结论很明显了。这里存在运行时异常的风险,这个时候建议使用queryList2的方式。
List<? extends Person> personList2的方式明确返回的值是未限定的。需要调用方识别。
queryList3的返回值也是List这与personList2有啥区别?其实这里没区别。
List<? extends Person>与List在返回值的情况我还没发现在运行时有啥区别。如果有,希望评论区指教一下。
那?存在的意义是什么。其实有的,在入参上兼容性上的区别。


List<Person> queryListByIds(List<? extends Number> idList) {
       // 略
}

List<Person> queryListByIds(List<Number> idList) {
       // 略
}

如果调用如下:

personService3.queryListByIds(new ArrayList<Integer>()); 
personService3.queryListByIds2(new ArrayList<Integer>());

第二个调用是会编译出错的.
可以理解成List<? extends Number>为List和List的逻辑父类
但是个人理解第二种方式更加“安全”,因为第一种其实只是接口兼容了入参的类型,并不方便真正去强转为子类,第二种方式就明确了类型。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值