Java Core: Фундаментальный курс

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

1. Синтаксис языка, структура кода, классы и создание объектов

Синтаксис языка, структура кода, классы и создание объектов

Добро пожаловать в курс Java Core: Фундаментальный курс. Мы начинаем наше путешествие с самых основ. Java — это не просто набор команд, это строгая, логичная и мощная экосистема, построенная на принципах объектно-ориентированного программирования (ООП).

В этой статье мы разберем анатомию Java-приложения, поймем, чем класс отличается от объекта, и напишем наш первый работающий код.

Анатомия Java-программы

Любая программа на Java — это набор взаимодействующих друг с другом классов. Даже самая простая программа, которая просто выводит текст на экран, обязана находиться внутри класса.

Рассмотрим классический пример:

Разберем этот код по кирпичикам, чтобы понять структуру.

1. Объявление класса

Строка public class HelloWorld объявляет новый класс.

* public — модификатор доступа. Он говорит о том, что этот класс виден всем. * class — ключевое слово, означающее, что мы создаем именно класс. * HelloWorld — имя класса. В Java принято называть классы в стиле PascalCase (каждое слово с большой буквы, без пробелов).

Важное правило: Если класс объявлен как public, то имя файла обязано совпадать с именем класса. То есть этот код должен находиться в файле HelloWorld.java.

2. Точка входа (Метод main)

Внутри фигурных скобок { ... } находится тело класса. Самое важное здесь — метод main.

public static void main(String[] args)

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

* public — метод доступен извне (чтобы JVM могла его вызвать). * static — метод принадлежит самому классу, а не конкретному объекту (об этом мы поговорим чуть позже). * void — метод ничего не возвращает (не выдает результат вычислений наружу). * String[] args — массив строк, через который в программу можно передать параметры при запуске.

3. Тело метода и команды

System.out.println("Hello, Java!");

Это команда вывода текста в консоль. Обратите внимание на точку с запятой ; в конце. В Java каждая команда (инструкция) должна заканчиваться точкой с запятой. Это сигнал компилятору, что мысль закончена.

Основы синтаксиса и оформление кода

Java — язык со строгой типизацией и чувствительностью к регистру. Это значит, что Main и main — это два совершенно разных слова.

Блоки кода

Код в Java структурируется с помощью фигурных скобок { и }. Они определяют границы классов, методов и циклов. Все, что открыто, должно быть закрыто.

Комментарии

Хороший код должен быть понятен, но иногда нужны пояснения. Компилятор игнорирует комментарии, они нужны только людям.

Философия ООП: Классы и Объекты

Java — это объектно-ориентированный язык. Чтобы понять Java, нужно научиться мыслить объектами. Но что это значит?

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

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

!Визуализация концепции: Класс как чертеж и Объекты как реализация этого чертежа.

Класс (Class)

Класс — это тот самый чертеж (шаблон). Он описывает:
  • Состояние (данные, поля) — характеристики объекта.
  • Поведение (методы) — что объект умеет делать.
  • Объект (Object)

    Объект — это конкретный экземпляр класса, созданный в памяти компьютера по этому шаблону. У него есть конкретные значения полей.

    Создание собственного класса

    Давайте отойдем от абстрактных примеров и создадим класс Cat (Кот).

    Здесь мы определили, что любой кот в нашей программе будет иметь имя (name), возраст (age) и цвет (color). И любой кот будет уметь мяукать (meow).

    Типы данных (краткий обзор)

    В примере выше мы использовали типы данных: * String — для текста (строк). * int — для целых чисел.

    Создание объектов

    Сам по себе класс Cat — это просто описание. Кота еще не существует. Чтобы «родить» кота, нам нужно создать объект. Это делается с помощью ключевого слова new.

    Вернемся в наш метод main (или создадим новый класс для запуска) и создадим котов.

    Разбор процесса создания

    Фраза Cat barsik = new Cat(); делает три важные вещи:

  • Cat barsik — мы объявляем переменную barsik, которая может хранить ссылку на объект типа Cat.
  • new — ключевое слово, которое выделяет память в компьютере (в области, называемой Heap или «Куча») под новый объект.
  • Cat() — вызов конструктора (особого метода для инициализации объекта). По умолчанию он пустой.
  • В результате переменная barsik становится своего рода «пультом управления», который связан с конкретным котом в памяти.

    Структурирование кода и пакеты

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

    Пакет — это, по сути, папка на диске. В начале файла указывается, в каком пакете лежит класс.

    Имена пакетов принято писать маленькими буквами, используя обратное доменное имя компании (например, com.google.search), чтобы избежать конфликтов имен с библиотеками других разработчиков.

    Чистый код: Naming Conventions

    Чтобы ваш код был читаемым и профессиональным, нужно соблюдать соглашения об именовании (Code Conventions). В Java они строгие:

  • Классы: PascalCase. Первая буква заглавная, каждое следующее слово тоже с большой буквы. Пример: MySuperCat, String, System.
  • Методы и переменные: camelCase. Первая буква маленькая, каждое следующее слово с большой. Пример: myAge, calculateTotalSum(), meow.
  • Константы: UPPER_SNAKE_CASE. Все буквы большие, слова разделены подчеркиванием. Пример: MAX_COUNT, PI.
  • > Код пишется для людей, а не для машин. Машина поймет и плохой код, а вот ваш коллега (или вы сами через месяц) — нет.

    Резюме

    Сегодня мы заложили фундамент: * Узнали, что программа начинается с метода main. * Поняли разницу: Класс — это чертеж, Объект — это дом, построенный по чертежу. * Научились создавать объекты через new. * Познакомились с правилами именования переменных и классов.

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

    2. Глубокое погружение в ООП, инкапсуляция и механизм обработки исключений

    Глубокое погружение в ООП, инкапсуляция и механизм обработки исключений

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

    Сегодня мы углубимся в один из трех китов объектно-ориентированного программирования (ООП) — инкапсуляцию, разберемся с модификаторами доступа, узнаем, что такое static, и научимся обрабатывать ошибки так, чтобы наша программа не «падала» при малейшем сбое.

    Инкапсуляция: Защита данных

    Представьте, что вы разрабатываете класс BankAccount (Банковский счет). У него есть поле balance (баланс).

    Если поле balance объявлено как public, любой программист в любой части кода может написать:

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

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

    !Метафора инкапсуляции: данные защищены, доступ к ним возможен только через специальные методы.

    Модификаторы доступа

    В Java безопасность данных регулируется с помощью модификаторов доступа. Они определяют, кто «видит» ваши поля и методы.

    Существует 4 уровня доступа:

  • private (Частный): Виден только внутри того же класса. Это самый строгий уровень защиты.
  • default (package-private): Если модификатор не указан, он считается default. Виден всем классам в том же пакете.
  • protected (Защищенный): Виден внутри пакета и всем классам-наследникам (даже в других пакетах).
  • public (Публичный): Виден всем и везде.
  • Геттеры и Сеттеры (Getters & Setters)

    Правильный подход к проектированию класса BankAccount выглядит так:

    Теперь никто не может изменить баланс напрямую. Изменение возможно только через метод deposit, который содержит проверку (валидацию). Мы скрыли реализацию и предоставили интерфейс.

    Ключевое слово static

    Иногда нам нужно, чтобы переменная или метод принадлежали не конкретному объекту (коту Барсику или счету №123), а всему классу в целом.

    Статические переменные

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

    Переменная catsCount с модификатором static создается в памяти в единственном экземпляре. Все объекты класса Cat обращаются к одной и той же ячейке памяти.

    Статические методы

    Статические методы можно вызывать, не создавая объект класса. Самый известный пример — Math.

    Нам не нужно писать new Math(), чтобы вычислить корень. Метод sqrt является статическим.

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

    Конструкторы

    Когда мы пишем new Cat(), мы вызываем особый метод — конструктор. Его задача — инициализировать объект, то есть задать начальные значения полям.

    Особенности конструктора: * Имя конструктора всегда совпадает с именем класса. * У конструктора нет возвращаемого типа (даже void).

    Теперь создать человека без имени и возраста невозможно:

    Ключевое слово this указывает на текущий объект. this.name — это поле объекта, а name — это аргумент, переданный в конструктор.

    Обработка исключений (Exceptions)

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

    Когда происходит ошибка, Java «выбрасывает» (throws) исключение. Если его не поймать, программа аварийно завершится.

    Блок try-catch

    Чтобы программа продолжила работу даже после ошибки, опасный код оборачивают в блок try...catch.

  • try: Здесь мы пишем код, который может вызвать ошибку.
  • catch: Этот блок выполняется только если в блоке try произошла ошибка указанного типа (ArithmeticException).
  • Блок finally

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

    Иерархия исключений

    Все исключения в Java являются объектами.

    * Throwable: Самый главный класс-родитель всех проблем. * Error: Критические ошибки JVM (например, закончилась память OutOfMemoryError). Их обычно не обрабатывают. * Exception: Ошибки, которые можно и нужно обрабатывать. * RuntimeException: Ошибки программиста (деление на ноль, выход за границы массива, NullPointerException). Компилятор не требует их обязательной обработки. * Checked Exceptions: Ошибки, которые программист обязан предусмотреть (например, FileNotFoundException). Компилятор не даст скомпилировать код, пока вы не обернете его в try-catch.

    Структурирование кода: Хорошие практики

    Чтобы ваш код был чистым и поддерживаемым, следуйте принципу Single Responsibility Principle (Принцип единственной ответственности). Один класс должен отвечать за одну задачу.

    * Не смешивайте логику вычислений и вывод в консоль в одном методе. * Используйте private поля и public методы для доступа к ним. * Называйте исключения понятно, чтобы по логам было ясно, что случилось.

    Резюме

    Сегодня мы сделали огромный шаг вперед: * Поняли, что инкапсуляция защищает данные от некорректного использования. * Разобрали модификаторы доступа: private, default, protected, public. * Узнали, что static делает поля и методы общими для всего класса. * Научились использовать конструкторы для правильного создания объектов. * Освоили конструкцию try-catch-finally для обработки ошибок.

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

    3. Обобщенное программирование (Generics) и архитектура коллекций Java

    Обобщенное программирование (Generics) и архитектура коллекций Java

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

  • Фиксированный размер. Создав массив на 10 элементов, вы не можете записать туда 11-й. Вам придется создавать новый массив большего размера и копировать туда данные вручную.
  • Отсутствие гибкости. Массивы примитивов не имеют удобных методов для поиска, сортировки или удаления элементов.
  • Сегодня мы познакомимся с Java Collections Framework (JCF) — набором структур данных, которые решают эти проблемы, и узнаем, как Generics (обобщения) делают наш код безопасным.

    Проблема типизации и появление Generics

    До появления Java 5 (2004 год) коллекции могли хранить только объекты типа Object. Это означало, что в один список можно было случайно положить и кота, и строку, и число.

    Посмотрите на этот опасный код из прошлого:

    Программа «падала» в момент выполнения, потому что мы пытались превратить число 123 в строку. Чтобы перенести проверку типов с этапа выполнения (Runtime) на этап компиляции (Compile time), были придуманы Generics.

    Что такое Generics (Обобщения)?

    Generics — это «параметризованные типы». Они позволяют указывать, объекты какого именно класса мы планируем хранить в контейнере. Синтаксис использует угловые скобки < >.

    Теперь компилятор выступает в роли строгого контролера: если вы пообещали хранить в списке строки (<String>), он физически не даст вам положить туда что-то иное.

    Иерархия коллекций Java

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

    !Схема наследования основных интерфейсов коллекций.

    Основные интерфейсы

  • Collection — корень иерархии (на самом деле наследуется от Iterable, что позволяет перебирать элементы в цикле).
  • List (Список) — упорядоченная коллекция, допускает дубликаты. Элементы имеют индексы.
  • Set (Множество) — коллекция уникальных элементов. Дубликаты запрещены.
  • Queue (Очередь) — коллекция, работающая по принципу «первый вошел — первый вышел» (FIFO) или по приоритету.
  • Map (Карта/Словарь) — хранилище пар «Ключ — Значение». Не является наследником Collection.
  • List: Списки

    Списки нужны, когда вам важен порядок добавления элементов и доступ к ним по индексу. Самые популярные реализации: ArrayList и LinkedList.

    ArrayList

    Это «динамический массив». Внутри ArrayList скрыт обычный массив. Когда место в нем заканчивается, Java создает новый массив (обычно в 1.5 раза больше), копирует туда старые данные и продолжает работу.

    * Плюсы: Мгновенный доступ к любому элементу по индексу. * Минусы: Медленная вставка и удаление элементов из середины (приходится сдвигать все последующие элементы).

    Математически доступ к элементу описывается сложностью .

    Где означает константное время: скорость доступа не зависит от количества элементов в списке.

    LinkedList

    Это «связный список». Каждый элемент (нода) хранит ссылку на предыдущий и следующий элемент. Они разбросаны по памяти и связаны цепочкой.

    * Плюсы: Быстрая вставка и удаление (нужно просто перекинуть ссылки). * Минусы: Медленный доступ по индексу (чтобы найти 100-й элемент, нужно перебрать 99 предыдущих).

    Set: Множества

    Используйте Set, если вам нужно гарантировать уникальность данных. Например, список email-адресов для рассылки (чтобы не отправить письмо дважды).

    HashSet

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

    Работает на основе хэширования. Каждый объект имеет метод hashCode(), который возвращает числовое представление объекта. HashSet использует это число, чтобы определить, в какую «корзину» положить объект.

    > Важно: Для корректной работы HashSetHashMap) в ваших классах должны быть правильно переопределены методы equals() и hashCode().

    TreeSet

    Хранит элементы в отсортированном виде. Если вы положите туда имена, они автоматически выстроятся по алфавиту. Работает медленнее, чем HashSet, но полезен, когда нужна сортировка.

    Map: Карты (Словари)

    Map — это, пожалуй, самая используемая структура данных после списков. Она хранит пары: Ключ (Key) -> Значение (Value).

    Пример: телефонная книга (Имя -> Номер) или словарь (Слово -> Перевод).

    Особенности Map: * Ключи должны быть уникальны. Если вы добавите запись с уже существующим ключом, старое значение перезапишется. * Значения могут дублироваться.

    HashMap

    Стандартная реализация. Не гарантирует порядок хранения ключей. Обеспечивает очень быстрый поиск значения по ключу.

    Сложность поиска элемента в идеальном случае составляет .

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

    Перебор коллекций

    Как пройтись по всем элементам коллекции? Использовать цикл for-each.

    Для Map перебор выглядит чуть сложнее, так как нужно перебирать пары:

    Класс Collections

    Не путайте интерфейс Collection и утилитарный класс Collections (с буквой 's' на конце). Последний содержит статические методы для работы с коллекциями:

    * Collections.sort(list) — сортирует список. * Collections.shuffle(list) — перемешивает список. * Collections.reverse(list) — разворачивает список. * Collections.min(list) / max(list) — ищет минимум и максимум.

    Резюме

  • Generics (<Type>) обеспечивают безопасность типов на этапе компиляции, предотвращая ошибки ClassCastException.
  • List (ArrayList, LinkedList) — когда важен порядок и индексы.
  • Set (HashSet, TreeSet) — когда нужна уникальность элементов.
  • Map (HashMap, TreeMap) — для хранения пар «Ключ-Значение».
  • ArrayList быстр для чтения, LinkedList — для вставки.
  • В следующей статье мы разберемся с вводом-выводом (I/O) и научимся читать и записывать файлы, чтобы данные наших коллекций не пропадали после выключения программы.

    4. Функциональный стиль с Java Stream API и возможности Reflection API

    Функциональный стиль с Java Stream API и возможности Reflection API

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

    До Java 8 (2014 год) для этого использовались циклы for и while. Код получался громоздким, и за лесом фигурных скобок часто терялась суть алгоритма. С приходом Java 8 в язык был добавлен Stream API и Лямбда-выражения, что позволило писать код в функциональном стиле.

    А во второй части статьи мы заглянем «под капот» самой Java с помощью Reflection API — инструмента, который позволяет программе изучать и изменять саму себя во время выполнения.

    Лямбда-выражения: Краткость — сестра таланта

    Прежде чем говорить о потоках (Streams), нужно понять, что такое лямбда. В объектно-ориентированном мире Java мы привыкли, что все является объектом. Но иногда нам нужно передать в метод не данные, а действие (функцию).

    Лямбда-выражение — это анонимная функция, короткий способ записи метода. Ее синтаксис выглядит так:

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

    Стрелка -> разделяет аргументы и логику. Это фундамент функционального программирования в Java.

    Java Stream API: Конвейер данных

    Stream API — это инструмент для обработки последовательностей элементов. Важно понимать: Stream (поток) — это не структура данных. Он не хранит элементы, как ArrayList. Он берет данные из источника (коллекции, массива), пропускает их через цепочку операций и выдает результат.

    Представьте заводской конвейер.

    !Визуализация работы Stream API: источник -> промежуточные операции -> терминальная операция.

    Как это работает в коде

    Допустим, у нас есть список телефонов, и мы хотим найти все iPhone, узнать их цену и отсортировать по возрастанию.

    Императивный подход (Старый стиль): Мы говорим компьютеру как делать (возьми первый, проверь, если подходит — положи, возьми следующий...).

    Декларативный подход (Stream API): Мы говорим компьютеру что мы хотим получить.

    Анатомия Stream

    Работа со стримом всегда состоит из трех этапов:

  • Создание источника (source). Чаще всего это collection.stream().
  • Промежуточные операции (Intermediate Operations). Они ленивые (lazy). Это значит, что они не выполняются мгновенно, а просто настраивают конвейер. Примеры:
  • * filter(Predicate) — отсеивает элементы, не подходящие под условие. * map(Function) — преобразует элемент (например, из объекта User достает поле email). * sorted() — сортирует элементы. * distinct() — убирает дубликаты.
  • Терминальная операция (Terminal Operation). Запускает весь конвейер. Без нее стрим не начнет работать. Примеры:
  • * collect() — собирает результат в коллекцию. * forEach() — выполняет действие для каждого элемента (например, вывод в консоль). * count() — возвращает количество элементов. * findFirst() — возвращает первый элемент.

    Магия ленивых вычислений

    Почему Stream API эффективен? Потому что он умный. Рассмотрим код:

    Если в списке миллион чисел, стрим не будет фильтровать весь миллион, а потом умножать весь миллион. Он возьмет первое число, проверит фильтр, умножит и сразу вернет результат. Остальные 999 999 чисел даже не будут затронуты. Сложность такой операции в лучшем случае будет константной:

    Где означает, что время выполнения не зависит от размера входных данных.

    Reflection API: Взгляд внутрь себя

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

    Reflection API (Рефлексия) — это механизм, позволяющий программе исследовать свою собственную структуру (классы, поля, методы) и манипулировать ею во время выполнения.

    !Метафора Reflection API: возможность видеть и изменять скрытую структуру классов.

    Объект Class

    У каждого загруженного класса в Java есть специальный объект-спутник типа Class. Он содержит всю информацию о структуре.

    Нарушение инкапсуляции

    Помните, мы говорили, что private поля недоступны извне? Reflection API позволяет обойти это правило. Это мощный, но опасный инструмент.

    Представьте класс с секретом:

    Обычным кодом мы не можем прочитать поле secret. Но с помощью рефлексии:

    Зачем это нужно?

    Вы можете спросить: «Зачем ломать принципы ООП?». В повседневной разработке бизнес-логики рефлексию использовать не стоит. Она медленная и небезопасная.

    Однако, на Reflection API построены все современные фреймворки (Spring, Hibernate, JUnit). * Когда Hibernate сохраняет ваш объект в базу данных, он использует рефлексию, чтобы прочитать поля. * Когда Spring создает бины (компоненты), он использует рефлексию, чтобы найти конструкторы.

    Сравнение подходов

    Чтобы закрепить материал, давайте сравним рассмотренные концепции.

    | Характеристика | Императивный стиль (Циклы) | Функциональный стиль (Streams) | Reflection API | | :--- | :--- | :--- | :--- | | Читаемость | Низкая (много кода) | Высокая (описываем суть) | Низкая (сложный код) | | Контроль | Полный контроль над шагами | Контроль отдан Java | Полный контроль над структурой | | Производительность | Высокая (нет накладных расходов) | Чуть ниже (создание объектов) | Низкая (очень медленно) | | Применение | Простые алгоритмы, индексы | Обработка коллекций | Фреймворки, библиотеки |

    Резюме

    Сегодня мы значительно расширили наш арсенал:

  • Лямбда-выражения позволили нам писать код короче, передавая поведение как параметр.
  • Stream API превратил работу с коллекциями в элегантный конвейер: filter -> map -> collect.
  • Мы узнали про ленивые вычисления, которые экономят ресурсы процессора.
  • Reflection API открыл дверь во внутренний мир Java, позволив анализировать классы и даже обходить private, что критически важно для создания универсальных библиотек.
  • В следующей статье мы перейдем к одной из самых сложных, но интересных тем — Многопоточность (Multithreading). Мы узнаем, как заставить программу делать несколько дел одновременно и что такое Thread.

    5. Работа с I/O, основы многопоточности и модель памяти (JMM)

    Работа с I/O, основы многопоточности и модель памяти (JMM)

    Мы прошли долгий путь: от создания простых объектов до функционального стиля с Stream API. Однако до сих пор наши программы жили в «вакууме». Данные исчезали после завершения работы, а все действия выполнялись строго по очереди, одно за другим.

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

    Сегодня мы разберем три фундаментальные темы:

  • Input/Output (I/O) — как Java общается с внешним миром.
  • Многопоточность — как заставить процессор работать на полную мощность.
  • Java Memory Model (JMM) — свод законов, по которым потоки обмениваются данными в памяти.
  • Java I/O: Ввод и Вывод

    Система ввода-вывода в Java построена на концепции потоков (Streams).

    > Важно: Не путайте I/O Streams (потоки ввода-вывода) с Stream API (функциональные потоки данных), которые мы изучали в прошлой статье. Это разные сущности с похожим названием.

    Поток I/O — это труба, по которой текут данные. Мы можем подключить эту трубу к файлу, сетевому сокету или консоли.

    Байтовые и Символьные потоки

    В Java вся иерархия I/O делится на две большие ветви:

  • Byte Streams (Байтовые потоки): Работают с «сырыми» данными (0 и 1). Используются для картинок, аудио, видео и любых бинарных файлов.
  • * Базовые классы: InputStream и OutputStream.
  • Character Streams (Символьные потоки): Работают с текстом (char). Они умеют корректно обрабатывать кодировки (UTF-8, ASCII и др.).
  • * Базовые классы: Reader и Writer.

    !Схема разделения потоков ввода-вывода на байтовые и символьные.

    Чтение и запись файлов

    Раньше работа с файлами требовала много кода. В современной Java (начиная с Java 7 и пакета java.nio.file) это делается элегантно через класс Files.

    Пример чтения текстового файла:

    Try-with-resources

    При работе с I/O критически важно закрывать потоки. Открытый файл — это занятый ресурс операционной системы. Если их не закрывать, программа «упадет» с ошибкой Too many open files.

    Чтобы не писать громоздкие блоки finally, в Java используется конструкция try-with-resources. Все объекты, созданные в круглых скобках после try, будут закрыты автоматически.

    Основы многопоточности

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

    Процесс и Поток

    * Процесс — это запущенная программа (например, IntelliJ IDEA или браузер). У процесса есть своя изолированная память. * Поток (Thread) — это единица исполнения внутри процесса. Потоки одного процесса делят общую память.

    Создание потока в Java

    В Java поток представлен классом java.lang.Thread. Есть два основных способа создать новый поток.

    Способ 1: Наследование от Thread

    Способ 2: Реализация интерфейса Runnable (Рекомендуемый)

    Этот способ лучше, так как он отделяет задачу (что делать) от исполнителя (потока).

    Когда вы вызываете start(), Java сообщает операционной системе: «Выдели ресурсы под новый поток и начни выполнять метод run() параллельно с основным кодом».

    Жизненный цикл потока

    Поток не просто «работает» или «не работает». Он проходит через несколько состояний:

  • New — объект создан, но start() еще не вызван.
  • Runnable — поток готов работать и ждет, когда процессор выделит ему время.
  • Running — поток непосредственно выполняется процессором.
  • Blocked / Waiting — поток ждет чего-то (ввода данных, освобождения ресурса).
  • Terminated — работа завершена.
  • Проблемы многопоточности: Гонка состояний

    Многопоточность дает скорость, но приносит опасность. Главный враг — Race Condition (Состояние гонки).

    Представьте банковский счет с балансом 100 рублей. Два потока одновременно пытаются снять 50 рублей.

  • Поток А проверяет баланс: 100 > 50. Ок.
  • Поток Б проверяет баланс: 100 > 50. Ок (Поток А еще не успел списать деньги!).
  • Поток А списывает 50. Остаток 50.
  • Поток Б списывает 50. Остаток 0.
  • Вроде все верно. Но если операции наложатся друг на друга иначе, оба потока могут прочитать «100», и в итоге баланс станет некорректным. В программировании это выглядит так:

    Если 1000 потоков вызовут increment(), результат почти никогда не будет 1000. Часть обновлений потеряется.

    Синхронизация

    Чтобы защитить данные, мы используем ключевое слово synchronized. Оно работает как замок на двери туалета: пока один поток внутри, другие ждут снаружи.

    Теперь только один поток может выполнять этот метод в конкретный момент времени.

    Закон Амдала

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

    Где: * — ускорение (во сколько раз быстрее). — доля программы, которую можно* распараллелить (от 0 до 1). * — количество процессоров (потоков).

    Если (только половину программы можно распараллелить), то даже с бесконечным числом процессоров () ускорение никогда не превысит 2 раза. Это учит нас тому, что архитектура важнее простого добавления потоков.

    Java Memory Model (JMM)

    Как потоки видят общие переменные? Это описывает Java Memory Model.

    В современных компьютерах есть оперативная память (RAM) и кэши процессора (L1, L2, L3). Кэши работают в сотни раз быстрее памяти. Когда поток изменяет переменную, он часто меняет её только в своем локальном кэше, а не в общей памяти.

    !Иллюстрация проблемы видимости: каждое ядро работает со своей копией данных в кэше.

    Это порождает проблему видимости (Visibility Problem). Один поток изменил флаг isRunning = false, а второй поток этого не увидел, потому что читает старое значение из своего кэша.

    Ключевое слово volatile

    Чтобы решить проблему видимости, используется ключевое слово volatile.

    Оно говорит компилятору и процессору: «Эту переменную нельзя кэшировать. Всегда читай и пиши её прямо в оперативную память». Это гарантирует, что все потоки всегда видят актуальное значение.

    Happens-Before

    JMM строится на отношении Happens-Before («Произошло-До»). Это набор правил, гарантирующих порядок операций.

    Пример правила: Запись в volatile переменную всегда happens-before (происходит до) чтения из этой же переменной. Это значит, что если Поток А записал данные, а Поток Б потом прочитал, то Поток Б гарантированно увидит изменения.

    Резюме

    Сегодня мы коснулись «железа» и операционной системы:

  • I/O: Используйте Files и Path для работы с файлами. Всегда закрывайте потоки через try-with-resources.
  • Потоки: Создавайте их через Runnable. Помните, что порядок выполнения потоков не гарантирован.
  • Синхронизация: Используйте synchronized для защиты общих данных от гонки состояний.
  • JMM: Помните про кэши процессора. Используйте volatile для флагов состояния, чтобы обеспечить видимость изменений.
  • Многопоточность — одна из самых сложных тем в программировании. В следующей статье мы рассмотрим, как структурировать код, чтобы не запутаться в этой сложности, и поговорим о принципах чистого кода.