Which Java Collection class is better to group the list of objects?
I have a list of messages from users like below:
aaa hi
bbb hello
ccc Gm
aaa Can?
CCC yes
ddd No
From this list of message object I want to count and display aaa(2)+bbb(1)+ccc(2)+ddd(1). Any code help?
You can use Map<String, Integer> where the keys represent the individual strings, and the map value is the counter for each one.
So you can do something like:
// where ever your input comes from: turn it into lower case,
// so that "ccc" and "CCC" go for the same counter
String item = userinput.toLowerCase();
// as you want a sorted list of keys, you should use a TreeMap
Map<String, Integer> stringsWithCount = new TreeMap<>();
for (String item : str) {
if (stringsWithCount.contains(item)) {
stringsWithCount.put(item, stringsWithCount.get(item)+1));
} else {
stringsWithCount.put(item, 0);
}
}
And then you can iterate the map when done:
for (Entry<String, Integer> entry : stringsWithCount.entrySet()) {
and build your result string.
That was like the old-school implementation; if you want to be fancy and surprise your teachers, you can go for the Java8/lambda/stream solution.
( where i wouldn't recommend that unless you really invest the time to completely understand the following solution; as this is untested from my side)
Arrays.stream(someListOrArrayContainingItems)
.collect(Collectors
.groupingBy(s -> s, TreeMap::new, Collectors.counting()))
.entrySet()
.stream()
.flatMap(e -> Stream.of(e.getKey(), String.valueOf(e.getValue())))
.collect(Collectors.joining())
Putting the pieces together from a couple of the other answers, adapting to your code from the other question and fixing a few trivial errors:
// as you want a sorted list of keys, you should use a TreeMap
Map<String, Integer> stringsWithCount = new TreeMap<>();
for (Message msg : convinfo.messages) {
// where ever your input comes from: turn it into lower case,
// so that "ccc" and "CCC" go for the same counter
String item = msg.userName.toLowerCase();
if (stringsWithCount.containsKey(item)) {
stringsWithCount.put(item, stringsWithCount.get(item) + 1);
} else {
stringsWithCount.put(item, 1);
}
}
String result = stringsWithCount
.entrySet()
.stream()
.map(entry -> entry.getKey() + '(' + entry.getValue() + ')')
.collect(Collectors.joining("+"));
System.out.println(result);
This prints:
aaa(2)+bbb(1)+ccc(2)+ddd(1)
You need a MultiSet from guava. That collection type is tailor-made for this kind of task:
MultiSet<String> multiSet = new MultiSet<>();
for (String line : lines) { // somehow you read the lines
multiSet.add(line.split(" ")[0].toLowerCase());
}
boolean first = true;
for (Multiset.Entry<String> entry : multiset.entrySet()) {
if (!first) {
System.out.println("+");
}
first = false;
System.out.print(entry.getElement() + "(" + entry.getCount() + ")");
}
Assuming that you use Java 8, it could be something like this using the Stream API:
List<Message> messages = ...;
// Convert your list as a Stream
// Extract only the login from the Message Object
// Lowercase the login to be able to group ccc and CCC together
// Group by login using TreeMap::new as supplier to sort the result alphabetically
// Convert each entry into login(count)
// Join with a +
String result =
messages.stream()
.map(Message::getLogin)
.map(String::toLowerCase)
.collect(
Collectors.groupingBy(
Function.identity(), TreeMap::new, Collectors.counting()
)
)
.entrySet()
.stream()
.map(entry -> entry.getKey() + '(' + entry.getValue() + ')')
.collect(Collectors.joining("+"))
System.out.println(result);
Output:
aaa(2)+bbb(1)+ccc(2)+ddd(1)
If you want to group your messages by login and have the result as a collection, you can proceed as next:
Map<String, List<Message>> groupedMessages =
messages.stream()
.collect(
Collectors.groupingBy(
message -> message.getLogin().toLowerCase(),
TreeMap::new,
Collectors.toList()
)
);
Related
I have a Map<String, List<StartingMaterial>>
I want to convert the Object in the List to another Object.
ie. Map<String, List<StartingMaterialResponse>>
Can I do this using java stream Collectors.toMap()?
I tried something like the below code.
Map<String, List<StartingMaterial>> startingMaterialMap = xxxx;
startingMaterialMap.entrySet().stream().collect(Collectors.toMap( Map.Entry::getKey, Function.identity(), (k, v) -> convertStartingMaterialToDto(v.getValue())));
And my conversion code to change the Object is like below,
private StartingMaterialResponse convertStartingMaterialToDto(StartingMaterial sm) {
final StartingMaterialMatrix smm = sm.getStartingMaterialMatrix();
final StartingMaterial blending1Matrix = smm.getBlending1Matrix();
final StartingMaterial blending2Matrix = smm.getBlending2Matrix();
return new StartingMaterialResponse(
sm.getId(),
sm.getComponent().getCasNumber(),
sm.getDescription(),
sm.getPriority(),
String.join(" : ",
Arrays.asList(smm.getCarryInMatrix().getComponent().getMolecularFormula(),
blending1Matrix != null ? blending1Matrix.getComponent().getMolecularFormula() : "",
blending2Matrix != null ? blending2Matrix.getComponent().getMolecularFormula() : ""
).stream().distinct().filter(m -> !m.equals("")).collect(Collectors.toList())),
smm.getFamily(),
smm.getSplitGroup());
}
You can use the toMap collector since your source is a map. However you have to iterate over all the values and convert each of them into the DTO format inside the valueMapper.
Map<String, List<StartingMaterialResponse>> result = startingMaterialMap.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().stream()
.map(s -> convertStartingMaterialToDto(s)).collect(Collectors.toList())));
I think you mean to do :
startingMaterialMap.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey,
e -> e.getValue().stream()
.map(this::convertStartingMaterialToDto)
.collect(Collectors.toList()))
);
Here is my approach to this problem :
Map<String, List<Integer>> deposits = new HashMap<>();
deposits.put("first", Arrays.asList(1, 2, 3));
deposits.forEach((depositName, products) -> {
products.stream()
.map(myIntegerProduct -> myIntegerProduct.toString())
.collect(Collectors.toList());
});
The above example convert the List<Integer> to a list of Strings.
In your example, instead of myIntegerProduct.toString() is the convertStartingMaterialToDto method.
The forEach method iterates through every Key-Value pair in the map and you set some names for the key and the value parameters to be more specific and keep an understandable code for everyone who reads it. In my example : forEach( (depositName, products)) -> the depositName is the Key ( in my case a String ) and the products is the Value of the key ( in my case is a List of integers ).
Finally you iterate through the list too and map every item to a new type
products.stream()
.map(myIntegerProduct -> myIntegerProduct.toString())
I have a Map with list of empid and name. I want to select only emp name of people whose id is greater than 7 and name starts with N.
The result must be SET.
I tried using map.entryset() but cannot think how to filter inside map.
Do we have to use if else?
How we will return to set if multiple elements are found?
It should be something like this,
Set<String> selectedEmps = empIdToName.entrySet().stream()
.filter(e -> e.getKey() > 7)
.filter(e -> e.getValue().startsWith("N"))
.map(Map.Entry::getValue)
.collect(Collectors.toSet());
If I understand you correctly, here's a solution:
Suppose we have this data:
Map<String, List<Integer>> map = new HashMap<>();
map.put("Noo", new ArrayList<>(Arrays.asList(8,8,9)));
map.put("No", new ArrayList<>(Arrays.asList(1,8,9)));
map.put("Aoo", new ArrayList<>(Arrays.asList(8,8,9)));
We can filter the data in this way:
map.entrySet().
stream()
.filter(e -> e.getKey().startsWith("N"))
.filter(e -> e.getValue().stream().filter(id -> id <= 7).findAny().orElse(0) == 0)
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
The first filter excludes the Names that does not start with "N", the second filter goes through the remaining entries and check if all their ids are greater than 7. In the foreach I just print the data, but you can change the logic to your needs
The result should the this:
Noo [8, 8, 9]
Can be done very easily with a simple for-loop:
Map<Integer, String> map = ...
Set<String> result = new HashSet<>();
for(Map.Entry<Integer, String> entry : map.entrySet()){
if(entry.getKey() > 7){
String name = entry.getValue();
if(name.charAt(0) == 'N'){
result.add(name);
}
}
}
Note: if the names can be empty (length() == 0) then the name.charAt(0) approach will not work as you'll get a StringIndexOutOfBoundsException
Map<Integer, String> nameMap = new HashMap<>(); //A dummy map with empid and name of emp
public static void main(String[] args) {
nameMap.put(1,"John");
nameMap.put(2,"Doe");
nameMap.put(37,"Neon");
nameMap.put(14,"Shaun");
nameMap.put(35,"Jason");
nameMap.put(0,"NEO");
Set<String> empSet = nameMap.entrySet()
.stream()
.filter(x->x.getKey()>7 && x.getValue().startsWith("N"))
.map(x->x.getValue())
.collect(Collectors.toSet());
empSet.forEach(System.out::println);
}
The actual implementation and namespaces will vary but the basic
operations will remain same.
I have a simple User class with a String and an int property.
I would like to add two Lists of users this way:
if the String equals then the numbers should be added and that would be its new value.
The new list should include all users with proper values.
Like this:
List1: { [a:2], [b:3] }
List2: { [b:4], [c:5] }
ResultList: {[a:2], [b:7], [c:5]}
User definition:
public class User {
private String name;
private int comments;
}
My method:
public List<User> addTwoList(List<User> first, List<User> sec) {
List<User> result = new ArrayList<>();
for (int i=0; i<first.size(); i++) {
Boolean bsin = false;
Boolean isin = false;
for (int j=0; j<sec.size(); j++) {
isin = false;
if (first.get(i).getName().equals(sec.get(j).getName())) {
int value= first.get(i).getComments() + sec.get(j).getComments();
result.add(new User(first.get(i).getName(), value));
isin = true;
bsin = true;
}
if (!isin) {result.add(sec.get(j));}
}
if (!bsin) {result.add(first.get(i));}
}
return result;
}
But it adds a whole lot of things to the list.
This is better done via the toMap collector:
Collection<User> result = Stream
.concat(first.stream(), second.stream())
.collect(Collectors.toMap(
User::getName,
u -> new User(u.getName(), u.getComments()),
(l, r) -> {
l.setComments(l.getComments() + r.getComments());
return l;
}))
.values();
First, concatenate both the lists into a single Stream<User> via Stream.concat.
Second, we use the toMap collector to merge users that happen to have the same Name and get back a result of Collection<User>.
if you strictly want a List<User> then pass the result into the ArrayList constructor i.e. List<User> resultSet = new ArrayList<>(result);
Kudos to #davidxxx, you could collect to a list directly from the pipeline and avoid an intermediate variable creation with:
List<User> result = Stream
.concat(first.stream(), second.stream())
.collect(Collectors.toMap(
User::getName,
u -> new User(u.getName(), u.getComments()),
(l, r) -> {
l.setComments(l.getComments() + r.getComments());
return l;
}))
.values()
.stream()
.collect(Collectors.toList());
You have to use an intermediate map to merge users from both lists by summing their ages.
One way is with streams, as shown in Aomine's answer. Here's another way, without streams:
Map<String, Integer> map = new LinkedHashMap<>();
list1.forEach(u -> map.merge(u.getName(), u.getComments(), Integer::sum));
list2.forEach(u -> map.merge(u.getName(), u.getComments(), Integer::sum));
Now, you can create a list of users, as follows:
List<User> result = new ArrayList<>();
map.forEach((name, comments) -> result.add(new User(name, comments)));
This assumes User has a constructor that accepts name and comments.
EDIT: As suggested by #davidxxx, we could improve the code by factoring out the first part:
BiConsumer<List<User>, Map<String, Integer>> action = (list, map) ->
list.forEach(u -> map.merge(u.getName(), u.getComments(), Integer::sum));
Map<String, Integer> map = new LinkedHashMap<>();
action.accept(list1, map);
action.accept(list2, map);
This refactor would avoid DRY.
There is a pretty direct way using Collectors.groupingBy and Collectors.reducing which doesnt require setters, which is the biggest advantage since you can keep the User immutable:
Collection<Optional<User>> d = Stream
.of(first, second) // start with Stream<List<User>>
.flatMap(List::stream) // flatting to the Stream<User>
.collect(Collectors.groupingBy( // Collecting to Map<String, List<User>>
User::getName, // by name (the key)
// and reducing the list into a single User
Collectors.reducing((l, r) -> new User(l.getName(), l.getComments() + r.getComments()))))
.values(); // return values from Map<String, List<User>>
Unfortunately, the result is Collection<Optional<User>> since the reducing pipeline returns Optional since the result might not be present after all. You can stream the values and use the map() to get rid of the Optional or use Collectors.collectAndThen*:
Collection<User> d = Stream
.of(first, second) // start with Stream<List<User>>
.flatMap(List::stream) // flatting to the Stream<User>
.collect(Collectors.groupingBy( // Collecting to Map<String, List<User>>
User::getName, // by name (the key)
Collectors.collectingAndThen( // reduce the list into a single User
Collectors.reducing((l, r) -> new User(l.getName(), l.getComments() + r.getComments())),
Optional::get))) // and extract from the Optional
.values();
* Thanks to #Aomine
As alternative fairly straight and efficient :
stream the elements
collect them into a Map<String, Integer> to associate each name to the sum of comments (int)
stream the entries of the collected map to create the List of User.
Alternatively for the third step you could apply a finishing transformation to the Map collector with collectingAndThen(groupingBy()..., m -> ...
but I don't find it always very readable and here we could do without.
It would give :
List<User> users =
Stream.concat(first.stream(), second.stream())
.collect(groupingBy(User::getName, summingInt(User::getComments)))
.entrySet()
.stream()
.map(e -> new User(e.getKey(), e.getValue()))
.collect(toList());
For my assignment I have to replace for loops with streams that count the frequency of words in a text document, and I am having trouble figuring the TODO part out.
String filename = "SophieSallyJack.txt";
if (args.length == 1) {
filename = args[0];
}
Map<String, Integer> wordFrequency = new TreeMap<>();
List<String> incoming = Utilities.readAFile(filename);
wordFrequency = incoming.stream()
.map(String::toLowerCase)
.filter(word -> !word.trim().isEmpty())
.collect(Collectors.toMap(word -> word, word -> 1, (a, b) -> a + b, TreeMap::new));
int maxCnt = 0;
// TODO add a single statement that uses streams to determine maxCnt
for (String word : incoming) {
Integer cnt = wordFrequency.get(word);
if (cnt != null) {
if (cnt > maxCnt) {
maxCnt = cnt;
}
}
}
System.out.print("Words that appear " + maxCnt + " times:");
I have tried this:
wordFrequency = incoming.parallelStream().
collect(Collectors.toConcurrentMap(w -> w, w -> 1, Integer::sum));
But that is not right and I'm not sure how to incorporate maxCnt into the stream.
Assuming you have all the words extracted from a file in a List<String> this word count for each word can be computed using this approach,
Map<String, Long> wordToCountMap = words.stream()
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()));
The most freequent word can then be computed using the above map like so,
Entry<String, Long> mostFreequentWord = wordToCountMap.entrySet().stream()
.max(Map.Entry.comparingByValue())
.orElse(new AbstractMap.SimpleEntry<>("Invalid", 0l));
You may change the above two pipelines together if you wish like this,
Entry<String, Long> mostFreequentWord = words.stream()
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
.entrySet().stream()
.max(Map.Entry.comparingByValue())
.orElse(new AbstractMap.SimpleEntry<>("Invalid", 0l));
Update
As per the following discussion it is always good to return an Optional from your computation like so,
Optional<Entry<String, Long>> mostFreequentWord = words.stream()
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
.entrySet().stream()
.max(Map.Entry.comparingByValue());
Well, you have done almost everything you needed with that TreeMap, but it seems you don't know that it has a method called lastEntry and that is the only one you need to call after you computed wordFrequency to get the word with the highest frequency.
The only problem is that this is not very optimal, since TreeMap sorts the data on each insert and you don't need sorted data, you need the max. Sorting in case of TreeMap is O(nlogn), while inserting into a HashMap is O(n).
So instead of using that TreeMap, all you need to change is to a HashMap:
wordFrequency = incoming.stream()
.map(String::toLowerCase)
.filter(word -> !word.trim().isEmpty())
.collect(Collectors.toMap(
Function.identity(),
word -> 1,
(a, b) -> a + b,
HashMap::new));
Once you have this Map, you need to find max - this operation is O(n) in general and could be achieved with a stream or without one:
Collections.max(wordFrequency.entrySet(), Map.Entry.comparingByValue())
This approach with give you O(n) for HashMap insert, and O(n) for finding the max - thus O(n) in general, so it's faster than TreeMap
Ok, first of all, your wordFrequency line can make use of Collectors#groupingBy and Collectors#counting instead of writing your own accumulator:
List<String> incoming = Arrays.asList("monkey", "dog", "MONKEY", "DOG", "giraffe", "giraffe", "giraffe", "Monkey");
wordFrequency = incoming.stream()
.filter(word -> !word.trim().isEmpty()) // filter first, so we don't lowercase empty strings
.map(String::toLowerCase)
.collect(Collectors.groupingBy(s -> s, Collectors.counting()));
Now that we got that out of the way... Your TODO line says use streams to determine maxCnt. You can do that easily by using max with naturalOrder:
int maxCnt = wordFrequency.values()
.stream()
.max(Comparator.naturalOrder())
.orElse(0L)
.intValue();
However, your comments make me think that what you actually want is a one-liner to print the most frequent words (all of them), i.e. the words that have maxCnt as value in wordFrequency. So what we need is to "reverse" the map, grouping the words by count, and then pick the entry with highest count:
wordFrequency.entrySet().stream() // {monkey=3, dog=2, giraffe=3}
.collect(groupingBy(Map.Entry::getValue, mapping(Map.Entry::getKey, toList()))).entrySet().stream() // reverse map: {3=[monkey, giraffe], 2=[dog]}
.max(Comparator.comparingLong(Map.Entry::getKey)) // maxCnt and all words with it: 3=[monkey, giraffe]
.ifPresent(e -> {
System.out.println("Words that appear " + e.getKey() + " times: " + e.getValue());
});
This solution prints all the words with maxCnt, instead of just one:
Words that appear 3 times: [monkey, giraffe].
Of course, you can concatenate the statements to get one big do-it-all statement, like this:
incoming.stream() // [monkey, dog, MONKEY, DOG, giraffe, giraffe, giraffe, Monkey]
.filter(word -> !word.trim().isEmpty()) // filter first, so we don't lowercase empty strings
.map(String::toLowerCase)
.collect(groupingBy(s -> s, counting())).entrySet().stream() // {monkey=3, dog=2, giraffe=3}
.collect(groupingBy(Map.Entry::getValue, mapping(Map.Entry::getKey, toList()))).entrySet().stream() // reverse map: {3=[monkey, giraffe], 2=[dog]}
.max(Comparator.comparingLong(Map.Entry::getKey)) // maxCnt and all words with it: 3=[monkey, giraffe]
.ifPresent(e -> {
System.out.println("Words that appear " + e.getKey() + " times: " + e.getValue());
});
But now we're stretching the meaning of "one statement" :)
By piecing together information I was able to successfully replace the for loop with
int maxCnt = wordFrequency.values().stream().max(Comparator.naturalOrder()).get();
System.out.print("Words that appear " + maxCnt + " times:");
I appreciate all the help.
I have a method like this:
public static void main(String[] args) {
Map<String, Long> var = Files.walk(Paths.get("text"), number)
.filter(path -> !Files.isDirectory(path))
.map(Path::getFileName)
.map(Object::toString)
.filter(fileName -> fileName.contains("."))
.map(fileName -> fileName.substring(fileName.lastIndexOf(".") + 1))
.collect(Collectors.groupingBy(
extension -> extension,
Collectors.counting()
)
);
System.out.println(var);
}
As we know, output will be like:
{text=1, text=2}
Is it possible to change the output to:
text = 1
text = 2
I want to have some more freedom, e. g. remove brackets and commas, add new lines after number etc.
You can iterate over the results of the map, and just print them out:
for (Map.Entry<String, Long> entry : var.entrySet())
{
System.out.println(entry.getKey() + " = " + entry.getValue());
}
Then if you want more control, you can just modify how the line gets printed, since you have the raw key and value objects.
What about something as simple as:
map.toString().replace(", ", "\n");