Парсинг математических выражений в Java: 5 проверенных методов
Для кого эта статья:
- Java-разработчики, стремящиеся улучшить свои навыки в парсинге математических выражений
- Студенты курсов по программированию и алгоритмическому мышлению
Технические специалисты, интересующиеся решением практических задач в приложениях с математическими вычислениями
Парсинг и вычисление математических выражений из строки — это задача, с которой рано или поздно сталкивается каждый Java-разработчик. Представьте: пользователь вводит формулу "2+2*2", и ваше приложение должно вернуть правильный ответ 6, а не наивные 8. Правила приоритета операций, работа со скобками, функциями, переменными — всё это превращает простую на первый взгляд задачу в серьёзный технический вызов. В этой статье мы рассмотрим пять проверенных методов, которые помогут вам элегантно решить эту проблему и сэкономят десятки часов разработки. 🧮
Задача парсинга математических выражений — отличный практический кейс для освоения Java и алгоритмического мышления. На Курсе Java-разработки от Skypro мы помогаем студентам не только изучить теорию, но и решать такие реальные задачи. Наши выпускники уверенно реализуют сложные алгоритмы, включая калькуляторы с поддержкой научных формул. А главное — они понимают, когда писать алгоритм с нуля, а когда эффективнее использовать готовые библиотеки.
Проблематика и задачи парсинга математических выражений в Java
При разработке приложений часто возникает необходимость вычислить значение математического выражения, введенного пользователем в виде строки. На первый взгляд задача кажется тривиальной, но при более глубоком анализе выявляется целый ряд сложностей.
Ключевые проблемы при парсинге математических выражений включают:
- Обработка операторов с разными приоритетами (умножение перед сложением и т.д.)
- Корректная обработка скобок для изменения порядка операций
- Работа с функциями (sin, cos, log и т.д.)
- Поддержка переменных и констант (π, e)
- Обработка ошибок и некорректного синтаксиса
- Производительность при вычислении сложных выражений
Существует несколько подходов к решению этой задачи — от использования встроенных возможностей Java до специализированных библиотек и реализации собственных алгоритмов.
| Подход | Преимущества | Недостатки |
|---|---|---|
| Встроенные инструменты Java | Не требует внешних зависимостей | Ограниченная функциональность |
| Специализированные библиотеки | Богатый функционал, оптимизированная производительность | Дополнительные зависимости в проекте |
| Собственный парсер | Полный контроль над логикой и функциональностью | Значительные затраты времени на разработку |
Алексей Петров, Lead Java Developer Однажды наша команда разрабатывала финансовое приложение, где пользователям требовалось вводить формулы для расчёта сложных процентов и амортизации. Изначально мы пошли самым очевидным путём — написали свой парсер на основе рекурсивного спуска. Казалось, что это несложно, но после трёх дней отладки и борьбы с краевыми случаями я понял, что мы изобретаем велосипед. Нам нужно было поддерживать десятки функций, работать с переменными и при этом обеспечивать высокую производительность. После исследования мы выбрали exp4j, и это сэкономило нам недели разработки. Основной урок, который я извлёк — выбор инструмента должен соответствовать сложности задачи. Если вам нужно просто вычислять "2+2", возможно, достаточно ScriptEngine, но для сложных финансовых расчётов стоит использовать специализированные решения.

Метод 1: Использование встроенного JavaScript ScriptEngine
Java предоставляет простой способ вычисления математических выражений через интеграцию с JavaScript с помощью ScriptEngine. Этот подход удобен своей доступностью прямо из коробки, без необходимости подключать сторонние библиотеки.
Вот базовый пример использования ScriptEngine для вычисления выражения:
import javax.script.ScriptEngineManager;
import javax.script.ScriptEngine;
import javax.script.ScriptException;
public class ScriptEngineCalculator {
public static double evaluate(String expression) {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("JavaScript");
try {
Object result = engine.eval(expression);
return Double.parseDouble(result.toString());
} catch (ScriptException e) {
throw new RuntimeException("Ошибка при вычислении выражения: " + expression, e);
}
}
public static void main(String[] args) {
String expression = "2 + 2 * 2";
System.out.println(expression + " = " + evaluate(expression));
expression = "Math.sin(Math.PI/6) + Math.cos(Math.PI/3)";
System.out.println(expression + " = " + evaluate(expression));
}
}
Преимущества использования ScriptEngine:
- Встроено в стандартную библиотеку Java (с Java 6)
- Поддержка всех математических операций JavaScript
- Доступ к стандартным математическим функциям через объект Math
- Простота реализации
Однако этот подход имеет свои ограничения:
- Снижение производительности при частых вычислениях
- Ограничения безопасности при использовании ввода от пользователя
- Отсутствие поддержки пользовательских функций без дополнительного кода
- В Java 15 и выше использование Nashorn JavaScript Engine объявлено устаревшим
Для небольших проектов или прототипов ScriptEngine — отличное решение, но для продакшен-систем с высокой нагрузкой лучше рассмотреть специализированные библиотеки. 🚀
Метод 2: Библиотека exp4j для эффективной обработки формул
Exp4j — это легковесная и высокопроизводительная библиотека для парсинга и оценки математических выражений в Java. Она специально разработана для эффективной работы с формулами и поддерживает широкий спектр операций.
Для использования exp4j необходимо добавить зависимость в ваш проект:
// Maven
<dependency>
<groupId>net.objecthunter</groupId>
<artifactId>exp4j</artifactId>
<version>0.4.8</version>
</dependency>
// Gradle
implementation 'net.objecthunter:exp4j:0.4.8'
Базовый пример использования exp4j:
import net.objecthunter.exp4j.ExpressionBuilder;
public class Exp4jExample {
public static double evaluate(String expression) {
return new ExpressionBuilder(expression)
.build()
.evaluate();
}
public static void main(String[] args) {
String expression = "3 * (2 + 4) / 6";
System.out.println(expression + " = " + evaluate(expression));
// С использованием функций
expression = "sin(π/4) + cos(π/3)";
System.out.println(expression + " = " +
new ExpressionBuilder(expression)
.variables("π")
.build()
.setVariable("π", Math.PI)
.evaluate());
}
}
Exp4j предоставляет множество продвинутых возможностей:
- Поддержка пользовательских функций и операторов
- Работа с переменными
- Оптимизация для многократного выполнения одного и того же выражения
- Проверка синтаксиса выражений
- Высокая производительность благодаря использованию обратной польской нотации
Пример использования пользовательских функций и переменных:
import net.objecthunter.exp4j.Expression;
import net.objecthunter.exp4j.ExpressionBuilder;
import net.objecthunter.exp4j.function.Function;
public class Exp4jAdvanced {
public static void main(String[] args) {
// Создаем пользовательскую функцию
Function factorial = new Function("factorial", 1) {
@Override
public double apply(double... args) {
double n = args[0];
if (n == 0 || n == 1) return 1;
double result = 1;
for (int i = 2; i <= n; i++) {
result *= i;
}
return result;
}
};
// Используем функцию в выражении
Expression expression = new ExpressionBuilder("factorial(n) / (factorial(k) * factorial(n-k))")
.variables("n", "k")
.functions(factorial)
.build()
.setVariable("n", 7)
.setVariable("k", 3);
double result = expression.evaluate();
System.out.println("C(7,3) = " + result); // Вычисление биномиального коэффициента
}
}
Мария Соколова, Java Backend Engineer Работая над проектом научных вычислений для исследовательского института, я столкнулась с необходимостью интегрировать калькулятор, способный обрабатывать сложные физические формулы с десятками переменных. Изначально использовала ScriptEngine, но быстро наткнулась на проблемы с производительностью — некоторые расчеты выполнялись десятки раз в секунду. После перехода на exp4j время обработки сократилось в 8 раз! Но самым большим преимуществом оказалась поддержка пользовательских функций. Например, нам требовались специфические статистические расчеты, и с exp4j мы легко реализовали функцию для вычисления стандартного отклонения одной строкой кода:
stdDev(x1, x2, ..., xn). Библиотека оказалась настолько гибкой, что мы смогли реализовать всю требуемую функциональность без единой модификации основного кода приложения.
Exp4j — это отличное решение для большинства проектов, где требуется вычисление математических выражений. Библиотека обеспечивает хороший баланс между простотой использования, функциональностью и производительностью. 🔥
Метод 3: Реализация алгоритма обратной польской нотации
Если вы предпочитаете полный контроль над парсингом выражений или хотите глубже понять, как работают математические парсеры, реализация алгоритма обратной польской нотации (RPN, или алгоритм Дейкстры) — отличный выбор.
Алгоритм состоит из двух основных этапов:
- Преобразование инфиксной нотации (стандартная запись, например, "2+34") в постфиксную (обратную польскую нотацию, например, "2 3 4 +")
- Вычисление выражения в постфиксной нотации с использованием стека
Вот полная реализация парсера с использованием RPN:
import java.util.*;
public class RPNCalculator {
public static double evaluate(String expression) {
// Шаг 1: Преобразование в RPN
String rpn = toRPN(expression);
// Шаг 2: Вычисление RPN
return evaluateRPN(rpn);
}
private static String toRPN(String expression) {
StringBuilder output = new StringBuilder();
Stack<Character> operators = new Stack<>();
Map<Character, Integer> precedence = new HashMap<>();
precedence.put('+', 1);
precedence.put('-', 1);
precedence.put('*', 2);
precedence.put('/', 2);
precedence.put('^', 3);
for (int i = 0; i < expression.length(); i++) {
char c = expression.charAt(i);
// Пропускаем пробелы
if (Character.isWhitespace(c)) {
continue;
}
// Если символ – число, добавляем его к выходной строке
if (Character.isDigit(c) || c == '.') {
StringBuilder numBuilder = new StringBuilder();
while (i < expression.length() &&
(Character.isDigit(expression.charAt(i)) ||
expression.charAt(i) == '.')) {
numBuilder.append(expression.charAt(i++));
}
i--; // Компенсируем инкремент цикла
output.append(numBuilder).append(" ");
}
// Если символ – открывающая скобка, помещаем ее в стек
else if (c == '(') {
operators.push(c);
}
// Если символ – закрывающая скобка, извлекаем операторы до открывающей скобки
else if (c == ')') {
while (!operators.isEmpty() && operators.peek() != '(') {
output.append(operators.pop()).append(" ");
}
if (!operators.isEmpty() && operators.peek() == '(') {
operators.pop(); // Удаляем открывающую скобку
}
}
// Если символ – оператор
else if (precedence.containsKey(c)) {
while (!operators.isEmpty() && operators.peek() != '(' &&
precedence.getOrDefault(operators.peek(), 0) >= precedence.get(c)) {
output.append(operators.pop()).append(" ");
}
operators.push(c);
}
}
// Извлекаем оставшиеся операторы
while (!operators.isEmpty()) {
output.append(operators.pop()).append(" ");
}
return output.toString().trim();
}
private static double evaluateRPN(String rpn) {
Stack<Double> stack = new Stack<>();
String[] tokens = rpn.split("\\s+");
for (String token : tokens) {
if (token.length() == 1 && "+-*/^".contains(token)) {
double b = stack.pop();
double a = stack.pop();
switch (token.charAt(0)) {
case '+': stack.push(a + b); break;
case '-': stack.push(a – b); break;
case '*': stack.push(a * b); break;
case '/': stack.push(a / b); break;
case '^': stack.push(Math.pow(a, b)); break;
}
} else {
stack.push(Double.parseDouble(token));
}
}
return stack.pop();
}
public static void main(String[] args) {
String expression = "3 + 4 * 2 / ( 1 – 5 ) ^ 2";
System.out.println("Выражение: " + expression);
System.out.println("RPN: " + toRPN(expression));
System.out.println("Результат: " + evaluate(expression));
}
}
Преимущества реализации собственного RPN-парсера:
- Полный контроль над логикой вычислений
- Отсутствие внешних зависимостей
- Возможность кастомизации для специфических требований
- Глубокое понимание алгоритмов парсинга выражений
Недостатки:
- Требуется больше кода и времени на разработку
- Необходимость самостоятельной обработки ошибок
- Сложнее добавлять поддержку функций и переменных
| Стадия | Временная сложность | Пространственная сложность |
|---|---|---|
| Преобразование в RPN | O(n) | O(n) |
| Вычисление RPN | O(n) | O(n) |
| Общая | O(n) | O(n) |
Этот метод особенно полезен для образовательных целей и случаев, когда необходимо иметь полный контроль над процессом парсинга. Если вам интересны алгоритмы, реализация RPN-парсера — отличное упражнение для углубления понимания структур данных и работы с стеками. 📚
Метод 4-5: Альтернативные библиотеки JEP и MXPARSER
Помимо exp4j, существуют и другие мощные библиотеки для парсинга и вычисления математических выражений в Java. Рассмотрим две популярные альтернативы: JEP (Java Expression Parser) и MXPARSER.
Метод 4: JEP (Java Expression Parser)
JEP — один из старейших и проверенных парсеров выражений для Java с богатым набором функций.
Для использования JEP добавьте зависимость:
// Maven
<dependency>
<groupId>org.scijava</groupId>
<artifactId>jep</artifactId>
<version>3.0.0</version>
</dependency>
Пример использования JEP:
import org.nfunk.jep.JEP;
public class JepExample {
public static double evaluate(String expression) {
JEP jep = new JEP();
jep.setImplicitMul(true); // Разрешает неявное умножение (например, 2x вместо 2*x)
jep.addStandardFunctions();
jep.addStandardConstants();
jep.parseExpression(expression);
if (jep.hasError()) {
throw new RuntimeException("Ошибка парсинга: " + jep.getErrorInfo());
}
return jep.getValue();
}
public static void main(String[] args) {
String expression = "sin(PI/4) * cos(PI/3)";
System.out.println(expression + " = " + evaluate(expression));
// С переменными
JEP jep = new JEP();
jep.addVariable("x", 5);
jep.addVariable("y", 3);
jep.parseExpression("x^2 + y^2");
System.out.println("x^2 + y^2 при x=5, y=3: " + jep.getValue());
}
}
Ключевые особенности JEP:
- Поддержка комплексных чисел
- Широкий набор встроенных функций
- Возможность определения пользовательских функций
- Работа с векторами и матрицами
- Хорошая документация и давнее сообщество
Метод 5: MXPARSER
MXPARSER — современная библиотека для математического парсинга с акцентом на удобство использования и расширенную функциональность.
Добавление зависимости MXPARSER:
// Maven
<dependency>
<groupId>org.mariuszgromada.math</groupId>
<artifactId>MathParser.org-mXparser</artifactId>
<version>5.0.7</version>
</dependency>
Пример использования MXPARSER:
import org.mariuszgromada.math.mxparser.*;
public class MXParserExample {
public static double evaluate(String expression) {
Expression e = new Expression(expression);
return e.calculate();
}
public static void main(String[] args) {
String expression = "2 + 3 * sin(pi/2)";
System.out.println(expression + " = " + evaluate(expression));
// Работа с аргументами и функциями
Argument x = new Argument("x", 3);
Argument y = new Argument("y", 4);
Expression e = new Expression("sqrt(x^2 + y^2)", x, y);
System.out.println("Расстояние от начала координат до точки (3,4): " + e.calculate());
// Определение и использование пользовательской функции
Function avgFunc = new Function("avg(a,b,c) = (a+b+c)/3");
Expression avgExpr = new Expression("avg(1,2,3)", avgFunc);
System.out.println("Среднее [1,2,3]: " + avgExpr.calculate());
}
}
Преимущества MXPARSER:
- Чрезвычайно богатый набор математических функций (более 300)
- Интуитивно понятный API
- Поддержка статистических функций
- Итеративные операторы (сумма, произведение)
- Логические операторы и условные выражения
- Нет зависимостей от других библиотек
Сравнение библиотек по ключевым характеристикам:
| Характеристика | exp4j | JEP | MXPARSER |
|---|---|---|---|
| Размер библиотеки | ~30 КБ | ~100 КБ | ~1 МБ |
| Производительность | Очень высокая | Средняя | Высокая |
| Функциональность | Базовая | Расширенная | Очень обширная |
| Поддержка логических выражений | Нет | Базовая | Полная |
| Активная разработка | Умеренная | Низкая | Высокая |
Выбор между JEP, MXPARSER и exp4j зависит от конкретных требований вашего проекта:
- Для максимальной производительности с минимальными зависимостями — exp4j
- Для работы со сложными математическими моделями и матрицами — JEP
- Для наиболее полного набора функций и современного API — MXPARSER
Все три библиотеки предоставляют надежные решения для парсинга и вычисления математических выражений, так что выбор часто сводится к личным предпочтениям и конкретным требованиям проекта. 🛠️
Вычисление математических выражений из строки — фундаментальная задача, для которой Java предлагает несколько решений. От простого использования ScriptEngine до специализированных библиотек и реализации собственного алгоритма — каждый метод имеет свои преимущества. Главный критерий выбора — баланс между сложностью реализации, производительностью и гибкостью. Как только вы научитесь эффективно парсить математические выражения, вы откроете новые горизонты для создания калькуляторов, научных приложений и бизнес-инструментов, способных работать с пользовательскими формулами любой сложности.