Reputation: 155
The aim is to use a Stream to iterate over an array, filtering/extending values as required and collecting the result in a new Stream.
Trying to use Stream.builder(), as in the following three examples, I'll always get a Stream with the expected Strings, but lots of trailing nulls. In addition, I can't process null elements this way.
I suspect, the internal fixed buffer in Stream.builder()
is the problem.
Is there a way, to prevent 'trailing' nulls with this approach, without loosing the ability to use null values as Stream elements?
String[] whitespaces = new String[] { " ", "\n", "\r", "\t" };
int len = whitespaces.length;
boolean addNulls = false;
int flexBoundary = addNulls ? len : len - 1;
Stream<String> whitespaceNullStringStream = IntStream.rangeClosed(0, flexBoundary)
.mapToObj(idx ->
addNulls && idx == flexBoundary
? null
: whitespaces[idx])
// #1
.collect(Stream::<String>builder, Builder::add, (b1, b2) -> Stream.concat(b1.build(), b2.build())).build();
// #2
// .collect(Stream::<String>builder, Builder::add, (b1, b2) -> Stream.builder().add(b1).add(b2)).build();
// #3
// .collect(
// Collector.of(
// Stream::<String>builder,
// Builder::add,
// (b1, b2) -> b1.add(b2.build().reduce(String::concat).get()),
// Builder::build
// )
// );
If I instead use the following, it'll work as expected, except null
values are converted to Strings, of course, which is not desirable here:
.collect(
Collector.of(
StringBuilder::new,
StringBuilder::append,
StringBuilder::append,
(sb) -> Stream.of(sb.toString())
)
)
To overcome this, I've used the following approach:
Stream<String> stream = IntStream.rangeClosed(0, flexBoundary)
.mapToObj(idx -> addNulls && idx == flexBoundary ? null : whitespaces[idx])
.collect(Collector.of(
ArrayList<String>::new,
List::add,
(l1, l2) -> { l1.addAll(l2); return l1; },
(list) -> list.stream()
)
);
But, as described above, I'd like to use the Stream.builder() approach inside a Collector, which works the same.
Upvotes: 0
Views: 615
Reputation: 103813
Most of the stream API will fast-crash when null
is involved. The things just aren't designed for it.
There are different ideas about what null
actually means. Reductively, null
in java means one of 3 things:
private String hello;
starts out as null
)new String[10]
starts with 10 null
values)null
, the keyword.But that's not too useful. Let's talk semantics. What does it mean when an API returns null
for something, or when you use null
in some code?
There are different semantic takes on it too:
In this case, exceptions are good and anything you could possibly want here would be wrong. You can't ask "concatenate this unknown thing to this string". The correct answer is not to silently just skip it. The correct answer is to crash: You can't concatenate an unknown. This is what SQL does with null
quite consistently, and is a usage of null
in java that I strongly recommend. It turns nulls downsides into upside: You use null
when you want that exception to occur if any code attempts to interact with the thing the pointer is pointing at (because the idea is: There is no value and the code flow should therefore not even check. If it does, there is a bug, and I would like an exception to occur exactly at the moment the bug is written please!).
In light of your code, if that's your interpretation of null
, then your code is acting correctly, by throwing an exception.
This is also common: That null
is being returned and that this has an explicit semantic meaning, hopefully described in the documentation. If you're ever written this statement:
if (x == null || x.isEmpty())
it is exceedingly likely you're using this semantic meaning of null
. After all, that code says: "At least for the purposes of this if
, there is no difference at all between an empty string and a null
pointer.
I strongly recommend you never do this. It's not necessary (just return an empty string instead!!), and it leads to friction: If you have a method in a Person
class named getTitle()
that returns null
when there is no title, and the project also states that title-less persons should just act as if the title is the empty string (Seems logical), then this is just wrong. Don't return null
. Return ""
. After all, if I call person.getTitle().length()
, then there is a undebatably correct answer to the question posed for someone with no title, and that is 0
.
Sometimes, some system defines specific behaviour that strongly leans towards 'undefined/unknown/unset' behaviour for a given field. For example, let's say the rules are: If the person's .getStudentId()
call returns a blank string that just means they don't have an ID yet. Then you should also never use null
then. If a value can represent a thing, then it should represent that thing in only one way. Use null
if you want exceptions if any code tries to ask anything at all about the nature of this value, use an existing value if one exists that does everything you want, and make a sentinel object that throws on certain calls but returns default values for others if you need really fine grained control.
Yes, if you ever write if (x == null || x.isEmpty())
, that's right: That's a code smell. Code that is highly indicative of suboptimal design. (Exception: Boundary code. If you're receiving objects from a system or from code that isn't under your direct control, then you roll with the punches. But if their APIs are bad, you should write an in-between isolating layer that takes their badly designed stuff and translates it to well-designed stuff explicitly. That translation layer is allowed to write if (x == null || x.isEmpty())
.
It sounds like this is the null
you want: It sounds like you want the act of appending null
to the stringbuilder to mean: "Just append nothing, then".
Thus, you want where you now have null
to act as follows when you append that to a stringbuilder: To do nothing at all to that stringbuilder.
There is already an object that does what you want: It's ""
.
Thus:
mapToObj(idx ->
addNulls && idx == flexBoundary
? ""
: whitespaces[idx])
You might want to rename your addNulls
variable to something else :)
Upvotes: 2