视频加载失败

Java 进阶 Day02:自定义泛型与集合入门

2064 字
10 分钟
Java 进阶 Day02:自定义泛型与集合入门

自定义泛型类#

用过现成的泛型集合之后,进一步是自己定义泛型类。

定义 MyArrayList<E>#

public class MyArrayList<E> { // <E> 在类名后 = 类级别的类型参数
private ArrayList list = new ArrayList(); // 内部是 raw type(见下方坑)
public boolean add(E e) { list.add(e); return true; } // E 出现在方法参数
public void remove(E e) { list.remove(e); }
}

使用:菱形运算符#

MyArrayList<String> list = new MyArrayList<>(); // JDK7 以后右边泛型可以不写

菱形运算符 <>:JDK7 起,new MyArrayList<>() 右边尖括号里的类型可省略,编译器从左边推断(String)。

坑:内部集合是 raw type#

MyArrayList 内部成员 list 是原始类型(raw type)ArrayList,没加泛型。所以泛型 E 只在 add(E) / remove(E) 的方法签名层做了编译期类型检查,内部存储其实没有类型约束——这是偷懒写法,真项目内部集合也要写成 ArrayList<E>。

泛型接口#

public interface Data<T> { // <T> 在接口名后
void add(T t);
void delete(T t);
void update(T t);
T query(int id); // T 出现在返回值
}

实现类可以把类型参数定死:

public class StudentData implements Data<Student> { // 实现时把 T 定死为 Student
@Override
public void add(Student student) { }
@Override
public Student query(int id) { return null; }
}
// TeacherData 同理:implements Data<Teacher>,所有方法签名换成 Teacher

老师、学生的数据都要增删改查——用泛型接口写一次,各实现类只关心自己的类型,不用为每种数据各定义一套接口。这是泛型接口最常见的用法:一套接口、多套数据类型。

泛型方法#

类型参数定义在方法上:

public static <T> void printArray(T[] names) { } // <T> 在修饰符后、返回类型前
public static <T> T getMax(T[] names) { // 返回类型也是 T
return names[0];
}
  • public static <T> T getMax(...):第一个 <T> 是声明类型参数,第二个 T 是返回类型
  • 静态方法也可以有泛型参数(类上的 <E> 在静态方法里不能用,因为静态不依赖对象,只能自己声明 <T>)

通配符与上限边界#

核心结论:类有继承关系,但泛型之间不协变。

ArrayList<Xiaomi> xiaomis = new ArrayList<>();
go(xiaomis); // 传得进 go(ArrayList<? extends Car>)
ArrayList<BYD> byds = new ArrayList<>();
go(byds);
public static void go(ArrayList<? extends Car> cars) { } // 上限通配符
  • BYD / LX / Xiaomi 都是 Car 的子类(extends Car)
  • 但泛型类型之间没有继承/协变关系:ArrayList<Xiaomi> 和 ArrayList<Car> 是两个不相干的类型
  • 所以参数不能写 ArrayList<Car>(那样传不进 ArrayList<Xiaomi>),只能用 ? extends Car 表示**「Car 及其任意子类的泛型」**
  • ? = 不知道也不关心具体类型,只关心边界(这里是上界 Car)
  • 约定俗成:用 ? extends 只能读不能写

包装类 Integer#

泛型集合只能装引用类型,所以必须有包装类(int 对应 Integer)。三个考点:

valueOf 缓存(享元模式)#

Integer it1 = Integer.valueOf(100);
Integer it2 = Integer.valueOf(100);
System.out.println(it1 == it2); // true

Integer.valueOf 对 -128 ~ 127 范围有缓存(返回同一个对象),== 为 true;超出范围则每次 new 新对象,== 变 false——经典面试坑。

自动装箱 / 拆箱#

Integer it3 = 100; // 自动装箱 int → Integer
int i = it3; // 自动拆箱 Integer → int
list1.add(100); // 自动装箱(集合装不下 int)
int rs = list1.get(0); // 自动拆箱

字符串互转#

String rs1 = Integer.toString(j); // 23 → "23"
System.out.println(rs1 + 1); // "231" —— 字符串拼接,不是数值加法!
int rs2 = Integer.parseInt(str); // "98" → 98

集合 Collection#

两大体系#

体系实现类特点
List 家族ArrayList有序、可重复、有索引(list.get(0))
Set 家族HashSet无序、不重复(重复 add("java") 后打印只有一个)
ArrayList<String> list = new ArrayList<>();
list.add("hello"); ...
System.out.println(list.get(0)); // List 有索引,可直接 get
Set<String> set = new HashSet<>() {}; // 这里多写了空的匿名内部类,直接 new HashSet<>() 即可
set.add("java"); set.add("java"); // 重复 add
System.out.println(set); // java 只输出一次,证明 Set 去重

Collection 接口常用方法#

面向接口编程:用父接口 Collection 引用 ArrayList(多态)。

Collection<String> list = new ArrayList<>();
list.add("hello"); list.size(); // 增 / 个数
list.remove("world"); // 按元素删除
list.isEmpty(); // 是否为空
list.clear(); // 清空
list.contains("hello"); // 是否包含(clear 后 false)
Object[] array = list.toArray(); // 转 Object 数组
String[] arr2 = list.toArray(String[]::new);// 转指定类型数组(方法引用)

三种遍历方式#

方式代码特点
迭代器Iterator<String> it = list.iterator(); while (it.hasNext()) { it.next(); }最底层;可安全删除
增强 forfor (String s : list) { ... }语法糖,底层仍是迭代器
lambdalist.forEach(System.out::println);最简洁;删除受限

迭代器没有元素后再调 next() 会抛 NoSuchElementException。

并发修改异常与安全删除#

问题:正序遍历时删除元素会「边删边漏」——删掉后集合索引前移补位,跳过下一个元素(删「枸杞子」后,「宁夏枸杞」被跳过)。

// 错误:正序 for 边删边漏
for (int i = 0; i < list.size(); i++) { ... list.remove(s); }
// 方案:remove 后手动 i-- 让索引回退
// 方案:倒着遍历删除(前提是支持索引,只适用 List 家族)
// 正确方案:迭代器自带 remove
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String s = iterator.next();
if (s.contains("枸杞")) {
iterator.remove(); // 迭代器的 remove 安全
}
}
System.out.println(list); // 所有带"枸杞"的元素被删干净

foreach 和 lambda 都解决不了并发修改异常问题,只有 iterator.remove() 可以。

重点速记#

  1. 泛型三种写法位置:类上 <E>(MyArrayList<E>)、接口上 <T>(Data<T>)、方法上 <T>(public static <T> T getMax)
  2. 菱形运算符 <>:JDK7 起右边类型参数可省略(new MyArrayList<>())
  3. 泛型接口 + 实现类定死类型:implements Data<Student>,一套接口多套数据
  4. 泛型不协变:ArrayList<Car> 和 ArrayList<Xiaomi> 没关系;接收子类泛型用上限通配符 ? extends Car
  5. 泛型 vs 通配符:泛型方法 <T> = 「传入即推断类型」;? = 「不知道也不关心具体类型、只关心边界」
  6. Integer 缓存:valueOf 对 -128~127 有缓存,范围内 == true、范围外 false
  7. 自动装箱/拆箱:Integer it = 100 装箱、int i = it 拆箱;集合只能装引用类型
  8. Integer.toString(j) + 1 是字符串拼接(“23”+“1”=“231”),不是加法
  9. List vs Set:有序可重复有索引 vs 无序不重复
  10. Collection 接口:父接口引用实现类,add/size/remove/isEmpty/clear/contains/toArray
  11. 遍历三方式:迭代器(可安全删)/ 增强 for / lambda forEach
  12. 遍历删除只用 iterator.remove():正序 for 漏删(索引前移)、foreach/lambda 抛并发修改异常

实战:面向接口改造与泛型工具方法#

在控制台文章管理程序里,把具体集合类型换成接口与泛型工具,分两步:

步骤做法用到的知识点
A全链路 ArrayList<Article> 改为 List<Article>(接口、实现、Service、Printer 各一处声明)面向接口 / Collection 多态
B新建工具类 ListUtil,提供泛型方法 filter(List<T>, Predicate<T>);原先手写 for 循环的 search/findByStatus 改为复用泛型方法 <T> + Lambda/Predicate

为什么声明用 List 而不是 ArrayList#

存储实现(ArrayList/LinkedList/将来换数据库)是可变细节,业务层只关心「这是有序集合」。声明父接口 List 之后实现类怎么换,Service/Printer 一行都不用改——和 Repository 面向接口是同一个道理。

过滤为什么用 Predicate<T>#

函数式接口可以接收 Lambda;过滤本身是通用模式(建新列表 → 遍历 → 判断 → 收集,固定四步),收敛到一个泛型方法里即可处处复用。工具类还可以加私有构造器防止实例化。

泛型推断与泛型擦除的区别#

容易混淆的点:泛型方法 filter(List<Article>, ...) 里之所以能对元素调用 getTitle(),靠的是调用处的类型推断——传入 List<Article>,编译器推断 T = Article;而不是「擦除成了 Article」。擦除发生在定义处,把 T 替换成上界(默认 Object),所以在 filter 方法体内部直接写 t.getTitle() 会编译报错。一句话:推断在调用处,擦除在定义处。

Scanner 输入错位的坑#

readInt() 若实现为 nextInt() 接 nextLine(),会吃掉行尾换行符。当发布文章先连续读三个 readLine() 再统一校验时,测试输入一旦错位,非数字内容被 nextInt() 读到会抛 InputMismatchException(message 为 null),而且坏的 token 没被消费,下一轮循环再次失败——形成死循环。教训:不是所有异常都适合无脑兜底,输入解析出错要先恢复输入流状态。

改造后残留的无用 import java.util.ArrayList; 记得随手清理。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或打赏支持!

打赏
Java 进阶 Day02:自定义泛型与集合入门
https://ryuki.icu/posts/java-advanced-day02/
作者
RK
发布于
2026-08-06
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
RK
Long may the sunshine
公告
Long may the sunshine
分类
标签
最新动态
站点天气
天气加载中…
站点统计
文章
27
分类
6
标签
59
总字数
39,139
篝火已燃
0 天
最后活动
0 天前
每日一签