Browse Source

提交Java泛型作业

main
ZhengShiyi 2 months ago
parent
commit
8958a9367e
  1. 64
      W8/Cache.java
  2. 84
      W8/Java泛型擦除后如何通过反射获取泛型信息.txt
  3. 47
      W8/Pair.java
  4. 190
      W8/代码审查报告.md

64
W8/Cache.java

@ -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());
}
}

84
W8/Java泛型擦除后如何通过反射获取泛型信息.txt

@ -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()`,即可读取对应的泛型信息;而方法内局部变量的泛型信息会被完全擦除,无法通过反射获取。

47
W8/Pair.java

@ -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);
}
}

190
W8/代码审查报告.md

@ -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> {
&#x20; private final Map<K, V> cacheMap;
&#x20; public Cache() {
&#x20; this.cacheMap = new HashMap<>();
&#x20; }
&#x20; // 存入缓存(增加非空判断)
&#x20; public void put(K key, V value) {
&#x20; Objects.requireNonNull(key, "键不能为空");
&#x20; Objects.requireNonNull(value, "值不能为空");
&#x20; cacheMap.put(key, value);
&#x20; }
&#x20; // 获取缓存
&#x20; public V get(K key) {
&#x20; return cacheMap.get(key);
&#x20; }
&#x20; // 删除
&#x20; public void remove(K key) {
&#x20; cacheMap.remove(key);
&#x20; }
&#x20; // 是否包含
&#x20; public boolean containsKey(K key) {
&#x20; return cacheMap.containsKey(key);
&#x20; }
&#x20; // 清空
&#x20; public void clear() {
&#x20; cacheMap.clear();
&#x20; }
&#x20; // 大小
&#x20; public int size() {
&#x20; return cacheMap.size();
&#x20; }
&#x20; // 打印缓存内容
&#x20; @Override
&#x20; public String toString() {
&#x20; return "Cache{" + "data=" + cacheMap + '}';
&#x20; }
&#x20; // 测试
&#x20; public static void main(String\[] args) {
&#x20; Cache<String, Integer> studentScoreCache = new Cache<>();
&#x20; studentScoreCache.put("张三", 90);
&#x20; studentScoreCache.put("李四", 85);
&#x20; studentScoreCache.put("王五", 95);
&#x20; System.out.println(studentScoreCache);
&#x20; System.out.println("张三的分数:" + studentScoreCache.get("张三"));
&#x20; System.out.println("缓存大小:" + studentScoreCache.size());
&#x20; studentScoreCache.remove("李四");
&#x20; System.out.println("删除李四后:" + studentScoreCache);
&#x20; studentScoreCache.clear();
&#x20; System.out.println("清空后缓存大小:" + studentScoreCache.size());
&#x20; }
}
Loading…
Cancel
Save