1. Типы данных и переменные

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

1.1. Базовые типы

Итак, рассмотрим систему встроенных базовых типов данных, которая используется для создания переменных в Java. А она представлена следующими типами.

boolean: хранит значение true или false.

boolean isActive = false;
boolean isAlive = true;

byte: хранит целое число от -128 до 127 и занимает 1 байт

byte a = 3;
byte b = 8;

short: хранит целое число от -32768 до 32767 и занимает 2 байта

short a = 3;
short b = 8;

int: хранит целое число от -2147483648 до 2147483647 и занимает 4 байта

int a = 4;
int b = 9;

long: хранит целое число от -9 223 372 036 854 775 808 до 9 223 372 036 854 775 807 и занимает 8 байт

long a = 5;
long b = 10;

double: хранит число с плавающей точкой двойной точности (стандарт IEEE 754) и занимает 8 байт

double x = 8.5;
double y = 2.7;

В качестве разделителя целой и дробной части в дробных литералах используется ТОЧКА.

float: хранит число с плавающей точкой одинарной точности (стандарт IEEE 754) и занимает 4 байта

float x = 8.5F;
float y = 2.7F;

char: хранит одиночный символ в кодировке Unicode и занимает 2 байта, поэтому диапазон хранимых значений от 0 до 65536.

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

1.1.1. Целые числа

Все целочисленные литералы, например, 10, 4, -5, воспринимаются как значения типа int, однако мы можем присваивать целочисленные литералы другим целочисленным типам: byte, long, short. В этом случае Java автоматически осуществляет соответствующие преобразования:

byte a = 1;
short b = 2;
long c = 2121;

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

int num111 = 0x6F; // шестнадцатеричная система, число 111
int num8 = 010; // восьмеричная система, число 8
int num13 = 0b1101; // двоичная система, число 13

Для задания шестнадцатеричного значения после символов 0x указывается число в шестнадцатеричном формате. Таким же образом восьмеричное значение указывается после символа 0, а двоичное значение - после символов 0b.

Также целые числа поддерживают разделение разрядов числа с помощью знака подчеркивания:

int x = 123_456;
int y = 234_567__789;
System.out.println(x); // 123456
System.out.println(y); // 234567789

1.1.2. Числа с плавающей точкой

При присвоении переменной типа float дробного литерала с плавающей точкой, например, 3.1, 4.5 и т.д., Java автоматически рассматривает этот литерал как значение типа double. Чтобы указать, что данное значение должно рассматриваться как float, нам надо использовать суффикс f:

float fl = 30.6f;
double db = 30.6;

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

1.2. Ссылочные типы

Все остальные за исключением примитивных. Если быть точным, то любой Object.

1.3. Переменные

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

Переменные объявляются следующим образом:

тип_данных имя_переменной;

Например, определим переменную, которая будет называться x и будет иметь тип int:

int x;

В этом выражении мы объявляем переменную x типа int. То есть x будет хранить некоторое число не больше 4 байт.

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

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

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

  • имя не может быть ключевым словом языка Java

Кроме того, при объявлении и последующем использовании надо учитывать, что Java - регистрозависимый язык, поэтому следующие объявления int num; и int NUM; будут представлять две разных переменных.

Объявив переменную, мы можем присвоить ей значений:

int x; // объявление переменной
x = 10; // присвоения значения
System.out.println(x); // 10

Также можно присвоить значение переменной при ее объявлении. Этот процесс называется инициализацией:

int x = 10; // объявление и инициализация переменной
System.out.println(x); // 10

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

int x;
System.out.println(x);

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

int x, y;
x = 10;
y = 25;
System.out.println(x); // 10
System.out.println(y); // 25

Также можно их сразу инициализировать:

int x = 8, y = 15;
System.out.println(x); // 8
System.out.println(y); // 15

Отличительной особенностью переменных является то, что мы можем в процессе работы программы изменять их значение:

int x = 10;
System.out.println(x); // 10
x = 25;
System.out.println(x); // 25

1.3.1. Ключевое слово var

Начиная с Java 10 в язык было добавлено ключевое слово var, которое также позволяет определять переменную:

var x = 10;
System.out.println(x); // 10

Слово var ставится вместо типа данных, а сам тип переменной выводится из того значения, которое ей присваивается. Например, переменой x присваивается число 10, значит, переменная будет представлять тип int.

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

var x; // ! Ошибка, переменная не инициализирована
x = 10;

1.4. Значения в других системах счисления

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

int num111 = 0x6F; // шестнадцатеричная система, число 111
int num8 = 010; // восьмеричная система, число 8
int num13 = 0b1101; // двоичная система, число 13

Для задания шестнадцатеричного значения после символов 0x указывается число в шестнадцатеричном формате. Таким же образом восьмеричное значение указывается после символа 0, а двоичное значение — после символов 0b.

1.5. Символы и строки

В качестве значения переменная символьного типа получает одиночный символ, заключенный в ординарные кавычки: char ch = 'e';. Кроме того, переменной символьного типа также можно присвоить целочисленное значение от 0 до 65536. В этом случае переменная опять же будет хранить символ, а целочисленное значение будет указывать на номер символа в таблице символов Unicode. Например:

char ch = 102; // символ 'f'
System.out.println(ch);

Еще одной формой задания символьных переменных является шестнадцатеричная форма: переменная получает значение в шестнадцатеричной форме, которое следует после символов "\u". Например, char ch = '\u0066'; опять же будет хранить символ 'f'.

Символьные переменные не стоит путать со строковыми, 'a' не идентично "a". Строковые переменные представляют объект String, который в отличие от char или int не является примитивным типом в Java:

String hello = "Hellow...";
System.out.println(hello);

1.6. Константы

Кроме переменных, в Java для хранения данных можно использовать константы. В отличие от переменных константам можно присвоить значение только один раз. Константа объявляется так же, как и переменная, только вначале идет ключевое слово final:

final int LIMIT = 5;
System.out.println(LIMIT); // 5
// LIMIT = 57; // так мы уже не можем написать, так как LIMIT - константа

Как правило, константы имеют имена в ВЕРХНЕМ_РЕГИСТРЕ.

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

2. Консольный ввод и вывод

Для получения данных, введенных пользователем, а также для вывода сообщений нам необходим ряд классов, через которые мы сможем взаимодействовать с консолью. Для взаимодействия с консолью нам необходим класс System. Этот класс располагается в пакете java.lang, который автоматически подключается в программу, поэтому нам не надо дополнительно импортировать данный пакет и класс.

2.1. Консольный вывод

Для создания потока вывода в класс System определен объект out. В этом объекте определен метод println, который позволяет вывести на консоль некоторое значение с последующим переводом консоли на следующую строку:

System.out.println("Hello world");

В метод println передается любое значение, как правило, строка, которое надо вывести на консоль. При необходимости можно и не переводить курсор на следующую строку. В этом случае можно использовать метод System.out.print(), который аналогичен println за тем исключением, что не осуществляет перевода на следующую строку.

System.out.print("Hello world");

Но с помощью метода System.out.print также можно осуществить перевод каретки на следующую строку. Для этого надо использовать escape-последовательность \n:

System.out.print("Hello world \n");

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

int x = 5;
int y = 6;
System.out.println("x=" + x + "; y=" + y);

Но в Java есть также функция для форматированного вывода, унаследованная от языка С: System.out.printf(). С ее помощью мы можем переписать предыдущий пример следующим образом:

int x = 5;
int y = 6;
System.out.printf("x=%d; y=%d \n", x, y);

В данном случае символы %d обозначают спецификатор, вместо которого подставляет один из аргументов. Спецификаторов и соответствующих им аргументов может быть множество. В данном случае у нас только два аргумента, поэтому вместо первого %d подставляет значение переменной x, а вместо второго - значение переменной y. Сама буква d означает, что данный спецификатор будет использоваться для вывода целочисленных значений типа int.

Кроме спецификатора %d мы можем использовать еще ряд спецификаторов для других типов данных:

  • %x: для вывода шестнадцатеричных чисел

  • %f: для вывода чисел с плавающей точкой

  • %e: для вывода чисел в экспоненциальной форме, например, 1.3e+01

  • %c: для вывода одиночного символа

  • %s: для вывода строковых значений

Например:

String name = "Иван";
int age = 30;
float height = 1.7f;
System.out.printf("Имя: %s   Возраст: %d лет   Рост: %.2f метров \n", name, age, height);

При выводе чисел с плавающей точкой мы можем указать количество знаков после запятой, для этого используем спецификатор на %.2f, где .2 указывает, что после запятой будет два знака. В итоге мы получим следующий вывод:

Имя: Иван   Возраст: 30 лет   Рост: 1,70 метров

2.2. Консольный ввод

Для получения консольного ввода в классе System определен объект in. Однако непосредственно через объект System.in не очень удобно работать, поэтому, как правило, используют класс Scanner, который, в свою очередь использует System.in. Например, создадим маленькую программу, которая осуществляет ввод чисел:

import java.util.Scanner;

public class FirstApp {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        int[] nums = new int[5];
        for (int i = 0; i < nums.length; i++) {
            nums[i] = in.nextInt();
        }

        for (int i = 0; i < nums.length; i++) {
            System.out.print(nums[i]);
        }
        System.out.println();
    }
}

Так как класс Scanner находится в пакете java.util, то мы вначале его импортируем. Для создания самого объекта Scanner в его конструктор передается объект System.in. После этого мы можем получать вводимые значения. Например, чтобы получить введенное число, используется метод in.nextInt();, который возвращает введенное с клавиатуры целочисленное значение.

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

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

  • next(): считывает введенную строку до первого пробела

  • nextLine(): считывает всю введенную строку

  • nextInt(): считывает введенное число int

  • nextDouble(): считывает введенное число double

  • hasNext(): проверяет, было ли введено слово

  • hasNextInt(): проверяет, было ли введено число int

  • hasNextDouble(): проверяет, было ли введено double

Кроме того, класс Scanner имеет еще ряд методов nextByte()/nextShort()/nextFloat()/nextBoolean(), которые по аналогии с nextInt() считывают данные определенного типа данных.

Создадим следующую программу для ввода информации о человеке:

import java.util.Scanner;

public class FirstApp {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        System.out.print("Введите имя: ");
        String name = in.nextLine();
        System.out.print("Введите возраст: ");
        int age = in.nextInt();
        System.out.println("Ваше имя: " + name + "   Ваш возраст: " + age);
    }
}

Например, если бы мы запускали проект в IntelliJ IDEA, то это выглядело бы так:

Console Output

2.3. Проблема с кириллическими символами

Нередко при вводе кириллических символов отображаются некорректные символы. В принципе проблема касается не только кириллических символов, но символов ряда других кодировок, отличающихся от латиницы. В этом случае надо настроить кодировку. Для этого можно пойти двумя путями.

Первый способ заключается в выборе кодировке в самой IDE. Для этого перейдем в меню File → Setting или hotkey Ctrl+Alt+S.

Нам откроется окно настроек проекта, где в самом низу нам надо выбрать вместо кодировки по умолчанию UTF-8 кодировку windows-1251:

Problem encoding

3. Преобразование базовых типов данных

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

byte x = 5;
byte y = x;

Но важно понимать, что это запись не эквивалентна следующей (хотя результат будет тот же):

byte x = 5;
int y = x;

В обоих случаях создается переменная типа byte, которая затем приравнивается другой переменной. Однако если в первом случае это простое приравнивание, а переменная y просто получает значение переменной x, то во втором примере происходит преобразование типов: данные типа byte преобразуются к типу int. Данный тип преобразований называется расширяющим, так как значение типа byte расширяет свой размер до размера типа int. Расширяющие преобразования проходят автоматически и обычно с этим никаких проблем не возникает.

Подобным образом происходит преобразование от типа float к типу double или от типа int к типу long.

Кроме расширяющих преобразований есть еще и сужающие. Сужающие преобразования позволяют привести данные к типу с меньшей разрядностью, например, от типа int, который занимает 4 байта в памяти, к типу byte, который занимает только 1 байт в памяти:

int a = 4;
byte b = a; // ! Error

Несмотря на то, что значение переменной a - число 4 укладывается в диапазон типа byte, мы все равно получим ошибку. И чтобы безошибочно провести преобразование из одного типа к другому, нам надо применить операцию приведения типов. Суть этой операции состоит в том, что в скобках указывается тип, к которому надо привести данное значение:

int a = 4;
byte b = (byte) a;

3.1. Потеря данных при преобразовании

В предыдущей ситуации число 4 вполне укладывалось в диапазон значений типа byte. Но что будет в следующем случае:

int a = 200;
byte b = (byte) a;

Результатом будет число -56. В данном случае число 200 вне диапазона для типа byte (от -128 до 127), поэтому произойдет усечение значения.

3.2. Усечение рациональных чисел до целых

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

double
a = 56.9898;
int b = (int) a;

Здесь значение числа b будет равно 56, несмотря на то, что число 57 было бы ближе к 56.9898. Чтобы избежать подобных казусов, надо применять функцию округления, которая есть в математической библиотеке Java:

double a = 56.9898;
int b = (int) Math.round(a);

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

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

  • если один из операндов операции относится к типу double, то и второй операнд преобразуется к типу double

  • если предыдущее условие не соблюдено, а один из операндов операции относится к типу float, то и второй операнд преобразуется к типу float

  • если предыдущие условия не соблюдены, один из операндов операции относится к типу long, то и второй операнд преобразуется к типу long

  • иначе все операнды операции преобразуются к типу int

Примеры преобразований:

int a = 3;
double b = 4.6;
double c = a + b;

Так как в операции участвует значение типа double, то и другое значение приводится к типу double и сумма двух значений a + b будет представлять тип double.

Другой пример:

byte a = 3;
short b = 4;
byte c = (byte) (a + b);

Две переменных типа byte и short (не double, float или long), поэтому при сложении они преобразуются к типу int, и их сумма a + b представляет значение типа int. Поэтому если затем мы присваиваем эту сумму переменной типа byte, то нам опять надо сделать преобразование типов к byte.

Если в операциях участвуют данные типа char, то они преобразуются в int:

int d = 'a' + 5;
System.out.println(d); // 102

Стоит отметить, что несмотря на то, что преобразования из int → float, long → float и long → double производятся без ошибок, но при преобразовании мы можем столкнуться с потерей информации. Например:

float b = 123456789;
System.out.println(b); // 1.23456792E8

4. Методы

4.1. Использование методов в программе

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

Общее определение методов выглядит следующим образом:

[модификаторы] тип_возвращаемого_значения название_метода([параметры]) {
    // тело метода
}

Модификаторы и параметры необязательны.

По умолчанию главный класс любой программы на Java содержит метод main, который служит точкой входа в программу:

public static void main(String[] args) {
    System.out.println("привет мир!");
}

Ключевые слова public и static являются модификаторами. Далее идет тип возвращаемого значения. Ключевое слово void указывает на то, что метод ничего не возвращает.

Затем идут название метода - main и в скобках параметры метода - String[] args. И в фигурные скобки заключено тело метода - все действия, которые он выполняет.

Создадим еще несколько методов:

public class Program {
    public static void main(String[] args) {

    }

    void hello() {
        System.out.println("Hello");
    }

    void welcome() {
        System.out.println("Welcome to Java 10");
    }
}

Здесь определены два дополнительных метода: hello() и welcome(), каждый из которых выводит некоторую строку на консоль. Методы определяются внутри класса - в данном случае внутри класса Program, в котором определен метод main.

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

Вызов метода осуществляется в форме:

имя_метода(аргументы);

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

Например, определим и выполним несколько методов:

public class Program {
    public static void main(String[] args) {
         hello();
         welcome();
         welcome();
    }

    static void hello() {
        System.out.println("Hello");
    }

    static void welcome() {
        System.out.println("Welcome to Java!");
    }
}

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

Также следует отметить, чтобы вызвать в методе main() другие методы, которые определены в одном классе с методом main(), они должны иметь модификатор static.

В итоге после компиляции и выполнения программы мы увидим на консоли:

Hello
Welcome to Java!
Welcome to Java!

4.2. Передача параметров в методы

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

static void sum(int x, int y) {
    int z = x + y;
    System.out.println(z);
}

Данная функция принимает два параметра - два числа, складывает их и выводит их сумму на консоль.

А при вызове этого метода в программе нам необходимо передать на место параметров значения, которые соответствуют типу параметра:

public class Program {
    public static void main(String[] args) {
        int a = 6;
        int b = 8;
        sum(a, b); // 14
        sum(3, a); // 9
        sum(5, 23); // 28
    }

    static void sum(int x, int y) {
        int z = x + y;
        System.out.println(z);
    }
}

Поскольку метод sum принимает два значения типа int, то на место параметров надо передать два значения типа int. Это могут быть и числовые литералы, и переменные типов данных, которые представляют тип int или могут быть автоматически преобразованы в тип int. Значения, которые передаются на место параметров, еще называются аргументами. Значения передаются параметрам по позиции, то есть первый аргумент первому параметру, второй аргумент - второму параметру и так далее.

Рассмотрим другой пример:

public class Program {
    public static void main(String[] args) {
        display("Tom", 34);
        display("Bob", 28);
        display("Sam", 23);
    }

    static void display(String name, int age) {
        System.out.println(name);
        System.out.println(age);
    }
}

Метод display() принимает два параметра. Первый параметр представляет тип String, а второй - тип int. Поэтому при вызове метода вначале в него надо передать строку, а затем число.

4.3. Параметры переменной длины

Метод может принимать параметры переменной длины одного типа. Например, нам надо передать в метод набор чисел и вычислить их сумму, но мы точно не знаем, сколько именно чисел будет передано - 3, 4, 5 или больше. Параметры переменной длины позволяют решить эту задачу:

public class Program {
    public static void main(String[] args) {
        sum(1, 2, 3); // 6
        sum(1, 2, 3, 4, 5); // 15
        sum(); // 0
    }

    static void sum(int ...nums) {
        int result = 0;
        for (int n : nums) {
            result += n;
        }
        System.out.println(result);
    }
}

Многоточие перед названием параметра int …​nums указывает на то, что он будет необязательным и будет представлять массив. Мы можем передать в метод sum() одно число, несколько чисел, а можем вообще не передавать никаких параметров. Причем, если мы хотим передать несколько параметров, то необязательный параметр должен указываться в конце:

public static void main(String[] args) {
    sum("Welcome!", 20, 10);
    sum("Hello World!");
}

static void sum(String message, int ...nums) {
    System.out.println(message);
    int result = 0;
    for (int x: nums) {
        result += x;
    }
    System.out.println(result);
}

4.4. Оператор return

Методы могут возвращать некоторое значение. Для этого применяется оператор return.

return возвращаемое_значение;

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

Например:

public class Program {
    public static void main(String[] args) {
        int x = sum(1, 2, 3);
        int y = sum(1, 4, 9);
        System.out.println(x); // 6
        System.out.println(y); // 14
    }

    static int sum(int a, int b, int c) {
        return a + b + c;
    }
}

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

При этом возвращаемое значение всегда должно иметь тот же тип, что значится в определении функции. И если функция возвращает значение типа int, то после оператора return стоит целочисленное значение, которое является объектом типа int. Как в данном случае это сумма значений параметров метода.

Метод может использовать несколько вызовов оператора return для возвращения разных значений в зависимости от некоторых условий:

public class Program {
    public static void main(String[] args) {
        System.out.println(daytime(7)); // Good morning
        System.out.println(daytime(13)); // Good after noon
        System.out.println(daytime(18)); // Good evening
        System.out.println(daytime(2)); // Good night
    }

    static String daytime(int hour) {
        if (hour >24 || hour < 0) {
            return "Invalid data";
        } else if (hour > 21 || hour < 6) {
            return "Good night";
        } else if (hour >= 15) {
            return "Good evening";
        } else if (hour >= 11) {
            return "Good after noon";
        } else {
            return "Good morning";
        }
    }
}

Здесь метод daytime() возвращает значение типа String, то есть строку, и в зависимости от значения параметра hour возвращаемая строка будет различаться.

4.4.1. Выход из метода

Оператор return применяется для возвращения значения из метода, но и для выхода из метода. В подобном качестве оператор return применяется в методах, которые ничего не возвращают, то есть имеют тип void:

public class Program {
    public static void main(String[] args) {
        daytime(7); // Good morning
        daytime(13); // Good after noon
        daytime(32); //
        daytime(56); //
        daytime(2); // Good night
    }

    static void daytime(int hour) {
        if (hour >24 || hour < 0) {
            return;
        } else if (hour > 21 || hour < 6) {
            System.out.println("Good night");
        } else if (hour >= 15) {
            System.out.println("Good evening");
        } else if (hour >= 11) {
            System.out.println("Good after noon");
        } else {
            System.out.println("Good morning");
        }
    }
}

Если переданное в метод daytime() значение больше 24 или меньше 0, то просто выходим из метода. Возвращаемое значение после return указывать в этом случае не нужно.

5. Операторы

5.1. Арифметические операторы

Большинство операций в Java аналогичны тем, которые применяются в других C-подобных языках. Для выполнения операции используются оператор (operator), т.е. символ, который используется для этой операции, например: +, - и т.д.

Есть следующие операторы:

  • унарные - выполняются над одним операндом

  • бинарные - выполняются над двумя операндами

  • тернарные - выполняются над тремя операндами

Операндом является переменная или литерал (например, литерал типа int), участвующее в операции. Рассмотрим все виды операторов.

В арифметических операциях участвуют числа. Для этих операций в Java есть бинарные и унарные арифметические операторы

5.1.1. Бинарные

+

+ - оператор сложения двух чисел:

int a = 10;
int b = 7;
int c = a + b;
int d = 4 + b;
System.out.println(c);
System.out.println(d);
17
11
-

- - оператор вычитания двух чисел:

int a = 10;
int b = 7;
int c = a - b;
int d = 4 - a;
System.out.println(c);
System.out.println(d);
3
-6
*

* - оператор умножения двух чисел

int a = 10;
int b = 7;
int c = a * b;
int d = b * 5;
System.out.println(c);
System.out.println(d);
70
35
/

/ -оператор деления двух чисел:

int a = 20;
int b = 5;
int c = a / b;
double d = 22.5 / 4.5;
System.out.println(c);
System.out.println(d);
4
5.0

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

double k = 10 / 4;
System.out.println(k);
2

Чтобы результат представлял число с плавающей точкой, один из операндов также должен представлять число с плавающей точкой:

double k = 10.0 / 4;
System.out.println(k);
2.5
%

% - получение остатка от деления двух чисел:

int a = 33;
int b = 5;
int c = a % b;
int d = 22 % 4;
System.out.println(c);
System.out.println(d);
3
2

5.1.2. Унарные

В Java есть два унарных арифметических оператора, которые применяются к одному числу:

  • ++ инкремент

  • -- декремент

Каждый из операторов имеет две разновидности:

  • префиксный

  • постфиксный

++ (префиксный инкремент)

Увеличение переменной на единицу, например, z = ++y (вначале значение переменной y увеличивается на 1, а затем ее значение возвращается и присваивается переменной z)

int a = 8;
int b = ++a;
System.out.println(a);
System.out.println(b);
9
9
++ (постфиксный инкремент)

Увеличение переменной на единицу, например, z = y++ (вначале значение переменной y возвращается и присваивается переменной z, а потом значение переменной y увеличивается на 1)

int a = 8;
int b = a++;
System.out.println(a);
System.out.println(b);
9
8
-- (префиксный декремент)

Уменьшение переменной на единицу, например, z = --y (вначале значение переменной y уменьшается на 1, а потом ее значение возвращается и присваивается переменной z)

int a = 8;
int b = --a;
System.out.println(a);
System.out.println(b);
7
7
-- (постфиксный декремент)

Уменьшение переменной на единицу, например, z = y-- (сначала значение переменной y возвращается и присваивается переменной z, а затем значение переменной y уменьшается на 1)

int a = 8;
int b = a--;
System.out.println(a);
System.out.println(b);
7
8

5.2. Операторы сравнения

Условные выражения представляют собой некоторое условие и возвращают значение типа boolean, то есть значение true (если условие истинно), или значение false (если условие ложно). К условным выражениям относятся выражения, которые содержат операторы сравнения и логические операторы.

С операторами сравнения в выражении используются два операнда, и возвращается значение типа boolean - true, если выражение верно, и false, если выражение неверно.

5.2.1. ==

== сравнивает два операнда на равенство и возвращает true (если операнды равны) и false (если операнды не равны).

int a = 10;
int b = 4;
boolean c = a == b;
boolean d = a == 10;
System.out.println(c);
System.out.println(d);
false
true

5.2.2. !=

!= сравнивает два операнда и возвращает true, если операнды НЕ равны, и false, если операнды равны.

int a = 10;
int b = 4;
boolean c = a != b;
boolean d = a != 10;
System.out.println(c);
System.out.println(d);
true
false

5.2.3. < (меньше чем)

Возвращает true, если первый операнд меньше второго, иначе возвращает false.

int a = 10;
int b = 4;
boolean c = a < b;
System.out.println(c);
false

5.2.4. > (больше чем)

Возвращает true, если первый операнд больше второго, иначе возвращает false.

int a = 10;
int b = 4;
boolean c = a > b;
System.out.println(c);
true

5.2.5. >= (больше или равно)

Возвращает true, если первый операнд больше второго или равен второму, иначе возвращает false.

boolean a = 10 >= 10;
boolean b = 10 >= 4;
boolean c = 10 >= 20;
System.out.println(a);
System.out.println(b);
System.out.println(c);
true
true
false

5.2.6. (меньше или равно)

Возвращает true, если первый операнд меньше второго или равен второму, иначе возвращает false.

boolean a = 10 <= 10;
boolean b = 10 <= 4;
boolean c = 10 <= 20;
System.out.println(a);
System.out.println(b);
System.out.println(c);
true
false
true

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

Также в Java есть логические операторы, которые используются в условиях и возвращают true или false и обычно объединяют несколько операторов сравнения.

5.3.1. !

boolean a = false;
boolean b = true;
boolean c = !a;
boolean d = !b;
System.out.println(c);
System.out.println(d);
true
false

c равно true, если b равно false, иначе c будет равно false.

5.3.2. ||

boolean c = a || b;

c равно true, если либо a, либо b, либо и a, и b равны true, иначе c будет равно false.

5.3.3. &&

boolean c = a && b;

c равно true, если и a, и b равны true, иначе c будет равно false.

5.3.4. Примеры:

boolean a1 = (5 > 6) || (4 < 6); // 5 > 6 - false, 4 < 6 - true
boolean a2 = (5 > 6) || (4 > 6); // 5 > 6 - false, 4 > 6 - false
boolean a3 = (5 > 6) && (4 < 6); // 5 > 6 - false, 4 < 6 - true
boolean a4 = (50 > 6) && (4 / 2 < 3); // 50 > 6 - true, 4/2 < 3 - true
System.out.println(a1);
System.out.println(a2);
System.out.println(a3);
System.out.println(a4);
true
false
false
true

5.4. Операторы присваивания

Операторы присваивания в основном представляют комбинацию простого присваивания с другими операторами.

5.4.1. =

c = b; переменной c приравнивает значение переменной b.

5.4.2. +=

c += b; переменной c присваивается результат сложения c и b.

5.4.3. -=

c -= b; переменной c присваивается результат вычитания c и b.

5.4.4. *=

c *= b; переменной c присваивается результат произведения c `и `b.

5.4.5. /=

c /= b; переменной c присваивается результат деления c на b.

5.4.6. %=

c %= b; переменной c присваивается остаток от деления c на b.

5.4.7. &=

c &= b; переменной c присваивается значение c & b.

5.4.8. |=

c |= b; переменной c присваивается значение c | b.

5.4.9. ^=

c ^= b; переменной c присваивается значение c ^ b.

5.4.10. <<=

c <<= b; переменной c присваивается значение c << b.

5.4.11. >>=

c >>= b; переменной c присваивается значение c >> b.

5.4.12. >>>=

c >>>= b; переменной c присваивается значение c >>> b.

5.4.13. Примеры операций:

int a = 5;
a += 10; // 15
a -= 3; // 12
a *= 2; // 24
a /= 6; // 4
a <<= 4; // 64
a >>= 2; // 16
System.out.println(a);
16

5.5. Побитовые операторы

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

5.5.1. Логические побитовые операторы

Логические побитовые операторы для чисел представляют собой поразрядные операторы. В данном случае числа рассматриваются в двоичном представлении, например, 2 в двоичной системе равно 10 и имеет два разряда, число 7 - 111 и имеет три разряда.

& (логическое умножение)

Умножение производится поразрядно, и если у обоих операндов значения разрядов равно 1, то после применения оператора возвращается 1, иначе возвращается число 0. Например:

int a1 = 2; // в двоичной системе счисления: 010
int b1 = 5; // в двоичной системе счисления: 101
System.out.println(a1 & b1); // в двоичной системе счисления: 000

int a2 = 4; // в двоичной системе счисления: 100
int b2 = 5; // в двоичной системе счисления: 101
System.out.println(a2 & b2); // в двоичной системе счисления: 100
0
4

В первом случае у нас два числа 2 и 5. 2 в двоичном виде представляет число 010, а 5 - 101. Поразрядное умножение чисел (0*1, 1*0, 0*1) дает результат 000.

Во втором случае у нас вместо 2 число 4, у которого в первом разряде 1, так же как и у числа 5, поэтому здесь результатом применения оператора (1*1, 0*0, 0 *1) = 100 будет число 4 в десятичном формате.

| (логическое сложение)

Данный оператор также применяется к двоичным разрядам, но теперь возвращается единица, если хотя бы у одного числа в данном разряде имеется единица (оператор логическое ИЛИ). Например:

int a1 = 2; // в двоичной системе счисления: 010
int b1 = 5; // в двоичной системе счисления: 101
System.out.println(a1 | b1); // в двоичной системе счисления: 111

int a2 = 4; // в двоичной системе счисления: 100
int b2 = 5; // в двоичной системе счисления: 101
System.out.println(a2 | b2); // в двоичной системе счисления: 101
7
5
^ (логическое исключающее ИЛИ)

Иногда этот оператор называют XOR. Если значения текущего разряда у обоих чисел разные, то возвращается 1, иначе возвращается 0.

Нередко его применяют для простого шифрования:

int number = 45; // Значение, которое надо зашифровать - в двоичной системе счисления: 101101
int key = 102; // Ключ шифрования - в двоичной системе счисления: 1100110

int encrypt = number ^ key; // Шифрование, результат в двоичной системе счисления: 1001011
System.out.println("Зашифрованное число: " + encrypt);

int decrypt = encrypt ^ key; // Дешифрование, результат в двоичной системе счисления: 101101
System.out.println("Расшифрованное число: " + decrypt);
Зашифрованное число: 75
Расшифрованное число: 45

Здесь также производятся поразрядное применение оператора. Например, результатом выражения 9 ^ 5 будет число 12. А чтобы расшифровать число, мы применяем обратный оператор к результату.

~ (логическое отрицание)

Поразрядный оператор, инвертирующий все разряды числа: если значение разряда равно 1, то оно становится равным 0, и наоборот.

byte a = 56;  // в двоичной системе счисления: 00000000 00000000 00000000 00111000
System.out.println(~a); // в двоичной системе счисления: 11111111 11111111 11111111 11000111

int b = 56; // в двоичной системе счисления: 00000000 00000000 00000000 00111000
System.out.println(~b); // в двоичной системе счисления: 11111111 11111111 11111111 11000111
-57
-57

5.5.2. Использование побитовых операторов для логических типов

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

|
boolean c = a | b;

c равно true, если либо a, либо b, либо и a, и b равны true, иначе c будет равно false

&
boolean c = a & b;

c равно true, если и a, и b равны true, иначе c будет равно false

^
boolean c = a ^ b;

c равно true, если либо a, либо b, но не одновременно, равны true, иначе c будет равно false

Разница между | и ||, & и &&

Здесь две пары операторов | и ||, а также & и &&, возвращают похожие результаты, однако же они не равнозначны.

Выражение c = a | b; будет вычислять сначала оба значения - a и b и на их основе выводить результат.

В выражении же c = a || b; вначале будет вычисляться значение a, и если оно равно true, то вычисление значения b уже смысла не имеет, так как у нас в любом случае уже c будет равно true. Значение b будет вычисляться только в том случае, если a равно false

То же самое касается пары операций &/&&. В выражении c = a & b; будут вычисляться оба значения - a и b.

В выражении же c = a && b; сначала будет вычисляться значение a, и если оно равно false, то вычисление значения b уже не имеет смысла, так как значение c в любом случае равно false. Значение b будет вычисляться только в том случае, если a равно true

Таким образом, операторы || и && более удобны в вычислениях, позволяя сократить время на вычисление значения выражения и тем самым повышая производительность. А операторы | и & больше подходят для выполнения поразрядных операций над числами.

5.5.3. Побитовые операторы сдвига

Операторы сдвига также производятся над разрядами чисел. Сдвиг может происходить вправо и влево.

<<
a << b

>> сдвигает число a влево на b разрядов. Например, выражение 4 << 1 сдвигает число 4 (которое в двоичном представлении 100) на один разряд влево, в результате получается число 1000 или число 8 в десятичном представлении.

>>
a >> b

>> смещает число a вправо на b разрядов. Например, 16 >> 1 сдвигает число 16 (которое в двоичной системе 10000) на один разряд вправо, то есть в итоге получается 1000 или число 8 в десятичном представлении.

>>>
a >>> b

>>> в отличие от предыдущих типов сдвигов данный оператор представляет беззнаковый сдвиг - сдвигает число a вправо на b разрядов. Например, выражение -8 >>> 2 будет равно 1073741822.

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

5.6. Приоритет операций

При работе с операторами важно понимать их приоритет, который можно описать следующей схемой (по убыванию приоритета):

i++ i--

++i --i +i -i ~ !

* / %

+ -

<< >> >>>

< > <= >= instanceof

== !=

&

^

|

&&

||

? : (тернарный оператор)

= += -= *= /= %= &= ^= |= <<= >>= >>>= (операторы присваивания)

Скобки повышают приоритет операторы, используемой в выражении.

6. Условные конструкции

Одним из фундаментальных элементов многих языков программирования являются условные конструкции. Данные конструкции позволяют направить работу программы по одному из путей в зависимости от определенных условий.

В языке Java используются следующие условные конструкции: if…​else и switch

6.1. Конструкция if…​else

Выражение if…​else проверяет истинность некоторого условия и в зависимости от результатов проверки выполняет определенный код:

int num1 = 6;
int num2 = 4;
if (num1 > num2) {
    System.out.println("Первое число больше второго");
}

После ключевого слова if ставится условие. И если это условие выполняется, то срабатывает код, который помещен в далее в блоке if после фигурных скобок. В качестве условий выступает операция сравнения двух чисел.

Так как, в данном случае первое число больше второго, то выражение num1 > num2 истинно и возвращает значение true. Следовательно, управление переходит в блок кода после фигурных скобок и начинает выполнять содержащиеся там инструкции, а конкретно метод System.out.println("Первое число больше второго");. Если бы первое число оказалось бы меньше второго или равно ему, то инструкции в блоке if не выполнялись бы.

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

int num1 = 6;
int num2 = 4;
if (num1 > num2) {
    System.out.println("Первое число больше второго");
} else {
    System.out.println("Первое число меньше второго");
}

Но при сравнении чисел мы можем насчитать три состояния:

  • первое число больше второго

  • первое число меньше второго

  • числа равны

С помощью выражения else if, мы можем обрабатывать дополнительные условия:

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 будет выполняться, если num1 > num2 равно true и одновременно num1 > 7 равно true.

6.2. Конструкция switch

Конструкция switch аналогична конструкции if…​else, так как позволяет обработать сразу несколько условий:

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.

switch работает с примитивными типами данных byte, short, char и int. Он также работает с enums, классом String и несколькими wrapper classes (классами обертками), такими как: Character, Byte, Short и Integer.

В конце блока сase ставится оператор break, чтобы избежать выполнения других блоков. Например, если бы убрали бы оператор break в следующем случае:

case 8:
    System.out.println("число равно 8");
    num++;
case 9:
    System.out.println("число равно 9");
    break;

то так как у нас переменная num равно 8, то выполнился бы блок case 8, но так как в этом блоке переменная num увеличивается на единицу, оператор break отсутствует, то начал бы выполняться блок case 9.

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

Строки можно использовать в выражении switch после 7 версии Java, включая 7 версию:

import java.util.Scanner;

public class FirstApp {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        System.out.println("Введите Y или N: ");
        String input = in.nextLine();
        switch(input) {
            case "Y":
                System.out.println("Вы нажали букву Y");
                break;
            case "N":
                System.out.println("Вы нажали букву N");
                break;
            default:
                System.out.println("Вы нажали неизвестную букву");
        }
    }
}

6.3. Тернарная операция

Тернарная операция имеет следующий синтаксис:

[первый операнд - условие] ? [второй операнд] : [третий операнд]

Таким образом, в этой операции участвуют сразу три операнда. В зависимости от условия тернарная операция возвращает второй или третий операнд:

если условие равно true,
    то возвращается второй операнд;
если условие равно false,
    то третий.

Например:

int x=3;
int y=2;
int z = x < y ? (x + y) : (x - y);
System.out.println(z);

Здесь результатом тернарной операции является переменная z. Сначала проверяется условие x < y. И если оно соблюдается, то z будет равно второму операнду - (x + y), иначе z будет равно третьему операнду.

7. Циклы

Еще одним видом управляющих конструкций являются циклы. Циклы позволяют в зависимости от определенных условий выполнять определенное действие множество раз. В языке Java есть следующие виды циклов:

  • for

  • while

  • do…​while

7.1. Цикл for

Цикл for имеет следующее формальное определение:

for ([инициализация счетчика]; [условие]; [изменение счетчика]) {
    // действия
}

Рассмотрим стандартный цикл for:

for (int i = 1; i < 9; i++) {
    System.out.printf("Квадрат числа %d равен %d \n", i, i * i);
}
  • объявления цикла - int i = 1 создает и инициализирует счетчик i. Счетчик необязательно должен представлять тип int. Это может быть и любой другой числовой тип, например, float. Перед выполнением цикла значение счетчика будет равно 1. В данном случае это то же самое, что и объявление переменной

  • условие, при котором будет выполняться цикл. В данном случае цикл будет выполняться, пока i не достигнет 9

  • приращение счетчика на единицу. Опять же нам необязательно увеличивать на единицу. Можно уменьшать: i--

В итоге блок цикла сработает 8 раз, пока значение i не станет равным 9. И каждый раз это значение будет увеличиваться на 1.

Нам необязательно указывать все условия при объявлении цикла. Например, мы можем написать так:

int i = 1;
for ( ; ; ) {
    System.out.printf("Квадрат числа %d равен %d \n", i, i * i);
}

Определение цикла осталось тем же, только теперь блоки в определении у нас пустые: for (; ;). Теперь нет инициализированной переменной-счетчика, нет условия, поэтому цикл будет работать вечно - бесконечный цикл.

Либо можно опустить ряд блоков:

int i = 1;
for ( ; i < 9; ) {
    System.out.printf("Квадрат числа %d равен %d \n", i, i * i);
    i++;
}

Этот пример эквивалентен первому примеру: у нас также есть счетчик, только создан он вне цикла. У нас есть условие выполнения цикла. И есть приращение счетчика уже в самом блоке for.

Цикл for может определять сразу несколько переменных и управлять ими:

int n = 10;
for (int i = 0, j = n - 1; i < j; i++, j--) {
    System.out.println(i * j);
}

7.1.1. Цикл foreach

Специальная версия цикла for предназначена для перебора элементов в наборах элементов, например, в массивах и коллекциях. Она аналогична действию цикла foreach, который имеется в других языках программирования. Формальное ее объявление:

for (тип_данных название_переменной : контейнер) {
    // действия
}

Например:

int[] array = new int[] { 1, 2, 3, 4, 5 };
for (int i : array) {
    System.out.println(i);
}

В качестве контейнера в данном случае выступает массив данных типа int. Затем объявляется переменная с типом int

То же самое можно было бы сделать и с помощью обычной версии for:

int[] array = new int[] { 1, 2, 3, 4, 5 };
for (int i = 0; i < array.length; i++) {
    System.out.println(array[i]);
}

В то же время эта версия цикла for более гибкая по сравнению for (int i : array). В частности, в этой версии мы можем изменять элементы:

int[] array = new int[] { 1, 2, 3, 4, 5 };
for (int i = 0; i < array.length; i++) {
    array[i] = array[i] * 2;
    System.out.println(array[i]);
}

7.2. Перебор многомерных массивов в цикле

int[][] nums = new int[][]
{
    {1, 2, 3},
    {4, 5, 6},
    {7, 8, 9}
};
for (int i = 0; i < nums.length; i++) {
    for (int j = 0; j < nums[i].length; j++) {
        System.out.printf("%d ", nums[i][j]);
    }
    System.out.println();
}

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

7.3. Цикл do

Цикл do сначала выполняет код цикла, а потом проверяет условие в инструкции while. И пока это условие истинно, цикл повторяется. Например:

int j = 7;
do {
    System.out.println(j);
    j--;
}
while (j > 0);

В данном случае код цикла сработает 7 раз, пока j не окажется равным нулю. Важно отметить, что цикл do гарантирует хотя бы однократное выполнение действий, даже если условие в инструкции while не будет истинно. Так, мы можем написать:

int j = -1;
do {
    System.out.println(j);
    j--;
}
while (j > 0);

Хотя переменная j изначально меньше 0, цикл все равно один раз выполнится.

7.4. Цикл while

Цикл while сразу проверяет истинность некоторого условия, и если условие истинно, то код цикла выполняется:

int j = 6;
while (j > 0) {
    System.out.println(j);
    j--;
}

7.5. Операторы continue и break

Иногда требуется выйти из цикла, не дожидаясь его завершения. В этом случае мы можем воспользоваться оператором break. Например:

int[] nums = new int[] { 1, 2, 3, 4, 12, 9 };
for (int i = 0; i < nums.length; i++) {
    if (nums[i] > 10)
        break;
    System.out.println(nums[i]);
}

Так как в цикле идет проверка, больше ли элемент массива 10, то мы не увидим на консоли последние два элемента, так как когда nums[i] окажется больше 10 (то есть равно 12), сработает оператор break, и цикл завершится.

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

int[] nums = new int[] { 1, 2, 3, 4, 12, 9 };
for (int i = 0; i < nums.length; i++) {
    if (nums[i] > 10) {
       continue;
    }
    System.out.println(nums[i]);
}

В этом случае, когда выполнение цикла дойдет до числа 12, которое не удовлетворяет условию проверки, то программа просто пропустит это число и перейдет к следующему элементу массива.

8. Массивы

8.1. Одномерные массивы

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

тип_данных[] название_массива;
// либо
тип_данных []название_массива;
// либо
тип_данных название_массива[];

Например, определим массив чисел:

int[] nums3; // best practice
int []nums2;
int nums[];

После объявления массива мы можем инициализировать его:

int nums[]; // объявили
nums = new int[4]; // инициализировали (массив из 4 чисел)

Создание массива производится с помощью следующей конструкции: new тип_данных[количество_элементов], где new - ключевое слово, выделяющее память для указанного в скобках количества элементов. Например, nums = new int[4]; - в этом выражении создается массив из четырех элементов int и каждый элемент по умолчанию равен 0.

Также можно сразу при объявлении массива инициализировать его:

int nums[] = new int[4]; // массив из 4 чисел
int[] nums2 = new int[5]; // массив из 5 чисел

При подобной инициализации все элементы массива имеют значение по умолчанию. Для числовых типов, в том числе для типа char, это число 0, для типа boolean это значение false, а для остальных объектов это значение null. Например, для типа int значением по умолчанию является число 0, поэтому выше определенный массив nums будет состоять из четырех нулей.

Однако также можно задать конкретные значения для элементов массива при его создании:

// эти два способа равноценны
int[] nums = new int[] {1, 2, 3, 5};
int[] nums2 = {1, 2, 3, 5};

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

После создания массива мы можем обратиться к любому его элементу и изменить его:

int[] nums = new int[4];
nums[0] = 1;
nums[1] = 2;
nums[2] = 4;
nums[3] = 100;
System.out.println(nums[2]); // 4

Отсчет элементов массива начинается с 0, поэтому в данном случае, чтобы обратиться к четвертому элементу в массиве, нам надо использовать выражение nums[3].

И так как у нас массив определен только для 4 элементов, то мы не можем обратиться, например, к шестому элементу: nums[5] = 5;. Если мы так попытаемся сделать, то мы получим ошибку.

8.2. Многомерные массивы

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

int[] nums1 = new int[] {0, 1, 2, 3, 4, 5};
int[][] nums2 = {
  {0, 1, 2},
  {3, 4, 5}
};

Визуально оба массива можно представить следующим образом:

8.2.1. Одномерный массив nums1

0

1

2

3

4

5

8.2.2. Двухмерный массив nums2

0

1

2

3

4

5

Поскольку массив nums2 двухмерный, он представляет собой простую таблицу. Его также можно было создать следующим образом: int[][] nums2 = new int[3][3];. Количество квадратных скобок указывает на размерность массива. А числа в скобках - на количество строк и столбцов. И также, используя индексы, мы можем использовать элементы массива в программе:

// установим элемент первого столбца второй строки
nums2[1][0] = 44;
System.out.println(nums2[1][0]);

Объявление трехмерного массива могло бы выглядеть так:

int[][][] nums3 = new int[2][3][4];

8.3. Массив массивов

Многомерные массивы могут быть также представлены как "зубчатые массивы". В вышеприведенном примере двухмерный массив имел 3 строчки и три столбца, поэтому у нас получалась ровная таблица. Но мы можем каждому элементу в двухмерном массиве присвоить отдельный массив с различным количеством элементов:

int[][] nums = new int[3][];
nums[0] = new int[2];
nums[1] = new int[3];
nums[2] = new int[5];
...

0

1

2

3

4

5

6

7

8

9

8.4. Работа с массивами

Важнейшее свойство, которым обладают массивы, является свойство length, возвращающее длину массива, то есть количество его элементов:

int length = nums.length;

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

int last = nums[nums.length - 1];

8.5. Класс Arrays

Класс java.util.Arrays предназначен для работы с массивами. Он содержит удобные методы для работы с целыми массивами:

  • String toString(T[]) − позволяет получить все элементы в виде одной строки

  • T[] copyOf(T[], int) − предназначен для копирования массива

  • T[] copyOfRange(T[], int, int) − копирует часть массива

  • void sort(T[]) — сортирует массив методом quick sort

  • void sort(T[], int, int) — сортирует массив методом quick sort

  • int binarySearch(T[], T) − ищет элемент методом бинарного поиска

  • int binarySearch(T[], int, int, T) − ищет элемент методом бинарного поиска

  • void fill(T[], T) − заполняет массив переданным значением (удобно использовать, если нам необходимо значение по умолчанию для массива)

  • void fill(T[], int, int, T) − заполняет массив переданным значением (удобно использовать, если нам необходимо значение по умолчанию для массива)

  • boolean equals(T[], T[]) − проверяет на идентичность массивы

  • boolean equals(T[], int, int, T[], int, int) − проверяет на идентичность массивы

  • int compare(T[], T[]) − сравнивает массивы

  • int compare(T[], int, int, T[], int, int) − сравнивает массивы

  • boolean deepEquals(Object[], Object[]) − проверяет на идентичность массивы массивов

  • List<T> asList(T…​) − возвращает массив как коллекцию

9. Строки

Строка представляет собой последовательность символов. Для работы со строками в Java определен класс String, который предоставляет ряд методов для манипуляции строками. Физически объект String представляет собой ссылку на область в памяти, в которой размещены символы.

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

public static void main(String[] args) {
    String str1 = "Java";
    String str2 = new String(); // пустая строка
    String str3 = new String(new char[] {'h', 'e', 'l', 'l', 'o'});
    String str4 = new String(new char[] {'w', 'e', 'l', 'c', 'o', 'm', 'e'}, 3, 4); // 3 -начальный индекс, 4 -количество символов

    System.out.println(str1); // Java
    System.out.println(str2); //
    System.out.println(str3); // hello
    System.out.println(str4); // come
}

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

9.1. Конкатенация строк

Для соединения строк можно использовать операцию сложения +:

String str1 = "Java";
String str2 = "Hello";
String str3 = str2 + " " + str1;

System.out.println(str3); // Hello Java

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

String str3 = "Год " + 2015;

Фактически же при сложении строк с не строковыми объектами будет вызываться метод valueOf() класса String. Данный метод имеет множество перегрузок и преобразует практически все типы данных к строке. Для преобразования объектов различных классов метод valueOf() вызывает метод toString() этих классов.

9.2. Основные методы класса String

9.2.1. length()

Поскольку строка рассматривается как набор символов, то мы можем применить метод length() для нахождения длины строки или длины набора символов:

String str1 = "Java";
System.out.println(str1.length()); // 4

9.2.2. toCharArray()

С помощью метода toCharArray() можно обратно преобразовать строку в массив символов:

String str1 = new String(new char[] {'h', 'e', 'l', 'l', 'o'});
char[] helloArray = str1.toCharArray();

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

String s = ""; // строка не указывает на объект
if (s.length() == 0) {
    System.out.println("String is empty");
}

В этом случае длина строки, возвращаемая методом length(), равна 0.

9.2.3. isEmpty()

Класс String имеет специальный метод, который позволяет проверить строку на пустоту - isEmpty(). Если строка пуста, он возвращает true:

String s = ""; // строка не указывает на объект
if (s.length() == 0) {
    System.out.println("String is empty");
}

Переменная String может не указывать на какой-либо объект и иметь значение null:

String s = null; // строка не указывает на объект
if (s == null) {
    System.out.println("String is null");
}

Значение null не эквивалентно пустой строке. Например, в следующем случае мы столкнемся с ошибкой выполнения:

String s = null; // строка не указывает на объект
if (s.length() == 0) { // NullPointerException
    System.out.println("String is empty");
}

Так как переменная не указывает ни на какой объект String, то соответственно мы не можем обращаться к методам объекта String. Чтобы избежать подобных ошибок, можно предварительно проверять строку на null:

String s = null; // строка не указывает на объект
if (s != null && s.length() == 0) {
    System.out.println("String is empty");
}

9.2.4. concat()

Для объединения строк используют метод concat():

String str1 = "Java";
String str2 = "Hello";
str2 = str2.concat(str1); // HelloJava

Метод concat() принимает строку, с которой надо объединить вызывающую строку, и возвращает соединенную строку.

9.2.5. join()

Для объединения строк используют метод join(), который позволяет объединить строки с учетом разделителя. Например, выше две строки сливались в одно слово HelloJava, но в идеале мы бы хотели, чтобы две подстроки были разделены пробелом. И для этого используем метод join():

String str1 = "Java";
String str2 = "Hello";
String str3 = String.join(" ", str2, str1); // Hello Java

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

9.2.6. charAt()

Для извлечения символов по индексу в классе String определен метод charAt(). Он принимает индекс, по которому надо получить символов, и возвращает извлеченный символ:

String str = "Java";
char c = str.charAt(2);
System.out.println(c); // v

Как и в массивах индексация начинается с нуля.

9.2.7. getChars()

Для извлечения группы символов или подстроку, то можно использовать метод getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin). Он принимает следующие параметры:

  • srcBegin индекс в строке, с которого начинается извлечение символов

  • srcEnd индекс в строке, до которого идет извлечение символов

  • dst массив символов, в который будут извлекаться символы

  • dstBegin индекс в массиве dst, с которого надо добавлять извлеченные из строки символы

String str = "Hello world!";
int start = 6;
int end = 11;
char[] dst=new char[end - start];
str.getChars(start, end, dst, 0);
System.out.println(dst); // world

9.2.8. equals() и equalsIgnoreCase()

Для сравнения строк используются методы equals() (с учетом регистра) и equalsIgnoreCase() (без учета регистра). Оба метода в качестве параметра принимают строку, с которой надо сравнить:

String str1 = "Hello";
String str2 = "hello";

System.out.println(str1.equals(str2)); // false
System.out.println(str1.equalsIgnoreCase(str2)); // true

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

9.2.9. regionMatches()

Еще один специальный метод regionMatches() сравнивает отдельные подстроки в рамках двух строк. Он имеет следующие формы:

boolean regionMatches(int toffset, String other, int oofset, int len)
boolean regionMatches(boolean ignoreCase, int toffset, String other, int oofset, int len)

Метод принимает следующие параметры:

  • ignoreCase надо ли игнорировать регистр символов при сравнении. Если значение true, регистр игнорируется

  • toffset начальный индекс в вызывающей строке, с которого начнется сравнение

  • other строка, с которой сравнивается вызывающая

  • oofset начальный индекс в сравниваемой строке, с которого начнется сравнение

  • len количество сравниваемых символов в обеих строках

Используем метод:

String str1 = "Hello world";
String str2 = "I work";
boolean result = str1.regionMatches(6, str2, 2, 3);
System.out.println(result); // true

В данном случае метод сравнивает 3 символа с 6-го индекса первой строки ("wor") и 3 символа со 2-го индекса второй строки ("wor"). Так как эти подстроки одинаковы, то возвращается true.

9.2.10. compareTo() и compareToIgnoreCase()

Методы compareTo() и compareToIgnoreCase() позволяют сравнить две строки, но при этом они также позволяют узнать больше ли одна строка, чем другая или нет. Если возвращаемое значение больше 0, то первая строка больше второй, если меньше нуля, то, наоборот, вторая больше первой. Если строки равны, то возвращается 0.

Для определения больше или меньше одна строка, чем другая, используется лексикографический порядок. То есть, например, строка "A" меньше, чем строка "B", так как символ 'A' в алфавите стоит перед символом 'B'. Если первые символы строк равны, то в расчет берутся следующие символы. Например:

String str1 = "hello";
String str2 = "world";
String str3 = "hell";

System.out.println(str1.compareTo(str2)); // -15 -> str1 меньше чем strt2
System.out.println(str1.compareTo(str3)); // 1 -> str1 больше чем str3

9.2.11. indexOf() и lastIndexOf()

Метод indexOf() находит индекс первого вхождения подстроки в строку, а метод lastIndexOf() - индекс последнего вхождения. Если подстрока не будет найдена, то оба метода возвращают -1:

String str = "Hello world";
int index1 = str.indexOf('l'); // 2
int index2 = str.indexOf("wo"); // 6
int index3 = str.lastIndexOf('l'); // 9

9.2.12. startsWith() и endsWith()

Метод startsWith() позволяют определить начинается ли строка с определенной подстроки, а метод endsWith() позволяет определить заканчивается строка на определенную подстроку:

String str = "myfile.exe";
boolean start = str.startsWith("my"); // true
boolean end = str.endsWith("exe"); // true

9.2.13. replace()

Метод replace() позволяет заменить в строке одну последовательность символов на другую:

String str = "Hello world";
String replStr1 = str.replace('l', 'd'); // Heddo world
String replStr2 = str.replace("Hello", "Bye"); // Bye world

9.2.14. trim()

Метод trim() позволяет удалить начальные и конечные пробелы:

String str = "  hello world  ";
str = str.trim(); // "hello world"

9.2.15. substring()

Метод substring() возвращает подстроку, начиная с определенного индекса до конца или до определенного индекса:

String str = "Hello world";
String substr1 = str.substring(6); // "world"
String substr2 = str.substring(3,5); // "lo"

9.2.16. toLowerCase() и toUpperCase()

Метод toLowerCase() переводит все символы строки в нижний регистр, а метод toUpperCase() - в верхний

String str = "Hello World";
System.out.println(str.toLowerCase()); // hello world
System.out.println(str.toUpperCase()); // HELLO WORLD

9.2.17. split()

Метод split() позволяет разбить строку на подстроки по определенному разделителю. Разделитель - какой-нибудь символ или набор символов передается в качестве параметра в метод. Например, разобьем текст на отдельные слова:

String text = "FIFA will never regret it";
String[] words = text.split(" ");
for (String word : words) {
    System.out.println(word);
}

В данном случае строка будет разделяться по пробелу. Консольный вывод:

FIFA
will
never
regret
it