泛型在项目中不怎么用?这次结合实战!

2024-02-15 02:44

本文主要是介绍泛型在项目中不怎么用?这次结合实战!,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

泛型在项目中用的确实相对没有那么多,但是也可以提供一些便捷,本文先从基础介绍,然后在文章最后讲解项目实战中的一些使用,超级详细!🌟

文章目录

  • 泛型基础
    • 为什么引入泛型
      • 实例
    • 泛型的基本使用
      • 泛型类
      • 泛型接口
      • 泛型方法
      • 泛型上下限
      • 泛型数组
  • 深入理解泛型
    • 类型擦除
    • 类型擦除保留的原始类型
    • 泛型在编译器的检查
  • 项目中的泛型实战

泛型基础

为什么引入泛型

  • 适用于多种数据类型执行相同的代码(代码复用)
  • 泛型中的类型在使用时指定,不需要强制类型转换(类型安全,编译器会检查类型
  • Java泛型也是一种语法糖,在编译阶段完成类型的转换的工作,避免在运行时强制类型转换而出现ClassCastException,类型转化异常。

实例

  • 不引入泛型
public class target_01 {public static void main(String[] args) {List list = new ArrayList();list.add(11);list.add("落雨既然");for (int i = 0; i < list.size(); i++) {System.out.println((String)list.get(i));}}
}

会报类型转换异常:
image.png

  • 使用泛型
public class target_01 {public static void main(String[] args) {List<String> list = new ArrayList();list.add("落雨既然");for (int i = 0; i < list.size(); i++) {System.out.println((String)list.get(i));}}
}

image.png
在上述的实例中,我们只能添加String类型的数据,否则编译器会报错。

泛型的基本使用

泛型类

  • 泛型类概述:把泛型定义在类上
  • 定义格式:

注意事项:泛型类型必须是引用类型(非基本数据类型)

泛型接口

  • 泛型方法概述:把泛型定义在方法上
  • 定义格式:

注意要点:方法声明中定义的形参只能在该方法里使用,而接口、类声明中定义的类型形参则可以在整个接口、类中使用。当调用fun()方法时,根据传入的实际对象,编译器就会判断出类型形参T所代表的实际类型。

class Demo {// 泛型方法,可以接收任意类型的数据public <T> T fun(T t) {// 直接将参数返回return t;}
}public class GenericsDemo26 {public static void main(String args[]) {// 实例化Demo对象Demo d = new Demo();// 传递字符串String str = d.fun("落雨既然");// 传递数字,自动装箱int i = d.fun(30);// 输出字符串内容System.out.println(str);// 输出数字内容System.out.println(i);}
//  输出:
//  落雨既然
//  30
}

泛型方法

image.png
image.png
说明一下,定义泛型方法时,必须在返回值前边加一个,来声明这是一个泛型方法,持有一个泛型T,然后才可以用泛型T作为方法的返回值。
Class的作用就是指明泛型的具体类型,而Class类型的变量c,可以用来创建泛型类的对象。
为什么要用变量c来创建对象呢?既然是泛型方法,就代表着我们不知道具体的类型是什么,也不知道构造方法如何,因此没有办法去new一个对象,但可以利用变量c的newInstance方法去创建对象,也就是利用反射创建对象。
泛型方法要求的参数是Class类型,而Class.forName()方法的返回值也是Class,因此可以用Class.forName()作为参数。其中,forName()方法中的参数是何种类型,返回的Class就是何种类型。在本例中,forName()方法中传入的是User类的完整路径,因此返回的是Class类型的对象,因此调用泛型方法时,变量c的类型就是Class,因此泛型方法中的泛型T就被指明为User,因此变量obj的类型为User。
当然,泛型方法不是仅仅可以有一个参数Class,可以根据需要添加其他参数。
为什么要使用泛型方法呢
因为泛型类要在实例化的时候就指明类型,如果想换一种类型,不得不重新new一次,可能不够灵活;而泛型方法可以在调用的时候指明类型,更加灵活。

泛型上下限

    public static void funC(List<? extends A> listA) {// ...          }public static void funD(List<B> listB) {funC(listB); // OK// ...             }

为了解决泛型中隐含的转换问题,Java泛型加入了类型参数的上下边界机制。<? extends A>表示该类型参数可以是A(上边界)或者A的子类类型。编译时擦除到类型A,即用A类型代替类型参数。这种方法可以解决开始遇到的问题,编译器知道类型参数的范围,如果传入的实例类型B是在这个范围内的话允许转换,这时只要一次类型转换就可以了,运行时会把对象当做A的实例看待。
如果不用泛型就会报错:
上界:

class Info<T extends Number>{    // 此处泛型只能是数字类型

下界:

    public static void fun(Info<? super String> temp){    // 只能接收String或Object类型的泛型,String类的父类只有Object类

小结:

<?> 无限制通配符
<? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类
<? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类// 使用原则《Effictive Java》
// 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限
1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>;
2. 如果它表示一个 T 的消费者,就使用 < ? super T>3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。

泛型数组

List<String>[] list11 = new ArrayList<String>[10]; //编译错误,非法创建 
List<String>[] list12 = new ArrayList<?>[10]; //编译错误,需要强转类型 
List<String>[] list13 = (List<String>[]) new ArrayList<?>[10]; //OK,但是会有警告 
List<?>[] list14 = new ArrayList<String>[10]; //编译错误,非法创建 
List<?>[] list15 = new ArrayList<?>[10]; //OK 
List<String>[] list6 = new ArrayList[10]; //OK,但是会有警告
  • 使用场景
public class GenericsDemo30{  public static void main(String args[]){  Integer i[] = fun1(1,2,3,4,5,6) ;   // 返回泛型数组  fun2(i) ;  }  public static <T> T[] fun1(T...arg){  // 接收可变参数  return arg ;            // 返回泛型数组  }  public static <T> void fun2(T param[]){   // 输出  System.out.print("接收泛型数组:") ;  for(T t:param){  System.out.print(t + "、") ;  }  }  
}

image.png

深入理解泛型

类型擦除

泛型的类型擦除原则是:

  • 消除类型参数声明,即删除<>及其包围的部分。
  • 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。
  • 为了保证类型安全,必要时插入强制类型转换代码。
  • 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。

image.png
image.png
image.png

类型擦除保留的原始类型

原始类型 就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型,无论何时定义一个泛型,相应的原始类型都会被自动提供,类型变量擦除,并使用其限定类型(无限定的变量用Object)替换。

泛型在编译器的检查

java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。
例如:

public static  void main(String[] args) {  ArrayList<String> list = new ArrayList<String>();  list.add("123");  list.add(123);//编译错误  
}

在上面的程序中,使用add方法添加一个整型,在IDE中,直接会报错,说明这就是在编译之前的检查,因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。

项目中的泛型实战

泛型很多都是理论,在项目中怎么用呢?
比如对于常见的缓存穿透,缓存击穿,我们就可以使用泛型将其封装到一个类里面。
比如下面代码,是黑马点评项目中的一个点:通过泛型 + 函数式编程封装成通用解决方案。
难点:

  • 泛型方法的使用:返回值类型不确定、id类型不确定。所以就声明泛型,让调用者告诉我们泛型是什么;
  • 使用函数式接口:牵扯到数据库查询,需要参数和返回值,使用函数式接口Function<ID,R>
    • 四大函数式接口 Function<T,R> Predicate Consumer Supplier
/*** 缓存工具封装*/
@Slf4j
@Component
public class CacheClient {@Resourceprivate StringRedisTemplate stringRedisTemplate;//缓存击穿使用的线程池private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);/*** 将任意java对象序列化为json字符串并存储在string类型的key中,并设置TTL** @param key   string类型的key* @param value 任意java对象* @param time  时间* @param unit  单位*/public void set(String key, Object value, Long time, TimeUnit unit) {stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);}/*** 将任意java对象序列化为json字符串并存储在string类型的key中,并设置逻辑过期时间,用于处理缓存击穿** @param key   string类型的key* @param value 任意java对象* @param time  逻辑时间* @param unit  单位*/public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {// RedisData对象,设置逻辑过期RedisData redisData = new RedisData();redisData.setData(value);redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));// 写入RedisstringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));}/*** 解决缓存穿透** @param keyPrefix  key前缀* @param id         id不知道什么类型,所以需要声名泛型ID,名字随意起* @param type       是什么类型* @param dbFallback 如果redis查询的不是"",那就需要查询数据库,函数式接口指定逻辑* @param time       重建缓存后的有效时间* @param unit       时间单位* @param <R>        返回值类型,例如Shop类型* @param <ID>       id不知道什么类型,所以需要声名泛型ID,名字随意起* @return*/public <R, ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {String key = keyPrefix + id;// 1.从redis查询缓存String json = stringRedisTemplate.opsForValue().get(key);// 2.判断是否存在 。isNotBlank只有在 字符串 才返回true。 换行 ,null, ""等都是falseif (StrUtil.isNotBlank(json)) {// 3.存在,直接返回return JSONUtil.toBean(json, type);}//      3.2  如果不存在,则有  null,"",换行  等可能性。如果是"", 则是为了解决缓存穿透而约定的规则if ("".equals(json)) {// 解决缓存穿透,不会再去查数据库return null;}// 4.如果不存在,且不是"" ;那么原因可能是缓存中为null,需要根据id去查询数据库R r = dbFallback.apply(id);// 5.不存在,返回错误if (r == null) {// 将空值写入redisstringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回错误信息return null;}// 6.存在,写入redis,调用已经写好的方法,超时剔除this.set(key, r, time, unit);return r;}/*** 逻辑过期 解决缓存击穿*/public <R, ID> R queryWithLogicalExpire(String keyPrefix, ID id, Class<R> type,String lockKeyPrefix, Function<ID, R> dbFallback, Long time, TimeUnit unit) {String key = keyPrefix + id;// 1.从redis查询缓存String json = stringRedisTemplate.opsForValue().get(key);// 2.判断是否存在if (StrUtil.isBlank(json)) {//  3.1 不存在直接返回null,不是热点keyreturn null;}//  3.2 存在,反序列化为RedisData对象RedisData redisData = JSONUtil.toBean(json, RedisData.class);//        得到R对象R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);LocalDateTime expireTime = redisData.getExpireTime();// 4.判断是否过期if (expireTime.isAfter(LocalDateTime.now())) {// 4.1.未过期,直接返回return r;}// 5. 已过期,需要缓存重建// 6.缓存重建// 6.1.获取互斥锁String lockKey = lockKeyPrefix + id;boolean isLock = tryLock(lockKey);// 6.2.判断是否获取锁成功if (isLock) {// 6.3.成功,开启独立线程,实现缓存重建
//            在这之前需要DoubleCheck,再次查看redis缓存是否过期json = stringRedisTemplate.opsForValue().get(key);
//            判断是否存在if (StrUtil.isNotBlank(json)) {//            5.2.2.1 存在则判断是否过期,未过期就直接返回,不需要缓存构建redisData = JSONUtil.toBean(json, RedisData.class);r = JSONUtil.toBean((JSONObject) redisData.getData(), type);expireTime = redisData.getExpireTime();if (expireTime.isAfter(LocalDateTime.now())) {//   未过期,直接返回return r;}}//   6.4 已过期 || 不存在  则重新构建,开启线程池(如果自己new 线程,性能不好)CACHE_REBUILD_EXECUTOR.submit(() -> {try {// 查询数据库R newR = dbFallback.apply(id);// 重建缓存--热点keythis.setWithLogicalExpire(key, newR, time, unit);} catch (Exception e) {throw new RuntimeException(e);} finally {// 释放锁unlock(lockKey);}});}// 6.4.返回过期的信息return r;}/*** 互斥锁 解决缓存击穿*/public <R, ID> R queryWithMutex(String keyPrefix, ID id, Class<R> type,String lockKeyPrefix, long sleepTime,Function<ID, R> dbFallback, Long time, TimeUnit unit) {String key = keyPrefix + id;// 1.从redis查询缓存String json = stringRedisTemplate.opsForValue().get(key);
//        2.判断是否存在 。isNotBlank只有在 字符串 才返回true。 换行 ,null, ""等都是falseif (StrUtil.isNotBlank(json)) {// 3.存在,直接返回return JSONUtil.toBean(json, type);}
//      3.2  如果不存在,则有  null,"",换行  等可能性。如果是"", 则是为了解决缓存穿透而约定的规则if ("".equals(json)) {
//            解决缓存穿透,不会再去查数据库return null;}//        4.如果不存在,且不是"" ;那么原因可能是缓存中为null,需要根据id去查询数据库
//        ==========解决缓存击穿==========
//        4.1 获取互斥锁String lockKey = lockKeyPrefix + id;R r = null;try {boolean isLock = tryLock(lockKey);// 4.2.判断是否获取成功if (!isLock) {// 4.3.获取锁失败,休眠并重试Thread.sleep(sleepTime);return queryWithMutex(keyPrefix, id ,type,lockKeyPrefix,sleepTime, dbFallback, time, unit);}// 4.4 成功,做双重检查锁,查看redis缓存是否存在,存在则无需重建缓存json = stringRedisTemplate.opsForValue().get(key);// 判断是否存在 。isNotBlank只有在 字符串 才返回true。 换行 ,null, ""等都是falseif (StrUtil.isNotBlank(json)) {//   存在直接返回r = JSONUtil.toBean(json, type);return r;}//   如果不存在,则有  null,"",换行  等可能性。如果是"", 则是为了解决缓存穿透而约定的规则if ("".equals(json)) {//           解决缓存穿透,不会再去查数据库return null;}//  5. 到这里说明通过双重检查锁,代表是第一个线程,则根据id查询数据库r = dbFallback.apply(id);// 不存在,返回错误if (r == null) {// 将空值写入redisstringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回错误信息return null;}// 6.存在,写入redisthis.set(key, r, time, unit);} catch (InterruptedException e) {throw new RuntimeException(e);} finally {// 7.释放锁unlock(lockKey);}// 8.返回return r;}private boolean tryLock(String key) {Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
//        不要直接返回,因为会自动拆箱,如果为null,会报空指针异常。
//        使用工具类return BooleanUtil.isTrue(flag);}private void unlock(String key) {stringRedisTemplate.delete(key);}
}

参考文章:
https://blog.csdn.net/nvd11/article/details/27393445
https://juejin.cn/post/6844903925666021389?searchId=202402142156151370FABE70EBA2501841#heading-8

这篇关于泛型在项目中不怎么用?这次结合实战!的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/710255

相关文章

部署Vue项目到服务器后404错误的原因及解决方案

《部署Vue项目到服务器后404错误的原因及解决方案》文章介绍了Vue项目部署步骤以及404错误的解决方案,部署步骤包括构建项目、上传文件、配置Web服务器、重启Nginx和访问域名,404错误通常是... 目录一、vue项目部署步骤二、404错误原因及解决方案错误场景原因分析解决方案一、Vue项目部署步骤

使用 sql-research-assistant进行 SQL 数据库研究的实战指南(代码实现演示)

《使用sql-research-assistant进行SQL数据库研究的实战指南(代码实现演示)》本文介绍了sql-research-assistant工具,该工具基于LangChain框架,集... 目录技术背景介绍核心原理解析代码实现演示安装和配置项目集成LangSmith 配置(可选)启动服务应用场景

golang内存对齐的项目实践

《golang内存对齐的项目实践》本文主要介绍了golang内存对齐的项目实践,内存对齐不仅有助于提高内存访问效率,还确保了与硬件接口的兼容性,是Go语言编程中不可忽视的重要优化手段,下面就来介绍一下... 目录一、结构体中的字段顺序与内存对齐二、内存对齐的原理与规则三、调整结构体字段顺序优化内存对齐四、内

配置springboot项目动静分离打包分离lib方式

《配置springboot项目动静分离打包分离lib方式》本文介绍了如何将SpringBoot工程中的静态资源和配置文件分离出来,以减少jar包大小,方便修改配置文件,通过在jar包同级目录创建co... 目录前言1、分离配置文件原理2、pom文件配置3、使用package命令打包4、总结前言默认情况下,

在Java中使用ModelMapper简化Shapefile属性转JavaBean实战过程

《在Java中使用ModelMapper简化Shapefile属性转JavaBean实战过程》本文介绍了在Java中使用ModelMapper库简化Shapefile属性转JavaBean的过程,对比... 目录前言一、原始的处理办法1、使用Set方法来转换2、使用构造方法转换二、基于ModelMapper

Java实战之自助进行多张图片合成拼接

《Java实战之自助进行多张图片合成拼接》在当今数字化时代,图像处理技术在各个领域都发挥着至关重要的作用,本文为大家详细介绍了如何使用Java实现多张图片合成拼接,需要的可以了解下... 目录前言一、图片合成需求描述二、图片合成设计与实现1、编程语言2、基础数据准备3、图片合成流程4、图片合成实现三、总结前

python实现简易SSL的项目实践

《python实现简易SSL的项目实践》本文主要介绍了python实现简易SSL的项目实践,包括CA.py、server.py和client.py三个模块,文中通过示例代码介绍的非常详细,对大家的学习... 目录运行环境运行前准备程序实现与流程说明运行截图代码CA.pyclient.pyserver.py参

Go语言利用泛型封装常见的Map操作

《Go语言利用泛型封装常见的Map操作》Go语言在1.18版本中引入了泛型,这是Go语言发展的一个重要里程碑,它极大地增强了语言的表达能力和灵活性,本文将通过泛型实现封装常见的Map操作,感... 目录什么是泛型泛型解决了什么问题Go泛型基于泛型的常见Map操作代码合集总结什么是泛型泛型是一种编程范式,允

nginx-rtmp-module构建流媒体直播服务器实战指南

《nginx-rtmp-module构建流媒体直播服务器实战指南》本文主要介绍了nginx-rtmp-module构建流媒体直播服务器实战指南,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有... 目录1. RTMP协议介绍与应用RTMP协议的原理RTMP协议的应用RTMP与现代流媒体技术的关系2

使用DeepSeek API 结合VSCode提升开发效率

《使用DeepSeekAPI结合VSCode提升开发效率》:本文主要介绍DeepSeekAPI与VisualStudioCode(VSCode)结合使用,以提升软件开发效率,具有一定的参考价值... 目录引言准备工作安装必要的 VSCode 扩展配置 DeepSeek API1. 创建 API 请求文件2.