Primary concerns are:
- correctness for all use cases
- efficiency in memory
- performance
Additionally, I am interested in use-cases or usability factors that are of concern, or should be added.
Stack Exchange network consists of 183 Q&A communities including Stack Overflow, the largest, most trusted online community for developers to learn, share their knowledge, and build their careers.
Visit Stack ExchangeTeams
Learn more about TeamsPrimary concerns are:
Additionally, I am interested in use-cases or usability factors that are of concern, or should be added.
Primary concerns are:
Additionally, I am interested in use-cases or usability factors that are of concern, or should be added.
This code is part of a growing "primitive" tools collection here on github
##Use Case
Consider:
IntArray ia = new IntArray();
ia.set(300, 5);
System.out.printf("Values at 10, 300, and 3000 are: %d %d and %d\n",
ia.get(10), ia.get(300), ia.get(3000));
This gives:
Values at 10, 300, and 3000 are: 0 5 and 0
##Background
Arrays are a very convenient system for storing data based on a simple index. Their constraints though (in Java), are that:
Many of these constraints can be avoided or reduced if you store array data in a 2-dimensional structure. Each 'row' is a portion of the overall span. If each row has the same, fixed size, then the translation from the logical linear system, the matrix system, is:
row -> index / rowsize
col -> index % rowsize
Similarly, the translation from a row/column position to the logical index, is:
index -> row * rowsize + col
If you choose a fixed row size that is also a power of 2, then you can accomplish the same with bitwise operations. A row size of 256 elements, is implemented as:
row -> index >>> 8
col -> index & 255
similarly:
index -> (row << 8) | col
The following is a collection of static methods designed to help manage a system where you create a matrix to represent a logical single-dimensional array as a matrix instead. In addition, an implementation of a primitive int
array that allows some structured and dynamic access to the data in the array.
##ArrayOps
Tools for managing the structure of the 2D matrices described above.
package net.tuis.primutils;
/**
* Common dynamic array tools used by various classes in this package.
* @author rolf
*
*/
class ArrayOps {
/**
* KVSHIFT, KVEXTENT, and KVMASK are used together when creating an
* efficient 2D matrix to store values in based on indexes. Treat the matrix
* as a collection of rows and columns. As your data grows, you add rows. To
* store data at any location:
*
* <pre>
* int row = index >>> KVSHIFT;
* if (row >= matrix.length) {
* matrix = Arrays.copyOf(matrix, extendSize(row);
* }
* if (matrix[row] == null) {
* matrix[row] = new SomeType[KVEXTENT];
* }
* matrix[row][index & KVMASK] = value;
* </pre>
*
* Using this system allows data to grow in an efficient way, and with
* limited memory overhead and garbage collection.
* <p>
* This IntKeyIndex, because of the way that it creates incrementing Index
* values for key associations, allows you to use an efficient linear
* storage system like the above to map arbitrary key values to linear
* storage.
*/
public static final int KVSHIFT = 8;
/**
* @see #KVSHIFT
*/
public static final int KVEXTENT = 1 << KVSHIFT;
/**
* @see #KVSHIFT
*/
public static final int KVMASK = KVEXTENT - 1;
/**
* Simple alias for Integer.MAX_VALUE;
*/
public static final int MAX_SIZE = Integer.MAX_VALUE;
/**
* Return the number of rows required to contain a dataset of the given
* size.
*
* @param size
* the size of the data to contain.
* @return the number of rows required to contain that size.
*/
public static final int getRowsFor(final int size) {
if (size <= 0) {
return 0;
}
// for a size of that, the last item will be at index (size - 1).
// what row would that last index be in?
// we need 1 more than that.
return 1 + ((size - 1) >> KVSHIFT);
}
/**
* Identify which row an index would appear in a value matrix as described
* by {@link #KVSHIFT}
*
* @param index
* the index to get the row for.
* @return the row in which that index would appear.
*/
public static final int getMatrixRow(int index) {
return index >>> KVSHIFT;
}
/**
* Identify which column an index would appear in a value matrix as
* described by {@link #KVSHIFT}
*
* @param index
* the index to get the column for.
* @return the column in which that index would appear.
*/
public static final int getMatrixColumn(int index) {
return index & KVMASK;
}
/**
* A simple helper method that returns a new size to use for a growing
* array. It checks for overflow conditions, and expands the size by
* approximately 25% each time.
*
* @param from
* the current size
* @return the recommended new size, with a hard limit at @link
* {@link #MAX_SIZE}
*/
public static final int extendSize(final int from) {
if (from <= 0) {
// some small number.
return 8;
}
int ns = from + (from >>> 2) + 1;
if (ns < from || ns > MAX_SIZE) {
// overflow conditions.
ns = MAX_SIZE;
}
if (ns == from) {
// unable to extend
throw new IllegalStateException("Unable to have more than " + MAX_SIZE + " values in the Map");
}
return ns;
}
}
##IntOps
Some small helper functions used for some common operations on Int primitives, are here. I expect this to grow over time.
package net.tuis.primutils;
import java.util.function.IntPredicate;
import java.util.function.IntUnaryOperator;
/**
* Simple tools and resources for int manipulations.
*
* @author rolf
*
*/
public class IntOps {
/**
* INCREMENT is a function that returns the input value, plus one.
*/
public static final IntUnaryOperator INCREMENT = (i) -> i + 1;
/**
* DECREMENT is a function that returns the input value, minus one.
*/
public static final IntUnaryOperator DECREMENT = (i) -> i - 1;
/**
* ISZERO is a simple zero-check.
*/
public static final IntPredicate ISZERO = (i) -> i == 0;
private IntOps() {
// inaccessible constructor.
}
}
##IntArray
This is the primary use case of the code above.
package net.tuis.primutils;
import static net.tuis.primutils.ArrayOps.KVEXTENT;
import static net.tuis.primutils.ArrayOps.KVMASK;
import static net.tuis.primutils.ArrayOps.extendSize;
import static net.tuis.primutils.ArrayOps.getMatrixColumn;
import static net.tuis.primutils.ArrayOps.getMatrixRow;
import static net.tuis.primutils.ArrayOps.getRowsFor;
import java.util.Arrays;
import java.util.function.IntUnaryOperator;
import java.util.stream.IntStream;
import java.util.stream.Stream;
/**
* A dynamic primitive int array that expands as needed.
*
* @author rolf
*
*/
public class IntArray {
private int[][] data;
private int hwm = -1; // high water mark
/**
* Create a dynamic array of int values with preinitialized capacity.
* @param capacity the initial capacity to budget for.
*/
public IntArray(int capacity) {
data = buildIntMatrix(getRowsFor(capacity));
}
/**
* Create a dynamic array of int values with default capacity.
*/
public IntArray() {
this(1);
}
/**
* Stream all values in this array to the high-water-mark.
* @return an IntStream which accesses, in order, all values.
*/
public IntStream stream() {
return IntStream.rangeClosed(0, hwm).map(index -> getValue(index));
}
/**
* Identify the high-water-mark for this array, which is the largest index accessed
* @return the high water mark.
*/
public int getHighWaterMark() {
return hwm;
}
private void accessed(int index) {
if (index > hwm) {
hwm = index;
}
}
/**
* Set the value at a particular index, to a particular value.
* @param index the index to set
* @param value the value to set at that index.
* @return the previous value at that index. Note that all values are initialized to 0.
*/
public int set(int index, int value) {
accessed(index);
return setValue(index, value);
}
/**
* Get the value at a particular index.
* @param index the index to get the value of.
* @return the value at that index.
*/
public int get(int index) {
accessed(index);
return getValue(index);
}
/**
* Retrieve the value at the given index, and then apply the supplied operation to that value.
* @param index the index of the value to operate on.
* @param op the operation to perform.
* @return the value before the operation was performed.
*/
public int postApply(int index, IntUnaryOperator op) {
accessed(index);
final int row = getMatrixRow(index);
final int col = getMatrixColumn(index);
ensureRow(row);
int[] line = data[row];
final int rv = line[col];
line[col] = op.applyAsInt(line[col]);
return rv;
}
/**
* Apply the supplied operation to the value at the given index, and then return the result.
* @param index the index of the value to operate on.
* @param op the operation to perform.
* @return the value after the operation was performed.
*/
public int preApply(int index, IntUnaryOperator op) {
accessed(index);
final int row = getMatrixRow(index);
final int col = getMatrixColumn(index);
ensureRow(row);
int[] line = data[row];
line[col] = op.applyAsInt(line[col]);
return line[col];
}
/**
* Increment the value at the given index, and return the value as it was before the increment
* @param index the index of the value to increment.
* @return the previous value.
*/
public int postIncrement(int index) {
return postApply(index, IntOps.INCREMENT);
}
/**
* Increment the value at the given index, and return the value as it is after the increment
* @param index the index of the value to increment.
* @return the incremented value.
*/
public int preIncrement(int index) {
return preApply(index, IntOps.INCREMENT);
}
/**
* Decrement the value at the given index, and return the value as it was before the decrement
* @param index the index of the value to decrement.
* @return the previous value.
*/
public int postDecrement(int index) {
return postApply(index, IntOps.DECREMENT);
}
/**
* Decrement the value at the given index, and return the value as it is after the decrement
* @param index the index of the value to decrement.
* @return the decremented value.
*/
public int preDecrement(int index) {
return preApply(index, IntOps.DECREMENT);
}
private final int setValue(final int index, final int value) {
if (index < 0) {
throw new ArrayIndexOutOfBoundsException("No index " + index);
}
final int row = getMatrixRow(index);
final int col = getMatrixColumn(index);
ensureRow(row);
final int old = data[row][col];
data[row][col] = value;
return old;
}
private final int getValue(final int index) {
if (index < 0) {
throw new ArrayIndexOutOfBoundsException("No index " + index);
}
final int r = getMatrixRow(index);
if (r >= data.length) {
return 0;
}
final int[] row = data[r];
return row == null ? 0 : row[index & KVMASK];
}
private final void ensureRow(final int row) {
if (row >= data.length) {
data = Arrays.copyOf(data, extendSize(row));
}
if (data[row] == null) {
data[row] = buildIntRow();
}
}
private static final int[][] buildIntMatrix(int rows) {
return new int[Math.max(1, rows)][];
}
private static final int[] buildIntRow() {
return new int[KVEXTENT];
}
@Override
public String toString() {
return String.format("IntArray(hwm: %d, alloc: %d)", hwm, Stream.of(data).mapToInt(row -> row == null ? 0 : row.length).sum());
}
@Override
public int hashCode() {
// because of the convenient row lengths, we can do:
int hash = 0;
for (final int[] row : data) {
if (row == null) {
continue;
}
int rh = 0;
for (final int v : row) {
Integer.rotateLeft(rh, 13);
rh ^= v;
}
hash ^= rh;
}
return hash;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof IntArray)) {
return false;
}
if (obj == this) {
return true;
}
final IntArray them = (IntArray)obj;
if (them.hwm != this.hwm) {
return false;
}
final int limit = getMatrixRow(hwm);
for (int r = 0; r <= limit; r++) {
final int[] a = r < this.data.length ? this.data[r] : null;
final int[] b = r < them.data.length ? them.data[r] : null;
if (a == null && b == null) {
continue;
}
if (a == null && !IntStream.of(b).allMatch(IntOps.ISZERO)) {
return false;
}
if (b == null && !IntStream.of(a).allMatch(IntOps.ISZERO)) {
return false;
}
if (!Arrays.equals(a, b)) {
return false;
}
}
return true;
}
}
##Unit tests for IntArray
May help understand the usage, and run the code.
package net.tuis.primutils;
import static org.junit.Assert.*;
import java.util.IntSummaryStatistics;
import org.junit.Test;
@SuppressWarnings("javadoc")
public class TestIntArray {
@Test
public void testIntArrayInt() {
IntArray ia = new IntArray(100);
assertEquals(-1, ia.getHighWaterMark());
}
@Test
public void testIntArray() {
IntArray ia = new IntArray();
assertEquals(-1, ia.getHighWaterMark());
}
@Test
public void testStream() {
IntArray ia = new IntArray();
ia.get(10000);
IntSummaryStatistics stats = ia.stream().summaryStatistics();
assertEquals(10001, stats.getCount());
assertEquals(0, stats.getMax());
assertEquals(0, stats.getMin());
}
@Test
public void testGetHighWaterMark() {
IntArray ia = new IntArray(100);
assertEquals(-1, ia.getHighWaterMark());
for (int i = 0; i < 10; i++) {
ia.set(i, i);
assertEquals(i, ia.getHighWaterMark());
}
for (int i = 5000; i >= 4000; i--) {
ia.set(i, i);
assertEquals(5000, ia.getHighWaterMark());
}
}
@Test
public void testSet() {
IntArray ia = new IntArray(100);
assertEquals(0, ia.set(0, 1));
assertEquals(1, ia.set(0, 2));
assertEquals(0, ia.set(10000, 1));
assertEquals(1, ia.set(10000, 2));
}
@Test
public void testGet() {
IntArray ia = new IntArray(100);
assertEquals(0, ia.get(0));
assertEquals(0, ia.getHighWaterMark());
assertEquals(0, ia.get(10000));
assertEquals(10000, ia.getHighWaterMark());
assertEquals(0, ia.set(10000, -1));
assertEquals(-1, ia.get(10000));
}
@Test
public void testPostApply() {
IntArray ia = new IntArray(100);
assertEquals(0, ia.postApply(0, i -> i + 10));
assertEquals(10, ia.get(0));
assertEquals(10, ia.postApply(0, i -> i + 10));
assertEquals(20, ia.get(0));
}
@Test
public void testPreApply() {
IntArray ia = new IntArray(100);
assertEquals(10, ia.preApply(0, i -> i + 10));
assertEquals(10, ia.get(0));
assertEquals(20, ia.preApply(0, i -> i + 10));
assertEquals(20, ia.get(0));
}
@Test
public void testPostIncrement() {
IntArray ia = new IntArray(100);
assertEquals(0, ia.postIncrement(0));
assertEquals(1, ia.get(0));
assertEquals(1, ia.postIncrement(0));
assertEquals(2, ia.get(0));
}
@Test
public void testPreIncrement() {
IntArray ia = new IntArray(100);
assertEquals(1, ia.preIncrement(0));
assertEquals(1, ia.get(0));
assertEquals(2, ia.preIncrement(0));
assertEquals(2, ia.get(0));
}
@Test
public void testPostDecrement() {
IntArray ia = new IntArray(100);
assertEquals(0, ia.postDecrement(0));
assertEquals(-1, ia.get(0));
assertEquals(-1, ia.postDecrement(0));
assertEquals(-2, ia.get(0));
}
@Test
public void testPreDecrement() {
IntArray ia = new IntArray(100);
assertEquals(-1, ia.preDecrement(0));
assertEquals(-1, ia.get(0));
assertEquals(-2, ia.preDecrement(0));
assertEquals(-2, ia.get(0));
}
}