跳转到内容

4:集合、泛型与Stream流

后端Banner

提交题目时 提交Q1至Q6的答案 附上你的思考与心得可以加分哦~

你知道什么是集合吗?

它和数组有什么相同点和不同点呢?

既然有了数组,为什么还需要集合呢?

下面是一张集合的体系结构

java4_set.png

聪明的你一定发现了,紫色方框是接口,而黄色方框是其实现类

Q1. 请你简单了解一下上面的集合接口以及实现类,简单说一说他们各自的功能,并概括一下数组与集合的区别


太好了,你现在已经了解了常用的集合! 那么如何去遍历一个集合呢? 下面以List为例,来详细阐述一下集合是如何遍历的

  1. 增强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中的元素 依次打印出结果 给出你的代码和运行结果截图


  1. 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表达式的用法


  1. 迭代器遍历

这是集合底层最本质的遍历方法,其他的两种遍历方法都是依赖于它

我们知道,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的语法糖的写法,可以还原成迭代器版本。作为开发人员,通常并不需要在乎他们底层封装方法的具体实现, 理解过后直接调用方法即可,这里不做重点要求。


请你简单了解一下什么是泛型,它是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函数中打印出仓库中所有元素的内容
  • 给出你的完整代码,以及输出截图

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)代码以及输出截图

设计模式是软件工程中针对特定问题的可复用解决方案,它并非具体的代码实现,而是一种经过验证的设计思想。合理运用设计模式可以提高代码的可维护性、可扩展性和复用性。

本次任务涉及三种经典的设计模式:

  • 单例模式(Singleton Pattern)

    确保一个类在全局范围内只有一个实例,并提供一个全局访问点。常用于配置管理、日志记录、数据库连接池等场景。实现方式包括饿汉式、懒汉式、双重检查锁等。

  • 工厂模式(Factory Pattern)

    将对象的创建过程封装起来,客户端无需关心具体类的实例化细节。通过工厂方法,可以根据参数或配置动态创建不同类型的对象,从而降低耦合。常见的有简单工厂、工厂方法和抽象工厂。

  • 建造者模式(Builder Pattern)

    将一个复杂对象的构建过程与它的表示分离,使得同样的构建过程可以创建不同的表示。适用于对象有大量可选参数或构造过程较为复杂的情况(如配置对象、DTO等)。

💡 新手建议: 上述三种模式中,单例模式工厂模式在实际开发中使用频率更高,概念也相对简单,建议优先掌握;建造者模式语法稍复杂,可以后续再深入学习。

学习时可以参考该链接:菜鸟教程-设计模式


Q6. 完成下述题目

假设你正在开发一款二次元抽卡游戏,游戏中有多种角色类型,每种角色都有不同的技能和定位。你需要设计一个角色创建系统,根据玩家选择的角色类型来创建对应的角色对象,未来还可能扩展新的角色类型。

游戏设定:

  • Saber(剑士):近战物理输出,技能为”Excalibur”

  • Archer(弓兵):远程物理输出,技能为”Unlimited Blade Works”

  • Caster(法师):远程魔法输出,技能为”Rho Aias”

要求:

定义一个 Character 接口,包含 attack() 方法

attack():打印角色使用技能进行攻击

分别实现 SaberArcherCaster 三个类

创建一个 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 发动攻击!
// ...
}
}
  • 给出你的完整代码,以及输出截图

提交点这里

出题人:出题人头像  達

QQ:3228523940

邮箱:3228523940@qq.com