是否有可能在 Stream 中获取下一个元素?
Is it possible to get next element in the Stream?
我正在尝试将 for loop
转换为功能代码。我需要向前看一个值,也需要向后看一个值。是否可以使用流?
下面的代码是将罗马文本转换为数值。
不确定带有 two/three 参数的 reduce 方法是否可以提供帮助。
int previousCharValue = 0;
int total = 0;
for (int i = 0; i < input.length(); i++) {
char current = input.charAt(i);
RomanNumeral romanNum = RomanNumeral.valueOf(Character.toString(current));
if (previousCharValue > 0) {
total += (romanNum.getNumericValue() - previousCharValue);
previousCharValue = 0;
} else {
if (i < input.length() - 1) {
char next = input.charAt(i + 1);
RomanNumeral nextNum = RomanNumeral.valueOf(Character.toString(next));
if (romanNum.getNumericValue() < nextNum.getNumericValue()) {
previousCharValue = romanNum.getNumericValue();
}
}
if (previousCharValue == 0) {
total += romanNum.getNumericValue();
}
}
}
我还没有看到流的这种用例,所以我不能说它是否可能。但是当我需要使用带索引的流时,我选择 IntStream#range(0, table.length)
,然后在 lambdas 中我从这个 table/list 中获取值。
例如
int[] arr = {1,2,3,4};
int result = IntStream.range(0, arr.length)
.map(idx->idx>0 ? arr[idx] + arr[idx-1]:arr[idx])
.sum();
不,使用流是不可能的,至少不容易。流 API 从处理元素的顺序中抽象出来:流可以并行处理,也可以按相反的顺序处理。所以 "the next element" 和 "previous element" 不存在于流抽象中。
您应该使用最适合该工作的 API:如果您需要对集合的 所有 元素应用一些操作而您不是对订单感兴趣。如果您需要按特定顺序处理元素,则必须使用迭代器或者通过索引访问列表元素。
根据流的性质,您不知道 next 元素,除非您阅读它。因此在处理当前元素时,直接获取next元素是不可能的。然而,由于您正在阅读 current 元素,您显然知道之前阅读了什么,因此要实现 "accesing previous element" 和 "accessing next element" 等目标,您可以依赖历史已处理的元素。
您的问题可能有以下两种解决方案:
- 获取对以前阅读的元素的访问权限。这样你就知道 current 元素和定义的先前读取元素的数量
- 假设在流处理时您读取了 next 元素并且在上一次迭代中读取了 current 元素。换句话说,您将先前读取的元素视为 "current",将当前处理的元素视为 next(见下文)。
解决方案 1 - 实施
首先我们需要一个数据结构,它可以跟踪流经流的数据。好的选择可能是 Queue 的一个实例,因为队列本质上允许数据流过它们。我们只需要将队列绑定到我们想知道的最后一个元素的数量(对于您的用例,这将是 3 个元素)。为此,我们创建了一个 "bounded" 队列来保存历史,如下所示:
public class StreamHistory<T> {
private final int numberOfElementsToRemember;
private LinkedList<T> queue = new LinkedList<T>(); // queue will store at most numberOfElementsToRemember
public StreamHistory(int numberOfElementsToRemember) {
this.numberOfElementsToRemember = numberOfElementsToRemember;
}
public StreamHistory save(T curElem) {
if (queue.size() == numberOfElementsToRemember) {
queue.pollLast(); // remove last to keep only requested number of elements
}
queue.offerFirst(curElem);
return this;
}
public LinkedList<T> getLastElements() {
return queue; // or return immutable copy or immutable view on the queue. Depends on what you want.
}
}
通用参数 T 是流的实际元素的类型。方法 save returns 对当前 StreamHistory 实例的引用,以便更好地与 java Stream api 集成(见下文),这并不是真正需要的。
现在唯一要做的就是将元素流转换为 StreamHistory 的实例流(其中流的每个下一个元素将包含实际的最后 n 个实例通过流的对象)。
public class StreamHistoryTest {
public static void main(String[] args) {
Stream<Character> charactersStream = IntStream.range(97, 123).mapToObj(code -> (char) code); // original stream
StreamHistory<Character> streamHistory = new StreamHistory<>(3); // instance of StreamHistory which will store last 3 elements
charactersStream.map(character -> streamHistory.save(character)).forEach(history -> {
history.getLastElements().forEach(System.out::print);
System.out.println();
});
}
}
在上面的示例中,我们首先创建一个包含字母表中所有字母的流。然后我们创建 StreamHistory 的实例,它将被推送到原始流上 map() 调用的每次迭代。通过调用 map(),我们转换为包含对 StreamHistory 实例的引用的流。
请注意,每次数据流经原始流时,对 streamHistory.save(character) 的调用都会更新 streamHistory 对象的内容以反映流的当前状态。
最后,在每次迭代中,我们打印最后 3 个保存的字符。此方法的输出如下:
a
ba
cba
dcb
edc
fed
gfe
hgf
ihg
jih
kji
lkj
mlk
nml
onm
pon
qpo
rqp
srq
tsr
uts
vut
wvu
xwv
yxw
zyx
解决方案 2 - 实施
虽然解决方案 1 在大多数情况下都可以完成工作并且相当容易遵循,但也有一些用例可以检查下一个元素和上一个元素,这真的很方便。在这种情况下,我们只对三个元素元组(上一个、当前、下一个)感兴趣,只有一个元素并不重要(对于简单的示例,请考虑以下谜语:"given a stream of numbers return a tupple of three subsequent numbers which gives the highest sum")。为了解决此类用例,我们可能希望 api 比 StreamHistory class.
更方便
对于这种情况,我们引入了 StreamHistory class 的新变体(我们称之为 StreamNeighbours)。 class 将允许直接检查 previous 和 next 元素。处理将在"T-1"时间完成(即:当前处理的原始元素被认为是下一个元素,之前处理的原始元素被认为是当前 元素)。这样,我们在某种意义上提前检查了一个元素。
修改后的class如下:
public class StreamNeighbours<T> {
private LinkedList<T> queue = new LinkedList(); // queue will store one element before current and one after
private boolean threeElementsRead; // at least three items were added - only if we have three items we can inspect "next" and "previous" element
/**
* Allows to handle situation when only one element was read, so technically this instance of StreamNeighbours is not
* yet ready to return next element
*/
public boolean isFirst() {
return queue.size() == 1;
}
/**
* Allows to read first element in case less than tree elements were read, so technically this instance of StreamNeighbours is
* not yet ready to return both next and previous element
* @return
*/
public T getFirst() {
if (isFirst()) {
return queue.getFirst();
} else if (isSecond()) {
return queue.get(1);
} else {
throw new IllegalStateException("Call to getFirst() only possible when one or two elements were added. Call to getCurrent() instead. To inspect the number of elements call to isFirst() or isSecond().");
}
}
/**
* Allows to handle situation when only two element were read, so technically this instance of StreamNeighbours is not
* yet ready to return next element (because we always need 3 elements to have previos and next element)
*/
public boolean isSecond() {
return queue.size() == 2;
}
public T getSecond() {
if (!isSecond()) {
throw new IllegalStateException("Call to getSecond() only possible when one two elements were added. Call to getFirst() or getCurrent() instead.");
}
return queue.getFirst();
}
/**
* Allows to check that this instance of StreamNeighbours is ready to return both next and previous element.
* @return
*/
public boolean areThreeElementsRead() {
return threeElementsRead;
}
public StreamNeighbours<T> addNext(T nextElem) {
if (queue.size() == 3) {
queue.pollLast(); // remove last to keep only three
}
queue.offerFirst(nextElem);
if (!areThreeElementsRead() && queue.size() == 3) {
threeElementsRead = true;
}
return this;
}
public T getCurrent() {
ensureReadyForReading();
return queue.get(1); // current element is always in the middle when three elements were read
}
public T getPrevious() {
if (!isFirst()) {
return queue.getLast();
} else {
throw new IllegalStateException("Unable to read previous element of first element. Call to isFirst() to know if it first element or not.");
}
}
public T getNext() {
ensureReadyForReading();
return queue.getFirst();
}
private void ensureReadyForReading() {
if (!areThreeElementsRead()) {
throw new IllegalStateException("Queue is not threeElementsRead for reading (less than two elements were added). Call to areThreeElementsRead() to know if it's ok to call to getCurrent()");
}
}
}
现在,假设已经读取了三个元素,我们可以直接访问current元素(即在时间T-1通过流的元素),我们可以访问next 元素(这是当前通过流的元素)和 previous(这是在时间 T 时通过流的元素- 2):
public class StreamTest {
public static void main(String[] args) {
Stream<Character> charactersStream = IntStream.range(97, 123).mapToObj(code -> (char) code);
StreamNeighbours<Character> streamNeighbours = new StreamNeighbours<Character>();
charactersStream.map(character -> streamNeighbours.addNext(character)).forEach(neighbours -> {
// NOTE: if you want to have access the values before instance of StreamNeighbours is ready to serve three elements
// you can use belows methods like isFirst() -> getFirst(), isSecond() -> getSecond()
//
// if (curNeighbours.isFirst()) {
// Character currentChar = curNeighbours.getFirst();
// System.out.println("???" + " " + currentChar + " " + "???");
// } else if (curNeighbours.isSecond()) {
// Character currentChar = curNeighbours.getSecond();
// System.out.println(String.valueOf(curNeighbours.getFirst()) + " " + currentChar + " " + "???");
//
// }
//
// OTHERWISE: you are only interested in tupples consisting of three elements, so three elements needed to be read
if (neighbours.areThreeElementsRead()) {
System.out.println(neighbours.getPrevious() + " " + neighbours.getCurrent() + " " + neighbours.getNext());
}
});
}
}
输出如下:
a b c
b c d
c d e
d e f
e f g
f g h
g h i
h i j
i j k
j k l
k l m
l m n
m n o
n o p
o p q
p q r
q r s
r s t
s t u
t u v
u v w
v w x
w x y
x y z
通过 StreamNeighbours class 更容易跟踪 previous/next 元素(因为我们有适当名称的方法),而在 StreamHistory class 中这更麻烦,因为我们需要手动"reverse"队列的顺序来实现。
我正在尝试将 for loop
转换为功能代码。我需要向前看一个值,也需要向后看一个值。是否可以使用流?
下面的代码是将罗马文本转换为数值。
不确定带有 two/three 参数的 reduce 方法是否可以提供帮助。
int previousCharValue = 0;
int total = 0;
for (int i = 0; i < input.length(); i++) {
char current = input.charAt(i);
RomanNumeral romanNum = RomanNumeral.valueOf(Character.toString(current));
if (previousCharValue > 0) {
total += (romanNum.getNumericValue() - previousCharValue);
previousCharValue = 0;
} else {
if (i < input.length() - 1) {
char next = input.charAt(i + 1);
RomanNumeral nextNum = RomanNumeral.valueOf(Character.toString(next));
if (romanNum.getNumericValue() < nextNum.getNumericValue()) {
previousCharValue = romanNum.getNumericValue();
}
}
if (previousCharValue == 0) {
total += romanNum.getNumericValue();
}
}
}
我还没有看到流的这种用例,所以我不能说它是否可能。但是当我需要使用带索引的流时,我选择 IntStream#range(0, table.length)
,然后在 lambdas 中我从这个 table/list 中获取值。
例如
int[] arr = {1,2,3,4};
int result = IntStream.range(0, arr.length)
.map(idx->idx>0 ? arr[idx] + arr[idx-1]:arr[idx])
.sum();
不,使用流是不可能的,至少不容易。流 API 从处理元素的顺序中抽象出来:流可以并行处理,也可以按相反的顺序处理。所以 "the next element" 和 "previous element" 不存在于流抽象中。
您应该使用最适合该工作的 API:如果您需要对集合的 所有 元素应用一些操作而您不是对订单感兴趣。如果您需要按特定顺序处理元素,则必须使用迭代器或者通过索引访问列表元素。
根据流的性质,您不知道 next 元素,除非您阅读它。因此在处理当前元素时,直接获取next元素是不可能的。然而,由于您正在阅读 current 元素,您显然知道之前阅读了什么,因此要实现 "accesing previous element" 和 "accessing next element" 等目标,您可以依赖历史已处理的元素。
您的问题可能有以下两种解决方案:
- 获取对以前阅读的元素的访问权限。这样你就知道 current 元素和定义的先前读取元素的数量
- 假设在流处理时您读取了 next 元素并且在上一次迭代中读取了 current 元素。换句话说,您将先前读取的元素视为 "current",将当前处理的元素视为 next(见下文)。
解决方案 1 - 实施
首先我们需要一个数据结构,它可以跟踪流经流的数据。好的选择可能是 Queue 的一个实例,因为队列本质上允许数据流过它们。我们只需要将队列绑定到我们想知道的最后一个元素的数量(对于您的用例,这将是 3 个元素)。为此,我们创建了一个 "bounded" 队列来保存历史,如下所示:
public class StreamHistory<T> {
private final int numberOfElementsToRemember;
private LinkedList<T> queue = new LinkedList<T>(); // queue will store at most numberOfElementsToRemember
public StreamHistory(int numberOfElementsToRemember) {
this.numberOfElementsToRemember = numberOfElementsToRemember;
}
public StreamHistory save(T curElem) {
if (queue.size() == numberOfElementsToRemember) {
queue.pollLast(); // remove last to keep only requested number of elements
}
queue.offerFirst(curElem);
return this;
}
public LinkedList<T> getLastElements() {
return queue; // or return immutable copy or immutable view on the queue. Depends on what you want.
}
}
通用参数 T 是流的实际元素的类型。方法 save returns 对当前 StreamHistory 实例的引用,以便更好地与 java Stream api 集成(见下文),这并不是真正需要的。
现在唯一要做的就是将元素流转换为 StreamHistory 的实例流(其中流的每个下一个元素将包含实际的最后 n 个实例通过流的对象)。
public class StreamHistoryTest {
public static void main(String[] args) {
Stream<Character> charactersStream = IntStream.range(97, 123).mapToObj(code -> (char) code); // original stream
StreamHistory<Character> streamHistory = new StreamHistory<>(3); // instance of StreamHistory which will store last 3 elements
charactersStream.map(character -> streamHistory.save(character)).forEach(history -> {
history.getLastElements().forEach(System.out::print);
System.out.println();
});
}
}
在上面的示例中,我们首先创建一个包含字母表中所有字母的流。然后我们创建 StreamHistory 的实例,它将被推送到原始流上 map() 调用的每次迭代。通过调用 map(),我们转换为包含对 StreamHistory 实例的引用的流。
请注意,每次数据流经原始流时,对 streamHistory.save(character) 的调用都会更新 streamHistory 对象的内容以反映流的当前状态。
最后,在每次迭代中,我们打印最后 3 个保存的字符。此方法的输出如下:
a
ba
cba
dcb
edc
fed
gfe
hgf
ihg
jih
kji
lkj
mlk
nml
onm
pon
qpo
rqp
srq
tsr
uts
vut
wvu
xwv
yxw
zyx
解决方案 2 - 实施
虽然解决方案 1 在大多数情况下都可以完成工作并且相当容易遵循,但也有一些用例可以检查下一个元素和上一个元素,这真的很方便。在这种情况下,我们只对三个元素元组(上一个、当前、下一个)感兴趣,只有一个元素并不重要(对于简单的示例,请考虑以下谜语:"given a stream of numbers return a tupple of three subsequent numbers which gives the highest sum")。为了解决此类用例,我们可能希望 api 比 StreamHistory class.
更方便对于这种情况,我们引入了 StreamHistory class 的新变体(我们称之为 StreamNeighbours)。 class 将允许直接检查 previous 和 next 元素。处理将在"T-1"时间完成(即:当前处理的原始元素被认为是下一个元素,之前处理的原始元素被认为是当前 元素)。这样,我们在某种意义上提前检查了一个元素。
修改后的class如下:
public class StreamNeighbours<T> {
private LinkedList<T> queue = new LinkedList(); // queue will store one element before current and one after
private boolean threeElementsRead; // at least three items were added - only if we have three items we can inspect "next" and "previous" element
/**
* Allows to handle situation when only one element was read, so technically this instance of StreamNeighbours is not
* yet ready to return next element
*/
public boolean isFirst() {
return queue.size() == 1;
}
/**
* Allows to read first element in case less than tree elements were read, so technically this instance of StreamNeighbours is
* not yet ready to return both next and previous element
* @return
*/
public T getFirst() {
if (isFirst()) {
return queue.getFirst();
} else if (isSecond()) {
return queue.get(1);
} else {
throw new IllegalStateException("Call to getFirst() only possible when one or two elements were added. Call to getCurrent() instead. To inspect the number of elements call to isFirst() or isSecond().");
}
}
/**
* Allows to handle situation when only two element were read, so technically this instance of StreamNeighbours is not
* yet ready to return next element (because we always need 3 elements to have previos and next element)
*/
public boolean isSecond() {
return queue.size() == 2;
}
public T getSecond() {
if (!isSecond()) {
throw new IllegalStateException("Call to getSecond() only possible when one two elements were added. Call to getFirst() or getCurrent() instead.");
}
return queue.getFirst();
}
/**
* Allows to check that this instance of StreamNeighbours is ready to return both next and previous element.
* @return
*/
public boolean areThreeElementsRead() {
return threeElementsRead;
}
public StreamNeighbours<T> addNext(T nextElem) {
if (queue.size() == 3) {
queue.pollLast(); // remove last to keep only three
}
queue.offerFirst(nextElem);
if (!areThreeElementsRead() && queue.size() == 3) {
threeElementsRead = true;
}
return this;
}
public T getCurrent() {
ensureReadyForReading();
return queue.get(1); // current element is always in the middle when three elements were read
}
public T getPrevious() {
if (!isFirst()) {
return queue.getLast();
} else {
throw new IllegalStateException("Unable to read previous element of first element. Call to isFirst() to know if it first element or not.");
}
}
public T getNext() {
ensureReadyForReading();
return queue.getFirst();
}
private void ensureReadyForReading() {
if (!areThreeElementsRead()) {
throw new IllegalStateException("Queue is not threeElementsRead for reading (less than two elements were added). Call to areThreeElementsRead() to know if it's ok to call to getCurrent()");
}
}
}
现在,假设已经读取了三个元素,我们可以直接访问current元素(即在时间T-1通过流的元素),我们可以访问next 元素(这是当前通过流的元素)和 previous(这是在时间 T 时通过流的元素- 2):
public class StreamTest {
public static void main(String[] args) {
Stream<Character> charactersStream = IntStream.range(97, 123).mapToObj(code -> (char) code);
StreamNeighbours<Character> streamNeighbours = new StreamNeighbours<Character>();
charactersStream.map(character -> streamNeighbours.addNext(character)).forEach(neighbours -> {
// NOTE: if you want to have access the values before instance of StreamNeighbours is ready to serve three elements
// you can use belows methods like isFirst() -> getFirst(), isSecond() -> getSecond()
//
// if (curNeighbours.isFirst()) {
// Character currentChar = curNeighbours.getFirst();
// System.out.println("???" + " " + currentChar + " " + "???");
// } else if (curNeighbours.isSecond()) {
// Character currentChar = curNeighbours.getSecond();
// System.out.println(String.valueOf(curNeighbours.getFirst()) + " " + currentChar + " " + "???");
//
// }
//
// OTHERWISE: you are only interested in tupples consisting of three elements, so three elements needed to be read
if (neighbours.areThreeElementsRead()) {
System.out.println(neighbours.getPrevious() + " " + neighbours.getCurrent() + " " + neighbours.getNext());
}
});
}
}
输出如下:
a b c
b c d
c d e
d e f
e f g
f g h
g h i
h i j
i j k
j k l
k l m
l m n
m n o
n o p
o p q
p q r
q r s
r s t
s t u
t u v
u v w
v w x
w x y
x y z
通过 StreamNeighbours class 更容易跟踪 previous/next 元素(因为我们有适当名称的方法),而在 StreamHistory class 中这更麻烦,因为我们需要手动"reverse"队列的顺序来实现。