← Назад к вопросам

Какой тип данных лучше использовать для хранения сумм денег?

2.2 Middle🔥 201 комментариев
#Основы Java

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Выбор типа данных для хранения денежных сумм в Java

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

Почему НЕ использовать float и double

Проблема: потеря точности из-за двоичной арифметики

public class MoneyPrecisionProblem {
    public static void main(String[] args) {
        // ОПАСНО: использование double
        double price = 0.1;
        double total = 0;
        
        for (int i = 0; i < 10; i++) {
            total += price;
        }
        
        System.out.println("Total: " + total);
        // Output: Total: 0.9999999999999999
        // Ожидали: 1.0
        // ПОТЕРЯ ДЕНЕГ!
    }
}

Почему это происходит:

public class FloatingPointDemo {
    public static void main(String[] args) {
        // Double использует IEEE 754 (двоичное представление)
        double d1 = 0.1;
        double d2 = 0.2;
        double d3 = 0.3;
        
        System.out.println(d1 + d2 == d3);  // false!
        System.out.println(d1 + d2);         // 0.30000000000000004
        System.out.println(d3);              // 0.3
        
        // Это касается и float
        float f1 = 0.1f;
        float f2 = 0.1f + 0.1f + 0.1f;
        System.out.println(f1 == f2);  // false
    }
}

Решение 1: BigDecimal — ПРАВИЛЬНЫЙ ВЫБОР

BigDecimal — это класс для точных вычислений с десятичными числами.

import java.math.BigDecimal;
import java.math.RoundingMode;

public class MoneyCalculationWithBigDecimal {
    public static void main(String[] args) {
        // ПРАВИЛЬНО: использование BigDecimal
        BigDecimal price = new BigDecimal("0.10");
        BigDecimal total = BigDecimal.ZERO;
        
        for (int i = 0; i < 10; i++) {
            total = total.add(price);
        }
        
        System.out.println("Total: " + total);
        // Output: Total: 1.00
        // ПРАВИЛЬНО!
    }
}

Почему BigDecimal работает:

  • Хранит число в виде (мантисса, экспонента)
  • Работает с десятичной арифметикой, а не двоичной
  • Сохраняет точность до определённого количества знаков

Пример: расчёт цены с налогом

import java.math.BigDecimal;
import java.math.RoundingMode;

public class TaxCalculator {
    private static final BigDecimal TAX_RATE = new BigDecimal("0.18");
    private static final int SCALE = 2;  // 2 знака после запятой
    private static final RoundingMode ROUNDING = RoundingMode.HALF_UP;
    
    public static BigDecimal calculateTotalWithTax(BigDecimal basePrice) {
        // Расчёт налога
        BigDecimal tax = basePrice
            .multiply(TAX_RATE)
            .setScale(SCALE, ROUNDING);
        
        // Итоговая сумма
        BigDecimal total = basePrice
            .add(tax)
            .setScale(SCALE, ROUNDING);
        
        return total;
    }
    
    public static void main(String[] args) {
        BigDecimal basePrice = new BigDecimal("100.00");
        
        BigDecimal total = calculateTotalWithTax(basePrice);
        System.out.println("Base price: " + basePrice);
        System.out.println("Total with tax: " + total);
        // Output:
        // Base price: 100.00
        // Total with tax: 118.00
    }
}

Лучшие практики с BigDecimal

1. Создание из строк, НЕ из double

public class BigDecimalCreation {
    public static void main(String[] args) {
        // ПРАВИЛЬНО
        BigDecimal correct1 = new BigDecimal("100.50");
        BigDecimal correct2 = BigDecimal.valueOf(100.50);  // Работает с double
        
        // ОПАСНО
        BigDecimal wrong = new BigDecimal(100.50);  // Пройдёт через double
        
        System.out.println(correct1);  // 100.50
        System.out.println(wrong);      // 100.5000000000000071...
    }
}

2. Управление точностью и округлением

import java.math.BigDecimal;
import java.math.RoundingMode;

public class RoundingExample {
    public static void main(String[] args) {
        BigDecimal amount = new BigDecimal("10.555");
        
        // Различные способы округления
        System.out.println("HALF_UP: " + 
            amount.setScale(2, RoundingMode.HALF_UP));      // 10.56
        
        System.out.println("HALF_DOWN: " + 
            amount.setScale(2, RoundingMode.HALF_DOWN));    // 10.55
        
        System.out.println("CEILING: " + 
            amount.setScale(2, RoundingMode.CEILING));      // 10.56
        
        System.out.println("FLOOR: " + 
            amount.setScale(2, RoundingMode.FLOOR));        // 10.55
        
        System.out.println("UNNECESSARY: " + 
            amount.setScale(2, RoundingMode.UNNECESSARY));  // Exception если нужно округлить
    }
}

HALF_UP — стандартное округление в финансах: 0.5 округляется вверх.

3. Реализация денежного класса

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Objects;

public class Money implements Comparable<Money> {
    private final BigDecimal amount;
    private final String currency;  // "USD", "RUB", и т.д.
    private static final int SCALE = 2;
    private static final RoundingMode ROUNDING = RoundingMode.HALF_UP;
    
    public Money(String amount, String currency) {
        this.amount = new BigDecimal(amount)
            .setScale(SCALE, ROUNDING);
        this.currency = currency;
    }
    
    public Money(BigDecimal amount, String currency) {
        this.amount = amount.setScale(SCALE, ROUNDING);
        this.currency = currency;
    }
    
    public Money add(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException(
                "Cannot add different currencies: " + this.currency + " and " + other.currency
            );
        }
        return new Money(this.amount.add(other.amount), this.currency);
    }
    
    public Money subtract(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException(
                "Cannot subtract different currencies"
            );
        }
        return new Money(this.amount.subtract(other.amount), this.currency);
    }
    
    public Money multiply(BigDecimal factor) {
        return new Money(this.amount.multiply(factor), this.currency);
    }
    
    public Money divide(BigDecimal divisor) {
        return new Money(
            this.amount.divide(divisor, SCALE, ROUNDING),
            this.currency
        );
    }
    
    public boolean isPositive() {
        return this.amount.compareTo(BigDecimal.ZERO) > 0;
    }
    
    public boolean isNegative() {
        return this.amount.compareTo(BigDecimal.ZERO) < 0;
    }
    
    public boolean isZero() {
        return this.amount.compareTo(BigDecimal.ZERO) == 0;
    }
    
    @Override
    public int compareTo(Money other) {
        if (!this.currency.equals(other.currency)) {
            throw new IllegalArgumentException(
                "Cannot compare different currencies"
            );
        }
        return this.amount.compareTo(other.amount);
    }
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Money)) return false;
        Money money = (Money) o;
        return this.amount.equals(money.amount) &&
               this.currency.equals(money.currency);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(amount, currency);
    }
    
    @Override
    public String toString() {
        return amount + " " + currency;
    }
}

// Использование
public class MoneyUsageExample {
    public static void main(String[] args) {
        Money price = new Money("100.00", "USD");
        Money tax = new Money("18.00", "USD");
        
        Money total = price.add(tax);
        System.out.println("Total: " + total);  // 118.00 USD
        
        Money discountedPrice = price.multiply(new BigDecimal("0.90"));
        System.out.println("Discounted: " + discountedPrice);  // 90.00 USD
    }
}

Альтернатива: хранить в центах (long)

Некоторые системы хранят деньги в центах (или минимальной единице валюты) как long:

public class MoneyInCents {
    private long amountInCents;  // $100.50 хранится как 10050
    private String currency;
    
    public MoneyInCents(String amount, String currency) {
        // Преобразуем "100.50" в 10050
        BigDecimal bd = new BigDecimal(amount);
        this.amountInCents = bd.multiply(
            new BigDecimal("100")
        ).longValue();
        this.currency = currency;
    }
    
    public MoneyInCents add(MoneyInCents other) {
        MoneyInCents result = new MoneyInCents(
            BigDecimal.valueOf(this.amountInCents + other.amountInCents)
                .divide(new BigDecimal("100"))
                .toPlainString(),
            this.currency
        );
        return result;
    }
    
    @Override
    public String toString() {
        return String.format("%s %.2f",
            currency,
            amountInCents / 100.0
        );
    }
}

Преимущества:

  • Быстрые вычисления (integer arithmetic)
  • Меньше памяти
  • Нет проблем с точностью

Недостатки:

  • Сложнее в использовании
  • Легко ошибиться при преобразовании
  • Не универсально (зависит от валюты)

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

public class MoneyTypeComparison {
    public static void main(String[] args) {
        // НЕПРАВИЛЬНО: double
        double d = 0.1 + 0.2;
        System.out.println(d);  // 0.30000000000000004 ❌
        
        // ПРАВИЛЬНО: BigDecimal
        BigDecimal bd = new BigDecimal("0.1").add(new BigDecimal("0.2"));
        System.out.println(bd);  // 0.3 ✓
        
        // ХОРОШО: long (в центах)
        long cents = 10 + 20;
        System.out.println(cents / 100.0);  // 0.3 ✓
    }
}

Лучшие практики для финансовых систем

  1. Используй BigDecimal для денежных сумм
  2. Никогда не используй float или double
  3. Создавай BigDecimal из строк, не из double
  4. Установи фиксированный SCALE (обычно 2)
  5. Выбери RoundingMode HALF_UP (стандартное округление)
  6. Оборачивай денежные суммы в специализированный класс (Money)
  7. Проверяй совпадение валют перед операциями
  8. Используй @Nonnull аннотации для денежных полей
  9. Логируй все денежные операции для аудита
  10. Тестируй граничные случаи (округление, переполнение)

Заключение

Для хранения денежных сумм используй:

  1. BigDecimal — основной и рекомендуемый выбор для большинства случаев
  2. long (в минимальных единицах валюты) — если критична производительность
  3. Никогда не используй double или float — они приводят к потере денег

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

Какой тип данных лучше использовать для хранения сумм денег? | PrepBro