views:

76

answers:

4

Hi. I'm looking for a Collection type data structure to implement the following. Say I have a class like this:

class Person() {

    String homeTown;   // key
    String sex;  // key 
    String eyeColour;  // key
    String name;
    long height;

    // other stuff....
}

I am processing multiple Person objects. I want to organise them into sets whereby each set contains Person objects with the same homeTown, sex and eyeColour. At the moment I am implementing something like this:

Map<String, HashSet<Person>> = new HashMap<String, HashSet<Person>>;

where the key is a concatanation of the homeTown, sex and eyeColour. This works but seems a bit untidy - can anyone suggest a more elegant solution or a better data structure to use, thanks?

A: 

You can use guava's Sets.filter method to filter the person objects.

Example:

Person class:

public class Person {
 String name;
 String hometown;
 int age;

 public Person(String name, String hometown, int age) {
  this.name = name;
  this.age = age;
  this.hometown = hometown;
 }

 @Override
 public int hashCode() {
  int hash = 17;
  hash = 37 * hash + name.hashCode();
  hash = 37 * hash + hometown.hashCode();
  hash = 37 * hash + age;
  return hash;
 }

 @Override
 public boolean equals(Object obj) {
  if (this == obj)
   return true;
  Person p;
  if (obj instanceof Person)
   p = (Person) obj;
  else
   return false;

  if (this.name.equals(p.name) && this.hometown.equals(p.hometown)
    && this.age == p.age)
   return true;

  return false;
 }

 @Override
 public String toString() {
  StringBuilder b = new StringBuilder();
  b.append("[name = ").append(name).append("\n");
  b.append("home town = ").append(hometown).append("\n");
  b.append("age = ").append(age).append("]");
  return b.toString();
 }

}

TestGuavaFilter class:

public class TestGuavaFilter {
 public static void main(String[] args) {
  Set<Person> set = new HashSet<Person>();

  set.add(new Person("emil", "NY", 24));
  set.add(new Person("Sam", "NY", 50));
  set.add(new Person("george", "LA", 90));
  System.out.println(Sets.filter(set, new FilterHomeTown("NY")));
 }
}

class FilterHomeTown implements Predicate<Person> {
 String home;

 public FilterHomeTown(String home) {
  this.home = home;
 }

 @Override
 public boolean apply(Person arg0) {

  if (arg0.hometown.equals(this.home))
   return true;
  return false;
 }

}

The advantage of using a filter is that you can filter Person object in any way ,suppose you want to filter only using home-town and not the other 2 attributes this will helpful.More over since guava's filter only produces a view of the the real Set,you can save memory.

Emil
Thank Emil - not sure if this suits me though. You're passing in new FilterHomeTown("NY") - I'm looking to group elements together that have the same homeTown, not all elements where the homeTown is "NY" or "LA" or whatever. Or have I misunderstood?
dairemac
+4  A: 

You could restructure your class to make the key explicit. This is more robust than simply concatenating the key values and avoids any additional object creation overhead at the point when you wish to store your Person instance in a Map, because you've eagerly created the key in advance.

public class Person {
  public class Key {
    private final String homeTown;
    private final String sex;
    private final String eyeColour;

    public Key(String homeTown, String sex, String eyeColour) { ... }

    public boolean equals(Object o) { /* Override to perform deep equals. */ }
    public int hashCode() { /* Could pre-compute in advance if the key elements never change. */ }
  }

  private final Key key;
  private final String name;
  private final long height;

  public Person(String homeTown, String sex, String eyeColour, String name, long height) {
    this.key = new Key(homeTown, sex, eyeColour);
    this.name = name;
    this.height = height;
  }

  public Key getKey() {
    return key;
  }

  public String getName() { return name; }
  public long getHeight() { return height; }
}
Adamski
yep, that makes sense, thanks
dairemac
I should also point out that using Strings for things like sex and eyeColour is probably a bad idea; you should consider using enums. Similarly you might want to check that height is non-negative during instantiation and also perhaps rename it to something explicit; e.g. "heightInCentimetres".
Adamski
It's just an example I made up for the sake of this question dude!
dairemac
@dairemac a quick pointer, store the hash as a one of your field and change the hashCode method to see if the hash was already computed, otherwise you'll do all the math over and over.
XecP277
+2  A: 

Create an object to model your key. For example class PersonKey { String homeTown, sex, eyeColour } (getters and setters omitted for brevity)

Implement the equals and hashCode method for this object.

Use this object as the key in your Map.

Either remove the attributes from your Person object or replace them with a reference to your PersonKey object.

In addition, consider making the type of your map the following i.e. you don't need to specify what type of Set you are using as the key to your map.

Map<String, Set<Person>> = new HashMap<String, Set<Person>>();

And, if you are using a Set<Person> then you'll need to override equals and hashCode for the Person as well, otherwise the Set cannot correctly determine if two Person objects represent the same person or not, which is needed to make sure the collection contains only unique elements.

Adrian Smith
thanks Adrian, same suggestion (more or less) as Adamski which I have implemented
dairemac
A: 

org.apache.commons.collections.map.MultiValueMap

foret
Hi foret, thanks for the suggestion. Had a look at the API for that. I don't think it's what I'm looking for in - it uses a Collection as the Map value but it doesn't give me a better way of implementing the Map key which is what I was asking. I could use it along with the Key solution suggested by Adamski and Adrian Smith I suppose.
dairemac