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

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

相关文章

idea+spring boot创建项目的搭建全过程

《idea+springboot创建项目的搭建全过程》SpringBoot是Spring社区发布的一个开源项目,旨在帮助开发者快速并且更简单的构建项目,:本文主要介绍idea+springb... 目录一.idea四种搭建方式1.Javaidea命名规范2JavaWebTomcat的安装一.明确tomcat

pycharm跑python项目易出错的问题总结

《pycharm跑python项目易出错的问题总结》:本文主要介绍pycharm跑python项目易出错问题的相关资料,当你在PyCharm中运行Python程序时遇到报错,可以按照以下步骤进行排... 1. 一定不要在pycharm终端里面创建环境安装别人的项目子模块等,有可能出现的问题就是你不报错都安装

Python爬虫HTTPS使用requests,httpx,aiohttp实战中的证书异步等问题

《Python爬虫HTTPS使用requests,httpx,aiohttp实战中的证书异步等问题》在爬虫工程里,“HTTPS”是绕不开的话题,HTTPS为传输加密提供保护,同时也给爬虫带来证书校验、... 目录一、核心问题与优先级检查(先问三件事)二、基础示例:requests 与证书处理三、高并发选型:

uni-app小程序项目中实现前端图片压缩实现方式(附详细代码)

《uni-app小程序项目中实现前端图片压缩实现方式(附详细代码)》在uni-app开发中,文件上传和图片处理是很常见的需求,但也经常会遇到各种问题,下面:本文主要介绍uni-app小程序项目中实... 目录方式一:使用<canvas>实现图片压缩(推荐,兼容性好)示例代码(小程序平台):方式二:使用uni

MyCat分库分表的项目实践

《MyCat分库分表的项目实践》分库分表解决大数据量和高并发性能瓶颈,MyCat作为中间件支持分片、读写分离与事务处理,本文就来介绍一下MyCat分库分表的实践,感兴趣的可以了解一下... 目录一、为什么要分库分表?二、分库分表的常见方案三、MyCat简介四、MyCat分库分表深度解析1. 架构原理2. 分

Oracle Scheduler任务故障诊断方法实战指南

《OracleScheduler任务故障诊断方法实战指南》Oracle数据库作为企业级应用中最常用的关系型数据库管理系统之一,偶尔会遇到各种故障和问题,:本文主要介绍OracleSchedul... 目录前言一、故障场景:当定时任务突然“消失”二、基础环境诊断:搭建“全局视角”1. 数据库实例与PDB状态2

Git进行版本控制的实战指南

《Git进行版本控制的实战指南》Git是一种分布式版本控制系统,广泛应用于软件开发中,它可以记录和管理项目的历史修改,并支持多人协作开发,通过Git,开发者可以轻松地跟踪代码变更、合并分支、回退版本等... 目录一、Git核心概念解析二、环境搭建与配置1. 安装Git(Windows示例)2. 基础配置(必

linux查找java项目日志查找报错信息方式

《linux查找java项目日志查找报错信息方式》日志查找定位步骤:进入项目,用tail-f实时跟踪日志,tail-n1000查看末尾1000行,grep搜索关键词或时间,vim内精准查找并高亮定位,... 目录日志查找定位在当前文件里找到报错消息总结日志查找定位1.cd 进入项目2.正常日志 和错误日

在.NET项目中嵌入Python代码的实践指南

《在.NET项目中嵌入Python代码的实践指南》在现代开发中,.NET与Python的协作需求日益增长,从机器学习模型集成到科学计算,从脚本自动化到数据分析,然而,传统的解决方案(如HTTPAPI或... 目录一、CSnakes vs python.NET:为何选择 CSnakes?二、环境准备:从 Py

基于 Cursor 开发 Spring Boot 项目详细攻略

《基于Cursor开发SpringBoot项目详细攻略》Cursor是集成GPT4、Claude3.5等LLM的VSCode类AI编程工具,支持SpringBoot项目开发全流程,涵盖环境配... 目录cursor是什么?基于 Cursor 开发 Spring Boot 项目完整指南1. 环境准备2. 创建