I'm comparing files in folders (acceptor & sender) using JCIFS. During comparation two situations may occur:
- file not exists at acceptor
- file exists at acceptor
I need to get a map, where compared files are groupped by mentioned two types, so i could copy non-existing files or chech size and modification date of existing...
I want to make it using lambdas and streams, because i woult use parallel streams in near future, and it's also convinient...\
I've managed to make a working prototype method that checks whether file exists and creates a map:
private Map<String, Boolean> compareFiles(String[] acceptor, String[] sender) {
return Arrays.stream(sender)
.map(s -> new AbstractMap.SimpleEntry<>(s, Stream.of(acceptor).anyMatch(s::equals)))
Map.Entry::getValue)));
.collect(collectingAndThen(
toMap(Map.Entry::getKey, Map.Entry::getValue),
Collections::<String,Boolean> unmodifiableMap));
}
but i cant add higher level grouping by map value...
I have such a non-working piece of code:
private Map<String, Boolean> compareFiles(String[] acceptor, String[] sender) {
return Arrays.stream(sender)
.map(s -> new AbstractMap.SimpleEntry<>(s, Stream.of(acceptor).anyMatch(s::equals)))
.collect(groupingBy(
Map.Entry::getValue,
groupingBy(Map.Entry::getKey, Map.Entry::getValue)));
}
}
My code can't compile, because i missed something very important.. Could anyone help me please and exlain how to make this lambda correct?
P.S. arrays from method parameters are SmbFiles samba directories:
private final String master = "smb://192.168.1.118/mastershare/";
private final String node = "smb://192.168.1.118/nodeshare/";
SmbFile masterDir = new SmbFile(master);
SmbFile nodeDir = new SmbFile(node);
Map<Boolean, <Map<String, Boolean>>> resultingMap = compareFiles(masterDir, nodeDir);
Collecting into nested maps with the same values, is not very useful. The resulting Map<Boolean, Map<String, Boolean>> can only have two keys, true and false. When you call get(true) on it, you’ll get a Map<String, Boolean> where all string keys redundantly map to true. Likewise, get(false) will give a you map where all values are false.
To me, it looks like you actually want
private Map<Boolean, Set<String>> compareFiles(String[] acceptor, String[] sender) {
return Arrays.stream(sender)
.collect(partitioningBy(Arrays.asList(acceptor)::contains, toSet()));
}
where get(true) gives you a set of all strings where the predicate evaluated to true and vice versa.
partitioningBy is an optimized version of groupingBy for boolean keys.
Note that Stream.of(acceptor).anyMatch(s::equals) is an overuse of Stream features. Arrays(acceptor).contains(s) is simpler and when being used as a predicate like Arrays.asList(acceptor)::contains, the expression Arrays.asList(acceptor) will get evaluated only once and a function calling contains on each evaluation is passed to the collector.
When acceptor gets large, you should not consider parallel processing, but replacing the linear search with a hash lookup
private Map<Boolean, Set<String>> compareFiles(String[] acceptor, String[] sender) {
return Arrays.stream(sender)
.collect(partitioningBy(new HashSet<>(Arrays.asList(acceptor))::contains, toSet()));
}
Again, the preparation work of new HashSet<>(Arrays.asList(acceptor)) is only done once, whereas the contains invocation, done for every element of sender, will not depend on the size of acceptor anymore.
I've managed to solve my problem. I had a type mismatch, so the working code is:
private Map<Boolean, Map<String, Boolean>> compareFiles(String[] acceptor, String[] sender) {
return Arrays.stream(sender)
.map(s -> new AbstractMap.SimpleEntry<>(s, Stream.of(acceptor).anyMatch(s::equals)))
.collect(collectingAndThen(
groupingBy(Map.Entry::getValue, toMap(Map.Entry::getKey, Map.Entry::getValue)),
Collections::<Boolean, Map<String, Boolean>> unmodifiableMap));
}
Related
I'm fairly new to Java and trying to learn how to use streams for easier code writing. If I can code like this:
Map<String, SomeConfig> temp = new HashMap<>();
resultStorage.forEach((key, value) -> key.getUsers().forEach(user -> {
if (!temp.containsKey(user.getMeta())) {
SomeConfig emailConfiguration = key
.withCheck1(masterAccountId)
.withCheck2(getClientTimezone())
.withCheck3(user.getMeta());
temp.put(user.getMeta(), emailConfiguration);
}
temp.get(user. getMeta()).getStreams().add(value);
}));
return new ArrayList<>(temp.values());
resultStorage declaration:
private Map< SomeConfig, byte[]> resultStorage = new ConcurrentHashMap<>();
getStreams is a getter on SomeConfig that returns a List<byte[]> as here:
private List<byte[]> attachmentStreams = new ArrayList<>();
public List<byte[]> getAttachmentStreams() {
return attachmentStreams;
}
My first attempt was something similar to this:
resultStorage.entrySet().stream()
.forEach(entry -> entry.getKey().getUsers().forEach(user -> {
}));
Are we able to use a forEach within one of the streams terminating operation, forEach? How would a stream benefit in this case as I saw documentation that it can significantly improve readability and performance of older pre-Java8 code?
Edit:
resultStorage holds a ConcurrentHashMap. It will contain Map<SomeConfig, byte[]> for email and attachments. Using another HashMap temp that is initially empty - we analyze resultStorage , see if temp contains a specific email key, and then put or add based on the existence of a user's email
The terminal operation of entrySet().stream().forEach(…) is entirely unrelated to the getUsers().forEach(…) call within the Consumer. So there’s no problem of “multiple terminal operations” here.
However, replacing the Map operation forEach((key, value) -> … with an entrySet() .stream() .forEach(entry -> …) rarely adds a benefit. So far, you’re not only made the code longer, you introduced the necessity to deal with a Map.Entry instead of just using key and value.
But you can simplify your operation by using a single computeIfAbsent instead of containsKey, put, and get:
resultStorage.forEach((key, value) -> key.getUsers().forEach(user ->
temp.computeIfAbsent(user.getMeta(), meta ->
key.withCheck1(masterAccountId).withCheck2(getClientTimezone()).withCheck3(meta))
.getStreams().add(value)));
Notes after the code.
Map<String, SomeConfig> temp = resultStorage.keySet()
.stream()
.flatMap(key -> key.getUsers()
.stream()
.map(user -> new AbstractMap.SimpleEntry(user, key)))
.collect(Collectors.toMap(e -> e.getKey().getMeta(),
e -> e.getValue()
.withCheck1(masterAccountId)
.withCheck2(getClientTimezone())
.withCheck3(e.getKey().getMeta())
resultStorage.keySet()
This returns Set<SomeConfig>.
stream()
This returns a stream where every element in the stream is an instance of SomeConfig.
.flatMap(key -> key.getUsers()
.stream()
.map(user -> new AbstractMap.SimpleEntry(user, key)))
Method flatMap() must return a Stream. The above code returns a Stream where every element is an instance of AbstractMap.SimpleEntry. The "entry" key is the user and the entry value is the key from resultStorage.
Finally I create a Map<String, SomeConfig> via [static] method toMap of class Collectors.
The first argument to method toMap is the key mapper, i.e. a method that extracts the [map] key from the AbstractMap.SimpleEntry. In your case this is the value returned by method getMeta() of the user – which is the key from AbstractMap.SimpleEntry, i.e. e.getKey() returns a user object.
The second argument to toMap is the value mapper. e.getValue() returns a SomeConfig object and the rest is your code, i.e. the withChecks.
There is no way I can test the above code because not only did you not post a minimal, reproducible example, you also did not post any sample data. Hence the above may be way off what you actually require.
Also note that the above code simply creates your Map<String, SomeConfig> temp. I could not understand the code in your question that processes that Map so I did not try to implement that part at all.
I want to get the following Data structure: Map<String, Map<String, Integer>>
Given is a class either containing the fields als primitives (position, destination, distance) or as a key (position) plus map (target). From each unique position one can target to many destinations (by distance).
private static class LocationPair {
String position, destination;
int distance;
}
Map<String, Map<String, Integer>> locations = locationPairs.stream()
.collect(Collectors.groupingBy(pair -> pair.position, Collectors.toMap(pair.destination, pair.distance)));
private static class LocationPair {
String position;
Map<String, Integer> target = Collections.singletonMap(destination, distance);
}
Map<String, Map<String, Integer>> locations = locationPairs.stream()
.collect(Collectors.groupingBy(pair -> pair.position, Collectors.mapping(pair -> pair.target)));
Regarding the second code-snippet: The result should be the same as in the first code.The only difference is, that the provided data in LocationPair have been further processed so that destination and distance have been put already into their target-Map.
I know this must be possible, but somehow I can't figure it out how to get it done. The stream-code snippets above shall show what I mean although I know that they aren't working.
Many thanks for any help
The code mostly looked correct. Minor tweaks got it working.
public static Map<String, Map<String, Integer>> locations(List<LocationPair> locationPairs) {
return locationPairs.stream()
.collect(
Collectors.groupingBy(LocationPair::getPosition, Collectors.toMap(LocationPair::getDestination, LocationPair::getDistance)));
}
Using variables instead of method references, this becomes -
locationPairs.stream()
.collect(
Collectors.groupingBy(tmp -> tmp.position, Collectors.toMap(tmp2 -> tmp2.destination, tmp2 -> tmp2.distance)));
Hope this helps. Let me know in-case I missed something
I have a Java stream that invokes .collect(Collectors.toMap). Collectors.toMap accepts a keyMapper and a valueMapper functions. I'd like to create two entries for each stream element, with two different keyMapper functions, but with the same valueMapper function. Is it possible to do this in a nice stream syntax without creating a custom collector?
Of course, I could also get one map, then add another set of keys with the same values to it, outside of the stream chain calls. But I was wondering if it could be made neater...
Basically what I have is:
List<A> someObjects = ...; // obtain somehow
Map<String, B> res = someObjects.stream().collect(Collectors.toMap(keyMapper1, valueMapper));
And functions keyMapper1 and keyMapper2 produce different strings, and I want both of those in my map with the same value.
What I can do is:
Map<A, B> map = someObjects.stream().collect(Collectors.toMap(Function.identity(), valueMapper));
Map<String, B> result = new HashMap<>();
map.forEach((a, b) -> {
result.put(keyMapper1(a), b);
result.put(keyMapper2(a), b);
});
But maybe something could be done without creating an intermediate variable?
You can use flatMap to create a stream of all the map entries first, and then collect them to a map. Something like this:
Map<String, String> map = someObjects.stream()
.flatMap(obj -> Stream.of(
Map.entry(keyMapper1(obj), valueMapper(obj)),
Map.entry(keyMapper2(obj), valueMapper(obj))))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
I have a flux of response form below responses as Flux.<Response>fromIterable(responses). I want to convert this to Mono of map as follows:
Mono< Map< String, Map< String, Collection< Response>>>> collectMap = ?
where company is first key for which another map of response will be generated with category as key.
List< Response> responses = new ArrayList(){
{
add(Response.builder().company("Samsung").category("Tab").price("$2000").itemName("Note").build());
add(Response.builder().company("Samsung").category("Phone").price("$2000").itemName("S9").build());
add(Response.builder().company("Samsung").category("Phone").price("$1000").itemName("S8").build());
add(Response.builder().company("Iphone").category("Phone").price("$5000").itemName("Iphone8").build());
add(Response.builder().company("Iphone").category("Tab").price("$5000").itemName("Tab").build());
}
};
Though I am able to achieve initial map as follow
Mono<Map<String, Collection<Response>>> collect = Flux.<Response>fromIterable( responses )
.collectMultimap( Response::getCompany );
Do someone has an idea how I can achieve my goal here.
I don't think collectMultiMap or collectMap helps you directly in this case:
The collectMultiMap (and its overloads) only can return Map<T, Collection<V> which is clearly different than what you want. Of course, you can process the resulting value set (namely the Collection<V> part of the map) with a O(n) complexity.
On the other hand collectMap (and its overloads) look a bit more promising, if you provide the value function. However, you don't have access to other V objects, which forbids you to build the Collection<V>.
The solution I came up with is using reduce; though the return type is:
Mono<Map<String, Map<String, List<Response>>>> (mind the List<V> instead of Collection<V>)
return Flux.<Response>fromIterable( responses )
.reduce(new HashMap<>(), (map, user) -> {
map.getOrDefault(user.getId(), new HashMap<>())
.getOrDefault(user.getEmail(), new ArrayList<>())
.add(user);
return map;
});
The full type for the HashMap in reduce is HashMap<String, Map<String, List<AppUser>>>, thankfully Java can deduce that from the return type of the method or the type of the assigned variable.
I have a function like this:
private static Map<String, ResponseTimeStats> perOperationStats(List<PassedMetricData> scopedMetrics, Function<PassedMetricData, String> classifier)
{
Map<String, List<PassedMetricData>> operationToDataMap = scopedMetrics.stream()
.collect(groupingBy(classifier));
return operationToDataMap.entrySet().stream()
.collect(toMap(Map.Entry::getKey, e -> StatUtils.mergeStats(e.getValue())));
}
Is there any way to have the groupBy call do the transformation that i do explicitly in line 2 so i dont have to separately stream over the map?
Update
Here is what mergeStats() looks like:
public static ResponseTimeStats mergeStats(Collection<PassedMetricData> metricDataList)
{
ResponseTimeStats stats = new ResponseTimeStats();
metricDataList.forEach(data -> stats.merge(data.stats));
return stats;
}
If you can rewrite StatUtils.mergeStats into a Collector, you could just write
return scopedMetrics.stream().collect(groupingBy(classifier, mergeStatsCollector));
And even if you can't do this, you could write
return scopedMetrics.stream().collect(groupingBy(classifier,
collectingAndThen(toList(), StatUtils::mergeStats)));
In order to group the PassedMetricData instances, you must consume the entire Stream since, for example, the first and last PassedMetricData might be grouped into the same group.
That's why the grouping must be a terminal operation on the original Stream and you must create a new Stream in order to do the transformation on the results of this grouping.
You could chain these two statements, but it won't make much of a difference :
private static Map<String, ResponseTimeStats> perOperationStats(List<PassedMetricData> scopedMetrics, Function<PassedMetricData, String> classifier)
{
return scopedMetrics.stream()
.collect(groupingBy(classifier)).entrySet().stream()
.collect(toMap(Map.Entry::getKey, e -> StatUtils.mergeStats(e.getValue())));
}