First off the general case: Using a flag to check if some element of a collection meets a certain condition is not uncommon. But the pattern I've seen most often to solve this is to move the check in an extra method and directly return from it (like Kilian Foth described in his answer):
private <T> boolean checkCollection(Collection<T> collection)
{
for (T element : collection)
if (checkElement(element))
return true;
return false;
}
Since Java 8 there is a more concise way using Stream.anyMatch(…)
:
collection.stream().anyMatch(this::checkElement);
In your case this would probably look like this (assuming list == entry.getValue()
in your question):
map.values().stream().anyMatch(list -> list.size() > limit);
The problem in your specific example is the additional call to fillUpList()
. The answer depends a lot on what this method is supposed to do.
Side note: As it stands, the call to fillUpList()
does not make much sense, because it does not depend on the element you are currently iterating. I guess this is a consequence of stripping down your actual code to fit the question format. But exactly that leads to an artificial example that is difficult to interpret and thus hard to reason about. Therefore it is so important to provide a Minimal, Complete, and Verifiable example.
So I assume that the actual code passes the current entry
to the method.
But there are more questions to ask:
- Are the lists in the map empty before reaching this code? If so, why is there already a map and not just the list or set of the
BigInteger
keys? If they are not empty, why do you need to fill up the lists? When there are already elements in the list, isn't it an update or some other computation in this case?
- What causes a list to become larger than the limit? Is this an error condition or expected to happen often? Is it caused by invalid input?
- Do you need the lists computed up to the point you reach a list larger than the limit?
- What does the "Do something" part do?
- Do you restart the filling after this part?
This are only some questions that came to my mind when I tried to understand the code fragment. So, in my opinion, that is the real code smell: Your code does not clearly communicate the intent.
It could either mean this ("all or nothing" and reaching the limit indicates an error):
/**
* Computes the list of all foo strings for each passed number.
*
* @param numbers the numbers to process. Must not be {@code null}.
* @return all foo strings for each passed number. Never {@code null}.
* @throws InvalidArgumentException if any number produces a list that is too long.
*/
public Map<BigInteger, List<String>> computeFoos(Set<BigInteger> numbers)
throws InvalidArgumentException
{
if (numbers.isEmpty())
{
// Do you actually need to log this here?
// The caller might know better what to do in this case...
logger.info("Nothing to compute");
}
return numbers.stream().collect(Collectors.toMap(
number -> number,
number -> computeListForNumber(number)));
}
private List<String> computeListForNumber(BigInteger number)
throws InvalidArgumentException
{
// compute the list and throw an exception if the limit is exceeded.
}
Or it could mean this ("update until first problem"):
/**
* Refreshes all foo lists after they have become invalid because of bar.
*
* @param map the numbers with all their current values.
* The values in this map will be modified.
* Must not be {@code null}.
* @throws InvalidArgumentException if any new foo list would become too long.
* Some other lists may have already been updated.
*/
public void updateFoos(Map<BigInteger, List<String>> map)
throws InvalidArgumentException
{
map.replaceAll(this::computeUpdatedList);
}
private List<String> computeUpdatedList(
BigInteger number, List<String> currentValues)
throws InvalidArgumentException
{
// compute the new list and throw an exception if the limit is exceeded.
}
Or this ("update all lists but keep original list if it becomes too large"):
/**
* Refreshes all foo lists after they have become invalid because of bar.
* Lists that would become too large will not be updated.
*
* @param map the numbers with all their current values.
* The values in this map will be modified.
* Must not be {@code null}.
* @return {@code true} if all updates have been successful,
* {@code false} if one or more elements have been skipped
* because the foo list size limit has been reached.
*/
public boolean updateFoos(Map<BigInteger, List<String>> map)
{
boolean allUpdatesSuccessful = true;
for (Entry<BigInteger, List<String>> entry : map.entrySet())
{
List<String> newList = computeListForNumber(entry.getKey());
if (newList.size() > limit)
allUpdatesSuccessful = false;
else
entry.setValue(newList);
}
return allUpdatesSuccessful;
}
private List<String> computeListForNumber(BigInteger number)
{
// compute the new list
}
Or even the following (using computeFoos(…)
from the first example but without exceptions):
/**
* Processes the passed numbers. An optimized algorithm will be used if any number
* produces a foo list of a size that justifies the additional overhead.
*
* @param numbers the numbers to process. Must not be {@code null}.
*/
public void process(Collection<BigInteger> numbers)
{
Map<BigInteger, List<String>> map = computeFoos(numbers);
if (isLimitReached(map))
processLarge(map);
else
processSmall(map);
}
private boolean isLimitReached(Map<BigInteger, List<String>> map)
{
return map.values().stream().anyMatch(list -> list.size() > limit);
}
Or it could mean something entirely different… ;-)