Chapter 4. The Java Type System
In this chapter, we move beyond basic object-oriented programming with classes and into the additional concepts required to work effectively with Java’s static type system.
Note
A statically typed language is one in which variables have definite types, and where it is a compile-time error to assign a value of an incompatible type to a variable. Java is an example of a statically typed language. Languages that only check type compatibility at runtime are called dynamically typed—JavaScript is an example of a dynamically typed language.
Java’s type system involves not only classes and primitive types, but also other kinds of reference type that are related to the basic concept of a class, but which differ in some way, and are usually treated in a special way by javac
or the JVM.
We have already met arrays and classes, two of Java’s most widely used kinds of reference type. This chapter starts by discussing another very important kind of reference type—interfaces. We then move on to discuss Java’s generics, which have a major role to play in Java’s type system. With these topics under our belts, we can discuss the differences between compile-time and runtime types in Java.
To complete the full picture of Java’s reference types, we look at specialized kinds of classes and interfaces—known as enums and annotations. We conclude the chapter by looking at nested types and finally the new lambda expressions functionality introduced in Java 8.
Let’s get started by taking a look at interfaces—probably the most important of Java’s reference types after classes, and a key building block for the whole of Java’s type system.
Interfaces
In Chapter 3, we met the idea of inheritance. We also saw that a Java class can only inherit from a single class. This is quite a big restriction on the kinds of object-oriented programs that we want to make. The designers of Java knew this, but they also wanted to ensure that Java’s approach to object-oriented programming was less complex than, for example, that of C++.
The solution that they chose was to create the concept of an interface. Like a class, an interface defines a new reference type. As its name implies, an interface is intended to represent only an API—so it provides a description of a type, and the methods (and signatures) that classes that implement that API should provide.
In general, a Java interface does not provide any implementation code for the methods that it describes. These methods are considered mandatory—any class that wishes to implement the interface must provide an implementation of these methods.
However, an interface may wish to mark that some API methods are optional, and that implementing classes do not need to implement them if they choose not to. This is done with the default
keyword—and the interface must provide a default implementation of these optional methods, which will be used by any implementation that elects not to implement them.
Note
The ability to have optional methods in interfaces is new in Java 8. It is not available in any earlier version. See “Default Methods” for a full description of how optional (also called default) methods work.It is not possible to directly instantiate an interface and create a member of the interface type. Instead, a class must implement the interface to provide the necessary method bodies.
Any instances of that class are members of both the type defined by the class and the type defined by the interface. Objects that do not share the same class or superclass may still be members of the same type by virtue of implementing the same interface.
Defining an Interface
An interface definition is much like a class definition in which all the (nondefault) methods are abstract and the keyword class
has been replaced with interface
. For example, the following code shows the definition of an interface named Centered
. A Shape
class, such as those defined in Chapter 3, might implement this interface if it wants to allow the coordinates of its center to be set and queried:
interface
Centered
{
void
setCenter
(
double
x
,
double
y
);
double
getCenterX
();
double
getCenterY
();
}
A number of restrictions apply to the members of an interface:
-
All mandatory methods of an interface are implicitly
abstract
and must have a semicolon in place of a method body. Theabstract
modifier is allowed, but by convention is usually omitted. -
An interface defines a public API. All members of an interface are implicitly
public
, and it is conventional to omit the unnecessarypublic
modifier. It is a compile-time error to try to define aprotected
orprivate
method in an interface. -
An interface may not define any instance fields. Fields are an implementation detail, and an interface is a specification not an implementation. The only fields allowed in an interface definition are constants that are declared both
static
andfinal
. -
An interface cannot be instantiated, so it does not define a constructor.
-
Interfaces may contain nested types. Any such types are implicitly
public
andstatic
. See “Nested Types” for a full description of nested types. -
As of Java 8, an interface may contain static methods. Previous versions of Java did not allow this, and this is widely believed to have been a flaw in the design of the Java language.
Extending Interfaces
Interfaces may extend other interfaces, and, like a class definition, an interface definition may include an extends
clause. When one interface extends another, it inherits all the methods and constants of its superinterface and can define new methods and constants. Unlike classes, however, the extends
clause of an interface definition may include more than one superinterface. For example, here are some interfaces that extend other interfaces:
interface
Positionable
extends
Centered
{
void
setUpperRightCorner
(
double
x
,
double
y
);
double
getUpperRightX
();
double
getUpperRightY
();
}
interface
Transformable
extends
Scalable
,
Translatable
,
Rotatable
{}
interface
SuperShape
extends
Positionable
,
Transformable
{}
An interface that extends more than one interface inherits all the methods and constants from each of those interfaces and can define its own additional methods and constants. A class that implements such an interface must implement the abstract methods defined directly by the interface, as well as all the abstract methods inherited from all the superinterfaces.
Implementing an Interface
Just as a class uses extends
to specify its superclass, it can use implements
to name one or more interfaces it supports. implements
is a Java keyword that can appear in a class declaration following the extends
clause. implements
should be followed by a comma-separated list of interfaces that the class implements.
When a class declares an interface in its implements
clause, it is saying that it provides an implementation (i.e., a body) for each mandatory method of that interface. If a class implements an interface but does not provide an implementation for every mandatory interface method, it inherits those unimplemented abstract
methods from the interface and must itself be declared abstract
. If a class implements more than one interface, it must implement every mandatory method of each interface it implements (or be declared abstract
).
The following code shows how we can define a CenteredRectangle
class that extends the Rectangle
class from Chapter 3 and implements our Centered
interface:
public
class
CenteredRectangle
extends
Rectangle
implements
Centered
{
// New instance fields
private
double
cx
,
cy
;
// A constructor
public
CenteredRectangle
(
double
cx
,
double
cy
,
double
w
,
double
h
)
{
super
(
w
,
h
);
this
.
cx
=
cx
;
this
.
cy
=
cy
;
}
// We inherit all the methods of Rectangle but must
// provide implementations of all the Centered methods.
public
void
setCenter
(
double
x
,
double
y
)
{
cx
=
x
;
cy
=
y
;
}
public
double
getCenterX
()
{
return
cx
;
}
public
double
getCenterY
()
{
return
cy
;
}
}
Suppose we implement CenteredCircle
and CenteredSquare
just as we have implemented this CenteredRectangle
class. Each class extends Shape
, so instances of the classes can be treated as instances of the Shape
class, as we saw earlier. Because each class implements the Centered
interface, instances can also be treated as instances of that type. The following code demonstrates how objects can be members of both a class type and an interface type:
Shape
[]
shapes
=
new
Shape
[
3
];
// Create an array to hold shapes
// Create some centered shapes, and store them in the Shape[]
// No cast necessary: these are all widening conversions
shapes
[
0
]
=
new
CenteredCircle
(
1.0
,
1.0
,
1.0
);
shapes
[
1
]
=
new
CenteredSquare
(
2.5
,
2
,
3
);
shapes
[
2
]
=
new
CenteredRectangle
(
2.3
,
4.5
,
3
,
4
);
// Compute average area of the shapes and
// average distance from the origin
double
totalArea
=
0
;
double
totalDistance
=
0
;
for
(
int
i
=
0
;
i
<
shapes
.
length
;
i
++)
{
totalArea
+=
shapes
[
i
].
area
();
// Compute the area of the shapes
// Be careful—in general, the use of instanceof to determine the
// runtime type of an object is quite often an indication of a
// problem with the design
if
(
shapes
[
i
]
instanceof
Centered
)
{
// The shape is a Centered shape
// Note the required cast from Shape to Centered (no cast would
// be required to go from CenteredSquare to Centered, however).
Centered
c
=
(
Centered
)
shapes
[
i
];
double
cx
=
c
.
getCenterX
();
// Get coordinates of the center
double
cy
=
c
.
getCenterY
();
// Compute distance from origin
totalDistance
+=
Math
.
sqrt
(
cx
*
cx
+
cy
*
cy
);
}
}
System
.
out
.
println
(
"Average area: "
+
totalArea
/
shapes
.
length
);
System
.
out
.
println
(
"Average distance: "
+
totalDistance
/
shapes
.
length
);
Note
Interfaces are data types in Java, just like classes. When a class implements an interface, instances of that class can be assigned to variables of the interface type.Don’t interpret this example to imply that you must assign a CenteredRectangle
object to a Centered
variable before you can invoke the setCenter()
method or to a Shape
variable before you can invoke the area()
method. CenteredRectangle
defines setCenter()
and inherits area()
from its Rectangle
superclass, so you can always invoke these methods.
Implementing Multiple Interfaces
Suppose we want shape objects that can be positioned in terms of not only their center points but also their upper-right corners. And suppose we also want shapes that can be scaled larger and smaller. Remember that although a class can extend only a single superclass, it can implement any number of interfaces. Assuming we have defined appropriate UpperRightCornered
and Scalable
interfaces, we can declare a class as follows:
public
class
SuperDuperSquare
extends
Shape
implements
Centered
,
UpperRightCornered
,
Scalable
{
// Class members omitted here
}
When a class implements more than one interface, it simply means that it must provide implementations for all abstract (aka mandatory) methods in all its interfaces.
Default Methods
With the advent of Java 8, it is possible to include methods in interfaces that include an implementation. In this section, we’ll discuss these methods, which represent optional methods in the API the interfaces represents—they’re usually called default methods. Let’s start by looking at the reasons why we need the default mechanism in the first place.
Backward compatibility
The Java platform has always been very concerned with backwards compatibility. This means that code that was written (or even compiled) for an earlier version of the platform must continue to keep working with later releases of the platform. This principle allows development groups to have a high degree of confidence that an upgrade of their JDK or JRE will not break currently working applications.
Backward compatibility is a great strength of the Java platform, but in order to achieve it, some constraints are placed on the platform. One of them is that interfaces may not have new mandatory methods added to them in a new release of the interface.
For example, let’s suppose that we want to update the Positionable
interface with the ability to add a bottom-left bounding point as well:
public
interface
Positionable
extends
Centered
{
void
setUpperRightCorner
(
double
x
,
double
y
);
double
getUpperRightX
();
double
getUpperRightY
();
void
setLowerLeftCorner
(
double
x
,
double
y
);
double
getLowerLeftX
();
double
getLowerLeftY
();
}
With this new definition, if we try to use this new interface with code developed for the old then it just won’t work, as the existing code is missing the mandatory methods setLowerLeftCorner()
, getLowerLeftX()
, and getLowerLeftY()
.
Note
You can see this effect quite easily in your own code. Compile a class file that depends on an interface. Then add a new mandatory method to the interface, and try to run the program with the new version of the interface, together with your old class file. You should see the program crash with aNoClassDefError
.This limitation was a concern for the designers of Java 8—as one of their goals was to be able to upgrade the core Java Collections libraries, and introduce methods that made use of lambda expressions.
To solve this problem, a new mechanism was needed, essentially to allow interfaces to evolve by allowing new, optional methods to be added to interfaces without breaking backward compatibility.
Implementation of default methods
To add new methods to an interface without breaking backward compatability requires that some implementation must be provided for the older implementations of the interface so that they can continue to work. This mechanism is a default
method, and it was first added to the platform in JDK 8.
Note
A default method (sometimes called an optional method) can be added to any interface. This must include an implementation, called the default implementation, which is written inline in the interface definition.The basic behavior of default methods is:
-
An implementing class may (but is not required to) implement the default method.
-
If an implementing class implements the default method, then the implementation in the class is used.
-
If no other implementation can be found, then the default implementation is used.
An example default method is the sort()
method. It’s been added to the interface java.util.List
in JDK 8, and is defined as:
// The <E> syntax is Java's way of writing a generic type—see
// the next section for full details. If you aren't familiar with
// generics, just ignore that syntax for now.
interface
List
<
E
>
{
// Other members omitted
public
default
void
sort
(
Comparator
<?
super
E
>
c
)
{
Collections
.<
E
>
sort
(
this
,
c
);
}
}
Thus, from Java 8 upward, any object that implements List
has an instance method sort()
that can be used to sort the list using a suitable Comparator
. As the return type is void
, we might expect that this is an in-place sort, and this is indeed the case.
Marker Interfaces
Sometimes it is useful to define an interface that is entirely empty. A class can implement this interface simply by naming it in its implements
clause without having to implement any methods. In this case, any instances of the class become valid instances of the interface. Java code can check whether an object is an instance of the interface using the instanceof
operator, so this technique is a useful way to provide additional information about an object.
The java.io.Serializable
interface is a marker interface of this sort. A class implements the Serializable
interface to tell ObjectOutputStream
that its instances may safely be serialized. java.util.RandomAccess
is another example: java.util.List
implementations implement this interface to advertise that they provide fast random access to the elements of the list. For example, ArrayList
implements RandomAccess
, while LinkedList
does not. Algorithms that care about the performance of random-access operations can test for RandomAccess
like this:
// Before sorting the elements of a long arbitrary list, we may want
// to make sure that the list allows fast random access. If not,
// it may be quicker make a random-access copy of the list before
// sorting it. Note that this is not necessary when using
// java.util.Collections.sort().
List
l
=
...;
// Some arbitrary list we're given
if
(
l
.
size
()
>
2
&&
!(
l
instanceof
RandomAccess
))
l
=
new
ArrayList
(
l
);
sortListInPlace
(
l
);
As we will see later, Java’s type system is very tightly connected to the names that types have—an approach called nominal typing. A marker interface is a great example of this—it has nothing at all except a name.
Java Generics
One of the great strengths of the Java platform is the standard library that it ships. It provides a great deal of useful functionality—and in particular robust implementations of common data structures. These implementations are relatively simple to develop with and are well documented. The libraries are known as the Java Collections, and we will spend a big chunk of Chapter 8 discussing them. For a far more complete treatment, see the book Java Generics and Collections by Maurice Naftalin and Philip Wadler (O’Reilly).
Although they were still very useful, the earliest versions of the collections had a fairly major limitation, however. This limitation was that the data structure (often called the container) essentially hid the type of the data being stored in it.
Note
Data hiding and encapsulation is a great principle of object-oriented programming, but in this case, the opaque nature of the container caused a lot of problems for the developer.Let’s kick off the section by demonstrating the problem, and showing how the introduction of generic types can solve it, and make life much easier for Java developers.
Introduction to Generics
If we want to build a collection of Shape
instances, we can use a List
to hold them, like this:
List
shapes
=
new
ArrayList
();
// Create a List to hold shapes
// Create some centered shapes, and store them in the list
shapes
.
add
(
new
CenteredCircle
(
1.0
,
1.0
,
1.0
));
// This is legal Java—but is a very bad design choice
shapes
.
add
(
new
CenteredSquare
(
2.5
,
2
,
3
));
// List::get() returns Object, so to get back a
// CenteredCircle we must cast
CenteredCircle
c
=
(
CentredCircle
)
shapes
.
get
(
0
);
// Next line causes a runtime failure
CenteredCircle
c
=
(
CentredCircle
)
shapes
.
get
(
1
);
A problem with this code stems from the requirement to perform a cast to get the shape objects back out in a usable form—the List
doesn’t know what type of objects it contains. Not only that, but it’s actually possible to put different types of objects into the same container—and everything will work fine until an illegal cast is used, and the program crashes.
What we really want is a form of List
that understands what type it contains. Then, javac
could detect when an illegal argument was passed to the methods of List
and cause a compilation error, rather than deferring the issue to runtime.
Java provides syntax to cater for this—to indicate that a type is a container that holds instances of another reference type we enclose the payload type that the container holds within angle brackets:
// Create a List-of-CenteredCircle
List
<
CenteredCircle
>
shapes
=
new
ArrayList
<
CenteredCircle
>();
// Create some centered shapes, and store them in the list
shapes
.
add
(
new
CenteredCircle
(
1.0
,
1.0
,
1.0
));
// Next line will cause a compilation error
shapes
.
add
(
new
CenteredSquare
(
2.5
,
2
,
3
));
// List<CenteredCircle>::get() returns a CenteredCircle, no cast needed
CenteredCircle
c
=
shapes
.
get
(
0
);
This syntax ensures that a large class of unsafe code is caught by the compiler, before it gets anywhere near runtime. This is, of course, the whole point of static type systems—to use compile-time knowledge to help eliminate whole swathes of runtime problems.
Container types are usually called generic types—and they are declared like this:
interface
Box
<
T
>
{
void
box
(
T
t
);
T
unbox
();
}
This indicates that the Box
interface is a general construct, which can hold any type of payload. It isn’t really a complete interface by itself—it’s more like a general description of a whole family of interfaces, one for each type that can be used in place of T
.
Generic Types and Type Parameters
We’ve seen how to use a generic type, to provide enhanced program safety, by using compile-time knowledge to prevent simple type errors. In this section, let’s dig deeper into the properties of generic types.
The syntax <T>
has a special name—it’s called a type parameter, and another name for a generic type is a parameterized type. This should convey the sense that the container type (e.g., List
) is parameterized by another type (the payload type). When we write a type like Map<String, Integer>
, we are assigning concrete values to the type parameters.
When we define a type that has parameters, we need to do so in a way that does not make assumptions about the type parameters. So the List
type is declared in a generic way as List<E>
, and the type parameter E
is used all the way through to stand as a placeholder for the actual type that the programmer will use for the payload when she makes use of the List
data structure.
Tip
Type parameters always stand in for reference types. It is not possible to use a primitive type as a value for a type parameter.The type parameter can be used in the signatures and bodies of methods as though it is a real type, for example:
interface
List
<
E
>
extends
Collection
<
E
>
{
boolean
add
(
E
e
);
E
get
(
int
index
);
// other methods omitted
}
Note how the type parameter E can be used as a parameter for both return types and method arguments. We don’t assume that the payload type has any specific properties, and only make the basic assumption of consistency—that the type we put in is the sane type that we will later get back out.
Diamond Syntax
When creating an instance of a generic type, the right-hand side of the assignment statement repeats the value of the type parameter. This is usually unnecessary, as the compiler can infer the values of the type parameters. In modern versions of Java, we can leave out the repeated type values in what is called diamond syntax.
Let’s look at an example of how to use diamond syntax, by rewriting one of our earlier examples:
// Create a List-of-CenteredCircle using diamond syntax
List
<
CenteredCircle
>
shapes
=
new
ArrayList
<>();
This is a small improvement in the verbosity of the assignment statement—we’ve managed to save a few characters of typing. We’ll return to the topic of type inference when we discuss lambda expressions towards the end of this chapter.
Type Erasure
In “Default Methods”, we discussed the Java platform’s strong preference for backwards compatibility. The addition of generics in Java 5 was another example of where backwards compatibility was an issue for a new language feature.
The central question was how to make a type system that allowed older, nongeneric collection classes to be used along with newer, generic collections. The design decision was to achieve this by the use of casts:
List
someThings
=
getSomeThings
();
// Unsafe cast, but we know that the
// contents of someThings are really strings
List
<
String
>
myStrings
=
(
List
<
String
>)
someThings
;
This means that List
and List<String>
are compatible as types, at least at some level. Java achieves this compatibility by type erasure. This means that generic type parameters are only visible at compile time—they are stripped out by javac
and are not reflected in the bytecode.1
Note
The nongeneric typeList
is usually called a raw type. It is still perfectly legal Java to work with the raw form of types—even for types that are now generic. This is almost always a sign of poor quality code, however.The mechanism of type erasure gives rise to a difference in the type system seen by javac
and that seen by the JVM—we will discuss this fully in “Compile and Runtime Typing”.
Type erasure also prohibits some other definitions, which would otherwise seem legal. In this code, we want to count the orders as represented in two slightly different data structures:
// Won't compile
interface
OrderCounter
{
// Name maps to list of order numbers
int
totalOrders
(
Map
<
String
,
List
<
String
>>
orders
);
// Name maps to total orders made so far
int
totalOrders
(
Map
<
String
,
Integer
>
orders
);
}
This seems like perfectly legal Java code—but it will not compile. The issue is that although the two methods seem like normal overloads, after type erasure, the signature of both methods becomes:
int
totalOrders
(
Map
);
All that is left after type erasure is the raw type of the container—in this case, Map
. The runtime would be unable to distinguish between the methods by signature, and so the language specification makes this syntax illegal.
Wildcards
A parameterized type, such as ArrayList<T>
, is not instantiable—we cannot create instances of them. This is because <T>
is just a type parameter—merely a placeholder for a genuine type. It is only when we provide a concrete value for the type parameter, (e.g., ArrayList<String>
), that the type becomes fully formed and we can create objects of that type.
This poses a problem if the type that we want to work with is unknown at compile time. Fortunately, the Java type system is able to accommodate this concept. It does so by having an explicit concept of the unknown type—which is represented as <?>
. This is the simplest example of Java’s wildcard types.
We can write expressions that involve the unknown type:
ArrayList
<?>
mysteryList
=
unknownList
();
Object
o
=
mysteryList
.
get
(
0
);
This is perfectly valid Java—ArrayList<?>
is a complete type that a variable can have, unlike ArrayList<T>
. We don’t know anything about the payload type of
, but that may not be a problem for our code. When working with the unknown type, there are some limitations on its use in user code. For example, this code will not compile:
// Won't compile
mysteryList
.
add
(
new
Object
());
The reason for this is simple—we don’t know what the payload type of mysteryList
is! For example, if mysteryList
was really a instance of ArrayList<String>
, then we wouldn’t expect to be able to put an Object
into it.
The only value that we know we can always insert into a container is null
—as we know that null is a possible value for any reference type. This isn’t that useful, and for this reason, the Java language spec also rules out instantiating a container object with the unknown type as payload, for example:
// Won't compile
List
<?>
unknowns
=
new
ArrayList
<?>();
A very important use for the unknown type stems from the question, “Is List<String>
a subtype of List<Object>
?” That is, can we write this?
// Is this legal?
List
<
Object
>
objects
=
new
ArrayList
<
String
>();
At first glance, this seems entirely reasonable—String
is a subclass of Object
, so we know that any String
element in our collection is also a valid Object
. However, consider the following code:
// Is this legal?
List
<
Object
>
objects
=
new
ArrayList
<
String
>();
// If so, what do we do about this?
objects
.
add
(
new
Object
());
As the type of objects
was declared to be List<Object>
, then it should be legal to add an Object
instance to it. However, as the actual instance holds strings, then trying to add an Object
would not be compatible, and so this would fail at runtime.
The resolution for this is to realize that although this is legal (because String
inherits from Object
):
Object
o
=
new
String
(
"X"
);
that does not mean that the corresponding statement for generic container types is also true:
// Won't compile
List
<
Object
>
objects
=
new
ArrayList
<
String
>();
Another way of saying this is that List<String>
is not a subtype of List<Object>
. If we want to have a subtyping relationship for containers, then we need to use the unknown type:
// Perfectly legal
List
<?>
objects
=
new
ArrayList
<
String
>();
This means that List<String>
is a subtype of List<?>
—although when we use an assignment like the preceding one, we have lost some type information. For example, the return type of get()
is now effectively Object
. You should also note that List<?>
is not a subtype of any List<T>
, for any value of T
.
The unknown type sometimes confuses developers—provoking questions like, “Why wouldn’t you just use Object
instead of the unknown type?” However, as we’ve seen, the need to have subtyping relationships between generic types essentially requires us to have a notion of the unknown type.
Bounded wildcards
In fact, Java’s wildcard types extend beyond just the unknown type, with the concept of bounded wildcards, also called type parameter constraints. This is the ability to restrict the types that can be used as the value of a type parameter.
They are used to describe the inheritance hierarchy of a mostly unknown type—effectively making statements like, for example, “I don’t know anything about this type, except that it must implement List
.” This would be written as ? extends List
in the type parameter. This provides a useful lifeline to the programmer—instead of being restricted to the totally unknown type, she knows that at least the capabilities of the type bound are available.
Warning
Theextends
keyword is always used, regardless of whether the constraining type is a class or interface type.This is an example of a concept called type variance, which is the general theory of how inheritance between container types relates to the inheritance of their payload types.
- Type covariance
-
This means that the container types have the same relationship to each other as the payload types do. This is expressed using the
extends
keyword. - Type contravariance
-
This means that the container types have the inverse relationship to each other as the payload types. This is expressed using the
super
keyword.
These principles tend to appear when discussing container types that act as producers or consumers of types. For example, if Cat
extends Pet
, then List<Cat>
is a subtype of List<? extends Pet>
. The List
is acting as a producer of Cat
objects and the appropriate keyword is extends
.
For a container type that is acting purely as a consumer of instances of a type, we would use the super
keyword.
Note
This is codified in the Producer Extends, Consumer Super (PECS) principle coined by Joshua Bloch.As we will see in Chapter 8, we see both covariance and contravariance throughout the Java Collections. They largely exist to ensure that the generics just “do the right thing” and behave in a manner that should not surprise the developer.
Array Covariance
In the earliest versions of Java, before the collections libraries were even introduced, the problem of type variance in container types was still present for Java’s arrays. Without type variance, even simple methods like this sort()
would have been very difficult to write in a useful way:
Arrays
.
sort
(
Object
[]
a
);
For this reason, arrays in Java are covariant—this was seen as a necessary evil in the very early days of the platform, despite the hole in the static type system that it exposes:
// This is completely legal
String
[]
words
=
{
"Hello World!"
};
Object
[]
objects
=
words
;
// Oh, dear, runtime error
objects
[
0
]
=
new
Integer
(
42
);
More recent research on modern open source codebases indicates that array covariance is extremely rarely used and is almost certainly a language misfeature.2 It should be avoided when writing new code.
Generic Methods
A generic method is a method that is able to take instances of any reference type.
For example, this method emulates the behavior of the ,
(comma) operator from the C
language, which is usually used to combine expressions with side effects together:
// Note that this class is not generic
public
class
Utils
public
static
<
T
>
T
comma
(
T
a
,
T
b
)
{
return
a
;
}
}
Even though a type parameter is used in the definition of the method, the class it is defined in need not be generic—instead, the syntax is used to indicate that the method can be used freely, and that the return type is the same as the argument.
Using and Designing Generic Types
When working with Java’s generics, it can sometimes be helpful to think in terms of two different levels of understanding:
- Practitioner
-
A practitioner needs to use existing generic libraries, and to build some fairly simple generic classes. At this level, the developer should also understand the basics of type erasure, as several Java syntax features are confusing without at least an awareness of the runtime handling of generics.
- Designer
-
The designer of new libraries that use generics needs to understand much more of the capabilities of generics. There are some nastier parts of the spec—including a full understanding of wildcards, and advanced topics such as “capture-of” error messages.
Java generics are one of the most complex parts of the language specification with a lot of potential corner cases, which not every developer needs to fully understand, at least on a first encounter with this part of Java’s type system.
Compile and Runtime Typing
Consider an example piee>ce of code:
List
<
String
>
l
=
new
ArrayList
<>();
System
.
out
.
println
(
l
);
We can ask the following question: what is the type of l
? The answer to that question depends on whether we consider l
at compile time (i.e., the type seen by javac
) or at runtime (as seen by the JVM).
javac
will see the type of l
as List-of-String
, and will use that type information to carefully check for syntax errors, such as an attempted add()
of an illegal type.
Conversely, the JVM will see l
as an object of type ArrayList
—as we can see from the println()
statement. The runtime type of l
is a raw type due to type erasure.
The compile-time and runtime types are therefore slightly different to each other. The slightly strange thing is that in some ways, the runtime type is both more and less specific than the compile-time type.
The runtime type is less specific than the compile-time type, because the type information about the payload type is gone—it has been erased, and the resulting runtime type is just a raw type.
The compile-time type is less specific than the runtime type, because we don’t know exactly what concrete type l
will be—all we know is that it will be of a type compatible with List
.
Enums and Annotations
Java has specialized forms of classes and interfaces that are used to fulfill specific roles in the type system. They are known as enumerated types and annotation types, normally just called enums and annotations.
Enums
Enums are a variation of classes that have limited functionality and that have only a small number of possible values that the type permits.
For example, suppose we want to define a type to represent the primary colors of red, green, and blue, and we want these to be the only possible values of the type. We can do this by making use of the enum
keyword:
public
enum
PrimaryColor
{
// The ; is not required at the end of the list of instances
RED
,
GREEN
,
BLUE
}
Instances of the type PrimaryColor
can then be referenced as though they were static fields: PrimaryColor.RED
, PrimaryColor.GREEN
, and PrimaryColor.BLUE
.
Note
In other languages, such as C++, this role is usually fulfilled by using constant integers, but Java’s approach provides better type safety, and more flexiblity. For example, as enums are specialized classes, enums can have member fields and methods. If they do have a body (consisting of fields or methods) then the semicolon at the end of the list of instances is required.For example, suppose that we want to have an enum that encompasses the first few regular polygons (shapes with all sides and all angles equal), and we want them to have some behavior (in the form of methods). We could achieve this by using an enum that takes a value as a parameter, like this:
public
enum
RegularPolygon
{
// The ; is mandatory for enums that have parameters
TRIANGLE
(
3
),
SQUARE
(
4
),
PENTAGON
(
5
),
HEXAGON
(
6
);
private
Shape
shape
;
public
Shape
getShape
()
{
return
shape
;
}
private
RegularPolygon
(
int
sides
)
{
switch
(
sides
)
{
case
3
:
// We assume that we have some general constructors
// for shapes that take the side length and
// angles in degrees as parameters
shape
=
new
Triangle
(
1
,
1
,
1
,
60
,
60
,
60
);
break
;
case
4
:
shape
=
new
Rectangle
(
1
,
1
);
break
;
case
5
:
shape
=
new
Pentagon
(
1
,
1
,
1
,
1
,
1
,
108
,
108
,
108
,
108
,
108
);
break
;
case
6
:
shape
=
new
Hexagon
(
1
,
1
,
1
,
1
,
1
,
1
,
120
,
120
,
120
,
120
,
120
,
120
);
break
;
}
}
}
These parameters (only one of them in this example) are passed to the constructor to create the individual enum instances. As the enum instances are created by the Java runtime, and can’t be instantiated from outside, the constructor is declared as private.
Annotations
Annotations are a specialized kind of interface that, as the name suggests, annotate some part of a Java program.
For example, consider the @Override
annotation. You may have seen it on some methods in some of the earlier examples, and may have asked the following question: what does it do?
The short, and perhaps surprising, answer is that it does nothing at all.
The less short (and flippant) answer is that, like all annotations, it has no direct effect, but instead acts as additional information about the method that it annotates—in this case, it denotes that a method overrides a superclass method.
This acts as a useful hint to compilers and integrated development environments (IDEs)—if a developer has misspelled the name of a method that she intended to be an override of a superclass method, then the presence of the @Override
annotation on the misspelled method (which does not override anything) alerts the compiler to the fact that something is not right.
Annotations are not allowed to alter program semantics—instead, they provide optional metadata. In its strictest sense, this means that they should not affect program execution and instead can only provide information for compilers and other pre-execution phases.
The platform defines a small number of basic annotations in java.lang
. The original set were @Override
, @Deprecated
, and @SuppressWarnings
—which were used to indicate that a method was overriden, deprecated, or that it generated some compiler warnings that should be suppressed.
These were augmented by @SafeVarargs
in Java 7 (which provides extended warning suppression for varargs methods) and @FunctionalInterface
in Java 8. This last annotation indicates an interface can be used as a target for a lambda expression—it is a useful marker annotation although not mandatory, as we will see.
Annotations have some special properties, compared to regular interfaces:
-
All (implicitly) extend
java.lang.annotation.Annotation
-
May not be generic
-
May not extend any other interface
-
May only define zero-arg methods
-
May not define methods that throw exceptions
-
Have restrictions on the return types of methods
-
Can have a default return value for methods
Defining Custom Annotations
Defining custom annotation types for use in your own code is not that hard. The @interface
keyword allows the developer to define a new annotation type, in much the same way that class
or interface
are used.
Note
The key to writing custom annotations is the use of “meta-annotations.” These are special annotations, which appear as annotations on the definition of new (custom) annotation types.The meta-annotations are defined in java.lang.annotation
and allow the developer to specify policy for where the new annotation type is to be used, and how it will be treated by the compiler and runtime.
There are two primary meta-annotations that are both essentially required when creating a new annotation type—@Target
and @Retention
. These both take values that are represented as enums.
The @Target
meta-annotation indicates where the new custom annotation can be legally placed within Java source code. The enum ElementType
has the following possible values: TYPE
, FIELD
, METHOD
, PARAMETER
, CONSTRUCTOR
, LOCAL_VARIABLE
, ANNOTATION_TYPE
, PACKAGE
, TYPE_PARAMETER
, and TYPE_USE
.
The other meta-annotation is @Retention
, which indicates how javac
and the Java runtime should process the custom annotation type. It can have one of three values, which are represented by the enum RetentionPolicy
:
SOURCE
-
Annotations with this retention policy are discarded by
javac
during compilation. CLASS
-
This means that the annotation will be present in the class file, but will not necessarily be accessible at runtime by the JVM. This is rarely used, but is sometimes seen in tools that do offline analysis of JVM bytecode.
RUNTIME
-
This indicates that the annotation will be available for user code to access at runtime (by using reflection).
Let’s take a look at an example, a simple annotation called @Nickname
, which allows the developer to define a nickname for a method, which can then be used to find the method reflectively at runtime:
@Target
(
ElementType
.
METHOD
)
@Retention
(
RetentionPolicy
.
RUNTIME
)
public
@interface
Nickname
{
String
[]
value
()
default
{};
}
This is all that’s required to define the annotation—a syntax element where the annotation can appear, a retention policy, and the name of the element. As we need to be able to state the nickname we’re assigning to the method, we also need to define a method on the annotation. Despite this, defining new custom annotations is a remarkably compact undertaking.
In addition to the two primary meta-annotations, there are also the @Inherited
and @Documented
meta-annotations. These are much less frequently encountered in practice, and details on them can be found in the platform documentation.
Type Annotations
With the release of Java 8, two new values for ElementType
were added—TYPE_PARAMETER
and TYPE_USE
. These new values allow the use of annotations in places where they were previously not legal, such as at any site where a type is used. This enables the developer to write code such as:
@NotNull
String
safeString
=
getMyString
();
The extra type information conveyed by the @NotNull
can then be used by a special type checker to detect problems (a possible NullPointerException
, in this example) and to perform additional static analysis. The basic Java 8 distribution ships with some basic pluggable type checkers, but also provides a framework for allowing developers and library authors to create their own.
In this section, we’ve met Java’s enum and annotation types. Let’s move on, to consider the next important part of Java’s type system: nested types.
Nested Types
The classes, interfaces, and enum types we have seen so far in this book have all been defined as top-level types. This means that they are direct members of packages, defined independently of other types. However, type definitions can also be nested within other type definitions. These nested types, commonly known as “inner classes,” are a powerful feature of the Java language.
Nested types are used for two separate purposes, both related to encapsulation:
-
A type may be nested because it needs especially intimate access to the internals of another type—by being a member type, it has access in the same way that member variables and methods do, and can bend the rules of encapsulation.
-
A type may be only required for a very specific reason, and in a very small section of code. It should be tightly localized, as it is really part of the implementation detail and should be encapsulated away from the rest of the system.
Another way of thinking of nested types is that they are types that are somehow tied together with another type—they don’t really have a completely independent existence as an entity. Types can be nested within another type in four different ways:
- Static member types
-
A static member type is any type defined as a
static
member of another type. Nested interfaces, enums, and annotations are always static (even if you don’t use the keyword). - Nonstatic member classes
-
A “nonstatic member type” is simply a member type that is not declared
static
. Only classes can be nonstatic member types. - Local classes
-
A local class is a class that is defined and only visible within a block of Java code. Interfaces, enums, and annotations may not be defined locally.
- Anonymous classes
-
An anonymous class is a kind of local class that has no meaningful name in the Java language. Interfaces, enums, and annotations cannot be defined anonymously.
The term “nested types,” while a correct and precise usage, is not widely used by developers. Instead, most Java prorammers user the much vaguer term “inner class.” Depending on the situation, this can refer to a nonstatic member class, local class, or anonymous class, but not a static member type, with no real way to distinguish between them.
Fortunately, although the terminology for describing nested types is not always clear, the syntax for working with them is, and it is usually clear from context which kind of nested type is being discussed.
Let’s move on to describe each of the four kinds of nested types in greater detail. Each section describes the features of the nested type, the restrictions on its use, and any special Java syntax used with the type. These four sections are followed by an implementation note that explains how nested types work under the hood.
Static Member Types
A static member type is much like a regular top-level type. For convenience, however, it is nested within the namespace of another type. Static member types have the following basic properties:
-
A static member type is like the other static members of a class: static fields and static methods.
-
A static member type is not associated with any instance of the containing class (i.e., there is no
this
object). -
A static member type can access (only) the
static
members of the class that contains it. -
A static member type has access to all the
static
members (including any other static member types) of its containing type. -
Nested interfaces, enums, and annotations are implicitly static, whether or not the
static
keyword appears. -
Any type nested within an interface or annotation is also implicitly
static
. -
Static member types may be defined within top-level types or nested to any depth within other static member types.
-
A static member type may not be defined within any other kind of nested type.
Let’s look at a quick example of the syntax for static member types. Example 4-1 shows a helper interface defined as a static member of a containing class. The example also shows how this interface is used both within the class that contains it and by external classes. Note the use of its hierarchical name in the external class.
Example 4-1. Defining and using a static member interface
// A class that implements a stack as a linked list
public
class
LinkedStack
{
// This static member interface defines how objects are linked
// The static keyword is optional: all nested interfaces are static
static
interface
Linkable
{
public
Linkable
getNext
();
public
void
setNext
(
Linkable
node
);
}
// The head of the list is a Linkable object
Linkable
head
;
// Method bodies omitted
public
void
push
(
Linkable
node
)
{
...
}
public
Object
pop
()
{
...
}
}
// This class implements the static member interface
class
LinkableInteger
implements
LinkedStack
.
Linkable
{
// Here's the node's data and constructor
int
i
;
public
LinkableInteger
(
int
i
)
{
this
.
i
=
i
;
}
// Here are the data and methods required to implement the interface
LinkedStack
.
Linkable
next
;
public
LinkedStack
.
Linkable
getNext
()
{
return
next
;
}
public
void
setNext
(
LinkedStack
.
Linkable
node
)
{
next
=
node
;
}
}
Features of static member types
A static member type has access to all static members of its containing type, including private
members. The reverse is true as well: the methods of the containing type have access to all members of a static member type, including the private
members. A static member type even has access to all the members of any other static member types, including the private
members of those types. A static member type can use any other static member without qualifying its name with the name of the containing type.
Note
A static member type cannot have the same name as any of its enclosing classes. In addition, static member types can be defined only within top-level types and other static member types. This is actually part of a larger prohibition againststatic
members of any sort within member, local, and anonymous classes.Top-level types can be declared as either public
or package-private (if they’re declared without the public
keyword). But declaring top-level types as private
and protected
wouldn’t make a great deal of sense—protected
would just mean the same as package-private and a private
top-level class would be unable to be accessed by any other type.
Static member types, on the other hand, are members and so can use any access control modifiers that other members of the containing type can. These modifiers have the same meanings for static member types as they do for other members of a type. Recall that all members of interfaces (and annotations) are implicitly public
, so static member types nested within interfaces or annotation types cannot be
or private
.
For example, in Example 4-1, the Linkable
interface is declared public
, so it can be implemented by any class that is interested in being stored on a LinkedStack
.
In code outside the containing class, a static member type is named by combining the name of the outer type with the name of the inner type (e.g., LinkedStack.
).
Under most circumstances, this syntax provides a helpful reminder that the inner class is interconnected with its containing type. However, the Java language does permit you to use the import
directive to directly import a static member type:
import
pkg.LinkedStack.Linkable
;
// Import a specific nested type
// Import all nested types of LinkedStack
import
pkg.LinkedStack.*
;
The nested type can then be referenced without including the name of its enclosing type (e.g., just as Linkable
).
Note
You can also use theimport static
directive to import a static member type. See “Packages and the Java Namespace” in Chapter 2 for details on import
and import static
.However, importing a nested type obscures the fact that that type is closely associated with its containing type—which is usually important information—and as a result it is not commonly done.
Nonstatic Member Classes
A nonstatic member class is a class that is declared as a member of a containing class or enumerated type without the static
keyword:
-
If a static member type is analogous to a class field or class method, a nonstatic member class is analogous to an instance field or instance method.
-
Only classes can be nonstatic member types.
-
An instance of a nonstatic member class is always associated with an instance of the enclosing type.
-
The code of a nonstatic member class has access to all the fields and methods (both
static
and non-static
) of its enclosing type. -
Several features of Java syntax exist specifically to work with the enclosing instance of a nonstatic member class.
Example 4-2 shows how a member class can be defined and used. This example extends the previous LinkedStack
example to allow enumeration of the elements on the stack by defining an iterator()
method that returns an implementation of the java.util.Iterator
interface. The implementation of this interface is defined as a member class.
Example 4-2. An iterator implemented as a member class
import
java.util.Iterator
;
public
class
LinkedStack
{
// Our static member interface
public
interface
Linkable
{
public
Linkable
getNext
();
public
void
setNext
(
Linkable
node
);
}
// The head of the list
private
Linkable
head
;
// Method bodies omitted here
public
void
push
(
Linkable
node
)
{
...
}
public
Linkable
pop
()
{
...
}
// This method returns an Iterator object for this LinkedStack
public
Iterator
<
Linkable
>
iterator
()
{
return
new
LinkedIterator
();
}
// Here is the implementation of the Iterator interface,
// defined as a nonstatic member class.
protected
class
LinkedIterator
implements
Iterator
<
Linkable
>
{
Linkable
current
;
// The constructor uses a private field of the containing class
public
LinkedIterator
()
{
current
=
head
;
}
// The following 3 methods are defined by the Iterator interface
public
boolean
hasNext
()
{
return
current
!=
null
;
}
public
Linkable
next
()
{
if
(
current
==
null
)
throw
new
java
.
util
.
NoSuchElementException
();
Linkable
value
=
current
;
current
=
current
.
getNext
();
return
value
;
}
public
void
remove
()
{
throw
new
UnsupportedOperationException
();
}
}
}
Notice how the LinkedIterator
class is nested within the LinkedStack
class. Because LinkedIterator
is a helper class used only within LinkedStack
, having it defined so close to where it is used by the containing class makes for a clean design, just as we discussed when we introduced nested types.
Features of member classes
Like instance fields and instance methods, every instance of a nonstatic member class is associated with an instance of the class in which it is defined. This means that the code of a member class has access to all the instance fields and instance methods (as well as the static
members) of the containing instance, including any that are declared private
.
This crucial feature was already illustrated in Example 4-2. Here is the LinkedStack.LinkedIterator()
constructor again:
public
LinkedIterator
()
{
current
=
head
;
}
This single line of code sets the current
field of the inner class to the value of the head
field of the containing class. The code works as shown, even though head
is declared as a private
field in the containing class.
A nonstatic member class, like any member of a class, can be assigned one of the standard access control modifiers. In Example 4-2, the LinkedIterator
class is declared protected
, so it is inaccessible to code (in a different package) that uses the LinkedStack
class but is accessible to any class that subclasses LinkedStack
.
Restrictions on member classes
Member classes have two important restrictions:
-
A nonstatic member class cannot have the same name as any containing class or package. This is an important rule, one that is not shared by fields and methods.
-
Nonstatic member classes cannot contain any
static
fields, methods, or types, except for constant fields declared bothstatic
andfinal
.
Note
static
members are top-level constructs not associated with any particular object while every nonstatic member class is associated with an instance of its enclosing class. Defining a static
top-level member within a member class that is not at the top level would cause confusion, so it is not allowed.Syntax for member classes
The most important feature of a member class is that it can access the instance fields and methods in its containing object. We saw this in the LinkedStack.Linked
constructor of Example 4-2:
public
LinkedIterator
()
{
current
=
head
;
}
In this example, head
is a field of the enclosing LinkedStack
class, and we assign it to the current
field of the LinkedIterator
class (which is a member of the nonstatic member class).
If we want to use explicit references, and make use of this
, then we have to use a special syntax for explicitly referring to the containing instance of the this
object. For example, if we want to be explicit in our constructor, we can use the following syntax:
public
LinkedIterator
()
{
this
.
current
=
LinkedStack
.
this
.
head
;
}
The general syntax is classname
.this
, where classname
is the name of a containing class. Note that member classes can themselves contain member classes, nested to any depth. However, because no member class can have the same name as any containing class, the use of the enclosing class name prepended to this
is a perfectly general way to refer to any containing instance.
Note
This special syntax is needed only when referring to a member of a containing class that is hidden by a member of the same name in the member class.Scope versus inheritance
We notice that a top-level class can extend a member class. With the introduction of nonstatic member classes, two separate hierarchies must be considered for any class. The first is the inheritance hierarchy, from superclass to subclass, that defines the fields and methods a member class inherits. The second is the containment hierarchy, from containing class to contained class, that defines a set of fields and methods that are in the scope of (and are therefore accessible to) the member class.
It is important to be familiar with the properties and rules of thumb that the two hierarchies have:
-
The two hierarchies are entirely distinct from each other; it is important that you do not confuse them.
-
Refrain from creating naming conflicts, where a field or method in a superclass has the same name as a field or method in a containing class.
-
If such a naming conflict does arise, the inherited field or method takes precedence over the field or method of the same name in the containing class.
-
Inherited fields and methods are in the scope of the class that inherits them and take precedence over fields and methods by the same name in enclosing scopes.
-
To prevent confusion between the class hierarchy and the containment hierarchy, avoid deep containment hierarchies.
-
If a class is nested more than two levels deep, it is probably going to cause more confusion than it is worth.
-
If a class has a deep class hierarchy (i.e., it has many ancestors), consider defining it as a top-level class rather than as a nonstatic member class.
Local Classes
A local class is declared locally within a block of Java code rather than as a member of a class. Only classes may be defined locally: interfaces, enumerated types, and annotation types must be top-level or static member types. Typically, a local class is defined within a method, but it can also be defined within a static initializer or instance initializer of a class.
Just as all blocks of Java code appear within class definitions, all local classes are nested within containing blocks. For this reason, local classes share many of the features of member classes. It is usually more appropriate to think of them as an entirely separate kind of nested type.
Note
See Chapter 5 for details as to when it’s appropriate to choose a local class versus a lambda expression.The defining characteristic of a local class is that it is local to a block of code. Like a local variable, a local class is valid only within the scope defined by its enclosing block. Example 4-3 shows how we can modify the iterator()
method of the Linked
class so it defines LinkedIterator
as a local class instead of a member class.
By doing this, we move the definition of the class even closer to where it is used and hopefully improve the clarity of the code even further. For brevity, Example 4-3 shows only the iterator()
method, not the entire LinkedStack
class that contains it.
Example 4-3. Defining and using a local class
// This method returns an Iterator object for this LinkedStack
public
Iterator
<
Linkable
>
Iterator
()
{
// Here's the definition of LinkedIterator as a local class
class
LinkedIterator
implements
Iterator
<
Linkable
>
{
Linkable
current
;
// The constructor uses a private field of the containing class
public
LinkedIterator
()
{
current
=
head
;
}
// The following 3 methods are defined by the Iterator interface
public
boolean
hasNext
()
{
return
current
!=
null
;
}
public
Linkable
next
()
{
if
(
current
==
null
)
throw
new
java
.
util
.
NoSuchElementException
();
Linkable
value
=
current
;
current
=
current
.
getNext
();
return
value
;
}
public
void
remove
()
{
throw
new
UnsupportedOperationException
();
}
}
// Create and return an instance of the class we just defined
return
new
LinkedIterator
();
}
Features of local classes
Local classes have the following interesting features:
-
Like member classes, local classes are associated with a containing instance and can access any members, including
private
members, of the containing class. -
In addition to accessing fields defined by the containing class, local classes can access any local variables, method parameters, or exception parameters that are in the scope of the local method definition and are declared
final
.
Restrictions on local classes
Local classes are subject to the following restrictions:
-
The name of a local class is defined only within the block that defines it; it can never be used outside that block. (Note, however, that instances of a local class created within the scope of the class can continue to exist outside of that scope. This situation is described in more detail later in this section.)
-
Local classes cannot be declared
public
,protected
,private
, orstatic
. -
Like member classes, and for the same reasons, local classes cannot contain
static
fields, methods, or classes. The only exception is for constants that are declared bothstatic
andfinal
. -
Interfaces, enumerated types, and annotation types cannot be defined locally.
-
A local class, like a member class, cannot have the same name as any of its enclosing classes.
-
As noted earlier, a local class can use the local variables, method parameters, and even exception parameters that are in its scope but only if those variables or parameters are declared
final
. This is because the lifetime of an instance of a local class can be much longer than the execution of the method in which the class is defined.
Note
A local class has a private internal copy of all local variables it uses (these copies are automatically generated byjavac
). The only way to ensure that the local variable and the private copy are always the same is to insist that the local variable is final
.Scope of a local class
In discussing nonstatic member classes, we saw that a member class can access any members inherited from superclasses and any members defined by its containing classes. The same is true for local classes, but local classes can also access final
local variables and parameters. Example 4-4 illustrates the different kinds of fields and variables that may be accessible to a local class:
Example 4-4. Fields and variables available to a local class
class
A
{
protected
char
a
=
'a'
;
}
class
B
{
protected
char
b
=
'b'
;
}
public
class
C
extends
A
{
private
char
c
=
'c'
;
// Private fields visible to local class
public
static
char
d
=
'd'
;
public
void
createLocalObject
(
final
char
e
)
{
final
char
f
=
'f'
;
int
i
=
0
;
// i not final; not usable by local class
class
Local
extends
B
{
char
g
=
'g'
;
public
void
printVars
()
{
// All of these fields and variables are accessible to this class
System
.
out
.
println
(
g
);
// (this.g) g is a field of this class
System
.
out
.
println
(
f
);
// f is a final local variable
System
.
out
.
println
(
e
);
// e is a final local parameter
System
.
out
.
println
(
d
);
// (C.this.d) d field of containing class
System
.
out
.
println
(
c
);
// (C.this.c) c field of containing class
System
.
out
.
println
(
b
);
// b is inherited by this class
System
.
out
.
println
(
a
);
// a is inherited by the containing class
}
}
Local
l
=
new
Local
();
// Create an instance of the local class
l
.
printVars
();
// and call its printVars() method.
}
}
Lexical Scoping and Local Variables
A local variable is defined within a block of code that defines its scope, and outside of that scope, a local variable cannot be accessed and ceases to exist. Any code within the curly braces that define the boundaries of a block can use local variables defined in that block.
This type of scoping, which is known as lexical scoping, just defines a section of source code within which a variable can be used. It is common for programmers to think of such a scope as temporal instead—that is, to think of a local variable as existing from the time the JVM begins executing the block until the time control exits the block. This is usually a reasonable way to think about local variables and their scope.
The introduction of local classes confuses the picture, however. To see why, notice that instances of a local class can have a lifetime that extends past the time that the JVM exits the block where the local class is defined.
Note
In other words, if you create an instance of a local class, that instance does not automatically go away when the JVM finishes executing the block that defines the class. So, even though the definition of the class was local, instances of that class can escape out of the place they were defined.This can cause effects that some developers initially find surprising. This is because local classes can use local variables, and so they can contain copies of values from lexical scopes that no longer exist. This can been seen in the following code:
public
class
Weird
{
// A static member interface used below
public
static
interface
IntHolder
{
public
int
getValue
();
}
public
static
void
main
(
String
[]
args
)
{
IntHolder
[]
holders
=
new
IntHolder
[
10
];
for
(
int
i
=
0
;
i
<
10
;
i
++)
{
final
int
fi
=
i
;
// A local class
class
MyIntHolder
implements
IntHolder
{
// Use the final variable
public
int
getValue
()
{
return
fi
;
}
}
holders
[
i
]
=
new
MyIntHolder
();
}
// The local class is now out of scope, so we can't use it. But we
// have 10 valid instances of that class in our array. The local
// variable fi is not in our scope here, but it is still in scope
// for the getValue() method of each of those 10 objects. So call
// getValue() for each object and print it out. This prints the
// digits 0 to 9.
for
(
int
i
=
0
;
i
<
10
;
i
++)
{
System
.
out
.
println
(
holders
[
i
].
getValue
());
}
}
}
To make sense of this code, remember that the lexical scope of the methods of a local class has nothing to do with when the interpreter enters and exits the block of code that defines the local class.
Each instance of a local class has an automatically created private copy of each of the final local variables it uses, so, in effect, it has its own private copy of the scope that existed when it was created.
Note
The local classMyIntHolder
is sometimes called a closure. In more general Java terms, a closure is an object that saves the state of a scope and makes that scope available later.Closures are useful in some styles of programming, and different programming languages define and implement closures in different ways. Java implements closures as local classes, anonymous classes, and lambda expressions.
Anonymous Classes
An anonymous class is a local class without a name. It is defined and instantiated in a single succinct expression using the new
operator. While a local class definition is a statement in a block of Java code, an anonymous class definition is an expression, which means that it can be included as part of a larger expression, such as a method call.
Note
For the sake of completeness, we cover anonymous classes here, but for most use cases in Java 8 and later, lambda expressions (see “Conclusion”) have replaced anonymous classes.Consider Example 4-5, which shows the LinkedIterator
class implemented as an anonymous class within the iterator()
method of the LinkedStack
class. Compare it with Example 4-4, which shows the same class implemented as a local class.
Example 4-5. An enumeration implemented with an anonymous class
public
Iterator
<
Linkable
>
iterator
()
{
// The anonymous class is defined as part of the return statement
return
new
Iterator
<
Linkable
>()
{
Linkable
current
;
// Replace constructor with an instance initializer
{
current
=
head
;
}
// The following 3 methods are defined by the Iterator interface
public
boolean
hasNext
()
{
return
current
!=
null
;
}
public
Linkable
next
()
{
if
(
current
==
null
)
throw
new
java
.
util
.
NoSuchElementException
();
Linkable
value
=
current
;
current
=
current
.
getNext
();
return
value
;
}
public
void
remove
()
{
throw
new
UnsupportedOperationException
();
}
};
// Note the required semicolon. It terminates the return statement
}
As you can see, the syntax for defining an anonymous class and creating an instance of that class uses the new
keyword, followed by the name of a class and a class body definition in curly braces. If the name following the new
keyword is the name of a class, the anonymous class is a subclass of the named class. If the name following new
specifies an interface, as in the two previous examples, the anonymous class implements that interface and extends Object
.
Note
The syntax for anonymous classes does not include any way to specify anextends
clause, an implements
clause, or a name for the class.Because an anonymous class has no name, it is not possible to define a constructor for it within the class body. This is one of the basic restrictions on anonymous classes. Any arguments you specify between the parentheses following the superclass name in an anonymous class definition are implicitly passed to the superclass constructor. Anonymous classes are commonly used to subclass simple classes that do not take any constructor arguments, so the parentheses in the anonymous class definition syntax are often empty. In the previous examples, each anonymous class implemented an interface and extended Object
. Because the Object()
constructor takes no arguments, the parentheses were empty in those examples.
Restrictions on anonymous classes
Because an anonymous class is just a type of local class, anonymous classes and local classes share the same restrictions. An anonymous class cannot define any static
fields, methods, or classes, except for static
final
constants. Interfaces, enumerated types, and annotation types cannot be defined anonymously. Also, like local classes, anonymous classes cannot be public
, private
, protected
, or static
.
The syntax for defining an anonymous class combines definition with instantiation. Using an anonymous class instead of a local class is not appropriate if you need to create more than a single instance of the class each time the containing block is executed.
Because an anonymous class has no name, it is not possible to define a constructor for an anonymous class. If your class requires a constructor, you must use a local class instead. However, you can often use an instance initializer as a substitute for a constructor.
Although they are not limited to use with anonymous classes, instance initializers (described earlier in “Field Defaults and Initializers”), were introduced into the language for this purpose. An anonymous class cannot define a constructor, so it only has a default constructor. By using an instance initializer, you can get around the fact that you cannot define a constructor for an anonymous class.
How Nested Types Work
The preceding sections explained the features and behavior of the four kinds of nested types. That should be all you need to know about nested types, especially if all you want to do is use them. You may find it easier to understand nested types if you understand how they are implemented, however.
Note
The introduction of nested types did not change the Java Virtual Machine or the Java class file format. As far as the Java interpreter is concerned, there is no such thing as a nested type: all classes are normal top-level classes.In order to make a nested type behave as if it is actually defined inside another class, the Java compiler ends up inserting hidden fields, methods, and constructor arguments into the classes it generates. These hidden fields and methods are often referred to as synthetic.
You may want to use the javap
disassembler to disassemble some of the class files for nested types so you can see what tricks the compiler has used to make the nested types work. (See Chapter 13 for information on javap
.)
The implementation of nested types works by having javac
compile each nested type into a separate class file, which is actually a top-level class. The compiled class files have a special naming convention, and have names that would not ordinarily be created from user code.
Recall our first LinkedStack
example (Example 4-1), which defined a static member interface named Linkable
. When you compile this LinkedStack
class, the compiler generates two class files. The first one is LinkedStack.class, as expected.
The second class file, however, is called LinkedStack$Linkable.class. The $
in this name is automatically inserted by javac
. This second class file contains the implementation of the static member interface defined in the exercise.
Because the nested type is compiled into an ordinary top-level class, there is no way it can directly access the privileged members of its container. Therefore, if a static member type uses a private
(or other privileged) member of its containing type, the compiler generates synthetic access methods (with the default package access) and converts the expressions that access the private
members into expressions that invoke these specially generated methods.
The naming conventions for the four kinds of nested type are:
- (Static or nonstatic) member types
-
Member types are named according to the
EnclosingType$Member.class
pattern. - Anonymous classes
-
Because anonymous classes have no names, the names of the class files that represent them are an implementation detail. The Oracle/OpenJDK
javac
uses numbers to provide anonymous class names (e.g.,EnclosingType$1.class
). - Local classes
-
A local class is named according to a combination (e.g.,
EnclosingType$1
).Member.class
Let’s also take a quick look at some implementation details of how javac
provides synthetic access for some of the specific cases that nested types need.
Nonstatic member class implementation
Each instance of a nonstatic member class is associated with an instance of the enclosing class. The compiler enforces this association by defining a synthetic field named this$0
in each member class. This field is used to hold a reference to the enclosing instance.
Every nonstatic member class constructor is given an extra parameter that initializes this field. Every time a member class constructor is invoked, the compiler automatically passes a reference to the enclosing class for this extra parameter.
Local and anonymous class implementation
A local class is able to refer to fields and methods in its containing class for exactly the same reason that a nonstatic member class can; it is passed a hidden reference to the containing class in its constructor and saves that reference away in a private
synthetic field added by the compiler. Like nonstatic member classes, local classes can use private
fields and methods of their containing class because the compiler inserts any required accessor methods.
What makes local classes different from member classes is that they have the ability to refer to local variables in the scope that defines them. The crucial restriction on this ability, however, is that local classes can reference only local variables and parameters that are declared final
. The reason for this restriction becomes apparent in the implementation.
A local class can use local variables because javac
automatically gives the class a private
instance field to hold a copy of each local variable the class uses.
The compiler also adds hidden parameters to each local class constructor to initialize these automatically created private
fields. A local class does not actually access local variables but merely its own private copies of them. This could cause inconsistencies if the local variables could alter outside of the local class.3
Lambda Expressions
One of the most eagerly anticated features of Java 8 was the introduction of lambda expressions. These allow small bits of code to be written inline as literals in a program and facilitate a more functional style of programming Java.
In truth, many of these techniques had always been possible using nested types, via patterns like callbacks and handlers, but the syntax was always quite cumbersome, especially given the need to explicitly define a completely new type even when only needing to express a single line of code in the callback.
As we saw in Chapter 2, the syntax for a lambda expression is to take a list of parameters (the types of which are typically inferred), and to attach that to a method body, like this:
(
p
,
q
)
->
{
/* method body */
}
This can provide a very compact way to represent simple methods, and can largely obviate the need to use anonymous classes.
Note
A lambda expression has almost all of the component parts of a method, with the obvious exception that a lambda doesn’t have a name. In fact, some developers like to think of lambdas as “anonymous methods.”For example, consider the list()
method of the java.io.File
class. This method lists the files in a directory. Before it returns the list, though, it passes the name of each file to a FilenameFilter
object you must supply. This FilenameFilter
object accepts or rejects each file.
Here’s how you can define a FilenameFilter
class to list only those files whose names end with .java, using an anonymous class:
File
dir
=
new
File
(
"/src"
);
// The directory to list
// Now call the list() method with a single anonymous implemenation of
// FilenameFilter as the argument
String
[]
filelist
=
dir
.
list
(
new
FilenameFilter
()
{
public
boolean
accept
(
File
f
,
String
s
)
{
return
s
.
endsWith
(
".java"
);
}
});
With lambda expressions, this can be simplified:
File
dir
=
new
File
(
"/src"
);
// The directory to list
String
[]
filelist
=
dir
.
list
((
f
,
s
)
->
{
return
s
.
endsWith
(
".java"
);
});
For each file in the list, the block of code in the lambda expression is evaluated. If the method returns true
(which happens if the filename ends in .java) then the file is included in the output—which ends up in the array filelist
.
This pattern, where a block of code is used to test if an element of a container matches a condition, and to only return the elements that pass the condition, is called a filter idiom—and is one of the standard techniques of functional programming, which we will discuss in more depth presently.
Lambda Expression Conversion
When javac
encounters a lambda expression, it interprets it as the body of a method with a specific signature—but which method?
To resolve this question, javac
looks at the surrounding code. To be legal Java code, the lambda expression must satisfy the following:
-
The lambda must appear where an instance of an interface type is expected.
-
The expected interface type should have exactly one mandatory method.
-
The expected interface method should have a signature that exactly matches that of the lambda expression.
If this is the case, then an instance is created of a type that implements the expected interface, and uses the lambda body as the implementation for the mandatory method.
This slightly complex explanation comes from the decision to keep Java’s type system as purely nominative (based on names). The lambda expression is said to be converted to an instance of the correct interface type.
Some developers also like to use the term single abstract method (or SAM) type to refer to the interface type that the lambda is converted into. This draws attention to the fact that to be usable by the lambda expression mechanism, an interface must have only a single nondefault method.
Note
Despite the parallels between lambda expressions and anonymous classes, lambdas are not simply syntactic sugar over anonymous classes. In fact, lambdas are implemented using method handles (which we will meet in Chapter 11) and a new, special JVM bytecode calledinvokedynamic
.From this discussion, we can see that although Java 8 has added lambda expressions, they have been specifically designed to fit into Java’s existing type system—which has a very strong emphasis on nominal typing.
Method References
Recall that we can think of lambda expressions as methods that don’t have names. Now, consider this lambda expression:
// In real code this would probably be shorter because of type inference
(
MyObject
myObj
)
->
myObj
.
toString
()
This will be autoconverted to an implementation of a @FunctionalInterface
that has a single nondefault method that takes a single MyObject
and returns String
. However, this seems like excessive boilerplate, and so Java 8 provides a syntax for making this easier to read and write:
MyObject:
:
toString
This is a shorthand, known as a method reference, that uses an existing method as a lambda expression. It can be thought of as using an existing method, but ignoring the name of the method, so it can be can used as a lambda, and autoconverted in the usual way.
Functional Programming
Java is fundamentally an object-oriented lanaguage. However, with the arrival of lambda expressions, it becomes much easier to write code that is closer to the functional approach.
Note
There’s no single definition of exactly what constitutes a functional language—but there is at least a consensus that it should at least contain the ability to represent a function as a value that can be put into a variable.Java has always (since version 1.1) been able to represent functions via inner classes, but the syntax was complex and lacking in clarity. Lambda expressions greatly simplify that syntax, and so it is only natural that more developers will be seeking to use aspects of functional programming in their Java code, now that it is considerably easier to do so.
The first taste of functional programming that Java developers are likely to encounter are three basic idioms that are remarkably useful:
map()
-
The map idiom is used with lists, and list-like containers. The idea is that a function is passed in that is applied to each element in the collection, and a new collection is created—consisting of the results of applying the function to each element in turn. This means that a map idiom converts a collection of one type to a collection of potentially a different type.
filter()
-
We have already met an example of the filter idiom, when we discussed how to replace an anonymous implementation of
FilenameFilter
with a lambda. The filter idiom is used for producing a new subset of a collection, based on some criteria. Note that in functional programming, it is normal to produce a new collection, rather than modifying an existing one in-place. reduce()
-
The reduce idiom has several different guises. It is an aggregation operation, which can be called fold or accumulate or aggregate as well as reduce. The basic idea is to take an initial value, and an aggregation (or reduction) function, and apply the reduction function to each element in turn, building up a final result for the whole collection by making a series of intermediate results—similar to a “running total”—as the reduce operation traverses the collection.
Java has full support for these key functional idioms (and several others). The implementation is explained in some depth in Chapter 8, where we discuss Java’s data structures and collections, and in particular the stream abstraction, that makes all of this possible.
Let’s conclude this introduction with some words of caution. It’s worth noting that Java is best regarded as having support for “slightly functional programming.” It is not an especially functional language, nor does it try to be. Some particular aspects of Java that mitigate against any claims to being a functional language include the following:
-
Java has no structural types, which means no “true” function types. Every lambda is automatically converted to the appropriate nominal type.
-
Type erasure causes problems for functional programming—type safety can be lost for higher-order functions.
-
Java is inherently mutable (as we’ll discuss in Chapter 6)—mutability is often regarded as highly undesirable for functional languages.
Despite this, easy access to the basics of functional programing—and especially idioms such as map, filter, and reduce—is a huge step forward for the Java community. These idioms are so useful that a large majority of Java developers will never need or miss the more advanced capabilities provided by languages with a more thoroughbred functional pedigree.
Conclusion
By examining Java’s type system, we have been able to build up a clear picture of the worldview that the Java platform has about data types. Java’s type system can be characterized as:
- Nominal
-
The name of a Java type is of paramount importance. Java does not permit structural types in the way some other languages do.
- Static
-
All Java variables have types that are known at compile time.
- Object/imperative
-
Java code is object-oriented, and all code must live inside methods, which must live inside classes. However, Java’s primitive types prevent adoption of the “everything is an object” worldview.
- Slightly functional
-
Java provides support for some of the more common functional idioms, but more as a convenience to programmers than anything else.
- Modestly type-inferred
-
Java is optimized for readability (even by novice progammers) and prefers to be explicit, even at the cost of repetition of information.
- Strongly backward compatible
-
Java is primarily a business-focused language, and backward compatibility and protection of existing codebases is a very high priority.
- Type erased
-
Java permits parameterized types, but this information is not available at runtime.
Java’s type system has evolved (albeit slowly and cautiously) over the years—and with the addition of lambda expressions, is now on a par with the type systems of other mainstream programming languages. Lambdas, along with default methods, represent the greatest transformation since the advent of Java 5, and the introduction of generics, annotations, and related innovations.
Default methods represent a major shift in Java’s approach to object-oriented programming—perhaps the biggest since the language’s inception. From Java 8 onward, interfaces can contain implementation code. This fundamentally changes Java’s nature—previously a single-inherited language, Java is now multiply inherited (but only for behavior—there is still no multiple inheritance of state).
Despite all of these innovations, Java’s type system is not (and is not intended to be) equipped with the power of the type systems of languages such as Scala or Haskell. Instead, Java’s type system is strongly biased in favor of simplicity, readability, and a simple learning curve for newcomers.
Java has also benefited enormously from the approaches to types developed in other languages over the last 10 years. Scala’s example of a statically typed language that nevertheless achieves much of the feel of a dynamically typed language by the use of type inference has been a good source of ideas for features to add to Java, even though the languages have quite different design philosophies.
Despite the long wait for lambda expressions in Java, the argument has been settled, and Java is a better language for them. Whether the majority of ordinary Java programmers require the added power—and attendant complexity—that comes from an advanced (and much less nominal) type system such as Scala’s, or whether the “slightly functional programming” of Java 8 (e.g., map, filter, reduce, and their peers) will suffice for most developers’ needs, remains to be seen in the months and years ahead. It should be an interesting journey.
1 Some small traces of generics remain, which can be seen at runtime via reflection.
2 Raoul-Gabriel Urma and Janina Voigt, “Using the OpenJDK to Investigate Covariance in Java,” Java Magazine (May/June 2012):44–47.
3 We will have more to say on this subject when we discuss memory and mutable state in Chapter 6.
Get Java in a Nutshell, 6th Edition now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.