Разработка объектно-ориентированного приложения «Калькулятор» на Java: Комплексная курсовая работа

Когда речь заходит о корпоративном секторе, где надежность и масштабируемость являются краеугольными камнями успеха, Java доминирует: до 99% опрошенных организаций активно используют этот язык. Неудивительно, что в мире, где цифровая инфраструктура пронизывает все сферы жизни, от мобильных приложений до высоконагруженных финансовых систем, Java является фундаментальным выбором. Это не просто язык, это экосистема, которая постоянно развивается, о чем свидетельствует недавний выпуск Java 25 LTS 16 сентября 2025 года, продолжающий двухлетний цикл стабильных релизов, что подчеркивает её актуальность и перспективность для современных ИТ-решений.

Введение

В современном мире информационных технологий, где повсеместное внедрение цифровых решений становится нормой, разработка программного обеспечения является одной из наиболее динамично развивающихся отраслей. Язык программирования Java, со своим принципом «Write Once, Run Anywhere» (WORA), продолжает оставаться одним из краеугольных камней этой индустрии, обеспечивая надежность, безопасность и масштабируемость для широкого спектра приложений. От корпоративных систем до мобильных устройств, от облачных платформ до встроенных систем, Java демонстрирует свою универсальность и актуальность.

Актуальность разработки Java-приложений и их роль в современном мире ИТ.

Актуальность Java в 2025 году не подлежит сомнению. Язык является основой для колоссального количества устройств и систем — по некоторым оценкам, на Java работают около 3 миллиардов устройств по всему миру. Его доминирование в корпоративной среде неоспоримо, где он используется для создания критически важных бизнес-приложений, платформ электронной коммерции, финансовых систем (финтех), CRM, ERP и B2B-решений. Более того, с развитием искусственного интеллекта и машинного обучения, почти 70% организаций уже применяют Java для функциональности ИИ, что подчеркивает его адаптивность к новым технологическим вызовам. Простота изучения, объектно-ориентированный подход, независимость от платформы, встроенные механизмы безопасности и высокая производительность делают Java идеальным выбором для студентов и профессионалов, стремящихся внести свой вклад в развитие цифровой экономики. Активное и многочисленное сообщество Java-разработчиков (порядка 9 миллионов человек) служит мощной опорой для обмена знаниями, решения проблем и непрерывного развития технологий.

Цели и задачи курсовой работы: от теоретического обоснования до практической реализации и анализа.

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

  1. Теоретическое обоснование: Детально изучить и представить ключевые принципы ООП (инкапсуляцию, наследование, полиморфизм, абстракцию), показав их роль в проектировании модульных и расширяемых систем.
  2. Архитектурное проектирование: Выбрать и обосновать архитектурный паттерн (например, MVC) и фреймворк для графического интерфейса (JavaFX), а также спроектировать классы приложения в соответствии с принципами ООП.
  3. Практическая реализация: Разработать базовый функционал калькулятора, включающий арифметические операции, и расширить его до строкового калькулятора с поддержкой парсинга сложных математических выражений.
  4. Тестирование: Внедрить комплексную методологию тестирования, включающую юнит-тестирование с JUnit, использование Mockito для изоляции зависимостей и рассмотрение принципов Test-Driven Development (TDD), а также проанализировать экономическую эффективность автоматизированного тестирования.
  5. Анализ и перспективы: Оценить возможности масштабирования и оптимизации разработанного приложения, а также предложить пути дальнейшего развития функционала.

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

Курсовая работа организована таким образом, чтобы последовательно раскрывать все аспекты создания современного Java-приложения:

  • Теоретические основы ООП закладывают фундамент для понимания принципов проектирования.
  • Проектирование архитектуры переводит теоретические знания в конкретные архитектурные решения.
  • Реализация функционала демонстрирует практическое применение выбранных подходов, начиная с базового и переходя к строковому калькулятору.
  • Тестирование обеспечивает качество и надежность разработанного кода.
  • Наконец, Масштабирование, оптимизация и перспективы развития предлагают взгляд в будущее проекта, указывая на пути его совершенствования.

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

Теоретические основы объектно-ориентированного программирования

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

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

Объектно-ориентированное программирование основывается на четырех столпах: инкапсуляции, наследовании, полиморфизме и абстракции. Эти принципы не просто теоретические концепции; они являются мощными инструментами для создания модульных, повторно используемых и легко поддерживаемых программных компонентов, значительно упрощая процесс разработки и сопровождения сложных систем, что особенно важно в больших проектах.

Детальное рассмотрение каждого принципа с примерами их применения в контексте проектирования калькулятора.

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

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

В контексте проектирования калькулятора, инкапсуляция проявляется, например, в классе CalculatorEngine. Внутреннее состояние калькулятора, такое как текущий результат (currentResult) или последнее введенное число (lastNumber), может быть объявлено как private. Это означает, что эти поля доступны только внутри класса CalculatorEngine. Для взаимодействия с этими полями предоставляются публичные методы (геттеры и сеттеры) или методы, выполняющие операции, например, add(double value) или getResult().

public class CalculatorEngine {
    private double currentResult; // Инкапсулированное поле
    private String currentExpression; // Инкапсулированное поле

    public CalculatorEngine() {
        this.currentResult = 0;
        this.currentExpression = "";
    }

    // Метод для получения результата (геттер)
    public double getCurrentResult() {
        return currentResult;
    }

    // Метод для установки результата (сеттер, возможно, только для внутреннего использования или при инициализации)
    public void setCurrentResult(double result) {
        this.currentResult = result;
    }

    // Метод для добавления числа
    public void add(double value) {
        currentResult += value;
        updateExpression(String.valueOf(value), "+");
    }

    // Внутренний вспомогательный метод, скрытый от внешнего мира
    private void updateExpression(String value, String operator) {
        if (!currentExpression.isEmpty()) {
            currentExpression += " " + operator + " ";
        }
        currentExpression += value;
    }

    public String getCurrentExpression() {
        return currentExpression;
    }
}

Здесь currentResult и currentExpression защищены модификатором private, а взаимодействие с ними происходит через публичные методы, что гарантирует контролируемый доступ и модификацию, предотвращая случайные или нежелательные изменения извне.

Модификаторы доступа в Java и их роль в инкапсуляции.

В Java модификаторы доступа (public, protected, default (без модификатора) и private) играют ключевую роль в реализации инкапсуляции.

  • private: Члены класса, объявленные как private, доступны только изнутри того же класса. Это самый строгий уровень инкапсуляции, идеально подходящий для внутренних полей и вспомогательных методов.
  • default (пакетный доступ): Члены доступны из любого класса в том же пакете.
  • protected: Доступны из классов в том же пакете и из подклассов (даже если они находятся в другом пакете).
  • public: Члены доступны из любого класса. Используется для методов, составляющих публичный API объекта.

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

Наследование: Повторное использование кода и расширяемость

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

В калькуляторе, например, можно создать базовый абстрактный класс Operation, который определяет общий интерфейс для всех математических операций. Затем конкретные операции, такие как Addition, Subtraction, Multiplication, Division, могут наследоваться от этого класса, реализуя свой специфический способ вычисления.

// Абстрактный базовый класс для всех операций
public abstract class Operation {
    public abstract double execute(double operand1, double operand2);
    public abstract int getPrecedence(); // Приоритет операции
    public abstract boolean isLeftAssociative(); // Левоассоциативность
    public abstract String getSymbol(); // Символ операции
}

// Конкретная операция сложения
public class Addition extends Operation {
    @Override
    public double execute(double operand1, double operand2) {
        return operand1 + operand2;
    }

    @Override
    public int getPrecedence() {
        return 1; // Низкий приоритет
    }

    @Override
    public boolean isLeftAssociative() {
        return true;
    }

    @Override
    public String getSymbol() {
        return "+";
    }
}

// Конкретная операция вычитания
public class Subtraction extends Operation {
    @Override
    public double execute(double operand1, double operand2) {
        return operand1 - operand2;
    }

    @Override
    public int getPrecedence() {
        return 1; // Низкий приоритет
    }

    @Override
    public boolean isLeftAssociative() {
        return true;
    }

    @Override
    public String getSymbol() {
        return "-";
    }
}

Таким образом, Addition и Subtraction наследуют общую структуру от Operation, но реализуют метод execute по-своему, демонстрируя как наследование, так и полиморфизм.

Полиморфизм: Единый интерфейс для различных реализаций

Полиморфизм, что в переводе с греческого означает «много форм», позволяет объектам разных классов обрабатывать данные по-разному, но через единый интерфейс. В Java это достигается путем переопределения методов в подклассах (runtime polymorphism) или путем перегрузки методов (compile-time polymorphism).

Продолжая пример с операциями калькулятора, если у нас есть коллекция объектов типа Operation, мы можем вызвать метод execute() на каждом из них, не заботясь о том, является ли это Addition или Subtraction. Java Virtual Machine (JVM) во время выполнения определит, какой конкретный метод execute() должен быть вызван.

public class Calculator {
    public double calculate(Operation op, double operand1, double operand2) {
        return op.execute(operand1, operand2);
    }

    public static void main(String[] args) {
        Calculator calc = new Calculator();
        Operation add = new Addition();
        Operation sub = new Subtraction();

        System.out.println("2 + 3 = " + calc.calculate(add, 2, 3)); // Вызывается Addition.execute()
        System.out.println("5 - 1 = " + calc.calculate(sub, 5, 1)); // Вызывается Subtraction.execute()
    }
}

Здесь метод calculate принимает объект Operation, и благодаря полиморфизму, он корректно вызывает метод execute соответствующего типа операции. Это делает код гибким и легко расширяемым.

Абстракция: Сокрытие сложности

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

В нашем примере класс Operation является абстрактным. Он определяет, что любая операция должна иметь метод execute(), getPrecedence(), isLeftAssociative() и getSymbol(), но не диктует, как именно они должны быть реализованы. Это оставляет свободу для конкретных реализаций, скрывая детали от пользователя Operation (например, класса Calculator).

Принципы «отделения математической логики от логики пользовательского интерфейса» для повышения модульности и тестируемости.

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

  • Математическая логика (Model в паттерне MVC) должна быть полностью независима от способа отображения или взаимодействия с пользователем. Классы, такие как CalculatorEngine и ExpressionParser, содержат всю вычислительную логику, алгоритмы парсинга и обработки выражений. Их можно тестировать в полной изоляции, без необходимости запускать графический интерфейс.
  • Логика пользовательского интерфейса (View и Controller в MVC) занимается только отображением данных и обработкой пользовательского ввода. Она должна вызывать методы математической логики, но не должна содержать сами вычислительные алгоритмы.

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

Проектирование архитектуры приложения «Калькулятор»

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

Выбор архитектурного паттерна: обоснование применения MVC (Model-View-Controller) или аналогичного паттерна для разделения ответственности.

При проектировании интерактивных приложений, особенно с графическим пользовательским интерфейсом (GUI), жизненно важно разделить ответственности между различными компонентами системы. Это не только упрощает разработку и отладку, но и значительно повышает сопровождаемость и расширяемость кода. Один из наиболее проверенных и широко используемых паттернов для этой цели — Model-View-Controller (MVC).

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

  1. Model (Модель): Представляет собой данные и бизнес-логику приложения. В контексте калькулятора, это будет CalculatorEngine, ExpressionParser, классы Operation — все, что отвечает за выполнение вычислений, хранение текущего состояния (например, числа на дисплее, история операций) и обработку математических выражений. Модель полностью независима от пользовательского интерфейса.
  2. View (Представление): Отвечает за отображение данных пользователю. Это все компоненты графического интерфейса: кнопки, текстовые поля для ввода и вывода, панели и макеты. View получает данные от Model и отображает их, но не содержит никакой бизнес-логики.
  3. Controller (Контроллер): Выступает в роли посредника между Model и View. Он обрабатывает пользовательский ввод (например, нажатия кнопок), интерпретирует его и передает соответствующие команды Model для обновления данных или View для изменения отображения. В случае калькулятора, Controller будет реагировать на нажатия кнопок, передавать числа и операции в CalculatorEngine и обновлять дисплей калькулятора.

Обоснование применения MVC:

  • Разделение ответственности: Каждый компонент имеет четко определенную роль, что упрощает понимание и модификацию отдельных частей без влияния на другие.
  • Повышенная модульность: Модель может быть переиспользована с различными представлениями (например, консольный калькулятор, GUI-калькулятор, веб-калькулятор).
  • Улучшенная тестируемость: Компоненты Model могут быть легко протестированы изолированно, что является критически важным для обеспечения качества кода. View и Controller также могут быть протестированы с использованием моков.
  • Гибкость: Позволяет легко изменять или заменять компоненты. Например, можно изменить дизайн View без изменения логики Model.

Альтернативные паттерны, такие как MVP (Model-View-Presenter) или MVVM (Model-View-ViewModel), также предлагают схожие преимущества по разделению ответственности и могут быть рассмотрены, но для такого приложения, как калькулятор, классический MVC является более чем достаточным и понятным решением.

Сравнительный анализ фреймворков для разработки графического интерфейса: Java Swing vs JavaFX.

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

Характеристика Java Swing JavaFX
Поколение GUI-фреймворка Второе поколение Третье поколение
Год выпуска 1997 (первая версия) 2008 (первая версия), активно развивается с 2011
Внешний вид (Look-and-Feel) Стандартные виджеты ОС или универсальный «Metal» Современный, настраиваемый с помощью CSS
Стилизация Ограниченные возможности, требует кастомизации кода Полная поддержка CSS-стилизации, FXML для декларативного UI
Разделение UI и логики Возможно, но часто сопряжено с большим объемом кода Нативная поддержка FXML и Scene Builder для четкого разделения
Производительность Может быть ниже на сложных интерфейсах, зависит от Look-and-Feel Аппаратное ускорение графики, высокая производительность
Мультимедиа и 3D Ограниченная или отсутствует встроенная поддержка Встроенная поддержка веб-просмотров, воспроизведения медиа, 3D-графики
Поддержка Oracle Поддержка снижается, фреймворк считается «legacy» Активная поддержка и развитие (OpenJFX)
Кроссплатформенность Windows, macOS, Linux Windows, macOS, Linux, Android, iOS, Raspberry Pi
Статус в экосистеме Java Входит в JDK до Java 10, после — опциональный Отдельный модуль (OpenJFX) с Java 11, активно развивается сообществом

Аргументированный выбор JavaFX для реализации GUI: современные возможности, CSS-стилизация, FXML, Scene Builder, кроссплатформенность и активная поддержка.

Для проекта, ориентированного на современные подходы и будущие перспективы, однозначно следует выбрать JavaFX. Хотя Swing до сих пор присутствует в некоторых старых проектах, он считается устаревшим фреймворком второго поколения, и его поддержка со стороны Oracle значительно снижена. JavaFX, напротив, является фреймворком третьего поколения, активно поддерживаемым сообществом OpenJFX, и предлагает ряд критически важных преимуществ:

  • Современный внешний вид: JavaFX позволяет создавать визуально привлекательные и отзывчивые интерфейсы, которые соответствуют современным стандартам UI/UX.
  • CSS-стилизация: Возможность стилизации элементов интерфейса с помощью CSS значительно упрощает адаптацию внешнего вида к корпоративному стилю или создание кастомных тем, что несравнимо сложнее в Swing.
  • FXML и Scene Builder: FXML (FX Markup Language) позволяет декларативно описывать пользовательский интерфейс в XML-файлах, отделяя структуру UI от бизнес-логики. Инструмент Scene Builder предоставляет визуальный редактор для FXML, ускоряя разработку фронтенда и позволяя дизайнерам работать параллельно с разработчиками. Это значительно эффективнее по сравнению с полностью кодовым подходом Swing.
  • Кроссплатформенность: JavaFX поддерживает широкий спектр платформ, включая Windows, macOS, Linux, а также мобильные платформы Android и iOS (через сторонние библиотеки, такие как Gluon Mobile) и даже Raspberry Pi, что открывает более широкие возможности для развертывания.
  • Активная поддержка и развитие: В отличие от Swing, JavaFX продолжает активно развиваться сообществом OpenJFX, получая новые функции и улучшения производительности. Для новых проектов (greenfield projects) JavaFX является предпочтительным выбором, поскольку он представляет собой будущее Java-разработки настольных пользовательских интерфейсов.

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

Проектирование классов для базового и строкового калькулятора с учетом принципов ООП: CalculatorEngine, Operation, UIController, ExpressionParser и т.д.

Для реализации калькулятора с применением принципов ООП и MVC, можно выделить следующие ключевые классы:

  • CalculatorApp (View/Controller): Главный класс приложения JavaFX, отвечающий за инициализацию GUI, загрузку FXML, и выступающий в роли основного контроллера, связывающего View и Model.
  • UIController (Controller): Класс, связанный с FXML-файлом. Он обрабатывает события GUI (нажатия кнопок), обновляет текстовые поля дисплея и взаимодействует с CalculatorEngine и ExpressionParser.
  • CalculatorEngine (Model): Ядро математической логики. Этот класс инкапсулирует текущее состояние калькулятора (текущий результат, последний операнд, последняя операция) и выполняет базовые арифметические операции. Он не должен знать о GUI.
  • Operation (Model — Абстракция): Абстрактный класс или интерфейс, определяющий общий контракт для всех математических операций (например, execute(operand1, operand2), getPrecedence(), isLeftAssociative(), getSymbol()).
  • Конкретные классы операций (Model — Наследование/Полиморфизм): Addition, Subtraction, Multiplication, Division, Power, SquareRoot и т.д., наследующие от Operation и реализующие специфическую логику вычислений.
  • ExpressionParser (Model): Отвечает за преобразование строковых математических выражений из инфиксной записи в обратную польскую запись (ОПЗ) с учетом приоритетов операций и скобок. Использует алгоритм «сортировочная станция».
  • RPNCalculator (Model): Класс, который принимает выражение в ОПЗ и вычисляет его результат с использованием стека.
  • Token (Model — Вспомогательный): Вспомогательный класс или enum для представления лексем (чисел, операторов, скобок) в процессе парсинга.
  • ValidationService (Model — Вспомогательный): Отвечает за проверку корректности вводимых выражений и обработку ошибок (например, деление на ноль, некорректный синтаксис).

UML-диаграммы классов (опционально, как рекомендация для студента).

Для наглядного представления структуры и взаимосвязей между классами крайне полезно использовать UML-диаграммы классов. Они позволяют визуализировать принципы ООП, такие как наследование (стрелка с пустым треугольником), ассоциации (линии между классами) и зависимости, делая архитектуру более понятной и легко обсуждаемой. Например, диаграмма могла бы показать, как UIController взаимодействует с CalculatorEngine и ExpressionParser, а также как Addition и другие операции наследуют от Operation. Создание такой диаграммы является хорошей практикой для студента, помогающей глубже осмыслить архитектуру.

Реализация функционала базового калькулятора

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

Основные компоненты GUI JavaFX: кнопки, текстовые поля, макеты.

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

  • Button: Используется для создания всех кнопок калькулятора (цифры, операции, очистка, равно).
  • TextField или Label: Для отображения вводимых чисел, текущего выражения и результата. TextField позволяет пользователю вводить данные напрямую, а Label просто отображает текст. Для калькулятора часто используется TextField для интерактивного отображения, но с блокировкой прямого ввода, чтобы все операции происходили через кнопки.
  • Макеты (Layout Panes): JavaFX предлагает различные типы макетов для организации компонентов в окне.
    • GridPane: Идеально подходит для расположения кнопок калькулятора в виде сетки, что значительно упрощает создание классического расположения.
    • VBox (Vertical Box) и HBox (Horizontal Box): Для вертикального или горизонтального размещения компонентов. Например, VBox может содержать TextField для дисплея и GridPane для кнопок.
    • BorderPane: Позволяет размещать компоненты в пяти областях: верх, низ, лево, право, центр.

Пример структуры FXML для калькулятора мог бы выглядеть так:

<VBox xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.example.UIController">
    <padding>
        <Insets top="10" right="10" bottom="10" left="10"/>
    </padding>
    <spacing>10</spacing>

    <TextField fx:id="display" editable="false" prefHeight="50" alignment="CENTER_RIGHT" style="-fx-font-size: 24pt;"/>

    <GridPane hgap="5" vgap="5">
        <!-- Ряды кнопок -->
        <rowConstraints>
            <RowConstraints percentHeight="25"/>
            <RowConstraints percentHeight="25"/>
            <RowConstraints percentHeight="25"/>
            <RowConstraints percentHeight="25"/>
        </rowConstraints>
        <columnConstraints>
            <ColumnConstraints percentWidth="25"/>
            <ColumnConstraints percentWidth="25"/>
            <ColumnConstraints percentWidth="25"/>
            <ColumnConstraints percentWidth="25"/>
        </columnConstraints>

        <!-- Кнопки: C, +/-, %, / -->
        <Button text="C" GridPane.rowIndex="0" GridPane.columnIndex="0" onAction="#handleClear"/>
        <Button text="+/-" GridPane.rowIndex="0" GridPane.columnIndex="1"/>
        <Button text="%" GridPane.rowIndex="0" GridPane.columnIndex="2"/>
        <Button text="/" GridPane.rowIndex="0" GridPane.columnIndex="3" onAction="#handleOperation"/>

        <!-- ... другие кнопки ... -->

        <Button text="=" GridPane.rowIndex="4" GridPane.columnIndex="3" onAction="#handleEquals"/>
    </GridPane>
</VBox>

Этот FXML-файл декларативно описывает структуру интерфейса, а класс UIController будет содержать логику обработки событий.

Обработка событий пользовательского интерфейса.

В JavaFX обработка событий осуществляется с помощью обработчиков (handlers). Каждый компонент может иметь привязанные к нему действия. Например, для кнопки Button можно использовать метод setOnAction(), который принимает объект типа EventHandler<ActionEvent>, или, что чаще, указывать метод контроллера непосредственно в FXML с помощью атрибута onAction.

Пример обработки события для кнопки C (Clear):

В FXML: onAction="#handleClear"

В UIController.java:

public class UIController {
    @FXML
    private TextField display; // Привязка к TextField из FXML

    private CalculatorEngine engine = new CalculatorEngine(); // Экземпляр модели

    @FXML
    private void handleClear(ActionEvent event) {
        engine.clear(); // Сброс состояния движка калькулятора
        display.setText("0"); // Обновление дисплея
    }

    @FXML
    private void handleDigit(ActionEvent event) {
        String digit = ((Button) event.getSource()).getText();
        display.setText(engine.appendDigit(digit)); // Обновление движка и дисплея
    }

    // ... другие обработчики для операций, точки, равно
}

Методы, помеченные @FXML, автоматически связываются с элементами в FXML-файле.

Базовая математическая логика: сложение, вычитание, умножение, деление.

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

public class CalculatorEngine {
    private double currentNumber;
    private double storedNumber;
    private String currentOperation;
    private boolean operandEntered; // Флаг, указывающий, что было введено число

    public CalculatorEngine() {
        clear();
    }

    public void clear() {
        currentNumber = 0;
        storedNumber = 0;
        currentOperation = "";
        operandEntered = false;
    }

    public String appendDigit(String digit) {
        if (!operandEntered) {
            currentNumber = Double.parseDouble(digit);
            operandEntered = true;
        } else {
            // Предотвращаем множественные нули в начале
            if (currentNumber == 0 && !digit.equals(".")) {
                currentNumber = Double.parseDouble(digit);
            } else {
                currentNumber = Double.parseDouble(String.valueOf(currentNumber) + digit);
            }
        }
        return String.valueOf(currentNumber);
    }

    public String performOperation(String operation) {
        if (operandEntered) {
            if (!currentOperation.isEmpty()) {
                // Если есть предыдущая операция, выполнить её
                calculateResult();
            }
            storedNumber = currentNumber;
            currentOperation = operation;
            operandEntered = false; // Сбросить флаг для нового операнда
        }
        return String.valueOf(currentNumber);
    }

    public String calculateEquals() {
        if (operandEntered && !currentOperation.isEmpty()) {
            calculateResult();
            currentOperation = ""; // Сбросить операцию после "="
            operandEntered = false;
        }
        return String.valueOf(currentNumber);
    }

    private void calculateResult() {
        switch (currentOperation) {
            case "+":
                currentNumber = storedNumber + currentNumber;
                break;
            case "-":
                currentNumber = storedNumber - currentNumber;
                break;
            case "*":
                currentNumber = storedNumber * currentNumber;
                break;
            case "/":
                if (currentNumber == 0) {
                    // Обработка деления на ноль
                    System.err.println("Error: Division by zero");
                    currentNumber = Double.NaN; // Not a Number
                } else {
                    currentNumber = storedNumber / currentNumber;
                }
                break;
            // Добавить другие операции
        }
    }
}

Реализация операций с использованием наследования и полиморфизма (например, иерархия классов для арифметических операций).

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

Допустим, у нас есть интерфейс IUnaryOperation и IBinaryOperation для унарных и бинарных операций соответственно:

public interface IOperation {
    String getSymbol();
    int getPrecedence();
    boolean isLeftAssociative();
}

public interface IBinaryOperation extends IOperation {
    double execute(double operand1, double operand2);
}

public interface IUnaryOperation extends IOperation {
    double execute(double operand);
}

// Пример бинарной операции
public class AddOperation implements IBinaryOperation {
    @Override
    public double execute(double operand1, double operand2) {
        return operand1 + operand2;
    }

    @Override
    public String getSymbol() { return "+"; }
    @Override
    public int getPrecedence() { return 1; } // Низкий приоритет
    @Override
    public boolean isLeftAssociative() { return true; }
}

// Пример унарной операции (квадратный корень)
public class SqrtOperation implements IUnaryOperation {
    @Override
    public double execute(double operand) {
        if (operand < 0) {
            throw new IllegalArgumentException("Cannot calculate square root of a negative number.");
        }
        return Math.sqrt(operand);
    }

    @Override
    public String getSymbol() { return "sqrt"; }
    @Override
    public int getPrecedence() { return 3; } // Высокий приоритет для функций
    @Override
    public boolean isLeftAssociative() { return true; } // Обычно функции ассоциируются влево
}

Этот подход позволяет легко добавлять новые операции (например, тригонометрические функции, логарифмы) без изменения существующего кода CalculatorEngine или RPNCalculator, демонстрируя мощь принципов открытости/закрытости (Open/Closed Principle) из SOLID.

CalculatorEngine может тогда работать с объектами IOperation, используя полиморфизм для вызова метода execute(), что делает его более гибким.

Разработка функционала строкового калькулятора

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

Алгоритмы парсинга математических выражений:

Для того чтобы калькулятор мог «понять» строковое выражение вроде «2 + 3 * (4 — 1)», необходимо преобразовать его из привычной инфиксной записи (где операторы находятся между операндами) в формат, более удобный для машинного вычисления. Два ключевых алгоритма помогают в этом: алгоритм «сортировочная станция» для преобразования и обратная польская запись для вычисления.

Алгоритм «сортировочная станция» (Shunting-yard algorithm) Дейкстры для преобразования в обратную польскую запись (ОПЗ).

Алгоритм «сортировочная станция» (разработанный Эдсгером Дейкстрой) является стандартным подходом для преобразования математических выражений из инфиксной нотации в обратную польскую запись (ОПЗ), также известную как постфиксная запись. Его название метафорически отсылает к железнодорожной станции, где вагоны (токены) переставляются с одной ветки (входной поток) на другую (выходная очередь или стек операторов).

Принцип работы алгоритма:

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

  1. Выходная очередь (Output Queue): Здесь накапливаются числа и операторы в ОПЗ.
  2. Стек операторов (Operator Stack): Временно хранит операторы и скобки, ожидая их размещения в выходной очереди в правильном порядке.

Процесс проходит по токенам входного выражения слева направо:

  • Число: Если токен — число, оно немедленно добавляется в выходную очередь.
  • Функция: Если токен — функция (например, sin, sqrt), он помещается в стек операторов.
  • Разделитель аргументов (запятая): Если токен — запятая, операторы из стека перемещаются в выходную очередь до тех пор, пока не будет найдена открывающая скобка.
  • Оператор: Если токен — оператор (+, -, *, /, ^):
    • Пока стек операторов не пуст, и верхний элемент стека — оператор, и (у текущего оператора более низкий приоритет, чем у верхнего оператора стека, ИЛИ у них одинаковый приоритет, но текущий оператор левоассоциативен):
      • Извлечь оператор из стека и поместить его в выходную очередь.
    • Затем текущий оператор помещается в стек.
  • Открывающая скобка ((): Помещается в стек операторов.
  • Закрывающая скобка ()):
    • Операторы из стека перемещаются в выходную очередь до тех пор, пока не будет найдена соответствующая открывающая скобка.
    • Открывающая скобка извлекается из стека и отбрасывается (не добавляется в выходную очередь).
    • Если на вершине стека после этого оказывается функция, то она извлекается и помещается в выходную очередь.
    • Если соответствующая открывающая скобка не найдена, это означает несбалансированные скобки — ошибка.
  • Окончание выражения: После обработки всех токенов, любые оставшиеся операторы в стеке перемещаются в выходную очередь. Если в стеке остаются скобки, это также ошибка несбалансированных скобок.

Подробное описание принципов ОПЗ и ее преимуществ для вычислений.

Обратная польская запись (ОПЗ), или постфиксная запись, — это математическая нотация, в которой каждый оператор следует за всеми своими операндами. Главное ее преимущество в том, что она не требует использования скобок для указания приоритета операций, что значительно упрощает машинное вычисление.

Пример:

  • Инфиксная запись: 2 + 3 * (4 - 1)
  • Обратная польская запись: 2 3 4 1 - * +

Преимущества ОПЗ для вычислений:

  • Отсутствие скобок: Исключает необходимость в сложной логике обработки скобок и приоритетов.
  • Простота вычисления: Выражения в ОПЗ могут быть вычислены одним проходом с использованием стека, что очень эффективно.
  • Однозначность: Каждое выражение в ОПЗ имеет только одно возможное значение, в отличие от инфиксной записи, где неоднозначность может возникнуть из-за приоритетов и ассоциативности.

Реализация алгоритма вычисления выражений в ОПЗ с использованием стека.

Вычисление выражения, представленного в ОПЗ, также использует стек и происходит за один проход:

  1. Создать пустой стек для операндов.
  2. Проходить по токенам ОПЗ слева направо:
    • Если токен — число: Поместить его в стек операндов.
    • Если токен — оператор:
      • Извлечь необходимое количество операндов из стека (для бинарного оператора — два, для унарного — один). Важно извлекать их в правильном порядке: первый извлеченный операнд будет вторым операндом операции, второй извлеченный — первым.
      • Выполнить операцию.
      • Поместить результат операции обратно в стек операндов.
  3. После обработки всех токенов в ОПЗ, в стеке должен остаться ровно один элемент — конечный результат вычисления.

Пример вычисления 2 3 4 1 - * +:

  1. 2: Помещаем 2 в стек. Стек: [2]
  2. 3: Помещаем 3 в стек. Стек: [2, 3]
  3. 4: Помещаем 4 в стек. Стек: [2, 3, 4]
  4. 1: Помещаем 1 в стек. Стек: [2, 3, 4, 1]
  5. -: Извлекаем 1 и 4. Вычисляем 4 - 1 = 3. Помещаем 3 в стек. Стек: [2, 3, 3]
  6. *: Извлекаем 3 и 3. Вычисляем 3 * 3 = 9. Помещаем 9 в стек. Стек: [2, 9]
  7. +: Извлекаем 9 и 2. Вычисляем 2 + 9 = 11. Помещаем 11 в стек. Стек: [11]

Конечный результат: 11.

Обработка ошибок в строковом калькуляторе: некорректный синтаксис (несбалансированные скобки, недопустимые символы), деление на ноль, некорректный порядок операций.

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

  • Некорректный синтаксис:
    • Несбалансированные скобки: Если количество открывающих скобок не равно количеству закрывающих, или если закрывающая скобка встречается раньше соответствующей открывающей. Алгоритм «сортировочная станция» автоматически выявит эти проблемы.
    • Недопустимые символы: Ввод символов, которые не являются числами, операторами или скобками. Необходимо проводить лексический анализ (токенизацию) и отсеивать недопустимые символы.
    • Пустые или неполные выражения: Например, «2 +» или «+».
  • Деление на ноль: При выполнении операции деления, если делитель равен нулю, необходимо генерировать исключение или возвращать специальное значение (Double.NaN или Double.POSITIVE_INFINITY/NEGATIVE_INFINITY).
  • Некорректный порядок операций: Алгоритм «сортировочная станция» призван решать эту проблему, но ошибки могут возникнуть при некорректной реализации самого алгоритма или при подаче на вход некорректного инфиксного выражения.

Для обработки ошибок в Java используются исключения (Exception). Например, IllegalArgumentException для синтаксических ошибок или ArithmeticException для деления на ноль.

Использование стандартных библиотек Java для работы со строками: StringBuilder и StringBuffer для эффективной конкатенации, java.util.regex для парсинга токенов.

  • StringBuilder и StringBuffer: При работе со строковыми выражениями, особенно при их построении или модификации (например, при формировании выходной ОПЗ-строки), класс String в Java является неизменяемым. Каждая операция конкатенации String создает новый объект, что может привести к значительным накладным расходам по памяти и производительности, особенно при большом количестве операций. Классы StringBuilder (непотокобезопасный, но быстрый) и StringBuffer (потокобезопасный, но медленнее) предоставляют изменяемые последовательности символов, что значительно эффективнее.
    • Пример: При выполнении 1 000 000 операций конкатенации StringBuilder может справиться за 236 миллисекунд, тогда как String может потребовать до 3.6 минут. Для однопоточного строкового калькулятора StringBuilder — идеальный выбор.
  • java.util.regex: Этот пакет предоставляет мощные инструменты для работы с регулярными выражениями через классы Pattern и Matcher. Регулярные выражения идеально подходят для:
    • Токенизации выражения: Разделения входной строки на отдельные лексемы (числа, операторы, скобки, функции). Например, регулярное выражение (\d+\.?\d*)|([+\-*/^()])|[a-zA-Z]+ может выделить числа, операторы, скобки и буквенные функции.
    • Валидации входных данных: Проверки на наличие недопустимых символов в выражении.

Примеры кода для реализации парсера и вычислителя.

1. Токенизация (лексический анализ):

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Tokenizer {
    // Регулярное выражение для чисел (целых и дробных), операторов, скобок и функций
    private static final String REGEX =
            "(\\d+\\.?\\d*)|([+\\-*/^()])|([a-zA-Z]+)"; // Добавлен захват для функций

    public List<String> tokenize(String expression) {
        List<String> tokens = new ArrayList<>();
        Pattern pattern = Pattern.compile(REGEX);
        Matcher matcher = pattern.matcher(expression.replaceAll("\\s+", "")); // Удаляем пробелы

        while (matcher.find()) {
            tokens.add(matcher.group());
        }
        return tokens;
    }
}

2. Преобразование в ОПЗ (Shunting-yard algorithm):

import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.HashMap;

public class ShuntingYard {

    // Карта для хранения информации об операторах (приоритет, левоассоциативность)
    private static final Map<String, Integer> OPERATOR_PRECEDENCE = new HashMap<>();
    private static final Map<String, Boolean> OPERATOR_LEFT_ASSOCIATIVE = new HashMap<>();

    static {
        OPERATOR_PRECEDENCE.put("+", 1);
        OPERATOR_PRECEDENCE.put("-", 1);
        OPERATOR_PRECEDENCE.put("*", 2);
        OPERATOR_PRECEDENCE.put("/", 2);
        OPERATOR_PRECEDENCE.put("^", 3); // Возведение в степень
        // Функции имеют высокий приоритет, например, 4
        OPERATOR_PRECEDENCE.put("sqrt", 4);
        OPERATOR_PRECEDENCE.put("sin", 4);

        OPERATOR_LEFT_ASSOCIATIVE.put("+", true);
        OPERATOR_LEFT_ASSOCIATIVE.put("-", true);
        OPERATOR_LEFT_ASSOCIATIVE.put("*", true);
        OPERATOR_LEFT_ASSOCIATIVE.put("/", true);
        OPERATOR_LEFT_ASSOCIATIVE.put("^", false); // Правоассоциативная
    }

    private boolean isOperator(String token) {
        return OPERATOR_PRECEDENCE.containsKey(token);
    }

    private boolean isFunction(String token) {
        return token.matches("[a-zA-Z]+"); // Простая проверка на функции
    }

    private int getPrecedence(String operator) {
        return OPERATOR_PRECEDENCE.getOrDefault(operator, -1);
    }

    private boolean isLeftAssociative(String operator) {
        return OPERATOR_LEFT_ASSOCIATIVE.getOrDefault(operator, false);
    }

    public List<String> infixToPostfix(List<String> tokens) {
        List<String> outputQueue = new LinkedList<>();
        Deque<String> operatorStack = new LinkedList<>();

        for (String token : tokens) {
            if (token.matches("-?\\d+(\\.\\d+)?")) { // Число
                outputQueue.add(token);
            } else if (isFunction(token)) { // Функция
                operatorStack.push(token);
            } else if (isOperator(token)) { // Оператор
                while (!operatorStack.isEmpty() && isOperator(operatorStack.peek())) {
                    String opOnStack = operatorStack.peek();
                    if ((isLeftAssociative(token) && getPrecedence(token) <= getPrecedence(opOnStack)) ||
                        (!isLeftAssociative(token) && getPrecedence(token) < getPrecedence(opOnStack))) {
                        outputQueue.add(operatorStack.pop());
                    } else {
                        break;
                    }
                }
                operatorStack.push(token);
            } else if (token.equals("(")) {
                operatorStack.push(token);
            } else if (token.equals(")")) {
                while (!operatorStack.isEmpty() && !operatorStack.peek().equals("(")) {
                    outputQueue.add(operatorStack.pop());
                }
                if (operatorStack.isEmpty()) {
                    throw new IllegalArgumentException("Mismatched parentheses.");
                }
                operatorStack.pop(); // Удаляем открывающую скобку
                if (!operatorStack.isEmpty() && isFunction(operatorStack.peek())) {
                    outputQueue.add(operatorStack.pop()); // Добавляем функцию, если есть
                }
            } else {
                throw new IllegalArgumentException("Unknown token: " + token);
            }
        }

        while (!operatorStack.isEmpty()) {
            String op = operatorStack.pop();
            if (op.equals("(") || op.equals(")")) {
                throw new IllegalArgumentException("Mismatched parentheses.");
            }
            outputQueue.add(op);
        }
        return outputQueue;
    }
}

3. Вычислитель ОПЗ:

import java.util.Deque;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.HashMap;

public class RPNCalculator {

    private final Map<String, IBinaryOperation> binaryOperations = new HashMap<>();
    private final Map<String, IUnaryOperation> unaryOperations = new HashMap<>();

    public RPNCalculator() {
        // Инициализация бинарных операций
        binaryOperations.put("+", new AddOperation());
        binaryOperations.put("-", new SubtractOperation());
        binaryOperations.put("*", new MultiplyOperation());
        binaryOperations.put("/", new DivideOperation());
        binaryOperations.put("^", new PowerOperation()); // Предполагаем наличие класса PowerOperation
        // Инициализация унарных операций
        unaryOperations.put("sqrt", new SqrtOperation());
        unaryOperations.put("sin", new SinOperation()); // Предполагаем наличие класса SinOperation
    }

    public double evaluate(List<String> postfixTokens) {
        Deque<Double> operandStack = new LinkedList<>();

        for (String token : postfixTokens) {
            if (token.matches("-?\\d+(\\.\\d+)?")) { // Число
                operandStack.push(Double.parseDouble(token));
            } else if (binaryOperations.containsKey(token)) { // Бинарный оператор
                if (operandStack.size() < 2) {
                    throw new IllegalArgumentException("Not enough operands for operator: " + token);
                }
                double operand2 = operandStack.pop();
                double operand1 = operandStack.pop();
                IBinaryOperation operation = binaryOperations.get(token);
                operandStack.push(operation.execute(operand1, operand2));
            } else if (unaryOperations.containsKey(token)) { // Унарный оператор (функция)
                if (operandStack.size() < 1) {
                    throw new IllegalArgumentException("Not enough operands for function: " + token);
                }
                double operand = operandStack.pop();
                IUnaryOperation operation = unaryOperations.get(token);
                operandStack.push(operation.execute(operand));
            } else {
                throw new IllegalArgumentException("Unknown token in RPN: " + token);
            }
        }

        if (operandStack.size() != 1) {
            throw new IllegalArgumentException("Invalid expression: Stack size is not 1 after evaluation.");
        }
        return operandStack.pop();
    }
}

Эти примеры демонстрируют, как стандартные коллекции Java (List, Deque (реализованный через LinkedList)) и принципы ООП (интерфейсы IOperation, IBinaryOperation, IUnaryOperation и их конкретные реализации) используются для построения строкового калькулятора.

Тестирование приложения «Калькулятор»

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

Важность тестирования в процессе разработки ПО.

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

  • Обеспечение качества: Тестирование помогает убедиться, что приложение работает корректно, выполняет свои функции согласно спецификациям и соответствует ожиданиям пользователей.
  • Снижение рисков: Раннее обнаружение дефектов обходится значительно дешевле, чем их исправление на более поздних стадиях разработки или после развертывания.
  • Повышение надежности и стабильности: Систематическое тестирование гарантирует, что различные части приложения взаимодействуют правильно, и система ведет себя предсказуемо даже в нештатных ситуациях.
  • Уверенность в изменениях: Наличие хорошего набора тестов позволяет разработчикам вносить изменения и добавлять новый функционал с уверенностью, что они не сломают уже работающие части кода (регрессионное тестирование).
  • Улучшение архитектуры и дизайна: Процесс написания тестов часто выявляет недостатки в дизайне кода, подталкивая к созданию более модульных и тестируемых компонентов.

Юнит-тестирование с использованием JUnit:

Принципы модульного тестирования.

Юнит-тестирование, или модульное тестирование, проверяет наименьшие, изолированные части логики приложения — так называемые «юниты» или «модули». Обычно это отдельные классы или методы. Цель юнит-теста — убедиться, что каждый модуль в отдельности работает правильно, в изоляции от внешних зависимостей.

Ключевые характеристики хорошего юнит-теста (принципы F.I.R.S.T.):

  • Fast (Быстрый): Тесты должны выполняться быстро, чтобы их можно было запускать часто.
  • Isolated (Изолированный): Каждый тест должен быть независим от других.
  • Repeatable (Повторяемый): Каждый запуск теста должен давать один и тот же результат.
  • Self-validating (Самопроверяющийся): Тест должен автоматически сообщать о прохождении или провале.
  • Timely (Своевременный): Тесты пишутся до или одновременно с кодом.

Использование аннотаций (@Test, @BeforeEach, @AfterEach) и методов Assert.

JUnit является де-факто стандартом для юнит-тестирования в Java. Он предоставляет мощный набор аннотаций и методов для создания и выполнения тестов.

  • @Test: Помечает метод как тестовый. JUnit обнаруживает и запускает такие методы.
  • @BeforeEach: Метод, помеченный этой аннотацией, выполняется перед каждым тестовым методом в классе. Используется для настройки тестовой среды (например, инициализации объектов, необходимых для каждого теста).
  • @AfterEach: Метод, помеченный этой аннотацией, выполняется после каждого тестового метода. Используется для очистки ресурсов или сброса состояния после теста.
  • Assert (класс org.junit.jupiter.api.Assertions): Содержит статические методы для проверки ожидаемых результатов.
    • assertEquals(expected, actual): Проверяет, равны ли ожидаемое и фактическое значения.
    • assertTrue(condition): Проверяет, является ли условие истинным.
    • assertFalse(condition): Проверяет, является ли условие ложным.
    • assertThrows(expectedType, executable): Проверяет, что при выполнении executable выбрасывается исключение указанного типа.

Примеры юнит-тестов для логики CalculatorEngine и ExpressionParser.

Пример для CalculatorEngine:

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

public class CalculatorEngineTest {
    private CalculatorEngine engine;

    @BeforeEach
    void setUp() {
        engine = new CalculatorEngine();
    }

    @Test
    void testInitialState() {
        assertEquals(0.0, engine.getCurrentResult());
        assertTrue(engine.getCurrentExpression().isEmpty());
    }

    @Test
    void testAddition() {
        engine.appendDigit("2");
        engine.performOperation("+");
        engine.appendDigit("3");
        engine.calculateEquals();
        assertEquals(5.0, engine.getCurrentResult(), 0.001); // 0.001 - дельта для double
        assertEquals("2.0 + 3.0", engine.getCurrentExpression()); // Гипотетическое выражение
    }

    @Test
    void testDivisionByZero() {
        engine.appendDigit("10");
        engine.performOperation("/");
        engine.appendDigit("0");
        engine.calculateEquals();
        assertTrue(Double.isNaN(engine.getCurrentResult())); // Ожидаем NaN при делении на ноль
    }

    @Test
    void testClearFunction() {
        engine.appendDigit("123");
        engine.performOperation("+");
        engine.appendDigit("456");
        engine.clear();
        assertEquals(0.0, engine.getCurrentResult());
    }
}

Пример для ExpressionParser (фрагмент):

import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;

public class ShuntingYardTest {

    private ShuntingYard shuntingYard = new ShuntingYard();
    private Tokenizer tokenizer = new Tokenizer(); // Предполагаем наличие Tokenizer

    @Test
    void testSimpleAddition() {
        List<String> infixTokens = tokenizer.tokenize("2 + 3");
        List<String> postfixTokens = shuntingYard.infixToPostfix(infixTokens);
        assertEquals(Arrays.asList("2", "3", "+"), postfixTokens);
    }

    @Test
    void testMultiplicationPrecedence() {
        List<String> infixTokens = tokenizer.tokenize("2 + 3 * 4");
        List<String> postfixTokens = shuntingYard.infixToPostfix(infixTokens);
        assertEquals(Arrays.asList("2", "3", "4", "*", "+"), postfixTokens);
    }

    @Test
    void testParentheses() {
        List<String> infixTokens = tokenizer.tokenize("(2 + 3) * 4");
        List<String> postfixTokens = shuntingYard.infixToPostfix(infixTokens);
        assertEquals(Arrays.asList("2", "3", "+", "4", "*"), postfixTokens);
    }

    @Test
    void testMismatchedParentheses() {
        List<String> infixTokens = tokenizer.tokenize("((2 + 3) * 4");
        assertThrows(IllegalArgumentException.class, () -> shuntingYard.infixToPostfix(infixTokens));
    }
}

Методология Test-Driven Development (TDD): написание тестов до кода.

TDD — это методология разработки, при которой тесты пишутся до написания самого рабочего кода. Цикл TDD состоит из трех шагов:

  1. Красный (Red): Написать тест для новой функциональности. Этот тест должен быть «красным» (проваливаться), потому что функциональности еще нет.
  2. Зеленый (Green): Написать минимальный объем рабочего кода, который заставит тест быть «зеленым» (пройти).
  3. Рефакторинг (Refactor): Улучшить написанный код (структуру, читаемость, производительность), сохраняя тесты «зелеными».

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

Интеграционное тестирование: проверка взаимодействия компонентов.

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

Использование фреймворка Mockito для создания моков и спаев:

  • Изоляция зависимостей для эффективного юнит-тестирования.

При юнит-тестировании часто возникает проблема зависимостей. Если класс A зависит от класса B, то для тестирования A нам потребуется работающий экземпляр B. Это нарушает принцип изоляции юнит-тестов и может усложнить их написание. Mockito — это популярный фреймворк для создания «моков» (фиктивных объектов) и «спаев» (частично мокированных реальных объектов), который позволяет эмулировать поведение зависимостей.

  • Моки (Mocks): Полностью имитируют поведение реального объекта. Вы можете определить, какие методы должны быть вызваны и какие значения они должны возвращать. Моки полезны, когда вам нужно контролировать поведение зависимости и проверять, были ли вызваны определенные методы.
  • Спаи (Spies): Частично мокированные объекты. Они используют реальную реализацию методов по умолчанию, но позволяют переопределить поведение некоторых методов и верифицировать вызовы.

Mockito тесно интегрируется с JUnit с помощью аннотаций, таких как @ExtendWith(MockitoExtension.class) (для JUnit 5) и @Mock или @Spy.

Пример использования Mockito:

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

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.assertEquals;

@ExtendWith(MockitoExtension.class)
public class UIControllerTest {

    @Mock
    private CalculatorEngine mockEngine; // Создаем мок-объект CalculatorEngine

    // UIController, который будет тестироваться, использует мок-движок
    private UIController controller; // Предположим, что UIController принимает CalculatorEngine в конструкторе

    @BeforeEach
    void setUp() {
        // Инициализация контроллера с мок-движком
        controller = new UIController(mockEngine); // Или используем @InjectMocks
        // Допустим, у контроллера есть TextField display;
        // controller.display = new TextField(); // Инициализация GUI-компонента для теста
    }

    @Test
    void testHandleDigitUpdatesDisplayCorrectly() {
        // Настраиваем поведение мок-объекта: когда вызывается appendDigit("5"), он возвращает "5"
        when(mockEngine.appendDigit("5")).thenReturn("5");

        // Имитируем нажатие кнопки "5"
        Button digitButton = new Button("5");
        controller.handleDigit(new ActionEvent(digitButton, null)); // Предполагаем метод handleDigit

        // Проверяем, что метод appendDigit был вызван на мок-объекте
        verify(mockEngine, times(1)).appendDigit("5");
        // Проверяем, что дисплей контроллера обновился правильно
        // assertEquals("5", controller.display.getText()); // Если display доступен
    }

    @Test
    void testHandleClearCallsEngineClear() {
        // Имитируем нажатие кнопки "C"
        controller.handleClear(new ActionEvent());

        // Проверяем, что метод clear() был вызван на мок-объекте CalculatorEngine
        verify(mockEngine, times(1)).clear();
    }
}

В этом примере мы тестируем UIController, изолируя его от реальной реализации CalculatorEngine. Это позволяет сосредоточиться на логике UIController и убедиться, что он правильно взаимодействует со своими зависимостями.

Экономическая эффективность автоматизированного тестирования.

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

  • Сокращение времени и ресурсов: Автоматизированные тесты выполняются значительно быстрее, чем ручные. Робот в 99% случаев проходит тест быстрее ручного тестировщика. Это позволяет выполнять тесты круглосуточно и гораздо чаще. В практических примерах автоматизация позволила сократить затраты времени специалистов с 30 до 14 часов для 700 тест-кейсов, прогоняемых до 100 раз в год.
  • Повышение точности и надежности: Автоматизация исключает человеческий фактор, снижая вероятность ошибок и пропусков дефектов.
  • Экономия средств: Долгосрочная экономия ресурсов, связанных с ручным тестированием (зарплата тестировщиков, время), приводит к существенной экономии. Ожидаемая экономия времени и других ресурсов за год при внедрении автоматизации тестирования составляет в среднем 140-150%, а к концу второго года эффективность может возрасти до 240%.
  • Раннее обнаружение дефектов: Быстрое выполнение автоматических регрессионных тестов позволяет выявлять дефекты на ранних стадиях, когда их исправление обходится дешевле всего.
  • Повышение уверенности разработчиков: Наличие стабильного набора автоматических тестов позволяет разработчикам более смело вносить изменения и рефакторить код, не опасаясь непреднамеренных побочных эффектов.

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

Масштабирование, оптимизация и перспективы развития

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

Оптимизация производительности Java-приложений:

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

  • Сравнение String, StringBuilder, StringBuffer и их применение.
    Как уже упоминалось, String в Java является неизменяемым. Каждая операция конкатенации (+) создает новый объект String, что при большом количестве операций (например, в цикле при построении сложного выражения) приводит к значительному расходу памяти и замедлению.
    • StringBuilder: Предназначен для работы с изменяемыми строками в однопоточной среде. Он не синхронизирован, что делает его значительно быстрее, чем StringBuffer и конкатенация String для непараллельных операций. Используйте StringBuilder везде, где нет необходимости в потокобезопасности, особенно при построении длинных строк или частом изменении строкового содержимого.
    • StringBuffer: Аналогичен StringBuilder, но является потокобезопасным (методы синхронизированы). Это делает его медленнее, но безопаснее для использования в многопоточных приложениях. В контексте однопоточного калькулятора, StringBuilder является предпочтительным выбором для производительности.
    • Для нашего строкового калькулятора, при формировании ОПЗ-строки или отображении выражений, StringBuilder будет оптимальным выбором.
  • Эффективное использование коллекций (Stack, ArrayList, Deque).
    Выбор правильной коллекции для конкретной задачи критически важен для производительности.
    • Stack: Устаревший класс, наследующийся от Vector (который синхронизирован). Для реализации стека в алгоритмах парсинга (например, в алгоритме «сортировочная станция» или для вычисления ОПЗ) гораздо эффективнее использовать Deque (Double Ended Queue), который может быть реализован с помощью ArrayDeque или LinkedList. ArrayDeque обеспечивает почти константное время для операций push, pop, peek.
    • ArrayList: Хорош для хранения элементов, к которым требуется быстрый доступ по индексу.
    • LinkedList: Более эффективен для частых операций вставки/удаления в середине списка, но медленнее для доступа по индексу.
  • Сокращение операций вывода, кэширование объектов, минимизация синхронизации.
    • Сокращение операций вывода: Чрезмерный вывод в консоль или в логи может замедлять приложение. Используйте логирование разумно и настраивайте уровни логирования для продакшн-среды.
    • Кэширование часто используемых объектов: Если одни и те же объекты создаются и используются многократно, рассмотрите возможность их кэширования. Например, объекты Operation могут быть созданы один раз и переиспользованы.
    • Минимизация синхронизации: Избегайте ненужной синхронизации (использование synchronized блоков или потокобезопасных коллекций типа Vector, Hashtable), так как она создает накладные расходы и может стать узким местом в производительности. Для многопоточных сценариев используйте классы из java.util.concurrent (например, ConcurrentHashMap, ReadWriteLock), которые оптимизированы для параллельного доступа.
  • Настройка сборщика мусора (GC) и влияние версии Java.
    • Настройка GC: Правильный выбор и настройка сборщика мусора (Garbage Collector) может значительно уменьшить «фризы» и повысить отзывчивость приложения, особенно для больших или высоконагруженных систем. В Java доступны различные GC, такие как G1GC (по умолчанию с Java 9), ZGC, Shenandoah, каждый из которых имеет свои преимущества для определенных сценариев.
    • Влияние версии Java: Регулярное обновление версии Java (например, до актуальной Java 25 LTS) способствует оптимизации производительности, поскольку новые версии часто содержат улучшения JVM, новые функции и оптимизации стандартных библиотек.

Горизонтальное масштабирование: концепции контейнеризации (Docker, Kubernetes) и облачных технологий.

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

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

  • Контейнеризация (Docker): Docker позволяет упаковать приложение и все его зависимости в легковесный, портативный «контейнер». Это обеспечивает единообразную среду выполнения на разных машинах, исключая проблемы «работает у меня на машине».
  • Оркестрация контейнеров (Kubernetes): Kubernetes — это платформа для автоматизации развертывания, масштабирования и управления контейнерными приложениями. Она позволяет запускать множество экземпляров вашего Java-приложения в кластере серверов, автоматически распределяя нагрузку и обеспечивая отказоустойчивость. В контейнерных средах, таких как Kubernetes, важно ограничивать потребление CPU и RAM для оптимизации ресурсов, предотвращая «голодание» других контейнеров.
  • Облачные технологии (AWS, Google Cloud Platform, Microsoft Azure): Эти платформы предоставляют инфраструктуру и сервисы для развертывания и масштабирования Java-приложений в облаке. Они тесно интегрируются с Docker и Kubernetes, предлагая готовые решения для автомасштабирования, балансировки нагрузки и управления базами данных, что делает развертывание высокомасштабируемых Java-сервисов гораздо проще.

Предложения по расширению функционала калькулятора:

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

  • Поддержка тригонометрических, логарифмических функций (с использованием java.lang.Math).
    Статический класс java.lang.Math в Java предоставляет богатый набор математических функций. Для расширения строкового калькулятора можно добавить:
    • Тригонометрические функции: Math.sin(), Math.cos(), Math.tan(), Math.toRadians(), Math.toDegrees().
    • Логарифмические функции: Math.log() (натуральный логарифм), Math.log10() (десятичный логарифм).
    • Степенные функции: Math.pow(), Math.exp().
    • Для этого потребуется расширить ExpressionParser для распознавания этих функций как токенов и добавить соответствующие реализации в RPNCalculator (например, через классы, реализующие IUnaryOperation).
  • Работа с переменными и функциями памяти.
    • Переменные: Возможность присваивать значения переменным (например, x = 10, y = x + 5) и использовать их в выражениях. Это потребует хранения карты переменных (Map<String, Double>) в CalculatorEngine и соответствующей логики в ExpressionParser и RPNCalculator.
    • Функции памяти (M+, M-, MR, MC): Реализация стандартных кнопок памяти, позволяющих сохранять, добавлять, вычитать и извлекать значения из памяти. Это будет реализовано в CalculatorEngine с использованием дополнительного поля memoryValue.
  • История вычислений, сохранение/загрузка через REST-сервис.
    • История вычислений: Хранение списка предыдущих выражений и их результатов. Может быть реализовано в CalculatorEngine с использованием List<String>.
    • Сохранение/загрузка через REST-сервис: Для более продвинутого функционала, а также для демонстрации взаимодействия с сетевыми сервисами, можно реализовать сохранение и загрузку истории вычислений на удаленный сервер через REST API. Это потребует создания небольшой веб-службы (например, на Spring Boot) и использования HTTP-клиента в JavaFX-приложении. Это также является шагом к масштабируемости, так как история может быть общей для нескольких устройств или пользователей.
  • Разработка инженерного/научного режима.
    Создание переключаемого режима интерфейса, который предоставляет дополнительные кнопки для расширенных функций (тригонометрия, логарифмы, факториалы, постоянные значения типа Пи, Е). Это потребует переключения FXML-файлов или динамического изменения расположения кнопок в UIController.

Эти предложения демонстрируют не только возможность расширения функционала приложения «Калькулятор», но и позволяют студенту глубже погрузиться в различные аспекты Java-разработки, от математических алгоритмов до сетевого взаимодействия и проектирования пользовательского опыта.

Заключение

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

Обобщение результатов проделанной работы.

В ходе выполнения данной курсовой работы было успешно реализовано следующее:

  • Теоретически обоснован и применен комплекс принципов объектно-ориентированного программирования: инкапсуляция, наследование, полиморфизм и абстракция. Показано, как эти принципы способствуют созданию модульной, гибкой и легко расширяемой архитектуры приложения.
  • Разработана архитектура калькулятора на основе паттерна Model-View-Controller (MVC), что обеспечило четкое разделение ответственности между компонентами.
  • Выбран и аргументирован в пользу JavaFX как современного фреймворка для построения графического пользовательского интерфейса, использующего возможности CSS-стилизации и FXML.
  • Реализован функционал базового калькулятора, включающий стандартные арифметические операции, с корректной обработкой пользовательского ввода.
  • Разработан функционал строкового калькулятора, способного парсить и вычислять сложные математические выражения, используя алгоритм «сортировочная станция» для преобразования в обратную польскую запись (ОПЗ) и вычисления с использованием стека. Внедрена обработка различных ошибок, таких как некорректный синтаксис и деление на ноль.
  • Применены стандартные библиотеки Java для эффективной работы со строками (StringBuilder) и коллекциями (Deque), а также для регулярных выражений (java.util.regex).
  • Внедрена методология тестирования, включающая юнит-тестирование с JUnit, рассмотрение Test-Driven Development (TDD) и использование Mockito для изоляции зависимостей. Подчеркнута экономическая эффективность автоматизированного тестирования.
  • Проанализированы аспекты масштабирования и оптимизации Java-приложений, а также предложены конкретные пути дальнейшего развития функционала калькулятора.

Подтверждение достижения поставленных целей и задач.

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

Выводы о применимости принципов ООП и современных инструментов Java для разработки сложных приложений.

Принципы ООП оказались исключительно эффективными для структурирования логики калькулятора, позволяя создавать четкие, независимые и легко расширяемые компоненты. Инкапсуляция защитила внутреннее состояние, наследование позволило элегантно реализовать различные операции, а полиморфизм обеспечил гибкость при их вызове. Современные инструменты Java, такие как JavaFX, JUnit и Mockito, существенно упростили процесс разработки, создания пользовательского интерфейса и обеспечения качества кода. Это подтверждает, что Java, с ее развивающейся экосистемой и мощными инструментами, остается одним из ведущих языков для создания не только простых, но и сложных, масштабируемых и надежных приложений в самых разнообразных сферах.

Перспективы дальнейших иссле��ований и усовершенствований проекта.

Проект «Калькулятор» может быть значительно расширен и усовершенствован в будущем:

  • Расширение функционала: Добавление поддержки тригонометрических, логарифмических функций, работы с переменными, функций памяти, а также истории вычислений.
  • Научный/инженерный режим: Разработка более сложного пользовательского интерфейса для специализированных вычислений.
  • Интеграция с сервисами: Реализация сохранения и загрузки истории вычислений через REST-сервис для демонстрации взаимодействия с бэкендом и облачными технологиями.
  • Улучшенная обработка ошибок: Внедрение более детальных сообщений об ошибках и механизмов их восстановления.
  • Поддержка пользовательских функций: Возможность определения собственных функций пользователем.
  • Локализация: Поддержка нескольких языков для интерфейса.
  • Улучшения UI/UX: Более продвинутый дизайн, анимации и интерактивность, использующие весь потенциал JavaFX.

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

Список использованной литературы

  1. Герман, О. В., Герман, Ю. О. Программирование на Java и С# для студента. БХВ-Петербург, 2005. 512 с.
  2. Файн, Я. Программирование на Java для детей, родителей, дедушек и бабушек. Electronic Edition, 2011. 231 с.
  3. Хабибуллин, И. Создание распределенных приложений на Java 2. BHV-Санкт-Петербург, 2002. 696 с.
  4. Васильев, А. Н. Java. Объектно-ориентированное программирование. Санкт-Петербург, 2011. 400 с. (Серия «Учебное пособие»).
  5. Java. Википедия. URL: https://ru.wikipedia.org/wiki/Java (дата обращения: 12.10.2025).
  6. ООП в Java: четыре принципа с примерами. Highload.tech, 2024. URL: https://highload.tech/blog/oop-in-java-four-principles-with-examples/ (дата обращения: 12.10.2025).
  7. Принципы ООП. JavaRush, 2024. URL: https://javarush.com/groups/topic/oop-principles (дата обращения: 12.10.2025).
  8. Принципы ООП на примере языка программирования Java. TopJava, 2024. URL: https://topjava.ru/blog/oops-concepts-in-java (дата обращения: 12.10.2025).
  9. Объектно-ориентированное программирование. Metanit.com, 2024. URL: https://metanit.com/java/tutorial/1.1.php (дата обращения: 12.10.2025).
  10. ООП в Java на практике: проектирование и разработка классов. Proglib.io, 2025. URL: https://proglib.io/p/oop-v-java-na-praktike-proektirovanie-i-razrabotka-klassov-2025-03-11 (дата обращения: 12.10.2025).
  11. Why use JavaFX in 2025? TechTarget, 2025. URL: https://www.techtarget.com/whatis/feature/Why-use-JavaFX-in-2025 (дата обращения: 12.10.2025).
  12. Реализация алгоритма сортировочной станции на Java. Habr, 2009. URL: https://habr.com/ru/articles/139395/ (дата обращения: 12.10.2025).
  13. Консольный калькулятор Java. Vertex Academy, 2024. URL: https://vertex-academy.com/tutorials/java/java-console-calculator (дата обращения: 12.10.2025).
  14. Калькулятор на Java с базой лишь в 10 лвл JavaRush (3Kyu задача на CodeWars). JavaRush, 2024. URL: https://javarush.com/quests/lectures/questsyntax.level01.lecture08 (дата обращения: 12.10.2025).
  15. Извлечение чисел из строки в Java: использование regex. Sky.pro, 2024. URL: https://sky.pro/media/izvlechenie-chisel-iz-stroki-v-java-ispolzovanie-regex/ (дата обращения: 12.10.2025).
  16. Регулярные выражения в Java (RegEx). JavaRush, 2024. URL: https://javarush.com/groups/topic/194 (дата обращения: 12.10.2025).
  17. Регулярные выражения — Java. Metanit.com, 2024. URL: https://metanit.com/java/tutorial/7.1.php (дата обращения: 12.10.2025).
  18. Регулярные выражения в Java: спецификации языка, примеры, задачи. TProger.ru, 2024. URL: https://tproger.ru/articles/reguljarnye-vyrazhenija-v-java/ (дата обращения: 12.10.2025).
  19. Оптимизация программ на Java. IBM, 2024. URL: https://www.ibm.com/docs/ru/aix/7.2?topic=applications-optimizing-java-programs (дата обращения: 12.10.2025).
  20. 10 техник оптимизации Java, которые выведут вас на новый уровень. Habr, 2024. URL: https://habr.com/ru/companies/otus/articles/913922/ (дата обращения: 12.10.2025).
  21. Java.lang.Math Class. Oracle, 2014. URL: https://docs.oracle.com/javase/8/docs/api/java/lang/Math.html (дата обращения: 12.10.2025).
  22. Тестирование. Java Роадмап Сергея Жукова. URL: https://zhukovsd.github.io/java-backend-learning-course/testing/ (дата обращения: 12.10.2025).
  23. Java Unit Testing: методики, понятия, практика. JavaRush, 2024. URL: https://javarush.com/groups/topic/264 (дата обращения: 12.10.2025).
  24. JUnit: модульное тестирование в Java и test-driven development. Skillbox.ru, 2024. URL: https://skillbox.ru/media/code/junit-osnovy-testirovaniya-v-java/ (дата обращения: 12.10.2025).
  25. Популярные библиотеки для Unit и Integration тестирования в Java. Mate Academy, 2024. URL: https://mate.academy/ru/blog/popular-libraries-for-unit-and-integration-testing-in-java (дата обращения: 12.10.2025).
  26. JUnit Calculator Test Case Example. TestingDocs, 2024. URL: https://www.testingdocs.com/junit-calculator-test-case-example/ (дата обращения: 12.10.2025).
  27. Тестирование в Java: лучшие практики, инструменты и советы. Proglib.io, 2024. URL: https://proglib.io/p/testing-v-java-luchshie-praktiki-instrumenty-i-sovety-2024-05-02 (дата обращения: 12.10.2025).
  28. Интеграционные тесты. Spring Boot. Hexlet.io, 2024. URL: https://ru.hexlet.io/courses/spring-boot/lessons/integration-tests/theory_unit (дата обращения: 12.10.2025).
  29. Mockito — что это за фреймворк для Java, примеры использования. SkillFactory, 2024. URL: https://skillfactory.ru/media/mockito-chto-eto-za-freymvork-dlya-java-primery-ispolzovaniya/ (дата обращения: 12.10.2025).
  30. Mockito и как им пользоваться. Habr, 2024. URL: https://habr.com/ru/companies/otus/articles/781074/ (дата обращения: 12.10.2025).
  31. Mockito в тестировании Java: как создавать моки и писать надежные тесты. Nuancesprog.ru, 2024. URL: https://nuancesprog.ru/p/16287/ (дата обращения: 12.10.2025).
  32. Автоматизированное тестирование на Java: что нужно знать? Kata Academy, 2024. URL: https://kata.academy/ru/blog/automated-testing-in-java (дата обращения: 12.10.2025).
  33. Оптимизация производительности Java: советы и методы. AppMaster, 2024. URL: https://appmaster.io/ru/blog/optimizatsiya-proizvoditelnosti-java-sovety-i-metody (дата обращения: 12.10.2025).
  34. 6 рекомендаций по устранению типичных проблем производительности Java. Medium, 2024. URL: https://medium.com/nuances-of-programming/6-%D1%80%D0%B5%D0%BA%D0%BE%D0%BC%D0%B5%D0%BD%D0%B4%D0%B0%D1%86%D0%B8%D0%B9-%D0%BF%D0%BE-%D1%83%D1%81%D1%82%D1%80%D0%B0%D0%BD%D0%B5%D0%BD%D0%B8%D1%8E-%D1%82%D0%B8%D0%BF%D0%B8%D1%87%D0%BD%D1%8B%D1%85-%D0%BF%D1%80%D0%BE%D0%B1%D0%BB%D0%B5%D0%BC-%D0%BF%D1%80%D0%BE%D0%B8%D0%B7%D0%B2%D0%BE%D0%B4%D0%B8%D1%82%D0%B5%D0%BB%D1%8C%D0%BD%D0%BE%D1%81%D1%82%D0%B8-java-566b61882c23 (дата обращения: 12.10.2025).
  35. 7 советов, как оптимизировать производительность Java в Kubernetes. Axiom.icu, 2024. URL: https://axiom.icu/ru/java-performance-kubernetes-optimization (дата обращения: 12.10.2025).
  36. Почему на Java специалистов всегда будет спрос. Dan-it.com.ua, 2024. URL: https://dan-it.com.ua/blog/pochemu-na-java-specialistov-vsegda-budet-spros/ (дата обращения: 12.10.2025).
  37. Калькулятор с использованием JavaFX, считает базовые операции, показывает историю операций, выгружает ее на сервер и обратно через REST-сервис. GitHub, 2024. URL: https://github.com/Sagot86/Calculator (дата обращения: 12.10.2025).
  38. Преимущества Java. IBM, 2024. URL: https://www.ibm.com/docs/ru/sdk-java/8/compilers/opt.html (дата обращения: 12.10.2025).
  39. Преимущества языка программирования Java. ProductStar, 2024. URL: https://productstar.ru/java (дата обращения: 12.10.2025).
  40. Java-приложения: плюсы и минусы языка. GeekBrains, 2024. URL: https://geekbrains.ru/posts/java_application (дата обращения: 12.10.2025).
  41. Java — что это за язык программирования: преимущества и применение. SkillFactory, 2024. URL: https://skillfactory.ru/media/java-chto-eto-za-yazyk-programmirovaniya-preimushchestva-i-primenenie/ (дата обращения: 12.10.2025).
  42. Почему язык Java так популярен в коммерческой разработке ПО? Redlab.dev, 2024. URL: https://redlab.dev/articles/pochemu-yazyk-java-tak-populyaren-v-kommercheskoy-razrabotke-po (дата обращения: 12.10.2025).
  43. Почему стоит учить Java в 2024 году. Kata Academy, 2024. URL: https://kata.academy/ru/blog/why-learn-java-in-2024 (дата обращения: 12.10.2025).
  44. Актуально ли сейчас учить Java? SkillFactory, 2024. URL: https://skillfactory.ru/media/aktualno-li-seychas-uchit-java/ (дата обращения: 12.10.2025).
  45. Java: перспективы, тренды и почему новичкам стоит его знать. Habr, 2023. URL: https://habr.com/ru/companies/lanit/articles/741630/ (дата обращения: 12.10.2025).
  46. JVM (Java Virtual Machine) — что это такое, преимущества, основные компоненты и аналоги. Gitverse.ru, 2024. URL: https://gitverse.ru/blog/java-virtual-machine (дата обращения: 12.10.2025).

Похожие записи