Most of programming is handling data in various formats. In this chapter, we will introduce Java’s support for handling two big classes of data—text and numbers. The second half of the chapter will focus on handling date and time information. This is of particular interest as Java 8 ships a completely new API for handling date and time. We cover this new interface in some depth, before finishing the chapter by briefly discussing Java’s original date and time API.
Many applications are still using the legacy APIs, so developers need to be aware of the old way of doing things, but the new APIs are so much better that we recommend converting as soon as possible. Before we get to those more complex formats, let’s get underway by talking about textual data and strings.
We have already met Java’s strings on many occasions. They consist of sequences of Unicode characters, and are represented as instances of the String
class. Strings are one of the most common types of data that Java programs process (a claim you can investigate for yourself by using the jmap
tool that we’ll meet in Chapter 13).
In this section, we’ll meet the String
class in some more depth, and understand why it is in a rather unique position within the Java language. Later in the section, we’ll introduce regular expressions, a very common abstraction for searching text for patterns (and a classic tool in the programmer’s arsenal).
The String
class is handled in a somewhat special way by the Java language. This is because, despite not being a primitive type, strings are so common that it makes sense for Java to have a number of special syntax features designed to make handling strings easy. Let’s look at some examples of special syntax features for strings that Java provides.
As we saw in Chapter 2, Java allows a sequence of characters to be placed in double quotes to create a literal string object. Like this:
String
pet
=
"Cat"
;
Without this special syntax, we would have to write acres of horrible code like this:
char
[]
pullingTeeth
=
{
'C'
,
'a'
,
't'
};
String
pet
=
new
String
(
pullingTeeth
);
This would get tedious extremely quickly, so it’s no surprise that Java, like all modern programming languages, provides a simple string literal syntax. The string literals are perfectly sound objects, so code like this is completely legal:
System
.
out
.
println
(
"Dog"
.
length
());
This method is defined on Object
, and is designed to allow easy conversion of any object to a string. This makes it easy to print out any object, by using the method System.out.println()
. This method is actually PrintStream::println
because System.out
is a static field of type PrintStream
. Let’s see how this method is defined:
public
void
println
(
Object
x
)
{
String
s
=
String
.
valueOf
(
x
);
synchronized
(
this
)
{
(
s
);
newLine
();
}
}
This creates a new string by using the static method String::valueOf()
:
public
static
String
valueOf
(
Object
obj
)
{
return
(
obj
==
null
)
?
"null"
:
obj
.
toString
();
}
valueOf()
method is used instead of toString()
directly, to avoid a NullPointerException
in the case where obj
is null
.This construction means that toString()
is always available for any object, and this turns out to come in very handy for another major syntax feature that Java provides—string concatenation.
Java has a language feature where we can create new strings by “adding” the characters from one string onto the end of another. This is called string concatenation and uses the operator +
. It works by first creating a “working area” in the form of a StringBuilder
object that contains the same sequence of characters as the original string.
The builder object is then updated and the characters from the additional string are added onto the end. Finally, toString()
is called on the StringBuilder
object (which now contains the characters from both strings). This gives us a new string with all the characters in it. All of this code is created automatically by javac
whenever we use the +
operator to concatenate strings.
The concatenation process returns a completely new String
object, as we can see in this example:
String
s1
=
"AB"
;
String
s2
=
"CD"
;
String
s3
=
s1
;
System
.
out
.
println
(
s1
==
s3
);
// Same object?
s3
=
s1
+
s2
;
System
.
out
.
println
(
s1
==
s3
);
// Still same?
System
.
out
.
println
(
s1
);
System
.
out
.
println
(
s3
);
The concatentation example directly shows that the +
operator is not altering (or mutating) s1
in place. This is an example of a more general principle: Java’s strings are immutable. This means that once the characters that make up the string have been chosen and the String
object has been created, the String
cannot be changed. This is an important language principle in Java, so let’s look at it in a little more depth.
In order to “change” a string, as we saw when we discussed string concatenation, we actually need to create an intermediate StringBuilder
object to act as a temporary scratch area, and then call toString()
on it, to bake it into a new instance of String
. Let’s see how this works in code:
String
pet
=
"Cat"
;
StringBuilder
sb
=
new
StringBuilder
(
pet
);
sb
.
append
(
"amaran"
);
String
boat
=
sb
.
toString
();
System
.
out
.
println
(
boat
);
Code like this is equivalent to what javac
would generate if, instead, we had written:
String
pet
=
"Cat"
;
String
boat
=
pet
+
"amaran"
;
System
.
out
.
println
(
boat
);
Of course, as well as being used under the hood by javac
, the StringBuilder
class can also be used directly in application code, as we’ve seen.
StringBuilder
Java also has a StringBuffer
class. This comes from the oldest versions of Java, and should not be used for new development—use StringBuilder
instead, unless you really need to share the construction of a new string between multiple threads.String immutability is an extremely useful language feature. For example, suppose the +
changed a string instead of creating a new one; then whenever any thread concatenated two strings together, all other threads would also see the change. This is unlikely to be a useful behavior for most programs, and so immutability makes good sense.
We have already met the hashCode()
method in Chapter 5, where we described the contract that the method must satisfy. Let’s take a look at the JDK source code and see how the method String::hashCode()
is defined:
public
int
hashCode
()
{
int
h
=
hash
;
if
(
h
==
0
&&
value
.
length
>
0
)
{
char
val
[]
=
value
;
for
(
int
i
=
0
;
i
<
value
.
length
;
i
++)
{
h
=
31
*
h
+
val
[
i
];
}
hash
=
h
;
}
return
h
;
}
The field hash
holds the hash code of the string, and the field value
is a char[]
that holds the characters that actually make up the string. As we can see from the code, the hash is computed by looping over all the characters of the string. It therefore takes a number of machine instructions proportional to the number of characters in the string. For very large strings this could take a bit of time. Rather than pre-compute the hash value, Java only calculates it when it is needed.
When the method runs, the hash is computed by stepping through the array of characters. At the end of the array, we exit the for
loop and write the computed hash back into the field hash
. Now, when this method is called again, the value has already been computed, and so we can just use the cached value. So subsequent calls to hashCode()
return immediately.
All of the fields of the String
class are final, except for hash
. So Java’s strings are not, strictly speaking, immutable. However, because the hash
field is just a cache of a value that is deterministically computed from the other fields, which are all immutable, then provided String
has been coded correctly, it will behave as if it was immutable. Classes that have this property are called effectively immutable—they are quite rare in practice, and working programmers can usually ignore the distinction between truly immutable and effectively immutable data.
Java has support for regular expressions (often shortened to regex or regexp). These are a representation of a search pattern used to scan and match text. A regex is a sequence of characters that we want to search for. They can be very simple—for example, abc
means that we’re looking for a, followed immediately by b, followed immediately by c, anywhere within the text we’re searching through. Note that a search pattern may match an input text in zero, one, or more places.
The simplest regexs are just sequences of literal characters, like abc
. However, the language of regexs can express more complex and subtle ideas than just literal sequences. For example, a regex can represent patterns to match like:
A numeric digit
Any letter
Any number of letters, which must all be in the range a to j but can be upper- or lowercase
a followed by any four characters, followed by b
The syntax we use to write regular expressions is simple, but because we can build up complex patterns, it is often possible to write an expression that does not implement precisely what we wanted. When using regexs, it is very important to always test them fully. This should include both test cases that should pass and cases that should fail.
To express these more complex patterns, regexs use metacharacters. These are special characters that indicate that special processing is required. This can be thought of as similar to the use of the *
character in the Unix or Windows shell. In those circumstances, it is understood that the *
is not to be interpreted literally but instead means “anything.” If we wanted to list all the Java source files in the current directory on Unix, we would issue the command:
ls
*.
java
The metacharacters of regexs are similar, but there are far more of them, and they are far more flexible than the set available in shells. They also have different meanings than they do in shell scripts, so don’t get confused.
Let’s meet a couple of examples. Suppose we want to have a spell-checking program that is relaxed about the difference in spelling between British and American English. This means that honor and honour should both be accepted as valid spelling choices. This is easy to do with regular expressions.
Java uses a class called Pattern
(from the package java.util.regex
) to represent a regex. This class can’t be directly instantiated, however. Instead, new instances are created by using a static factory method, compile()
. From a pattern, we then derive a Matcher
for a particular input string that we can use to explore the input string. For example, let’s examine a bit of Shakespeare from the play Julius Caesar:
Pattern
p
=
Pattern
.
compile
(
"honou?r"
);
String
caesarUK
=
"For Brutus is an honourable man"
;
Matcher
mUK
=
p
.
matcher
(
caesarUK
);
String
caesarUS
=
"For Brutus is an honorable man"
;
Matcher
mUS
=
p
.
matcher
(
caesarUS
);
System
.
out
.
println
(
"Matches UK spelling? "
+
mUK
.
find
());
System
.
out
.
println
(
"Matches US spelling? "
+
mUS
.
find
());
Matcher
as it has a method called matches()
. However, this method indicates whether the pattern can cover the entire input string. It will return false
if the pattern only starts matching in the middle of the string.The last example introduces our first regex metacharacter ?
, in the pattern honou?r
. This means “the preceding character is optional”—so both honour
and honor
will match. Let’s look at another example. Suppose we want to match both minimize and minimise (the latter spelling is more common in British English). We can use square brackets to indicate that any character from a set (but only one alternative) []
can be used—like this:
Pattern
p
=
Pattern
.
compile
(
"minimi[sz]e"
);
Table 9-1 provides an expanded list of metacharacters available for Java regexs.
Metacharacter | Meaning | Notes |
---|---|---|
|
Optional character—zero or one instance |
|
|
Zero or more of preceding character |
|
|
One or more of preceding character |
|
|
Between |
|
|
A digit |
|
|
A nondigit character |
|
|
A word character |
Digits, letters, and |
|
A nonword character |
|
|
A whitespace character |
|
|
A nonwhitespace character |
|
|
Newline character |
|
|
Tab character |
|
|
Any single character |
Does not include newline in Java |
|
Any character contained with the brackets |
Called a character class |
|
Any character not contained with the brackets |
Called a negated character class |
|
Build up a group of pattern elements |
Called a group (or capturing group) |
|
Define alternative possbilities |
Implements logical |
|
Start of string |
|
|
End of string |
There are a few more, but this is the basic list, and from this, we can construct more complex expressions for matching such as the examples given earlier in this section:
// Note that we have to use \ because we need a literal
// and Java uses a single as an escape character
String
pStr
=
"\d"
;
// A numeric digit
String
text
=
"Apollo 13"
;
Pattern
p
=
Pattern
.
compile
(
pStr
);
Matcher
m
=
p
.
matcher
(
text
);
System
.
out
.
(
pStr
+
" matches "
+
text
+
"? "
+
m
.
find
());
System
.
out
.
println
(
" ; match: "
+
m
.
group
());
pStr
=
"[a..zA..Z]"
;
//Any letter
p
=
Pattern
.
compile
(
pStr
);
m
=
p
.
matcher
(
text
);
System
.
out
.
(
pStr
+
" matches "
+
text
+
"? "
+
m
.
find
());
System
.
out
.
println
(
" ; match: "
+
m
.
group
());
// Any number of letters, which must all be in the range 'a' to 'j'
// but can be upper- or lowercase
pStr
=
"([a..jA..J]*)"
;
p
=
Pattern
.
compile
(
pStr
);
m
=
p
.
matcher
(
text
);
System
.
out
.
(
pStr
+
" matches "
+
text
+
"? "
+
m
.
find
());
System
.
out
.
println
(
" ; match: "
+
m
.
group
());
text
=
"abacab"
;
pStr
=
"a....b"
;
// 'a' followed by any four characters, followed by 'b'
p
=
Pattern
.
compile
(
pStr
);
m
=
p
.
matcher
(
text
);
System
.
out
.
(
pStr
+
" matches "
+
text
+
"? "
+
m
.
find
());
System
.
out
.
println
(
" ; match: "
+
m
.
group
());
Let’s conclude our quick tour of regular expressions by meeting a new method that was added to Pattern
as part of Java 8: asPredicate()
. This method is present to allow us to easily bridge from regular expressions to the Java Collections and their new support for lambda expressions.
For example, suppose we have a regex and a collection of strings. It’s very natural to ask the question: “Which strings match against the regex?” We do this by using the filter idiom, and by converting the regex to a Predicate
using the helper method, like this:
String
pStr
=
"\d"
;
// A numeric digit
Pattern
p
=
Pattern
.
compile
(
pStr
);
String
[]
inputs
=
{
"Cat"
,
"Dog"
,
"Ice-9"
,
"99 Luftballoons"
};
List
<
String
>
ls
=
Arrays
.
asList
(
inputs
);
List
<
String
>
containDigits
=
ls
.
stream
()
.
filter
(
p
.
asPredicate
())
.
collect
(
Collectors
.
toList
());
System
.
out
.
println
(
containDigits
);
Java’s built-in support for text processing is more than adequate for the majority of text processing tasks that business applications normally require. More advanced tasks, such as the search and processing of very large data sets, or complex parsing (including formal grammars) are outside the scope of this book, but Java has a large ecosystem of helpful libraries and bindings to specialized technologies for text processing and analysis.
In this section, we will discuss Java’s support for numeric types in some more detail. In particular, we’ll discuss the two’s complement representation of integral types that Java uses. We’ll introduce floating-point representations, and touch on some of the problems they can cause. We’ll work through examples that use some of Java’s library functions for standard mathematical operations.
Java’s integer types are all signed, as we first mentioned in “Primitive Data Types”. This means that all integer types can represent both positive and negative numbers. As computers work with binary, this means that the only really logical way to represent this is to split the possible bit patterns up and use half of them to represent negative numbers.
Let’s work with Java’s byte
type to investigate how Java represents integers. This has 8 bits, so can represent 256 different numbers (i.e., 128 negative and 128 non-negative numbers). It’s logical to use the pattern 0b0000_0000
to represent zero (recall that Java has the syntax 0b<binary digits>
to represent numbers as binary), and then it’s easy to figure out the bit patterns for the positive numbers:
byte
b
=
0
b0000_0001
;
System
.
out
.
println
(
b
);
// 1
b
=
0
b0000_0010
;
System
.
out
.
println
(
b
);
// 2
b
=
0
b0000_0011
;
System
.
out
.
println
(
b
);
// 3
// ...
b
=
0
b0111_1111
;
System
.
out
.
println
(
b
);
// 127
When we set the first bit of the byte, the sign should change (as we have now used up all of the bit patterns that we’ve set aside for non-negative numbers). So the pattern 0b1000_0000
should represent some negative number—but which one?
1
, then the number being represented is negative.Consider the bit pattern consisting of all set bits: 0b1111_1111
. If we add 1
to this number, then the result will overflow the 8 bits of storage that a byte
has, resulting in 0b1_0000_0000
. If we want to constrain this to fit within the byte
data type, then we should ignore the overflow, so this becomes 0b0000_0000
- zero. It is therefore natural to adopt the representation that “all set bits is -1
.” This allows for natural arithmetic behavior, like this:
b
=
(
byte
)
0
b1111_1111
;
// -1
System
.
out
.
println
(
b
);
b
++;
System
.
out
.
println
(
b
);
b
=
(
byte
)
0
b1111_1110
;
// -2
System
.
out
.
println
(
b
);
b
++;
System
.
out
.
println
(
b
);
Finally, let’s look at the number that 0b1000_0000
represents. It’s the most negative number that the type can represent, so for byte
:
b
=
(
byte
)
0
b1000_0000
;
System
.
out
.
println
(
b
);
// -128
This representation is called two’s complement, and is the most common representation for signed integers. To use it effectively, there are only two points that you need to remember:
A bit pattern of all 1’s is the representation for -1.
If the high bit is set, the number is negative.
Java’s other integer types (short
, int
, and long
) behave in very similar ways but with more bits in their representation. The char
datatype is different because it represents a Unicode character, but in some ways behaves as an unsigned 16-bit numeric type. It is not normally regarded as an integer type by Java programmers.
Computers represent numbers using binary. We’ve seen how Java uses the two’s complement representation for integers. But what about fractions or decimals? Java, like almost all modern programming languages, represents them using floating-point arithmetic. Let’s take a look at how this works, first in base-10 (regular decimal) and then in binary. Java defines the two most important mathematical constants, e
and π
as constants in java.lang.Math
like this:
public
static
final
double
E
=
2.7182818284590452354
;
public
static
final
double
PI
=
3.14159265358979323846
;
Of course, these constants are actually irrational numbers and cannot be precisely expressed as a fraction, or by any finite decimal number.12 This means that whenever we try to represent them in a computer, there is always rounding error. Let’s suppose we only want to deal with eight digits of π, and we want to represent the digits as a whole number. We can use a representation like this:
This starts to suggest the basis of how floating-point numbers work. We use some of the bits to represent the significant digits (314159265
, in our example) of the number and some bits to represent the exponent of the base (-8
, in our example). The collection of significant digits is called the significand and the exponent describes whether we need to shift the significand up or down to get to the desired number.
Of course, in the examples we’ve met until now, we’ve been working in base-10. Computers use binary, so we need to use this as the base in our floating-point examples. This introduces some additional complications.
0.1
cannot be expressed as a finite sequence of binary digits. This means that virtually all calculations that humans care about will lose precision when performed in floating point, and rounding error is essentially inevitable.Let’s look at an example that shows the rounding problem:
double
d
=
0.3
;
System
.
out
.
println
(
d
);
// Special-cased to avoid ugly representation
double
d2
=
0.2
;
// Should be -0.1 but prints -0.09999999999999998
System
.
out
.
println
(
d2
-
d
);
The standard that describes floating-point arithmetic is IEEE-754 and Java’s support for floating point is based on that standard. The standard uses 24 binary digits for standard precision and 53 binary digits for double precision.
As we mentioned briefly in Chapter 2, Java can be more accurate than the standard requires, by using hardware features if they are available. In extremely rare cases, usually where very strict compatability with other (possibly older) platforms is required, this behavior can be switched off by using strictfp
to mandate perfect compliance with the IEEE-754 standard. This is almost never necessary and the vast majority of programmers will never need to use (or even see) this keyword.
Rounding error is a constant source of headaches for programmers who work with floating-point numbers. In response, Java has a class java.math.BigDecimal
that provides arbitrary precision arithmetic, in a decimal representation. This works around the problem of 0.1
not having a finite representation in binary, but there are still some edge conditions when converting to or from Java’s primitive types, as you can see:
double
d
=
0.3
;
System
.
out
.
println
(
d
);
BigDecimal
bd
=
new
BigDecimal
(
d
);
System
.
out
.
println
(
bd
);
bd
=
new
BigDecimal
(
"0.3"
);
System
.
out
.
println
(
bd
);
However, even with all arithmetic performed in base-10, there are still numbers, such as 1/3
, that do not have a terminating decimal representation. Let’s see what happens when we try to represent such numbers using BigDecimal
:
bd
=
new
BigDecimal
(
BigInteger
.
ONE
);
bd
.
divide
(
new
BigDecimal
(
3.0
));
System
.
out
.
println
(
bd
);
// Should be 1/3
As BigDecimal
can’t represent 1/3
precisely, the call to divide()
blows up with ArithmeticException
. When working with BigDecimal
, it is therefore necessary to be acutely aware of exactly which operations could result in a nonterminating decimal result. To make matters worse, ArithmeticException
is an unchecked, runtime exception and so the Java compiler does not even warn about possible exceptions of this type.
As as a final note on floating-point numbers, the paper “What Every Computer Scientist Should Know About Floating-Point Arithmetic” by David Goldberg should be considered essential further reading for all professional programmers. It is easily and freely obtainable on the Internet.
To conclude this look at Java’s support for numeric data and math, let’s take a quick tour of the standard library of functions that Java ships with. These are mostly static helper methods that are located on the class java.lang.Math
and include functions like:
abs()
Returns the absolute value of a number. Has overloaded forms for various primitive types.
Basic functions for computing the sine, cosine, tangent, etc. Java also includes hyperbolic versions and the inverse functions (such as arc sine).
max()
, min()
Overloaded functions to return the greater and smaller of two arguments (both of the same numeric type).
floor()
Used to return the largest integer smaller than the argument (which is a double). ceil()
returns the smallest integer larger than the argument.
pow()
, exp()
, log()
Functions for raising one number to the power of another, and for computing exponentials and natural logarithms. log10()
provides logarithms to base-10, rather than the natural base.
Let’s look at some simple examples of how to use these functions:
System
.
out
.
println
(
Math
.
abs
(
2
));
System
.
out
.
println
(
Math
.
abs
(-
2
));
double
cosp3
=
Math
.
cos
(
0.3
);
double
sinp3
=
Math
.
sin
(
0.3
);
System
.
out
.
println
((
cosp3
*
cosp3
+
sinp3
*
sinp3
));
// Always 1.0
System
.
out
.
println
(
Math
.
max
(
0.3
,
0.7
));
System
.
out
.
println
(
Math
.
max
(
0.3
,
-
0.3
));
System
.
out
.
println
(
Math
.
max
(-
0.3
,
-
0.7
));
System
.
out
.
println
(
Math
.
min
(
0.3
,
0.7
));
System
.
out
.
println
(
Math
.
min
(
0.3
,
-
0.3
));
System
.
out
.
println
(
Math
.
min
(-
0.3
,
-
0.7
));
System
.
out
.
println
(
Math
.
floor
(
1.3
));
System
.
out
.
println
(
Math
.
ceil
(
1.3
));
System
.
out
.
println
(
Math
.
floor
(
7.5
));
System
.
out
.
println
(
Math
.
ceil
(
7.5
));
System
.
out
.
println
(
Math
.
round
(
1.3
));
// Returns long
System
.
out
.
println
(
Math
.
round
(
7.5
));
// Returns long
System
.
out
.
println
(
Math
.
pow
(
2.0
,
10.0
));
System
.
out
.
println
(
Math
.
exp
(
1
));
System
.
out
.
println
(
Math
.
exp
(
2
));
System
.
out
.
println
(
Math
.
log
(
2.718281828459045
));
System
.
out
.
println
(
Math
.
log10
(
100_000
));
System
.
out
.
println
(
Math
.
log10
(
Integer
.
MAX_VALUE
));
System
.
out
.
println
(
Math
.
random
());
System
.
out
.
println
(
"Let's toss a coin: "
);
if
(
Math
.
random
()
>
0.5
)
{
System
.
out
.
println
(
"It's heads"
);
}
else
{
System
.
out
.
println
(
"It's tails"
);
}
To conclude this section, let’s briefly discuss Java’s random()
function. When this is first called, it sets up a new instance of java.util.Random
. This is a pseudorandom number generator (PRNG)—a deterministic piece of code that produces numbers that look random but are actually produced by a mathematical formula.13 In Java’s case, the formula used for the PRNG is pretty simple, for example:
// From java.util.Random
public
double
nextDouble
()
{
return
(((
long
)(
next
(
26
))
<<
27
)
+
next
(
27
))
*
DOUBLE_UNIT
;
}
If the sequence of pseudorandom numbers always starts at the same place, then exactly the same stream of numbers will be produced. To get around this problem, the PRNG is seeded by a value that should contain as much true randomness as possible. For this source of randomness for the seed value, Java uses a CPU counter value that is normally used for high-precision timing.
While Java’s built-in pseudorandom numbers are fine for most general applications, some specialist applications (notably cryptography and some types of simulations) have much more stringent requirements. If you are working on an application of that sort, seek expert advice from programmers who are already working in the area.
Now that we’ve looked at text and numeric data, let’s move on to look at another of the most frequently encountered kinds of data: date and time information.
Almost all business software applications have some notion of date and time. When modeling real-world events or interactions, collecting a point at which the event occurred is critical for future reporting or comparison of domain objects. Java 8 brings a complete overhaul to the way that developers work with date and time. This section introduces those concepts for Java 8. In earlier versions, the only support is via classes such as java.util.Date
that do not model the concepts. Code that uses the older APIs should move as soon as possible.
Java 8 introduces a new package java.time
, which contains the core classes that most developers work with. It is split into four subpackages:
java.time.chrono
Alternative chronologies that developers using calendaring systems that do not follow the ISO standard will interact with. An example would be a Japanese calendaring system.
java.time.format
Contains the DateTimeFormatter
used for converting date and time objects into a String
and also for parsing strings into the data and time objects.
java.time.temporal
Contains the interfaces required by the core date and time classes and also abstractions (such as queries and adjusters) for advanced operations with dates.
java.time.zone
Classes used for the underlying time zone rules; most developers won’t require this package.
One of the most important concepts when representing time is the idea of an instantaneous point on the timeline of some entity. While this concept is well defined within, for example, Special Relativity, representing this within a computer requires us to make some assumptions. In Java 8, we represent a single point in time as an Instant
, which has these key assumptions:
We cannot represent more seconds than can fit into a long
.
We cannot represent time more precisely than nanosecond precision.
This means that we are restricting ourselves to modeling time in a manner that is consistent with the capabilities of current computer systems. However, there is another fundamental concept that should also be introduced.
An Instant
is about a single event in space-time. However, it is far from uncommon for programmers to have to deal with intervals between two events, and so Java 8 also introduces the java.time.Duration
class. This class ignores calendar effects that might arise (e.g., from daylight saving time). With this basic conception of instants and durations between events, let’s move on to unpack the possible ways of thinking about an instant.
In Figure 9-1, we show the breakdown of the different parts of a timestamp in a number of possible ways.
The key concept here is that there are a number of different abstractions that might be appropriate at different times. For example, there are applications where a LocalDate
is key to business processing, where the needed granularity is a business day. Alternatively, some applications require subsecond, or even millisecond precision. Developers should be aware of their domain and use a suitable representation within their application.
The date and time API can be a lot to take in at first glance, so let’s start by looking at an example, and discuss a diary class that keeps track of birthdays. If you happen to be very forgetful about birthdays, then a class like this (and especially methods like getBirthdaysInNextMonth()
) might be very helpful:
public
class
BirthdayDiary
{
private
Map
<
String
,
LocalDate
>
birthdays
;
public
BirthdayDiary
()
{
birthdays
=
new
HashMap
<>();
}
public
LocalDate
addBirthday
(
String
name
,
int
day
,
int
month
,
int
year
)
{
LocalDate
birthday
=
LocalDate
.
of
(
year
,
month
,
day
);
birthdays
.
put
(
name
,
birthday
);
return
birthday
;
}
public
LocalDate
getBirthdayFor
(
String
name
)
{
return
birthdays
.
get
(
name
);
}
public
int
getAgeInYear
(
String
name
,
int
year
)
{
Period
period
=
Period
.
between
(
birthdays
.
get
(
name
),
birthdays
.
get
(
name
).
withYear
(
year
));
return
period
.
getYears
();
}
public
Set
<
String
>
getFriendsOfAgeIn
(
int
age
,
int
year
)
{
return
birthdays
.
keySet
().
stream
()
.
filter
(
p
->
getAgeInYear
(
p
,
year
)
==
age
)
.
collect
(
Collectors
.
toSet
());
}
public
int
getDaysUntilBirthday
(
String
name
)
{
Period
period
=
Period
.
between
(
LocalDate
.
now
(),
birthdays
.
get
(
name
));
return
period
.
getDays
();
}
public
Set
<
String
>
getBirthdaysIn
(
Month
month
)
{
return
birthdays
.
entrySet
().
stream
()
.
filter
(
p
->
p
.
getValue
().
getMonth
()
==
month
)
.
map
(
p
->
p
.
getKey
())
.
collect
(
Collectors
.
toSet
());
}
public
Set
<
String
>
getBirthdaysInNextMonth
()
{
return
getBirthdaysIn
(
LocalDate
.
now
().
getMonth
());
}
public
int
getTotalAgeInYears
()
{
return
birthdays
.
keySet
().
stream
()
.
mapToInt
(
p
->
getAgeInYear
(
p
,
LocalDate
.
now
().
getYear
()))
.
sum
();
}
}
This class shows how to use the low-level API to build up useful functionality. It also uses innovations such as the Java Streams API, and demonstrates how to use LocalDate
as an immutable class and how dates should be treated as values.
Under a wide variety of circumstances we may find ourselves wanting to answer a question about a particular temporal object. Some example questions we may want answers to are:
Is the date before March 1st?
Is the date in a leap year?
How many days is it from today until my next birthday?
This is acheived by the use of the TemporalQuery
interface, which is defined like this:
public
interface
TemporalQuery
<
R
>
{
R
queryFrom
(
TemporalAccessor
temporal
);
}
The parameter to queryFrom()
should not be null, but if the result indicates that a value was not found, null could be used as a return value.
Predicate
interface can be thought of as a query that can only represent answers to yes-or-no questions. Temporal queries are more general and can return a value of “How many?” or “Which?” instead of just “yes” or “no.”Let’s look at an example of a query in action, by considering a query that answers the following question: “Which quarter of the year is this date in?” Java 8 does not support the concept of a quarter directly. Instead, code like this is used:
LocalDate
today
=
LocalDate
.
now
();
Month
currentMonth
=
today
.
getMonth
();
Month
firstMonthofQuarter
=
currentMonth
.
firstMonthOfQuarter
();
This still doesn’t give quarter as a separate abstraction and instead special case code is still needed. So let’s slightly extend the JDK support by defining this enum type:
public
enum
Quarter
{
FIRST
,
SECOND
,
THIRD
,
FOURTH
;
}
Now, the query can be written as:
public
class
QuarterOfYearQuery
implements
TemporalQuery
<
Quarter
>
{
@Override
public
Quarter
queryFrom
(
TemporalAccessor
temporal
)
{
LocalDate
now
=
LocalDate
.
from
(
temporal
);
if
(
now
.
isBefore
(
now
.
with
(
Month
.
APRIL
).
withDayOfMonth
(
1
)))
{
return
Quarter
.
FIRST
;
}
else
if
(
now
.
isBefore
(
now
.
with
(
Month
.
JULY
)
.
withDayOfMonth
(
1
)))
{
return
Quarter
.
SECOND
;
}
else
if
(
now
.
isBefore
(
now
.
with
(
Month
.
NOVEMBER
)
.
withDayOfMonth
(
1
)))
{
return
Quarter
.
THIRD
;
}
else
{
return
Quarter
.
FOURTH
;
}
}
}
TemporalQuery
objects can be used directly or indirectly. Let’s look at an example of each:
QuarterOfYearQuery
q
=
new
QuarterOfYearQuery
();
// Direct
Quarter
quarter
=
q
.
queryFrom
(
LocalDate
.
now
());
System
.
out
.
println
(
quarter
);
// Indirect
quarter
=
LocalDate
.
now
().
query
(
q
);
System
.
out
.
println
(
quarter
);
Under most circumstances, it is better to use the indirect approach, where the query object is passed as a parameter to query()
. This is because it is normally a lot clearer to read in code.
Adjusters modify date and time objects. Suppose, for example, that we want to return the first day of a quarter that contains a particular timestamp:
public
class
FirstDayOfQuarter
implements
TemporalAdjuster
{
@Override
public
Temporal
adjustInto
(
Temporal
temporal
)
{
final
int
currentQuarter
=
YearMonth
.
from
(
temporal
)
.
get
(
IsoFields
.
QUARTER_OF_YEAR
);
switch
(
currentQuarter
)
{
case
1
:
return
LocalDate
.
from
(
temporal
)
.
with
(
TemporalAdjusters
.
firstDayOfYear
());
case
2
:
return
LocalDate
.
from
(
temporal
)
.
withMonth
(
Month
.
APRIL
.
getValue
())
.
with
(
TemporalAdjusters
.
firstDayOfMonth
());
case
3
:
return
LocalDate
.
from
(
temporal
)
.
withMonth
(
Month
.
JULY
.
getValue
())
.
with
(
TemporalAdjusters
.
firstDayOfMonth
());
case
4
:
return
LocalDate
.
from
(
temporal
)
.
withMonth
(
Month
.
OCTOBER
.
getValue
())
.
with
(
TemporalAdjusters
.
firstDayOfMonth
());
default
:
return
null
;
// Will never happen
}
}
}
Let’s look at an example of how to use an adjuster:
LocalDate
now
=
LocalDate
.
now
();
Temporal
fdoq
=
now
.
with
(
new
FirstDayOfQuarter
());
System
.
out
.
println
(
fdoq
);
The key here is the with()
method, and the code should be read as taking in one Temporal
object and returning another object that has been modified. This is completely usual for APIs that work with immutable objects.
Unfortunately, many applications are not yet converted to use the superior date and time libraries that ship with Java 8. So, for completeness, we briefly mention the legacy date and time support (which is based on java.util.Date
).
java.util.Date
, should not be used in Java 8 environments.In older versions of Java, java.time
is not available. Instead, programmers rely upon the legacy and rudimentary support provided by java.util.Date
. Historically, this was the only way to represent timestamps, and although named Date
this class actually consisted of both a date and a time component—and this led to a lot of confusion for many programmers.
There are many problems with the legacy support provided by Date
, for example:
The Date
class is incorrectly factored. It doesn’t actually refer to a date, and instead is more like a timestamp. It turns out that we need different representations for a date, versus a date and time, versus an instantaneous timestamp.
Date
is mutable. We can obtain a reference to a date, and then change when it refers to.
The Date
class doesn’t actually accept ISO-8601, the universal ISO date standard, as being as valid date.
Date
has a very large number of deprecated methods.
The current JDK uses two constructors for Date
—the void
constructor that is intended to be the “now constructor," and a constructor that takes a number of milliseconds since epoch.
In this chapter, we’ve met several different classes of data. Textual and numeric data are the most obvious examples, but as working programmers we will meet a large number of different sorts of data. Let’s move on to look at whole files of data, and new ways to work with I/O and networking. Fortunately, Java provides good support for dealing with many of these abstractions.