4 changed files with 385 additions and 0 deletions
@ -0,0 +1,64 @@ |
|||
import java.util.HashMap; |
|||
import java.util.Map; |
|||
|
|||
public class Cache<K, V> { |
|||
// 底层用 HashMap 存储数据
|
|||
private final Map<K, V> cacheMap; |
|||
|
|||
public Cache() { |
|||
this.cacheMap = new HashMap<>(); |
|||
} |
|||
|
|||
// 存入缓存
|
|||
public void put(K key, V value) { |
|||
cacheMap.put(key, value); |
|||
} |
|||
|
|||
// 从缓存获取
|
|||
public V get(K key) { |
|||
return cacheMap.get(key); |
|||
} |
|||
|
|||
// 删除缓存
|
|||
public void remove(K key) { |
|||
cacheMap.remove(key); |
|||
} |
|||
|
|||
// 判断缓存是否包含某个 key
|
|||
public boolean containsKey(K key) { |
|||
return cacheMap.containsKey(key); |
|||
} |
|||
|
|||
// 清空缓存
|
|||
public void clear() { |
|||
cacheMap.clear(); |
|||
} |
|||
|
|||
// 获取缓存大小
|
|||
public int size() { |
|||
return cacheMap.size(); |
|||
} |
|||
|
|||
// 测试主方法
|
|||
public static void main(String[] args) { |
|||
Cache<String, Integer> studentScoreCache = new Cache<>(); |
|||
|
|||
// 存入数据
|
|||
studentScoreCache.put("张三", 90); |
|||
studentScoreCache.put("李四", 85); |
|||
studentScoreCache.put("王五", 95); |
|||
|
|||
// 查询数据
|
|||
System.out.println("张三的分数:" + studentScoreCache.get("张三")); |
|||
System.out.println("缓存大小:" + studentScoreCache.size()); |
|||
|
|||
// 删除数据
|
|||
studentScoreCache.remove("李四"); |
|||
System.out.println("删除李四后缓存大小:" + studentScoreCache.size()); |
|||
System.out.println("是否包含李四?" + studentScoreCache.containsKey("李四")); |
|||
|
|||
// 清空缓存
|
|||
studentScoreCache.clear(); |
|||
System.out.println("清空后缓存大小:" + studentScoreCache.size()); |
|||
} |
|||
} |
|||
@ -0,0 +1,84 @@ |
|||
一、Java 泛型擦除的本质 |
|||
Java 的泛型是一种编译期语法糖,并非原生支持的语言特性。为了兼容 JDK 1.5 之前没有泛型的代码,编译器会在编译阶段执行类型擦除(Type Erasure): |
|||
|
|||
1. 擦除泛型参数 |
|||
所有泛型类型参数(如 `<T>`、`<K, V>`)会被替换为它们的上界类型: |
|||
- 未指定上界时,默认擦除为 `Object`。 |
|||
例如 `List<String>` → 编译后变为 `List`,`add` 方法变成 `add(Object)`,`get` 方法返回 `Object`。 |
|||
- 指定上界时,擦除为上界类型。 |
|||
例如 `List<? extends Number>` → 编译后擦除为 `List<Number>`。 |
|||
|
|||
2. 插入强制类型转换 |
|||
为了保证代码在运行时的类型安全,编译器会在读取泛型元素的地方自动插入强制类型转换。 |
|||
例如 `String s = list.get(0);` 编译后会变成 `String s = (String) list.get(0);`。 |
|||
|
|||
核心结果: |
|||
运行时 JVM 只能看到原始类型(如 `List`、`Map`),无法直接感知到 `String`、`Integer` 这类泛型参数信息。 |
|||
|
|||
二、为什么还能通过反射获取部分泛型信息? |
|||
虽然大部分泛型信息被擦除了,但有一部分结构级别的泛型信息会被编译器刻意保留,并写入 `.class` 字节码文件的 `Signature` 属性中。 |
|||
|
|||
1. 哪些泛型信息会被保留? |
|||
只有定义在类、接口、成员变量、方法签名上的泛型信息会被保留,具体包括: |
|||
- 类/接口的父类/实现接口声明 |
|||
例如 `public class MyList extends ArrayList<String>`,`ArrayList<String>` 这个带泛型的父类信息会被保留。 |
|||
- 类中成员变量(Field)的泛型类型 |
|||
例如 `private List<Integer> scores;`,`List<Integer>` 的泛型信息会被保留。 |
|||
- 方法(Method)的参数类型与返回值类型 |
|||
例如 `public List<String> getNames(List<String> ids)`,参数和返回值的泛型信息会被保留。 |
|||
|
|||
2. 为什么这些信息会被保留? |
|||
- 为了保证编译器的类型检查:IDE 和编译器需要这些信息来检查代码的类型安全。 |
|||
- 为了支持反射 API:JDK 设计了专门的反射接口来读取这些信息,保证框架(如 Spring、Jackson)能基于泛型做自动化处理。 |
|||
- 这些信息保存在字节码的 `Signature` 属性中,不会影响运行时的兼容性。 |
|||
|
|||
三、通过反射获取泛型信息的步骤 |
|||
|
|||
步骤 1:获取目标的反射元数据对象 |
|||
根据你要获取的泛型类型,先拿到对应的元数据对象: |
|||
- 获取类/父类的泛型:拿到目标类的 `Class` 对象。 |
|||
- 获取成员变量的泛型:拿到目标变量的 `Field` 对象。 |
|||
- 获取方法参数/返回值的泛型:拿到目标方法的 `Method` 对象。 |
|||
|
|||
步骤 2:调用 `getGenericXXX()` 方法获取带泛型的 `Type` 对象 |
|||
反射 API 提供了专门的方法,来读取字节码中保留的 `Signature` 信息,返回一个 `java.lang.reflect.Type` 对象(它是所有类型的通用接口): |
|||
- 父类:`Class.getGenericSuperclass()` |
|||
- 实现接口:`Class.getGenericInterfaces()` |
|||
- 成员变量类型:`Field.getGenericType()` |
|||
- 方法返回值类型:`Method.getGenericReturnType()` |
|||
- 方法参数类型:`Method.getGenericParameterTypes()` |
|||
|
|||
注意:这些方法和普通的 `getSuperclass()`、`getType()` 不同,后者返回的是擦除后的原始类型(如 `ArrayList`),而前者返回的是保留了泛型信息的 `Type` 对象。 |
|||
|
|||
步骤 3:判断并解析 `ParameterizedType`(参数化类型) |
|||
`Type` 是一个空接口,它的一个重要实现是 `ParameterizedType`,用来表示带泛型的参数化类型(如 `List<String>`、`Map<K, V>`)。 |
|||
|
|||
你需要先判断获取到的 `Type` 是否是 `ParameterizedType` 实例: |
|||
- 如果是,就可以调用它的核心方法 `getActualTypeArguments()`,这个方法会返回一个 `Type[]` 数组,里面就是定义时声明的真实泛型参数。 |
|||
- 数组里的每个元素,都对应着原始声明中的泛型参数(如 `List<String>` 会返回一个长度为 1 的数组,元素为 `String`;`Map<K, V>` 会返回长度为 2 的数组,分别为 `K` 和 `V`)。 |
|||
|
|||
步骤 4:递归解析嵌套泛型(如 `List<List<String>>`) |
|||
如果泛型本身还是一个参数化类型(如 `List<List<String>>`),`getActualTypeArguments()` 返回的 `Type` 仍然是一个 `ParameterizedType`,你可以重复步骤 3,递归解析嵌套的泛型信息。 |
|||
|
|||
四、不同场景下的获取能力对比 |
|||
| 场景 | 示例 | 能否通过反射获取泛型信息? | 原因 | |
|||
|
|||
| 类继承的父类泛型 | `class A extends B<String>` | ✅ 能 | 信息保存在类的 `Signature` 属性中 | |
|||
| 类实现的接口泛型 | `class C implements D<Integer>` | ✅ 能 | 信息保存在类的 `Signature` 属性中 | |
|||
| 成员变量的泛型 | `private List<String> list;` | ✅ 能 | 信息保存在字段的 `Signature` 属性中 | |
|||
| 方法返回值的泛型 | `public List<String> getList()` | ✅ 能 | 信息保存在方法的 `Signature` 属性中 | |
|||
| 方法参数的泛型 | `public void setList(List<String> list)` | ✅ 能 | 信息保存在方法的 `Signature` 属性中 | |
|||
| 方法内局部变量的泛型 | `public void test() { List<String> l = new ArrayList<>(); }` | ❌ 不能 | 局部变量的泛型信息编译后完全擦除,无任何保留 | |
|||
| 直接创建的泛型对象 | `new ArrayList<String>()` | ❌ 不能 | 仅在编译期有效,运行时没有任何元数据 | |
|||
|
|||
五、局限性与注意事项 |
|||
1. 只能获取定义时声明的泛型,无法获取运行时传入的类型 |
|||
例如 `List<String> list = new ArrayList<>();`,反射无法知道 `list` 里存的是 `String`,只能知道 `list` 这个变量声明时的类型是 `List<String>`。 |
|||
2. 无法获取通配符泛型的具体类型 |
|||
例如 `List<? extends Number>`,反射能知道它的上界是 `Number`,但无法知道运行时实际传入的具体子类型。 |
|||
3. 类型变量(如 `<T>`)的解析需要上下文 |
|||
如果是未绑定的类型变量(如 `class Box<T>` 中的 `T`),反射只能获取到变量名,无法知道它的具体类型,除非它被绑定到了具体的类上(如 `class StringBox extends Box<String>`)。 |
|||
|
|||
|
|||
六、总结 |
|||
Java 泛型擦除后,运行时大部分泛型信息会被移除,但类、接口、成员变量、方法签名上声明的泛型信息会被保留在字节码的 `Signature` 属性中。通过反射 API 获取这些元数据,将其解析为 `ParameterizedType` 并调用 `getActualTypeArguments()`,即可读取对应的泛型信息;而方法内局部变量的泛型信息会被完全擦除,无法通过反射获取。 |
|||
@ -0,0 +1,47 @@ |
|||
public class Pair<K, V> { |
|||
private K key; |
|||
private V value; |
|||
|
|||
public Pair(K key, V value) { |
|||
this.key = key; |
|||
this.value = value; |
|||
} |
|||
|
|||
public K getKey() { |
|||
return key; |
|||
} |
|||
|
|||
public void setKey(K key) { |
|||
this.key = key; |
|||
} |
|||
|
|||
public V getValue() { |
|||
return value; |
|||
} |
|||
|
|||
public void setValue(V value) { |
|||
this.value = value; |
|||
} |
|||
|
|||
// swap 方法:交换 key 和 value
|
|||
public Pair<V, K> swap() { |
|||
return new Pair<>(this.value, this.key); |
|||
} |
|||
|
|||
@Override |
|||
public String toString() { |
|||
return "Pair{" + |
|||
"key=" + key + |
|||
", value=" + value + |
|||
'}'; |
|||
} |
|||
|
|||
// 测试主方法
|
|||
public static void main(String[] args) { |
|||
Pair<String, Integer> pair = new Pair<>("年龄", 20); |
|||
System.out.println("原对象:" + pair); |
|||
|
|||
Pair<Integer, String> swapped = pair.swap(); |
|||
System.out.println("交换后:" + swapped); |
|||
} |
|||
} |
|||
@ -0,0 +1,190 @@ |
|||
# 代码审查报告(Cache<K,V> 泛型缓存类) |
|||
|
|||
### 一、代码整体评价 |
|||
|
|||
这段代码整体质量很高,实现了一个基于泛型的键值缓存,功能完整、结构清晰、符合 Java 编码规范,完全满足作业要求。 |
|||
|
|||
#### 二、代码优点 |
|||
|
|||
正确使用泛型 <K,V>泛型定义规范,支持任意类型的键值对,通用性强。 |
|||
|
|||
封装性良好使用 private final Map<K,V> 存储数据,外部无法直接修改,保证数据安全。 |
|||
|
|||
功能完整实现了缓存的核心操作: |
|||
|
|||
添加 put |
|||
|
|||
获取 get |
|||
|
|||
删除 remove |
|||
|
|||
判断存在 containsKey |
|||
|
|||
清空 clear |
|||
|
|||
获取大小 size |
|||
|
|||
代码简洁易读方法命名规范、注释清晰、逻辑直白,符合 Java 标准写法。 |
|||
|
|||
自带测试方法main 方法提供完整测试用例,可直接运行验证功能。 |
|||
|
|||
依赖合理基于 HashMap 实现,性能高效,适合缓存场景。 |
|||
|
|||
### 三、代码存在的问题 |
|||
|
|||
这些不是错误,只是不够规范: |
|||
|
|||
没有对 null 做安全处理 |
|||
|
|||
允许存入 null 键 / 值 |
|||
|
|||
获取时可能返回 null,外部使用容易空指针 |
|||
|
|||
→ 建议增加非空校验 |
|||
|
|||
线程不安全HashMap 是非线程安全的,多线程环境下会出现数据错误。 |
|||
|
|||
没有最大容量限制无缓存上限,不断存入会导致内存占用过高。 |
|||
|
|||
无过期策略真实缓存都有过期时间,当前代码数据永久保存。 |
|||
|
|||
缺少 toString () 方法无法直接打印查看缓存内容,调试不方便。 |
|||
|
|||
### 四、改进后的优化代码 |
|||
|
|||
import java.util.HashMap; |
|||
|
|||
import java.util.Map; |
|||
|
|||
import java.util.Objects; |
|||
|
|||
|
|||
|
|||
public class Cache<K, V> { |
|||
|
|||
  private final Map<K, V> cacheMap; |
|||
|
|||
|
|||
|
|||
  public Cache() { |
|||
|
|||
  this.cacheMap = new HashMap<>(); |
|||
|
|||
  } |
|||
|
|||
|
|||
|
|||
  // 存入缓存(增加非空判断) |
|||
|
|||
  public void put(K key, V value) { |
|||
|
|||
  Objects.requireNonNull(key, "键不能为空"); |
|||
|
|||
  Objects.requireNonNull(value, "值不能为空"); |
|||
|
|||
  cacheMap.put(key, value); |
|||
|
|||
  } |
|||
|
|||
|
|||
|
|||
  // 获取缓存 |
|||
|
|||
  public V get(K key) { |
|||
|
|||
  return cacheMap.get(key); |
|||
|
|||
  } |
|||
|
|||
|
|||
|
|||
  // 删除 |
|||
|
|||
  public void remove(K key) { |
|||
|
|||
  cacheMap.remove(key); |
|||
|
|||
  } |
|||
|
|||
|
|||
|
|||
  // 是否包含 |
|||
|
|||
  public boolean containsKey(K key) { |
|||
|
|||
  return cacheMap.containsKey(key); |
|||
|
|||
  } |
|||
|
|||
|
|||
|
|||
  // 清空 |
|||
|
|||
  public void clear() { |
|||
|
|||
  cacheMap.clear(); |
|||
|
|||
  } |
|||
|
|||
|
|||
|
|||
  // 大小 |
|||
|
|||
  public int size() { |
|||
|
|||
  return cacheMap.size(); |
|||
|
|||
  } |
|||
|
|||
|
|||
|
|||
  // 打印缓存内容 |
|||
|
|||
  @Override |
|||
|
|||
  public String toString() { |
|||
|
|||
  return "Cache{" + "data=" + cacheMap + '}'; |
|||
|
|||
  } |
|||
|
|||
|
|||
|
|||
  // 测试 |
|||
|
|||
  public static void main(String\[] args) { |
|||
|
|||
  Cache<String, Integer> studentScoreCache = new Cache<>(); |
|||
|
|||
|
|||
|
|||
  studentScoreCache.put("张三", 90); |
|||
|
|||
  studentScoreCache.put("李四", 85); |
|||
|
|||
  studentScoreCache.put("王五", 95); |
|||
|
|||
|
|||
|
|||
  System.out.println(studentScoreCache); |
|||
|
|||
  System.out.println("张三的分数:" + studentScoreCache.get("张三")); |
|||
|
|||
  System.out.println("缓存大小:" + studentScoreCache.size()); |
|||
|
|||
|
|||
|
|||
  studentScoreCache.remove("李四"); |
|||
|
|||
  System.out.println("删除李四后:" + studentScoreCache); |
|||
|
|||
|
|||
|
|||
  studentScoreCache.clear(); |
|||
|
|||
  System.out.println("清空后缓存大小:" + studentScoreCache.size()); |
|||
|
|||
  } |
|||
|
|||
} |
|||
|
|||
Loading…
Reference in new issue