Перейти к основному содержимому

Введение в javax.measure

· 6 мин. чтения

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 , который представляет количественное свойство , такое как объем или площадь. Библиотека предоставляет множество подинтерфейсов, которые моделируют наиболее часто используемые количественные атрибуты. Некоторые примеры: Volume , Length , ElectricCharge , Energy , Temperature .

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

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

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

Объект типа Quantity<Q extends Quantity<Q>> имеет методы для получения единицы измерения и значения: 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. Преобразование единиц

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

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

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

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

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

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

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

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

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

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

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

@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));
}

Точно так же пример ProductUnit и его преобразование:

@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);
}

Здесь мы создали составную единицу 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 , который связывает общесистемные метки с единицами измерения .

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

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

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

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

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

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

// 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 , мы также увидели, насколько удобно конвертировать одну единицу в другую несколькими способами.

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

И, как всегда, весь код доступен на GitHub .