Java 泛型 - 从入门到入土

1、概念

泛型的本质是参数化类型,即给类型指定一个参数,然后在使用时再指定此参数具体的值,那样这个类型就可以在使用时决定了。这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

Java中引入泛型最主要的目的是将类型检查工作提前到编译时期,将类型强转(cast)工作交给编译器,从而让你在编译时期就获得类型转换异常以及去掉源码中的类型强转代码。

2、泛型作用的对象

泛型有三种使用方式,分别为:泛型类、泛型接口和泛型方法。

1、泛型类

在类的申明时指定参数,即构成了泛型类,例如下面代码中就指定T为类型参数,那么在这个类里面就可以使用这个类型了。例如申明T类型的变量name,申明T类型的形参param等操作。

public class Generic<T> {public T name;public Generic(T param){name=param;}public T m(){return name;}
}

那么在使用类时就可以传入相应的类型,构建不同类型的实例,如下面代码分别传入了String,Integer,Boolean3个类型:

private static void genericClass(){Generic<String> str=new Generic<>("总有刁民想害朕");Generic<Integer> integer=new Generic<>(110);Generic<Boolean> b=new Generic<>(true);
​System.out.println("传入类型:"+str.name+"  "+integer.name+"  "+b.name);
}

输出结果为:传入类型:总有刁民想害朕 110 true

如果没有泛型,我们想要达到上面的效果需要定义三个类,或者一个包含三个构造函数,三个取值方法的类。

2、泛型接口

泛型接口与泛型类的定义基本一致

3、泛型方法

public class Generic<T> {public T name;public  Generic(){}public Generic(T param){name=param;}public T m(){return name;}public <E> void m1(E e){ }public <T> T m2(T e){ }
}

重点看public <E> void m1(E e){ }这就是一个泛型方法,判断一个方法是否是泛型方法关键看方法返回值前面有没有使用<>标记的类型,有就是,没有就不是。这个<>里面的类型参数就相当于为这个方法声明了一个类型,这个类型可以在此方法的作用块内自由使用。

上面代码中,m()方法不是泛型方法,m1()m2()都是。值得注意的是m2()方法中声明的类型T与类申明里面的那个参数T不是一个,也可以说方法中的T隐藏了类型中的T。下面代码中类里面的T传入的是String类型,而方法中的T传入的是Integer类型。

Generic<String> str=new Generic<>("总有刁民想害朕");
str.m2(123);

3、类型通配符

首先思考以下问题

public class Animal {
}
public class Cat extends Animal {
}
public static void main(String[] args) {Cat cat = new Cat();  Animal animal = cat;  ❶List<Cat> cats = new ArrayList<>();List<Animal> animals = cats;  ❷
}

以上代码中①处以及②处代码是否正确?

①处的代码没有任何问题,是多态的典型用法,即父类引用指向子类对象,而②处的代码与①处类似,但是却提示编译错误。原因是因为Java泛型中规定,即使泛型类型具有继承关系,但是并不意味着该泛型类型的容器也具有继承关系。

可以把泛型理解成一个标签,一个list中贴上了animal的标签,另一个list贴上了cat的标签,可以说cat是animal的子类,但是不能说贴了cat标签的list就是贴了animal标签list的子类,他们实际上都是list。

知道泛型的这一特性后,就出现了一个比较麻烦的问题: 泛型容器似乎没办法实现多态。

举个例子,在上例中我们为Animal添加一个方法getFood,同时Cat类需要重写该方法

public class Animal {public String getFood() {return "食肉动物吃肉,食草动物吃草";}
}
public class Cat implements Animal {@Overridepublic String getFood() {return "猫粮,小鱼干,猫薄荷";}
}

如果想在控制台输出不同动物的食物,此时需要定一个print方法,利用多态特性,形参可定义为父类Animal,这样实参就可以传Animal以及其子类对象了。

public void print(Animal animal) {System.out.println(animal.getFood());
}
​
public static void main(String[] args) {Test test = new Test();test.print(new Animal());test.print(new Cat());
}

但是如果想打印一批动物的食物信息,就无法利用多态这一特性,因为List<Cat>并不是List<Animal>的子类,所以只能额外增加方法。

public void printAnimalFood(List<Animal> animals) {animals.forEach(a -> System.out.println(a.getFood());
}
​
public void printCatFood(List<Cat> cats) {cats.forEach(c -> System.out.println(c.getFood());
}
​
public static void main(String[] args) {Test test = new Test();List<Cat> cats = Arrays.asList(new Cat());test.printAnimalFood(cats);   // 编译错误,List<Cat>不是List<Animal>的子类test.printCatFood(cats);  // 正确执行
}

如果还需要增加Dog,Rabbit等子类,那么就需要增加相应对应的方法printDogFood,printRabbitFood等,久而久之代码就会很臃肿

1、无界通配符

Java泛型提供了通配符(Wildcards),用?表示,例如List<?>Set<?>等。我们知道Object是任意类的父类,而在Java泛型中,通配符就是任意同泛型类型容器的父类,如List<?>是所有泛型List的父类,为了与接下来的要介绍的另外两种形式的通配符予以区分,可以称之为无界通配符,意味没有界限限制。

由于?不是泛型标识,所以也就无法在类、接口以及方法中传递,以下写法是不允许的

public class TestGeneric<?> {private ? a;
}
​
public <?> ? test(? a) {// Do something
}

?只能出现在引用中或方法入参中,如

// 出现在引用中
Class<?> cls = xxx;
​
// 出现在方法入参中
public void print(List<?> list) {// Do something
}

需要注意的是方法入参中的无界通配符只起到了告知的作用,并不能进行传递,也不需要传递(因为是任意类型)。

总结:

  1. 无界通配符不能标注在泛型类、泛型接口以及泛型方法上

  2. 无界通配符只能使用在引用以及方法入参中,并且无法进行传递

另外无界通配符也可以起到占位以及修饰作用,可以直观告诉使用者这里可以接受任意类型。比如Java中的反射,我们经常写成下面的写法:

Class<?> cls = xxx;

知道了无界通配符的含义之后,我们可以把前言中的例子做一下优化

public void print(List<?> animals) {animals.forEach(a -> System.out.println(((Animal) a).getFood()));
}
​
public static void main(String[] args) {Test test = new Test();List<Animal> animals = Arrays.asList(new Animal());List<Cat> cats = Arrays.asList(new Cat());test.print(animals);  // 正确执行test.print(cats); // 正确执行
}

使用无界通配符后,即使后续有新增的子类,此方法也可以正确执行。至此我们解决了参数传递问题,但是新问题出现了:使用无界通配符似乎与不使用泛型而直接使用List没有任何区别:list中的元素可以是任意类型,并且取元素时需要强制转换。无界通配符看起来并没有特殊的用途,那为什么要需要使用无界通配符呢?

  • List可以理解为持有任意Object类型的原始列表。

  • List<?>可以理解为想要使用泛型列表,但是不确定具体的类型,用?标识任意类型都可以。由于是泛型,所以列表中的元素的类型理应一致,但是以下写法是可以通过编译的

// 正确执行
List list = new ArrayList();
list.add(1);
list.add("2");
List<?> list2 = list;
public void print(List<?> list) {//..
}
​
// 正确执行
public static void main(String[] args) {List list = new ArrayList();list.add(1);list.add("2");new Test().print(list);
}

共同点:由于List<?>List编译期间都无法确定元素的实际类型,所以获取的时候都为Object类型

List<?> list1 = ...;
Object obj1 = list1.get(0);
​
List list2 = ...;
Object obj2 = list2.get(0);

不同点:List<?>由于无法在编译期间确定泛型的实际类型,所以没法List<?>中添加除了null外的任意类型元素。而List由于可以存放Object对象,所以List可以添加任意类型的元素。

List<?> list1 = new ArrayList<>();
list1.add(1);   // 编译错误
list1.add("2"); // 编译错误
list1.add(null);    // 正确执行
​
List list2 = new ArrayList();
list2.add(1);   // 正确执行
list2.add("2"); // 正确执行

应用场景:

public void testMap(Map<String, ?> map) {// Do something
}
Map<String, String> map1 = ...;
Map<String, Integer> map2 = ...;
Map<String, Object> map3 = ...;
​
Test test = new Test();
test.testMap(map1); // 正确执行
test.testMap(map2); // 正确执行
test.testMap(map3); // 正确执行

testMap方法入参map的value想接受任意类型,只能使用无界通配符来进行占位。

2、上界通配符

无界通配符由于可以接受任意类型,所以某些情况下还是不太适用,往往需要强制转换类型,一是不方便,二是可能引发转换异常,这个时候就可能需要使用上界通配符来解决我们的问题了。

上界通配符(Upper Bounde Wildcard),顾名思义,存在一个最上级的界限,即指定一个最高级别的父类,它表示对于该上界类型以及其子类都适用。

基本写法:

? extends xx

无界通配符,由于包含?,所以这种写法只能出现在引用以及方法入参中,如:

List<? extends Number> numbers = xxx;public void test(Class<? extends Number> cls) {
}

前面分析?并不能进行泛型类型传递,所以如果想在在泛型类、泛型接口、泛型方法中使用上界通配符,需要将?指定为标识类型,如

// 泛型类
public class TestGeneric<T extends Number> {private T t;public TestGeneric(T t) {this.t = t;}
}
// 泛型接口
public interface TestGerneric<T extends Number> {
}
// 泛型
public <E extends Collection> void test(E e) {System.out.println(e.size());
}

多重上界

当需要指定多个上界时,需要使用&来连接,并且只能在指定泛型标识的时候使用,例如:

public class TestGeneric<T extends LongFur & BlueEye> {
} 

上例中表示该泛型类型为同时满足为两个指定上界类型或其子类的类型。

需要注意的是,被指定的多个上界不能有有冲突的方法,否则会编译错误

特点:

我们在编译期只能知道上界通配符的上界是什么类型,所以在取元素时,只能获取到上界的类型。具体的实际类型只有在运行时才能确定,同时也构成了多态。

List<Integer> ints = Arrays.asList(1);
List<Double> doubles = Arrays.asList(2.2);
public void test(List<? extends Number> numbers) {// 编译期间无法确定实际类型是Integer、Long、Double还是其他,只能使用Number接收// 运行时构成多态:Number number = 1; Number number = 2.2Number number = numbers.get(0);System.out.println(number);
}

同时与无界通配符相似,由于编译期无法知晓具体的实际类型,所以不支持上界通配符的容器添加元素(或赋值)

List<? extends Number> list = new ArrayList<>();
list.add(1);  // 编译错误
list.add(2.2); // 编译错误
// 如果以上操作允许,那么list中的类型就不统一了

应用场景

public void print(List<? extends Animal> animals) {animals.forEach(a -> System.out.println(a.getFood()));
}
​
public static void main(String[] args) {Test test = new Test();List<Animal> animals = Arrays.asList(new Animal());List<Cat> cats = Arrays.asList(new Cat());test.print(animals);  // 正确执行test.print(cats); // 正确执行
}

由于方法形参定义为List<? extends Animal>,所以不用担心转型的问题,获取的元素肯定为Animal类型(包括子类),也不用担心实参类型传递错误。

通过优化我们可以感受到上界通配符的好处,既能够做到向无界通配符一样的通用型,又能够限制具体的类型范围,所以如果想达到此目的,就可以使用上界通配符,而实际编码中我们使用上界通配符的次数也是最多的。

3、下界通配符

上界通配符相反,下界通配符(Lower Bound Wildcard),顾名思义,存在一个最低级的界限,即指定一个最低级别的子类,它表示对于该下界类型以及其父类都适用。

基本用法:

下界通配符上界通配符类似,只需将extends改为super即可:

? super xx

List<? super Integer> list = xxx;
public void test(Class<? super Integer> cls) {
}

值得注意的是下界通配符不支持制定泛型标识以及多重下界的写法。

特点:

上界通配符相反,我们在编译期只能知道下界通配符的下界是什么类型,所以在添加元素时,只能向其中添加下界类型。

public void test(List<? super Integer> list) {// 编译期间无法确定实际类型是Integer还是Number或更高级别的父类,只能添加Integerlist.add(1);
}List<Number> numbers = Arrays.asList(1, 2L, 3.3);
xxx.test(numbers);

同时由于编译期无法知晓具体的实际类型,所以只能使用Object来接收获取的元素

List<? super Integer> list = new ArrayList<>();
// 编译失败,因为无法确定是否是Integer,有可能是Number
Integer i = list.get(0);
// 编译失败,因为无法确定是否是Number,有可能是Object
Number m = list.get(1);
// 编译正确,Object可以接受任意值
Object o = list.get(2);

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/232094.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

VR虚拟仿真技术应用到外事警察岗位技能培训的场景及优势

VR治安民警常态化工作实战教学演练是一种利用VR虚拟现实制作和web3d开发技术进行治安民警培训和实战演练的新型教学模式。相较于传统的培训方式&#xff0c;VR治安民警常态化工作实战教学演练具有以下优点&#xff1a; VR实战是一种完全虚拟的实战训练方式&#xff0c;他可以根…

Java 中单例模式的常见实现方式

一、什么是单例模式&#xff1f; 单例模式是一种创建型设计模式&#xff0c;它确保类只有一个实例&#xff0c;并提供全局访问点让外部代码可以访问该实例。 在 Java 中&#xff0c;可以使用单例模式来实现一些全局性的操作&#xff0c;例如配置文件管理、线程池管理、数据库连…

Linux系统中查看路由表的命令(ip route)

以下命令是在Linux系统中查看路由表的命令&#xff1a; 在Linux系统中&#xff0c;有多种方法可以查看路由设置。以下是一些常用的命令&#xff1a; ip route 或 ip -4 route&#xff08;IPv4&#xff09;/ ip -6 route&#xff08;IPv6&#xff09;&#xff1a; 这是最常用且功…

【数据结构】树状数组算法总结

知识概览 树状数组有两个作用&#xff1a; 快速求前缀和 时间复杂度O(log(n))修改某一个数 时间复杂度O(log(n)) 例题展示 1. 单点修改&#xff0c;区间查询 题目链接 活动 - AcWing本活动组织刷《算法竞赛进阶指南》&#xff0c;系统学习各种编程算法。主要面向…

使用IDEA创建springboot依赖下载很慢,解决方法

显示一直在resolving dependencies&#xff0c;速度很慢 原因&#xff1a;maven会使用远程仓库来加载依赖&#xff0c;是一个国外的网站&#xff0c;所以会很慢。应该使用阿里云的镜像&#xff0c;这样速度会提升很多。 步骤&#xff1a;1.右击pom.xml&#xff0c;选择"m…

nodejs+vue+微信小程序+python+PHP购物商城网站-计算机毕业设计推荐

综合购物商城管理经历和对网上信息归纳整理的结果&#xff0c;在实际应用中&#xff0c;将用户分为两种&#xff1a;管理员和用户两个角色。其中用户可以操作的内容是有限的&#xff0c;管理员可以进行的操作最多。了解系统用户的分类以及可以进行的操作&#xff0c;对于接下来…

windows下seleninum环境搭建

一&#xff1a;介绍 selenium 是一个web的自动化测试工具&#xff0c;不少学习功能自动化的同学开始首选selenium &#xff0c;相因为它相比QTP有诸多有点&#xff1a; * 免费&#xff0c;也不用再为破解QTP而大伤脑筋 * 小巧&#xff0c;对于不同的语言它只是一个包而已&…

10kw直流负载主要工作方式

直接供电方式&#xff1a;是最简单的工作方式&#xff0c;即通过一个稳定的直流电源直接为10kW直流负载供电。这种方式的优点是简单、可靠&#xff0c;但缺点是电源的选择和配置较为复杂&#xff0c;需要考虑到负载的工作电压、电流、功率因数等因素。此外&#xff0c;如果电源…

JavaWeb编程语言—登录校验

一、前言&简介 前言&#xff1a;小编的上一篇文章“JavaWeb编程语言—登录功能实现”&#xff0c;介绍了如何通过Java代码实现通过接收前端传来的账号、密码信息来登录后端服务器&#xff0c;但是没有实现登录校验功能&#xff0c;这代表着用户不需要登录也能直接访问服务器…

SpringBoot项目jar包加密防止反编译

业务场景 由于公司业务需要&#xff0c;需要把jar包部署到其它公司的服务器&#xff0c;又不想泄露源码。 解决方法 1、代码混淆 采用proguard-maven-plugin插件 在单模块中此方案还算简单&#xff0c;但是现在项目一般都是多模块&#xff0c;一个模块依赖多个公共模块。那…

4.docker镜像及相关命令

目录 1 查看所有镜像 docker images 1.1 基本用法 1.2 docker images -q 只显示所有镜像ID 1.3 docker images -f [筛选条件] -q 只显示符合条件的所有镜像ID 1.4 docker images --no-trunc 显示完整的IMAGE ID 1.5 docker images --format [模板] 使用模板 2 从…

十问ByteHouse:如何基于ClickHouse玩转向量检索?

更多技术交流、求职机会&#xff0c;欢迎关注字节跳动数据平台微信公众号&#xff0c;回复【1】进入官方交流群 向量检索被广泛使用于以图搜图、内容推荐以及大模型推理等场景。随着业务升级与 AI 技术的广泛使用&#xff0c;用户期望处理的向量数据规模越来越大&#xff0c;对…

Java智慧工地数字化云平台源码(SaaS模式)

智慧工地是智慧城市理念在建筑工程行业的具体体现&#xff0c;智慧工地解决方案是建立在高度信息化基础上一种支持人事物全面感知、施工技术全面智能、工作互通互联、信息协同共享、决策科学分析、风险智慧预控的新型信息化手段。围绕人、机、料、法、环等关键要素&#xff0c;…

056:vue工具 --- CSS在线格式化

第056个 查看专栏目录: VUE ------ element UI 专栏目标 在vue和element UI联合技术栈的操控下&#xff0c;本专栏提供行之有效的源代码示例和信息点介绍&#xff0c;做到灵活运用。 &#xff08;1&#xff09;提供vue2的一些基本操作&#xff1a;安装、引用&#xff0c;模板使…

SimpleFOC核心代码,解决Id一直为正 无法控为0

注&#xff1a;&#xff08;我这个是用simulink仿真做的SimpleFOC&#xff0c;因此代码是m语言的&#xff0c;在stm32可以改成C的&#xff0c;这两种基本差不多&#xff0c;搭建的SimpleFOC仿真下载链接&#xff1a;https://download.csdn.net/download/qq_35239859/88642607?…

虚幻学习笔记18—C++委托(多播)和事件

一、前言 委托分单播和多播&#xff0c;多播就是可以绑定多个回调函数&#xff0c;然后一次性执行。这样也可以理解为啥多播没有返回值&#xff0c;多个回调函数执行后返回哪一个都是问题啊。而事件呢官方官方文档说法是“对于事件而言&#xff0c;只有定义事件的类才能调用 Br…

用python编写一个对列表降序排列的方法。 定义函数des(),该函数接收一个整数列表作为参数。 在函数内部,实现降序排序。

编写一个对列表降序排列的方法。 定义函数des()&#xff0c;该函数接收一个整数列表作为参数。 在函数内部&#xff0c;实现降序排序。 然后&#xff0c;返回排序后的列表。 最后&#xff0c;在函数外部打印排序后的列表。 以下是一个使用Python编写的对列表降序排列的方法的示…

后端笔记之gin框架学习

gin框架学习 1. 使用脚手架搭建gin框架2. 应用框架3. 路由管理4.自定义中间件的使用5. 通过中间件设置路由权限校验1. 自定义校验2. 配置跨域3. 使用jwt进行tokn校验 6. 接口入参获取和绑定2. 参数校验3. protobuf 7. 集成mysql数据库1. gorm使用 1. 使用脚手架搭建gin框架 gi…

MapReduce和Yarn部署+入门

看的黑马视频记的笔记 目录 1.入门知识点 分布式计算&#xff1a; 概念&#xff1a; 两种模式&#xff1a; MapReduce&#xff08;分布式计算&#xff0c;分散汇总模式&#xff09; 概念 执行原理 注&#xff1a; Yarn&#xff08;分布式资源调度&#xff09; 概述 Y…

VS Code配置Go语言开发环境

提示&#xff1a;首先这是一个新型语言&#xff0c;最好把vscode更新到最新版。 1&#xff1a;去官网下载Go语言编译器&#xff0c;之后配置到系统环境中&#xff0c;能看到版本就行。 2&#xff1a;创建一个文件夹&#xff0c;存放go的工具文件&#xff0c;我的在D:\GoFile\G…