Java Основы: Введение

Java Basics


В рамках 1-го Java файла может быть создано несколько классов, интерфейсов и прочих языковых элементов. При этом только 1 элемент может иметь модификатор доступа public.

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

1
java MyApp "multi-valued variable" single variables

Приложение только с 1 классом можно запустить без компиляции.

1
java MyApp.java MyArg

Определение classapath

Может быть использовано несколько способов:

1
2
3
4
5
java -cp mylibs pack.with.classes.MyClass

java -classpath mylibs pack.with.classes.MyClass

java --class-path mylibs pack.with.classes.MyClass

Компиляция в директорию

1
2
3
java -d mylibs MyApp.java

java -d mylibs pack/with/classes/MyClass.java

Создание jar

1
2
3
jar -cvf myJarLib.jar

jar  --create --verbose --file myJarLib.jar

Для передачи директории, которую нужно поместить в jar используется флаг -c

1
jar -cvf myJarLib.jar -c myoutput/dir

На экзамене следует уделять внимание нумерации строк. Если кусок кода начинает нумерацию с 1, следует обратить внимание на наличие import, package и прочего.

Конструктор. Последовательность

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

Правила инициализации:

  1. static блоки и static переменные всех родителей.
  2. static блоки и static переменные для собственного класса. Выполняется последовательно, согласно порядку объявления.
  3. Конструкторы родителей последовательно, начиная с самого раннего. Если принудительно super вызван не будет, будет выполнен его родительский конструктор без аргументов.
  4. Блоки и переменные последовательно для собственного класса.
  5. Конструктор целевого класса

Пример:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class MyClass {
    static X x = new X(); // 1
    static {
        ... // 2
    }
    private Z z = new Z(); // 5
    static Y y = new Y(); // 3
    {
    ... // 6
    }

    public MyClass() {
        super(); // 10
        x2 = new X(); // 11
    }

    {
    ... // 7
    }

    public Y y2 = new Y(); // 8
    static Z z2 = new Z(); // 4

    {
    ... // 9
    }
    X x2;
}

В случае наличия родительских классов, для них точно так же актуальна указанная выше последовательность инициализации. Важно отметить что порядок инициализации классов - от самого раннего к ближайшему (например, Object -> Number -> Integer -> MyCustomInteger).

Примитивные типы

int
char
short
byte
float
double

Поскольку 0 то же число, допустимые границы следует учитывать как -max - +max-1. 0 будет рассматриваться как положительное значение.

Short/char

Данные типы могут переходить друг в друга без потерь, поскольку имеют одинаковую размерность:

1
2
short x = 'd';              //  int = 100
char y = (short) 83; // char = ‘s’

Double/float

Хранятся в виде двух чисел в форме a*10b.

Создание чисел в разных системах

  • Восьмеричная. Префиксом является 0: 017
  • Шестнадцатеричная. Префиксом является 0x или 0X: 0x1F
  • Бинарная. Префиксом является 0b или 0B: 0b10010011

Использование undescore

Для удобства можно использовать нижнее подчеркивание (_):

1
2
int x = 1_000_00_2;
double y = 1_000_0.00_01;

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

Именование переменных

Может содержать только $, _, числа (но только не первым символом) и буквы.

Локальные переменные

Для локальных переменных характерно то, что они не получают начальное значение для примитивов.:

1
2
3
4
5
6
7
8
class Example {
private int y;

public void math() {
int x; // ошибка компиляции. переменная x не инициализирована (в отличае от y, которая будет иметь значение 0)
return x + y;
}
}

Переменные экземпляра

Внутренняя для объекта переменная, по умолчанию принимает следующие значения:

1
2
3
4
5
boolean = false
byte, short, long, int = 0
float, double = 0.0
char = ‘\u0000’ (null)
<reference> = null

В конструкторе де-факто выполняется фоновая инициализация в значение по умолчанию. Даже если конструктор не определен явно, он все равно присутствует и инициализирует все instance переменные.

var

Ключевое слово может быть использовано только для local переменных. Единожды инициированная, не может менять свой тип (но может менять значение). Тип же определяется при создании (де-факто, на уровне компиляции):

1
2
3
var x = 1;
var y = 'y';
y = 1; // ошибка компиляции. компилятор определил переменную с типом char и поменять его уже невозможно

Множественное создание в одном выражении так же запрещены:

1
var i=1, x = 2;    //  ошибка компиляции. var запрещено использовать для совместного объявления

Нельзя присваивать null изначально (компилятор не знает какого он типа). Но допускается указание будущего типа для пустого указателя:

1
2
var x = null;           // ошибка компиляции. компилятор не может определить тип
var y = (String) null;

var в параметрах методов использовать нельзя - они не являются local переменными:

1
public void method(var x, String x) {}      // ошибка компиляции.

Для переменной имя var допускается:

1
var var = "1";

Но при этом, var - это имя (псевдо-)типа, а следовательно это ключевое слово, которое запрещено для определения класса:

1
public class var {}  // ошибка компиляции. var не может быть использован для объявления типа

Операторы

Последовательность операторов (по приоритету):

  1. <exp>++, <exp>--
  2. ++<exp>, --<exp>
  3. !, ~, (type-cast), - (указатель отрицательный), + (указатель положительный)
  4. *, \, %
  5. +, -
  6. <<, >>, >>>
  7. <, >, <=,  >=, instanceof
  8. ==, !=
  9. &, ^, |
  10. &&, ||
  11. <exp> ? <exp1> : <exp2>
  12. =, +=, -=, *=, \=, %=, &=, ^=, |=, <<=, >>=, >>>=
  13. ->

При равных приоритетах, операция выполняется слева направо.

Инкрементация

<exp>++ и <exp>-- добавляют +1 и -1 только после возврата значения. Т.е.:

1
2
int x = 0;
int y = x++; // y = 0, x = 1

++<exp> и --<exp> сначала выполняет операцию +1 или -1 и только потом возвращает новое значение:

1
2
int x = 0;
int y = ++x; // y = 1, x = 1

Пример очередности операций:

1
2
int x = 3;
int y = ++x \* 5 / x--; // x-- , ++x , \*, /, =

Дополнительный пример:

1
2
int i = 10;
i =+ (i++) + (i++) + (i++); // i = 33

И еще:

1
2
double x = 3D;
double y = 2D \* 2D / x-- \* ++x + x; // y = 7, x = 3

Компилятор (условно) увидит следующее: 2 \* 2 / 4(1) \* 4(2) + 3(3).

  1. это будет первое действие, но эффект от него будет “виден” только после факта присвоения нового значения для переменной x
  2. второй шаг, результат которого скажется на переменную x моментально - ее значение увеличится на 1 и станет равно 4
  3. последовательно выполняются арифметические действия: 2 * 2 -> 4 / 4 -> 1 * 4. Поскольку во время выполнения второй операции переменная x будет иметь значаение 4, а результат x-- будет применен только после выполнения всей арифметической последовательности (на шаге 3)
  4. последнее действие, 4 + 3. будет выполно присвоение результата x--, но ранее ++x уже увеличил значение на 1, а следовательно x будет равен 3

Оператор =+ не выполняет прибавление + присвоение (в отличае от +=!); фактически данная операция выполняет следующие действия: i = (i+=1) + (i+=1) + (i) Если пример с =+ относительно предсказуем, то применительно к -= и =- ситуация сложнее:

1
2
3
int x= 1;
int y = 10;
y =- x + 2; // y = 1, x = 1

Стоит поменять на -=:

1
y -= x + 2;    //  y = 7, x = 1

Необходимо следить за позицией оператора, поскольку при перестановке в примере выше, результат будет разительно различаться:

1
2
3
4
5
6
int i = 10;
i += (i++) + (i++) + (i++); // = 43
i=10;
i += (i++) + (i++) + (i++); // = 43
I=10;
i += (i++) + (++i) + (++i); // = 45

Оператор ~

Побитовый разворот можно посчитать используя очень простую формулу: ~x = -1 * x - 1

Преобразование при операциях

Правила преобразования следующие:

  1. При операциях с разными типами, выполняется приведение и большему (в битах) типу.
  2. При операциях с плавающей точкой выполняется преобразование в тип с плавающей точкой.
  3. При операциях с byte, short, char - всегда сначала преобразуется в int, а затем обратно (если это требуется)
  4. Если результат операции (точнее, результирующий тип) тот, который требуется, то дополнительное преобразование не выполняется.

Правило 3 не распространяется на унарные операции - ++, -- - они выполняются первыми.

Для принудительного преобразования значение к меньшему типу, используя (type-cast). При этом, преобразование может изменить ожидаемый результат максимальным значением типа:

1
short x = (short) 123456;           // x = 20678

Или перескакивание в положительное значение из отрицательного и наоборот:

1
int x = 2147483647 + 1;             // -2147483647

При операциях присваивания могут возникать ошибки понижения типов:

1
2
3
long x = 1L;
int y = 2;
y = x * y; // ошибка компиляции. попытка присвоения long к int (результат x * y будет long, присвоение к int y)

Можно выполнить так:

1
2
3
long x = 1L;
int y = 2;
y *= x;

Особенности преобразования при присвоении

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

1
2
3
4
short i = 10;
byte j = 8 * 10;
short k = Short.MAX\_VALUE;
byte l = Byte.MIN;

В данном примере Java компилятор понимает что short и byte значения достаточны для хранения в них предопределенных значений (10 и 80, соответственно). Но если в результате компиляции значение будет превышать допустимый размер, компилятор выдаст ошибку:

1
2
3
4
short i = 32767+ 1;                 //  ошибка компиляции. значение слишком большое для short
byte j = -128 - 1;   // ошибка компиляции. значение слишком маленькое для byte
short i = Short.MAX_VALUE + 1; // ошибка компиляции. значение слишком большое для short
byte j = Byte.MIN - 1; // ошибка компиляции. значение слишком маленькое для byte

Но в случае присвоения значения с использованием переменной будет выдана ошибка, поскольку компилятор “видит” только факт присвоения нового значения переменной по ходу выполнения, а следовательно, не может знать будет ли значение выходить за границы типа или нет:

1
2
short x = 1;
short i = 1 + x; // ошибка компиляции. попытка присвоения int к short, требуется явное понижение типа

Даже при сложении нескольких “маленьких” типов:

1
2
3
short x = 1;
short y = 1;
short z = x + y; // ошибка компиляции. требуется явное преобразование в short

Это правило касается только short, byte, char и любой попытке понижения типа (например, int x = long + int).

Сравнение

При сравнении null с null, Java считает их идентичными:

1
boolean a = null == null;       // a = true

instanceof

Начиная с Java 15 в оператор instanceof была добавлена конвертация в объект проверяемого типа, что помогает сократить количество кода и повысить читабельность. Ранее для проверки и дальнейшего преобразования приходилось делать следующее:

1
2
3
4
5
Object number = Integer.valueOf(10);
if(number instanceof Integer) {
Integer intNumber = (Integer) number;
if(intNumber > 1) ...
}

Но начиная с Java 15 достаточно:

1
2
3
if(number instanceof Integer intNumber) {
if(intNumber > 1) ...
}

При проверке на instanceof не может возникать ClassCastException, что расширяет возможности до следующей записи:

1
2
3
if(number instanceof Integer intNumber && intNumber > 1) {
...
}

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

Конвертация имеет несколько ограничений:

  1. Нельзя выполнять конвертация с instancof в тот же тип, которым уже является проверяемый операнд:
1
2
3
Integer number = Integer.valueOf(10);
if(number instanceof Integer) { ... }   // ok
if(number instanceof Integer intNumber) { ... }   // ошибка компиляции. попытка проверки/преобразования из Integer в Integer
  1. Конвертацию (как и саму проверку instanceof) можно выполнить только по отношению к родственному типу:
1
2
Integer number = Integer.valueOf(10);
if(number instanceof String str) {...}   // ошибка копиляции. попытка проверки/преобразования к неродственному классу String
  1. Как и в случае других видов блоков, те переменные, которые были объявлены в рамках блока, будут доступны только в нем (1 важное исключение будет ниже!). Область видимости может быть даже ограничена блоком условия:
1
2
Number number = Integer.valueOf(10);
if(!(number instanceof Integer intNumber) && intNumber > 1) { ... } // ошибка компиляции. попытка обращения к intNumber в случае, когда это условие заведомо не может быть выполнено, поскольку первое условие проверяет не(!) соответствие типу Number

Преобразованная переменная может быть использована только в том случае если все условия описанные ранее имееют шанс выполниться.

1
if(!(number instanceof Integer intNumber && intNumber > 1)) { ... }

Но примечательно что в случае отрицательной конвертации преобразованный объект будет(!) доступен при выходе из блока и только если if блок завершает текущее выполнение (т.е. выполняет continue, break или return), т.е. вероятности того что intNumber не будет получен в участке использования исключена:

1
2
3
4
5
6
Number number = Double.valueOf(1D);
if(!(number instanceof Integer intNumber && intNumber > 1)) {
if(intNumber > 10) { ... }   // ошибка компиляции. intNumber может не быть Integer и в этом случае инициализация будет невозможна, а следовательно и данное обращение выполнено быть не может
return;
}
if(intNumber > 1) {...}  // ok

Касаемо строки с успешым обращением к intNumber компилятор посчитает так: если условие instanceof будет удовлетворено и объект будет не(!) Integer , а так же, ход выполнения работы прерывается при этом условии, то весьма логично рассматривать объект исключительно как Integer (поскольку в блоке на проверку не instanceof программа совершила прерывание).

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

Проверка instanceof и null

Проверка null на соответствие какому-либо типу всегда выдает false:

1
2
3
boolean isObject = (null instanceof String);    //  isObject = false
String x = null;
isObject = x instanceof String; // isObject = false

Но для instanceof правый операнд не может быть null - будет выдана ошибка компиляции:

1
boolean isNull = "string" instanceof null;    //  ошибка компиляции. сравнение невозможно

На этапе компиляции будет выполнена проверка возможности для полиморфизма и если ее нет, будет выведена ошибка:

1
2
String x = ...;
if(x instanceof Number) {} // ошибка компиляции. String не может быть преобразован в Number

Так же, instanceof с проверкой переменной, имеющей значение null, всегда будет false.

Оператор ^

Проверяет чтобы одно значение было true, и другое false (любые), тогда вернет true:

1
2
3
4
boolean xor1 = true ^ false;    // true
xor = true ^ true; // false
xor = false ^ false; // false
xor = false ^ true; // true

Оператор || , | и &&

Логические операторы, которые предполагают что оба операнда будут выполнены.

Оператор &

Правый операнд будет выполнен только если левый вернет true.

switch

Поддерживаемые типы:

int
byte
short
char
string
enum
Character
Byte
Short
Integer
String
var - если он представляет какой-либо из представленных выше

Следует заметить что long и Long запрещены для использования в switch.

В случае если default попадается на пути обработки (например, если он в середине), он всегда выполняется. Например:

1
2
3
4
5
6
var x = 2;
switch(x) {
case 3: System.out.println("3");
default: System.out.println("0");  // вывод
case 2: System.out.println("2");  // вывод
}

Необходимо следить за операторами контроля хода выполнения! Так например, стоит добавить break (или return) в блок default в примере выше, и до case 2 выполнение не дойдет.

В блоках допускается использование переменных, но только final (или effective final), а так же формируемые только при компиляции:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
final int getX() {
return 3;
}

void example() {
long a = 1;
final int b = 2;
final int c = getX();
final short d = 4;
final long e = 5;
int target = 6;
switch(target) {
case a: ...;  // ошибка компиляции. (1) a не является финальной; (2) требуется понижение типа int
case b: ...;
case c: ...;  // ошибка компиляции. высчитывается в момент выполнения (хотя объявлена как final!)
case getX(): ...; // ошибка компиляции. высчитывается в момент выполнения
    case d: ...;
    case (int) e: ...;
default: ...;
case 1+2: ...;
}

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

1
2
3
4
5
6
7
8
9
10
short test = 10;
final int a = 5;
final int b = Integer.MAX_VALUE;
switch(test) {
case a: ...;
case b: ...;   // ошибка компиляции. требуется понижение типа до short
case (short) b: ...; // сравнение будет не с Integer.MAX_VALUE, а с Short.MAX_VALUE!
case 1+2: ...;
default: ...;
}

Начиная с Java 14 появилась возможность указывать значения для опций через запятую (“,”) - раньше для того же эффекта использовались опции без остановок (без continue, break или return).

1
2
3
4
5
6
7
switch(pet) {
case DOG:
case CAT: 
case RABBIT: return "feed";
case WOLF: return "escape";
default: return "ignore";
}

Начиная с Java 14 можно написать так:

1
2
3
4
5
switch(pet) {
    case DOG, CAT, RABBIT: return "feed";
    case WOLF: return "escape";
    default: return "ignore";
}

Условие switch (как и if) не требует наличия перечня условий, т.е. запись ниже допустима:

1
switch(pet) {}

Для опций в качестве проверок можно указывать только те значения, которые известны на момент компиляции. Переменные:

1
2
3
4
5
6
7
8
9
final int v1 = 10 * 20;
final int v2 = compute(v1);
int v3 = v1;
swtich(petAge) {
    case 5 * 10: ...;
    case v1: ...;
    case v2: ...; // ошибка компиляции. переменная v2 высчитывается по ходу выполнения
    case v3: ...; // ошибка компиляции. переменная v3 высчитывается по ходу выполнения
}

Switch выражения

Аналогичны классическому switch, но позволяют существенно сократить код. В отличает от классического switch, дают возможность получать результат выполнения switch в форме какого-то значения:

1
2
3
4
5
String action = switch(pet) {
    case DOG, CAT, RABBIT -> "feed";
    case WOLF -> "escape";
    default -> "ignore";
};

Ранее для достижения подобного эффекта приходилось оборачивать код в метод, который выполнял вычислительные (return) действия. После каждого условия (branch) обязательно должна присутствовать точка с запятой (;).

В случае использования switch выражения без присвоения переменной или без return, компилятор выдаст ошибку! В случае если switch не затрагивает все допустимые значения (например, для int сложно охватить все допустимые числа в границах int числа), то default становится обязательным:

1
2
3
4
5
int x = 1;
String y = switch(x) {    //  ошибка компиляции. не представлены все допустимые значения int или default
    case 1 -> "good";
    case 0 -> "bad";
}

Существует еще несколько ограничений:

  1. Все условия должны возвращать одинаковый тип
  2. В случае если какое-то условие (branch) в switch выражении  состоит из несколиких строк (возможно, условий и прочих конструкций), то оно должно выполнять yield (yield можно сравнить с return)
  3. Условие default обязателен (в классическом swich - условие (branch) не является таковым) в случае если не выполнен охват всех допустимых значений для switch

Не всегда в switch выражении достаточно просто выдавать какой-то результат в случае удовлетворения условия (branch) - иногда необходимо что-то высчитать в рамках условия, сделать вложенное условие и т.д. Для этого применяется оператор yield, который предоставляет значение в качестве результата вычисления одного из условий (branch) для switch выражения:

1
2
3
4
5
6
7
8
9
var action = switch(pet) {
case DOG, CAT, RABBIT -> "feed";
case WOLF -> {
if(petSize > 10) yield "escape";
system.out.println("don’t panic!");
yield "ignore";
}
default -> "ignore";
};

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

Так же для switch выражений существует дополнительно условие, отличающее его от классического switch - он должен реализовывать обработку всех вероятных условий. Для достижения этого:

  • если для проверки switch выражения поступает enum, то можно обработать все доступные значения из этого enum. Важно, что даже если все значения enum будут перечислены в качестве условий (branch), то добавление default ошибки не выдаст.
  • если все enum обработать невозможно, или тип данных у switch не enum - достаточно добавить обработку default условия (branch). Данный вариант предпочтительнее, поскольку даже в случае расширения enum, компилятор выдаст ошибку.

switch не обязательно должен что-то производить в качестве результата, но если произведение либо должно отсутствовать для всех case, либо присутствовать для всех!

1
2
3
4
5
6
7
8
9
10
11
12
int result = 0;
switch (sign) {
    case '+' -> result = a + b;
    case '^' -> {
        int tmp = 1;
        for (int i = 1; i <= b; i++) {
            tmp *= a;
        }
        yield tmp;    //  ошибка компиляции. yield присутствует только для одного case (либо убрать, либо добавить для всех
    }
    default -> result = 0;
};

for

В блоке инициализации все переменные должны быть одного типа:

1
for(long x=1L, int y=1; x<5; x++) {}    // ошибка компиляции. попытка объявления переменной y иного типа

На что обращать внимание:

  1. Проверка на бесконечность цикла
  2. В цикле for допускается несколько действий в блоке обновления
1
for(int i=0, j=0; i<10 && j<5; i++, j++) { ... }
  1. Запрещена повторная инициализация существующей переменной
  2. В блоке инициализации запрещено объявлять несколько переменных с разными типами:
1
for(int i=0, long j=0L; i<10 && j<5; i++, j++) { ... }
  1. Использование переменных, объявленных в блоке инициализации можно использовать только в рамках цикла.

for-each

Может быть использован только для:

  • Java массива
  • Объекта, который наследуется от Iterable

Маркировка

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

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

1
2
3
4
5
6
7
8
9
while(...) {
SOME_LABEL: for(...) {
for(...) {
while(...) {
if(...) break SOME_LABEL;
}
}
}
}

break прервет выполнение текущего выполнения цикла SOME_LABEL (и всех дочерних блоков), но не затронет цикл 1, который начнет новый цикл с label = SOME_LABEL.

Аналогично работает и continue в сочетании с label - управление передается тому блоку, который необходимо выполнить. При этом, последующие команды текущего блока + блока, который должны были выполниться или дочерние к label, останутся нетронутыми (т.е. не выполнятся).

Java массивы

Необходимо следить за объявлением массивов. Так, например:

1
2
3
int[] a, b[];       //  a[], b[][]
int a, b[]; // a, b[]
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
2
3
4
5
var a = new A[] {new A(1), new A(2), new A(7), new A(3)};
var b = new B[] {new B(1), new B(2), new B(7), new B(3)};
int result  = Arrays.compare(a, b);
class A implements Comparable {...}
class B implements Comparable {...}

В примере выше оба типа реализуют Comparable<Object>, но стоит поменять дженерик (generic) тип и компилятор выдаст ошибку в связи с тем что типы массивов не будут соответствовать:

1
2
3
int result  = Arrays.compare(a, b);     //  ошибка компиляции. попытка сравнить объекты разных типов
class A implements Comparable<A> {...}
class B implements Comparable<A> {...}

Любая детализация для Comparable с указанием массивов нек имеющих общего наследуемого типа, будет рассматриваться как сравнение массивов с отличающимися типами и будет выдана ошибка компиляции:

1
2
3
4
5
int result  = Arrays.compare(a, b);
interface X {}
interface Y extends X {}
class A implements Comparable<X> implements X {...}
class B implements Comparable<Object> implements Y {...}
  • можно передать реализацию интерфейса Comparator, которая и будет сравнивать объекты из массивов

Arrays.missmatch(T[], T[])

Метод выполняет сравнение схожее с compare, но возвращает индекс певого элемента первого массива, который не совпал с элементом второго массива с таким же индексом. Если массивы одинаковы - результатом Arrays.mismatch будет -1 (в таком случае, результатом Arrays.compare должен быть 0).

Autoboxing и Unboxing

Конвертация происходит автоматически. Важно учесть что autoboxing в меньший тип производится автоматически, но в старший тип компилятор потребует принудительной конвертации.

1
2
3
4
5
Short s = 1;
Integer i = 1;
Long l = 1; // ошибка компиляции. попытка выполнить boxing с повышением типа (из int в Long)
Long l2 = 1L;
long l3 = 1;

При unboxing’е может быть выброшено исключение NullPointerException:

1
2
Integer i1 = null;
int i2 = i1; // ошибка ходы выполнения. возникнет NullPointerException при попытке выполнения unboxing

Чеклист

  1. Особое внимание на очередность инициализации
  2. Обращать внимание на именование переменных. Оно может не соответствовать правилам
  3. Следить за инициализацией локальныйх переменных, они частно не инициализируются
  4. Важным является конвертация из одного типа в другой! важно следить не только за возможностью конвертации, но и за обрезанием значения (например, при чрезмерно большом long в int). По частоте появления:
    1. попытка понижения типа
    2. для short, byte и char попытка присвоения с потенциально большим типом, чем допустим у переменной
  5. Следить за указанием var в качестве параметра
    1. var не может быть указано только в качестве имени класса
    2. Сделаить за идентификацией типа для var. Он должен быть определен при инициализациия переменной.
1
var x = null;

Инициализация так же не может быть отложенной

1
2
var x;
... x = "string";
  1. Следить за очередностью операций, особенно, инкрементами (они используются часто) - они всегда выполняются первыми. Для инкрементов так же следует обращать внимание на позицию инкремента: x++/-- - инкремент после возврата; ++/--x - инкремент до возврат
  2. null == null - всегда true
  3. null instanceof Class - всегда false
  4. При выполнении instanceof необходимо следить за связанностью классов!
  5. В switch следить за типами проверяемых объектов. Следить за позицией default, а так же, логическими операторами прерывания.
  6. В switch следить за используемыми локальных переменными - они должны быть final или effective final
  7. Вычислительная switch должна либо возвращать что-то у всех case, либо ни у одного. Тип возврата так же должен быть одинаковым (можно преобразовать в Object и тогда компилятор увидит только его)
  8. В блоке инициализации для for поддерживается инициализация только одинаковых типов
  9. Unboxing в больший тип потребует принудительной типизации: int -> Long - выдаст ошибку
 Comments
Comment plugin failed to load
Loading comment plugin