Cover | Table of Contents | Colophon
for/in loop.
Without getting into those details yet, you can get some immediate bang
for your buck by checking out the java.util.Arrays class, which is
chock-full of static utility methods (many of which are new to Tiger).java.util.Arrays class is a set
of static methods that all are useful
for working with arrays. Most of these methods are particularly helpful if
you have an array of numeric primitives, which is what Example 1-1 demonstrates (in varied and mostly useless ways).
package com.oreilly.tiger.ch01;
import java.util.Arrays;
import java.util.List;
public class ArraysTester {
private int[] ar;
public ArraysTester(int numValues) {
ar = new int[numValues];
for (int i=0; i < ar.length; i++) {
ar[i] = (1000 - (300 + i));
}
}
public int[] get( ) {
return ar;
}
public static void main(String[] args) {
ArraysTester tester = new ArraysTester(50);
int[] myArray = tester.get( );
// Compare two arrays
int[] myOtherArray = tester.get().clone( );
if (Arrays.equals(myArray, myOtherArray)) {
System.out.println("The two arrays are equal!");
} else {
System.out.println("The two arrays are not equal!");
}
// Fill up some values
Arrays.fill(myOtherArray, 2, 10, new Double(Math.PI).intValue( ));
myArray[30] = 98;
// Print array, as is
System.out.println("Here's the unsorted array...");
System.out.println(Arrays.toString(myArray));
System.out.println( );
// Sort the array
Arrays.sort(myArray);
// print array, sorted
System.out.println("Here's the sorted array...");
System.out.println(Arrays.toString(myArray));
System.out.println( );
// Get the index of a particular value
int index = Arrays.binarySearch(myArray, 98);
System.out.println("98 is located in the array at index " + index);
String[][] ticTacToe = { {"X", "O", "O"},
{"O", "X", "X"},
{"X", "O", "X"}};
System.out.println(Arrays.deepToString(ticTacToe));
String[][] ticTacToe2 = { {"O", "O", "X"},
{"O", "X", "X"},
{"X", "O", "X"}};
String[][] ticTacToe3 = { {"X", "O", "O"},
{"O", "X", "X"},
{"X", "O", "X"}};
if (Arrays.deepEquals(ticTacToe, ticTacToe2)) {
System.out.println("Boards 1 and 2 are equal.");
} else {
System.out.println("Boards 1 and 2 are not equal.");
}
if (Arrays.deepEquals(ticTacToe, ticTacToe3)) {
System.out.println("Boards 1 and 3 are equal.");
} else {
System.out.println("Boards 1 and 3 are not equal.");
}
}
}
for/in loop.
Without getting into those details yet, you can get some immediate bang
for your buck by checking out the java.util.Arrays class, which is
chock-full of static utility methods (many of which are new to Tiger).java.util.Arrays class is a set
of static methods that all are useful
for working with arrays. Most of these methods are particularly helpful if
you have an array of numeric primitives, which is what Example 1-1 demonstrates (in varied and mostly useless ways).
package com.oreilly.tiger.ch01;
import java.util.Arrays;
import java.util.List;
public class ArraysTester {
private int[] ar;
public ArraysTester(int numValues) {
ar = new int[numValues];
for (int i=0; i < ar.length; i++) {
ar[i] = (1000 - (300 + i));
}
}
public int[] get( ) {
return ar;
}
public static void main(String[] args) {
ArraysTester tester = new ArraysTester(50);
int[] myArray = tester.get( );
// Compare two arrays
int[] myOtherArray = tester.get().clone( );
if (Arrays.equals(myArray, myOtherArray)) {
System.out.println("The two arrays are equal!");
} else {
System.out.println("The two arrays are not equal!");
}
// Fill up some values
Arrays.fill(myOtherArray, 2, 10, new Double(Math.PI).intValue( ));
myArray[30] = 98;
// Print array, as is
System.out.println("Here's the unsorted array...");
System.out.println(Arrays.toString(myArray));
System.out.println( );
// Sort the array
Arrays.sort(myArray);
// print array, sorted
System.out.println("Here's the sorted array...");
System.out.println(Arrays.toString(myArray));
System.out.println( );
// Get the index of a particular value
int index = Arrays.binarySearch(myArray, 98);
System.out.println("98 is located in the array at index " + index);
String[][] ticTacToe = { {"X", "O", "O"},
{"O", "X", "X"},
{"X", "O", "X"}};
System.out.println(Arrays.deepToString(ticTacToe));
String[][] ticTacToe2 = { {"O", "O", "X"},
{"O", "X", "X"},
{"X", "O", "X"}};
String[][] ticTacToe3 = { {"X", "O", "O"},
{"O", "X", "X"},
{"X", "O", "X"}};
if (Arrays.deepEquals(ticTacToe, ticTacToe2)) {
System.out.println("Boards 1 and 2 are equal.");
} else {
System.out.println("Boards 1 and 2 are not equal.");
}
if (Arrays.deepEquals(ticTacToe, ticTacToe3)) {
System.out.println("Boards 1 and 3 are equal.");
} else {
System.out.println("Boards 1 and 3 are not equal.");
}
}
}
java.util.Queue class, for all
those occasions when you need FIFO (first-in, first-out) action. Using this
class is a breeze, and you'll find it's a nice addition to the already robust
Java Collection ...er...collection.Queue implementation is
to avoid the standard collection methods add( ) and remove( ). Instead, you'll need to use offer( ) to
add elements. Keep in mind that most queues have a fixed size. If you call add( ) on a full queue, an unchecked exception is thrown—which really isn't appropriate, as a queue being full
is a normal condition, not an exceptional one. offer( ) simply returns false if an element cannot be added, which is more in line with standard queue usage.remove( ) throws an exception if the queue is empty; a
better choice is the new poll( ) method,
which returns null if there is nothing in the queue. Both methods attempt to remove elements from the
head of the queue. If you want the head without removing it, use
element( ) or peek( ). Example 1-2 shows these methods in action.
package com.oreilly.tiger.ch01;
import java.io.IOException;
import java.io.PrintStream;
import java.util.LinkedList;
import java.util.Queue;
public class QueueTester {
public Queue q;
public QueueTester( ) {
q = new LinkedList( );
}
public void testFIFO(PrintStream out) throws IOException {
q.add("First");
q.add("Second");
q.add("Third");
Object o;
while ((o = q.poll( )) != null) {
out.println(o);
}
}
public static void main(String[] args) {
QueueTester tester = new QueueTester( );
try {
tester.testFIFO(System.out);
} catch (IOException e) {
e.printStackTrace( );
}
}
}
testFIFO( ), you can see that the first items into the queue are the
first ones out:PriorityQueue, another Queue
implementation. You provide it a
Comparator, and it does the rest.PriorityQueue works just as any other Queue implementation, and you don't even need to learn any new methods. Instead of performing FIFO
ordering, though, a PriorityQueue orders its items by using the
Comparator interface. If you create a new queue and don't specify a
Comparator, you get what's
called natural ordering, which applies to any classes that implement Comparable. For numerical values, for instance, this places highest values, well, highest! Here's an example:
PriorityQueue<Integer> pq =
new PriorityQueue<Integer>(20);
// Fill up with data, in an odd order
for (int i=0; i<20; i++) {
pq.offer(20-i);
}
// Print out and check ordering
for (int i=0; i<20; i++) {
System.out.println(pq.poll( ));
}
Comparator implementation is given to PriorityQueue, it orders the numbers lowest to highest, even though they're not added to the
queue in that order. So when peeling off elements, the lowest item
comes out first:
[echo] Running PriorityQueueTester...
[java] 1
[java] 2
[java] 3
[java] 4
[java] 5
[java] 6
[java] 7
[java] 8
[java] 9
[java] 10
[java] 11
[java] 12
[java] 13
[java] 14
[java] 15
[java] 16
[java] 17
[java] 18
[java] 19
[java] 20
java.util.Comparator.
package com.oreilly.tiger.ch01;
import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Queue;
public class PriorityQueueTester {
public static void main(String[] args) {
PriorityQueue<Integer> pq =
new PriorityQueue<Integer>(20,
new Comparator<Integer>( ) {
public int compare(Integer i, Integer j) {
int result = i%2 - j%2;
if (result == 0)
result = i-j;
return result;
}
}
);
// Fill up with data, in an odd order
for (int i=0; i<20; i++) {
pq.offer(20-i);
}
// Print out and check ordering
for (int i=0; i<20; i++) {
System.out.println(pq.poll( ));
}
}
}
class Point2D {
protected int x, y;
public Point2D( ) {
this.x=0;
this.y=0;
}
public Point2D(int x, int y) {
this.x = x;
this.y = y;
}
}
class Point3D extends Point2D {
protected int z;
public Point3D(int x, int y) {
this(x, y, 0);
}
public Point3D(int x, int y, int z) {
this.x = x;
this.y = y;
this.z = z;
}
}
class Position2D {
Point2D location;
public Position2D( ) {
this.location = new Point2D( );
}
public Position2D(int x, int y) {
this.location = new Point2D(x, y);
}
public Point2D getLocation( ) {
return location;
}
}
class Position3D extends Position2D {
Point3D location;
public Position3D(int x, int y, int z) {
this.location = new Point3D(x, y, z);
}
public Point3D getLocation( ) {
return location;
}
}
public Point3D getLocation( ), which probably
looks pretty odd to you, but get used to it. This is called a covariant
return, and is
only allowed if the return type of the subclass is an extension
of the return type of the superclass. In this case, this is satisfied by
Point3D extending Point2Dchar). Things are different,
now, so you'll need to understand a bit more.char, and that has some far-reaching consequences. You'll have to use
int to represent these characters, and as a result methods like
Character.isUpperCase( ) and Character.isWhitespace( ) now have
variants that accept int
arguments. So if you're needing values in Unicode
3.0 that are not available in Unicode 3.0, you'll need to use these
new methods..\u0000 through \uFFFF. All of
these codepoints fit into a Java char.
int.
char, then, represents a BMP Unicode codepoint. To get all the supplementary characters in addition to the BMP, you need to use an StringBuilder is used, most often in the manner that you're used
to seeing StringBuffer used. StringBuilder is a new Tiger class
intended to be a drop-in replacement for StringBuffer in cases where
thread safety isn't an issue.StringBuffer code with StringBuilder code. Really—it's as simple as that. If you're working in a single-thread environment, or in a piece of code where you aren't worried about multiple threads accessing the code, or synchronization, it's best to use StringBuilder instead of StringBuffer. All the methods you are used to seeing on StringBuffer exist for StringBuilder, so there shouldn't be any compilation problems doing a straight search and replace on your code. Example 1-5 is just such an example; I wrote it using StringBuffer, and then did a straight
search-and-replace, converting every occurrence of "StringBuffer" with
"StringBuilder".
package com.oreilly.tiger.ch01;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class StringBuilderTester {
public static String appendItems(List list) {
StringBuilder b = new StringBuilder( );
for (Iterator i = list.iterator( ); i.hasNext( ); ) {
b.append(i.next( ))
.append(" ");
}
return b.toString( );
}
public static void main(String[] args) {
List list = new ArrayList( );
list.add("I");
list.add("play");
list.add("Bourgeois");
list.add("guitars");
list.add("and");
list.add("Huber");
list.add("banjos");
System.out.println(StringBuilderTester.appendItems(list));
}
}
StringBuilder in the rest
of this book, so you'll be thoroughly comfortable with the class by book's
end.printf( ) and format( )? StringBuilder, as does Strings, and ditch all that annoying
class-casting code. Even better, you can limit types that your custom
classes and methods accept, removing a huge amount of tedious errorchecking
and type-checking code.Object.
This provides a tremendous
amount of type-safety—your methods can
take Integers, Strings, Lists, Maps, or your own custom objects as parameters, and know at the outset what they'll have to work with.List, when you already know what's in the List (such as when
you fill it yourself, or a trusted source handles populating it): List listOfStrings = getListOfStrings( );
for (Iterator i = listOfStrings.iterator( ); i.hasNext( ); ) {
String item = (String)i.next( );
// Work with that string
}
String)—and you'll get a compiler
error: [javac] Compiling 1 source file to code\classes
[javac] code\src\com\oreilly\tiger\ch02\GenericsTester.java:17:
incompatible types
[javac] found : java.lang.Object
[javac] required: java.lang.String
[javac] String item = i.next( );
[javac] ^
[javac] Note: code\src\com\oreilly\tiger\ch02\GenericsTester.java uses
unchecked or unsafe operations.
[javac] Note: Recompile with -Xlint:unchecked for details.
[javac] 1 error
getListOfStrings( ) method, the
compiler doesn't trust it one bit. It assumes the worst, and if you've ever
had anyone else work with you, you realize the compiler is often right
more than you are.List class, it wouldn't be much good if that
was the only collection that could be parameterized. All of the various
collection classes are now generic types, and accept type parameters.
Since most of these behave like List, I'll spare you the boring prose of
covering each one. It is worth looking at Map, though, as it takes two type
parameters, instead of just one. You use it just as you use List, but with
two types at declaration and initialization.java.util.Map has a
key type (which can be any type) and a value type
(which can be any type). While it's common to use a numeric or String
key, that's not built into the language, and you can't depend on it—at
least, not until Tiger came along: Map<Integer, Integer> squares = new HashMap<Integer, Integer>( );
for (int i=0; i<100; i++) {
squares.put(i, i*i);
}
for (int i=0; i<10; i++) {
int n = i*3;
out.println("The square of " + n + " is " + squares.get(n));
}
Map is declared, and both its
key and value types are defined as Integer. This ensures that you don't
have to do any casting, either in putting values into the Map or pulling them out. Pretty easy stuff, isn't it? Of course, you could use any of the
following lines of code as well: // Key and value are Strings
Map<String, String> strings = new HashMap<String, String>( );
// Key is a String, value is an Object
Map<String, Object> map = new HashMap<String, Object>( );
// Key is a Long, value is a String
Map<Long, String> args = new HashMap<Long, String>( );
Map is defined to take Integers, it's the int counter i that is used to
create values. Without getting into the details covered in Chapter 4, Java
autoboxes the int value of ifor/in loop provides a
means of almost completely avoiding
the java.util.Iterator class, that particular feature of Tiger isn't
covered until Chapter 7. But until you get to that chapter (and probably
occasionally after that), it's still useful to know how generic collection
types affect Iterator. You'll need to perform an extra step to get the full
power of generics.Iterator and using it would be trivial: List<String> listOfStrings = new LinkedList<String>( );
listOfStrings.add("Happy");
listOfStrings.add("Birthday");
listOfStrings.add("To");
listOfStrings.add("You");
for (Iterator i = listOfStrings.iterator( ); i.hasNext( ); ) {
String s = i.next( );
out.println(s);
}
[javac] code\src\com\oreilly\tiger\ch02\GenericsTester.java:54:
incompatible types
[javac] found : java.lang.Object
[javac] required: java.lang.String
[javac] String s = i.next( );
[javac] ^
[javac] Note: code\src\com\oreilly\tiger\ch02\GenericsTester.java
uses unchecked or unsafe operations.
[javac] Note: Recompile with -Xlint:unchecked for details.
[javac] 1 error
List, you
haven't parameterized your Iterator. It's still spitting out Objects, and
doesn't know that it should only expect to receive and respond with
String types. Just like the collections, Iterator is a generic type in Java,
and is declared as public interface Iterator<E>. Its next( ) method,
then, returns E (which is a placeholder,
as detailed in "Using Type-Safe
Lists"). To parameterize it, you use the same syntax as you did for collection
classes: List<String> listOfStrings = new LinkedList<String>( );
listOfStrings.add("Happy");
listOfStrings.add("Birthday");
listOfStrings.add("To");
listOfStrings.add("You");
for (Iterator private void printListOfStrings(List<String> list, PrintStream out)
throws IOException {
for (Iterator<String> i = list.iterator( ); i.hasNext( ); ) {
out.println(i.next( ));
}
}
Iterator as well, because the compiler ensures that only List<String>
is passed into the method. Any other List types are refused (at compiletime).List, without any parameterization, even if
it has only Strings in it? This actually will work, with the caveat that
you're left to your own devices in ensuring that the List has in it what
it's supposed to. If not, you'll get more ClassCastExceptions than you
can shake a stick at, all at runtime. In either case, you'll get lint warnings,
which are described in "Checking for Lint," later in this chapter.getListOfStrings( ) method, referred to in "Using Type-Safe Lists"? Here is the actual code for that method: private List getListOfStrings( ) {
List list = new LinkedList ( );
list.add("Hello");
list.add("World");
list.add("How");
list.add("Are");
list.add("You?");
return list;
}
lint
warnings (see Checking for Lint for details) because it doesn't specify a
type for the List. Even more importantly, code that uses this method
can't assume that it is really getting a List of Strings. To correct this,
just parameterize the return type, as well as the List that is eventually
returned by the method:private List<String> getListOfStrings( ) { List<String> list = new LinkedList<String>(); list.add("Hello"); list.add("World"); list.add("How"); list.add("Are"); list.add("You?"); return list; }
List<String> strings = getListOfStrings( );
for (String s : strings) {
out.println(s);
}
getListOfStrings( ) has a parameterized return value.Map interface takes two type parameters: one for the key, and one for
the value itself. While the key is usually a String or numeric ID, the
value can be anything—including a generic type, like a List of Strings.List<String> becomes a parameterized type, which can be supplied
to the Map declaration:Map<String, List<String>> map = new HashMap<String, List<String>>( );
Map<String, List<List<int[]>>> map = getWeirdMap( );
int value = map.get(someKey).get(0).get(0)[0];
List, but instead can just let the compiler
unravel all your parameterized types for you.lint warnings, which
sounds more like something you get out of a dryer than a compiler. These
warnings are a new feature of Tiger, though, and important in figuring
out how to really bulletproof your code. private List getList( ) {
List list = new LinkedList( );
list.add(3);
list.add("Blind");
list.add("Mice");
return list;
}
-source 1.5 flag, you'll get this
message: Note: GenericsTester.java uses unchecked or unsafe operations.
Note: recompile with -Xlint:unchecked for details.
lint warnings (-Xlint), and specifically to show those warnings
that are unchecked. .
[javac] code\src\com\oreilly\tiger\ch02\GenericsTester.java:63: warning:
[unchecked] unchecked call to add(E) as a member of
the raw type java.util.List
[javac] list.add(3);
[javac] ^
[javac] src\com\oreilly\tiger\ch02\GenericsTester.java:64: warning:
[unchecked] unchecked call to add(E) as a member of
the raw type java.util.List
[javac] list.add("Blind");
[javac] ^
[javac] src\com\oreilly\tiger\ch02\GenericsTester.java:65: warning:
[unchecked] unchecked call to add(E) as a member of
the raw type java.util.List
[javac] list.add("Mice");
[javac] ^
[javac] 3 warnings
list in this case) are the intended type.
That's because List of Integers
gets tossed into that Map of Numbers...then again, it's not quite that easy.
You'll need to take great care if you want these conversions to actually
work.LinkedList<Float> floatList = new LinkedList<Float>( );
LinkedList, not
Float. So this is legal:List<Float> moreFloats = floatList;
LinkedList<Number> numberList = floatList;
Float is indeed a subclass of Number, it's the generic type that is
important, not the parameter type.LinkedList<Float> to a LinkedList<Number> (or even to a
LinkedList<Object>) should indeed be illegal: List<Integer> ints = new ArrayList<Integer>( );
ints.add(1);
ints.add(2);
// This is illegal, but use it for illustration purposes
List<Number> numbers = ints;
// Now a float is being added to a list of ints! This results in a
// ClassCastException when the item is retrieved from the
// list and used as an int (instead of a float)
numbers.add(1.2);
// This is even worse
List<Object> objects = ints;
objects.add("How are you doing?");
List, or Map, or whatever, without parameterization. This is going to result in unchecked errors, unless you employ
the generics wildcard.List: public void printList(List list, PrintStream out) throws IOException {
for (Iterator i = list.iterator( ); i.hasNext( ); ) {
out.println(i.next( ).toString( ));
}
}
printList( ) takes any
List. This is where the wildcard operator comes in, which for generics is a question mark (?). Make the following
change:public void printList(List<?> list, PrintStream out) throws IOException { for (Iterator<?> i = list.iterator( ); i.hasNext( ); ) { out.println(i.next( ).toString( )); } }
List<Object> to get around this same problem? You might want
to review Generics and Type Conversions, and see if you really want to do that. A List<Integer>
cannot be passed to a method that takes a List<Object>, remember? So your printList( ) method would be limited to collections defined as List<Object>, which isn't much use at all. In these cases, the wildcard really is the only viable solution.package com.oreilly.tiger.ch02;
import java.util.ArrayList;
import java.util.List;
public class Box<T> {
protected List<T> contents;
public Box( ) {
contents = new ArrayList<T>( );
}
public int getSize( ) {
return contents.size( );
}
public boolean isEmpty( ) {
return (contents.size( ) == 0);
}
public void add(T o) {
contents.add(o);
}
public T grab( ) {
if (!isEmpty( )) {
return contents.remove(0);
} else
return null;
}
}
Box<String> box = new Box<String>( );
T with String for that specific
instance, and suddenly you've got yourself a String Box, so to speak.Box<Integer>, a Box<String>, and a Box<List<Float>>, all
with a shared static variable. That variable, then, cannot make assumptions
about the typing of any particular instance, as they may be different.
It also cannot use a parameterized type—so the following is illegal:Box that only accepts numbers—and
further, that based on that, you want to add some functionality that's
specific to numbers. To accomplish this, you need to restrict the types
that are allowed.className onto
your type variable, and voila! Check out Example 2-3.package com.oreilly.tiger.ch02;
import java.util.Iterator;
public class NumberBox<N extends Number> extends Box<N> {
public NumberBox( ) {
super( );
}
// Sum everything in the box
public double sum( ) {
double total = 0;
for (Iterator<N> i = contents.iterator( ); i.hasNext( ); ) {
total = total + i.next( ).doubleValue( );
}
return total;
}
}
Number (or
Number itself). So the following statement is illegal:NumberBox<String> illegal = new NumberBox<String>( );
[javac] code\src\com\oreilly\tiger\ch02\GenericsTester.java:118:
type parameter java.lang.String is not within its bound
[javac] NumberBox<String> illegal = new NumberBox<String>( );
[javac] ^
[javac] code\src\com\oreilly\tiger\ch02\GenericsTester.java:118:
type parameter java.lang.String is not within its bound
[javac] NumberBox<String> illegal = new NumberBox<String>( );
[javac] ^
public static double sum(Box<? extends Number> box1,
Box<? extends Number> box2) {
double total = 0;
for (Iterator<? extends Number> i = box1.contents.iterator( );
i.hasNext( ); ) {
total = total + i.next( ).doubleValue( );
}
for (Iterator<? extends Number> i = box2.contents.iterator( );
i.hasNext( ); ) {
total = total + i.next( ).doubleValue( );
}
return total;
}Grade that can only be
assigned values of A, B, C, D, F, or Incomplete. Any other values are illegal
for this type. This sort of construct is possible prior to Tiger, but it
takes a lot of work, and there are still some significant problems.enum keywordenum keywordGrade object.
package com.oreilly.tiger.ch03;
public enum Grade { A, B, C, D, F, INCOMPLETE };
package com.oreilly.tiger.ch03;
public class Student {
private String firstName;
private String lastName;
private Grade grade;
public Student(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getFirstName( ) {
return firstName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getLastName( ) {
return lastName;
}
public String getFullName( ) {
return new StringBuffer(firstName)
.append(" ")
.append(lastName)
.toString( );
}
DownloadStatus enum, for example, but only
within a Downloader class:
public class Downloader {
public enum DownloadStatus { INITIALIZING, IN_PROGRESS, COMPLETE };
// Class body
}
public class Downloader {
public static enum DownloadStatus { INITIALIZING, IN_PROGRESS, COMPLETE };
// Class body
}
static modifier has been added. This has no effective
change on the enum, as nested enums are implicitly static. In other
words, it's sort of like declaring an interface abstract—it's redundant.
Because of this redundancy, I'd recommend against using the static
keyword in these declarations.values( ) method. This method provides
access to all of the types within an enum.values( ) method on an enum returns an array of all the
values in the type:
public void listGradeValues(PrintStream out) throws IOException {
Grade[] gradeValues = Grade.values( );
for (Grade g : Grade.values( )) {
out.println("Allowed value: '" + g + "'");
}
}
run-ch03:
[echo] Running Chapter 3 examples from Java 5.0: A Developer's Notebook
[echo] Running GradeTester...
[java] Allowed value: 'A'
[java] Allowed value: 'B'
[java] Allowed value: 'C'
[java] Allowed value: 'D'
[java] Allowed value: 'F'
[java] Allowed value: 'INCOMPLETE'
values( ) doesn't return an
array of String values—instead it returns an array of Grade instances. In