泛型在项目中用的确实相对没有那么多,但是也可以提供一些便捷,本文先从基础介绍,然后在文章最后讲解项目实战中的一些使用,超级详细!🌟
泛型基础
为什么引入泛型
- 适用于多种数据类型执行相同的代码(代码复用)
- 泛型中的类型在使用时指定,不需要强制类型转换(类型安全 ,编译器会检查类型)
- Java泛型也是一种语法糖,在编译阶段完成类型的转换的工作,避免在运行时强制类型转换而出现ClassCastException,类型转化异常。
实例
- 不引入泛型
java
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));
}
}
}
会报类型转换异常:
- 使用泛型
java
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));
}
}
}
在上述的实例中,我们只能添加String类型的数据,否则编译器会报错。
泛型的基本使用
泛型类
- 泛型类概述:把泛型定义在类上
- 定义格式:
注意事项:泛型类型必须是引用类型(非基本数据类型)
泛型接口
- 泛型方法概述:把泛型定义在方法上
- 定义格式:
注意要点:方法声明 中定义的形参只能在该方法里使用 ,而接口、类声明中定义的类型形参则可以在整个接口、类中使用。当调用fun()方法时,根据传入的实际对象,编译器就会判断出类型形参T所代表的实际类型。
java
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
}
泛型方法
说明一下,定义泛型方法时,必须在返回值前边加一个,来声明这是一个泛型方法,持有一个泛型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一次,可能不够灵活;而泛型方法可以在调用的时候指明类型,更加灵活。
泛型上下限
java
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的实例看待。 如果不用泛型就会报错:
上界:
java
class Info<T extends Number>{ // 此处泛型只能是数字类型
下界:
java
public static void fun(Info<? super String> temp){ // 只能接收String或Object类型的泛型,String类的父类只有Object类
小结:
java
<?> 无限制通配符
<? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类
<? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类
// 使用原则《Effictive Java》
// 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限
1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>;
2. 如果它表示一个 T 的消费者,就使用 < ? super T>;
3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。
泛型数组
java
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,但是会有警告
- 使用场景
java
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 + "、") ;
}
}
}
深入理解泛型
类型擦除
泛型的类型擦除原则是:
- 消除类型参数声明,即删除<>及其包围的部分。
- 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。
- 为了保证类型安全,必要时插入强制类型转换代码。
- 自动产生"桥接方法"以保证擦除类型后的代码仍然具有泛型的"多态性"。
类型擦除保留的原始类型
原始类型 就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型,无论何时定义一个泛型,相应的原始类型都会被自动提供,类型变量擦除,并使用其限定类型(无限定的变量用Object)替换。
泛型在编译器的检查
java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。 例如:
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
java
/**
* 缓存工具封装
*/
@Slf4j
@Component
public class CacheClient {
@Resource
private 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)));
// 写入Redis
stringRedisTemplate.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, ""等都是false
if (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) {
// 将空值写入redis
stringRedisTemplate.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,不是热点key
return 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);
// 重建缓存--热点key
this.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, ""等都是false
if (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, ""等都是false
if (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) {
// 将空值写入redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
// 返回错误信息
return null;
}
// 6.存在,写入redis
this.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);
}
}