Главная страница
Навигация по странице:

  • AND () и OR (|)

  • private

  • Почему класс String неизменяем (Immutable)

  • Блок Примитивные типы


    Скачать 6.67 Mb.
    НазваниеБлок Примитивные типы
    АнкорJava Core
    Дата19.04.2022
    Размер6.67 Mb.
    Формат файлаdocx
    Имя файлаJava Core.docx
    ТипДокументы
    #485860
    страница1 из 9
      1   2   3   4   5   6   7   8   9


    Блок 2. Примитивные типы


    1. Какие примитивные типы есть в Java


    Размер типа boolean зависит от виртуальной машины. Он может занимать и 1 бит. Как правило, в массиве тип boolean занимает 1 байт, а как отдельная переменная 4 байт.


    Все примитивные типы передаются по значению.
    В целочисленных примитивах возможно переполнение данных. В этом случае (при выходе за пределы доступной памяти) значение просто перескочит на минимальное. К примеру, при добавлении к переменной типа byte, равной 127, единицы, значение этой переменной станет равно -128.
    Для каждого примитивного типа в Java есть класс-обертка. Классы-обертки полезны в нескольких случаях. К примеру, чтобы хранить числа и символы в коллекциях. Или, к примеру, чтобы выразить в программе факт отсутствия значения (null). Для упаковки примитивного типа в класс-обертку есть метод valueOf(). Для распаковки класса-обертки в примитив есть метод intValue() (для типа int). Но как правило примитивы приводятся к классам-оберткам просто через знак =. Такой синтаксический сахар.
    Для целочисленных классов-оберток существует пул примитивов, в чем-то похожий на String pool. Он хранит значения от -128 до 127. Но в Java пул примитивов может быть расширен.


    1. Что такое явные и неявные приведения, с чем связано их наличие

    2. Какие данные мы рискуем потерять при явных приведениях


    Безопасные приведения (не требующие явного указания типа):


    При приведении int к float, или long к float или double возможна потеря точности.
    При приведении byte и short к int необходимо явное приведение. При приведении лишняя часть СТАРШИХ битов отбрасывается.
    При приведении double к int необходимо явное приведение. При приведении дробная часть просто отбрасывается, а не округляется.
    При приведении float к int необходимо явное приведение. При приведении слишком большого числа (больше Integer.MAX_VALUE) переменной int будет присвоено максимально возможное значение переменной типа int.
    При приведении double к float необходимо явное приведение. При приведении слишком большого числа переменной float будет присвоено значение +бесконечность.
    При проведении арифметических операций возможно автоматическое расширение типов, т.е. сначала все операнды приводятся к единому типу. Оно действует по следующим правилам:


    • Если один из операндов имеет тип double, все операнды приводятся к double.

    • Иначе если один из операндов имеет тип float, все операнды приводятся к float.

    • Иначе если один из операндов имеет тип long, все операнды приводятся к long.

    • Иначе операнды приводятся к типу int.


    При сокращенных арифметических операциях (к примеру, а+=5) происходит неявное приведение.



    1. StringBuilder


    Строки в Java — это неизменяемые объекты (immutable). Так было сделано для того, чтобы класс-строку можно было сильно оптимизировать и использовать повсеместно. Именно поэтому в язык Java все же добавили тип String, который можно менять. Называется он StringBuilder.
    Часть его методов:


    Похожий класс, StringBuffer, используется при многопоточке, а StringBuider при одном потоке. Но при этом StringBuilder работает быстрее, поэтому при одном потоке выгоднее использовать его.


    1. Логические операторы




    Кроме стандартных операторов AND (&) и OR (|) существуют сокращённые операторы && и ||.

    Если взглянуть на таблицу, то видно, что результат выполнения оператора OR равен true, когда значение операнда A равно true, независимо от значения операнда B. Аналогично, результат выполнения оператора AND равен false, когда значение операнда A равно false, независимо от значения операнда B. Получается, что нам не нужно вычислять значение второго операнда, если результат можно определить уже по первому операнду. Это становится удобным в тех случаях, когда значение правого операнда зависит от значения левого.

    В языке Java есть также специальный тернарный условный оператор, которым можно заменить определённые типы операторов if-then-else - это оператор ?:

    Тернарный оператор использует три операнда. Выражение записывается в следующей форме:
    логическоеУсловие ? выражение1 : выражение2

    Если логическоеУсловие равно true, то вычисляется выражение1 и его результат становится результатом выполнения всего оператора. Если же логическоеУсловие равно false, то вычисляется выражение2, и его значение становится результатом работы оператора. Оба операнда выражение1 и выражение2 должны возвращать значение одинакового (или совместимого) типа.



    1. Тип char


    Char - символьный тип данных, но также может хранить числовое значение, которое, по сути, является кодировкой символа. Но это числовое значение поддерживает все арифметические операции, характерные для целочисленных типов.
    При проведении над переменными типа char арифметических операций, они приводятся к типу int.



    1. Неизменяемые типы


    Класс String отвечает за создание строк, состоящих из символов. А если быть точнее, заглянув в реализацию и посмотрев способ их хранения, то строки представляют собой массив символов (так было до Java 9). Начиная с Java 9 строки хранятся, как массив байт.
    Строки в Java являются immutable, т. е. неизменяемыми.

    Создать объект класса String можно двумя способами: при помощи строкового литерала и конструктора.

    Первый способ, а он является рекомендуемым, удобен и прост. Под строковым литералом понимается последовательность символов, заключенных в двойные кавычки.
    Класс String имеет в своем распоряжении множество конструкторов, которые могут принимать на вход данные разного типа. Например, строковый литерал или массив символов.
    Экземпляр класса String хранится в памяти, именуемой куча (heap), но есть некоторые нюансы. Если строка, созданная при помощи конструктора хранится непосредственно в куче, то строка, созданная как строковый литерал, уже хранится в специальном месте кучи — в так называемом пуле строк (string pool). В нем сохраняются исключительно уникальные значения строковых литералов, а не все строки подряд. Процесс помещения строк в пул называется интернирование (от англ. interning).

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


    В строке «Top» + «Java» создаются два строковых объекта со значениями «Top» и «Java», которые помещаются в пул. «Склеенные» строки образуют еще одну строку со значением «TopJava», ссылка на которую берется из пула строк (а не создается заново), т.к. она была интернирована в него ранее.

    Значения всех строковых литералов из данного примера известно на этапе компиляции.

    Иллюстративно итоговый результат выглядит так:


    Причиной получения false является то, что интернирование происходит не во время работы приложения (runtime), а во время компиляции. А т.к. значение строки str3 вычисляется во время выполнения приложения, то на этапе компиляции оно не известно и потому, не добавляется в пул строк.
    Теперь давайте рассмотрим детальнее процесс создания объекта String при помощи конструктора.

    Когда мы создаем экземпляр класса String с помощью оператора new, компилятор размещает строки в куче. При этом каждая строка, созданная таким способом, помещается в кучу (и имеет свою ссылку), даже если такое же значение уже есть в куче или в пуле строк.

    Создадим строки через интернирование и с помощью конструктора, а затем сравним их ссылки:

    В Java существует возможность вручную выполнить интернирование строки в пул путем вызова метода intern() у объекта типа String. Видоизменим приведенный ранее пример, добавив метод intern() к созданным при помощи конструктора строкам.



    Принимая во внимание всё вышесказанное, вы можете спросить: «Почему бы все строки сразу после их создания не добавлять в пул строк? Ведь это приведет к экономии памяти…». Да, среди достаточно большого количества программистов такое заблуждение присутствует. Именно заблуждение, поскольку не все учитывают дополнительные затраты виртуальной машины на процесс интернирования, а также падение производительности в целом. Можно сказать, что интернирование (в виде применения метода intern ()) рекомендуется вообще не использовать. Вместо интернирования необходимо использовать дедупликацию.
    Что же тогда делать, если мы создаем много объектов класса String? Нам ничего не мешает написать свой собственный пул строк, доступ к которому может быть быстрее, чем к пулу виртуальной машины. После того, как он справится со своей работой, его можно легко уничтожить.



    До Java версии 7 виртуальная машина размещала пул строк в области памяти под названием PermGen, которая имеет фиксированный размер и не может быть расширена во время выполнения приложения. Также следует отметить, что на эту область памяти не распространяется действие сборщика мусора.

    Риск интернирования строк в область PermGen (вместо кучи) заключается в том, что мы можем получить от JVM ошибку OutOfMemoryError, если будем интернировать слишком много строк (PermGen имеет фиксированный размер).

    Учтите, что однажды интернированную строку в версии Java ниже 7й нельзя деинтернировать: она будет занимать память программы даже тогда, когда перестанет быть нужна. Из этого следует, что чрезмерное интернирование строк может оказать негативный эффект, связанный с утечками памяти!

    Начиная с Java 8 пул строк размещается в куче, на которую распространяется процесс сборки мусора. Преимуществом данного подхода является снижение вероятности появления ошибки OutOfMemoryError, так как строки, на которые не будет ссылаться ни одна переменная в выполняемой программе, будут удалены сборщиком мусора из пула, что приведет к освобождению памяти.
    В Java 9 было внедрено новое представление для типа String, получившее название компактные строки (Compact Strings). Благодаря новому формату хранения строк (в зависимости от контента) делается выбор между массивом символов char[] и массивом байт byte[].

    Поскольку новый способ хранения объектов типа String использует кодировку UTF-16 лишь в том случае, когда в этом есть необходимость, объем памяти, занимаемый пулом строк в куче, будет значительно ниже, что в свою очередь уменьшит издержки работы сборщика мусора.

    Ключевые моменты:

    • Строки в Java представляют собой константы, которые не могут быть изменены

    • Создать объект класса String можно двумя способами: при помощи строкового литерала и конструктора

    • Строковый литерал сохраняется в пул строк, если до этого он там отсутствовал

    • Строка, созданная при помощи конструктора, сохраняется в heap, а не в пул строк

    • Java 6: Пул строк хранится в памяти фиксированного размера, именуемого PermGen.

    • Java 7, 8: Пул строк хранится в heap и соответственно, для пула строк можно использовать всю память приложения

    • При помощи параметра -XX:StringTableSize=N, где N — размер HashMap, можно изменить размер пула строк. Его размер является фиксированным, поскольку он реализован, как HashMap со списками в корзинах

    • Инженеры по оптимизации Java компании Oracle настоятельно не рекомендуют самостоятельно интернировать строки, поскольку это приводит к замедлению работы приложения. Их рекомендация — дедупликация.


    Как мы написали в самом начале, класс String представляет собой массив байт:

    private final byte[] value;

    А т.к. созданный экземпляр класса String нельзя модифицировать, т. е. содержимое массива value[] нельзя изменить, то его значение может быть безопасно использовано одновременно несколькими объектами String.

    Дедупликация представляет собой не что иное, как переприсваивание виртуальной машиной адресов поля value. Т. е. мы выполняем дедупликацию не объектов String, а массивов их байт. Поля value нескольких объектов типа String с одинаковым значением текста изначально ссылаются на разные участки памяти (разные массивы байт), а после дедупликации будут ссылаться на один и тот же участок памяти, содержащий массив байт.

    Кроме того, у нас все еще остаются накладные расходы в виде заголовка объекта, полей и др. Такие накладные расходы зависят от платформы/конфигурации и варьируются в пределах от 24 до 32 байт. Однако, для средней длины объекта String в 45 символов (90 байт + заголовок массива), это все еще значительные цифры. Принимая во внимание вышеперечисленное, актуальный выигрыш в экономии памяти может быть около 10%.
    Во время сборки мусора GC проверяет живые (имеющие рабочие ссылки) объекты в куче на возможность провести их дедупликацию. Ссылки на подходящие объекты вставляются в очередь для последующей обработки. Далее, происходит попытка дедупликации каждого объекта String из очереди, а затем удаление из нее ссылок на объекты, на которые они ссылаются. Так же для отслеживания всех уникальных массивов байт, используемых объектами String, используется хеш-таблица. При дедупликации в этой хеш-таблице выполняется поиск идентичных массивов байт (символов).

    При положительном результате, значение поля value объекта String переприсваивается так, чтобы указывать на этот существующий массив байт. Соответственно, предыдущий массив байт value становится ненужным, на него ничего не ссылается и впоследствии, он попадает под сборку мусора.

    При отрицательном результате, массив байт, соответствующий value, вставляется в хеш-таблицу, чтобы впоследствии быть использованным совместно с новым объектом String в какой-то другой момент в будущем.

    Как видим, дедупликация не сработала. Для ее активации необходимо в параметрах виртуальной машины указать -XX:+UseStringDeduplication, а также активировать сборщик мусора G1 (если он не используется по умолчанию), указав также -XX:+UseG1GC.
    Результат говорит о следующем: создав два объекта с помощью new, мы получили два разных объекта, с разными идентификационными хешами для массивов байт. Запустив сборщик мусора и подождав некоторое время (дедупликация не происходит мгновенно), мы видим, что хеши для двух объектов стали одинаковы (ссылаются на один и тот же массив).
    Ключевые моменты:

    • Дедупликация строк доступна с Java 8 Update 20

    • Она активируется параметром для виртуальной машины: -XX:+UseStringDeduplication

    • Дедупликация строк работает только со сборщиком мусора G1. Для его активации в Java 8 необходимо указать параметр для виртуальной машины -XX:+UseG1GC. Начиная с Java 9, G1 является сборщиком мусора по умолчанию

    • Опыты показывают, что применение дедупликации строк сокращает расходы кучи на примерно 10%, что в принципе неплохо, учитывая, что нам не нужно вносить изменение в код

    • Дедупликация строк работает в фоновом режиме без приостановления работы приложения

    • В отличие от пула строк, который применим только для строк, интернированных командой intern(), или строковых литералов, но не применим для строк, созданных динамически во время жизни приложения, дедупликация строк, применима для строк, созданных всеми этими способами

    Сегодня поговорим о специальном модификаторе final.

    Он, можно сказать, “цементирует” те участки нашей программы, где нам нужно постоянное, однозначное, не меняющееся поведение.

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

    Если в объявлении класса стоит модификатор final, это значит, что от данного класса нельзя наследоваться.

    В Java уже реализовано много final-классов. Наиболее известный из тех, которыми ты постоянно пользуешься — String.

    Кроме того, если класс объявлен как final, все его методы тоже становятся final.

    Что это значит?

    Если для метода указан модификатор final — этот метод нельзя переопределить.

    Теперь по поводу final-переменных. По-другому они называются константами.

    Во-первых (и в-главных), первое значение, присвоенное константе, нельзя изменить. Оно присваивается один раз и навсегда.

    Из того, что тебе железобетонно нужно запомнить уже сейчас — все классы-обертки над примитивными типами — неизменяемые.

    Integer, Byte, Character, Short, Boolean, Long, Double, Float — все эти классы создают Immutable объекты. Сюда же относятся и классы, используемые для создания больших чисел — BigInteger и BigDecimal.

    Мы недавно проходили исключения и затрагивали StackTrace.

    Так вот: объекты класса java.lang.StackTraceElement тоже неизменяемые.

    Виртуальная Машина Java (в дальнейшем JVM) обрабатывая код, запускает методы один за другим, начиная с метода main. Когда она доходит до очередного метода, говорится, что этот метод находится на вершине стека. После полного выполнения метода, он удаляется из стека, и сменяется следующим в очереди.

    Когда вы увидите стек-трейс в окне вывода, просто знайте, что первая строка — это то место, где проблема возникла, а остальные строки (если конечно они есть), куда исключение было передано вверх по стеку, обычно заканчивая методом main.

    Условия, при которых объекты становятся immutable:

    • Все поля класса объекта должны иметь модификатор private.

    • Класс не должен иметь сеттера, только геттер.

    • Данные ссылочного типа должны клонироваться (метод clone()) при работе геттера.

    • Сам класс должен иметь ключевое слово final, как и поля класса.

    Почему класс String неизменяем (Immutable)?

    У неизменности строк есть ряд неоспоримых преимуществ:

    • Строковый пул (String pool) возможен только благодаря тому, что строки в Java неизменяемы. Виртуальная машина имеет возможность сохранить много места в памяти (heap space) т.к. разные строковые переменные указывают на одну переменную в пуле. При изменяемости строк было бы невозможно реализовать интернирование, поскольку если какая-либо переменная изменит значение, это отразится также и на остальных переменных, ссылающихся на эту строку.

    • Изменяемость строк несло бы в себе потенциальную угрозу безопасности приложения. Поскольку в Java строки используются для передачи параметров для авторизации, открытия файлов и т.д. — неизменяемость позволяет избежать проблем с доступом.

    • Так как строка неизменяемая то, она безопасна для много поточности и один экземпляр строки может быть совместно использован различными потоками. Это позволяет избежать синхронизации для потокобезопасности. Таким образом, строки в Java полностью потокобезопасны.

    • Поскольку строка неизменная, её hashcode кэшируется в момент создания и нет никакой необходимости рассчитывать его снова. Это делает строку отличным кандидатом для ключа в Map и его обработка будет быстрее, чем других ключей HashMap. Поэтому строка наиболее часто используется в качестве ключа HashMap.

    Если резюмировать вышесказанное, то получаем, что основные причины неизменяемости String в Java это безопасность и наличие пула строк (String pool).

    • можно передавать строку между потоками не опасаясь, что она будет изменена

    • отсутствуют проблемы с синхронизацией потоков

    • отсутствие проблем с утечкой памяти

    • отсутствие проблем с доступом и безопасностью при использовании строк для передачи параметров авторизации, открытия файлов и т.д.

    • кэширование hashcode

    • Экономия памяти при использовании пула строк для хранения повторяющихся строк.

    Рекурсия

    Рекурсию порой сложно понять, особенно новичкам в программировании. Если говорить просто, то рекурсия – это функция, которая сама вызывает себя.

    Рекурсивная функция всегда должна знать, когда ей нужно остановиться. В рекурсивной функции всегда есть два случая: рекурсивный и граничный случаи. Рекурсивный случай – когда функция вызывает саму себя, а граничный – когда функция перестает себя вызывать. Наличие граничного случая и предотвращает зацикливание.

    Рекурсивные функции используют так называемый «Стек вызовов». Когда программа вызывает функцию, функция отправляется на верх стека вызовов. Это похоже на стопку книг, вы добавляете одну вещь за одни раз. Затем, когда вы готовы снять что-то обратно, вы всегда снимаете верхний элемент.

    Но в рекурсивном подходе нет стопки. Так как тогда алгоритм понимает в какой коробке следует искать? Ответ: «Стопка коробок» сохраняется в стеке. Формируется стек из наполовину выполненных обращений к функции, каждое из которых содержит свой наполовину выполненный список из коробок для просмотра. Стек следит за стопкой коробок для Вас!
    Любой алгоритм, реализованный с использованием рекурсии, также может быть реализован с использованием итерации.
      1   2   3   4   5   6   7   8   9


    написать администратору сайта