Java Basics
В рамках 1-го Java файла может быть создано несколько классов, интерфейсов и прочих языковых элементов. При этом только 1 элемент может иметь модификатор доступа public
.
Для передачи аргумента в приложение, в виде единой строки, ее следует заключить в кавычки.
1 | java MyApp "multi-valued variable" single variables |
Приложение только с 1 классом можно запустить без компиляции.
1 | java MyApp.java MyArg |
Определение classapath
Может быть использовано несколько способов:
1 | java -cp mylibs pack.with.classes.MyClass |
Компиляция в директорию
1 | java -d mylibs MyApp.java |
Создание jar
1 | jar -cvf myJarLib.jar |
Для передачи директории, которую нужно поместить в jar
используется флаг -c
1 | jar -cvf myJarLib.jar -c myoutput/dir |
На экзамене следует уделять внимание нумерации строк. Если кусок кода начинает нумерацию с 1, следует обратить внимание на наличие
import
,package
и прочего.
Конструктор. Последовательность
Блоки всегда выполняются последовательно в соответствии с тем, как они указаны в классе. Аналогично, переменные инициализируются так же последовательно согласно правилам инициализации и порядку объявления.
Правила инициализации:
static
блоки иstatic
переменные всех родителей.static
блоки иstatic
переменные для собственного класса. Выполняется последовательно, согласно порядку объявления.- Конструкторы родителей последовательно, начиная с самого раннего. Если принудительно super вызван не будет, будет выполнен его родительский конструктор без аргументов.
- Блоки и переменные последовательно для собственного класса.
- Конструктор целевого класса
Пример:
1 | public class MyClass { |
В случае наличия родительских классов, для них точно так же актуальна указанная выше последовательность инициализации. Важно отметить что порядок инициализации классов - от самого раннего к ближайшему (например, Object
-> Number
-> Integer
-> MyCustomInteger
).
Примитивные типы
int
char
short
byte
float
double
Поскольку 0 то же число, допустимые границы следует учитывать как -max
- +max-1
. 0 будет рассматриваться как положительное значение.
Short/char
Данные типы могут переходить друг в друга без потерь, поскольку имеют одинаковую размерность:
1 | short x = 'd'; // int = 100 |
Double/float
Хранятся в виде двух чисел в форме a*10b
.
Создание чисел в разных системах
- Восьмеричная. Префиксом является 0:
017
- Шестнадцатеричная. Префиксом является 0x или 0X:
0x1F
- Бинарная. Префиксом является 0b или 0B:
0b10010011
Использование undescore
Для удобства можно использовать нижнее подчеркивание (_
):
1 | int x = 1_000_00_2; |
Единственным правилом является соблюдение условия, при котором между подчеркиванием должно стоять хотя бы одно число. Компилятор отбрасывает все подчеркивания и в скомпилированном представлении будет храниться только число.
Именование переменных
Может содержать только $
, _
, числа (но только не первым символом) и буквы.
Локальные переменные
Для локальных переменных характерно то, что они не получают начальное значение для примитивов.:
1 | class Example { |
Переменные экземпляра
Внутренняя для объекта переменная, по умолчанию принимает следующие значения:
1 | boolean = false |
В конструкторе де-факто выполняется фоновая инициализация в значение по умолчанию. Даже если конструктор не определен явно, он все равно присутствует и инициализирует все instance переменные.
var
Ключевое слово может быть использовано только для local
переменных. Единожды инициированная, не может менять свой тип (но может менять значение). Тип же определяется при создании (де-факто, на уровне компиляции):
1 | var x = 1; |
Множественное создание в одном выражении так же запрещены:
1 | var i=1, x = 2; // ошибка компиляции. var запрещено использовать для совместного объявления |
Нельзя присваивать null
изначально (компилятор не знает какого он типа). Но допускается указание будущего типа для пустого указателя:
1 | var x = null; // ошибка компиляции. компилятор не может определить тип |
var
в параметрах методов использовать нельзя - они не являются local
переменными:
1 | public void method(var x, String x) {} // ошибка компиляции. |
Для переменной имя var
допускается:
1 | var var = "1"; |
Но при этом, var
- это имя (псевдо-)типа, а следовательно это ключевое слово, которое запрещено для определения класса:
1 | public class var {} // ошибка компиляции. var не может быть использован для объявления типа |
Операторы
Последовательность операторов (по приоритету):
<exp>++
,<exp>--
++<exp>
,--<exp>
!
,~
,(type-cast)
,-
(указатель отрицательный),+
(указатель положительный)*
,\
,%
+
,-
<<
,>>
,>>>
<
,>
,<=
,>=
,instanceof
==
,!=
&
,^
,|
&&
,||
<exp> ? <exp1> : <exp2>
=
,+=
,-=
,*=
,\=
,%=
,&=
,^=
,|=
,<<=
,>>=
,>>>=
->
При равных приоритетах, операция выполняется слева направо.
Инкрементация
<exp>++
и <exp>--
добавляют +1 и -1 только после возврата значения. Т.е.:
1 | int x = 0; |
++<exp>
и --<exp>
сначала выполняет операцию +1 или -1 и только потом возвращает новое значение:
1 | int x = 0; |
Пример очередности операций:
1 | int x = 3; |
Дополнительный пример:
1 | int i = 10; |
И еще:
1 | double x = 3D; |
Компилятор (условно) увидит следующее: 2 \* 2 / 4(1) \* 4(2) + 3(3)
.
- это будет первое действие, но эффект от него будет “виден” только после факта присвоения нового значения для переменной
x
- второй шаг, результат которого скажется на переменную x моментально - ее значение увеличится на
1
и станет равно4
- последовательно выполняются арифметические действия:
2 * 2 -> 4 / 4 -> 1 * 4
. Поскольку во время выполнения второй операции переменнаяx
будет иметь значаение4
, а результатx--
будет применен только после выполнения всей арифметической последовательности (на шаге 3) - последнее действие,
4 + 3
. будет выполно присвоение результатаx--
, но ранее++x
уже увеличил значение на1
, а следовательноx
будет равен3
Оператор =+
не выполняет прибавление + присвоение (в отличае от +=
!); фактически данная операция выполняет следующие действия: i = (i+=1) + (i+=1) + (i)
Если пример с =+
относительно предсказуем, то применительно к -=
и =-
ситуация сложнее:
1 | int x= 1; |
Стоит поменять на -=
:
1 | y -= x + 2; // y = 7, x = 1 |
Необходимо следить за позицией оператора, поскольку при перестановке в примере выше, результат будет разительно различаться:
1 | int i = 10; |
Оператор ~
Побитовый разворот можно посчитать используя очень простую формулу: ~x = -1 * x - 1
Преобразование при операциях
Правила преобразования следующие:
- При операциях с разными типами, выполняется приведение и большему (в битах) типу.
- При операциях с плавающей точкой выполняется преобразование в тип с плавающей точкой.
- При операциях с
byte
,short
,char
- всегда сначала преобразуется вint
, а затем обратно (если это требуется) - Если результат операции (точнее, результирующий тип) тот, который требуется, то дополнительное преобразование не выполняется.
Правило 3 не распространяется на унарные операции - ++
, --
- они выполняются первыми.
Для принудительного преобразования значение к меньшему типу, используя (type-cast)
. При этом, преобразование может изменить ожидаемый результат максимальным значением типа:
1 | short x = (short) 123456; // x = 20678 |
Или перескакивание в положительное значение из отрицательного и наоборот:
1 | int x = 2147483647 + 1; // -2147483647 |
При операциях присваивания могут возникать ошибки понижения типов:
1 | long x = 1L; |
Можно выполнить так:
1 | long x = 1L; |
Особенности преобразования при присвоении
Скрытые преобразования типов Java компилятор выполняет в том случае если используются переменные или типы данных большего размера. В момент компиляции, Java может понять какой тип значения достаточен для выполнения операции.
1 | short i = 10; |
В данном примере Java компилятор понимает что short
и byte
значения достаточны для хранения в них предопределенных значений (10 и 80, соответственно). Но если в результате компиляции значение будет превышать допустимый размер, компилятор выдаст ошибку:
1 | short i = 32767+ 1; // ошибка компиляции. значение слишком большое для short |
Но в случае присвоения значения с использованием переменной будет выдана ошибка, поскольку компилятор “видит” только факт присвоения нового значения переменной по ходу выполнения, а следовательно, не может знать будет ли значение выходить за границы типа или нет:
1 | short x = 1; |
Даже при сложении нескольких “маленьких” типов:
1 | short x = 1; |
Это правило касается только short
, byte
, char
и любой попытке понижения типа (например, int x = long + int
).
Сравнение
При сравнении null
с null
, Java считает их идентичными:
1 | boolean a = null == null; // a = true |
instanceof
Начиная с Java 15 в оператор instanceof
была добавлена конвертация в объект проверяемого типа, что помогает сократить количество кода и повысить читабельность. Ранее для проверки и дальнейшего преобразования приходилось делать следующее:
1 | Object number = Integer.valueOf(10); |
Но начиная с Java 15 достаточно:
1 | if(number instanceof Integer intNumber) { |
При проверке на instanceof не может возникать ClassCastException
, что расширяет возможности до следующей записи:
1 | if(number instanceof Integer intNumber && intNumber > 1) { |
Переменным, над которыми выполнили конвертацию можно присваивать модификатор final
, чтобы не допустить ее изменения.
Конвертация имеет несколько ограничений:
- Нельзя выполнять конвертация с instancof в тот же тип, которым уже является проверяемый операнд:
1 | Integer number = Integer.valueOf(10); |
- Конвертацию (как и саму проверку
instanceof
) можно выполнить только по отношению к родственному типу:
1 | Integer number = Integer.valueOf(10); |
- Как и в случае других видов блоков, те переменные, которые были объявлены в рамках блока, будут доступны только в нем (1 важное исключение будет ниже!). Область видимости может быть даже ограничена блоком условия:
1 | Number number = Integer.valueOf(10); |
Преобразованная переменная может быть использована только в том случае если все условия описанные ранее имееют шанс выполниться.
1 | if(!(number instanceof Integer intNumber && intNumber > 1)) { ... } |
Но примечательно что в случае отрицательной конвертации преобразованный объект будет(!) доступен при выходе из блока и только если if
блок завершает текущее выполнение (т.е. выполняет continue, break
или return
), т.е. вероятности того что intNumber
не будет получен в участке использования исключена:
1 | Number number = Double.valueOf(1D); |
Касаемо строки с успешым обращением к intNumber
компилятор посчитает так: если условие instanceof
будет удовлетворено и объект будет не(!) Integer
, а так же, ход выполнения работы прерывается при этом условии, то весьма логично рассматривать объект исключительно как Integer (поскольку в блоке на проверку не instanceof
программа совершила прерывание).
Как указывалось в рекомендациях (ссылка!), отрицательные условия сложны в прочтении и в реальном коде лучше, по возможности, их преобразовывать в позитивные сценарии. Но если преобразование кода невозможно, самым доступным будет рассмотрение такого участка зеркально, т.е. попытаться переложить сценарий в позитивный и мысленно пройти его по позитивному сценарию.
Проверка instanceof
и null
Проверка null
на соответствие какому-либо типу всегда выдает false
:
1 | boolean isObject = (null instanceof String); // isObject = false |
Но для instanceof
правый операнд не может быть null
- будет выдана ошибка компиляции:
1 | boolean isNull = "string" instanceof null; // ошибка компиляции. сравнение невозможно |
На этапе компиляции будет выполнена проверка возможности для полиморфизма и если ее нет, будет выведена ошибка:
1 | String x = ...; |
Так же, instanceof с проверкой переменной, имеющей значение null, всегда будет false.
Оператор ^
Проверяет чтобы одно значение было true
, и другое false
(любые), тогда вернет true
:
1 | boolean xor1 = true ^ false; // true |
Оператор || , | и &&
Логические операторы, которые предполагают что оба операнда будут выполнены.
Оператор &
Правый операнд будет выполнен только если левый вернет true
.
switch
Поддерживаемые типы:
int
byte
short
char
string
enum
Character
Byte
Short
Integer
String
var
- если он представляет какой-либо из представленных выше
Следует заметить что
long
иLong
запрещены для использования в switch.
В случае если default
попадается на пути обработки (например, если он в середине), он всегда выполняется. Например:
1 | var x = 2; |
Необходимо следить за операторами контроля хода выполнения! Так например, стоит добавить
break
(илиreturn
) в блокdefault
в примере выше, и доcase 2
выполнение не дойдет.
В блоках допускается использование переменных, но только final
(или effective final
), а так же формируемые только при компиляции:
1 | final int getX() { |
Компилятор сам пытается перевести тип данных в целевой для проверки, но если сделать это невозможно, то возникает ошибка (в частности, на понижении):
1 | short test = 10; |
Начиная с Java 14 появилась возможность указывать значения для опций через запятую (“,”) - раньше для того же эффекта использовались опции без остановок (без continue, break или return).
1 | switch(pet) { |
Начиная с Java 14 можно написать так:
1 | switch(pet) { |
Условие switch
(как и if
) не требует наличия перечня условий, т.е. запись ниже допустима:
1 | switch(pet) {} |
Для опций в качестве проверок можно указывать только те значения, которые известны на момент компиляции. Переменные:
1 | final int v1 = 10 * 20; |
Switch выражения
Аналогичны классическому switch
, но позволяют существенно сократить код. В отличает от классического switch
, дают возможность получать результат выполнения switch
в форме какого-то значения:
1 | String action = switch(pet) { |
Ранее для достижения подобного эффекта приходилось оборачивать код в метод, который выполнял вычислительные (return
) действия. После каждого условия (branch) обязательно должна присутствовать точка с запятой (;
).
В случае использования switch
выражения без присвоения переменной или без return, компилятор выдаст ошибку! В случае если switch
не затрагивает все допустимые значения (например, для int
сложно охватить все допустимые числа в границах int
числа), то default становится обязательным:
1 | int x = 1; |
Существует еще несколько ограничений:
- Все условия должны возвращать одинаковый тип
- В случае если какое-то условие (branch) в
switch
выражении состоит из несколиких строк (возможно, условий и прочих конструкций), то оно должно выполнятьyield
(yield
можно сравнить сreturn
) - Условие
default
обязателен (в классическомswich
- условие (branch) не является таковым) в случае если не выполнен охват всех допустимых значений дляswitch
Не всегда в switch
выражении достаточно просто выдавать какой-то результат в случае удовлетворения условия (branch) - иногда необходимо что-то высчитать в рамках условия, сделать вложенное условие и т.д. Для этого применяется оператор yield
, который предоставляет значение в качестве результата вычисления одного из условий (branch) для switch
выражения:
1 | var action = switch(pet) { |
Важное примечание: если используются
switch
выражения, каждое условие должно иметь какой-то результат, будь то используя значение напрямую, либо после дополнительных вычислений.
Так же для switch
выражений существует дополнительно условие, отличающее его от классического switch
- он должен реализовывать обработку всех вероятных условий. Для достижения этого:
- если для проверки
switch
выражения поступаетenum
, то можно обработать все доступные значения из этогоenum
. Важно, что даже если все значенияenum
будут перечислены в качестве условий (branch), то добавлениеdefault
ошибки не выдаст. - если все
enum
обработать невозможно, или тип данных уswitch
неenum
- достаточно добавить обработкуdefault
условия (branch). Данный вариант предпочтительнее, поскольку даже в случае расширения enum, компилятор выдаст ошибку.
switch
не обязательно должен что-то производить в качестве результата, но если произведение либо должно отсутствовать для всех case
, либо присутствовать для всех!
1 | int result = 0; |
for
В блоке инициализации все переменные должны быть одного типа:
1 | for(long x=1L, int y=1; x<5; x++) {} // ошибка компиляции. попытка объявления переменной y иного типа |
На что обращать внимание:
- Проверка на бесконечность цикла
- В цикле for допускается несколько действий в блоке обновления
1 | for(int i=0, j=0; i<10 && j<5; i++, j++) { ... } |
- Запрещена повторная инициализация существующей переменной
- В блоке инициализации запрещено объявлять несколько переменных с разными типами:
1 | for(int i=0, long j=0L; i<10 && j<5; i++, j++) { ... } |
- Использование переменных, объявленных в блоке инициализации можно использовать только в рамках цикла.
for-each
Может быть использован только для:
- Java массива
- Объекта, который наследуется от
Iterable
Маркировка
Java допускает использование label для маркировки блоков кода. Затем, использование данного label позволяют выполнять переходы между блоками, прерывание циклов и прочую навигацию.
При операторе break можно указать label того блока, который необходимо остановить (по умолчанию останавливает выполнение текущего блока). После вызова break label прерывание касается всех блоков, дочерних для целевого - т.е.:
1 | while(...) { |
break
прервет выполнение текущего выполнения цикла SOME_LABEL (и всех дочерних блоков), но не затронет цикл 1, который начнет новый цикл с label = SOME_LABEL.
Аналогично работает и continue
в сочетании с label - управление передается тому блоку, который необходимо выполнить. При этом, последующие команды текущего блока + блока, который должны были выполниться или дочерние к label, останутся нетронутыми (т.е. не выполнятся).
Java массивы
Необходимо следить за объявлением массивов. Так, например:
1 | int[] a, b[]; // a[], b[][] |
Для вывода массива в виде строки, разделенной “,” можно использовать статичный метод Arrays.toString(T[])
.
Для сортировки лучше использовать Arrays.sort()
.
Для поиска - Arrays.binarySearch()
. Лучше выполнять на заранее отсортированном массиве - так операция будет выполняться быстрее. Результат операции зависит от массива:
Результат метода | Значение | Массив | Выполнение |
---|---|---|---|
Найдено | Идекс элемента в массиве | Отсортированный | Быстрое, зависит от позиции элемента |
Неотростированный | Медленное, зависит от позиции элемента | ||
Не найдено | Индекс того элемента, где предполагалось бы нахождение элементов, но -N-1 | Отсортированный | Полный обход |
Неотростированный | Полный обход |
Если массив не будет отсортирован, то поведение будет довольно трудно предугадать!
Arrays.compare(T[], T[])
Метод сравнивает 2 массива. Сравнение выполняется согласно следующим правилам:
- Если длинна массивов одинакова и все элементы массивов одинаковы и в одинаковом порядке - возвращается 0;
- Если все элементы одинаковы, но во втором массиве присутствуют дополнительные элементы, то возвращается отрицательное значение (<= -1)
- Если все элементы одинаковы, но в первом массиве присутствуют дополнительные элементы, то возвращается положительное значение (>= 1)
- При обходе массива сравниваются элементы с одинаковым индексом, и:
- Если в первом массиве значение при сравнении меньше, то возвращается отрицательное значение (<= -1)
- Если в первом массиве значение при сравнении больше, то возвращается положительное значение (>= 1)
- Если элементы массива одинаковы, то выполняется проверка следующего элемента
Примечания по сравнению элементов:
null
всегда считается меньше чем любое другое не-null
значение;- Числа сравниваются в порядке убывания;
- Строки в соответствии со сравнением строк (точнее, таблицей символов - например, буквы в соответствии с алфавитным порядком);
- Если элементы реализуют тип
Comparable
, то элементы сравниваются посредством вызова compareTo; - Допускается вызов метода только для массивов с примитивными типами, строками или собственными типами, но в этом случае накладывается ряд ограничений:
- объекты из массивов должны реализовывать интерфейс
Comparable
- объекты из массивов должны реализовывать интерфейс
1 | var a = new A[] {new A(1), new A(2), new A(7), new A(3)}; |
В примере выше оба типа реализуют Comparable<Object>
, но стоит поменять дженерик (generic) тип и компилятор выдаст ошибку в связи с тем что типы массивов не будут соответствовать:
1 | int result = Arrays.compare(a, b); // ошибка компиляции. попытка сравнить объекты разных типов |
Любая детализация для Comparable
с указанием массивов нек имеющих общего наследуемого типа, будет рассматриваться как сравнение массивов с отличающимися типами и будет выдана ошибка компиляции:
1 | int result = Arrays.compare(a, b); |
- можно передать реализацию интерфейса Comparator, которая и будет сравнивать объекты из массивов
Arrays.missmatch(T[], T[])
Метод выполняет сравнение схожее с compare, но возвращает индекс певого элемента первого массива, который не совпал с элементом второго массива с таким же индексом. Если массивы одинаковы - результатом Arrays.mismatch
будет -1 (в таком случае, результатом Arrays.compare
должен быть 0).
Autoboxing и Unboxing
Конвертация происходит автоматически. Важно учесть что autoboxing в меньший тип производится автоматически, но в старший тип компилятор потребует принудительной конвертации.
1 | Short s = 1; |
При unboxing’е может быть выброшено исключение NullPointerException
:
1 | Integer i1 = null; |
Чеклист
- Особое внимание на очередность инициализации
- Обращать внимание на именование переменных. Оно может не соответствовать правилам
- Следить за инициализацией локальныйх переменных, они частно не инициализируются
- Важным является конвертация из одного типа в другой! важно следить не только за возможностью конвертации, но и за обрезанием значения (например, при чрезмерно большом
long
вint
). По частоте появления:- попытка понижения типа
- для
short
,byte
иchar
попытка присвоения с потенциально большим типом, чем допустим у переменной
- Следить за указанием
var
в качестве параметраvar
не может быть указано только в качестве имени класса- Сделаить за идентификацией типа для
var
. Он должен быть определен при инициализациия переменной.
1 | var x = null; |
Инициализация так же не может быть отложенной
1 | var x; |
- Следить за очередностью операций, особенно, инкрементами (они используются часто) - они всегда выполняются первыми. Для инкрементов так же следует обращать внимание на позицию инкремента:
x++/--
- инкремент после возврата;++/--x
- инкремент до возврат null == null
- всегдаtrue
null instanceof Class
- всегдаfalse
- При выполнении
instanceof
необходимо следить за связанностью классов! - В
switch
следить за типами проверяемых объектов. Следить за позициейdefault
, а так же, логическими операторами прерывания. - В
switch
следить за используемыми локальных переменными - они должны бытьfinal
илиeffective final
- Вычислительная
switch
должна либо возвращать что-то у всехcase
, либо ни у одного. Тип возврата так же должен быть одинаковым (можно преобразовать вObject
и тогда компилятор увидит только его) - В блоке инициализации для
for
поддерживается инициализация только одинаковых типов - Unboxing в больший тип потребует принудительной типизации: int -> Long - выдаст ошибку