Exploring Java's Units of Measurement API (JSR 385)

 · 13 min
AI-generated by DALL-E

Dealing with unit conversion is always a pain point. At first, it seems simple enough until you hit the first edge-case. Just like with Date and Time (JSR 310), there’s a well-specified solution available, although not directly in the JDK: the Units of Measurement API (JSR 385).

There’s going to be a mix of British English (BE) and American English (AE) writing of unit names throughout the article, as the library I’m talking about uses BE for constants, etc. However, I’m used to writing AE, and most of my audience is most likely too.


Dealing with Units is Hard

Working with different units of measurement in our code can be challenging. While we have well-specified and standardized SI units, their consistent and correct use or conversion remains complex.

And the problem is getting worse when non-SI units, such as United States customary units or the Imperial units, are thrown into the mix.

12 inches is a foot
3 feet is a yard
22 yards is a chain
10 chains is a furlong
8 furlongs is a mile
3 miles is a league

This often leads to potential conversion errors, inaccuracies, and inefficiencies.

There are many real-life examples of things going totally wrong in places you’d expect them to know better.

Notable Unit Conversion Errors

In 1983, Air Canada Flight 143 ran out of fuel mid-flight due to a series of issues. A faulty “fuel-quantity indicator sensor” was replaced with another nonfunctional one. To refuel the plane, a dripstick was used to manually measure the volume and calculate the required amount. The Boeing 767 was the first of its type in the Air Canada fleet, and it used kg for fuel, unlike all the other planes and all the manuals, which used lb. This resulted in the plane only carrying ~45% of the required fuel load for its trip.

In 1999, NASA lost the $125 million Mars Climate Orbiter due to miscalculations by parts of the ground software provided by Lockheed Martin using United States customary units instead of the required SI units.

In 1999, Korean Air Cargo Flight 6316, originating in Shanghai, got clearance to climb to 1.500m, which it did. The first officer mistook the “1.500” as feet and concluded they should descend to 1.500 feet (~460m). They descended too fast and lost control, eventually crashing the plane. Almost all countries use feet to measure altitudes in compliance with the International Civil Aviation Organization convention, except China, Russia (until 2017), and North Korea.

In 2003, a roller coaster train derailed at Space Mountain ride in Tokyo Disneyland due to faulty axles. The original specifications were converted from Imperial unit to the metric system, which introduced an error that wasn’t discovered when replacement parts were ordered.

As you can see, there are a lot of ways dealing with units can go wrong, especially in high-stress situations, like taking off from a busy airport or needing to refuel a plane quickly. In the best-case scenario, you can glide safely to an airport, as Air Canada Flight 143 did. Worst-case scenario, however, the plane crashes, as Korean Air Cargo Flight 6316, killing eight people and injuring 42.

That’s why we absolutely need to know about the problems and caveats of working with different units in our code.


What’s the Unit of Measurement API

JSR 385, also known as the Units of Measurement API 2.0, is a Java Specification Request aiming to provide a standardized and unified API for handling units and quantities.

History of the API

Like Java Enhancement Proposals (JEP), JSRs are often superseded by newer requests, and the origins of JSR 385 reach back to multiple earlier ones, like JSR 108 (2001-2004, withdrawn), JSR 275 (2005-2010, rejected), and JSR 363 (2014-2016, released as 1.0).

In February 2022, version 2.1, and version 2.2 was released in May, 2024.

Specification Vs. Implementation

Unlike another JSR you’re most likely familiar with, JSR 310, the Date and Time API introduced in Java 8, the Units of Measurement API is not included in the JDK. Instead, it’s a dependency you need to include with the code residing in the javax.measure package.

Furthermore, the dependency is only the specification, which includes only interfaces, not implementation in the form of classes. The Java/Jakarta EE dependencies work similarly. The downside, however, is the need for a reference implementation to actually use the API. Thankfully, JSR 385 got as covered.


A Taste of Indriya

The people behind the API also provide a reference implementation called “Indriya”.

© Units of Measurement project
© Units of Measurement project

The project not only implements the API but also provides some additional types, like MixedQuantity.

Gradle
implementation 'tech.units:indriya:2.2'
Maven
<dependency>
  <groupId>tech.units</groupId>
  <artifactId>indriya</artifactId>
  <version>2.2</version>
</dependency>

The API and reference implementation both support Java 8, with some additions in the case of Java 9+. However, the code examples throughout the rest of the article will use more modern Java features.

Enough about technicalities, it’s time for some code!


How to Use javax.measure

The API is build on three concepts:

  • Units
  • Quantities
  • Dimensions

Each of them has a corresponding type in the API:

The interfaces provide methods for unit conversion, arithmetic operations, and more.

The reference implementation provides the additional types with static factory methods, constants, etc., to create any of the three base types as required.

Units and Quantities

A Unit is defined by its symbol, name, and dimension. Take Kelvin (name), for example. It’s a temperature (dimension) with the symbol “K”.

There are two ways to create a Unit:

  • Using a constant from the tech.units.indriya.unit.Units type
  • Combining a Unit with a javax.measure.Prefix

Using a constant is the simplest way:

java
Unit<Temperature> kelvin = Units.KELVIN;

Indriya provides a variety of 43 units based on the 33 defined Quantity implementations of the API:

  • Acceleration
  • AmountOfSubstance
  • Angle
  • Area
  • CatalyticActivity
  • Dimensionless
  • ElectricCapacitance
  • ElectricCharge
  • ElectricConductance
  • ElectricCurrent
  • ElectricInductance
  • ElectricPotential
  • ElectricResistance
  • Energy
  • Force
  • Frequency
  • Illuminance
  • Length
  • LuminousFlux
  • LuminousIntensity
  • MagneticFlux
  • MagneticFluxDensity
  • Mass
  • Power
  • Pressure
  • RadiationDoseAbsorbed
  • RadiationDoseEffective
  • Radioactivity
  • SolidAngle
  • Speed
  • Temperature
  • Time
  • Volume

Even though the available constants already cover a lot of use-cases, their real power lies in combining units with a javax.measure.Prefix. A Prefix prepends a unit of measurement to indicate multiples or fractions of that unit.

For example, there’s no constant for kilometre. Instead, we’re supposed to use MetricPrefix.KILO in combination with Units.METRE:

java
Unit<Length> meter = Units.METRE;
Unit<Length> kilometer = MetricPrefix.KILO(Units.METRE);

The API gives us two Prefix implementations:

With a Unit, we can create a corresponding Quantity.

The simplest way to create a Quantity instance is using one of the static factory methods of tech.units.indriya.quantity.Quantities called getQuantity(...):

java
// Tokyo Tower = 333m
Quantity<Length> heightOfTokyoTower =
    Quantities.getQuantity(333, Units.METRE);

// Mass of the Moon = 0.07346*10^24
Unit<Mass> yottaKg = MetricPrefix.YOTTA(Units.KILOGRAM);
Quantity<Mass> moon = Quantities.getQuantity(0.07346D, yottaKg);

Mixing Quantities

Some values are easier to represent by multiple units, such as “1m, 74cm”. We could just say “174 cm”, but in many scenarios, splitting up the values can make more sense.

Indriya got you covered by giving us two approaches to create “mixed-radix” quantities.

The first one is MixedQuantity<Q extends Quantity<Q>>, which doesn’t actually implement Quantity<Q extends Quantity<Q>> but QuantityConverter<Q> instead. Its only purpose of MixedQuantity is to provide an easy way to create a type holding multiple Quantity instances and then converting it to another unit, making it a real Quantity:

java
Quantity<Length> m = Quantities.getQuantity(1, Units.METRE);

Quantity<Length> cm =
    Quantities.getQuantity(74, MetricPrefix.CENTI(Units.METRE));


MixedQuantity<Length> heightMixed = MixedQuantity.of(m, cm);

Quantity<Length> height = heightMixed.to(MetricPrefix.CENTI(Units.METRE));

System.out.println(height);
// => 174 cm

The second option is using one of the Quantities factory methods. The code is not as easy on the eyes as MixedQuantitiy, but we neither need Quantity instances nor do we need the conversion step:

java
Quantity<Length> height =
    Quantities.getQuantity(new Number[] {23,  43},
                           Units.METRE,
                           MetricPrefix.CENTI(Units.METRE));

System.out.println(height);
// => 174 cm

Unit Dimensions

As mentioned before, part of a Unit definition is a Dimension. This type represents the physical dimensions of the unit, used to categorize non-interchangeable quantities.

Physical quantities can be a single base dimension, such as length, mass, time, etc., or a combination, like kilometers per hour.

The Dimension type provides a standardized representation to ensure that different units of the same physical quantity are correctly compared and converted.

The type tech.units.indriya.unit.UnitDimension provides 8 dimensions:

  • NONE
  • LENGTH (L)
  • MASS (M)
  • TIME (T)
  • ELECTRIC_CURRENT (I)
  • TEMPERATURE (Θ)
  • AMOUNT_OF_SUBSTANCE (N)
  • LUMINOUS_INTENSITY (J)

The Dimension type is crucial for accurate unit handling by ensuring that operations with units and quantities adhere to physical laws.

Operations involving dimensions must respect their distinct properties, such as not confusing mass with length or volume with time. When physical measures are multiplied or divided, the resulting dimensions reflect these operations, like “length squared for area” when multiplying two meter-based quantities.


Creating Custom Units

A lot of bases are already covered by the available units, but what if you need the “banana equivalent dose”?

The banana equivalent dose (BED) is an informal measure of radiation exposure. We absorb ~0.1 µSv when consuming an average banana.

Be careful with those radioactive bananas!
Be careful with those radioactive bananas!

If we look at the implementations of tech.units.indriya.AbstractUnit<Q extends Quantity<Q>> in the tech.units.indriya.unit package, we see the different options for creating a Unit instance.

AlternateUnit<Q extends Quantity<Q>>

An alternative unit format, like Ohm is Volt divided by Ampere.

java
Unit<ElectricResistance> OHM =
    AlternateUnit.of(Units.VOLT.divide(Units.AMPERE), // parent unit
                     "Ω",                             // symbol
                     "Ohm");                          // name

This works for system units, not for calculation-based units.

AnnotatedUnit<Q extends Quantity<Q>>

Units that are based on another one, but add an annotation:

java
Quantity<Length> height = Quantities.getQuantity(333, Units.METRE);

System.out.println(height);
// => 333 m

Unit<Length> annotatedMeter = AnnotatedUnit.of(Units.METRE, "annotation");

Quantity<Length> heightAnnotation = Quantities.getQuantity(333, Units.METRE);

System.out.println(heightAnnotation);
// => 333 m{annotation}

The annotation only affects formatting.

BaseUnit<Q extends Quantity<Q>>

You might have guessed it, but this is the lowest level unit that other units build up on:

java
Unit<Length> METRE = new BaseUnit<>("m", "Metre", UnitDimension.LENGTH);

There’s no static of(...) factory method available for some reason.

ProductUnit<Q extends Quantity<Q>>

Units that are products of other units, like square and cubic meters:

java
Unit<Area> SQUARE_METRE = new ProductUnit<>(Units.METRE.multiply(Units.METRE));
Unit<Area> CUBIC_METRE  = new ProductUnit<>(squareMetre.multiply(Units.METRE));

There are static of... factory methods available, but they require casting to make the compiler happy:

java
Unit<Area> squareMeter = (Unit<Area>) ProductUnit.ofProduct(Units.METRE,
                                                            Units.METRE);

TransformedUnit<Q extends Quantity<Q>>

A TransormedUnit is derived from other units, using a UnitConverter to calculate the new value. Perfect for our “banana problem”!

java
Unit<RadiationDoseEffective> parentUnit = MetricPrefix.MICRO(Units.SIEVERT);

UnitConverter<RadiationDoseEffective> converter =
    MultiplyConverter.ofRationale(BigDecimal.ONE,
                                  BigDecimal.TEN);

Unit<RadiationDoseEffective> BED =
    new TransformedUnit<>("BED",                              // symbol
                          "Banana equivalent dose",           // name
                          MetricPrefix.MICRO(Units.SIEVERT),  // parent unit
                          MultiplyConverter.ofRational(1L,    // converter)
                                                       10L));

Quantity<RadiationDoseEffective> fiveThousandBananas =
    Quantity.getQuantity(5_000, BED);

System.out.println("Dose: " + fiveThousandBananas.to(Units.SIEVERT));
// => 0.0005 Sv

Even after devouring 5,000 bananas, we’re far away from the lethal dose of 3.5 Sv.

The UnitConverter interface is used to convert between different units and the package tech.units.indriya.function provides many different operations.

Let’s take a look at a unit that might be more common than BED…

We can concatenate multiple converters to create more complex units, like Fahrenheit:

java
Unit<Temperature> FAHRENHEIT =
  new TransformedUnit<>("°F",
                        Units.KELVIN,
                        MultiplyConverter.ofRational(BigInteger.valueOf(5),
                                                     BigInteger.valueOf(9))
                                         .concatenate(new AddConverter(459.67)));

It’s still easy on the eyes and straightforward.

Most custom units will most likely by single multiplications with a base unit to start from. Take a look at the OpenHAB project, an open-source home automation software which uses Indriya and implements Imperial units in a few lines of code.


Safer Calculation with Multiple Units

To illustrate how to calculate with different units, let’s find out how many American football fields (100 yards excluding endzones) are needed to reach the moon.

To the moon!
To the moon!

First, we need to declare the missing units, and not just yard in relationship to metre:

java
Unit<Length> INCH =
    new TransformedUnit<>("in",
                          Units.METRE,
                          MultiplyConverter.ofRational(BigInteger.valueOf(254),
                                                       BigInteger.valueOf(10_000)));

Unit<Length> FOOT =
    new TransformedUnit<>("ft",
                          INCH,
                          MultiplyConverter.of(12));

Unit<Length> YARD =
    new TransformedUnit<>("yd",
                          FOOT,
                          MultiplyConverter.of(3));

We can even go a step further and make “football field” an custom unit!

java
Unit<Length> FOOTBALL_FIELDS =
    new TransformedUnit<>("ff",
                          YARD,
                          MultiplyConverter.of(100L));

This way, the calculation becomes even simpler/more readable. Even though I’d recommend sticking to official units.

java
Quantity<Length> distanceToMoon =
    Quantities.getQuantity(384_400, MetricPrefix.KILO(Units.METRE));

Quantity<Length> oneFootballField =
    Quantities.getQuantity(1, FOOTBALL_FIELDS);

Quantity<?> footballFieldsToMoon =
    distanceToMoon.divide(oneFootballField);

System.out.println(footballFieldsToMoon);

And the result is *drum roll*

384400 km/ff

Well, that’s not really helpful…

The JavaDoc of divide explains what happened:

Returns the quotient of this {@code Quantity} divided by the {@code Quantity} specified.

To convert the quotient to an actual value, wee need to call toSystemUnit():

java
System.out.println(footballFieldsToMoon.toSystemUnit());
// => 4203849.518810148731408573928258968

And there you have it! It’s approx. 4.203.850 American football fields to the moon, excluding endzones.


Formatting Units and Quantities

Formatting is done via an implementation of UnitFormat and QuantityFormat.

UnitFormat

There are three implementations available:

  • EBNFUnitFormat
  • LocalUnitFormat
  • SimpleUnitFormat

Usually, the singleton instance is retrieved via static getInstance() and one of the format(...) methods is used. However, there are static factory methods available to create specialized/customized variants.

The provided formatters support to specify the label for a unit:

java
SimpleUnitFormat.getInstance().label(METER.multiply(0.3048), "ft");

Because Unit implements boolean equals(Object obj), the label override is used for any equal unit:

java
Unit<Length> FOOT = new TransformedUnit<>("ft",
                                          Units.METRE,
                                          MultiplyConverter.of(0.3048));


SimpleUnitFormat.getInstance().label(Units.METRE.multiply(0.3048), "FT");

String formatted = SimpleUnitFormat.getInstance().format(FOOT);

System.out.println(formatted);
// => FT

LocalUnitFormat is a locale-sensitive formatter and provides partial translations for the following Locales:

  • Arabic (ar)
  • Chinese (cn)
  • German (de)
  • English/Indonesia (en_ID)
  • English/India (en_IN)
  • English/Malaysia (en_MY)
  • English (en)
  • Japanese (ja)
  • Norway (no)
  • Portugese (pt)
  • Russian (ru)
  • Swedish (sv)
  • Thai (th)

EBNFUnitFormat is a locale-neutral format that uses an Extended Backus-Naur Form grammar for parsing. Check out the class document for details.

QuantityFormat

The QuantityFormat actually extends java.text.Format.

Two implementations are provided:

  • NumberDelimiterQuantityFormat
  • SimpleQuantityFormat

The first one, NumberDelimiterQuantityFormat, combines NumberFormat and UnitFormat with a specific delimiter. To create your own, use the tech.units.indriya.format.NumberDelimiterQuantityFormat.Builder:

java
Quantity<Length> length = Quantities.getQuantity(123.456, Units.METRE);

NumberDelimiterQuantityFormat customFormat =
  NumberDelimiterQuantityFormat.builder()
                               .setNumberFormat(NumberFormat.getInstance())
                               .setUnitFormat(SimpleUnitFormat.getInstance())
                               .setDelimiter(" ~ ")
                               .build();

String formatted = customFormat.format(length);

System.out.println(formatted);
// => 123.456 ~ m

The SimpleQuantityFormat is pattern-based. To remove the usual space between the number (n) and the unit (u), try this:

java
Quantity<Length> length = Quantities.getQuantity(123.456, Units.METRE);

SimpleQuantityFormat customFormat = SimpleQuantityFormat.getInstance("nu");

String formatted = customFormat.format(length);

System.out.println(formatted);
// => 123.456m

There are a few restrictions, like the unit has to come after the value, and nothing is allowed after the unit. Check out the type’s JavaDoc for more information about the possible patterns.


Conclusion

The Unit of Measurements API is quite fascinating to me.

On the one hand, it’s a perfect way to mitigate the common problems when working with measurements:

  • Conversion errors
  • Inconsistency
  • Precision/rounding issues
  • Ambiguity and miscommunication
  • Handling compound units

The reference implementation offers a robust framework for creating safer and more expressive code dealing with SI units, and adding other units is straightforward, too.

On the other hand, though, it’s another layer that can make things more complicated than it needs to be. If you don’t need to deal with different units and conversions, there’s no need to use specific types to represent values except for increased expressiveness. And what about persistence or interoperability with other code that doesn’t know about Quantity et.al.?

In my opinion, that’s the reason why JSR 385 wasn’t included directly in the JDK, whereas JSR 310, the Date and Time API, was.

It’s a powerful API, but niche, for specific use cases that require such a way of doing things.

Still, it’s not a must-have for all your calculation needs. However, knowing about a reliable and mature API for straightforward unit handling when needed is advantageous, so I wrote this article about it.


A Functional Approach to Java Cover Image
Interested in using functional concepts and techniques in your Java code?
Check out my book!
Available in English, Polish, Korean, and soon, Chinese.

Resources