UDTopia's Pure*
base classes make it easy to wrap basic values in rich, well-named UDTs.
To Wrap This | Extend This |
---|---|
primitive double |
PureDouble |
primitive long |
PureLong |
primitive int |
PureInt |
String 1 |
PureString |
any other object12 | PureValue |
public final @Value class BodyTemp extends PureDouble<BodyTemp>
{
public BodyTemp(double reading) { super(BodyTemp::new, reading); }
}
- Declare a class and extend
PureDouble
,PureInt
,PureLong
,PureString
, orPureValue
. Repeat your class name in the generic type. - Make the class
final
. You can also add the@Value
annotation to document that it's a pure value. (Read more about that here.) - Declare a constructor that takes a single argument (the raw value); and passes a method reference to itself, and the raw value, to the superclass constructor.
If you prefer to expose static factory methods instead of the constructor, declare it like this:
public final @Value class BodyTemp extends PureDouble<BodyTemp>
{
private BodyTemp(double reading) { super(BodyTemp::inCelsius, reading); }
public static BodyTemp inCelsius(double reading) { return new BodyTemp(reading); }
public static BodyTemp inFahrenheit(double reading) { return inCelsius((reading - 32.0) / 1.8); }
}
The constructor/factory method reference allows methods like map
to return new instances of your subclass.
More about that below.
Other than PureValue
, the underlying raw type (primitive or String
) guarantees immutability.
PureValue
can wrap any raw type, so when that raw type is mutable, we need to do a little more to ensure immutability.
We need to provide a way to make a defensive copy.
public final @Value class MousePosition extends PureValue<Point, MousePosition>
{
public MousePosition(Point point)
{
super(MousePosition::new, point, p -> new Point(p.x, p.y));
}
}
PureValue
will automatically make a defensive copy in the constructor, and when passing or returning the value to other objects.
A string is always a String
, but we can name UDTs whatever we want.
Not only the names of UDT classes, but also their methods and parameters.
Well-chosen names make code clearer, easier to read, and self-documenting.
Applications deal with a lot of raw values — mostly String
, but also some int
and long
.
But a String
is essentially an unbounded, unconstrained array of bytes.
Do we really allow any string of characters in every place we use String
?
Similarly, do we really allow any integer value in every place we use int
or long
?
Of course not. Each is constrained to a set of valid values, with a specific purpose.
Clearly, our BodyTemp
class should not allow the full range of double
values.
We should trap values outside the valid range.
UDTopia can normalize and validate values automatically with Rule Annotations:
@Min(MIN_BODY_TEMP) @Max(MAX_BODY_TEMP)
public final @Value class BodyTemp extends PureDouble<BodyTemp>
Raw values enter our app from the outside world: user input, files, databases, network services, and so on. The best place to trap invalid raw data is at the entry point, the "edges" of our app. In this way, we can trust that all values we process internally are valid. That means we don't need extra logic to guard against invalid data in our core logic. Clean data means cleaner, simpler code.
Since each value has a specific purpose, we have to be careful to not get them mixed up. Substitute a user's first name with their email address, and we'd have an embarrassing bug. Substitute a stock trade's price with its quantity, and we could go out of business!
It's a shame the compiler can't help us catch these bugs before we launch the app… Or can it?
When we wrap values in UDTs, we give them names, so they never get mixed up. Even better, the compiler won't let us accidentally substitute one for another.
@Min(MIN_BLOOD_O2) @Max(MAX_BLOOD_O2)
public final @Value class BloodOxygen extends PureDouble<BloodOxygen>
{
public BloodOxygen(double reading) { super(BloodOxygen::new, reading); }
}
Without UDTs, our app would have to take two double
inputs.
There might be a method like recordVitals(double, double)
.
How will developers calling this method know which double
is which?
Well, they could look at the Javadoc, or read the source code to find the parameter names.
But, with UDTs, the method would be recordVitals(BodyTemp, BloodOxygen)
.
It is impossible to get them mixed up; the compiler won't let us.
Deeper inside the app, where we process these values, not only is it easy to identify and distinguish them, but we can also be confident they are valid values.
By creating a class to wrap the data, we also create a place to put logic related to that data: as methods of the class.
public boolean isFever() { return getAsDouble() > FEVER_THRESHOLD; }
public double getAsFahrenheit() { return getAsDouble() * 1.8 + 32.0; }
When we keep the logic close to the data it processes, it's easier to reason about, easier to test, and more reusable.
The JVM is excellent at optimizing and inlining at runtime. Benchmarks show there's not much difference in throughput or latency performance between a raw value and a UDT.
Particularly when compared to primitive types, which don't allocate objects on the heap, UDTs do generate more GC pressure. However, the GC algorithms of modern JVMs are extraordinarily sophisticated. Java GC is not the performance drag it once was. Do not fear the GC Bogeyman!
That said, if GC pressure is a problem for your app, UDTopia has an advanced instance recycling feature.
Pure*
classes implement Java's supplier interfaces.
To get the raw value, call one of the get*
methods.
When the conversion would change the value or overflow, the get*
methods throw ArithmeticException
.
To round to the closest value, call one of the roundTo*
methods.
Pure Class | Supplier Interface | Getter Method | Rounding Method |
---|---|---|---|
PureDouble |
DoubleSupplier |
getAsDouble() |
- |
LongSupplier |
getAsLong() |
roundToLong() |
|
IntSupplier |
getAsInt() |
roundToInt() |
|
PureLong |
DoubleSupplier |
getAsDouble() |
- |
LongSupplier |
getAsLong() |
- | |
IntSupplier |
getAsInt() |
roundToInt() |
|
PureInt |
DoubleSupplier |
getAsDouble() |
- |
LongSupplier |
getAsLong() |
- | |
IntSupplier |
getAsInt() |
- | |
PureString |
Supplier<String> |
get() |
- |
PureValue<Raw> |
Supplier<Raw> |
get() |
- |
When extending a Pure*
class, don't add any independent fields.
They are meant to be single values, used as fields in other classes, elements of collections, and in method signatures.
They include correct implementations of hashCode()
and equals(@Nullable Object)
, which are final.
The toString()
implementations are not final, and just return the default string conversion of the raw value by default.
You can override toString()
to customize its format.
To supplement equals(@Nullable Object)
, there's a shortcut eq(This)
method.
It accepts only non-null objects of the same type, and is faster than equals
.
The following Pure*
classes are Comparable
via the UDTComparable
interface.
We can use compareTo(This)
to compare with other values of the same class.
Pure Class | Comparable |
---|---|
PureDouble |
yes |
PureLong |
yes |
PureInt |
yes |
PureString |
yes |
PureValue |
yes, if implements UDTComparable |
UDTComparable
classes expose more useful methods:
-
min(This)
max(This)
Return the lesser or greater of two values. For example, we can easily find the maximum value in a stream, using:stream.reduce(MyUDT::max)
-
isGreaterThan(This)
isLessThan(This)
isGreaterThanOrEqualTo(This)
isLessThanOrEqualTo(This)
Compare two values.
We can operate on the raw value, producing new UDT values, without unwrapping and re-wrapping them.3
final ArticleTitle title = new ArticleTitle(formInput.get("title"));
final ArticleTitle trimmed = title.map(String::trim);
final ArticleTitle capitalized = trimmed.map(WordUtils::capitalizeFully);
final ArticleTitle noDot = capitalized.map(t -> t.replaceAll("\\.$", ""));
Or, to map the raw value and convert to another UDT, just add a constructor reference for the destination type.
final UrlSlug urlSlug = noDot.map(t -> t.replaceAll("\\s", "-"), UrlSlug::new);
We can check the raw value without unwrapping it, using the is
and isNot
methods.
if (title.isNot(String::isEmpty))
{
if (title.is(t -> t.length() > LONG_TITLE_LENGTH))) {...}
}
The numeric Pure*
classes (PureDouble
, PureLong
, PureInt
) provide some useful numeric operations:
-
add
(multiple signatures)
Addition with overflow protection. -
subtract
subtractFrom
(multiple signatures)
Subtraction with overflow protection. -
multiplyBy
(multiple signatures)
Multiplication with overflow protection. -
divideBy
divide
(multiple signatures)
Division with rounding. -
increment()
decrement()
Add or subtract one with overflow protection. -
isZero()
isNonZero()
Check if the raw value is zero. -
isPositive()
isNegative()
Check the sign of non-zero values. -
negate()
Flip the sign of non-zero values. -
invert()
Get the inverse of non-zero values. -
format(NumberFormat formatter)
format(String pattern)
Format the number as a string.
Numeric operations work with primitive values, and UDTs of different classes can work together:
// This is only an illustration
// Don't use floating-point values for money!
final Quantity orderQuantity = new Quantity(5);
final Price unitPrice = new Price(24.95);
final Price orderTotal = unitPrice.multiplyBy(orderQuantity);
final Price withTax = orderTotal.multiplyBy(1.15);
PureDouble
also supports rounding off the fractional part:
round()
roundUp()
roundDown()
Return the nearest integer, or the next one above/below thedouble
value.
Footnotes
-
PureValue
doesn't support array objects. Arrays need special handling forequals
,hashCode
, andtoString
, which would be slower. Typically,Pure*
classes are for single values, but if you really need a collection, use aList
orSet
. ↩ -
Note: Since pure values are immutable, each
map
produces a new instance with the mapped value. ↩