В комментариях к «Как реализовать List, Set и Map в нулевом бесплатном дизайне?» , Steven Sudit и я вступил в дискуссию об использовании обратного вызова с обработчиками для ситуаций "найдено" и "не найдено" вместо метода tryGet()
, принимающего параметр out
и возвращающего логическое значение, указывающее, был ли заполнен параметр out. Стивен утверждал, что метод обратного вызова был более сложным и почти наверняка медленным; Я утверждал, что сложность была не больше, а производительность в худшем случае такая же.
Но код говорит громче, чем слова, поэтому я решил реализовать оба варианта и посмотреть, что у меня получилось. Первоначальный вопрос был довольно теоретическим в отношении языка («И ради аргумента, допустим, у этого языка даже нет null
») - я использовал Java здесь, потому что это то, что мне пригодилось. У Java нет параметров out, но у нее также нет функций первого класса, поэтому по стилю она должна одинаково сосать для обоих подходов.
(Отступление: Что касается сложности: мне нравится дизайн обратного вызова, потому что он по своей сути заставляет пользователя API обрабатывать оба случая, тогда как дизайн tryGet()
требует, чтобы вызывающие абоненты выполняли свою собственную условную проверку, которая могла забыть или ошибиться. Но теперь, когда реализовали оба варианта, я понимаю, почему tryGet()
дизайн выглядит проще, по крайней мере, в краткосрочной перспективе.)
Во-первых, пример обратного вызова:
class CallbackMap<K, V> {
private final Map<K, V> backingMap;
public CallbackMap(Map<K, V> backingMap) {
this.backingMap = backingMap;
}
void lookup(K key, Callback<K, V> handler) {
V val = backingMap.get(key);
if (val == null) {
handler.handleMissing(key);
} else {
handler.handleFound(key, val);
}
}
}
interface Callback<K, V> {
void handleFound(K key, V value);
void handleMissing(K key);
}
class CallbackExample {
private final Map<String, String> map;
private final List<String> found;
private final List<String> missing;
private Callback<String, String> handler;
public CallbackExample(Map<String, String> map) {
this.map = map;
found = new ArrayList<String>(map.size());
missing = new ArrayList<String>(map.size());
handler = new Callback<String, String>() {
public void handleFound(String key, String value) {
found.add(key + ": " + value);
}
public void handleMissing(String key) {
missing.add(key);
}
};
}
void test() {
CallbackMap<String, String> cbMap = new CallbackMap<String, String>(map);
for (int i = 0, count = map.size(); i < count; i++) {
String key = "key" + i;
cbMap.lookup(key, handler);
}
System.out.println(found.size() + " found");
System.out.println(missing.size() + " missing");
}
}
Теперь, пример tryGet()
- насколько я понимаю схему (и я вполне могу ошибаться):
class TryGetMap<K, V> {
private final Map<K, V> backingMap;
public TryGetMap(Map<K, V> backingMap) {
this.backingMap = backingMap;
}
boolean tryGet(K key, OutParameter<V> valueParam) {
V val = backingMap.get(key);
if (val == null) {
return false;
}
valueParam.value = val;
return true;
}
}
class OutParameter<V> {
V value;
}
class TryGetExample {
private final Map<String, String> map;
private final List<String> found;
private final List<String> missing;
private final OutParameter<String> out = new OutParameter<String>();
public TryGetExample(Map<String, String> map) {
this.map = map;
found = new ArrayList<String>(map.size());
missing = new ArrayList<String>(map.size());
}
void test() {
TryGetMap<String, String> tgMap = new TryGetMap<String, String>(map);
for (int i = 0, count = map.size(); i < count; i++) {
String key = "key" + i;
if (tgMap.tryGet(key, out)) {
found.add(key + ": " + out.value);
} else {
missing.add(key);
}
}
System.out.println(found.size() + " found");
System.out.println(missing.size() + " missing");
}
}
И, наконец, код теста производительности:
public static void main(String[] args) {
int size = 200000;
Map<String, String> map = new HashMap<String, String>();
for (int i = 0; i < size; i++) {
String val = (i % 5 == 0) ? null : "value" + i;
map.put("key" + i, val);
}
long totalCallback = 0;
long totalTryGet = 0;
int iterations = 20;
for (int i = 0; i < iterations; i++) {
{
TryGetExample tryGet = new TryGetExample(map);
long tryGetStart = System.currentTimeMillis();
tryGet.test();
totalTryGet += (System.currentTimeMillis() - tryGetStart);
}
System.gc();
{
CallbackExample callback = new CallbackExample(map);
long callbackStart = System.currentTimeMillis();
callback.test();
totalCallback += (System.currentTimeMillis() - callbackStart);
}
System.gc();
}
System.out.println("Avg. callback: " + (totalCallback / iterations));
System.out.println("Avg. tryGet(): " + (totalTryGet / iterations));
}
С первой попытки я получил на 50% худшую производительность для обратного вызова, чем для tryGet()
, что меня очень удивило. Но, догадываясь, я добавил сборщик мусора, и снижение производительности исчезло.
Это соответствует моему инстинкту, который заключается в том, что мы в основном говорим о том, чтобы брать одинаковое количество вызовов методов, проверок условий и т. Д. И переставлять их. Но затем я написал код, поэтому вполне мог бы написать неоптимальную или подсознательно оштрафованную реализацию tryGet()
. Мысли?
Обновлено: За комментарий от Майкл Аарон Сафьян , исправлено TryGetExample
для повторного использования OutParameter
.