Java за 30 дней: Интенсивный курс с практикой

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

Неделя 1: Основы синтаксиса, переменные, типы данных и управляющие конструкции

Неделя 1: Основы синтаксиса, переменные, типы данных и управляющие конструкции

Добро пожаловать на курс «Java за 30 дней». Вы сделали отличный выбор. Java — это не просто язык программирования, это фундамент корпоративной разработки, мобильных приложений (Android) и серверных решений. Это язык со строгой статической типизацией, что означает, что он не позволит вам совершить многие глупые ошибки, которые возможны в других языках.

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

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

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

Самый главный метод, с которого начинается выполнение любой программы — это метод main.

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

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

Разберем каждую строчку:

  1. public class FirstApp — мы объявляем класс с именем FirstApp. В Java имя файла обязано совпадать с именем публичного класса.
  2. public static void main(String[] args) — это точка входа. Без этого метода программа просто не запустится.
  3. System.out.println(...) — команда вывода текста в консоль.

Переменные и Типы данных

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

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

Примитивные типы данных

В Java существует 8 примитивных типов данных. Они хранят простые значения и занимают фиксированное место в памяти. Мы сосредоточимся на самых используемых:

Тип Что хранит Пример
int Целые числа int age = 25;
double Дробные числа double price = 19.99;
boolean Истина или ложь boolean isJavaFun = true;
char Один символ char grade = 'A';

Остальные (byte, short, long, float) используются реже, в специфических ситуациях.

Ссылочные типы данных

Самый популярный ссылочный тип — это String (строка). Строка — это не примитив, это полноценный объект.

String name = "Алекс";

Важно: Примитивы пишутся с маленькой буквы (int), а ссылочные типы (классы) — с большой (String).

Математические операции

Программирование неразрывно связано с математикой. В Java доступны стандартные операторы: +, -, *, / (деление), % (остаток от деления).

Рассмотрим пример вычисления индекса массы тела (ИМТ). Формула выглядит так:

I=mh2I = \frac{m}{h^2}

Где II — индекс массы тела, mm — масса тела в килограммах, hh — рост в метрах.

В коде это будет выглядеть так:

double weight = 70.5;
double height = 1.75;
double bmi = weight / (height * height);
System.out.println(bmi);

Управляющие конструкции

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

Условный оператор if-else

Этот оператор позволяет программе «думать». Если условие истинно — делаем одно, иначе — другое.

int score = 85;

if (score >= 90) {
    System.out.println("Отлично!");
} else if (score >= 75) {
    System.out.println("Хорошо");
} else {
    System.out.println("Нужно подучить");
}

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

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

  • && (И) — истина, только если оба условия верны.
  • || (ИЛИ) — истина, если хотя бы одно условие верно.
  • ! (НЕ) — меняет значение на противоположное.

Пример логики:

R=(A>5)∧(B<10)R = (A > 5) \land (B < 10)

Где RR — результат (true/false), AA и BB — переменные, ∧\land — логическое «И» (в Java это &&). Если A=6A=6 и B=8B=8, то результат будет true.

Циклы (Loops)

Циклы позволяют выполнять блок кода многократно. Это спасает нас от дублирования кода.

Цикл while

Выполняется, пока условие истинно.

int count = 0;
while (count < 5) {
    System.out.println("Счетчик: " + count);
    count++; // Увеличиваем на 1
}
Цикл for

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

for (int i = 0; i < 5; i++) {
    System.out.println("Итерация №" + i);
}

Синтаксис for состоит из трех частей:

  1. Инициализация (int i = 0): выполняется один раз перед началом.
  2. Условие (i < 5): проверяется перед каждой итерацией.
  3. Обновление (i++): выполняется после каждой итерации.

Практическое задание недели

Чтобы закрепить материал, попробуйте написать простую программу «Калькулятор скидок» в вашей IDE.

  1. Создайте переменную price (цена товара).
  2. Создайте переменную isRegularCustomer (постоянный клиент, тип boolean).
  3. Если клиент постоянный, дайте скидку 10%.
  4. Если цена выше 1000, дайте дополнительную скидку 5%.
  5. Выведите итоговую цену.

Заключение

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

На следующей неделе мы углубимся в объектно-ориентированное программирование (ООП) — сердце Java. Мы узнаем, что такое объекты, наследование и полиморфизм.

Готовы проверить свои знания? Переходите к заданиям ниже.

Неделя 2: Объектно-ориентированное программирование: классы, наследование, полиморфизм и инкапсуляция

Неделя 2: Объектно-ориентированное программирование: классы, наследование, полиморфизм и инкапсуляция

Поздравляю с успешным завершением первой недели! Вы уже умеете объявлять переменные, писать циклы и принимать решения с помощью if-else. Но пока ваш код напоминал список покупок: просто последовательность действий. В реальной разработке, особенно в Enterprise-секторе (банковские системы, маркетплейсы), такой подход быстро превращается в хаос.

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

Классы и Объекты: Чертеж и Дом

В процедурном программировании (то, что мы делали на первой неделе) данные и методы живут отдельно. В ООП они объединяются в сущности.

Что такое класс?

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

Что такое объект?

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

Рассмотрим пример. Допустим, мы создаем игру с машинами.

// Это чертеж (Класс)
public class Car {
    // Поля (характеристики, состояние)
    String model;
    String color;
    int speed;

    // Методы (поведение, действия)
    void drive() {
        System.out.println(model + " едет со скоростью " + speed + " км/ч");
    }
}

Теперь создадим объекты в методе main:

public class Main {
    public static void main(String[] args) {
        // Создаем первый объект (экземпляр)
        Car myCar = new Car();
        myCar.model = "Tesla";
        myCar.speed = 100;

        // Создаем второй объект
        Car yourCar = new Car();
        yourCar.model = "BMW";
        yourCar.speed = 120;

        myCar.drive();  // Вывод: Tesla едет со скоростью 100 км/ч
        yourCar.drive(); // Вывод: BMW едет со скоростью 120 км/ч
    }
}

Обратите внимание на ключевое слово new. Именно оно выделяет память под новый объект.

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

Представьте, что в нашем классе Car кто-то напишет: myCar.speed = -200;. С точки зрения синтаксиса это верно (int может быть отрицательным), но с точки зрения логики — абсурд. Машина не может ехать с отрицательной скоростью.

Инкапсуляция — это механизм скрытия данных и предоставления доступа к ним только через публичные методы. Это защита от «дурака» и неправильного использования вашего кода.

В Java для этого используются модификаторы доступа:

  • private: доступ только внутри этого же класса.
  • public: доступ из любой части программы.

Перепишем наш класс правильно:

public class Car {
    private int speed; // Прямой доступ закрыт

    // Сеттер - метод для установки значения
    public void setSpeed(int newSpeed) {
        if (newSpeed < 0) {
            System.out.println("Ошибка: скорость не может быть отрицательной!");
        } else {
            this.speed = newSpeed;
        }
    }

    // Геттер - метод для получения значения
    public int getSpeed() {
        return this.speed;
    }
}

Теперь, если кто-то попытается установить -200, сработает наша проверка.

Наследование: Не повторяй себя

Программисты ленивы, и это хорошо. Если у нас есть классы Dog (Собака), Cat (Кошка) и Cow (Корова), у них у всех есть общие черты: имя, возраст, умение есть и спать. Писать один и тот же код в трех файлах — плохая практика.

Наследование позволяет создать родительский класс (например, Animal) и унаследовать от него свойства в дочерних классах. Используется ключевое слово extends.

// Родительский класс
public class Animal {
    String name;

    public void eat() {
        System.out.println(name + " кушает");
    }
}

// Дочерний класс
public class Dog extends Animal {
    public void bark() {
        System.out.println("Гав-гав!");
    }
}

Теперь объект Dog умеет и гавкать (свой метод), и кушать (метод родителя).

Полиморфизм: Один интерфейс, много форм

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

Допустим, у всех животных есть метод makeSound() (издать звук). Но собака лает, а кошка мяукает.

Мы можем переопределить (Override) метод родителя в дочерних классах.

public class Animal {
    public void makeSound() {
        System.out.println("Какой-то звук");
    }
}

public class Dog extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Гав!");
    }
}

public class Cat extends Animal {
    @Override
    public void makeSound() {
        System.out.println("Мяу!");
    }
}

Теперь магия полиморфизма:

public class Zoo {
    public static void main(String[] args) {
        // Создаем массив животных
        Animal[] animals = new Animal[2];
        animals[0] = new Dog();
        animals[1] = new Cat();

        // Проходим по массиву. Нам не важно, кто конкретно лежит в ячейке.
        // Мы знаем, что это Animal, и он умеет делать makeSound.
        for (Animal beast : animals) {
            beast.makeSound();
        }
    }
}

Результат:

Гав!
Мяу!

Программа сама определяет, какой именно метод вызвать, в зависимости от того, какой объект был создан (new Dog() или new Cat()).

Пример с математикой: Площадь фигур

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

S=π⋅r2S = \pi \cdot r^2

Где SS — это площадь круга, π\pi — математическая константа (приблизительно равна 3.14159), а rr — радиус круга.

В коде мы можем создать общий класс Shape (Фигура) и наследника Circle (Круг):

class Shape {
    public double getArea() {
        return 0;
    }
}

class Circle extends Shape {
    private double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double getArea() {
        // Math.PI - это встроенная константа в Java
        return Math.PI * (radius * radius);
    }
}

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

Вы могли заметить метод Circle(double radius) в примере выше. Это конструктор. Это специальный метод, который вызывается только один раз — в момент создания объекта (при использовании new).

Правила конструктора:

  1. Имя совпадает с именем класса.
  2. У него нет возвращаемого типа (даже void).
  3. Используется для начальной настройки объекта.

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

Иногда нам нужны методы или переменные, которые принадлежат не конкретному объекту (конкретной машине), а всему классу в целом (заводу).

Пример: счетчик созданных машин.

public class Car {
    public static int carCount = 0;

    public Car() {
        carCount++; // Увеличиваем счетчик при создании каждой новой машины
    }
}

К статическим переменным обращаются через имя класса: Car.carCount, а не через объект.

Заключение

Сегодня вы сделали огромный шаг. Вы перешли от написания скриптов к проектированию архитектуры.

  • Класс — это чертеж.
  • Объект — это реализация.
  • Инкапсуляция защищает данные (private).
  • Наследование (extends) позволяет переиспользовать код.
  • Полиморфизм позволяет работать с разными объектами одинаково.

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

А пока — выполните задания ниже, чтобы закрепить материал.

Неделя 3: Обработка исключений, работа со строками и фреймворк коллекций

Неделя 3: Обработка исключений, работа со строками и фреймворк коллекций

Добро пожаловать на третью неделю курса «Java за 30 дней»! Вы уже проделали огромную работу: освоили синтаксис, переменные, циклы и погрузились в мир Объектно-Ориентированного Программирования (ООП). Ваши программы уже имеют структуру, классы и объекты.

Но есть проблема. Ваши программы пока очень «хрупкие». Что произойдет, если пользователь введет буквы вместо цифр? Или если файл, который программа пытается открыть, не существует? Скорее всего, приложение просто «упадет» с красным текстом ошибки в консоли. В реальном мире (Enterprise-разработка) это недопустимо.

На этой неделе мы сделаем ваш код надежным, гибким и эффективным. Мы разберем три кита профессиональной разработки:

  1. Исключения (Exceptions): как грамотно обрабатывать ошибки.
  2. Строки (Strings): как эффективно работать с текстом.
  3. Коллекции (Collections): где хранить данные, когда массивов недостаточно.

Обработка исключений: Право на ошибку

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

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

Блок try-catch

Чтобы программа не падала, мы используем конструкцию try-catch (попытаться — поймать). Мы «оборачиваем» опасный код в блок try, а в блоке catch пишем инструкцию, что делать в случае аварии.

Рассмотрим классический пример деления на ноль:

public class ExceptionExample {
    public static void main(String[] args) {
        int a = 10;
        int b = 0;

        try {
            // Опасный участок кода
            int result = a / b;
            System.out.println("Результат: " + result);
        } catch (ArithmeticException e) {
            // Этот блок выполнится только при ошибке
            System.out.println("Ошибка: Нельзя делить на ноль!");
        }

        System.out.println("Программа продолжает работу.");
    }
}

Если бы мы не использовали try-catch, программа остановилась бы на строке int result = a / b, и последняя фраза никогда бы не напечаталась.

Блок finally

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

try {
    // Открываем файл
    // Читаем данные
} catch (Exception e) {
    // Обрабатываем ошибку чтения
} finally {
    // Закрываем файл (выполнится всегда)
}

Работа со строками: String и его секреты

Вы уже использовали тип String. Кажется, что это просто текст. Но в Java строки — это особые объекты. Главное, что нужно запомнить: строки в Java неизменяемы (immutable).

Это значит, что когда вы пишете:

String s = "Привет";
s = s + ", Мир";

Java не изменяет старую строку «Привет». Она создает в памяти совершенно новый объект «Привет, Мир», а старый объект «Привет» остается висеть в памяти, пока его не удалит сборщик мусора. Если вы делаете тысячи таких операций в цикле, это может «убить» производительность.

StringBuilder

Для частых изменений текста используйте класс StringBuilder. Он работает как изменяемый буфер.

StringBuilder sb = new StringBuilder("Java");
sb.append(" ");
sb.append("за");
sb.append(" ");
sb.append(30);
sb.append(" дней");

String result = sb.toString(); // Превращаем обратно в строку
System.out.println(result); // Вывод: Java за 30 дней

Сравнение строк

Никогда не сравнивайте строки через ==. Оператор == проверяет, являются ли переменные ссылкой на один и тот же объект в памяти. Чтобы проверить, одинаковый ли у строк текст, используйте метод .equals().

String s1 = "Hello";
String s2 = new String("Hello");

System.out.println(s1 == s2);       // false (разные объекты в памяти)
System.out.println(s1.equals(s2));  // true (текст одинаковый)

Фреймворк Коллекций (Collections Framework)

На прошлой неделе мы создавали массив животных: Animal[] animals = new Animal[2];. У массивов есть фатальный недостаток: фиксированный размер. Вы должны знать заранее, сколько элементов вам нужно. Но что, если мы пишем список покупок и не знаем, сколько товаров добавит пользователь?

Здесь на помощь приходят коллекции. Это «умные» контейнеры для данных, которые умеют динамически расширяться.

1. List (Список)

Самая популярная коллекция — ArrayList. Это «резиновый» массив. Он хранит элементы в порядке добавления и позволяет дубликаты.

import java.util.ArrayList;

ArrayList<String> todoList = new ArrayList<>();

todoList.add("Купить молоко");
todoList.add("Выучить Java");
todoList.add("Поспать");

System.out.println(todoList.get(1)); // Вывод: Выучить Java
System.out.println(todoList.size());   // Вывод: 3

todoList.remove("Поспать"); // Удаление элемента

Обратите внимание на <String>. Это Generics (обобщения). Мы указываем Java, что в этом списке будут лежать только строки.

2. Set (Множество)

Если вам нужен список уникальных элементов (где повторы запрещены), используйте HashSet. Порядок элементов здесь не гарантируется.

import java.util.HashSet;

HashSet<String> names = new HashSet<>();
names.add("Анна");
names.add("Борис");
names.add("Анна"); // Этот элемент не добавится, так как "Анна" уже есть

System.out.println(names.size()); // Вывод: 2

3. Map (Словарь или Карта)

Это самая интересная структура. Она хранит данные парами: Ключ -> Значение. Как в реальном словаре: слово (ключ) -> перевод (значение). Ключи должны быть уникальны.

Самая популярная реализация — HashMap.

import java.util.HashMap;

// Ключ - String (имя), Значение - Integer (возраст)
HashMap<String, Integer> phoneBook = new HashMap<>();

phoneBook.put("Алекс", 25);
phoneBook.put("Мария", 30);

System.out.println(phoneBook.get("Алекс")); // Вывод: 25
Как работает HashMap внутри? (Немного математики)

Чтобы HashMap работала быстро (находила значение по ключу почти мгновенно), она использует вычисление хеш-кода. Когда вы кладете пару в карту, Java вычисляет индекс ячейки (bucket), куда положить данные, по формуле:

index=h(modn)index = h \pmod n

Где:

  • indexindex — итоговый номер ячейки в массиве, где будет храниться запись.
  • hh — хеш-код ключа (целое число, генерируемое методом hashCode()).
  • nn — текущий размер внутреннего массива (емкость карты).
  • (mod)\pmod{} — операция взятия остатка от деления.

Благодаря этой формуле, Java не перебирает весь список, чтобы найти «Алекса», а сразу вычисляет математически, в какой ячейке он лежит. Это делает поиск молниеносным.

Итерация по коллекциям

Как пройтись по всем элементам коллекции? Используйте цикл for-each, который мы изучили ранее. Он отлично работает с коллекциями.

for (String task : todoList) {
    System.out.println("Надо сделать: " + task);
}

Заключение

Сегодня вы получили мощнейшие инструменты.

  • Исключения делают ваш код устойчивым к сбоям.
  • StringBuilder экономит память при работе с текстом.
  • Коллекции (List, Set, Map) позволяют удобно хранить любые объемы данных.

Теперь вы можете написать не просто калькулятор, а, например, записную книжку, систему учета студентов или простой чат-бот. Это уже уровень Junior-разработчика.

На следующей неделе мы коснемся продвинутых тем: работы с файлами (ввод-вывод) и основ многопоточности. А пока — закрепите знания на практике!

Дни 22-26: Продвинутые темы: потоки ввода-вывода (I/O) и введение в Stream API

Дни 22-26: Продвинутые темы: потоки ввода-вывода (I/O) и введение в Stream API

Приветствую вас, студенты! Мы выходим на финишную прямую нашего курса «Java за 30 дней». Вы уже научились моделировать объекты, обрабатывать ошибки и хранить данные в коллекциях. Но до сих пор ваши программы жили в «вакууме»: данные исчезали сразу после завершения работы программы, и вы не могли загрузить информацию извне.

В ближайшие несколько дней мы разрушим эту стену. Мы научимся работать с файловой системой, читать и записывать данные (I/O), а также познакомимся с одним из самых мощных инструментов современной Java — Stream API, который превращает обработку данных в элегантный конвейер.

Часть 1: Потоки ввода-вывода (Java I/O)

В Java любое чтение или запись данных (будь то файл, сетевое соединение или консоль) происходит через потоки (Streams). Не путайте их с Stream API, о котором мы поговорим во второй части лекции. Здесь «поток» — это последовательность данных, текущая от источника к приемнику.

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

Пакет java.io делит все потоки на две большие группы:

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

Золотое правило: Если вы работаете с текстом — используйте Reader/Writer. Если с чем-то другим — InputStream/OutputStream.

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

Давайте попробуем записать текст в файл. Для этого используем класс FileWriter.

import java.io.FileWriter;
import java.io.IOException;

public class WriteToFile {
    public static void main(String[] args) {
        try (FileWriter writer = new FileWriter("notes.txt")) {
            writer.write("Привет, Java!\n");
            writer.write("Это мой первый записанный файл.");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Обратите внимание на конструкцию try (...). Это try-with-resources, введенный в Java 7. Он автоматически закрывает поток после завершения работы, даже если произошла ошибка. Раньше нам приходилось писать блок finally и вызывать метод close() вручную, что часто приводило к утечкам памяти.

Буферизация

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

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

Для этого мы «оборачиваем» наши потоки в BufferedReader или BufferedWriter.

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class ReadFile {
    public static void main(String[] args) {
        try (BufferedReader reader = new BufferedReader(new FileReader("notes.txt"))) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

Часть 2: Введение в Stream API (Java 8+)

Теперь перейдем к теме, которая изменила стиль написания кода на Java навсегда. Stream API (пакет java.util.stream) — это инструмент для обработки коллекций данных в функциональном стиле.

Представьте, что у вас есть список из 1000 товаров, и вам нужно найти все товары дороже 5000 рублей, отсортировать их по названию и получить список их имен. В старом стиле (императивном) вы бы написали огромный цикл for с кучей условий if. В новом стиле (декларативном) вы просто описываете, что хотите получить.

Конвейер операций

Работа со стримом похожа на заводской конвейер:

  1. Источник (Source): Коллекция, массив или файл.
  2. Промежуточные операции (Intermediate): Фильтрация, преобразование, сортировка. Они ленивы (lazy) — ничего не происходит, пока не будет вызвана терминальная операция.
  3. Терминальная операция (Terminal): Получение результата (сбор в список, сумма, вывод на экран).

Основные методы Stream API

Рассмотрим пример. У нас есть список имен: ["Анна", "Иван", "Алексей", "Ольга", "Александр"]. Мы хотим оставить только имена на букву «А», перевести их в верхний регистр и отсортировать.

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class StreamExample {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Анна", "Иван", "Алексей", "Ольга", "Александр");

        List<String> result = names.stream() // 1. Создаем стрим
                .filter(name -> name.startsWith("А")) // 2. Фильтруем (остаются Анна, Алексей, Александр)
                .map(name -> name.toUpperCase())      // 3. Преобразуем (АННА, АЛЕКСЕЙ, АЛЕКСАНДР)
                .sorted()                             // 4. Сортируем (АЛЕКСАНДР, АЛЕКСЕЙ, АННА)
                .collect(Collectors.toList());        // 5. Собираем результат в новый список

        System.out.println(result);
    }
}

Разберем ключевые методы:

  • filter(Predicate): Пропускает дальше только те элементы, которые удовлетворяют условию.
  • map(Function): Преобразует каждый элемент в нечто другое (например, строку в число или объект в одно из его полей).
  • sorted(): Сортирует элементы.
  • collect(): Превращает стрим обратно в коллекцию.

Математика в Stream API

Stream API отлично подходит для вычислений. Допустим, нам нужно найти сумму квадратов всех четных чисел в списке. Математически это выглядит так:

S=∑x∈Evenx2S = \sum_{x \in Even} x^2

Где:

  • SS — искомая сумма.
  • ∑\sum — знак суммирования.
  • x∈Evenx \in Even — условие, что число xx принадлежит множеству четных чисел списка.
  • x2x^2 — операция возведения числа в квадрат.

В коде это реализуется буквально в одну строку:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);

int sum = numbers.stream()
        .filter(n -> n % 2 == 0) // Оставляем четные: 2, 4, 6
        .mapToInt(n -> n * n)    // Возводим в квадрат: 4, 16, 36
        .sum();                  // Суммируем: 56

System.out.println(sum); // Вывод: 56

Лямбда-выражения

Вы заметили странный синтаксис n -> n * n? Это лямбда-выражения. Это короткий способ записи анонимных функций. Читается как: «берем n и возвращаем n * n».

Связь I/O и Stream API

Эти две темы прекрасно работают вместе. Например, класс Files позволяет читать строки из файла сразу в виде стрима.

import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.stream.Stream;

public class FileStream {
    public static void main(String[] args) {
        try (Stream<String> stream = Files.lines(Paths.get("notes.txt"))) {
            stream.filter(line -> line.contains("Java"))
                  .forEach(System.out::println);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

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

Заключение

За эти дни вы получили инструменты профессионального уровня. java.io позволяет вашей программе общаться с внешним миром, а Stream API дает возможность обрабатывать данные лаконично и эффективно.

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

Практикуйтесь, экспериментируйте с файлами и стримами. Удачи!

Дни 27-30: Разработка финального консольного приложения и разбор частых ошибок

Дни 27-30: Разработка финального консольного приложения и разбор частых ошибок

Поздравляю! Вы добрались до финала. За прошедшие 26 дней вы прошли путь от «Hello World» до понимания потоков ввода-вывода и Stream API. Теперь пришло время собрать все знания воедино и создать что-то работающее, полезное и архитектурно верное.

В эти последние дни мы напишем Консольный Менеджер Задач (Task Manager). Это классический проект для начинающих, который позволяет отработать навыки работы с ООП, коллекциями, файлами и обработкой исключений.

Архитектура приложения

Прежде чем писать код, нужно спроектировать структуру. Новички часто пишут весь код в одном классе Main. Это ошибка. Мы разделим приложение на три логические части:

  1. Модель данных (Model): Класс, описывающий саму задачу.
  2. Логика (Service/Controller): Класс, управляющий списком задач (добавление, удаление, сохранение).
  3. Интерфейс (View): Класс, отвечающий за общение с пользователем через консоль.

Шаг 1: Создаем модель (Task.java)

Наша задача — это объект. У нее должны быть свойства: уникальный номер (id), описание и статус (выполнена или нет).

import java.io.Serializable;

public class Task implements Serializable {
    private static int idCounter = 1;
    private int id;
    private String title;
    private boolean isDone;

    public Task(String title) {
        this.id = idCounter++;
        this.title = title;
        this.isDone = false;
    }

    // Геттеры и метод toString для красивого вывода
    public int getId() { return id; }
    public String getTitle() { return title; }
    public boolean isDone() { return isDone; }

    public void setDone(boolean done) { isDone = done; }

    @Override
    public String toString() {
        // Используем тернарный оператор для галочки
        String status = isDone ? "[X]" : "[ ]";
        return id + ". " + status + " " + title;
    }
}

Мы добавили implements Serializable, чтобы Java могла легко превратить этот объект в поток байтов для сохранения в файл.

Шаг 2: Пишем логику (TaskManager.java)

Этот класс будет хранить список задач и выполнять операции над ними. Здесь мы применим коллекции и Stream API.

import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

public class TaskManager {
    private List<Task> tasks;
    private final String FILE_NAME = "tasks.dat";

    public TaskManager() {
        this.tasks = new ArrayList<>();
        loadTasks(); // Загружаем при старте
    }

    public void addTask(String title) {
        tasks.add(new Task(title));
    }

    public void removeTask(int id) {
        // Удаляем задачу, если id совпадает
        tasks.removeIf(task -> task.getId() == id);
    }

    public void markAsDone(int id) {
        for (Task task : tasks) {
            if (task.getId() == id) {
                task.setDone(true);
                return;
            }
        }
        System.out.println("Задача с таким ID не найдена.");
    }

    public List<Task> getAllTasks() {
        return tasks;
    }

    // Метод для фильтрации только активных задач через Stream API
    public List<Task> getActiveTasks() {
        return tasks.stream()
                .filter(task -> !task.isDone())
                .collect(Collectors.toList());
    }

    // Сохранение в файл (Сериализация)
    public void saveTasks() {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(FILE_NAME))) {
            oos.writeObject(tasks);
            System.out.println("Задачи сохранены.");
        } catch (IOException e) {
            System.out.println("Ошибка сохранения: " + e.getMessage());
        }
    }

    // Загрузка из файла (Десериализация)
    @SuppressWarnings("unchecked")
    private void loadTasks() {
        File file = new File(FILE_NAME);
        if (file.exists()) {
            try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) {
                tasks = (List<Task>) ois.readObject();
            } catch (IOException | ClassNotFoundException e) {
                System.out.println("Ошибка загрузки: " + e.getMessage());
            }
        }
    }
}

Математическая логика фильтрации

В методе getActiveTasks мы использовали фильтр. С точки зрения математической логики, мы формируем подмножество AA из множества всех задач TT:

A={t∈T∣¬tdone}A = \{ t \in T \mid \neg t_{done} \}

Где:

  • AA — множество активных (невыполненных) задач.
  • TT — множество всех существующих задач в списке.
  • tt — конкретная задача (элемент множества).
  • ∣\mid — разделитель, читается как «таких, что».
  • ¬tdone\neg t_{done} — логическое отрицание свойства «выполнена» (то есть задача не выполнена).

Шаг 3: Интерфейс пользователя (Main.java)

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

import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        TaskManager manager = new TaskManager();

        while (true) {
            System.out.println("\n=== МЕНЮ ===");
            System.out.println("1. Добавить задачу");
            System.out.println("2. Показать все задачи");
            System.out.println("3. Выполнить задачу");
            System.out.println("4. Удалить задачу");
            System.out.println("5. Сохранить и Выйти");
            System.out.print("Выберите пункт: ");

            String choice = scanner.nextLine();

            try {
                switch (choice) {
                    case "1":
                        System.out.print("Введите название: ");
                        String title = scanner.nextLine();
                        manager.addTask(title);
                        break;
                    case "2":
                        manager.getAllTasks().forEach(System.out::println);
                        break;
                    case "3":
                        System.out.print("Введите ID задачи: ");
                        int doneId = Integer.parseInt(scanner.nextLine());
                        manager.markAsDone(doneId);
                        break;
                    case "4":
                        System.out.print("Введите ID задачи: ");
                        int removeId = Integer.parseInt(scanner.nextLine());
                        manager.removeTask(removeId);
                        break;
                    case "5":
                        manager.saveTasks();
                        System.out.println("До свидания!");
                        return; // Завершение программы
                    default:
                        System.out.println("Неверный ввод.");
                }
            } catch (NumberFormatException e) {
                System.out.println("Ошибка: Введите число, а не текст!");
            }
        }
    }
}

Топ-5 ошибок новичков (Anti-patterns)

Теперь, когда код написан, давайте разберем ошибки, которые часто допускают Junior-разработчики.

1. Проглатывание исключений (Swallowing Exceptions)

Плохо:

try {
    // код
} catch (IOException e) {
    // пустота
}

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

2. Злоупотребление static

Новички любят делать все методы и поля static, чтобы вызывать их из main без создания объекта. Это убивает ООП. Статика нужна только для утилитных методов (как Math.pow) или констант. Данные должны принадлежать объектам.

3. Магические числа

Плохо: if (status == 1) ... Что такое 1? Через неделю вы забудете. Используйте константы или Enum. Хорошо: if (status == Status.ACTIVE) ...

4. NullPointerException (NPE)

Это самая частая ошибка. Она возникает, когда вы пытаетесь вызвать метод у переменной, которая равна null.

Oobj→method()  ⟹  Crash, если Oobj=∅O_{obj} \to \text{method}() \implies \text{Crash}, \text{ если } O_{obj} = \emptyset

Где:

  • OobjO_{obj} — ссылка на объект.
  • →\to — попытка обращения.
  • ∅\emptyset — пустое значение (null).
  •   ⟹  \implies — логическое следствие.

Всегда проверяйте объекты на null перед использованием или используйте Optional.

5. Нарушение конвенций именования

В Java принят CamelCase.

  • Классы: MyTask (с большой буквы).
  • Методы и переменные: calculateSum, userName (с маленькой).
  • Константы: MAX_VALUE (верхний регистр).

Что учить дальше?

Этот курс — только начало. Чтобы стать профессионалом, вам предстоит изучить:

  1. Системы сборки: Maven или Gradle (чтобы не подключать библиотеки вручную).
  2. Базы данных: SQL, JDBC, Hibernate (хранить данные в файлах — это несерьезно).
  3. Spring Framework: Стандарт де-факто в мире Java Enterprise.
  4. Git: Система контроля версий для командной работы.

Спасибо, что были с нами эти 30 дней. Практикуйтесь, пишите код каждый день, и у вас все получится!

«Программирование — это не о том, что вы знаете. Это о том, что вы можете выяснить». — Крис Пайн

Удачи в ваших проектах!