4:集合、泛型与Stream流
提交题目时 提交Q1至Q6的答案 附上你的思考与心得可以加分哦~
Task1. 集合与遍历
Section titled “Task1. 集合与遍历”你知道什么是集合吗?
它和数组有什么相同点和不同点呢?
既然有了数组,为什么还需要集合呢?
下面是一张集合的体系结构
聪明的你一定发现了,紫色方框是接口,而黄色方框是其实现类
Q1. 请你简单了解一下上面的集合接口以及实现类,简单说一说他们各自的功能,并概括一下数组与集合的区别
太好了,你现在已经了解了常用的集合! 那么如何去遍历一个集合呢? 下面以List为例,来详细阐述一下集合是如何遍历的
- 增强for循环( 又称for-each循环 )
我们先从创建一个List对象开始吧! 但由于List是一个接口,通常我们会创建它的实现类ArrayList作为它的对象实例,并添加4个元素
List<Integer> list = new ArrayList<>();list.add(1);list.add(2);list.add(3);list.add(4);你可能会有小小的疑问:欸? 这里的Integer 是什么? 这便是泛型了。简单来说,这里的 Integer 就是用来规定 list 当中的元素类型的,但是泛型的强大之处远不止如此,这只是它的冰山一角。不过没关系,在后面的题目中我们再来详细讨论它
增强for循环是一种语法糖,自行了解一下,然后完成第二题吧!
Q2. 请你用增强for循环遍历list中的元素 依次打印出结果 给出你的代码和运行结果截图
- forEach方法
List已经为我们封装好了一些方法 常用的遍历方法便是forEach了
list.forEach(new Consumer<Integer>() { @Override public void accept(Integer integer) { System.out.println(integer); }});你能看懂这段代码吗? 去了解一下匿名内部类吧! 内部类实际上有很多,但其中最常用的就是匿名内部类了
通常,当一个方法的参数是一个类时,我们会传入这个类的对象实例,而当方法的参数是一个抽象类或接口时,我们应该传入实现了这个类或接口的类的对象实例! 这个题便是一个很好的例子:其中的Consumer实际上是一个函数式接口,accept是它内部的抽象方法
然而!这并不是最简洁的写法。实际上,在 jdk8 之后 Java 引入了lambda表达式,它可以在很大程度上简化匿名内部类的写法,让代码变得更加简洁。了解一下lambda表达式,然后完成第三题吧!
Q3. 什么是匿名内部类,什么是函数式接口? ( 感兴趣的同学可以去了解一下四大函数式接口,体会一下OOP和FP的区别 ) 把上面的遍历代码用lambda表达式改写,给出你的代码和运行结果截图,并总结一下lambda表达式的用法
- 迭代器遍历
这是集合底层最本质的遍历方法,其他的两种遍历方法都是依赖于它
我们知道,List实现了Collection接口,而 Collection 接口又继承了Iterable接口
public interface Iterable<T> { Iterator<T> iterator();
default void forEach(Consumer<? super T> action) { Objects.requireNonNull(action); for (T t : this) { action.accept(t); } }
default Spliterator<T> spliterator() { return Spliterators.spliteratorUnknownSize(iterator(), 0); }}在Iterable接口中,给出了 forEach 方法的默认实现。看到这里,你应该就能理解 List 中的 forEach 方法是怎么来的了。在 forEach 方法的内部,实际上是增强for的语法糖的写法,可以还原成迭代器版本。作为开发人员,通常并不需要在乎他们底层封装方法的具体实现, 理解过后直接调用方法即可,这里不做重点要求。
Task2. 泛型
Section titled “Task2. 泛型”请你简单了解一下什么是泛型,它是JDK5中引入的特性,它提供了编译时类型安全检测机制,把运行时期的问题提前到了编译期间
学习时可以参考该链接:菜鸟教程-泛型
泛型常用在三处地方
- 泛型类
- 泛型方法
- 泛型接口
Q4. 请你完成下面的模拟项目
- 创建一个泛型接口,该接口是一个仓库,要求能够储存任何类型的数据 ( 使用泛型 )
public interface Repository- 这个接口内有两个方法 save( ) 方法用于向仓库中添加数据 getById( ) 方法用来根据id获取数据
- 创建MyRepository类,实现上面的接口,完成上述两个方法的具体实现 ( 注意:id是整数类型,从0开始自增,是数据的唯一标识 )
- 提示: 可以使用HashMap
public class MyRepository给出下述User类
public class User { private String name; private int age;
public User(String name, int age) { this.name = name; this.age = age; }
@Override public String toString() { return "User{name='" + name + "', age=" + age + "}"; }}要求:
- 分别在你的 MyRepository 储存String User Integer类型的三组数据,并调用你写的遍历方法,在main函数中打印出仓库中所有元素的内容
- 给出你的完整代码,以及输出截图
Task3. Stream流
Section titled “Task3. Stream流”Stream流可以帮我们更便捷的筛选和获取数据,体现了函数式的编程风格,代码更加简洁
Stream流的方法大致分为下面三类:
-
获取Stream流
我们可以通过Collection的静态方法:从集合创建流,也可以使用Arrays工具类的stream方法:从数组创建流等等等等
-
中间方法
创建了流之后,我们可以通过中间方法对元素进行转换,过滤等操作。有趣的是,这些方法的返回值仍然为流,就像一条流水线一样—— 我们可以不断使用中间方法,返回新的流,经过层层筛选,最后只在流上保留你需要的元素。filter map sorted等都是常见的中间方法
-
终结方法
常见的终结方法有forEach遍历 count计数 collect收集 等等
Q5. 完成下述题目
- 定义一个学生类
public class Student { String name; int score;
public Student(String name, int score) { this.name = name; this.score = score; }
public String getName() { return name; }
public int getScore() { return score; }
public void setName(String name) { this.name = name; }
public void setScore(int score) { this.score = score; }}- 初始代码
public class Main {
public static void main(String[] args) { // 测试数据:学生列表 List<Student> students = Arrays.asList( new Student("Alice", 85), new Student("Bob", 58), new Student("Charlie", 90), new Student("David", 45), new Student("Eve", 72), new Student("Frank", 60), new Student("Grace", 55), new Student("Heidi", 95) );
// 请在这里补充代码,完成以下任务: // 1. 过滤分数≥60的学生 // 2. 姓名转换成大写 // 3. 按姓名字母顺序排序 // 4. 收集成 List<String> 返回并打印
// --- 你的代码开始 ---
List<String> passingStudents = students.stream() // TODO: 补充流操作链
// --- 你的代码结束 ---
// 打印结果 System.out.println(passingStudents); }}- 给出你的主方法(main method)代码以及输出截图
Task4. 面向对象设计模式
Section titled “Task4. 面向对象设计模式”设计模式是软件工程中针对特定问题的可复用解决方案,它并非具体的代码实现,而是一种经过验证的设计思想。合理运用设计模式可以提高代码的可维护性、可扩展性和复用性。
本次任务涉及三种经典的设计模式:
-
单例模式(Singleton Pattern)
确保一个类在全局范围内只有一个实例,并提供一个全局访问点。常用于配置管理、日志记录、数据库连接池等场景。实现方式包括饿汉式、懒汉式、双重检查锁等。
-
工厂模式(Factory Pattern)
将对象的创建过程封装起来,客户端无需关心具体类的实例化细节。通过工厂方法,可以根据参数或配置动态创建不同类型的对象,从而降低耦合。常见的有简单工厂、工厂方法和抽象工厂。
-
建造者模式(Builder Pattern)
将一个复杂对象的构建过程与它的表示分离,使得同样的构建过程可以创建不同的表示。适用于对象有大量可选参数或构造过程较为复杂的情况(如配置对象、DTO等)。
💡 新手建议: 上述三种模式中,单例模式和工厂模式在实际开发中使用频率更高,概念也相对简单,建议优先掌握;建造者模式语法稍复杂,可以后续再深入学习。
学习时可以参考该链接:菜鸟教程-设计模式
Q6. 完成下述题目
假设你正在开发一款二次元抽卡游戏,游戏中有多种角色类型,每种角色都有不同的技能和定位。你需要设计一个角色创建系统,根据玩家选择的角色类型来创建对应的角色对象,未来还可能扩展新的角色类型。
游戏设定:
-
Saber(剑士):近战物理输出,技能为”Excalibur”
-
Archer(弓兵):远程物理输出,技能为”Unlimited Blade Works”
-
Caster(法师):远程魔法输出,技能为”Rho Aias”
要求:
定义一个 Character 接口,包含 attack() 方法
attack():打印角色使用技能进行攻击
分别实现 Saber、Archer 和 Caster 三个类
创建一个 CharacterFactory 工厂类,根据传入的角色类型返回对应的角色对象
在 Main 中完成测试
初始代码框架:
// 枚举:角色类型public enum CharacterType { SABER, ARCHER, CASTER}
// 角色接口public interface Character { // TODO: 定义 attack 方法}
// 剑士实现public class Saber implements Character { private String name; private String skill;
public Saber() { this.name = "阿尔托莉雅"; this.skill = "Excalibur"; }
// TODO: 实现 attack 方法,打印格式为:"[剑士] 阿尔托莉雅 使用 Excalibur 发动攻击!"}
// 弓兵实现public class Archer implements Character { private String name; private String skill;
public Archer() { this.name = "卫宫"; this.skill = "Unlimited Blade Works"; }
// TODO: 实现 attack 方法 // attack 格式:"[弓兵] 卫宫 使用 Unlimited Blade Works 发动攻击!"}
// 法师实现public class Caster implements Character { private String name; private String skill;
public Caster() { this.name = "美狄亚"; this.skill = "Rho Aias"; }
// TODO: 实现 attack 方法 // attack 格式:"[法师] 美狄亚 使用 Rho Aias 发动攻击!"}
// 角色工厂public class CharacterFactory { // TODO: 实现工厂方法,根据角色类型返回对应的角色对象 // 提示:如果传入未知类型,可以抛出 IllegalArgumentException}
// 测试类public class Main { public static void main(String[] args) { // TODO: // 1. 通过工厂分别创建 Saber、Archer、Caster 三个角色 // 2. 让每个角色发动攻击(调用 attack) // 期待输出格式参考如下: // [剑士] 阿尔托莉雅 使用 Excalibur 发动攻击! // ... }}- 给出你的完整代码,以及输出截图
本题提交方式
Section titled “本题提交方式”出题人联系方式
Section titled “出题人联系方式”出题人:
達
QQ:3228523940
