/*******************************************************************************
* Companion code for the book "Introduction to Software Design with Java",
* 2nd edition by Martin P. Robillard.
*
* Copyright (C) 2022 by Martin P. Robillard
*
* This code is licensed under a Creative Commons
* Attribution-NonCommercial-NoDerivatives 4.0 International License.
*
* See http://creativecommons.org/licenses/by-nc-nd/4.0/
*
*******************************************************************************/
package e2.chapter4;
/**
* Implementation of a playing card. This class yields immutable objects.
* This version of the class shows an application of the Flyweight design
* pattern where the flyweight store is pre-initialized.
*/
public class Card {
/*
* Implements the flyweight store as a bidimensional array. The
* first dimension indexes the suits by the ordinal value of their enumerated type,
* and the second dimension, the ranks. For example, to retrieve the two of clubs,
* we access CARDS[Suit.CLUBS.ordinal()][Rank.TWO.ordinal()].
*/
private static final Card[][] = new Card[Suit.values().length][Rank.values().length];
private final Rank aRank;
private final Suit aSuit;
// Initialization of the flyweight store
{
for( Suit suit : Suit.values() ) {
for( Rank rank : Rank.values() ) {
CARDS[suit.ordinal()][rank.ordinal()] = new Card(rank, suit);
}
}
}
// Private constructor
Card( Rank pRank, Suit pSuit) {
aRank = pRank;
aSuit = pSuit;
}
/**
* @param pRank The rank of the requested card.
* @param pSuit The suit of the requested card.
* @return The unique Card instance with pRank and pSuit
* @pre pRank != null && pSuit != null
*/
public static Card (Rank pRank, Suit pSuit) {
assert pRank != null && pSuit != null;
return CARDS[pSuit.ordinal()][pRank.ordinal()];
}
/**
* @return The rank of the card.
*/
public Rank getRank() {
return aRank;
}
/**
* @return The suit of the card.
*/
public Suit getSuit() {
return aSuit;
}
@Override
public String toString() {
return String.format("%s of %s", aRank, aSuit);
}
}
String
class represents character strings. All
string literals in Java programs, such as "abc"
, are
implemented as instances of this class.
String
class represents character strings. All
string literals in Java programs, such as "abc"
, are
implemented as instances of this class.
Strings are constant; their values cannot be changed after they are created. String buffers support mutable strings. Because String objects are immutable they can be shared. For example:
String str = "abc";
is equivalent to:
char data[] = {'a', 'b', 'c'}; String str = new String(data);
Here are some more examples of how strings can be used:
System.out.println("abc"); String cde = "cde"; System.out.println("abc" + cde); String c = "abc".substring(2, 3); String d = cde.substring(1, 2);
The class String
includes methods for examining
individual characters of the sequence, for comparing strings, for
searching strings, for extracting substrings, and for creating a
copy of a string with all characters translated to uppercase or to
lowercase. Case mapping is based on the Unicode Standard version
specified by the Character
class.
The Java language provides special support for the string concatenation operator ( + ), and for conversion of other objects to strings. For additional information on string concatenation and conversion, see The Java Language Specification.
Unless otherwise noted, passing a null
argument to a constructor
or method in this class will cause a NullPointerException
to be
thrown.
A String
represents a string in the UTF-16 format
in which supplementary characters are represented by surrogate
pairs (see the section Unicode
Character Representations in the Character
class for
more information).
Index values refer to char
code units, so a supplementary
character uses two positions in a String
.
The String
class provides methods for dealing with
Unicode code points (i.e., characters), in addition to those for
dealing with Unicode code units (i.e., char
values).
Unless otherwise noted, methods for comparing Strings do not take locale
into account. The Collator
class provides methods for
finer-grain, locale-sensitive String comparison.
javac
compiler
may implement the operator with StringBuffer
, StringBuilder
,
or java.lang.invoke.StringConcatFactory
depending on the JDK version. The
implementation of string conversion is typically through the method toString
,
defined by Object
and inherited by all classes in Java.The result might look something like this (remember that arrays are initialized
with null
values):
public static Card get(Rank pRank, Suit pSuit)
{
assert pRank != null && pSuit != null;
Card card = CARDS[pSuit.ordinal()][pRank.ordinal()];
if (card == null)
{
card = new Card(pRank, pSuit);
// don't forget to store the new object!
CARDS[pSuit.ordinal()][pRank.ordinal()] = card;
}
return card;
}
The result might look something like this (remember that arrays are initialized
with null
values):
public static Card get(Rank pRank, Suit pSuit)
{
assert pRank != null && pSuit != null;
Card card = CARDS[pSuit.ordinal()][pRank.ordinal()];
if (card == null)
{
card = new Card(pRank, pSuit);
// don't forget to store the new object!
CARDS[pSuit.ordinal()][pRank.ordinal()] = card;
}
return card;
}
Declaring the constructor to be private forces client code to obtain instances of the class using the access method.
Declaring the constructor to be private forces client code to obtain instances of the class using the access method.
This is the access method for this flyweight class. Because the application of the Flyweight pattern uses greedy initialization, the method just returns an instance which we know already exists in the store. When the lazy initialization variant is used, this method would need to as well. Note that the access method must be a static method.
This is the access method for this flyweight class. Because the application of the Flyweight pattern uses greedy initialization, the method just returns an instance which we know already exists in the store. When the lazy initialization variant is used, this method would need to as well. Note that the access method must be a static method.
In this application of the Flyweight pattern, I chose a greedy initialization of the flyweight objects. Once the class
is loaded into the JVM, all the Card
instances are created at once. An alternative to this scheme is lazy initialization,
where cards are initialized on-demand in the . With this alternative, the static initialization block would
be removed.
In this application of the Flyweight pattern, I chose a greedy initialization of the flyweight objects. Once the class
is loaded into the JVM, all the Card
instances are created at once. An alternative to this scheme is lazy initialization,
where cards are initialized on-demand in the . With this alternative, the static initialization block would
be removed.
Chapter 4, insight #4
Consider declaring instance variables final
whenever possible
Chapter 4, insight #4
Consider declaring instance variables final
whenever possible
In the Flyweight pattern, the method that returns the unique flyweight object requested. In this class it's the get
method.
In the Flyweight pattern, the method that returns the unique flyweight object requested. In this class it's the get
method.
This implementation of the flyweight store is just one alternative among many.
Dictionary-based approaches are also possible. In particular, I would consider
a two-level map using EnumMap
to be a good choice. A design based on EnumMap
is more robust than an array
because, as opposed to arrays, it is less easy to make an indexing error.
This implementation of the flyweight store is just one alternative among many.
Dictionary-based approaches are also possible. In particular, I would consider
a two-level map using EnumMap
to be a good choice. A design based on EnumMap
is more robust than an array
because, as opposed to arrays, it is less easy to make an indexing error.
In Java, a static initialization block executes once, when the class is first loaded into the running application. It is typically used to initialized static fields with code that does not need to be used anywhere else. If the code can be reused, it is better to put it in a static private method.
In Java, a static initialization block executes once, when the class is first loaded into the running application. It is typically used to initialized static fields with code that does not need to be used anywhere else. If the code can be reused, it is better to put it in a static private method.