Не так давно, я написал небольшой класс для автоматического кэширования и обновления произвольных значений. Пользоваться им было очень легко — стоило лишь создать анонимный класс с перегруженным методом update, а потом, когда нужно, вызывать функции для пометки значения устаревшим и для получения самого значения:
Тесты в маленькой программе-этюде проходили успешно, тогда как в большом рабочем проекте с использованием этого класса стала появляться ошибка переполнения стека — значения вызывали обновления друг друга, не имея другого источника информации. Этой ошибки не возникло бы, если перерассчет происходил каждый раз, а не в последний момент.
Довольно быстро мне порядком надоело вставлять костыли в код, чтобы предотвратить рекурсию тут и там, и я решил встроить в класс кэширования детектор рекурсии, который бы сработав, разматывал стэк до «зачинщика» рекурсии и выдавал какое-то значение по-умолчанию тем самым предотвращая StackOverflowException. Вот какая получилась реализация:
Вся магия кроется в классе RSLazyValue (RS — это Recursion-Safe). В тот момент, когда обновление началось, ставится специальный флаг, который потом обязательно по окончанию обновления снимается. Если мы только что зашли в ту же функцию, а обновление в процессе, значит мы поймали рекурсию за хвост и надо с ней что-то делать. Решение в лоб — сразу вернуть значение по-умолчанию. Однако это не лучший способ, ведь тогда обработанное значение вернется в первый вызов функции и она вернет значение, основанное на своем же, только обработанном извне. Наиболее подходящий вариант — выкинуть исключение, которое размотает стэк вызовов до предыдущей «ипостаси» этой же функции и вернуть значение по-умолчанию оттуда. Вот как это работает:
Решение позволило в большом проекте сохранить массу времени. Как времени программиста, так и процессорного. Экономия производительности состоит в том, что данные рассчитываются только по запросу, и если запроса так и не произошло, то следовательно эти данные никому понадобились и считать их не нужно. Надеюсь эти 2 маленьких класса позволят сэкономить и ваше время. Буду рад конструктивной критике и случаям, при которых «ничего не работает».
Полный код классов:
LazyValue.java
RSLazyValue.java
public static void main(String[] args)
{
LazyValue<Integer> ultimateQuestionOfLife = new LazyValue<Integer>()
{
@Override
protected Integer update()
{
return findNewUltimateAnswer();
}
};
// пометить устаревшим
ultimateQuestionOfLife.invalidate();
// вызовется update()
System.out.println("Answer is: " + ultimateQuestionOfLife.get());
// update() не вызовется, вернется кэшированное значение
System.out.println("Answer is: " + ultimateQuestionOfLife.get());
// пометить устаревшим
ultimateQuestionOfLife.invalidate();
// update() вызовется во второй раз
System.out.println("Answer is: " + ultimateQuestionOfLife.get());
}
Тесты в маленькой программе-этюде проходили успешно, тогда как в большом рабочем проекте с использованием этого класса стала появляться ошибка переполнения стека — значения вызывали обновления друг друга, не имея другого источника информации. Этой ошибки не возникло бы, если перерассчет происходил каждый раз, а не в последний момент.
private static LazyValue<Integer> lv1 = new LazyValue<Integer>()
{
@Override
protected Integer update()
{
return lv2.get(); // нарочно создаем губительную рекурсию
}
};
private static LazyValue<Integer> lv2 = new LazyValue<Integer>()
{
@Override
protected Integer update()
{
return lv1.get() + 1;
}
};
public static void main(String[] args)
{
// хитроумный способ вызвать StackOverflowException
System.out.println(lv2.get());
}
Довольно быстро мне порядком надоело вставлять костыли в код, чтобы предотвратить рекурсию тут и там, и я решил встроить в класс кэширования детектор рекурсии, который бы сработав, разматывал стэк до «зачинщика» рекурсии и выдавал какое-то значение по-умолчанию тем самым предотвращая StackOverflowException. Вот какая получилась реализация:
private static LazyValue<Integer> lv1 = new RSLazyValue<Integer>()
{
@Override
protected Integer update()
{
return lv2.get();
}
@Override
protected Integer getDefault()
{
return 0;
}
};
private static LazyValue<Integer> lv2 = new RSLazyValue<Integer>()
{
@Override
protected Integer update()
{
return lv1.get() + 1;
}
@Override
protected Integer getDefault()
{
return 10;
}
};
public static void main(String[] args)
{
// выведет 10
System.out.println(lv2.get());
}
Вся магия кроется в классе RSLazyValue (RS — это Recursion-Safe). В тот момент, когда обновление началось, ставится специальный флаг, который потом обязательно по окончанию обновления снимается. Если мы только что зашли в ту же функцию, а обновление в процессе, значит мы поймали рекурсию за хвост и надо с ней что-то делать. Решение в лоб — сразу вернуть значение по-умолчанию. Однако это не лучший способ, ведь тогда обработанное значение вернется в первый вызов функции и она вернет значение, основанное на своем же, только обработанном извне. Наиболее подходящий вариант — выкинуть исключение, которое размотает стэк вызовов до предыдущей «ипостаси» этой же функции и вернуть значение по-умолчанию оттуда. Вот как это работает:
public ValueType get()
{
// вызов update() находится ниже по коду, следовательно
// выполнение этого условия означает, что get() косвенно
// или напрямую был вызван из update()
if (updateInProgress)
throw new RecursionDetectedException(this);
// выполняем update() только если значение устарело
if (isInvalid)
{
// этот флаг обязательно нужно снять по выходу из update()
updateInProgress = true;
try
{
// выполняем обновление
value = update();
// isInvalid станет false только если update()
// выполнился штатно без эксепшенов
isInvalid = false;
}
catch (RecursionDetectedException e)
{
if (e.getRecursionStarter() != this)
throw e; // выкидываем исключение дальше, если рекурсию начали не мы
else
return getDefault(); // возвращаем значение по-умолчанию
}
finally
{
// снимаем флаг в любом случае, как бы не завершилась get()
updateInProgress = false;
}
}
return value;
}
Решение позволило в большом проекте сохранить массу времени. Как времени программиста, так и процессорного. Экономия производительности состоит в том, что данные рассчитываются только по запросу, и если запроса так и не произошло, то следовательно эти данные никому понадобились и считать их не нужно. Надеюсь эти 2 маленьких класса позволят сэкономить и ваше время. Буду рад конструктивной критике и случаям, при которых «ничего не работает».
Полный код классов:
LazyValue.java
RSLazyValue.java