2

I have a series of small arrays (consisting of two doubles), many of which are the same. E.g.

{5.0, 15.0}
{5.0, 15.0}
{5.0, 15.0}
{12.0, 8.0}
{10.0, 8.0}
{10.0, 8.0}

I want to be able to count the number of each arrays, i.e.

3 of {5.0, 15.0}
1 of {12.0, 8.0}
2 of {10.0, 8.0}

To do this, I tried making use of a LinkedHashMap (linked, because the order might come into use later on):

import java.util.Map;
import java.util.LinkedHashMap;

public class MapArrayInt {
        Map<double[], Integer> arrays = new LinkedHashMap<double[], Integer>();

    public static void main(String[] args) {
        MapArrayInt mapArrayInt = new MapArrayInt();
        mapArrayInt.addArray(5.0, 15.0);
        mapArrayInt.addArray(5.0, 15.0);
        mapArrayInt.addArray(5.0, 15.0);
        mapArrayInt.addArray(12.0, 8.0);
        mapArrayInt.addArray(10.0, 8.0);
        mapArrayInt.addArray(10.0, 8.0);
        System.out.println(String.valueOf(mapArrayInt.arrays.get(new double[]{5.0, 15.0})));
        System.out.println(String.valueOf(mapArrayInt.arrays.get(new double[]{12.0, 8.0})));
        System.out.println(String.valueOf(mapArrayInt.arrays.get(new double[]{10.0, 8.0})));
    }

    void addArray(double val1, double val2) {
        double[] newArray = new double[]{val1, val2};
        if (!arrays.containsKey(newArray)) {
            arrays.put(newArray, 1);
        } else {
            arrays.put(newArray, arrays.get(newArray) + 1);
        }
    }
}

I expected this output,

3
1
2

but got,

null
null
null

I'm quite new to Java, but I suspect this might be because each double[] counts as a unique because they are different instances, even though they contain the same two doubles.

How can I fix this, if I should at all (is there a better way)? I just need a data structure that allows me to

  1. Add doubles[]
  2. Preserves order of doubles[]
  3. Easily iterate through to get doubles[] and number of said doubles[]
ning
  • 1,823
  • 1
  • 19
  • 25
  • You're right about why you have that problem, but can you explain what this is *for*? Doing stuff like equality comparisons (such as lookups) on floating-point is generally a Bad Idea. – chrylis -cautiouslyoptimistic- Jun 29 '17 at 22:46
  • Can you use `List` instead of `double[]`? – shmosel Jun 29 '17 at 22:47
  • @chrylis A `map` represents an exercise; each `double[]` represents one set of each exercise, where `val1` is the number of reps, and `val2` is the value of the weight used. By retrieving the number of `double[]`s with the same values, I can easily print out something like `3x8 reps of 20kg`, where `3` is the number of doubles with values `8` and `20`. – ning Jun 29 '17 at 22:50
  • 1
    Everytime you add something to the map, you're adding a **new** `double[]` array. When you try to receive a value from it via `arrays.get(new double[]{5.0, 15.0})`, you're also creating a **new** array, which is a different reference than those in your map. Hence, you get `null` when trying to retrieve an entry. – QBrute Jun 29 '17 at 22:50
  • @shmosel I may be wrong, but I don't think a `List` would work with [my use case](https://stackoverflow.com/questions/44836067/linkedhashmapdouble-integer-cannot-access-integer-with-get-or-containske#comment76652843_44836067) – ning Jun 29 '17 at 22:52
  • You could create a class that extends a `HashMap`. Otherwise, I would create a function that converts the `Double[]`s to `String`s and uses those in the `HashMap` instead. – victor Jun 29 '17 at 22:57
  • The OP is using `double[]`. not `Double[]`, and converting to `String` is not an optimal approach. Neither is extending `HashMap`. Not sure why the OP went with `LinkedHashMap`. since insertion order doesn't seem relevant to the OP's situation. The problem, as others pointed out, is that arrays don't have value semantics for `equals`. It's also a Really Bad Idea to use mutable keys in a map. – Lew Bloch Jun 29 '17 at 23:16
  • 1
    @ning Technically, any use case requiring arrays can be substituted with lists. – shmosel Jun 29 '17 at 23:21

2 Answers2

3

As I stated in my comment, with new you're creating a new instance of an object. Which means that the arrays you added with mapArrayInt.addArray(5.0, 15.0); and the arrays in mapArrayInt.arrays.get(new double[]{5.0, 15.0}) reference different objects. That's why you get null, because for the map those are different keys.

In order to circumvent this, you could create a custom wrapper class

import java.util.Arrays;
public class Exercise {
    private final double[] array;
    public Exercise(double first, double second) {
        this.array = new double[]{first, second};
    }

    public boolean equals(Object obj) {
        if(!(obj instanceof Exercise)) {
            return false;
        }
        Exercise other = (Exercise)obj;
        return Arrays.equals(this.array, other.array);
    }

    public int hashCode() {
        return Arrays.hashCode(array);
    }
}

The equals and hashCode methods are important, when you want to use this class in collections like Map, otherwise the hashcode of Object is used for checking equality and you'd have the same problem as you have now.

Then, in your main class you can use it like so:

void addArray(double val1, double val2) {
    Exercise exercise = new Exercise(val1, val2);
    if (!arrays.containsKey(exercise)) {
        arrays.put(exercise, 1);
    } else {
        arrays.put(exercise, arrays.get(exercise) + 1);
    }
}

And System.out.println(String.valueOf(mapArrayInt.arrays.get(new Exercise(5.0, 15.0))));

QBrute
  • 4,405
  • 6
  • 34
  • 40
  • 1
    I think this is the answer I am looking for. So using the static `hashCode` method on an array will return the same value for two different instances of an array with the same values/contents, so that `map.containsKey` works? Why doesn't this happen by default for normal `Array`s? – ning Jun 29 '17 at 23:31
  • I just made almost the same Example below. Is it prefered to Override `equals` and `hashCode` rather than providing a single instance in such a case (my Example)? – Felix Jun 29 '17 at 23:32
  • Nevermind, I found an excellent answer about the hashCode stuff [on another thread](https://stackoverflow.com/a/744753/6910451) – ning Jun 29 '17 at 23:44
  • If you're creating a separate class, dispense entirely with the array and use meaningful variable names. – chrylis -cautiouslyoptimistic- Jun 30 '17 at 00:35
2

EDIT: I changed one of the doubles to int (you said you're representing reps and weight ... and reps can only be a natural number right?)

You could build create an Exercise-Class like below and use the static method "of" to create the instances:

package somepackage;

import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;

public class Exercise
{
    private static final Map<Integer, Map<Double, WeakReference<Exercise>>> instances = new HashMap<>();

    private final int reps;
    private final double weight;

    private Exercise(int reps, double weight)
    {
        this.reps = reps;
        this.weight = weight;
    }

    public static Exercise of(int reps, double weight)
    {
        if (!instances.containsKey(reps))
        {
            instances.put(reps, new HashMap<>());
        }

        Map<Double, WeakReference<Exercise>> innerMap = instances.get(reps);
        WeakReference<Exercise> weakRef = innerMap.get(weight);
        Exercise instance = null;

        if (weakRef != null)
        {
            instance = weakRef.get();
        }

        if (weakRef == null || instance == null || weakRef.isEnqueued())
        {
            instance = new Exercise(reps, weight);
            innerMap.put(weight, new WeakReference<>(instance));
        }

        return instance;
    }

    public int getReps()
    {
        return this.reps;
    }

    public double getWeight()
    {
        return this.weight;
    }
}

And then you could put those exercises in a map like below:

public void addArray(int reps, double weight)
{
    Exercise exercise = Exercise.of(reps, weight);
    if (!arrays.containsKey(exercise))
    {
        arrays.put(exercise, 1);
    }
    else
    {
        arrays.put(exercise, arrays.get(exercise) + 1);
    }
}

OR: Instead of an double[] as key you can use the a Map<Double, Integer> as your value for 2 values:

package somepackage;

import java.util.HashMap;
import java.util.Map;

public class MapArrayInt
{
    private final Map<Double, Map<Double, Integer>> values;

    public MapArrayInt()
    {
        this.values = new HashMap<>();
    }

    public void addArray(double val1, double val2)
    {
        if (!this.values.containsKey(val1))
        {
            this.values.put(val1, new HashMap<>());
        }

        Map<Double, Integer> innerValues = this.values.get(val1);
        if (innerValues.containsKey(val2))
        {
            innerValues.put(val2, innerValues.get(val2) + 1);
        }
        else
        {
            innerValues.put(val2, 1);
        }
    }

    public int getArrayValue(double val1, double val2)
    {
        Map<Double, Integer> innerValues = this.values.get(val1);
        if (innerValues == null)
        {
            // you may also throw an Exception here
            return 0;
        }

        Integer value = innerValues.get(val2);
        if (value == null)
        {
            // also here you may throw an Exception
            return 0;
        }

        return value;
    }

    public int getArrayValue(double[] values)
    {
        return getArrayValue(values[0], values[1]);
    }
}
Felix
  • 2,256
  • 2
  • 15
  • 35
  • 1
    Nah, go with the wrapper class. It's a lot less complicated, and scalable to arbitrary-length arrays. – Lew Bloch Jun 29 '17 at 23:22