Which collection to use? - java

What kind of collection should I use if I need to create a collection that will allow me to store books and how many copies there are in circulation (for a library)?
I would use an ArrayList, but I also want to be able to sort the books by order of issue year.

You can create a Book Class with all the attributes you have for a book. And implement a Comparable for that Book Class and write sorting logic in there.
Maintain a List<Book>, and use Collections.sort method, to sort your List according to the implemented Sorting logic.
UPDATE: -
As far as, fast look-up is concerned, a Map is always the best bet. And is appropriate to implement a dictionary look-up kind of structure. For that, you would need some attribute that uniquely identifies each book. And then store your book as Map<String, Book>, where your key might be id of type String.
Also, in this case, your sorting logic will change a little. Now you would have to sort on the basis of your Map's value, i.e. on the basis of attributes of Book.
Here's a sample code you can make use of. I have just considered sorting on the basis of id. You can change the sorting logic as needed: -
class Book {
private int id;
private String title;
public Book() {
}
public Book(int id, String title) {
this.id = id;
this.title = title;
}
#Override
public String toString() {
return "Book[Title:" + this.getTitle() + ", Id:" + this.getId() + "]";
}
// Getters and Setters
}
public class Demo {
public static void main(String[] args) {
final Map<String, Book> map = new HashMap<String, Book>() {
{
put("b1", new Book(3, "abc"));
put("b2", new Book(2, "c"));
}
};
List<Map.Entry<String, Book>> keyList = new LinkedList<Map.Entry<String, Book>>(map.entrySet());
Collections.sort(keyList, new Comparator<Map.Entry<String, Book>>() {
#Override
public int compare(Map.Entry<String, Book> o1, Map.Entry<String, Book> o2) {
return o1.getValue().getId() - o2.getValue().getId();
}
});
Map<String, Book> result = new LinkedHashMap<String, Book>();
for (Iterator<Map.Entry<String, Book>> it = keyList.iterator(); it.hasNext();) {
Map.Entry<String, Book> entry = it.next();
result.put(entry.getKey(), entry.getValue());
}
System.out.println(result);
}
}
OUTPUT: -
"{b2=Book[Title:c, Id:2], b1=Book[Title:abc, Id:3]}"

Well, If the entire purpose of your collection is to store the counts of the books, than a dictionary/map, or whatever java's key-value collection is called.
It would probably have title as your key, and the count as your value.
Now I suspect that your collection might be a little more complicated than that, so you might want to make a Book class which has Count as a field, and then I'd probably have a string -> Book dictionary/map anyway, with the string as it's dewy decimal number or some other unique identifier.

Beyond a simple educational or toy project, you'd want to use a database rather than an in-memory collection. (Not really an answer, but I think worth stating.)

java.util.TreeMap can be used to index and sort this kind of requirements.
Check http://docs.oracle.com/javase/6/docs/api/java/util/TreeMap.html for more details.
You can use your Book object as key mapped to the number of copies as the value.

Related

Group objects in list by multiple fields

I have a simple object like this
public class Person {
private int id;
private int age;
private String hobby;
//getters, setters
}
I want to group a list of Person by attributes
Output should be like this
Person count/Age/Hobby
2/18/Basket
5/20/football
With a chart for more understanding
X axis : hobby repartition
Y axis : count of person distribution
Colors represents age
I managed to group by one attribute using map, but I can't figure how to group by multiples attributes
//group only by age . I want to group by hobby too
personMapGroupped = new LinkedHashMap<String, List<Person>>();
for (Person person : listPerson) {
String key = person.getAge();
if (personMapGroupped.get(key) == null) {
personMapGroupped.put(key, new ArrayList<Person>());
}
personMapGroupped.get(key).add(person);
}
Then I retrieve the groupable object like this
for (Map.Entry<String, List<Person>> entry : personMapGroupped .entrySet()) {
String key = entry.getKey();// group by age
String value = entry.getValue(); // person count
// I want to retrieve the group by hobby here too...
}
Any advice would be appreciated.
Thank you very much
Implement methods for comparing people according to the different fields. For instance, if you want to group by age, add this method to Person:
public static Comparator<Person> getAgeComparator(){
return new Comparator<Person>() {
#Override
public int compare(Person o1, Person o2) {
return o1.age-o2.age;
}
};
}
Then you can simply call: Arrays.sort(people,Person.getAgeComparator()) or use the following code to sort a Collection:
List<Person> people = new ArrayList<>();
people.sort(Person.getAgeComparator());
To sort using more than one Comparator simultaneously, you first define a Comparator for each field (e.g. one for age and one for names). Then you can combine them using a ComparatorChain. You would use the ComparatorChain as follows:
ComparatorChain chain = new ComparatorChain();
chain.addComparator(Person.getNameComparator());
chain.addComparator(Person.getAgeComparator());
You could simply combine the attributes to a key.
for (Person person : listPerson) {
String key = person.getAge() + ";" + person.getHobby();
if (!personMapGrouped.contains(key)) {
personMapGrouped.put(key, new ArrayList<Person>());
}
personMapGrouped.get(key).add(person);
}
The count of entries is easy to determine by using personMapGrouped.get("18;Football").getSize().
I'm not sure about your requirements, but I'd probably use multiple maps (Google Guava's Multimap would make that easier btw) and sets, e.g. something like this:
//I'm using a HashMultimap since order of persons doesn't seem to be relevant and I want to prevent duplicates
Multimap<Integer, Person> personsByAge = HashMultimap.create();
//I'm using the hobby name here for simplicity, it's probably better to use some enum or Hobby object
Multimap<String, Person> personsByHobby = HashMultimap.create();
//fill the maps here by looping over the persons and adding them (no need to create the value sets manually
Since I use value sets Person needs a reasonable implementation of equals() and hashCode() which might make use of the id field. This also will help in querying.
Building subsets would be quite easy:
Set<Person> age18 = personsByAge.get(18);
Set<Person> basketballers = personsByHobby.get( "basketball" );
//making use of Guava again
Set<Person> basketballersAged18 = Sets.intersection( age18, basketballers );
Note that I made use of Google Guava here but you can achieve the same with some additional manual code (e.g. using Map<String, Set<Person>> and manually creating the value sets as well as using the Set.retainAll() method).

Sort a List of Map<String, String> [duplicate]

This question already has answers here:
Sort a Map<Key, Value> by values
(64 answers)
Closed 8 years ago.
I saw this thread sorting a List of Map<String, String> and I know mine could sound a duplicate, but it is slight differen.
My example is:
List<Map<String, String>> myList = new ArrayList<Map<String, String>>();
...
for(MyClass1 c1 : c1)
{
...
for(MyClass2 c2 : c12)
{
SimpleBindings myBindindings= new SimpleBindings();
myBindindings.put(c1.getName(), c2.getName());
myList.add(myBindindings);
}
}
...
Concretely I can have
{
(John, Mike)
(John, Jack)
(Sam, Jack)
(Gloria, Anna)
(Jane, Carla)
...
}
and would like that my list is sorted by the maps key:
{
(Gloria, Anna)
(Jane, Carla)
(John, Mike)
(John, Jack)
(Sam, Jack)
...
}
Are you sure that
List<Map<String, String>>
is the approriate data type you want?
To me it looks like you are in fact looking simplify for
TreeMap<String, String>
i.e. a sorted map key -> value?
Or do you mean to use a List<StringPair> (for that, please choose a more appropriate name than StringPair, and implement that class to your needs)? I have the impression that in lack of an obvious Pair<String, String> class in Java you have been abusing SimpleBinding as a pair class. The proper way to have pairs in Java is to implement a new class, with a proper class name - "pair" is technical, not semantic.
You could also do
List<String[]>
and implement a Comparator<String[]> for sorting. But that doesn't save you any work over implementing a NamePair class and making it comparable yourself.
You need to implement Comparator to accomplish this...
Collections.sort(myList, new Comparator<ObjectBeingCompared>() {
#Override
public int compare(ObjectBeingCompared obj1, ObjectBeingCompared obj2) {
//Of course you will want to return 1, 0, -1 based on whatever you like
//this is just a simple example
//return 1 if obj1 should be ordered first
//return 0 if obj1 and obj2 are the same
//return -1 if obj1 should be ordered after obj2
return obj1.compareTo(obj2);
}
});
The HashMap data structure is used to allow access to its elements in O(1) time.
Because it is a container of data its pool or keys can vary in time. This mean that you can not assure in long therm an order for list of maps.
In your example you match two Strings and create Pair of data called SimpleBindings.
In case of your simple example you should not use Map<String,String> data structure to represent a Pair of data.
If you SimpleBindings really consist of two string, everything you must do is only implement a Comparable in SimpleBindings class like this:
class SimpleBinding implements Comparable<SimpleBinding> {
private final String key;
private final String value;
public SimpleBinding(String key, String value) {
Objects.nonNull(key);
Objects.nonNull(value);
this.key = key;
this.value = value;
}
#Override
public int compareTo(SimpleBinding that) {
return this.key.compareTo(that.key);
}
}
And the you just use the Collections.sort(bindings ) to have sorted result.
In case you do not have access to the class you should use the Comparator interface like this
enum SimpleBindingComparator implements Comparator<SimpleBinding> {
DEFUALT {
#Override
public int compare(SimpleBinding fist, SimpleBinding second) {
return fist.key.compareTo(second.key);
}
};
Then you sort your bindings like this Collections.sort(bindings ,SimpleBindingComparator.DEFAULT);
But if your case is more complex than this and your store a Map in the list you should define a logic that represent the order. In your case it can be sad that the order must maintained by c1.getName()
One choice is that you should not create a List but a map of list Map<String>,List<String>> this is so called multi map where a single key matches to multiple values. See MultiMap of guava and if you want it to be sorted then i propose to read about TreeMultiMap

TreeMap filtered view performance

I have a class that has (among other things):
public class TimeSeries {
private final NavigableMap<LocalDate, Double> prices;
public TimeSeries() { prices = new TreeMap<>(); }
private TimeSeries(NavigableMap<LocalDate, Double> prices) {
this.prices = prices;
}
public void add(LocalDate date, double price) { prices.put(date, price); }
public Set<LocalDate> dates() { return prices.keySet(); }
//the 2 methods below are examples of why I need a TreeMap
public double lastPriceAt(LocalDate date) {
Map.Entry<LocalDate, Double> price = prices.floorEntry(date);
return price.getValue(); //after some null checks
}
public TimeSeries between(LocalDate from, LocalDate to) {
return new TimeSeries(this.prices.subMap(from, true, to, true));
}
}
Now I need to have a "filtered" view on the map where only some of the dates are available. To that effect I have added the following method:
public TimeSeries onDates(Set<LocalDate> retainDates) {
TimeSeries filtered = new TimeSeries(new TreeMap<> (this.prices));
filtered.dates().retainAll(retainDates);
return filtered;
}
The onDates method is a huge performance bottleneck, representing 85% of the processing time of the program. And since the program is running millions of simulations, that means hours spent in that method.
How could I improve the performance of that method?
I'd give ImmutableSortedMap a try, assuming you can use it. It's based on a sorted array rather then a balanced tree, so I guess its overhead is much smaller(*). For building it, you need to employ biziclop's idea as the builder supports no removals.
(*) There's a call to Collection.sort there, but it should be harmless as the collection is already sorted and TimSort is optimized for such a case.
In case your original map doesn't change after creating onDates, maybe a view could help. In case it does, you'd need some "persistent" map, which sounds rather complicated.
Maybe some hacky solution based on sorted arrays and binary search could be fastest, maybe you could even convert LocalDate first to int and then to double and put everything into a single interleaved double[] in order to save memory (and hopefully also time). You'd need your own binary search, but this is rather trivial.
The view idea is rather simple, assuming that
you don't need all NavigableMap methods, but just a couple of methods
the original map doesn't change
only a few elements are missing in retainDates
An example method:
public double lastPriceAt(LocalDate date) {
Map.Entry<LocalDate, Double> price = prices.floorEntry(date);
while (!retainDates.contains(price.getKey()) {
price = prices.lowerEntry(price.getKey()); // after some null checks
}
return price.getValue(); // after some null checks
}
The simplest optimisation:
public TimeSeries onDates(Set<LocalDate> retainDates) {
TreeMap<LocalDate, Double> filteredPrices = new TreeMap<>();
for (Entry<LocalDate, Double> entry : prices.entrySet() ) {
if (retainDates.contains( entry.getKey() ) ) {
filteredPrices.put( entry.getKey(), entry.getValue() );
}
}
TimeSeries filtered = new TimeSeries( filteredPrices );
return filtered;
}
Saves you the cost of creating a full copy of your map first, then iterating across the copy again to filter.

Is there an aggregateBy method in the stream Java 8 api?

Run across this very interesting but one year old presentation by Brian Goetz - in the slide linked he presents an aggregateBy() method supposedly in the Stream API, which is supposed to aggregate the elements of a list (?) to a map (given a default initial value and a method manipulating the value (for duplicate keys also) - see next slide in the presentation).
Apparently there is no such method in the Stream API. Is there another method that does something analogous in Java 8 ?
The aggregate operation can be done using the Collectors class. So in the video, the example would be equivalent to :
Map<String, Integer> map =
documents.stream().collect(Collectors.groupingBy(Document::getAuthor, Collectors.summingInt(Document::getPageCount)));
The groupingBy method will give you a Map<String, List<Document>>. Now you have to use a downstream collector to sum all the page count for each document in the List associated with each key.
This is done by providing a downstream collector to groupingBy, which is summingInt, resulting in a Map<String, Integer>.
They give basically the same example in the documentation where they compute the sum of the employees' salary by department.
I think that they removed this operation and created the Collectors class instead to have a useful class that contains a lot of reductions that you will use commonly.
Let's say we have a list of employees with their department and salary and we want the total salary paid by each department.
There are several ways to do it and you could for example use a toMap collector to aggregate the data per department:
the first argument is the key mapper (your aggregation axis = the department),
the second is the value mapper (the data you want to aggregate = salaries), and
the third is the merging function (how you want to aggregate data = sum the values).
Example:
import static java.util.stream.Collectors.*;
public static void main(String[] args) {
List<Person> persons = Arrays.asList(new Person("John", "Sales", 10000),
new Person("Helena", "Sales", 10000),
new Person("Somebody", "Marketing", 15000));
Map<String, Double> salaryByDepartment = persons.stream()
.collect(toMap(Person::department, Person::salary, (s1, s2) -> s1 + s2));
System.out.println("salary by department = " + salaryByDepartment);
}
As often with streams, there are several ways to get the desired result, for example:
import static java.util.stream.Collectors.*;
Map<String, Double> salaryByDepartment = persons.stream()
.collect(groupingBy(Person::department, summingDouble(Person::salary)));
For reference, the Person class:
static class Person {
private final String name, department;
private final double salary;
public Person(String name, String department, double salary) {
this.name = name;
this.department = department;
this.salary = salary;
}
public String name() { return name; }
public String department() { return department; }
public double salary() { return salary; }
}
This particular Javadoc entry is about the closest thing I could find on this piece of aggregation in Java 8. Even though it's a third party API, the signatures seem to line up pretty well - you provide some function to get values from, some terminal function for values (zero, in this case), and some function to combine the function and the values together.
It feels a lot like a Collector, which would offer us the ability to do this.
Map<String, Integer> strIntMap =
intList.stream()
.collect(Collectors
.groupingBy(Document::getAuthor,
Collectors.summingInt(Document::getPageCount)));
The idea then is that we group on the author's name for each entry in our list, and add up the total page numbers that the author has into a Map<String, Integer>.

Which collections to use?

Suppose I want to store phone numbers of persons. Which kind of collection should I use for key value pairs? And it should be helpful for searching. The name may get repeated, so there may be the same name having different phone numbers.
In case you want to use key value pair. Good choice is to use Map instead of collection.
So what should that map store ?
As far it goes for key. First thing you want to assure is that your key is unique to avoid collisions.
class Person {
long uniqueID;
String name;
String lastname;
}
So we will use the uniqueID of Person for key.
What about value ?
In this case is harder. As the single Person can have many phone numbers. But for simple task lest assume that a person can have only one phone number. Then what you look is
class PhoneNumberRegistry {
Map<Long,String> phoneRegistry = new HashMap<>();
}
Where the long is taken from person. When you deal with Maps, you should implement the hashCode and equals methods.
Then your registry could look like
class PhoneNumberRegistry {
Map<Person,String> phoneRegistry = new HashMap<>();
}
In case when you want to store more then one number for person, you will need to change the type of value in the map.
You can use Set<String> to store multiple numbers that will not duplicate. But to have full control you should introduce new type that not only store the number but also what king of that number is.
class PhoneNumberRegistry {
Map<Person,HashSet<String>> phoneRegistry = new HashMap<>();
}
But then you will have to solve various problems like, what phone number should i return ?
Your problem has different solutions. For example, I'll go with a LIST: List<Person>, where Person is a class like this:
public class Person{
private String name;
private List<String> phoneNumbers;
// ...
}
For collections searching/filtering I suggest Guava Collections2.filter method.
You should use this:
Hashtable<String, ArrayList<String>> addressbook = new Hashtable<>();
ArrayList<String> persons = new ArrayList<String>()
persons.add("Tom Butterfly");
persons.add("Maria Wanderlust");
addressbook.put("+0490301234567", persons);
addressbook.put("+0490301234560", persons);
Hashtable are save to not have empty elements, the ArrayList is fast in collect small elements. Know that multiple persons with different names may have same numbers.
Know that 2 persons can have the same number and the same Name!
String name = "Tom Butterfly";
String[] array = addressbook.keySet().toArray(new String[] {});
int firstElement = Collections.binarySearch(Arrays.asList(array),
name, new Comparator<String>() {
#Override
public int compare(String top, String bottom) {
if (addressbook.get(top).contains(bottom)) {
return 0;
}
return -1;
}
});
System.out.println("Number is " + array[firstElement]);
Maybe
List<Pair<String, String> (for one number per person)
or
List<Pair<String, String[]> (for multiple numbers per person)
will fit your needs.

Categories

Resources