«1. Обзор

В этой статье мы представим Units of Measurement API, который обеспечивает унифицированный способ представления мер и единиц в Java.

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

JSR-363 (ранее JSR-275 или библиотека javax.measure) помогает нам сэкономить время разработки и в то же время делает код более читабельным.

2. Зависимости Maven

Давайте просто начнем с зависимости Maven, чтобы получить библиотеку:

<dependency>
    <groupId>javax.measure</groupId>
    <artifactId>unit-api</artifactId>
    <version>1.0</version>
</dependency>

Последнюю версию можно найти на Maven Central.

Проект unit-api содержит набор интерфейсов, определяющих, как работать с количествами и единицами измерения. Для примеров мы будем использовать эталонную реализацию JSR-363, которая представляет собой unit-ri:

<dependency>
    <groupId>tec.units</groupId>
    <artifactId>unit-ri</artifactId>
    <version>1.0.3</version>
</dependency>

3. Изучение API

Давайте посмотрим на пример, где мы хотим хранить воду в бак.

Устаревшая реализация выглядела бы так:

public class WaterTank {
    public void setWaterQuantity(double quantity);
}

Как видим, приведенный выше код не упоминает единицу количества воды и не подходит для точных вычислений из-за наличия типа double.

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

API JSR-363 предоставляет нам интерфейсы Quantity и Unit, которые устраняют эту путаницу и оставляют подобные ошибки вне области действия нашей программы.

3.1. Простой пример

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

Как упоминалось ранее, JSR-363 содержит интерфейс Quantity, который представляет количественное свойство, такое как объем или площадь. Библиотека предоставляет множество подинтерфейсов, которые моделируют наиболее часто используемые количественные атрибуты. Некоторые примеры: объем, длина, электрический заряд, энергия, температура.

Мы можем определить объект Quantity\u003cVolume\u003e, который должен хранить количество воды в нашем примере:

public class WaterTank {
    public void setCapacityMeasure(Quantity<Volume> capacityMeasure);
}

Помимо интерфейса Quantity, мы также можем использовать интерфейс Unit для определения единицы измерения для имущество. Определения часто используемых единиц можно найти в библиотеке unit-ri, например: КЕЛЬВИН, МЕТРЫ, НЬЮТОН, ЦЕЛЬСИЙ.

Объект типа Quantity\u003cQ extends Quantity\u003cQ\u003e\u003e имеет методы для получения единицы измерения и значения: getUnit() и getValue().

Давайте рассмотрим пример установки значения количества воды:

@Test
public void givenQuantity_whenGetUnitAndConvertValue_thenSuccess() {
    WaterTank waterTank = new WaterTank();
    waterTank.setCapacityMeasure(Quantities.getQuantity(9.2, LITRE));
    assertEquals(LITRE, waterTank.getCapacityMeasure().getUnit());

    Quantity<Volume> waterCapacity = waterTank.getCapacityMeasure();
    double volumeInLitre = waterCapacity.getValue().doubleValue();
    assertEquals(9.2, volumeInLitre, 0.0f);
}

Мы также можем быстро преобразовать этот объем в ЛИТР в любую другую единицу измерения:

double volumeInMilliLitre = waterCapacity
  .to(MetricPrefix.MILLI(LITRE)).getValue().doubleValue();
assertEquals(9200.0, volumeInMilliLitre, 0.0f);

Но, когда мы попытаемся преобразовать количество воды в другую единицу – не типа Volume, получаем ошибку компиляции:

// compilation error
waterCapacity.to(MetricPrefix.MILLI(KILOGRAM));

3.2. Параметризация класса

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

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

Unit<Length> Kilometer = MetricPrefix.KILO(METRE);
Unit<Length> Centimeter = MetricPrefix.CENTI(LITRE); // compilation error

Всегда есть возможность обойти проверку типа с помощью метода asType():

Unit<Length> inch = CENTI(METER).times(2.54).asType(Length.class);

Мы также можем использовать подстановочный знак, если мы не уверены в типе количества:

Unit<?> kelvinPerSec = KELVIN.divide(SECOND);

4. Преобразование единиц

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

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

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

Мы также можем использовать префиксы или множители из класса MetricPrefix, такие как KILO(Unit\u003cQ\u003e unit) и CENTI(Unit\u003cQ\u003e unit), которые эквивалентны умножению и делению на степень 10 соответственно.

Например, мы можем определить «Километр» и «Сантиметр» как:

Unit<Length> Kilometer = MetricPrefix.KILO(METRE);
Unit<Length> Centimeter = MetricPrefix.CENTI(METRE);

«

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

4.1. Пользовательские единицы

    В любом случае, если единица не существует в системе единиц, мы можем создать новые единицы с новыми символами:

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

@Test
public void givenUnit_whenAlternateUnit_ThenGetAlternateUnit() {
    Unit<Pressure> PASCAL = NEWTON.divide(METRE.pow(2))
      .alternate("Pa").asType(Pressure.class);
    assertTrue(SimpleUnitFormat.getInstance().parse("Pa")
      .equals(PASCAL));
}

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

@Test
public void givenUnit_whenProduct_ThenGetProductUnit() {
    Unit<Area> squareMetre = METRE.multiply(METRE).asType(Area.class);
    Quantity<Length> line = Quantities.getQuantity(2, METRE);
    assertEquals(line.multiply(line).getUnit(), squareMetre);
}

Аналогично, пример ProductUnit и его преобразование:

Здесь мы создали составную единицу SquareMetre, умножив METER на себя.

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

@Test
public void givenMeters_whenConvertToKilometer_ThenConverted() {
    double distanceInMeters = 50.0;
    UnitConverter metreToKilometre = METRE.getConverterTo(MetricPrefix.KILO(METRE));
    double distanceInKilometers = metreToKilometre.convert(distanceInMeters );
    assertEquals(0.05, distanceInKilometers, 0.00f);
}

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

Для облегчения однозначной электронной связи величин с их единицами измерения библиотека предоставляет интерфейс UnitFormat, этикетки с единицами измерения.

@Test
public void givenSymbol_WhenCompareToSystemUnit_ThenSuccess() {
    assertTrue(SimpleUnitFormat.getInstance().parse("kW")
      .equals(MetricPrefix.KILO(WATT)));
    assertTrue(SimpleUnitFormat.getInstance().parse("ms")
      .equals(SECOND.divide(1000)));
}

Проверим метки некоторых системных единиц с помощью реализации SimpleUnitFormat:

5. Выполнение операций с количествами

@Test
public void givenUnits_WhenAdd_ThenSuccess() {
    Quantity<Length> total = Quantities.getQuantity(2, METRE)
      .add(Quantities.getQuantity(3, METRE));
    assertEquals(total.getValue().intValue(), 5);
}

Интерфейс Quantity содержит методы для наиболее распространенных математических операций: add(), subtract() , умножить(), разделить(). Используя их, мы можем выполнять операции между объектами Quantity:

// compilation error
Quantity<Length> total = Quantities.getQuantity(2, METRE)
  .add(Quantities.getQuantity(3, LITRE));

Методы также проверяют единицы измерения объектов, над которыми они работают. Например, попытка умножить метры на литры приведет к ошибке компиляции:

Quantity<Length> totalKm = Quantities.getQuantity(2, METRE)
  .add(Quantities.getQuantity(3, MetricPrefix.KILO(METRE)));
assertEquals(totalKm.getValue().intValue(), 3002);

С другой стороны, можно добавить два объекта, выраженных в единицах измерения, имеющих одинаковую размерность:

В этом примере , метры и километры соответствуют измерению длины, поэтому их можно добавить. Результат выражается в единицах измерения первого объекта.

6. Заключение

В этой статье мы увидели, что Units of Measurement API дает нам удобную модель измерения. И, помимо использования Quantity и Unit, мы также увидели, насколько удобно конвертировать одну единицу в другую несколькими способами.

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