4 мая 2015 г.

Сравнение экземпляров классов-оберток примитивных типов

Так как мы только начали изучать ООП, то эта статья может показаться сложной, но прочитать ее все же стоит, чтобы в сознании отложилось понимание что из себя представляют ссылочные типы данных. Мы уже немного говорили об этом во введении в типы данных Java.

Сейчас уже следует узнать, что при сравнении переменных с использованием оператора сравнения (==) примитивные типы сравниваются по значению, а ссылочные по ссылке. Сейчас эта фраза может быть не особо понятной, но надеюсь эта статья прояснит ситуацию.

Значения переменных примитивных типов хранятся в стеке, и каждая переменная примитивного типа хранит свое независимое значение в стеке. Со ссылочными типами вообще, и с классами обертками примитивов, в частности, все не так. В стеке хранится только ссылка на объект. Сам же объект храниться в так называемой куче (heap).

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

Также нужно обратить внимание, что JVM использует больше памяти, чем занимает куча. Например, для методов Java и стеков потоков выделяется память отдельно от кучи.

Размер кучи зависит от используемой платформы, но, как правило, это где-то между 2 и 128 Кб.

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

Ну для начала хватит уже теории и немного попрактикуемся, чтобы понять о чем идет речь.

Сперва рассмотрим простой пример и его вывод:

CW002

CW003

Здесь стоит обратить внимание на область выделенную желтым цветом. Все вроде как обычно и как должно быть. То есть все так как мы привыкли уже при работе с примитивными типами данных.

Ни что, вроде бы, не предвещает беды :) но надо быть на чеку. Грабли по всюду!!!

Программер, бди! И не бзди! :)

Теперь немного изменим программу и посмотрим что будет :)

CW004

CW005

Grabli

Отани то! Грабельки наши!!! Ну куда ж без них то родимых!

Надеюсь вы заметили в чем разница?


Теперь intA и intB хотя и равны 500, но между собой не равны :) Верней сказать они ссылаются на объекты которые содержат значение поля int равное 500.

 

Так в чем же дело? Давайте разбираться. Во первых надо отметить что intA и intB это ссылки на объекты типа Integer. Но почему же тогда при значении 50 они были равны, а при значении 500 они вдруг стали не равны? Все дело в кэше. Кэш в данном случае это не деньги :) Это массив для кэширования значений, который находится во внутреннем классе класса Integer. Так как мы классы, а тем более внутренние классы, еще не проходили то это все, скорей всего, вам сейчас не будет понятно. Но все же для информации надо завязать себе узелок на память. Теперь посмотрим на этот внутренний класс:

CW006

Собственно здесь даже о классах пока ни чего можно и не знать, достаточно просто хоть немного понимать английский язык. И так кэшируемыми значениями для значений в классе Integer являются значения от -128 до 127 по умолчанию. Но этот диапазон можно изменить при помощи опций инициализации JVM или же через аргумент запуска в командной строке. И тогда начнут происходить чудеса :)

CW001

Как видите в данном случае я задал аргумент в командной строке где переопределил кэш и теперь у нас сравнение работает правильно, так как значение 500 попадает в указанный диапазон.

Все тоже самое можно сделать и из среды разработки, просто, в данном случае, через командную строку это гораздо нагляднее и быстрее, как мне кажется.

Вообще объекты надо сравнивать через метод .equals(), который есть у каждого класса, так как он есть у класса Object, от которого наследуются все классы в Java. Но это тема для большого и отдельного разговора о наследовании и классе Object. И пока об этом говорить еще рано. В принципе, сейчас, даже эта статья еще сложна для понимания если вы только начали изучать Java, но все же ее стоит дочитать до конца, а потом когда уже у вас будет больше знаний и опыта, вернуться к ней и прочитать еще разок :) Я привел эту статью сейчас чтобы предупредить о возможных граблях.


Но это еще не все грабли :) Вас еще ждет много неожиданных сюрпризов!

К нашему классу я дописал еще несколько строк. Теперь рассмотрим их и их вывод:

CW007

CW008

Опять же тут может быть пока не понятно, что такое valueOf() и оператор new, так как это мы еще не изучали. Вкратце скажу что valueOf() это метод класса Integer, а оператор new используется для создания новых объектов.

Пока на все внимание обращаем на вывод программы. Как видим при сравнении объектов Integer содержащих одинаковые значения, созданных с использованием оператора new, даже при значениях попадающие в диапазон кэширования, мы получаем в результате false. Это происходит потому, что это ссылки intA и intB указывают на разные объекты, а при использовании valueOf(), в данном примере, ссылки intA и intB указывают на один и тот же объект. От сюда вывод, что при использовании для сравнения оператора == для ссылочных типов, сравниваются ссылки, а не объекты. Чтобы сравнить объекты надо использовать метод equals():

println("Сравнение через equals() intA и intB дает " + intA.equals(intB));

Данный код нам выведет на экран: Сравнение через equals() intA и intB дает true

Теперь немного изменим значения и посмотрим на результат:

CW009

CW010

Когда мы задали значение 500, то получили вполне ожидаемый результат при сравнении ссылок. Но все же хорошо бы посмотреть на код метода valueOf().
CW011

Как видно из кода (хотя возможно вам пока это пока и не видно) при выходе за пределы значений по умолчанию метод возвращает новый объект типа Integer. Если же значение находится в пределах кэша, то оно берется от туда. Кэш из себя представляет просто массив значений типа Integer в заданном диапазоне, по умолчанию от -128 до 127.

Из всего вышесказанного следует:

  • Кэшируемыми значениями для Integer по умолчанию являются значения от -128 до 127
  • Диапазон значений по умолчанию для Integer можно изменить при помощи опций запуска JVM
  • Кэширование работает только в случае autoboxing
  • При создании объекта через оператор new кэширования не происходит
  • При сравнении объектов при помощи оператора == можно получить по морде гарблями ошибку
  • Объекты надо сравнивать при помощи метода equals()

Кэширование так же есть и для других объектов классов оберток примитивных типов:

  • Byte, Short, Long (диапазон от -128 до 127 включительно)
  • Charaster (диапазон от 0 до 127 включительно)
  • В отличие от Integer эти значения заданы жестко и изменить их параметрами запуска нельзя

Классы Float и Double не имеют механизма кэширования поскольку не могут представлять точные значения. Ну а Boolean кэшировать вообще бессмысленно.

Ну и на последок еще один примерчик и его вывод:

CW012

CW013

Хотя, опять же, многое может быть пока не понятно, но просто намотайте на ус :) ну или если нет усов завяжите узелок на память. Суть в том, что мой статический метод iHash возвращает хэш объекта, который однозначно его идентифицирует. Из вывода программы видно, что при присвоении ссылкам intA и intB значения 5, данные ссылки указывают на один и тот же объект, в данном случае находящийся в кэше. Просьба не путать хэш и кэш. А при присвоении этим ссылкам значения 500 происходит скрытый вызов оператора new и объекты создаются каждый раз новые, что видно по их хэшу, в то время как при присвоении этим ссылкам снова значения 5 они продолжают указывать, но тот же самый объект, поскольку он всегда хранится в кэше.

На этом, пожалуй, можно закончить эту статью. Как я уже и говорил, что если вы только начали изучать Java, то возможно все написанное тут будет вам не понятно. Но продолжайте читать этот блог и вы достигните просветления в Java :) и вам эта статья станет понятной :)

Размещать эту статью в другом месте блога мне представлялось мало логичным, так как она все же относится к классам оберткам для примитивных типов, которые мы уже начали обсуждать, но для понимания этой статьи нужно знать материал из последующих моих статей. Поэтому товарищи … вперед и с песней :)

4 комментария:

  1. Шикарно. Спасибо за подробные объяснения того, почему я получил по морде граблями :)
    Спасибо большое

    ОтветитьУдалить
  2. библиотечные методы hashCode() для Integer возвращают соответствующий int
    public int hashCode() {
    return Integer.hashCode(value);
    }

    public static int hashCode(int value) {
    return value;
    }
    Я думаю надо было бы привести листинг iHash потому что для тех кто попробует повторить эксперемент будет немножко удивлен результатом

    ОтветитьУдалить
    Ответы
    1. Было написано в предыдущих статьях

      static int iHash(String str){
      return System.identityHashCode(str);
      }

      Удалить