Изменяемые числовые объекты

    Как известно, в Java существуют примитивные типы для чисел (byte, short, int, long, float, double) и объектные обёртки над ними (Byte, Short, Integer, Long, Float, Double). В различных статьях можно встретить диаметрально противоположные рекомендации о том, чем пользоваться. С одной стороны объектные обёртки универсальны: их можно использовать со стандартными коллекциями, которые удобны, инкапсулированы и вообще прекрасны. Но боксинг убивает производительность и ест кучу памяти. Примитивные типы быстры и компактны, но их можно поместить только в массивы, которые и от записи не защитишь, и абстракция на нуле. Если же вам нужно что-то типа Map, для отображения чего-нибудь на числа, то придётся либо мириться с потерей производительности и памяти, либо использовать сторонние библиотеки, реализующие нестандартный интерфейс. Однако в некоторых случаях вам помогут изменяемые (mutable) числа.

    Представьте себе, что вам нужно подсчитывать количества разных строк, которые откуда-то поступают. Часто пишут примерно такой код:

    public Map<String, Integer> countStrings() {
    	Map<String, Integer> counts = new HashMap<String, Integer>();
    	while(true) {
    		String next = getNextString();
    		if(next == null) break;
    		Integer val = counts.get(next);
    		if(val == null) counts.put(next, 1);
    		else counts.put(next, val+1);
    	}
    	return counts;
    }

    Если типов строк не так много, а повторов хватает, то боксинг будет создавать миллионы временных Integer-объектов, которые потом будет чистить сборщик мусора. Представить страшно. Нет, это не катастрофически медленно, но всё же несложно ускорить процедуру 2-3 раза. Для этого мы и используем MutableInteger.

    Такой класс есть в некоторых библиотеках (например, в org.apache.commons.lang), но его несложно и написать самому. Простая реализация может выглядеть примерно так:

    public class MutableInteger {
    	private int value;
    	
    	public MutableInteger(int value) {
    		this.value = value;
    	}
    	
    	public int intValue() {
    		return value;
    	}
    	
    	public void set(int value) {
    		this.value = value;
    	}
    	
    	public void increment() {
    		value++;
    	}
    	
    	public String toString() {
    		return String.valueOf(value);
    	}
    }

    Далее можно добавить простые методы для арифметических действий. Ещё удобно унаследовать Number, реализовать интерфейс Comparable, а также не забыть про equals и hashCode (в hashCode можно просто вернуть value). Но для текущей задачи нам хватит того, что написано. Теперь countStrings() можно переписать следующим образом:

    public Map<String, MutableInteger> countStrings() {
    	Map<String, MutableInteger> counts = new HashMap<String, MutableInteger>();
    	while(true) {
    		String next = getNextString();
    		if(next == null) break;
    		MutableInteger val = counts.get(next);
    		if(val == null) counts.put(next, new MutableInteger(1));
                    else val.increment();
    	}
    	return counts;
    }

    Не очень хорошо, конечно, раскрывать детали реализации, возвращая Map<String, MutableInteger>, но если мы унаследуем java.lang.Number, то можно вернуть Map<String, Number>. Ну или в крайнем случае после подсчёта скопировать всё в новую Map. Аналогично можно собирать не только количество, но и другую статистику по набору объектов.

    Заметим также, что java.util.concurrent.atomic.AtomicInteger по сути тоже MutableInteger, однако накладные расходы на атомарность могут даже превысить расходы на создание объектов и сборку мусора из первого примера, поэтому отдельный класс MutableInteger всё же нужен.
    Share post

    Comments 54

      +5
      Про милионы временных Integer-объектов немного не правда — как минимум от -128 до 127 значения Integer кэшируются.
        +3
        + все эти временные Integer будут довольно быстро появляться и умирать в эдене.
        У вас есть какие-нить замеры перфоманса на тему Integer vs MutableInteger?
          0
          Я считал 10M строк, которые раскидывались на 4 группы. MutableInteger более чем вдвое быстрее. Собственно, я написал об этом.
            +2
            Накидал грубый тест:
            pastebin.com/zKYxyiSG
            Результат:
            Map<String, MutableInteger> быстрее ~ на 51%
            Работа через массив (выгодно если у нас малое/фиксированное колличество ключей) ~ на 48% быстрее.
            В тривиальной имплементации (Map<String, Integer>) проседание по перфомансу естественно есть, видимо из-за боксинга, GC работает моментально.
            Имплементация c Mutable работает быстрее всего, но совсем немного опережает имплементацию через массив (грубый пример можно посмотреть в тесте)
            lany, с вами согласен — в данном конктертом случае mutableInteger более быстр и вполне себе гуд решение, однако с ним надо аккуратно
            я бы остановился на решении с массивом, но это уже для каждой задачи своё.
              0
              Массив, на мой взгляд, тут как раз загрязняет код сильнее. Ваша функция с массивом и так длиннее вышла, а если предположить, что вам надо заботиться о его растягивании, будет ещё хуже. Но да, зависит от задачи. Я, конечно, не предлагаю панацею, я лишь говорю, что если такой инструмент, который иногда может пригодиться. У нас в проекте на 4000 классов Mutable-числа используются от силы в двух местах, а массивы значительно чаще :-)
                0
                если -> есть, опечатка.
                +2
                Вот прямо так и тестировали? Без разогревающих циклов? Без JIT'а? Нда… «Copyright © Luxoft», ничего не скажешь.
                  0
                  Ну я предупредил что это грубый тест, разогревающие циклы просто не были учтены в результатах.
                  Целью было не получить точные цифры, а увидеть общую картинку.
                  «Copyright © Luxoft» — посыпаю голову пеплом, что не почистил файл перед копированием… или вы на что-то намекаете
            0
            Если вы считаете до 10000000, скажем, то как вам этот кэш поможет? Только накладные расходы на проверку, входит ли новое число в этот диапазон.
              0
              А я еще, по сравнению с первым алгоритмом, мы неплохо выигрываем на том, что избавляемся от counts.put(next, val+1), особенно если у нас частые повторы слов. Давно уже пользуюсь вторым подходом как раз, чтобы избегать лишних вставок в map, на счет боксинга тоже подозревал, но никак руки не доходили проверить
              +3
              А еще можно использовать вот это trove.starlight-systems.com/, особенно если работать с такого рода коллекциями нужно много и не смущает дополнительный джарник.
                0
                Ну пор это я тоже написал:
                либо использовать сторонние библиотеки, реализующие нестандартный интерфейс.
                +2
                Я бы не советовал так делать, Изменяемые объекты это ужас-ужас.

                Кто то возьмет количество из Map, чтобы потом изпользовать, а его под носом изменят. А что с многопоточными приложениями? Не, ну его нафиг такие оптимизации. Кстати — короткоживущие объекты почто не влияют на производительность
                  +1
                  К сожалению, концепция неизменяемых объектов в джаве довольно ущербная — у вас нет стандартных интерфейсов неизменяемых коллекций, у вас нет возможности просто указать _компилятору_, что аргумент/возвращаемое значение — не изменяемое… Нужные интерфейсы можно ввести самому — но это немаленькая работа, и большинство библиотек вас не поддержит. В общем, полноценная реализация разграничения mutable/immutable в яве обойдется крайне дорого. Как правило это разграничение проводится неявно, на основе соглашений. И в этом случае нет никакой разницы, что возвращать изменяемый словарь с неизменяемыми значениями, что возвращать изменяемый словарь с изменяемыми значениями…
                    0
                    В других языках бывает ещё хуже, и ничего — живут и радуются.
                      0
                      Моя мысль была такая: от замены Map[String, Integer] на Map[String,MutableInteger] ничего концептуально не ухудшится. Неизменяемости и до того толком не было, поэтому нет особого смысла жаловаться, что она-де пропадет.

                      А что с этим жить можно — безусловно. И жить, и даже кое-где подправлять.
                        0
                        На самом деле можно создать интерфейс ImmutableInteger со всеми методами из MutableInteger, кроме set и increment и реализовать его в MutableInteger. и Возвращать соответственно Map[String,ImmutableInteger].
                        Тогда все станет хорошо.
                          0
                          ну или не плодить свои классы и реализовать в MutableInteger абстрактный Number(как и предлагал автор) и его собственно и возвращать.
                            +1
                            «На самом деле» нужно делать интерфейс Counters{ int get(K key); Iterable keys(); } — это если вы в самом деле хотите правильно.

                            Об этом я и говорю: предлагаемое автором решение уже заметно неправославно, от замены Integer на MutableInteger ничего не ухудшится. Для приватной реализации сойдет, для public API по-любому плохо.
                        0
                        Языковая поддержка неизменяемых коллекций, конечно никакая, но это не знит, что надо делать еще хуже. Если я возвращаю не коллекцию, а счетчик, то лучше чтобы он был неизменяемым
                          0
                          Да на здоровье же. Я ж написал:
                          Ну или в крайнем случае после подсчёта скопировать всё в новую Map.
                          Что не так?
                            0
                            А то, что проблема не в Map, а в самих неизменяемых числах. Вам нужно будет еще и их клонировать. Т.е. если на основе этого примера сторить реальное приложение, то надо будет добавить столько всего, что все ваши оптимизации сойдут на нет. Дайте компилятору и виртуальной машине оптимизировать, ради бога. Вы не слышали фразу «Преждевременная оптимизация — корень всего зла»?
                              0
                              Ну где я написал, что я рекомендую оптимизировать преждевременно? Что ж вы мне приписываете то, чего я не говорил? Надо в начало каждой статьи вставлять слова, что перед оптимизацией сперва используйте профайлер? По-моему, всем давно очевидно.

                              У вас очень жизненная заметка «Про релевантность опыта», спасибо.
                                0
                                Надо в начало каждой статьи вставлять слова, что перед оптимизацией сперва используйте профайлер?

                                Увы, но надо. Как ни печально, но тех, кто понимает, что в статье описывается достаточно экзотическая ситуация* и решения, которые тут применяются, тоже достаточно экзотичны (хотя и, безусловно, интересны) — на Хабре меньшинство. И это меньшинство довольно хорошо совпадает с другим меньшинством — тех, кто в случае просадок производительности сначала исследуют ситуацию (например, с помощью профайлера), а уже потом начинают что-то исправлять.

                                * — ситуация, когда java используется для data mining-а экзотична по определению, так как она не очень для этого подходит, ни как язык, ни как среда исполнения.
                        0
                        Любой примитивный int — изменяемый объект. Вы их вообще не используете? А если используете, то как же многопоточность? Или вы решили, что я предлагаю использовать MutableInteger всегда? Но я же ясно написал:
                        Однако в некоторых случаях вам помогут изменяемые (mutable) числа.

                        Внутренние детали вычислительно нетривиальной операции могут обойтись без высокоуровневых концепций, вам так не кажется?
                        Кстати — короткоживущие объекты почто не влияют на производительность

                        Кроме тех случаев, когда влияют.
                          0
                          int передается по значению, а объект по ссылке. Если я куда то передал примитивный счетчик, мне уже не надо волноваться, что кто то его изменит.

                          Расскажите мне в каких случаях короткоживущие объекты влияют на производительность
                            +1
                            Вы можете вернуть наружу из алгоритма любую структуру данных. Если вы боитесь за изменчивость, скопируйте результат в неизменяемый тип (я это упомянул в статье). В рассмотренном примере это вообще проблем не несёт. И, надо полагать, вы из тех людей, которые обычные Java-массивы не используют вообще? Ведь их там могут изменить.

                            Короткоживущие объекты влияют на производительность в примере, рассмотренном в статье.
                              0
                              Вот пример из жизни — habrahabr.ru/post/147552/ о котором я недавно писал. В этом случае коротко живущие объекты не только влияли на производительность, но и приводили к постоянному срабатыванию сборщика что просто вешало машину. Я уже не говорю про десятки таких мест в высоконагруженных системах.
                          +1
                          Присоединюсь к критикам. Конкретно задачу из примера намного лучше решить с использованием AtomicInteger и его атомарного инкремента. Мало того что тривиально добавляется многопоточность, так ещё и никаких левых костылей, затрудняющих чтение кода.

                          К тому же, если пока вы считали распределение строк, ваши изменяемые числа не дай бог успели постареть и попасть в OldGen — прощай всякая производительность.
                            0
                            Конкретно задачу из примера намного лучше решить с использованием AtomicInteger и его атомарного инкремента.
                            Вы проверяли, насколько это быстро? Атомарный инкремент ужасно медленный. Просто ужасно. Попробуйте эту задачу распараллелить, вы больше потеряете, чем приобретёте.

                            И почему вы считаете, что MutableInteger и increment больше затрудняют чтение кода, чем AtomicInteger и incrementAndGet? Код абсолютно одинаковый.

                            К тому же, если пока вы считали распределение строк, ваши изменяемые числа не дай бог успели постареть и попасть в OldGen
                            Нестрашно, если их мало.
                            –5
                            Руки оторвать:
                            — Тем кто придумал Java generic не как first class.
                            — Тем кто использует Integer вообще в коллекциях.
                            — Тем кто из-за недостатков подхода из второго пункта придумывает гибриды костылей с велосипедами.

                            Извините, наболело.
                              –1
                              * — Естественно не только Integer, но и Byte, Short, Long, Float, Double.
                                +1
                                В чём проблема с использованием Integer в коллекциях?
                                  0
                                  А уж чем Byte не угодил — вообще неясно. Байты в стандартной реализации закэшированы абсолютно все, новых объектов при боксинге вообще не создаётся. Почитайте исходник java.lang.Byte.

                                  Любопытно, что мнения комментаторов опять же противоположны, о чём я и сказал в начале статьи: от «накладные расходы невелики, молодые объекты удаляются быстро, используйте Integer и не парьтесь» до «Integer использовать ни в коем случае нельзя, это ужасно» :-)
                                    –1
                                    Да, простите, сонный был. :)
                                    Byte — исключение.
                                  +6
                                  Голову надо отрывать тем критикам, кто мечтает оторвать руки инженерам, решавшим (и довольно успешно решившим) проблемы, о которых оный критик даже не думал толком :)
                                    +2
                                    Мне кажется, вы оторваны от реального мира и его задач. В книжках-то конечно всё идеально прекрасно, одновременно и быстро, и абстрактно, и концептуально правильно.
                                      –1
                                      Ничего не оторвано. В сишарпе как раз эти проблемы решены.
                                        –1
                                        Вы прекрасно меня поняли.

                                        Собственно крик души у меня был в том, что в C# эти (и далеко не только) проблемы решены, однако, по некоторым причинам .NET использовать не представляется возможности.
                                    0
                                    Может, MutableInteger и полезен, но пример не совсем удачный. Для указанной задачи идеально подходит TObjectIntHashMap из библиотеки Trove с его методом adjustOrPutValue.
                                    0
                                    Вставлю свои 5 копеек. Если забыть о недостатках и сконцентрироваться на достоинствах подхода с классом оберткой, то он вполне пригоден для использования с оговоркой not thread-safe. Надеюсь автор сам написал класс и еще просто не знаком с commons-lang и классами из пакета org.apache.commons.lang.mutable.
                                    По поводу перформанса. Немного быстрее будет если заменить класс-обертку одноэлементным массивов. И то, и другое HotSpot успешно компилирует в быстрый код, но доступ к элементам массива происходит немного быстрее. Ну и для удобства можно написать вспомогательный класс:
                                    class MutableInt {
                                    	static Object create() { return new int[1]; }
                                    	static int get(Object ref) { return ((int[])ref)[0]; }
                                    	static void set(Object ref, int value) { ((int[])ref)[0] = value; }
                                    	static int incAndGet(Object ref) { return ++((int[])ref)[0]; }
                                    	static int getAndInc(Object ref) { return ((int[])ref)[0]++; }
                                    }
                                    
                                      0
                                      Надеюсь автор сам написал класс и еще просто не знаком с commons-lang и классами из пакета org.apache.commons.lang.mutable.
                                      Автор знаком и сослался на commons.lang. Но здесь класс написал сам, чтобы понятнее было.
                                        0
                                        Действительно, ссылка есть, прошу прощения за невнимательность.
                                        0
                                        Немного быстрее будет если заменить класс-обертку одноэлементным массивов. И то, и другое HotSpot успешно компилирует в быстрый код, но доступ к элементам массива происходит немного быстрее.

                                        Можно с этого места поподробнее? Почему доступ к элементам массива быстрее?
                                      +1
                                      Думаю, тут тормоза не из-за создания новых объектов, а из-за двукратного лазания в мапу — при гете и при путе. Если бы можно было получить Map.Entry и модифицировать уже его…
                                        +1
                                        Объясните пожалуйста, а почему нельзя использовать < String, int >?
                                          0
                                          Потому что так устроены генерики в Java: в них не может быть примитивного типа. Сами попробуйте.
                                            0
                                            Потому что generic'и требуют объекта в качестве параметра.
                                            А упирается это в генерацию кода, совместимого с Java 1.4.
                                            Реально в runtime вместо типа T подставляется ближайший тип, прописанный в extends, и используются приведения типов. Они, естественно, будут безопасными, т.к. компилятор уже все проверил.
                                              0
                                              Кстати, это одна из причин, почему нельзя сделать так:

                                                  public class SuperClass<T> {
                                                      public void setT(T obj) {
                                                         //...
                                                      }
                                                  }
                                                  
                                                  public class SubClass<Number> extends SuperClass<Number> {
                                                      // Здесь будет ошибка компиляции, т.к. метод с такой сигнатуров уже есть
                                                      public void setT(Object obj) {
                                                          //...
                                                      }
                                              
                                                      public void setT(Number obj) {
                                                          //...
                                                      }
                                                  }
                                              
                                                0
                                                Интересный пример. Правильно ли я понимаю, что в байткоде SubClass будет метод с сигнатурой public void setT (Object obj), в котором будет приведение входного параметра к Number? Почему-то всегда был уверен, что компилятор меняет сигнатуру для конечного типа.
                                            0
                                            IMHO, решать проблему надо в корне.
                                            generic'и нужно заменить на template'ы с поддержкой примитивных типов.
                                            А если следовать парадигме обратной совместимости, то добавить рядом.
                                            Синтаксис различимый вполне можно сделать.
                                            Преобразование между ними — тоже.
                                            Зато приседаний станет гораздо меньше.
                                              0
                                              Вот, кажется, для оптимизации целых вычислений в JIT именно MutableInteger может быть очень полезен.

                                              Only users with full accounts can post comments. Log in, please.