The Concrete Numbers library is designed to integrate seamlessly into your Java codebase, providing strong compile-time guarantees around integer values and ranges. Whether you're modeling dimensions, indices, or bounded configuration values, the library helps express these constraints directly in your type signatures.
Below you'll find usage patterns that highlight how Concrete Numbers can replace error-prone manual checks with declarative, type-safe logic. These examples range from simple value constraints to more structured range enforcement and API contracts.
A primary use case for Concrete Numbers is to parameterize methods whose inputs naturally vary over a well-defined natural number range. The setMonth() example we saw previously illustrates this: rather than accepting any int and checking for validity at runtime, the method signature can be constrained at compile time to accept only values from One through Twelve.
Another usage pattern — and a more powerful one — is to parameterize types themselves. This allows you to define entire families of types whose structure or behavior depends on integer constraints, with those constraints enforced statically by the type system. For example, a Polygon<N> type can be constrained so that N, its number of sides, must be at least three, ensuring that only valid polygons can be instantiated.
/**
* An N-sided Polygon
*/
public interface Polygon<N extends ConcreteNumber & AtLeastThree> {
/**
* Returns the number of sides
*/
N numSides();
}
The previous pattern becomes even more expressive when methods within a parameterized type introduce their own local constraints. For example, the method vertex(K) accepts an index K that must be at least one and at most the number of sides N. This allows the compiler to guarantee that the provided index refers to a valid vertex of the polygon — without requiring a single runtime check. The result is not just safer code, but also code that communicates its contract with precision and elegance.
/**
* An N-sided Polygon
*/
public interface Polygon<N extends ConcreteNumber & AtLeastThree> {
/**
* Returns the number of sides
*/
N numSides();
/**
* Gets the vertex at the given index (1-indexed)
*/
<K extends AtLeastOne & AtMost<? super N>>
Point vertex(K k);
}
The flexibility of Concrete Numbers also allows us to express the more conventional 0-based indexing by incorporating the NMinus1 type.
/**
* An N-sided Polygon
*/
public interface Polygon<N extends ConcreteNumber & AtLeastThree> {
/**
* Returns the number of sides
*/
N numSides();
/**
* Gets the vertex at the given index (0-indexed)
*/
<K extends AtLeastZero & NMinus1<? extends AtMost<? super N>>>
Point vertex(K k);
}
Let’s implement Polygon to see how these constraints work in practice.
/**
* A Triangle is a Polygon with 3 sides
*/
public class Triangle implements Polygon<Three> {
private Point[] vertices;
public Triangle(Point a, Point b, Point c) {
this.vertices = new Point[] { a, b, c };
}
@Override
public Three numSides() {
return THREE;
}
@Override
public <K extends AtLeastZero & NMinus1<? extends AtMost<? super Three>>>
Point vertex(K k) {
return vertices[k.intValue()];
}
}
Triangle t = new Triangle(p0, p1, p2);
t.vertex(ZERO); // ✅ OK
t.vertex(ONE); // ✅ OK
t.vertex(TWO); // ✅ OK
t.vertex(THREE); // ❌ Compile-time error
Concrete Numbers uses intuitive naming for the predefined singleton types representing integers. For values from -1 through 20, the types are spelled out in words (e.g., MinusOne, Zero, One, ..., Twenty).
For values above 20, the naming follows a numeric prefix pattern: N21, N22, ..., N255. This provides a consistent and compact way to reference higher values without requiring verbose or nonstandard naming.
These class names are case-sensitive and follow Java identifier conventions, allowing for clean imports and consistent usage throughout your codebase.
In addition to using ConcreteNumber types for compile-time guarantees, the library also provides utility methods for streaming ranges of concrete numbers.
The static methods count0ToN, count1ToN, and countMToN allow you to generate type-safe streams of concrete numbers within compile-time bounded ranges:
ConcreteNumber.count0ToN(FIVE).forEach(n -> {
// n is a ConcreteNumber, Zero to Five inclusive
});
ConcreteNumber.countMToN(THREE, SIX).forEach(n -> {
// n is a ConcreteNumber, Three to Six inclusive
});
These methods produce streams where each element remains a concrete number, preserving type safety throughout iteration. You don’t need to downcast or unwrap values until you choose to.
For even more concise syntax, every concrete number inherits the default method countTo(Integer i) from its parent interface Integer. This lets you write elegant, conventional-style loops while still using concrete numbers:
ZERO.countTo(TEN).forEach(i -> {
// i is a ConcreteNumber, Zero to Ten inclusive
});
This hybrid approach—mixing type safety and ergonomic iteration—is particularly useful when generating indexed sequences or building structures incrementally.
Concrete Numbers requires **Java 17 or newer** and has no external dependencies, making it easy to adopt within any modern Java project.
Concrete Numbers is published to Maven Central. To include it in your project, add the following dependency to your pom:
<dependency>
<groupId>org.zfcj</groupId>
<artifactId>ConcreteNumbers</artifactId>
<version>1.0</version>
</dependency>