Теория дженериков в java или как на практике ставить скобки
Содержание:
Загрузка статического и динамического класса
- Добавление класса для запуска в JVM называется загрузкой класса.
- Классы загружаются статично с помощью нового оператора.
- Первый класс загружается через метод static main(). Затем подгружаются остальные классы.
- В серверных проектах отсутствует main(), поскольку сервер сам отвечает за всю инфраструктуру. Первый класс для загрузки отмечается в config файле. Довольно часто фреймворк реализует метод main() и предоставляет API. Пример: Контейнерный класс вызывает метод init() в сервлетах.
- main нужен для запуска Java-программы из командной строки в JVM.
- Если при загрузке статического класса не находится ссылка на класс, то выбрасывается NoClassDefinationFoundException.
- Динамические классы загружаются через программный вызов при выполнении. Пример: Class.forName(String ClassName);
- ClassNotFoundException выбрасывается при загрузке динамического класса.
2.1 Целочисленные типы (Integral types)
Integer-типы и тип char все вместе называются integral-типами.
2.1.1 Символьный тип char
Тип char является беззнаковым. Его значения это беззнаковые целые, которые обозначают все 65536 (216) символов из 16-битной Unicode таблицы символов. Эта таблица включает буквы, цифры и специальные символы.
Диапазон символьных значений |
Первые 128 символов Unicode-таблицы такие же как и в 7-битной ASCII таблице символов. А первые 256 символов Unicode-таблицы такие же как и в 8-битной ISO Latin-1 таблице символов.
Символьный литерал заключается в одинарные кавычки (‘). Все символьные литералы имеют примитивный тип данных char. Символьный литерал представляется в соответствии с 16-битной символьной таблицей Unicode, которая включает в себя символы 8-битной ISO-Latin-1 и 7-битной ASCII
Обратите внимание, что цифры (0-9), прописные буквы (A-Z) и строчные буквы (a-z) имеют Unicode значения расположенные соответственно их порядку
Примеры символьных литералов |
Unicode символ может быть указан как четырехзначное шестнадцатеричное число (т.е. 16-бит) с префиксом \u.Escape-последовательности (escape sequences) определяются специальные символы. Для определения символьного литерала они должны быть заключены в одинарные кавычки. Например, символьные литералы \t и \u0009 являются эквивалентными. Однако символьные литералы \u000a и \u000d не должны использоваться в исходном коде для представления новой строки и возврата каретки. Компилятор интерпретирует эти значения как терминаторы строк, что вызовет ошибку компиляции. Вам следует использовать escape-последовательности \n и \r для правильной интерпретации этих символов в коде.
Вы также можете использовать escape-последовательность \ddd чтобы указывать символьный литерал как восьмеричное значение, где вместо каждой d может быть восьмеричная цифра (0–7). Количество цифр должно быть три или меньше и восьмеричное значение не должно превышать \377; другими словами только первые 256 символов могут быть указаны в такой нотации.
Примеры \ddd escape-последовательностей |
2.1.2 Целые типы (integer types): byte, short, int, long
Целые типы данных (Integer data types) охватывают следующие типы данных: int, long, byte и short. Типы byte, short, int, long являются знаковыми.
Примитивные значения данных не являются объектами. Каждый примитивный тип данных определяет диапазон значений и операции над этими значениями с помощью операторов. Каждый примитивный тип данных имеет соответствующий ему класс-обертку (wrapper), который может быть использован для представления значения в форме объекта.
Диапазон целых значений |
По умолчанию для любого целочисленного литерала используется тип int. Но если добавить к целочисленному значению суффикс L (или l), то будет использоваться тип long. Обычно используют суффикс L потому что l похож на единицу, что может вызвать путаницу. Без суффикса L long-литералы 2000L и 0L будут интерпретированы как int-литералы. Не существует прямого способа чтобы указать short и byte литерал.
В дополнение целочисленные литералы могут быть указаны не только в десятичной системе счисления, но в двоичной (основание 2, цифры 0-1), восьмеричной (основание 8, цифры 0-7) и шестнадцатеричной (основание 16, цифры 0-9 и a-f). Цифры от a до f в шестнадцатеричной системе счисления соответствуют числам от 10 до 15. Двоичные, восьмеричные и шестнадцатеричные числа указываются с префиксами оснований 0b (или 0B), 0, и 0x (или 0X) соответственно. Шестнадцатеричные цифры от a до f также могут указываться в верхнем регистре (A-F). Негативные целые (например, -90) указываются с минусом (-) в виде префикса независимо от системы счисления (например, -0b1011010, -0132, или -0X5A).
Примеры десятичных, бинарных, восьмеричных и шестнадцатеричных литералов |
Приведение типов
Когда мы производим какие-то действия с переменными, то нужно следить за типами. Нельзя умножать котов на футбольные мячи, это противоречит здравому смыслу. Также и с переменными. Если вы присваиваете переменной одного типа значение другого типа, то вспоминайте теорию. Например, вы без проблем можете присвоить значение типа int переменной типа long, так как все числа из диапазона типа int гарантировано помещаются в диапазон чисел long. В этом случае Java выполнит преобразование автоматически, вы даже ничего не заметите.
Представим обратную картину — мы хотим присвоить переменной типа byte значение типа double. Java не сможет автоматически выполнить ваше желание. Не все числа типа double могут стать числом типа byte. Но часть чисел может, например, число 9. В таком случае используется так называемое приведение типов, чтобы подсказать Java о допустимости операции.
Итак, автоматическое преобразование типов осуществляется, если оба типа совместимы и длина целевого типа больше длины исходного типа. В этом случае происходит преобразование с расширением. Вы всегда можете преобразовать любое число типа byte в число типа int. Такая операция произойдёт без вашего участия автоматически.
Таблица выглядит следующим образом.
Сплошные линии обозначают преобразования, выполняемые без потери данных. Штриховые линии говорят о том, что при преобразовании может произойти потеря точности.
Типы целых чисел и чисел с плавающей точкой совместимы частично. Например, число 5 вполне может быть числом с плавающей точкой (5.0).
Совсем не совместимы, например, char и boolean.
С автоматическим приведением мы разобрались. Рассмотрим вариант, когда нужно преобразовать число типа int в число типа byte. Преобразование автоматически невозможно, поскольку byte меньше int. Но, например, число 99 вполне можно использовать и как int и как byte. В этом случае используется явное приведение типов, то есть преобразование из одного типа в другой (преобразование с сужением).
Выглядит это следующим образом:
Как видите, вы в скобках указываете тип, к которому нужно явно привести переменную.
Существует ещё вариант приведения с усечением. Это когда число с плавающей точкой приводится к целочисленному типу. В этом случае отбрасывается дробная часть (хвост). Например, число 3.14 будет усечено до числа 3:
Если размер целочисленной части слишком велик для целочисленного типа, то значение будет уменьшено до результата деления по модулю на диапазон целевого типа.
Например, попробуйте преобразовать число 454.874 в тип byte:
У меня вывелся удивительный результат: b равно -58.
Рассмотрим такой пример. Допустим у нас есть выражение, где промежуточное значение может выходить за пределы допустимого диапазона:
При умножении переменных a * b промежуточный результат вышел за пределы диапазона допустимых значений для типов byte. Java во время вычисления промежуточных результатов автоматически повышает тип каждого операнда до int и ошибки не происходит.
Это удобно, но может поставить в тупик в следующем примере:
С виду всё правильно. Если не слишком больше число типа byte, а итоговый результат тоже не выходит за диапазон допустимых значений. Но Java не позволит вам написать подобный код. Происходит следующее. Во время вычисления выражения тип операндов был автоматически повышен до int, как об этом говорилось выше. При этом тип результата тоже был повышен до int. Получается, что результат вычисления равен типу int, а мы пытаемся его присвоить переменной b, которая у нас объявлена как byte. И это несмотря на то, что итоговый результат может быть типом byte. Как же выйти из этого положения? Следует использовать явное приведение типов:
Мы рассмотрели единичные примеры. Пора обобщить и запомнить несколько правил.
Типы всех значений byte, short, char повышаются до типа int, как это было рассмотрено выше.
Если один операнд имеет тип long, то тип всего выражения повышается до long.
Если один операнд имеет тип float, то тип всего выражения повышается до float.
Если один операнд имеет тип double, то тип всего выражения повышается до double.
В первом промежуточном выражении (f * b) тип переменной b повышается до float и промежуточный результат также становится float. В следующем выражении (i / c) тип у переменной c повышается до int и промежуточный результат также становится типом int. В выражении (d * s) тип переменной s повышается до double и промежуточное выражение также становится double. В результате у нас появились три промежуточные значения типов: float, int, double. При сложении float и int мы получаем float, затем при вычитании с использованием float и double тип повышается до double, который и становится окончательным типом результата выражения.
Условные конструкции
Последнее обновление: 17.04.2018
Одним из фундаментальных элементов многих языков программирования являются условные конструкции. Данные конструкции
позволяют направить работу программы по одному из путей в зависимости от определенных условий.
В языке Java используются следующие условные конструкции: и
Конструкция if/else
Выражение if/else проверяет истинность некоторого условия и в зависимости от результатов проверки выполняет определенный код:
int num1 = 6; int num2 = 4; if(num1>num2){ System.out.println("Первое число больше второго"); }
После ключевого слова ставится условие. И если это условие выполняется, то срабатывает код, который помещен в далее в
блоке if после фигурных скобок. В качестве условий выступает операция сравнения двух чисел.
Так как, в данном случае первое число больше второго, то выражение истинно и возвращает значение
. Следовательно, управление переходит в блок кода после фигурных скобок и начинает выполнять содержащиеся там инструкции, а
конкретно метод . Если бы первое число оказалось бы меньше второго или равно ему, то инструкции в блоке if не выполнялись бы.
Но что, если мы захотим, чтобы при несоблюдении условия также выполнялись какие-либо действия? В этом случае мы можем добавить блок :
int num1 = 6; int num2 = 4; if(num1>num2){ System.out.println("Первое число больше второго"); } else{ System.out.println("Первое число меньше второго"); }
Но при сравнении чисел мы можем насчитать три состояния: первое число больше второго, первое число меньше второго и числа равны.
С помощью выражения , мы можем обрабатывать дополнительные условия:
int num1 = 6; int num2 = 8; if(num1>num2){ System.out.println("Первое число больше второго"); } else if(num1<num2){ System.out.println("Первое число меньше второго"); } else{ System.out.println("Числа равны"); }
Также мы можем соединить сразу несколько условий, используя логические операторы:
int num1 = 8; int num2 = 6; if(num1 > num2 && num1>7){ System.out.println("Первое число больше второго и больше 7"); }
Здесь блок if будет выполняться, если равно и одновременно равно .
Конструкция switch
Конструкция switch/case аналогична конструкции , так как позволяет обработать сразу несколько условий:
int num = 8; switch(num){ case 1: System.out.println("число равно 1"); break; case 8: System.out.println("число равно 8"); num++; break; case 9: System.out.println("число равно 9"); break; default: System.out.println("число не равно 1, 8, 9"); }
После ключевого слова switch в скобках идет сравниваемое выражение. Значение этого выражения последовательно сравнивается со значениями, помещенными после операторов
сase. И если совпадение найдено, то будет выполняет соответствующий блок сase.
В конце блока сase ставится оператор break, чтобы избежать выполнения других блоков. Например, если бы убрали оператор в следующем случае:
case 8: System.out.println("число равно 8"); num++; case 9: System.out.println("число равно 9"); break;
то выполнился бы блок , (поскольку переменная num равна 8). Но так как в этом блоке оператор break отсутствует, то начал бы выполняться блок .
Если мы хотим также обработать ситуацию, когда совпадения не будет найдено, то можно добавить блок default,
как в примере выше. Хотя блок default необязателен.
Также мы можем определить одно действие сразу для нескольких блоков case подряд:
int num = 3; int output = 0; switch(num){ case 1: output = 3; break; case 2: case 3: case 4: output = 6; break; case 5: output = 12; break; default: output = 24; } System.out.println(output);
Тернарная операция
Тернарную операция имеет следующий синтаксис: .
Таким образом, в этой операции участвуют сразу три операнда.
В зависимости от условия тернарная операция возвращает второй или третий операнд: если условие равно , то
возвращается второй операнд; если условие равно , то третий. Например:
int x=3; int y=2; int z = x<y? (x+y) : (x-y); System.out.println(z);
Здесь результатом тернарной операции является переменная z. Сначала проверяется условие .
И если оно соблюдается, то z будет равно второму операнду — (x+y), иначе z будет равно третьему операнду.
НазадВперед
JSON
Сериализация и Десериализация
JSON — невероятно удобный и полезный синтаксис для хранения и обмена данными. Java полностью поддерживает это.
Прим. перев. Для использования JSON из примера необходимо подключить библиотеку JSON Simple.
Вы можете сериализовать данные следующим образом:
Получается следующая строка JSON:
Десериализация в Java выглядит так:
Используемый в примере файл JSON (jsonDemoFile.json):
Прим. перев. В Java проектах очень часто для работы с JSON используют библиотеки Gson от Google или Jackson. Обе библиотеки очень популярны и хорошо поддерживаются. Попробуйте и их.
Конструкторы
- Их единственная цель — создавать экземпляры класса. Они вызываются в процессе создания объекта класса.
- Если конструктор с аргументами определен в классе, то нельзя будет работать со стандартным конструктором без аргументов (no-argument constructor) — придется их прописать.
- Java не поддерживает конструктор копирования.
- Имя конструктора и класса совпадает.
- Если конструктор вызывается из другого конструктора с синтаксисом this, то речь идет именно об этом объекте.
- В Java есть стандартный конструктор.
Приватный конструктор:
- Защищает класс от явного превращения в экземпляр.
- Построение объекта возможно только внутри конструктора.
- Используется в шаблоне «Одиночка» (Singleton).
Вопрос: Можно ли синхронизировать конструкторы в Java?
Нет. В Java запрещен многопоточный доступ к конструкторам объекта, поэтому необходимость в синхронизации отсутствует.
Вопрос: Наследуются ли конструкторы? Может ли подкласс вызывать конструктор родительского класса?
Конструкторы не наследуются. При переопределении конструктора суперклассов нарушается инкапсуляция языка. Конструктор родительского класса вызывается ключевым словом super.
Объекты
Все в Java является объектом.
Вернее, очень мало чего в Java объектом не является. Например, примитивные типы. Но это скорее редкое исключение, чем правило.
Что же такое объект?
Объект — это сгруппированные вместе данные и методы для того, чтобы эти данные обрабатывать. Когда мы говорим о данных, имеем в виду переменные, конечно.
Про переменные объекта говорят, что это «данные объекта» или «состояние объекта».
Про методы объекта говорят: это «поведение объекта». Состояние объекта (переменные объекта) принято менять только с помощью методов того же объекта. Менять переменные объекта напрямую (не через методы объекта) считается дурным тоном.
У каждого объекта, как и у каждой переменной, есть тип. Этот тип определяется один раз при создании объекта и поменять его в дальнейшем нельзя. Типом объекта считается его класс.
У каждого объекта есть собственная копия переменных класса (полей класса). Если в классе была объявлена нестатическая переменная int a, и ваша программа во время работы создала 10 объектов этого класса, теперь в каждом объекте есть собственная переменная int a.
Взаимодействие с объектом
Самый удобный способ работы с объектом — сохранить ссылку на объект в переменную, и потом вызывать методы у этой переменной. Выглядит это для вас знакомо:
Где — это переменная, которая хранит в себе ссылку на объект, а метод — это метод класса объекта.
Если вы хотите обратиться к полю (переменной) объекта, то тоже нужно использовать оператор точка:
Где — это переменная, которая хранит в себе ссылку на объект, а поле — это переменная класса (поле объекта).
Примитивные типы в Java
Тип переменной может быть создан разработчиком путем композиции из предопределенного набора типов, обычно называемых примитивными типами.
В Java есть 8 примитивных типов, и каждый из них предназначен для представления определенного типа информации и использования определенного количества памяти.
Кроме того, говоря об объявлении переменных, мы сказали, что «локальные» переменные должны быть инициализированы, чтобы избежать ошибок компиляции, это неверно для переменных экземпляра, для которых для каждого примитивного типа указано значение по умолчанию.
Тип | Кол-во памяти | Представленная информация | Значение по умолчанию |
byte | 8 бит | Переменная представляет значения в диапазоне от -128 до 127(включая крайние значения) | |
short | 16 бит | Целые числа в диапазоне от -32768 до 32767 | |
int | 32 бит | Целые числа в диапазоне от -2147483648 до 2147483647 | |
long | 64 бит | Целые числа в диапазоне от -9223372036854775808 до 9223372036854775807. 0.0L | |
float | 32 бит | Числа с плавающей запятой одинарной. В 32-битной версии, представляемый диапазон должен вычисляться с учетом знакового бита, 23-битной мантиссы и 8-битной экспоненты со значениями от -126 до 127. 0.0f | 0.0f |
double | 64 бит | Числа с плавающей запятой двойной. Точность представления чисел увеличивается за счет увеличения количества используемых битов. | 0.0d |
boolean | не указано, но достаточно одного бита | он служит для представления только двух значений: истина или ложь ( true false). Значение по умолчанию false | false |
char | 16 бит | Он используется для хранения символов кодировки Unicode ) в диапазоне (в шестнадцатеричном формате). | \u0000 |
Следует добавить, что каждая переменная объектного типа (т.е. не примитивного типа) по умолчанию инициализируется специальным значением null.
Следует отметить, что сама официальная документация Java сообщает, что, хотя значения по умолчанию гарантированы для всех неинициализированных полей, не инициализировать переменные — плохая практика, и поэтому вам следует избегать этого.
Абстрактный класс и интерфейс
- В интерфейсе отсутствует код реализации, а все методы являются абстрактными. То есть, все методы объявляются, но ни один не определяется.
- В абстрактном классе есть исполняемые и абстрактные методы.
- Класс реализует сколько угодно интерфейсов, но расширяет только один абстрактный класс.
- Методы абстрактного класса могут быть или не быть абстрактными.
- Абстрактный класс не может превратиться в экземпляр, но может стать подклассом.
- Все абстрактные методы должны определяться в подклассе, то есть, подкласс является абстрактным.
- Создавать экземпляры из интерфейса нельзя. Их можно реализовывать в других классах или расширять другими интерфейсами.
- Переменные интерфейсов конечные и статические. По умолчанию, все методы интерфейса публичные и абстрактные.
- Интерфейс не может содержать реализацию и не может превращаться в подкласс. Все переменные должны быть постоянными.
Сужение типов
А что насчет остальных вариантов? Что делать, если нужно переменной типа присвоить значение переменной типа ?
Представьте, что переменная — это корзина. У нас есть корзины разных размеров: 1, 2, 4 и 8 байт. При перекладывании пирожков из меньшей корзины в большую проблем не будет. А вот при перекладывании из большей в меньшую часть пирожков может потеряться.
Это преобразование — от типа большего размера к меньшему — называют сужением типа. При таком присваивании часть числа может просто не поместиться в новую переменную и «остаться за бортом».
При сужении типа мы должны явно показать компилятору, что мы не ошиблись, и отбрасывание части числа сделано осознанно. Для этого используется оператор приведения типа. Это имя типа в круглых скобочках.
В таких ситуациях Java-компилятор требует от программиста указывать оператор преобразования типа. Выглядит в общем виде он так:
Примеры:
Код | Описание |
---|---|
Каждый раз нужно явно указывать оператор преобразования типа |
В данном случае равно , и это кажется излишним. А что если бы было больше?
Код | Описание |
---|---|
Миллион отлично помещается и в тип , и в тип . А вот при присваивании миллиона переменной типа два первых байта были отброшены, и остались только два последних байта. А при присваивании типу вообще остался один последний байт.
Устройство чисел в памяти:
Тип | Двоичная запись | Десятичная запись |
---|---|---|
0b00000000000011110100001001000000 | ||
0b0100001001000000 | ||
0b01000000 |
Тип
Тип , как и тип , занимает два байта, но для их преобразования в друг друга всегда нужно использовать оператор приведения типа. Все дело в том, что тип знаковый, и может содержать значения от до , а тип беззнаковый, и может содержать значения от до .
В нельзя сохранить отрицательные числа, которые могут храниться в . А в нельзя сохранить числа больше , которые могут храниться в .
Методы
Универсальные методы очень похожи на универсальные классы. Они отличаются друг от друга только одним аспектом, заключающимся в том, что информация о области действия или типе находится только внутри метода. Универсальные методы вводят свои параметры типа.
public static <T> int countAllOccurrences(T[] list, T element) { int count = 0; if (element == null) { for ( T listElement : list ) if (listElement == null) count++; } else { for ( T listElement : list ) if (element.equals(listElement)) count++; } return count; }
Если вы передадите список String для поиска в этом методе, он будет работать нормально. Но если вы попытаетесь найти число в списке строк, это даст ошибку времени компиляции.
Что такое Generics в Java?
Дженерики в Java – это термин, обозначающий набор языковых возможностей, связанных с определением и использованием общих типов и методов. Общие методы Java отличаются от обычных типов данных и методов. До Generics мы использовали коллекцию для хранения любых типов объектов, т.е. неуниверсальных. Теперь Generics заставляет программиста Java хранить объекты определенного типа.
Если вы посмотрите на классы платформы Java-коллекции, то увидите, что большинство классов принимают параметр / аргумент типа Object. По сути, в этой форме они могут принимать любой тип Java в качестве аргумента и возвращать один и тот же объект или аргумент. Они в основном неоднородны, т.е. не похожего типа.
Иногда в приложении Java тип данных ввода не является фиксированным. Входными данными могут быть целое число, число с плавающей запятой или строка. Чтобы назначить ввод переменной правильного типа данных, необходимо было провести предварительные проверки.
В традиционном подходе после получения ввода проверяется тип данных ввода, а затем назначается переменная правого типа данных. При использовании этой логики длина кода и время выполнения были увеличены. Чтобы избежать этого, были введены дженерики.
Когда вы используете Generics, параметры в коде автоматически проверяются во время компиляции, и он устанавливает тип данных по умолчанию. Так что это то место, где вам нужна концепция обобщений в Java.
Существует 4 различных способа применения:
- Типовой класс
- Интерфейс
- Метод
- Конструктор
Оператор множественного выбора (switch)
Он применяется, когда нужно выполнить один из нескольких блоков кода в зависимости от значения аргумента.
Выгода от switch тем очевиднее, чем больше проверок и вариантов действий нам нужно.
Например:
В круглых скобках указывается аргумент для switch, а в каждом блоке case — чему этот аргумент должен равняться, чтобы выполнился код после двоеточия.
В нашем случае выполнится case 5, так как переменная dayOfWeekNum (порядок дня в неделе) равна 5.
В конце каждого блока case мы ставим break. Если этого не сделать, то выполнится также код из следующего блока case и так далее.
Например:
Если для нескольких значений аргумента нужно выполнять один и тот же код, то блоки case можно объединить.
Например, для будних дней (dayOfWeekNum от 1 до 5) будем выводить, какой это по счёту рабочий день, а для уикенда — первый или второй это выходной:
Теперь при значении переменной dayOfWeekNum от 1 до 5 выполнится один и тот же код, и для значений 6 и 7 — тоже одинаковый.
Также можно задать действие, если ни одно из условий не сработало. Делается это с помощью ключевого слова default:
Примечание. Слово break означает выход из switch…case. Поэтому если ваш блок default стоит не последним, то тоже завершайте его словом break, иначе выполнится код из следующего case.