Skip to content

Latest commit

 

History

History
416 lines (326 loc) · 31.8 KB

01_Hello_World.adoc

File metadata and controls

416 lines (326 loc) · 31.8 KB

Обзор синтаксиса языка Java

Давайте начнём знакомство с языком Java! Перед вами простейшая программа "Здравствуй, мир".

// test/Hello.java
package test;
public class Hello {
    public static void main(String[] args) {
        System.out.println("Здравствуй, мир!);
    }
}

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

Классы

Класс является главным элементом Java-программы! Любая Java-программа всегда содержит хотя бы один класс, а любой код на Java находится внутри какого-то класса.

С помощью классов реализуется парадигма программирования ООП = объектно-ориентированное программирование. Обучение обычно начинают с процедурного программирования, краеугольным камнем которого является разбиение задачи на подзадачи, после чего каждая подзадача реализуется как процедура или функция, а задача в целом — как главная функция. В объектно-ориентированном программировании краеугольным камнем являются понятия или объекты, которые фигурируют в задаче. Программист, решая задачу, выделяет в ней наиболее важные понятия и реализует их в виде класса. Класс содержит данные, которые описывают понятия задачи, и функции, которые работают с этими данными.

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

public class Hello {
    // Тело класса
}

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

Пакеты

Язык Java придерживается ряда соглашений о структуре проекта. В частности, все файлы с исходным кодом должны находится в директории, которая называется Source root (переводится на русский примерно как "корневая директория исходного кода"). В проекте с использованием систем сборки Maven или Gradle директория Source root = ProjectDir/src/main/java. В проекте без использования систем сборки всё проще: Source root = ProjectDir/src. Содержимое же директории Source root определяется структорой пакетов (package). Например:

// test/Hello.java
package test;
public class Hello {
}

Обратите внимание на директиву package test перед определением класса. Это означает, что файл Hello.java обязан находиться в директории test, которая, в свою очередь, должна являться поддиректорией Source root. О классе Hello в этом случае говорят, что он находится в пакете test, а полное его имя — test.Hello. Имя файла обязано совпадать с именем определённого в нём открытого класса (отсюда, в частности, следует, что открытый класс в файле может быть определён лишь один).

// another/subpack/Some.java
package another.subpack;
public class Some {
}

Здесь класс Some находится в пакете another.subpack, его полное имя — another.subpack.Some, он обязан находиться в файле Some.java, а путь к этому файлу должен быть another/subpack/Some.java. Директория another должна быть поддиректорией Source root.

Тела классов

На языке Java тела классов пишутся в фигурных скобках. В первую очередь, классы состоят из полей (данных) и методов (операций над данными). Метод является синонимом функции (часто считается, что метод — это функция, определённая внутри класса). В классе Hello нет полей, и имеется один метод main:

    public static void main(String[] args) {
        System.out.println("Здравствуй, мир!);
    }

Модификатор public задаёт видимость. Как мы уже видели раньше, public — это открытая видимость, то есть доступная всем.

Видимости

Всего в Java имеется четыре разных видимости:

  • открытая public

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

  • пакетно-закрытая — не имеет модификатора, видна внутри того же класса, а также внутри того же пакета

  • защищённая protected — также может использовать только внутри класса, видна внутри него же, внутри того же пакета, а также внутри наследников этого класса (о них поговорим позже)

Таким образом, видимостей имеется четыре, но модификаторов видимости всего три. Без модификатора (по умолчанию) считается, что видимость пакетно-закрытая (package private). Иногда программисты на Java подобную видимость подчёркивают комментарием:

// FILE: SomeClass.java
package test;

public class SomeClass {
    private int x = 3;
    int y = 4;

    public void foo() {
        System.out.println(x); // Ok (same class)
    }
}

/* package-private */ class AnotherClass {
    public void bar() {
        SomeClass sc = new SomeClass(); // Ok (public)
        System.out.println(sc.x); // ERROR! (private, access from another class)
        System.out.println(sc.y); // Ok (package private, same package)
    }
}

Обратите внимание, что имя SomeClass обязано совпадать с именем файла (открытый класс), а имя AnotherClass — нет.

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

Стоит также отметить, что для классов, определённых на верхнем уровне файла, доступными являются только две видимости: открытая public и пакетно-закрытая. Для методов и полей классов, а также для вложенных классов (определённых внутри других классов), доступны все четыре видимости: открытая public, закрытая private, защищённая protected и пакетно-закрытая.

Статичность

    public static void main(String[] args) {
        System.out.println("Здравствуй, мир!);
    }

Как мы видим, функция main также является static, то есть статической. Чтобы понять, что это такое, нам придётся коснуться разницы между классами (class) и их экземплярами (class instance). Иногда вместо "экземпляр класса" говорят "объект класса", это синонимы.

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

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

Попробуйте сами определить, может ли нестатический метод прочитать статическое поле.

Примерно то же самое можно объяснить и другими словами. Любой нестатический метод имеет дополнительный параметр, не указанный явно в списке — так называемый получатель (receiver). Получатель — всегда ссылка на экземпляр класса, в котором описан данный метод; для её обозначения можно использовать ключевое слово this. Статический метод такого дополнительного параметра не имеет. Для вызова нестатического метода или обращения к нестатическому полю всегда требуется получатель правильного типа, указанный явго или неявно. Для вызова статического метода или обращения к статическому полю этого не требуется.

public class SomeClass {
    public int x = 1;
    static public final y = 2;
    public void foo() {
        bar(); // Ok (implicit receiver)
        this.bar(); // Also Ok (explicit receiver)
        System.out.println(this.x); // Ok (explicit receiver)
        System.out.println(y); // Ok (no receiver required)
    }

    public void bar() {
        baz(); // Ok (no receiver required)
    }

    static public void baz() {
        System.out.println(y); // Ok (no receiver required)
        System.out.println(x); // ERROR (receiver required!)
        SomeClass sc = new SomeClass();
        System.out.println(sc.x); // Ok (explicit receiver)
        System.out.println("123".x); // ERROR (incorrect explicit receiver)
    }
}

Типы

Язык Java имеет статическую типизацию. Это значит, что тип любой переменной, параметра, поля, результата функции известен на момент компиляции программы либо выводится во время компиляции программы. Типы бывают разные и делятся на две большие группы:

  • Примитивных типов всего восемь: четыре целочисленных int, long, short, byte; два с плавающей точкой double и float; логический boolean; символьный char. Имена примитивных типов записываются со строчной буквы, все они являются ключевыми словами Java (то есть такие же имена нельзя, например, давать переменным). К этой же группе можно условно отнести псевдо-тип void, который обозначает отсутствие какого-либо типа. Тип результата функции записывается перед её именем, для функции main это как раз void, то есть результат у функции main отсутствует.

  • Ссылочных типов может быть неограниченное количество. Их принципиальное отличие от примитивных состоит в том, что в стеке для подобных переменных хранится не значение, а ссылка на участок кучи, где уже хранится сам объект. Ссылочные типы могут быть описаны классом, или являться массивом (который в свою очередь может хранить примитивные или ссылочные элементы). В функции main тип параметра args задан как String[] — обратите внимание, что тип здесь тоже находится перед именем, это общее правило для Java. String — это строковый тип, определяемый библиотечным классом String. String[] — это массив строк.

Главная функция

По правилам языка Java, исполнение программы начинается с главной функции. Подобная функция обязана называться main, иметь открытую видимость, быть статической, иметь массив строк в качестве единственного параметра (через него передаются аргументы командной строки, подробнее см. раздел 7) и не иметь результата (тип void). Разрешается иметь в одной программе несколько главных функций — в этом случае при работе из IDE мы сами выбираем, с какой из них начинать работу, а при сборке JAR-пакета это указывается в так называемом MANIFEST-файле.

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

Вывод на консоль

Как можно догадаться из примера, вывод информации на консоль в программе на Java производится с помощью функции System.out.println(). Почему у неё такое длинное название? По правилам Java каждая функция обязана находиться в классе; функция println находится в классе PrintStream, то есть поток печати. Класс System содержит ссылки на два стандартных потока печати — один для вывода обычной информации, статическое поле out и другой для вывода ошибок, статическое поле err. Запись System.out позволяет нам обратиться к статическому полю класса, а дальнейшее .println — вызвать на соответствующем объекте функцию println.

Справочник по синтаксису Java

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

  • byte (1 байт, от -128 до 127)

  • short (2 байта, от -32768 до 32767)

  • int (4 байта, от -2^31 до 2^31-1)

  • long (8 байт, от -2^63 до 2^63-1)

  • float (4 байта: 24 бита мантисса + 8 бит порядок)

  • double (8 байт: 53 бита мантисса + 11 бит порядок)

  • boolean (1 байт: истина или ложь)

  • char (2 байта: юникод)

Переменные и поля

Как мы уже видели в примерах выше, для описания данных (переменных, параметров, констант, полей) Java использует синтаксис с типом впереди (как в языке Си). Для локальных переменных начиная с версии 10 разрешается используется синтаксис var <имя> = …​, позволяющий вывести тип переменной автоматически. Например

public class SomeClass {
    public String name = "Some"; // Поле типа String
    public int x = 2; // Поле типа int
    public void foo(double param /* параметр типа double */) {
        char ch = ' '; // Локальная переменная типа char
        var y = 3 * 5; // Локальная переменная типа double -- используется вывод типов
    }
}

Константы

Целые

  • 57, +323, -48 (десятичная форма, 4 байта)

  • 024, -0634, 0777 (восьмеричная форма)

  • 0xabcd, -0x19f (шестнадцатеричная форма)

  • 0b010001001 (двоичная форма, только JDK 1.7+)

  • 43_934 (форма с _, только в JDK 1.7+)

  • 1234567890123L, 0xabcdef1234L (8-байтные, long)

Вещественные

  • 37.29, -19.41 (обычная форма, 8 байт)

  • 3e+12, -1.1e-7 (экспоненциальная форма)

  • 3.6F, -1.0e-1F (4-байтные, float)

Символьные

  • 'a', '?', ' ', '\n', '\t', '\\' (обычный вариант)

  • '\40', '\62' – символ по восьмеричному коду

  • '\u0053' – символ по юникоду

Строковые

  • "Hello, world\n"

  • "Сложение " + "строк"

Операции

  • Арифметические: + - * / %. Сложение-вычитание-умножение-деление-взятие остатка.

  • Инкремент/декремент: ++ -- (увеличение/уменьшение на 1).

  • Логические: & && | || ^ !. Все логические операции требуют boolean аргументов. &, |, ^ являются жадными; && и || ленивыми.

  • Сравнения: > < >= == !=. Сравнение на равенство для примитивных типов происходит по значению, для ссылочных — по ссылке. Для сравнения объектов по значению существует функция equals.

  • Побитовые: ~ & | ^. Работают с целочисленными аргументами.

  • Сдвиговые: << >> >>>. Операция >> осуществляет арифметический сдвиг, то есть оставляет знак тем же; операция >>> осуществляет беззнаковый сдвиг.

  • Присваивания/модификации: = += -= *= /= %= &= |= ^= <⇐ >>= >>>=. Пример: a += b эквивалентно a = a + b.

  • Условная: a > b ? a : b. Если условие перед вопросом верно, результат операции — аргумент перед двоеточием, если нет — после двоеточия.

  • Приведения типа: int a = (int)2.5. "Силой" изменяет тип выражения в правой части. Численные типы при этом приводятся друг к другу (выполняется округление, если это требуется).

Ветвления

Основной оператор ветвления if (condition) { …​ } else { …​ }. Условие должно быть логическим. В ветвях может быть любое количество операторов; если ветвь содержит лишь один оператор, фигурные скобки можно опустить (делать этого не рекомендуется). Оператор ветвления не имеет результата, т.е. код вида int x = if (a > b) a else b запрещён, вместо этого можно применять условную операцию int x = a > b ? a : b.

Табличное ветвление по ключу

switch (someInt /* ключ */) {
case 1:
    ...
    break;
case 5:
    ...
    break;
default:
    ...
    break;
}

работает так. Если someInt в примере равно 1, код выполняется начиная с метки case 1. При выполнении оператора break мы покидаем конструкцию switch. Аналогично, если someInt равно 5, выполняем код начиная с метки case 5. Если ни одна из меток не содержит истинного значения — выполняем код с метки default.

По правилам Java, ключом оператора switch может являться

  • целое число

  • символ

  • элемент перечисления

  • строка (начиная с версии 1.7)

Начиная с версии 14, Java разрешает использование switch expressions (выражений табличного ветвления), то есть оператор switch теперь может иметь результат (который может быть присвоен переменной или использован каким-либо иным образом). Например:

int grade = switch (gradeWord /* ключ */) {
case "уд", "удовл", "удовлетворительно":
    yield 3;
case "хор", "хорошо":
    yield 4;
case "отл", "отлично":
    yield 5;
case "неуд", "неудовл", "неудовлетворительно":
    yield 2;
default:
    yield 0;
}

Выполнение команды yield здесь ведёт к формированию результата switch и немедленному выходу из конструкции. Можно считать, что yield ~= return from switch.

Тот же код может быть записан без помощи yield, если использовать новый синтаксис switch с заменой : на :

int grade = switch (gradeWord /* ключ */) {
    case "уд", "удовл", "удовлетворительно" -> 3
    case "хор", "хорошо" -> 4
    case "отл", "отлично" -> 5
    case "неуд", "неудовл", "неудовлетворительно" -> 2
    default -> 0;
}

Циклы

Язык Java включает четыре вида циклов: while (с предусловием), do-while (с постусловием), for (со счётчиком), for[each] (для каждого).

public class SomeClass {
    public void foo() {
        // 1. Проверить условие, если оно верно, выполнить тело, если нет, выйти из цикла
        // 2. Вернуться к пункту 1
        while (condition) {
            doSomething(); // Тело
        }

        // 1. Выполнить тело.
        // 2. Проверить условие, если оно верно, вернуться к пункту 1, если нет, выйти из цикла
        do {
            doSomething(); // Тело
        } while (condition);

        // 1. Выполнить начало (i=0)
        // 2. Проверить условие (i<10), если оно верно, выполнить тело, если нет, выйти из цикла
        // 3. Выполнить шаг
        // 4. Вернуться к пункту 2
        for (int i=0 /* начало */; i<10 /* условие */; i++ /* шаг */) {
            doSomething(); // Тело
        }

        int[] arr = new int[] { 2, 3, 5, 8, 13 }; // Создать массив из пяти элементов
        // 1-5. Для каждого из пяти элементов массива вызвать doSomething()
        for (int element: arr) {
            doSomething(); // Тело
        }
    }
}

Цикл на Java немедленно прерывается, если в его теле выполняется оператор break. Другой оператор управления continue заставляет цикл немедленно перейти к проверке условия выполнения следующей итерации (другими словами, continue немедленно прерывает текущую итерацию цикла). Напомним, что итерацией цикла называется одно выполнение его тела.

Строки

Строки в Java описываются библиотечным классом String. Строковые константы записываются в двойных кавычках. По правилам Java строки можно складывать с помощью оператора + и сравнивать на равенство с помощью метода equals. Использовать для строк оператор ссылочного равенства == не рекомендуется.

public class SomeClass {
    public void foo(String s1) {
        String s2 = "Alpha";
        System.out.println(s1.equals(s2)); // true, если s1 тоже "Alpha"
        System.out.println(s1 == s2); // true, если s1 и s2 являются ссылками на один и тот же объект класса String
        String s3 = "Alpha"; // По факту s2 и s3 ссылаются на один и тот же объект
        System.out.println(s2 == s3); // true
        String s4 = "Al" + "pha";
        System.out.println(s3 == s4); // Все ещё true
    }

    public void bar() {
        foo("Alpha"); // Выведется четыре раза true
    }
}

Компилятор Java умеет оптимизировать строковые константы. Если, например, у вас в программе 20 раз встречается константа "Alpha", в памяти будет создан всего один строковый объект с таким содержимым. И даже если вы напишите что-то вроде String s = "Al" + "pha";, ваша строка всё ещё равняется "Alpha" и переиспользует тот же самый объект. Если, однако, строка составляется из нескольких других и компилятор не может определить, что их значения всегда одинаковы, объекты будут созданы заново. Попробуйте в качестве упражнения написать пример, в котором ссылочное равенство даёт результат false, несмотря на то, что строки равны по значению.

Массивы

Массивы в Java — единственный составной тип, существующий на уровне языка и не имеющий библиотечного описания (скажем, связанные списки LinkedList и строки String описаны в Java как классы стандартной библиотеки). Любой массив — ссылочный тип. Массив элементов типа Type обозначается как Type[]. Размер (длина) массива всегда задаётся при его создании и в дальнейшем не меняется. Индексы элементов начинаются от нуля, индекс последнего элемента равен размеру массива минус 1. Примеры:

public class SomeClass {
    public void foo() {
        double[] darr = new double[10]; // Создать массив из 10 вещественных элементов, содержащий нули
        String[] sarr = new String[] { "Alpha", "Beta", "Omega" }; // Создать массив из трёх заданных строк
        int[] arr = null; // Создать нулевую ссылку на массив
        System.out.println(sarr[1]); // "Beta". Индексация идёт с нуля
        System.out.println(darr[10]); // Ошибка во время выполнения -- ArrayIndexOutOfBoundsException. Допустимы индексы от 0 до 9
        System.out.println(arr[0]); // Ошибка во время выполнения -- NullPointerException. Попытка обратиться к массиву по нулевой ссылке
        System.out.println(darr.length); // 10 -- число элементов (размер, длина) массива
    }
}