#面试 #Java
面试官:工作中用过 Stream 流吗?
大树:没有。
面试官:没有你也要给我介绍一下Java stream 流的概念和作用!
大树:好的老板,那我也可以用过一些,我介绍一下~ 😜
Java中的流(Stream)概念,可是Java 8中的明星特性呢!
Java流(Stream)是一种高级迭代器,它允许我们以 声明式的方式 处理数据集合。与传统的迭代器不同,流不存储数据,而是代表了一个计算的过程,这个过程可以对数据集合进行操作,比如过滤、排序、聚合等。流就像是一个管道,数据在这个管道中按照一定的规则流动,最终得到处理结果。
流的作用主要体现在以下几个方面:
面试官:你刚刚提到了个 惰性求值;那流的哪些操作是惰性求值,哪些是非惰性求值?他们的区别是什么?
大树:
在Java流(Stream)API中,惰性求值(Lazy Evaluation)和非惰性求值(Eager Evaluation)是两种不同的数据处理策略。下面我解释这两种策略,并指出哪些流操作是惰性求值,哪些是非惰性求值,以及它们之间的区别。
惰性求值意味着操作不会立即执行,而是等到真正需要结果的时候才会执行。在流中,大多数的中间操作(如filter
、map
、flatMap
、sorted
等)都是惰性求值的。这些操作只是定义了一个处理流程,并不立即执行,只有在最终操作(如collect
、forEach
、limit
等)被调用时,中间操作才会实际执行。
例子
Stream<String> stream = people.stream()
.filter(p -> p.age > 18)
.map(Person::getName)
.sorted();
在这个例子中,filter
、map
和sorted
都是惰性求值操作,它们不会立即执行,而是等待最终操作。
非惰性求值意味着操作会立即执行,并且通常会立即产生结果。在流API中,最终操作(如collect
、forEach
、findFirst
、findAny
、limit
、count
、min
、max
、reduce
等)通常是非惰性求值的。当这些操作被调用时,它们会触发前面定义的所有中间操作,并生成最终结果。
例子
List<String> names = people.stream()
.filter(p -> p.age > 18)
.map(Person::getName)
.sorted()
.collect(Collectors.toList());
在这个例子中,collect
是一个非惰性求值操作,它会立即执行,并触发前面的filter
、map
和sorted
操作,最终生成一个包含结果的列表。
面试官:那流通常用于什么场景下?最好写个代码举例子哦
大树:humm,那我就写下我日常会用到的吧。
Java流(Stream)API在多种场景下都非常有用武之地,它能够简化集合(Collection)的操作,提高代码的可读性和效率。下面是一些典型的使用场景:
filter
方法可以轻松实现。就像是在一群音乐家中挑选出只会演奏特定乐器的人,比如只选择会弹钢琴的音乐家。List<String> instruments = Arrays.asList("piano", "guitar", "flute", "cello", "trumpet");
List<String> stringInstruments = instruments.stream()
.filter(instrument -> instrument.startsWith("g"))
.collect(Collectors.toList());
// stringInstruments 将包含 "guitar"
// 这段代码过滤出了以 "g" 开头的乐器名称。就像是从乐器中挑选出吉他手一样。map
方法。这就像是将每个音乐家的乐谱翻译成另一种语言,修改音乐家名字为其他别名,以便其他音乐家理解。List<String> musicians
= Arrays.asList("Yo-Yo Ma", "Lang Lang", "Itzhak Perlman");
List<String> fullNames = musicians.stream()
.map(musician -> musician + " (Cellist)")
.collect(Collectors.toList());
// fullNames 将包含 "Yo-Yo Ma (Cellist)" 等
// 这里我们将音乐家的名字转换成带有乐器标识的全名。就像是给每个音乐家定制一张名片,上面写着他们的名字和擅长的乐器。collect
、reduce
等方法就派上用场了。List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
String concatenated = numbers.stream()
.collect(Collectors.joining(", ", "The sum is: ", "")));
// concatenated 将是 "The sum is: 1, 2, 3, 4, 5"sorted
方法,可以对数据集合进行排序。这就像是在音乐会开始前,根据音乐家们的座位安排,将他们按照一定的顺序排列在舞台上。List<String> cities = Arrays.asList("New York", "Los Angeles", "Chicago", "Houston");
cities.stream()
.sorted()
.forEach(System.out::println);
// 输出将会是按字母顺序排列的城市名collect
方法结合Collectors.groupingBy
可以轻松实现这一点。就好比将乐器按照类别分组,比如弦乐、木管乐、铜管乐等。Map<String, List<String>> musicGroups
= instruments.stream()
.collect(Collectors.groupingBy(instrument -> {
if ("piano".equals(instrument)) {
return "Keyboard";
} else if (instrument.startsWith("c")) {
return "Strings";
} else {
return "Others";
}
}));
// musicGroups 将乐器按类别分组min
、max
、average
等。这就像是在音乐会结束后,统计观众的掌声次数,或者计算平均分贝数。List<Integer> scores = Arrays.asList(90, 85, 95, 100, 80);
Optional<Integer> maxScore = scores.stream()
.max(Integer::compare);
// maxScore 将包含分数中的最大值,这里是 100面试官:多级排序怎么做?
大树:
List<Musician> musicians = Arrays.asList(
new Musician("Yo-Yo Ma", "Cello", 65),
new Musician("Lang Lang", "Piano", 38),
new Musician("Itzhak Perlman", "Violin", 76),
new Musician("Chick Corea", "Piano", 71),
new Musician("Hilary Hahn", "Violin", 45)
);
// 首先根据乐器类型排序,如果乐器类型相同,则根据年龄排序
List<Musician> sortedMusicians = musicians.stream()
// 一级排序:乐器类型
.sorted(Comparator.comparing(Musician::getInstrument)
// 二级排序:年龄
.thenComparing(Comparator.comparing(Musician::getAge))
)
.collect(Collectors.toList());
面试官:并行流适用于什么场景,使用时要注意些什么?
大树:并行流(Parallel Stream)是Java 8中引入的一种流操作,它利用多线程来并行处理数据,适用于需要快速处理大量数据的场景,特别是当计算非常密集时。但是,使用并行流时需要考虑到线程安全、数据处理顺序、性能调优、内存消耗、异常处理和调试难度等问题。
然而,并行流并不是万能的,使用时需要注意以下几点:
面试官:在并行流操作中如何处理并发异常?
大树:处理并行流中的并发异常需要采取一些预防措施和策略,因为并行流会在多个线程中执行操作,这增加了程序的复杂性和潜在的错误点。以下是一些处理并发异常的方法和建议:
ConcurrentHashMap
,以避免锁竞争和死锁。synchronized
块或java.util.concurrent
包中的工具类。ThreadLocal
),这样可以保证每个线程有自己的数据副本,避免了并发问题。try-catch
块来捕获并处理可能抛出的异常。由于并行流中的操作可能在不同的线程中执行,所以异常可能不会像在单个线程中那样直接抛出。可以考虑使用reduce
操作来收集所有的异常,然后统一处理。 `collect
的并行版本:如果你需要收集并行流的结果,可以使用Collectors
中的并行收集器,如Collectors.toConcurrentMap
,这样可以安全地将结果合并到一个共享集合中。sequential
方法将并行流转换为顺序流,以简化问题。Executors
和Future
,可以帮助你更好地管理和处理并发任务中的异常。处理并发异常没有通用的解决方案,通常需要根据具体的应用场景和需求来设计合适的策略。
面试官:Java流(Stream)API在多线程环境下如何保证线程安全?
大树:
Java流(Stream)API在多线程环境下提供了几种机制来帮助保证线程安全:
filter
、map
、reduce
等)是并行执行的。流API内部处理了线程创建、任务分配和结果合并等细节,而且通常会使用ForkJoinPool
这样的工作窃取(work-stealing)策略来高效地利用多核处理器。这种内部并行化设计的足够健壮,可以在多线程环境下安全地使用。filter
、map
等)通常是无状态的,即它们不依赖于外部的可变状态。这意味着每个操作都是独立的,不会改变共享状态,从而避免了并发问题。ConcurrentHashMap
、AtomicInteger
等。这些数据结构提供了同步机制,可以安全地在多线程环境下更新和访问。Collectors.toConcurrentMap
、Collectors.toConcurrentList
等。这些收集器能够安全地将并行流的结果合并到线程安全的目标容器中。synchronized
)或并发原子类(Atomic
系列)。sequential()
方法来切换到顺序流。顺序流会按照元素的插入顺序依次处理,避免了并发问题。ConcurrentModificationException
。在设计流操作时,应考虑异常处理策略,确保程序的健壮性。parallelStream.parallelism()
方法来获取或设置并行流的并行度。在某些情况下,限制并行度可以减少线程间的竞争,从而降低并发问题的可能性。面试官 :今天面试就先到这里吧,回去等通知吧
推荐下大树的公众号,欢迎大家扫码关注一起交流呀
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。