Я пытаюсь реализовать хеш-минусы в java, сопоставимые с тем, что String.intern
делает для строк. Т.е. я хочу, чтобы класс сохранял все различные значения типа данных T
в наборе и предоставлял метод T intern(T t)
который проверяет, находится ли t
в наборе. Если это так, возвращается экземпляр в наборе, иначе t
добавляется в набор и возвращается. Причина в том, что результирующие значения можно сравнить с помощью ссылочного равенства, так как, конечно, оба одинаковых значения возвращаются из intern
.
Конечно, наиболее очевидной структурой данных кандидатов для хэш-минусов является java.util.HashSet<T>
. Тем не менее, кажется, что его интерфейс несовершенен и не позволяет эффективно вставлять, потому что нет способа получить элемент, который уже находится в наборе, или вставить его, если он там отсутствует.
Алгоритм, использующий HashSet
будет выглядеть так:
class HashCons<T>{
HashSet<T> set = new HashSet<>();
public T intern(T t){
if(set.contains(t)) {
return ???; // <----- PROBLEM
} else {
set.add(t); // <--- Inefficient, second hash lookup
return t;
}
}
Как вы видите, проблема двоякая:
contains
и один раз для add
. Но все в порядке, это может быть не слишком большой удар производительности, поскольку правильный кусок будет в кеше после contains
, поэтому add
не приведет к провалу кеша и, таким образом, будет довольно быстрым.PROBLEM
). Нет способа получить элемент в наборе. Поэтому реализовать это невозможно. Я что-то упустил? Или действительно невозможно построить обычный хеш-минус с java.util.HashSet
?
Well HashSet
реализуется как оболочка HashMap
в OpenJDK, поэтому вы не выиграете в использовании памяти по сравнению с решением, предложенным aRestless.
10-минутный эскиз
class HashCons<T> {
T[] table;
int size;
int sizeLimit;
HashCons(int expectedSize) {
init(Math.max(Integer.highestOneBit(expectedSize * 2) * 2, 16));
}
private void init(int capacity) {
table = (T[]) new Object[capacity];
size = 0;
sizeLimit = (int) (capacity * 2L / 3);
}
T cons(@Nonnull T key) {
int mask = table.length - 1;
int i = key.hashCode() & mask;
do {
if (table[i] == null) break;
if (key.equals(table[i])) return table[i];
i = (i + 1) & mask;
} while (true);
table[i] = key;
if (++size > sizeLimit) rehash();
return key;
}
private void rehash() {
T[] table = this.table;
if (table.length == (1 << 30))
throw new IllegalStateException("HashCons is full");
init(table.length << 1);
for (T key : table) {
if (key != null) cons(key);
}
}
}
Я не думаю, что это возможно с помощью HashSet
. Вместо этого вы можете использовать какую-то Map
и использовать свое значение как ключ и как значение. java.util.concurrent.ConcurrentMap
имеет довольно удобный метод
putIfAbsent(K key, V value)
который возвращает значение, если оно уже существует. Однако я не знаю о производительности этого метода (по сравнению с проверкой "вручную" на неконкурентных реализациях Map
).
Вот как вы это сделаете, используя HashMap
:
class HashCons<T>{
Map<T,T> map = new HashMap<T,T>();
public T intern(T t){
if (!map.containsKey(t))
map.put(t,t);
return map.get(t);
}
}
Я думаю, что причина, по которой это невозможно с HashSet
, довольно проста: для множества, если contains(t)
, это означает, что данный t
также равен одному из t 'в наборе. Нет причин для возможности вернуть его (поскольку у вас уже есть).
HashMap
два раза, это позор :(