Implementing get, containsKey, put, and remove

get uses indexOf:

@Override
public V get(K key) {
  if (key == null) {
    throw new IllegalArgumentException();
  }
  int i = indexOf(key);
  if (i == -1) {
    return null;  // key not present
  }
  return arr[i].value;
}

And containsKey:

@Override
public boolean containsKey(K key) {
  if (key == null) {
    throw new IllegalArgumentException();
  }
  return indexOf(key) != -1;
}

The put operation searches for the key first:

@Override
public boolean put(K key, V value) {
  if (key == null || value == null) {
    throw new IllegalArgumentException();
  }
  int i = indexOf(key);
  if (i != -1) {
    arr[i].value = value;  // key present; replace its value
    return false;
  }
  if (size == arr.length) {
    grow();                // grow when the array is full
  }
  arr[size] = new Entry<>(key, value);  // append at the end
  size++;
  return true;
}

The grow helper doubles the array, the same way we did for ArrayList.

remove uses the swap-with-last removal from ArraySet:

@Override
public boolean remove(K key) {
  if (key == null) {
    throw new IllegalArgumentException();
  }
  int i = indexOf(key);
  if (i == -1) {
    return false;          // not present; nothing changed
  }
  arr[i] = arr[size - 1];  // move the last entry into the gap
  arr[size - 1] = null;    // clear the vacated slot
  size--;
  return true;
}