Many applications have a need for defining units and measures. Engineering and scientific applications need to support length, mass, volume, velocity, and many more types of units. Financial applications require currencies, perhaps of several different countries. Mathematical applications need functions and complex numbers. By default, Java does not provide support for application developers to add these concepts to their application. Fortunately, third party libraries have filled in the need with support for strongly-typed units, measures, and calculations.
One of the most advanced of these libraries is JScience, which might someday be incorporated in Java Standard Edition. JScience aims to "provide the most comprehensive Java^{TM} library for the scientific community." It provides all the features mentioned in the previous paragraph and more, with Java 5-based type safety. Another important advantage to using JScience over rolling your own unit handling framework is that JScience is well tested.
For engineering and scientific applications, the core classes are SI and NonSI. SI contains the basic units defined as part of the International System of Units, which are commonly known as the metrics units, such as meters, seconds, grams, radians, and less common units like candelas, moles, and webers. NonSI contains British and other units like miles, pounds (both force and mass), faradays, light years, and horsepower, which are defined in terms of SI units.
In order to simplify the API, JScience includes base units, not multiplicative units like kilometer and centimeter. To create those units, SI provides methods to build composite units.
import javax.units.SI; import javax.units.Unit; import javax.quantities.Length; import javax.quantities.Mass; import javax.quantities.Power; public class SIConversion { public static void main(String[] args) { Unit<Mass> centigram = SI.CENTI(SI.GRAM); Unit<Length> kilometer = SI.KILO(SI.METER); Unit<Power> megawatt = SI.MEGA(SI.WATT); } }
Of course, this can be simplified slightly using the static import feature added in Java 5. Below is the example from above using static imports. The rest of the examples in the article do not use this feature.
import static javax.units.SI.*; import javax.units.Unit; import javax.quantities.Length; import javax.quantities.Mass; import javax.quantities.Power; public class SIConversion { public static void main(String[] args) { Unit<Mass> centigram = CENTI(GRAM); Unit<Length> kilometer = KILO(METER); Unit<Power> megawatt = MEGA(WATT); } }
Units are parameterized (<Q extends
Quantity
>)
to enforce compile-time checks of units/quantities consistency. We will
see this in use later.
With these basic units, let's see what we can do with units and measures of a basic type, Length. All length units are defined in terms of the SI unit of length, the meter. Because SI provides only the SI standard units, meter is the only length unit in the SI class. However, the NonSI provides several other length units based on meter:
ANGSTROM
ASTRONOMICAL_UNIT
COMPUTER_POINT
FOOT
FOOT_SURVEY_US
INCH
LIGHT_YEAR
MILE
NAUTICAL_MILE
PARSEC
PIXEL
POINT
YARD
Additionally, as mentioned above, there are methods on the SI class used to create derived units like kilometer and centimeter.
With these units, you can convert unit values, determine the type of a particular unit, to validate types, or create composite units by multiplication and division. Here is an example of how to use these methods and the results:
import javax.quantities.Length; import javax.units.NonSI; import javax.units.SI; import javax.units.Unit; public class LengthUnits { public static void main(String[] args) { //Generate Units with scalar multiplication Unit<Length> FURLONG = NonSI.FOOT.times(660); // Ok // Unit<Length> FURLONG = SI.HOUR.times(60); // Compile error //Generate Units with prefixes Unit<Length> GIGAMETER = SI.GIGA(SI.METER); // Ok // Unit<Length> GIGAMETER = SI.GIGA(SI.NEWTON); // Compile error // Retrieval of the system unit (meter in this case) System.out.println(FURLONG.getSystemUnit()); // Retrieval of the unit dimension (L represents length) System.out.println(GIGAMETER.getDimension()); // Dimension checking (allows/disallows conversions) System.out.println(NonSI.LIGHT_YEAR.isCompatible(NonSI.PARSEC)); //legal System.out.println(NonSI.LIGHT_YEAR.isCompatible(SI.WATT)); //illegal System.out.println(NonSI.LIGHT_YEAR.isCompatible(NonSI.PARSEC.divide(SI.SECOND))); //illegal } } >m >[L] >true >false >false
The commented-out lines show a couple conversions that would cause compile-time errors when using Java 5 because of type discrepancies. JScience can be used with older versions of Java that do not have generics. With those JVMs, those lines (minus the parameterization) would compile but produce runtime errors.
Now let's look at a more complicated physical quantity, energy. The units of energy are composed of length, mass, and time. The base unit of energy is the SI unit is the joule (kg·m²/s²). NonSI provides the following other energy units based on the joule:
Surprisingly, the common unit of energy used in the United States, the BTU, is not defined. This might be because there are several slightly different versions of the BTU. However, let's see what's necessary to define the BTU.
import javax.quantities.Energy; import javax.units.SI; import javax.units.Unit; public class EnergyExample { public static void main(String[] args) { double BTU_CONVERSION = 1055.05585262; // Conversion that uses the International //(Steam) Table calorie, defined as 4.1868 J Unit<Energy> BTU = SI.JOULE.times(BTU_CONVERSION); System.out.println(BTU); System.out.println(BTU.getSystemUnit()); System.out.println(BTU.getDimension()); System.out.println(BTU.isCompatible(SI.JOULE)); } } >[J*1055.05585262] //toString() shows how a unit is built from base units >J //getSystemUnit() shows what the base unit(s) are >[L]²*[M]/[T]² //getDimension() shows that energy is composed of //Length squared times Mass divided by Time squared >true //isCompatible(SI.JOULE) shows that BTU and JOULE are //of the same dimensions
Now, BTU can be used in calculations just like any other unit of energy.
Now that we've defined the units and shown how to derive new units, let's look at how to use them in calculations. There are three basic ways to convert a value from one unit to another. The first is to ask the source unit for an appropriate converter to the destination unit and use the converter. The second is to create an instance of the Scalar class that contains both the type and the size, and have the Scalar convert the value. The third and most powerful approach is to generate an instance of the Measure class that contains both the type and the size, and have the Measure convert the value.
import javax.quantities.Length; import javax.quantities.Quantity; import javax.quantities.Scalar; import javax.units.NonSI; import javax.units.SI; import javax.units.Unit; import org.jscience.physics.measures.Measure; public class LengthCalculation { public static void main(String[] args) { Unit<Length> FURLONG = NonSI.FOOT.times(660); Unit<Length> MICRON = SI.MICRO(NonSI.INCH); //Derive the converter from the Unit System.out.println(FURLONG.getConverterTo(MICRON).convert(2)); // System.out.println(FURLONG.getConverterTo(SI.MICRO(SI.JOULE)).convert(2)); // Runtime error. //Build a Scalar and have it calculate Quantity<Length> furlongScalar = new Scalar<Length>(2, FURLONG); // Ok. System.out.println(furlongScalar.doubleValue(MICRON)); //Build a Measure and have it calculate Quantity<Length> lengthInFurlong = Measure.valueOf(2, FURLONG); System.out.println(lengthInFurlong.doubleValue(MICRON)); // long lengthInMicrons = furlongScalar.longValue(SI.JOULE); // Compile error. // Quantity<Length> badLength = new Scalar<Length>(2, SI.CELSIUS); // Compile error. } } >1.584E10 >1.584E10 >1.584E10
What is the difference between Scalar and Measure? Basically, Scalar is limited to performing conversions between units of the same basic type (between FURLONG and MICRON in this case, two units of Length). In contrast, Measure can be used to combine values of different units and to show error calculations.
import javax.quantities.Acceleration; import javax.quantities.Force; import javax.quantities.Mass; import javax.units.NonSI; import javax.units.SI; import org.jscience.physics.measures.Measure; public class ExactForce { public static void main(String[] args) { Measure<Mass> mass = Measure.valueOf(5, SI.KILOGRAM); Measure<Acceleration> acceleration = Measure.valueOf(9, SI.METER_PER_SQUARE_SECOND); Measure<Force> force = mass.times(acceleration).to(SI.NEWTON); double pounds = force.doubleValue(NonSI.POUND_FORCE); Measure<Force> force2 = mass.times(acceleration).to(NonSI.POUND_FORCE); System.out.println("mass = " + mass); System.out.println("acceleration = " + acceleration); System.out.println("force = " + force); System.out.println("pounds = " + pounds); System.out.println("force2 = " + force2); } } >mass = 5 kg >acceleration = 9 m/s² >force = 45 N >pounds = 10.116402439486972 >force2 = (10.1164024394869728 ± 1.8E-15) lbf
This example shows multiplying exactly 5 kg by exactly 9 m/s² to derive exactly 45 N. Then, we convert those 45 newtons to lbf and calculate the value. Finally, we multiply the inputs again and convert to lbf. This time however, the value is slightly different. What is going on?
Measure provides many ways both to specify precision of input values and to determine the precision of the calculated values. In the previous example, all the inputs were defined exactly because an int or long was passed into Measure.valueOf(). When a double is passed into those methods, the input is considered inexact, and the output is different.
import javax.quantities.Acceleration; import javax.quantities.Force; import javax.quantities.Mass; import javax.units.NonSI; import javax.units.SI; import org.jscience.physics.measures.Measure; public class InexactForce { public static void main(String[] args) { Measure<Mass> mass = Measure.valueOf(5.0, SI.KILOGRAM); Measure<Acceleration> acceleration = Measure.valueOf(9.0, SI.METER_PER_SQUARE_SECOND); Measure<Force> force = mass.times(acceleration).to(SI.NEWTON); double pounds = force.doubleValue(NonSI.POUND_FORCE); Measure<Force> force2 = mass.times(acceleration).to(NonSI.POUND_FORCE); System.out.println("mass = " + mass); System.out.println("acceleration = " + acceleration); System.out.println("force = " + force); System.out.println("pounds = " + pounds); System.out.println("force2 = " + force2); } } >mass = (5.0 ± 8.9E-16) kg >acceleration = (9.0 ± 1.8E-15) m/s² >force = (45.0 ± 2.1E-14) N >pounds = 10.116402439486972 >force2 = (10.1164024394869728 ± 7.1E-15) lbf
Because the double version of the methods was used, JScience calculates the imprecision built into Java's representation of double values and builds that imprecision into the measurement. So, the input values have explicit imprecision, and the calculated values have imprecision built in, too.
In the previous example, all the input values were exact because integer inputs are considered exact. However, the conversion from newtons to lbf introduces an imprecision due to a double-based multiplication, and that imprecision is carried forward.
In addition to showing the implicit imprecision of Java, JScience lets you define the known imprecision and uses it in calculations.
import javax.quantities.Acceleration; import javax.quantities.Force; import javax.quantities.Mass; import javax.units.NonSI; import javax.units.SI; import org.jscience.physics.measures.Measure; public class ExplicitInexactForce { public static void main(String[] args) { Measure<Mass> mass = Measure.valueOf(5.0, 0.5, SI.KILOGRAM); Measure<Acceleration> acceleration = Measure.rangeOf(8.75, 9.25, SI.METER_PER_SQUARE_SECOND); Measure<Force> force = mass.times(acceleration).to(SI.NEWTON); double pounds = force.doubleValue(NonSI.POUND_FORCE); Measure<Force> force2 = mass.times(acceleration).to(NonSI.POUND_FORCE); System.out.println("mass = " + mass); System.out.println("acceleration = " + acceleration); System.out.println("force = " + force); System.out.println("pounds = " + pounds); System.out.println("force2 = " + force2); } } >mass = (5.0 ± 5.0E-1) kg >acceleration = (9.0 ± 2.5E-1) m/s² >force = (45.1 ± 5.8) N >pounds = 10.144503557374438 >force2 = (10.1 ± 1.3) lbf
In this case, the mass is defined as being between 4.5 and 5.5 kg, and the acceleration is defined as being between 8.75 and 9.25 m/s², defined separately using valueOf() and rangeOf() methods. Additionally, the errors are combined when calculating the product, force.
You can use these error ranges to determine if two values are approximately equal. Explicitly, the comparison is whether the error ranges overlap at all. The following code snippet comes after the last line of ExplicitInexactForce, and it shows that the calculated pounds value is close to 10, but not close to 15.
Measure<Force> approx1 = Measure.valueOf(10.0, NonSI.POUND_FORCE); Measure<Force> approx2 = Measure.valueOf(15.0, NonSI.POUND_FORCE); System.out.println("(approx1 ~= force2) = " + approx1.approximates(force2)); System.out.println("(approx2 ~= force2) = " + approx2.approximates(force2)); >(approx1 ~= force2) = true >(approx2 ~= force2) = false
By default, JScience displays error ranges using a '±' symbol combined with two digits of accuracy and scientific notation. There are other ways to display error ranges using the MeasureFormat class.
import javax.quantities.Mass; import javax.units.SI; import org.jscience.physics.measures.Measure; import org.jscience.physics.measures.MeasureFormat; public class RangeDisplay { public static void main(String[] args) { Measure<Mass> mass = Measure.valueOf(100.0, SI.KILOGRAM).divide(3); System.out.println("mass = " + mass); MeasureFormat.setInstance(MeasureFormat.getPlusMinusErrorInstance(4)); System.out.println("mass = " + mass); MeasureFormat.setInstance(MeasureFormat.getBracketErrorInstance(2)); System.out.println("mass = " + mass); MeasureFormat.setInstance(MeasureFormat.getExactDigitsInstance()); System.out.println("mass = " + mass); } } >mass = (33.333333333333336 ± 1.4E-14) kg >mass = (33.33333333333333504 ± 1.421E-14) kg >mass = 33.333333333333336[14] kg >mass = 33.333333333333 kg
The first output shows the default style. The second shows using the '±' symbol combined with four digits. The third shows the range of imprecision in brackets. The final output shows the calculation value limited to the digits that are known exactly.
JScience enables monetary calculations using its Currency class, which by default provides the Australian, Canadian, Chinese, European, British, Japanese, Korean, Taiwanese, and United States currencies. Currency is a derived Unit, similar to many of the physical units. Unlike the physical units, the conversions between the different instances fluctuate over time (the conversion rate between Dollars and Euros changes daily; the conversion rate between inches and centimeters is fixed). JScience allows you to define a conversion rate between different currencies and use that conversion for calculations. Here is an example of the cost of a car trip in the United States calculated for a German tourist. In the example, we combine a currency conversion ($1 = 0.78 €), the rental car's mileage (25 mi/gal), the price of gas ($2.75/gal), and the length of the drive (400 miles) to calculate the cost of the gasoline used, in Euros:
import javax.quantities.Length; import javax.units.NonSI; import javax.units.UnitFormat; import org.jscience.economics.money.Currency; import org.jscience.economics.money.Money; import org.jscience.physics.measures.Measure; public class CurrencyExample { public static void main(String[] args) { // Changes the units for output UnitFormat.getStandardInstance().label(Currency.USD, "$"); UnitFormat.getStandardInstance().label(Currency.EUR, "€"); // Sets the exchange rate Currency.setReferenceCurrency(Currency.EUR); Currency.USD.setExchangeRate(0.78); // 0.78 € = $1 // Calculates the trip cost Measure<?> carMileage = Measure.valueOf(25, NonSI.MILE.divide(NonSI.GALLON_LIQUID_US)); // 25 mi/gal Measure<?> gasPrice = Measure.valueOf(2.75, Currency.USD.divide(NonSI.GALLON_LIQUID_US)); // $2.75/gal Measure<Length> tripDistance = Measure.valueOf(400, NonSI.MILE); // 400 mi Measure<Money> tripCost = tripDistance.divide(carMileage).times(gasPrice).to(Currency.EUR); System.out.println("Trip cost = " + tripCost + " (" + tripCost.to(Currency.USD) + ")"); } } >Trip cost = 34.32 € (44.00 $)
In addition to the Physical Units packages and the Money package, JScience provides several other interesting packages worth noting, but outside the scope of this document. Specifically, org.jscience.mathematics.vectors supports matrix and vector mathematics and org.jscience.mathematics.functions supports calculation, differentiation, and integration of symbolic equations.
According to the developers, several other packages are planned for inclusion in the project within the next year:
You may have noticed that some of the core JScience classes like SI, NonSI, Length, and Force are in subpackages of javax. This is true because the core of JScience has been accepted by the JCP committee for possible inclusion in Java. JSR 275 is an effort to refine JScience so that it will provide core support for Physical Units within Java.
This JSR is relatively early in the process, but its existence indicates that JScience is likely going to be the standard way of supporting conversions and calculations in the Java community.
Developing units systems for scientific, engineering, and mathematical applications is difficult, tedious, and error-prone. Fortunately, JScience provides a comprehensive, well-tested, and standard way for Java developers to support scientific, mathematical, and economic units. The JScience API uses generics to provide type-safety, and the core of JScience is under consideration for future include in Java SE.
If you are requested to add units-based functionality to your application, and you have considered rolling your own, you owe yourself to investigate JScience. If you decide to use it, you will likely save many hours of development and become familiar with future Java standard.
Lance Finney would like to thank Mario Aquino, Jeff Brown, Tom Wheeler, and Jean-Marie Dautelle for reviewing this article and providing useful suggestions.