Adding Multiple Set<String> in Java - java

I have two sets as: set1 and set2 that I want to combine.
set1 contains personID and place as: [1-NY, 2-CA, 3-MD, 1-TX, 3-VA]
set2 contains personName and place as: [John-NY, Bill-CA, Ron-CA, Rick-MD, John-TX, Rick-VA]
I want to combine both the set such that I will get the output of personID, personName and place as: [1-John-NY, 2-Bill-CA, 2-Ron-CA, 3-Rick-MD, 1-John-TX, 3-Rick-VA].
Basically the thing is: I want to use "place" as the anchor to combine.
Set<String> set1 = new LinkedHashSet<String>();
Set<String> set2 = new LinkedHashSet<String>();
Set<String> combination = new LinkedHashSet<String>();
combination.addAll(set1);
combination.addAll(set2);
But, I am not able to get the output in my expected way. Any suggestion please.
Thanks!

You should rethink your approach a bit. In order to combine these two sets you should create some kind of look-up table. I would use simple HashMap for this.
The code is really self-explanatory, but fell free to ask questions)
Using Java 8:
Set<String> personIds = new LinkedHashSet<>(Arrays.asList("1-NY", "2-CA", "3-MD", "1-TX", "3-VA"));
Set<String> personNames = new LinkedHashSet<>(Arrays.asList("John-NY", "Bill-CA", "Ron-CA", "Rick-MD", "John-TX", "Rick-VA"));
Map<String, String> personIdMap = personIds.stream().map(v -> v.split("-"))
.collect(Collectors.toMap(v -> v[1], v -> v[0]));
Set<String> combination = new LinkedHashSet<>();
personNames.forEach(name -> {
final String[] split = name.split("-");
final String personId = personIdMap.get(split[1]);
combination.add(personId + '-' + name);
});
Using Java 7:
Set<String> personIds = new LinkedHashSet<>(Arrays.asList("1-NY", "2-CA", "3-MD", "1-TX", "3-VA"));
Set<String> personNames = new LinkedHashSet<>(Arrays.asList("John-NY", "Bill-CA", "Ron-CA", "Rick-MD", "John-TX", "Rick-VA"));
Map<String, String> personIdMap = new HashMap<>();
for (String id : personIds) {
final String[] split = id.split("-");
personIdMap.put(split[1], split[0]);
}
Set<String> combination = new LinkedHashSet<>();
for (String name : personNames) {
final String[] split = name.split("-");
final String personId = personIdMap.get(split[1]);
combination.add(personId + '-' + name);
}

As user chrylis suggests, you could use class for this propose. First, create a class Person.class to store the required values: person ID / person name / place name. For simplifying the process, a constructor with 3 parameters is used here to construct the object, but it's not the only choice. By the way, I strongly suggest you to use a unique value for each person.
public Person(String id, String name, String place) {
this.id = id;
this.name = name;
this.place = place;
}
Then create a method to combine the different information stored in the person class.
public String getCombination() {
return String.format("%s-%s-%s", id, name, place);
}
Now you can put the data into the set combinations:
Set<Person> people = new LinkedHashSet<>();
people.add(new Person("1", "John", "NY"));
people.add(new Person("2", "Bill", "CA"));
people.add(new Person("2", "Ron", "CA"));
people.add(new Person("3", "Rick", "MD"));
people.add(new Person("1", "John", "TX"));
people.add(new Person("3", "Rick", "VA"));
Set<String> combinations = new LinkedHashSet<>();
for (Person p : people) {
combinations.add(p.getCombination());
}
Here's the full implementation of class Person.
public class Person {
private String id; // maybe place id ?
private String name;
private String place;
public Person(String id, String name, String place) {
this.id = id;
this.name = name;
this.place = place;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPlace(String place) {
return place;
}
public void setPlace(String place) {
this.place = place;
}
public String getCombination() {
return String.format("%s-%s-%s", id, name, place);
}
}

Related

List of objects - get object fields distinct count

For a list of objects I have to check for (some) fields:
all objects having same value for that field
all objects having a different value for that field
class Person {
final String name;
final int age;
final int group;
public Person( final String name, final int age, final int group ) {
this.name = name;
this.age = age;
this.group = group;
}
public String getName() {
return this.name;
}
public int getAge() {
return this.age;
}
public int getGroup() {
return this.group;
}
}
public static <T> long distinctByField( final List<Person> personList, final Function<Person, T> field ) {
return personList.stream()
.map( field )
.distinct().count();
}
public static void main( final String[] args ) {
final List<Person> personList = Arrays.asList(
new Person( "Fred", 25, 1 ),
new Person( "Bill", 22, 1 ),
new Person( "Fred", 27, 1 ),
new Person( "Lisa", 25, 1 )
);
System.out.println( distinctByField( personList, Person::getName ) );
System.out.println( distinctByField( personList, Person::getAge ) );
System.out.println( distinctByField( personList, Person::getGroup ) );
}
With result of stream/distinct/count I can compare with current list size:
if count == 1 : all objects having same value for that field
if count == list.size : all objects having different value for that field
Drawback is, i have to stream for every interested field.
Is it possible to do this with one query (for a list of interested fields) ?
It's possible using reflection:
public class ReflectionTest {
class Person {
final String name;
final int age;
final int group;
public Person(final String name, final int age, final int group) {
this.name = name;
this.age = age;
this.group = group;
}
public String getName() {
return this.name;
}
public int getAge() {
return this.age;
}
public int getGroup() {
return this.group;
}
}
#DisplayName("should determine distinct fields")
#Test
public void distinct() {
final List<Person> personList = Arrays.asList(new Person("Fred", 25, 1),
new Person("Bill", 22, 1),
new Person("Fred", 27, 1),
new Person("Lisa", 25, 1));
Map<String,Long> fieldCountMap = Stream.of("name", "age", "group")
.map(fieldName -> ReflectionUtils.findField(Person.class, fieldName))
.filter(Objects::nonNull)
.collect(Collectors.toMap(Field::getName, field -> personList.stream().map(person -> getField(field, person)).distinct().count()));
assertEquals(3,fieldCountMap.get("name"));
assertEquals(1,fieldCountMap.get("group"));
assertEquals(3,fieldCountMap.get("age"));
}
//extracted into a method because field.get throws a checked exception
Object getField(Field field, Person object) {
try {
return field.get(object);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
Let me first mention: This is a more or less huge downside in code quality (searching manually through all fields, using an extra class to store the results). And I doubt, this would be more efficient in terms of computation time or memory. By logic you will have to touch every field of every person and store values that already occured in order to find the distinct count for every field. Which is exactly what your solution with 3 streams does. I'd advice you to stay with it.
But here is a solution. I built a collector, that collects in one run all the different values for each field into a custom class.
static class PersonStatistic {
Set<String> names = new HashSet<>();
Set<Integer> ages = new HashSet<>();
Set<Integer> groups = new HashSet<>();
}
public static void main(final String[] args) {
final List<Person> personList = Arrays.asList(
new Person("Fred", 25, 1),
new Person("Bill", 22, 1),
new Person("Fred", 27, 1),
new Person("Lisa", 25, 1));
PersonStatistic personStatistic = personList.stream().collect(
// Create new Statistic
PersonStatistic::new,
// Merge A Person into statistic
(statistic, person) -> {
statistic.names.add(person.name);
statistic.ages.add(person.age);
statistic.groups.add(person.group);
},
// Merge second statistic into first
(stat1, stat2)-> {
stat1.names.addAll(stat2.names);
stat1.ages.addAll(stat2.ages);
stat1.groups.addAll(stat2.groups);
});
System.out.println(personStatistic.names.size());
System.out.println(personStatistic.ages.size());
System.out.println(personStatistic.groups.size());
}

What is the best way to get the result through Java8 function?

I need to filter elements and then sort based on certain column. Post that I would need to find the unique entries based on combination of columns. Since it is file processing, pipe(|) is used as delimiter to denote the column value.
String s1= "12|Thor|Asgaurd|1000000|Avenger|Active"
String s2= "234|Iron man|New York|9999999|Avenger|Active"
String s3= "420|Loki|Asgaurd|||Inactive"
String s4= "12|Thor|Asgaurd Bank|1000000|Avenger HQ|Active"
Data first needs to be filtered based on the Active/Inactive status. Then it needs to be sorted based on 4th column. Lastly, the uniqueness needs to be maintained by combining column 1,2,3.
Expected Output =
"234|Iron man|New York|9999999|Avenger|Active"
"12|Thor|Asgaurd|1000000|Avenger|Active"
Creating a model class and parsing the string is the way to go, but if for some reaseon you don't want to do that you can do it this way:
import java.util.Comparator;
import java.util.stream.Collectors;
import java.util.stream.Stream;
List<String> result = Stream.of(s1, s2, s3, s4)
.filter(s -> s.split("\\|")[5].equals("Active"))
.sorted(Comparator.comparing(e -> e.split("\\|")[4]))
.collect(Collectors.toList());
First of all you should create an Object which represents your String data. Something like this:
public class MyObject {
private int id;
private String name;
private String location;
private Integer value;
private String category;
private String state;
public MyObject(String entry) {
String[] parts = entry.split("\\|");
if (parts.length != 6) {
throw new IllegalArgumentException("entry has not 6 parts");
}
id = Integer.parseInt(parts[0]);
name = parts[1];
location = parts[2];
try {
value = Integer.parseInt(parts[3]);
} catch (NumberFormatException ignored) {
}
category = parts[4];
state = parts[5];
}
// getters
#Override
public String toString() {
return String.join("|", String.valueOf(id), name, location, String.valueOf(value), category, state);
}
}
With this you can create a Stream of objects from your Strings and to the filter, sort and distinct operations afterwards:
Collection<MyObject> result = Stream.of(s1, s2, s3, s4)
.map(MyObject::new)
.filter(o -> "Active".equals(o.getState()))
.sorted(Comparator.comparing(MyObject::getValue).reversed())
.collect(Collectors.toMap(o -> Arrays.asList(o.getId(), o.getName()),
Function.identity(), (o1, o2) -> o1, LinkedHashMap::new))
.values();
result.forEach(System.out::println);
After the map operation you filter the values by state and sort them by column 4 (value in my case). At the end you collect all the values in a map for the distinct operation. Add all values you need distinction for to the Arrays.asList(). As values the map takes all the original values (Function.identity()). For duplicates we keep the first value ((o1, o2) -> o1) and we are using a LinkedHashMap to keep the order of the items. At the end wee use only the values of the map.
If you need a List instead of a Collection use new ArrayList(result).
The result will be this:
234|Iron man|New York|9999999|Avenger|Active
12|Thor|Asgaurd|1000000|Avenger|Active
It seems like you're unable to filter while everything is string only.
Try this,
create a new model class which can hold your columns.
Ex:
class MyData{
private String name;
private String city;
private String distance;
private String organization;
private String status;
//And create Getter Setter method for all above fields.
}
Now came to your main class where you can play with your code stuff.
Map<MyData> map = new HashMap<MyData>();
MyData myData = new MyData();
myData.setName("Thor");
myData.setCity("Asgaurd");
myData.setDistance("1000000");
myData.setOrganization("Avenger");
myData.setStatus("Active");
map.put(12, myData);
//Same thing for all other data (note: use the loop for data insertion in map)
Map<String, MyData> sorted = map.entrySet().stream().sorted(comparingByValue()).collect(toMap(e -> e.getKey(), e -> e.getValue().getName(), (e1, e2) -> e2,LinkedHashMap::new));
System.out.println("map after sorting by values: " + sorted);
You can solve your task this way:
Firstly, just create POJO(Plain Old Java Object) and override the toString() method.
class MarvelPerson {
private Integer id;
private String name;
private String origin;
private Integer point = null;
private String faction;
private String status;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getOrigin() {
return origin;
}
public void setOrigin(String origin) {
this.origin = origin;
}
public Integer getPoint() {
return point;
}
public void setPoint(Integer point) {
this.point = point;
}
public String getFaction() {
return faction;
}
public void setFaction(String faction) {
this.faction = faction;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
#Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append(id);
builder.append("|");
builder.append(name);
builder.append("|");
builder.append(origin);
builder.append("|");
if(point != null) {
builder.append(point);
}
builder.append("|");
if(faction != null) {
builder.append(faction);
}
builder.append("|");
builder.append(status);
return builder.toString();
}
}
Then, you should write the parser from string to MarvelPerson. Side note: Carefully, my implementation is pretty basic, and I suppose it should be modified because I may not have foreseen some corner cases.
class PersonParser {
static MarvelPerson parse(String data) {
MarvelPerson person = new MarvelPerson();
String[] array = data.split("\\|", -1);
person.setId(Integer.parseInt(array[0]));
person.setName(array[1]);
person.setOrigin(array[2]);
if(!array[3].isEmpty()) {
person.setPoint(Integer.parseInt(array[3]));
}
if(!array[4].isEmpty()) {
person.setFaction(array[4]);
}
person.setStatus(array[5]);
return person;
}
}
And then your solution:
public class Test {
public static void main(String[] args) {
List<MarvelPerson> list = new ArrayList<>();
list.add(PersonParser.parse("12|Thor|Asgaurd|1000000|Avenger|Active"));
list.add(PersonParser.parse("234|Iron man|New York|9999999|Avenger|Active"));
list.add(PersonParser.parse("420|Loki|Asgaurd|||Inactive"));
list.add(PersonParser.parse("12|Thor|Asgaurd Bank|1000000|Avenger HQ|Actie"));
list.stream()
.filter(marvelPerson -> marvelPerson.getStatus().equals("Active"))
.sorted((o1, o2) -> o1.getPoint() <= o2.getPoint() ? 1 : -1)
.forEach(marvelPerson -> {
System.out.println(marvelPerson.toString());
});
}
}
The output to be printed:
234|Iron man|New York|9999999|Avenger|Active
12|Thor|Asgaurd|1000000|Avenger|Active

How to collect properties of List<Map> by unique property using MultiMap?

I have List of stories. Using unique property(id) I want to collect keyword and targeting as list of values. Can I do this with MultiMap? Or is there other library for this?
[{
id = 1,
title = Onboarding,
keyword = new joinee,
targeting = finance
}, {
id = 1,
title = Onboarding,
keyword = training,
targeting = HR
}]
The Desired output must like this :
{
id = 1,
title = Onboarding,
keyword = [new joinee,training], //may be keywords - plural
targeting = [HR,finance]
}
Sample my tried Code as follows:
package prac;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class JavaPrac {
public static void main(String[] args) {
Multimap<Integer, Map> multiMap = ArrayListMultimap.create();
List<Map> stories=new ArrayList();
Map story1=new HashMap();
story1.put("id", 1);
story1.put("title", "Onboarding");
story1.put("keyword","new joinee");
story1.put("targeting","finance");
Map story2=new HashMap();
story2.put("id", 1);
story2.put("title", "Onboarding");
story2.put("keyword","training");
story2.put("targeting","HR");
stories.add(story1);
stories.add(story2);
System.out.println(stories);
stories.forEach((story) -> {
multiMap.put((Integer) story.get("id"), story);
});
}
}
A multimap can only store multiple values per key but what you want is to combine those multiple values so that you get one element that has the same id and title as well as a collection of keywords and targeting information. Thus it would probably be best to either have something like MultiStory or already have Story contain those collections.
I'd suggest using proper objects instead of just maps but with maps and Java 8 lambdas you could use compute() etc. to build maps that contain collections and combine maps that don't.
Here's an example of how you'd do it with maps. Note that this is very bad style and an example using proper pojos will follow:
Disclaimer: example based on the OP's code, not recommended (read text above)
//Problem 1: we don't know the type of the values, i.e. we could put anything for "id" etc.
Map<String, Object> story1=new HashMap<>();
story1.put("id", 1);
story1.put("title", "Onboarding");
story1.put("keyword","new joinee");
story1.put("targeting","finance");
Map<String, Object> story2=new HashMap<>();
story2.put("id", 1);
story2.put("title", "Onboarding");
story2.put("keyword","training");
story2.put("targeting","HR");
List<Map<String, Object>> stories=new ArrayList<>();
stories.add(story1);
stories.add(story2);
Map<Integer, Map<String, Object>> combined = new HashMap<>();
stories.forEach((story) -> {
//Problem 2: because we don't know the type of the values we need a lot of nasty casts
Map<String, Object> combinedStory = combined.computeIfAbsent( (Integer)story.get( "id" ), k -> new HashMap<String, Object>() );
combinedStory.put("id", story.get( "id" ) );
combinedStory.put("title", story.get( "title" ) );
//Problem 3: the combined map would look a lot like your "story" maps but would contain different types
((List<String>)combinedStory.computeIfAbsent( "keyword", v -> new List<String>() )).add( (String)story.get("keyword") );
((List<String>)combinedStory.computeIfAbsent( "targeting", v -> new List<String>() )).add( (String)story.get("targeting") );
});
Using POJOs
Here's a greatly simplified example of how you'd do it with proper Java objects (POJOs). Note that those are meant to resemble your code as much as possible and there are a lot of other issues but addressing those would be way too much here and better designed code would be a lot larger and probably harder to understand - after all it's just meant to show you a difference.
First let's define our classes (for simplicity I made the fields public, you'd normally not do that):
class Story {
public final int id;
public String title;
public String keyword;
public String targeting;
public Story(int storyId) {
id = storyId ;
}
}
class MultiStory {
public final int id;
public String title;
public Set<String> keywords = new HashSet<>();
public Set<String> targetingInfo = new HashSet<>();
public MultiStory( int storyId ) {
id = storyId ;
}
}
Then let's reiterate the code above:
Story story1=new Story( 1 );
story1.title = "Onboarding";
story1.keyword = "new joinee";
story1.targeting = "finance";
Story story2=new Story( 1 );
story2.title = "Onboarding";
story2.keyword = "training";
story2.targeting = "HR";
List<Story> stories=new ArrayList<>();
stories.add(story1);
stories.add(story2);
Map<Integer, MultiStory> combined = new HashMap<>();
stories.forEach((story) -> {
MultiStory multiStory = combined.computeIfAbsent( story.id, v -> new MultiStory( story.id ) );
multiStory.title = story.title;
multiStory.keywords.add( story.keyword );
multiStory.targetingInfo.add( story.targeting );
});
As you can see, there are no casts needed and it's clear what fields are available (though not necessarily filled) which makes it easier to reason about the code and spot errors (the compiler can help a lot here which it couldn't to in the example that uses maps).
Here is a solution using classes to represent the story and tags:
public static void main(String[] args) {
TagsCollector app = new TagsCollector();
app.go();
}
private void go() {
List<Story> stories = createStories();
System.out.println(stories);
Map<Long, Tags> tagsById = collectTags(stories);
tagsById.forEach((aLong, tags) -> System.out.println(tags));
}
private List<Story> createStories() {
return Arrays.asList(
new Story(1, "Onboarding", "new joinee", "finance"),
new Story(1, "Onboarding", "training", "HR")
);
}
private Map<Long, Tags> collectTags(List<Story> stories) {
Map<Long, Tags> tagsById = new HashMap<>();
stories.forEach(s -> {
Tags tags = tagsById.computeIfAbsent(s.id, v -> new Tags(s));
tags.getKeywords().add(s.getKeyword());
tags.getTargetings().add(s.getTargeting());
});
return tagsById;
}
Class used to represent the Story:
public class Story {
private final long id;
private final String title;
private final String keyword;
private final String targeting;
public Story(long id, String title, String keyword, String targeting) {
this.id = id;
this.title = title;
this.keyword = keyword;
this.targeting = targeting;
}
public long getId() {
return id;
}
public String getTitle() {
return title;
}
public String getKeyword() {
return keyword;
}
public String getTargeting() {
return targeting;
}
#Override
public String toString() {
return String.format("Story %s, title=%s, keyword=%s, targeting=%s", id, title, keyword, targeting);
}
}
Class used to represent the Tags:
public class Tags {
private final long id;
private final String title;
private final List<String> keywords = new ArrayList<>();
private final List<String> targetings = new ArrayList<>();
Tags(Story story) {
this.id = story.id;
this.title = story.title;
}
public List<String> getKeywords() {
return keywords;
}
public List<String> getTargetings() {
return targetings;
}
#Override
public String toString() {
return String.format("Tags for id %s, title:%s: keywords=%s, targetings=%s", id, title, keywords, targetings);
}
}
Output
[Story 1, title=Onboarding, keyword=new joinee, targeting=finance, Story 1, title=Onboarding, keyword=training, targeting=HR]
Tags for id 1, title:Onboarding: keywords=[new joinee, training], targetings=[finance, HR]
Yes, you can do that with a Multimap. First I would define a pojo for Story in order to make things clearer:
public class Story {
private int id;
private String title;
private String keyword;
private String targeting;
//getters setters
}
Second you need to define a key with hashcode and equals.
public static class StoryKey {
private final int id;
private final String title;
public StoryKey(int id, String title) {
this.id = id;
this.title = title;
}
//getters
#Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
StoryKey storyKey = (StoryKey) o;
if (id != storyKey.id) return false;
return title != null ? title.equals(storyKey.title) : storyKey.title == null;
}
#Override
public int hashCode() {
int result = id;
result = 31 * result + (title != null ? title.hashCode() : 0);
return result;
}
The code will look like:
ArrayListMultimap<StoryKey, Story> multiMap = ArrayListMultimap.create();
List<Story> stories = new ArrayList();
Story story1 = new Story();
story1.setId(1);
story1.setTitle("Onboarding");
story1.setKeyword("training");
story1.setTargeting("HR");
Story story2 = new Story();
story2.setId(1);
story2.setTitle("Onboarding");
story2.setKeyword("new joinee,");
story2.setTargeting("finance");
stories.add(story1);
stories.add(story2);
System.out.println(stories);
stories.
forEach((story) -> {
multiMap.put(new StoryKey(story.getId(), story.getTitle()), story);
});
multiMap.keys().forEach(key ->
System.out.println(
"id =" + key.getId() +
" title =" + key.getTitle()+
"keyword =" + multiMap.get(key).stream().map(story->story.getKeyword()).collect(Collectors.toList()).toString()+
"targeting ="+ multiMap.get(key).stream().map(story->story.getTargeting()).collect(Collectors.toList()).toString())
);

How to use Java 8 Streams groupingBy to map multiple database records to a single object with a List<String> property for one column

My problem essentially comes down to this simplified example. I have data coming back from a DB which has some duplicate information in the rows.
In this example I have a list of TeamRow objects that come back from the DB. I can easily group these using Collectors.groupingBy:
public class TeamRow {
private int id;
private String name;
private String player;
public TeamRow(int id, String name, String player) {
this.id = id;
this.name = name;
this.player = player;
}
public int getId() {return id;}
public String getName() { return name; }
public String getPlayer() {return player;}
}
public class Team {
private int id;
private String name;
private List<String> players;
public Team(int id, String name, List<String> players) {
this.id = id;
this.name = name;
this.players = new ArrayList<String>(players);
}
}
List<TeamRow> dbTeams = new ArrayList<TeamRow>();
dbTeams.add(new TeamRow(1, "Team1", "Jonny"));
dbTeams.add(new TeamRow(1, "Team1", "Rob"));
dbTeams.add(new TeamRow(1, "Team1", "Carlos"));
dbTeams.add(new TeamRow(2, "Team2", "Shane"));
dbTeams.add(new TeamRow(2, "Team2", "Lucas"));
dbTeams.add(new TeamRow(3, "Team3", "Geraint"));
dbTeams.add(new TeamRow(3, "Team3", "Rocky"));
dbTeams.add(new TeamRow(3, "Team3", "Wayne"));
dbTeams.add(new TeamRow(3, "Team3", "Dwayne"));
dbTeams.add(new TeamRow(3, "Team3", "Lester"));
Map<Integer, List<TeamRow>> myMap = dbTeams.stream().collect(Collectors.groupingBy(TeamRow::getId));
However, what I'm actually trying to achieve is to convert the TeamRows to Teams. So that the id and name are only represented once and the players are stored in a List in the Team object. I can achieve this by adding a forEach over the map as shown below.
But I've been trying to figure out if there is a way I can achieve the same result by adding some sort of mapper or downstream collector. Would this even offer any benefit over adding a subsequent forEach ?? Eg:
List<Team> teams = dbTeams.stream().collect(Collectors.groupingBy(TeamRow::getId, ???), ???).???;
Conversion using forEach:
List<Team> teams = new ArrayList<>();
myMap.forEach((id, teamRows) -> {
if (teamRows.size() > 0) {
TeamRow tr = teamRows.get(0);
List<String> players = teamRows.stream().map(TeamRow::getPlayer).collect(Collectors.toList());
teams.add(new Team(id, tr.getName(), players));
}
});
Previously I said I would do it by creating an atomic transformer function like this:
Function<TeamRow, Team> getTeamRowTransformer() {
final Map<Integer, Team> map = new ConcurrentHashMap<Integer, Team>();
return (teamRow)->{
Team result = map.computeIfAbsent(teamRow.getId(), id->new Team(id, teamRow.getName(), Collections.emptyList()));
result.players.add(teamRow.getPlayer());
return result;
};
}
It handles the mapping and your stream code becomes one very legible step:
Set<Team> finalTeams = dbTeams.stream()
.map(getTeamRowTransformer())
.collect(Collectors.toSet());
HOWEVER, I realized, you can also do this:
List<Team> teams = dbTeams.stream()
.map(tr->new Team(tr.getId(), tr.getName(), Arrays.asList(tr.getPlayer())))
.collect(Collectors.collectingAndThen(
Collectors.groupingBy(t->t.id,
Collectors.reducing((Team a, Team b)->{
a.players.addAll(b.players);
return (Team)a;
})
), m->m.values().stream()
.filter(Optional::isPresent)
.map(Optional::get)
.collect(Collectors.toList())
)
);
This way you never have an accessible mutable collection until List<Team> teams is assigned.
You may use toMap collector with custom merge function. It's probably a good idea to add merge method to the Team class:
public class Team {
private final int id;
private final String name;
private final List<String> players;
public Team(int id, String name, List<String> players) {
this.id = id;
this.name = name;
this.players = new ArrayList<>(players);
}
// merges other team into this team, returning this team
public Team merge(Team other) {
assert id == other.id; // remove asserts if you don't like them
assert name.equals(other.name);
players.addAll(other.players);
return this;
}
}
Now you can solve your problem this way:
Collection<Team> teams = dbTeams.stream()
.map(tr -> new Team(tr.id, tr.name, Arrays.asList(tr.player)))
.collect(Collectors.toMap(t -> t.id, t -> t, Team::merge)).values();
You could try something like
List<Team> teamList = dbTeams.stream().collect(Collectors.collectingAndThen(Collectors.groupingBy(TeamRow::getId),
(m -> m.entrySet().stream().map(
e -> {
List<TeamRow> l = e.getValue();
return new Team(l.get(0).getId(), l.get(0).getName(), l.stream().map(TeamRow::getPlayer).collect(Collectors.toList()));
}
).collect(Collectors.toList()))));
Using collectingAndThen() you can use a function which maps the entries of the map to Teams. l.get(0) should not fail as there is always at least one entry in the list.
I am not sure if this is more concise, but at least it does not use foreach.

Compilation problem when working with java.util.Map

I am trying to compile this program. It works perfectly for 2 Strings(Name, phone number) But not for 3 Strings (Name, phone number and sex).
CODE (Not working code - 3 Strings (Name, phone number and sex))
import java.util.Map;
import java.util.TreeMap;
public class Ann {
String name, phone;
public Ann() {
}
public static void testMap() {
Map<String, String, String> theMap = new TreeMap<String, String,String>();
// new HashMap<K,V>(); could also be used
theMap.put("Roger M", "090-997-2918", "Male");
theMap.put("Jane M", "090-997-1987", "FeMale");
theMap.put("Stacy K", "090-997-9188", "FeMale");
theMap.put("Gary G", "201-119-8765", "Male");
theMap.put("Jane M", "090-233-0000", "FeMale");
System.out.println("Testing TreeMap and Map");
System.out.print("Stacy K has phone ");
System.out.print(theMap.get("Stacy K"));
System.out.print("\n");
System.out.print("Jane M has phone ");
System.out.print(theMap.get("Jane M"));
} // testMap()
public static void main(String[] args) {
testMap();
}
}
ERROR
wrong number of type arguments; required 2
wrong number of type arguments; required 2
WORKING CODE (For 2 Strings (Name, phonenumber))
import java.util.Map;
import java.util.TreeMap;
public class Ann {
String name, phone;
public Ann() {
}
public static void testMap() {
Map<String, String> theMap = new TreeMap<String, String>();
// new HashMap<K,V>(); could also be used
theMap.put("Roger M", "090-997-2918");
theMap.put("Jane M", "090-997-1987");
theMap.put("Stacy K", "090-997-9188");
theMap.put("Gary G", "201-119-8765");
theMap.put("Jane M", "090-233-0000");
System.out.println("Testing TreeMap and Map");
System.out.print("Stacy K has phone ");
System.out.print(theMap.get("Stacy K"));
System.out.print("\n");
System.out.print("Jane M has phone ");
System.out.print(theMap.get("Jane M"));
} // testMap()
public static void main(String[] args) {
testMap();
}
}
I want the code to work for about 5 attributes like name , phone, sex,age,address. If someone can help me compile the code at the top of the question, I can figure out the rest.
Thanks
You can't just add type parameters arbitrarily to generic types - they are defined with a certain number, and have to use that many (disregarding raw types). The type parameters have specific meanings for the implementation - how would the HashMap class know what you wanted to get out if you called map.get(name)?
You should encapsulate all the properties into a class (e.g. Person or Contact) and then create a Map<String, Person> to map from the name to the person. For example:
public enum Gender
{
FEMALE, MALE;
}
public final class Person
{
private final String name;
private final Gender gender;
private final Date dateOfBirth;
private final String address;
private final String telephone;
public Person(String name, Gender gender, Date dateOfBirth,
String address, String telephone)
{
// You probably want to put some validation in here
this.name = name;
this.gender = gender;
this.dateOfBirth = dateOfBirth;
this.address = address;
this.telephone = telephone;
}
public String getName()
{
return name;
}
// etc for the other properties
}
...
Map<String, Person> map = new HashMap<String, Person>();
Person jon = new Person("Jon", Gender.MALE, /* etc */);
map.put("Jon", jon);

Categories