It's hard to know what to suggest to you without knowing the cases you need to optimize for.
For example, this simple wrapper around a map is a sparse 2D matrix optimized for setting and getting specific indices (doesn't need to "grow" at all), but bad for iterating over all indices.
public class SparseMatrix<T> {
private final Map<Coordinates, T> map = new HashMap<Coordinates, T>();
private final T defaultValue;
public SparseMatrix(T defaultValue) {
this.defaultValue = defaultValue;
}
private static class Coordinates {
private final int[] coordinates;
Coordinates(int... coordinates) {
this.coordinates = coordinates;
}
@Override
public boolean equals(Object o) {
return Arrays.equals(coordinates, ((Coordinates)o).coordinates);
}
@Override
public int hashCode() {
return Arrays.hashCode(coordinates);
}
}
public T get(int x, int y) {
T value = map.get(new Coordinates(x, y));
if ( value == null ) {
return defaultValue;
}
}
public T set(int x, int y, T val) {
return map.put(new Coordinates(x, y), val);
}
}
Usage:
SparseMatrix<Integer> matrix = new SparseMatrix<Integer>(0);
matrix.set(3, 5, 7);
int seven = matrix.get(3, 5);
int zero = matrix.get(3, 6); //not set yet, uses default
It can also be very easily adapted to N-dimensions. Of course in production code you wouldn't roll your own, you'd use a library that does a better job.