03 Prepare : Reading
Advanced Java Types and Collections
Objectives
Understand some nuances of the Java typing system.
Understand Java collections.
Understand how Generics work in Java.
Understand the role that interfaces play in the use of collections.
Java Types
Let's take a moment to discuss Java's basic data types. Like most languages, Java has int
, float
, char
, etc.... These are called primitive types. They are memory efficient and take up relatively little space.
In addition to primitive types, Java also has wrapper classes for each of the primitives. For example, there is a class called Integer
that serves as a wrapper class for the int
primitive. Aside from providing some useful functionality, these wrapper classes allow us to use primitive data types in situations that require objects, such as when dealing with collections:
Integer myInt = 3;
List<Integer> myNumbers;
myNumbers.add(myInt);
Java will automatically convert back and forth between primitives and wrapper classes as needed through a process called autoboxing:
Integer myInt = 3;
System.out.println(myInt + 2);
People try to avoid the use of wrapper classes and autoboxing whenever possible, because there are pretty strong performance hits in their use.
Collections
Collections are containers that allow us to efficiently store and process data. In C++, you likely worked with vectors, and possibly trees, maps, and linked-lists. These are all types of collections.
In Java, all collections implement the Collection
interface. This interface defines a list of methods that all collections must implement. Some of these include:
add()
A way to add elements to a collection.clear()
A way to remove all elements from a collection.contains()
A way to see if a collection contains an element.size()
A way to see how many elements a collection contains.etc...
Take some time to look over the complete list of methods every collection must provide, by looking at the official documentation for the Collection interface.
Collection Interfaces and Generics
Java provides several subinterfaces that extend the collection interface. Some commonly used ones are List
, Map
, and Set
. Each of these interfaces define additional methods that are appropriate for those types of collections.
Java's collections are all "generic" types. They work in a similar way to how templates work in C++. Last week, we saw a List
of Creature
objects could be declared like this:
List<Creature> someCreatures;
As with C++, the type of thing that will be stored in the collection goes inside the angle brackets. Once a collection has been initialized, only objects of that type (or objects that inherit from that type) may be stored in the collection.
Note that it's not just class names we can use as the generic type. We can also use interfaces:
List<Movable> someThingsThatMove;
Concrete Implementations
Java also provides several concrete classes that implement the various collection interfaces. (A concrete class is a class you can instantiate using the new
keyword, as opposed to an abstract class or interface).
You can see a list of all the classes that implement a particular interface by looking at the All Known Implementing Classes: section of the interface documentation. For example, the documentation for the List interface shows several classes that implement that interface. The most commonly used are ArrayList
and LinkedList
.
Typically, when we use a collection, we instantiate it using the concrete class whose attributes we need, but we store it as an interface:
List<Creature> creatures = new ArrayList<Creature>();
By storing it as an interface, the rest of our code is isolated from having to change if we decide on a different implementation, as long as we have something that implements List
, nothing else has to change.
Collections Example
Consider this example:
You've been working on an asteroids game, and you need to store a collection of Rock
classes. You decide to use ArrayList
because it's the most familiar to you. You write your AsteroidManager
class that keeps track of all of the asteroids like this:
public class AsteroidManager {
ArrayList<Asteroid> _asteroids;
public AsteroidManager() {
_asteroids = new ArrayList<Asteroid>();
}
public ArrayList<Asteroid> getAsteroids() {
return _asteroids;
}
...
}
Let's imagine that this is a complex program with lots of other classes interacting with the AsteroidManager
class. Each of those classes calls the getAsteroids()
function at different times and performs operations on the ArrayList
.
One day, you realize that ArrayList
is actually the wrong choice for this situation, because you frequently need to add and remove asteroids at the end of the list, a task that the LinkedList
class is better suited for. So you decide to change your AsteroidManager
class to use a LinkedList
:
public class AsteroidManager {
LinkedList<Asteroid> _asteroids;
public AsteroidManager() {
_asteroids = new LinkedList<Asteroid>();
}
public LinkedList<Asteroid> getAsteroids() {
return _asteroids;
}
...
}
Unfortunately, not only did you have to change the AsteroidManager
code in several places, but you'll also have to modify every single class that calls getAsteroids()
, because the return type has changed.
This problem could have been prevented if we had initially used the List
interface to store our ArrayList
:
public class AsteroidManager {
List<Asteroid> _asteroids;
public AsteroidManager() {
_asteroids = new ArrayList<Asteroid>();
}
public List<Asteroid> getAsteroids() {
return _asteroids;
}
...
}
Now, to switch to a LinkedList
, we only need to change one line of code:
_asteroids = new LinkedList<Asteroid>();
All the rest of our code just cares that we're using something that implements the List
interface, no matter what that something is. These types of situations are one of the main reasons why we "program to an interface, not an implementation".
Primitive Arrays
One downside to the collection classes like ArrayList
is that they can only store objects. While wrapper classes or autoboxing will allow us to store primitive values using these classes, there is enough of a performance penalty that we usually want to avoid it.
So, instead of using something like ArrayList
to store a list of int
values, we can use a simple array, just like in C++:
int[] a = {1, 2, 3};
System.out.println(a[0]);
A couple of things to note about Java arrays:
First, once an array has been created, its size cannot be changed. (You can modify its values, but you can't add or remove values.)
Second, Java arrays can store anything, including objects (though we usually use a Java collection class for those situations):
// Create an array that can hold six Student objects
Student[] students = new Student[6];
students[0].printName();
Finally, Java arrays always know their length:
// Create an array that can hold four floats
float grades[] = new float[4];
// Prints 4
System.out.println(grades.length);
Reading
Read the following resources to gain a better understanding of these principles:
Start with this tutorial about Java wrapper classes.
Next, read these Stack Overflow posts to see the pros and cons of using the wrapper classes:
-
Also, take a look at the first and second answers to this post to see the "gotcha" that can occur with autoboxing.
-
Make sure you also understand how to compare two Strings in Java.
-
Then, read over these tables that describe some of the differences between the different collection interfaces and their concrete implementation classes.
-
This article from Oracle provides a nice summary of how the collection interfaces are related.
-
Finally, take a look at this tutorial for some code examples that use collections.
Reading Quiz
Don't forget to take the Reading Quiz in I-Learn. This quiz can be taken as many times as you like, but you must score at least 90% to pass. If you fail the quiz, review the relevant parts of the reading and try again.
One of the quiz questions is a "deep thought" question. The answer to this question won't come directly from the reading, but will require you to think deeply about what you've learned from the reading.