1

I currently have a three dimensional list in my Java project. I am using it to store the following. I have a list of Pairs. A Pair has a gender (male, female or mixed) and a food preference (meat, veggie or vegan).

In my code I have to access sometimes only the pairs which have veggie as food preference and are male or other combinations. I first thought that a three dimensional list would be the best to do this. My 3D list looks like the following:

ArrayList<ArrayList<ArrayList<Pair>>> pairsSplitUp;

The list is filled with values such that if we access it we get the following:

  • Index 0: Index 0: all Pairs that are male and have meat as food Preference
  • Index 0: Index 1: all Pairs that are female and have meat as food Preference
  • Index 0: Index 2: all Pairs that are mixed and have meat as food Preference
  • Index 1: Index 0: all Pairs that are male and have veggie as food Preference
  • ...
  • Index 2: Index 0: all Pairs that are male and have vegan as food Preference
  • ...

I first thought that this might be the best way to store this data, but then I read this question, where it is stated that if you are implementing a 3D list you are probably not handling your data right. In the mentioned question there is also an answer where a Map is used to store the data, but I am not really understanding this abstraction and I don't know how it could be used for my case. Because of this I wanted to ask, if there is a better, cleaner way for storing my data such that I can still access only the Pairs which are male and vegan etc.

In the code there exists a ArrayList<Pair> pairList which holds all the Pairs. The Pair class looks like the following:

public class Pair {
    private Person person1;
    private Person person2; 
    private String foodPreference;
    private String gender;
    
    public Pair(Person person1, Person person2) {
        this.person1 = person1;
        this.person2 = person2;
        this.foodPreference = decideFoodPreference();
        this.gender = decideGender();
    }
 
    /*
      contains Getter for the fields and methods to decide and set the foodPreference
      and the gender.
    */

}

The Person class contains information about a specific Person like the name, age, gender, location and so on.

Is there a better, cleaner way to do the storing of the Pairs, where I can still get only the Pairs which have foodPreference x and gender y or is my way with the three dimensional list the best way for my specific data?

David Krell
  • 230
  • 1
  • 10
  • Did I understood correctly, that foodPreference+gender are attributes of a couple, not attributes of a single person? – gdomo Jun 16 '23 at 22:09
  • If so, does Person->gender attribute also exists and does it correlate with Pair's gender? – gdomo Jun 16 '23 at 22:23
  • Would be easier to define right abstractions with some fundamental questions: 1) What do the dimensions signify? 2) Why do you need to store Pair instances in dimensions? In other words, why can these dimensional filters not apply on demand? 3) What are read/write use cases? – sankalpn Jun 16 '23 at 22:26
  • @gdomo Yes the foodPreference and gender attributes are attributes of a Pair. A person also has a gender and a foodPreference. If both persons in the Pair are male then the gender is male, if both are female then the gender is female and if both are mixed then the gender is mixed. For the foodPreference: if the foodPreference from both persons is equal than the foodPreference of the Pair is the foodPreference from one of the Persons. If its (vegan, veggie) than its vegan. Pairs with one meat and one veggie/vegan person are not existing. – David Krell Jun 16 '23 at 22:31
  • @sankalpn I am not sure if I understand the questions correctly. 1) The dimensions are not really signifying anything. I just have a list of Pairs and have to split it such that I can access for example Pairs that are meat eaters and are male. 2) I thought that it might not be good to apply the filters each time I have to access the lists, because I have to access it in numerous methods. 3) Let's say that I have the 3D List like presented, then I never add a Pair to these lists. But I remove Pairs in one method, but the other methods still need the list from the beginning. – David Krell Jun 17 '23 at 09:45
  • @DavidKrell then it might be simpler to have a wrapper class, say PairManager which has a List allPairs and a cache List maleMeatEaters. First use, you filter, next use you only return from cache. That way, cache can also be transparent to callers. You add or remove pairs with a call to PairManager class and that can internally invalidate cache. – sankalpn Jun 19 '23 at 23:18

4 Answers4

1

Should be able to use a single List<Pair>, and apply various filters to get a subset of relevant pairs.

For example:

List<Pair> allPairs = List.of(new Pair(...), 
                              new Pair(...), 
                              new Pair(...),
                              ....);

List<Pair> allMaleCarnivores = allPairs.stream()
                                  .filter(p -> p.getPerson1().isCarnivore() 
                                                 && p.getPerson2().isCarnivore()
                                                 && p.getPerson1().isMale()
                                                 && p.getPerson2().isMale()
                                          )
                                  .toList();


 // then do something with the subset
 allMaleCarnivores.forEach(p -> eatSalad(p);
Andrew S
  • 2,509
  • 1
  • 12
  • 14
  • According to the code snippet, foodPreference and gender are a pair attributes, not a person's. If so, answer should be corrected a bit, however it doesn't affect the entire approach – gdomo Jun 16 '23 at 22:10
  • I thought of this as well, but I have to access the sublists in different methods. For that I could maybe introduce 12 fields, because I don't want to pass 12 lists to each method. But the problem is that I am removing elements from the lists, but another method still needs the full list? I could just make a copy of every list on the beginning of the method where I am removing but is this a good approach? Because then I am using 12 lines just for copying and somehow this doesn't look like the best approach. – David Krell Jun 17 '23 at 09:48
  • Or I could just readd the removed Pairs at the end of the method, where they are removed, but I don't know if this is an elegant way. But I will just try different forms of your approach and will look how it works. Nonetheless I thank you very much for your answer. – David Krell Jun 17 '23 at 10:16
1

While @Andrew S answer works, I could also suggest to use a Map for quick retrieve of a desired Pairs. It would take constant (O(1)) time to look up instead of filtering the whole list (O(n)). The flip side is extra memory consumption.

To do that you have to extract a kind of PairAttributes class which holds foodPreference and gender with properly defined equals and hashCode methods. Use lombok or auto-generation by IDE. Don't forget to update them in case of new attributes.

public static class PairAttributes {
    private final String foodPreference;
    private final String gender;

    public PairAttributes(Pair pair) {
        this.foodPreference = pair.foodPreference;
        this.gender = pair.gender;
    }

    public PairAttributes(String foodPreference, String gender) {
        this.foodPreference = foodPreference;
        this.gender = gender;
    }

    // getters

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        PairAttributes that = (PairAttributes) o;
        return foodPreference.equals(that.foodPreference) && gender.equals(that.gender);
    }

    @Override
    public int hashCode() {
        return Objects.hash(foodPreference, gender);
    }
}

Now, just put given Pairs to a HashMap with PairAttributes as a key:

one by one

    private final Map<PairAttributes, List<Pair>> pairsByAttributes = new HashMap<>();
    ...
    pairsByAttributes.computeIfAbsent(new PairAttributes(somePair), p -> new ArrayList<>()).add(somePair);

or if given a whole list:

List<Pair> allPairs = List.of(new Pair(...),
        new Pair(...),
        new Pair(...),
        ...
);

Map<PairAttributes, List<Pair>> pairsByAttributes = allPairs
        .stream().
        collect(Collectors.groupingBy(PairAttributes::new));

then to get desired pairs just get them:

List<Pair> veggieMales = pairsByAttributes.get(
        new PairAttributes("veggie", "male")
);
gdomo
  • 1,650
  • 1
  • 9
  • 18
  • This looks like a good approach! Can you maybe explain the usage of the hashCode() method? I know that the equals method just checks if two instances of the class are equal but what is the hashCode() method used for? And just to check if I understood correctly: From my point of view, this will still work even if a Pair has more attributes like the ones I showed in my question, because the only important attributes for the PairAttributes class are foodPreference and gender. – David Krell Jun 17 '23 at 09:57
  • And just to be sure: With *"Don't forget to update them in case of new attributes."* You mean only new attributes that will decide how the list is split up? So only new attributes for the PairAttribute class or? If for example my Pair has the following field ```Double[] location``` but this is not relevant for accessing all Pairs that are meat eaters and male then I don't have to change something in the PairAttribute class? – David Krell Jun 17 '23 at 09:59
  • 1
    @DavidKrell [Here](https://www.baeldung.com/java-equals-hashcode-contracts#hashcode) is an article on hashCode. In a nutshell, you need hashCode+equals (must be always overridden consistently) to make `Map` works. When you put/get into/from HashMap it uses calculated hashCode end equals to instantly navigate to the desired map entry. Search for HashMap implementation explanation for more details if you are interested – gdomo Jun 17 '23 at 12:37
  • 1
    @DavidKrell yep. Include in PairAttributes and it's equals/hashcode only attributes according to which you split pairs, and according to which you are looking up for list of pairs. You are free to add any additional fields to a Person and a Pair if they don't affect search for pairs logic. – gdomo Jun 17 '23 at 12:42
  • I used your method and it works very good. I made the HashMap with a list of Pairs. I now want to remove pairs from the Map, the Pairs could be from every combination of attributes. What would be more efficient? To remove the pairs from the pair list from which I made the map in the first place or to iterate over the pairs which I want to remove, then get the corresponding list from the Map and remove the Pair from that list? – David Krell Jun 18 '23 at 19:33
  • 1
    If I understood correctly, first approach would work as is: manipulating with initial list of all Pairs from which the map was build wouldn't affect the map. You have to rebuild the map from updated list then. So I suggest get list from map for each Pair you want to remove by their attributes and remove a Pair from that list. – gdomo Jun 18 '23 at 19:58
  • Thank you, and rebuilding the map with the manipulated list of all Pairs would not be as efficient as just removing the Pairs from the lists in the map, or? – David Krell Jun 18 '23 at 20:09
  • 1
    Yes, rebuilding the map is less efficient in general. Also consider using LinkedList instead of ArrayList as it performs removal from the list more efficient. Also be careful, since your Pair class has no equals/hashCode you have to remove exactly that objects from which you built the map. Not `new Pair(..)` even with exactly same fields values. – gdomo Jun 18 '23 at 20:30
1

"... I read this question, where it is stated that if you are implementing a 3D list you are probably not handling your data right. ..."

More or less; typically, 3-dimensional arrays are used to represent mathematical matrices.

"... In the mentioned question there is also an answer where a Map is used to store the data, but I am not really understanding this abstraction and I don't know how it could be used for my case. ..."

In computer science, a map structure—sometimes referred to as a dictionary, or an associative array—it's simply a list of values, each of which are mapped to separate keys.  Similar to a literal dictionary, which has a word and a definition.

Here is the Java Tutorial on using the Map collection.
The Map Interface (The Java™ Tutorials > Collections > Interfaces).

Here is a basic example.

Map<String, Integer> map = new HashMap<>();
map.put("abc", 123);
map.put("def", 456);

And, you can retrieve values using the "key".

map.get("abc");

... Is there a better, cleaner way to do the storing of the Pairs, where I can still get only the Pairs which have foodPreference x and gender y or is my way with the three dimensional list the best way for my specific data?"

Yes, I'll demonstrate; you can use a single ArrayList.

Consider the following objects, to hold your data.

class Pair {
    Person personA, personB;
}

class Person {
    String name, location;
    int age;
    Gender gender;
    FoodPreference foodPreference;
}

enum Gender {
    MALE, FEMALE, MIXED
}

enum FoodPreference {
    MEAT, VEGGIE, VEGAN
}

From here, you can create new constructors for both Person, and Pair.
Additionally, here I added a toString method, as a way to debug the data.

class Pair {
    Person personA, personB;

    public Pair(Person personA, Person personB) {
        this.personA = personA;
        this.personB = personB;
    }
}

class Person {
    String name, location;
    int age;
    Gender gender;
    FoodPreference foodPreference;

    public Person(Gender gender, FoodPreference foodPreference) {
        this.gender = gender;
        this.foodPreference = foodPreference;
    }

    @Override
    public String toString() {
        return name;
    }
}

Here is an example of adding new items to the list.

List<Pair> list = new ArrayList<>();

Person personA = new Person("personA", 1, Gender.MALE);
personA.foodPreference = FoodPreference.MEAT;
Person personB = new Person("personB", 2, Gender.FEMALE);
personB.foodPreference = FoodPreference.VEGGIE;
list.add(new Pair(personA, personB));

Person personC = new Person("personC", 3, Gender.MALE);
personC.foodPreference = FoodPreference.MEAT;
Person personD = new Person("personD", 4, Gender.FEMALE);
personD.foodPreference = FoodPreference.VEGAN;
list.add(new Pair(personC, personD));

And, here is an example of acquiring Person objects, with a specific value.

List<Person> find(List<Pair> list, FoodPreference foodPreference, Gender gender) {
    List<Person> matches = new ArrayList<>();
    Person person;
    for (Pair pair : list) {
        person = pair.personA;
        if (person.foodPreference == foodPreference && person.gender == gender)
            matches.add(person);
        person = pair.personB;
        if (person.foodPreference == foodPreference && person.gender == gender)
            matches.add(person);
    }
    return matches;
}

And, usage.

List<Person> matches = find(list, FoodPreference.MEAT, Gender.MALE);

Output

personA
personC
Reilas
  • 3,297
  • 2
  • 4
  • 17
  • Thank you for your answer, this also looks like a good approach, but I think you somehow swapped a few things. In my project a **Pair** has a foodPreference and a Gender. I explained this more in detail in this [comment](https://stackoverflow.com/questions/76493704/how-to-refactor-a-three-dimensional-list/76494147#comment134874735_76493704). So I want the Pairs with a specific food Preference and gender, but I still understand the general idea. In my opinion it's quite similar to the approach of @AndrewS. So we are just generating a list for each combination of foodPreference and gender. – David Krell Jun 17 '23 at 10:09
  • You can also take a look at my comment [here](https://stackoverflow.com/questions/76493704/how-to-refactor-a-three-dimensional-list/76494147#comment134877327_76493789). And then I have the problem that I always have to carry 12 lists around. I think I will combine your approach with Enums and the approach from @gdomo with the map, because then I just have a single Map where everything is stored. – David Krell Jun 17 '23 at 10:17
0

After some discussion with OP, I think we have a layer of abstraction missing. Not including any necessary synchronized keywords, as I don't yet know all details.

public class PairManager {
    List<Pair> allPairs;

    // Add any cache worthy queries
    List<Pair> maleMeatEaters;

    public addPair(Pair newPair) {
        allPairs.add(newPair);
        // invalidate caches
        maleMeatEaters = null;
    }

    public removePair(Pair pair) {
        allPairs.remove(pair)
        // invalidate caches
        maleMeatEaters = null;
    }

    public List<Pair> listMaleMeatEaters() {
        return maleMeatEaters == null ? filterMaleMeatEaters() : maleMeatEaters;
    }

    private List<Pair> filterMaleMeatEaters() {
        maleMeatEaters = allPairs.stream()
            .filter(pair -> pair.getGender() == Gender.MALE)
            .filter(pair -> pair.getFoodPreference() == FoodPreference.MEAT)
            .toList(); // pre-Java17 .collect(Collectors.toList());
        return maleMeatEaters;
    }
}
sankalpn
  • 76
  • 6