This article examines extending Java Enums used as property values
within JavaBeans combined with the Java Stream API to create and extend
fluent interfaces. From
Wikipedia:
The Java Stream API provides the method chaining; this article will examine
how Java Enums may be extended (specifically to implement
Predicate to contribute to a fluent interface’s quality of
being similar to “written prose.” For example:
public enum Rank implements Predicate<Card> {
JOKER, ACE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN, JACK, QUEEN, KING;
...
}
public enum Suit implements Predicate<Card> {
CLUBS, DIAMONDS, HEARTS, SPADES;
...
}
public class Card {
private final Suit suit;
private final Rank rank;
...
}
will allow support for fluent expressions like:
List<Card> hand = ...;
boolean areAllHearts =
hand.stream()
.filter(Suit.HEARTS)
.allMatch();
boolean hasKingOfSpades =
hand.stream()
.filter(Rank.KING.and(Suit.SPADES))
.anyMatch();
Complete javadoc is provided.
Extending Enums
Enum values are constants but they are also subclasses of Enum
and those subclass implementations may have custom fields and methods. For
example, java.time.DayOfWeek implements the
TemporalAccessor and
TemporalAdjuster interfaces so DayOfWeek provides
implementation methods for those interface methods. The Suit
implementation demonstrates how subclass fields may be defined and set by
defining a custom constructor.
public enum Suit {
CLUBS(Color.BLACK, "\u2667"),
DIAMONDS(Color.RED, "\u2662"),
HEARTS(Color.RED, "\u2661"),
SPADES(Color.BLACK, "\u2664");
...
private final Color color;
private final String string;
@ConstructorProperties({ "color", EMPTY })
private Suit(Color color, String string) {
this.color = color;
this.string = string;
}
public Color getColor() { return color; }
@Override
public String toString() { return string; }
...
}
Implementing Predicate
The key to contributing to the fluent interface provided by
Stream is for the Enum subclass to implement
Predicate. Of course, that Predicate must test the bean
that the Enum is a property for. For example, Rank and
Suit must test Card:
public enum Rank implements Predicate<Card> {
...
@Override
public boolean test(Card card) {
return is(this).test(card.getRank());
}
...
public static Predicate<Rank> is(Rank rank) {
return t -> Objects.equals(rank, t);
}
...
}
public enum Suit implements Predicate<Card> {
...
@Override
public boolean test(Card card) {
return is(this).test(card.getSuit());
}
...
public static Predicate<Suit> is(Suit rank) {
return t -> Objects.equals(rank, t);
}
...
}
Note: Both implementations provide static is methods to further contribute
to the “fluency” of the API.
To re-inforce the fact that Rank and Suit are
bean properties of Card, Rank and Suit are implemented as
inner classes of Card.
Fluent Implementation - Poker Hand Ranking
To demonstrate the “fluency” of the API, a Poker Ranking
Enum may be defined.
public enum Ranking implements Predicate<List<Card>> {
Empty(0, Collection::isEmpty),
HighCard(1, t -> true),
Pair(2, Rank.SAME),
TwoPair(4, Pair.with(Pair)),
ThreeOfAKind(3, Rank.SAME),
Straight(5, Rank.SEQUENCE),
Flush(5, Suit.SAME),
FullHouse(5, ThreeOfAKind.with(Pair)),
FourOfAKind(4, Rank.SAME),
StraightFlush(5, holding(ACE, KING).negate().and(Straight).and(Flush)),
RoyalFlush(5, holding(ACE, KING).and(Straight).and(Flush)),
FiveOfAKind(5, Rank.SAME);
private final int required;
private final Predicate<List<Card>> is;
private Ranking(int required, Predicate<List<Card>> is) {
this.required = required;
this.is = Objects.requireNonNull(is);
}
...
public int required() { return required; }
...
@Override
public boolean test(List<Card> list) {
return (list.size() >= required()
&& is.test(subListTo(list, required())));
}
...
}
To complete the Poker “domain specific language” the Rank and
Suit types must provide static Predicate
SAME fields,
...
private static <T> Predicate<List<T>> same(Function<T,Predicate<T>> mapper) {
return t -> ((! t.isEmpty()) && t.stream().allMatch(mapper.apply(t.get(0))));
}
...
public enum Rank implements Predicate<Card> {
...
public static final Predicate<List<Card>> SAME = same(Card::getRank);
...
}
...
public enum Suit implements Predicate<Card> {
...
public static final Predicate<List<Card>> SAME = same(Card::getSuit);
...
}
...
Rank must provide a static SEQUENCE
Predicate,
...
private static <T,R> List<R> listOf(Collection<T> collection,
Function<T,R> mapper) {
return collection.stream().map(mapper).collect(Collectors.toList());
}
...
public enum Rank implements Predicate<Card> {
...
public static final List<Rank> ACE_HIGH =
unmodifiableList(asList(JOKER,
TWO, THREE, FOUR, FIVE,
SIX, SEVEN, EIGHT, NINE,
TEN, JACK, QUEEN, KING, ACE));
public static final List<Rank> ACE_LOW =
unmodifiableList(asList(values()));
private static final Map<String,Rank> MAP;
private static final List<List<Rank>> SEQUENCES;
static {
TreeMap<String,Rank> map =
new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
for (Rank rank : values()) {
map.put(rank.name(), rank);
map.put(rank.toString(), rank);
}
MAP = unmodifiableMap(map);
List<Rank> high = new ArrayList<>(Rank.ACE_HIGH);
List<Rank> low = new ArrayList<>(Rank.ACE_LOW);
reverse(high);
reverse(low);
SEQUENCES =
unmodifiableList(asList(unmodifiableList(high),
unmodifiableList(low)));
}
...
public static final Predicate<List<Card>> SEQUENCE =
t -> ((! t.isEmpty()) && sequence(listOf(t, Card::getRank)));
private static boolean sequence(List<Rank> list) {
return (SEQUENCES.stream().anyMatch(t -> indexOfSubList(t, list) >= 0));
}
...
}
...
and Ranking must provide static with and holding methods:
public enum Ranking implements Predicate<List<Card>> {
...
private Predicate<List<Card>> with(Predicate<List<Card>> that) {
return t -> test(t) && that.test(subListFrom(t, required()));
}
private static <T> Predicate<List<T>> holding(int count, Predicate<List<T>> predicate) {
return t -> (t.isEmpty() || predicate.test(subListTo(t, count)));
}
@SafeVarargs
@SuppressWarnings({ "varargs" })
private static <T> Predicate<List<T>> holding(Predicate<T>... array) {
return holding(Stream.of(array).collect(Collectors.toList()));
}
private static <T> Predicate<List<T>> holding(List<Predicate<T>> list) {
return t -> ((list.isEmpty() || t.isEmpty())
|| (list.get(0).test(t.get(0))
&& (holding(subListFrom(list, 1)).test(subListFrom(t, 1)))));
}
private static <T> List<T> subListTo(List<T> list, int to) {
return list.subList(0, Math.min(to, list.size()));
}
private static <T> List<T> subListFrom(List<T> list, int from) {
return list.subList(from, list.size());
}
...
}
The Ranking Predicate Enum combined
with the Combinations Stream introduced in
this article may be
used to test for a specific Poker hand.
List<Card> hand = ...;
int size = Math.min(5, hand.size());
boolean isStraight =
Combinations.of(size, hand)
.filter(Ranking.STRAIGHT)
.anyMatch();
While this implementation is complete, the Combinations Stream
provides an of(int,int,Predicate<List<T>>,Collection<T>) method that
allows the specification of a Predicate that when it
evaluates to false will stop iterating over that branch. The
Ranking Enum may be extended to provide that
Predicate by providing a possible() method:
public enum Ranking implements Predicate<List<Card>> {
Empty(0, null, Collection::isEmpty),
HighCard(1, t -> true, t -> true),
Pair(2, Rank.SAME, Rank.SAME),
TwoPair(4, holding(2, Rank.SAME), Pair.with(Pair)),
ThreeOfAKind(3, Rank.SAME, Rank.SAME),
Straight(5, Rank.SEQUENCE, Rank.SEQUENCE),
Flush(5, Suit.SAME, Suit.SAME),
FullHouse(5, holding(3, Rank.SAME), ThreeOfAKind.with(Pair)),
FourOfAKind(4, Rank.SAME, Rank.SAME),
StraightFlush(5,
holding(ACE, KING).negate().and(Rank.SEQUENCE).and(Suit.SAME),
holding(ACE, KING).negate().and(Straight).and(Flush)),
RoyalFlush(5,
holding(ACE, KING).and(Rank.SEQUENCE).and(Suit.SAME),
holding(ACE, KING).and(Straight).and(Flush)),
FiveOfAKind(5, Rank.SAME, Rank.SAME);
private final int required;
private final Predicate<List<Card>> possible;
private final Predicate<List<Card>> is;
private Ranking(int required, Predicate<List<Card>> possible, Predicate<List<Card>> is) {
this.required = required;
this.possible = possible;
this.is = Objects.requireNonNull(is);
}
...
public Predicate<List<Card>> possible() {
return t -> (possible == null || possible.test(subListTo(t, required())));
}
...
}
which in combination with the
Combinations.of(int,int,Predicate<List<T>>,Collection<T>) method will
optimize the search for ThreeOfAKind by escaping a branch if the first
Cards are not the same Rank, Straight if the
first Cards are not a sequence, etc.
List<Card> hand = ...;
int size = Math.min(5, hand.size());
boolean isStraight =
Combinations.of(size, size, STRAIGHT.possible(), hand)
.filter(Ranking.STRAIGHT)
.anyMatch();
The logic in Ranking.find(Collection<Card>) and Evaluator
demonstrate more sophisticated logic.
Summary
Implementing Predicate(BEAN) for BEAN property types
(including Enum) will contribute to making an API “fluent” when
used in combination with Stream.