Allen D. Ball
Posted on October 29, 2019
This article examines extending Java Enum
s 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 Enum
s 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 Card
s are not the same Rank
, Straight
if the first Card
s 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
.
Posted on October 29, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
July 12, 2022