Home Collections and objects
Interfaces
When declaring or initializing an object, you should always make the declared type an interface, not a concrete object.
When declaring an object, you should never do something like this:
ArrayList<String> lst = new ArrayList<String>();
Instead, whenever possible, you should change the declared type to use an interface, like so:
List<String> lst = new ArrayList<String>();
The reason for this is because in programming, we often like to make the distinction between an actual object, and an interface.
For example, let's say that we want to program different kinds of cars – perhaps we might have a Toyota, perhaps we'll have a Ford, etc... Each of these cars have different internals and parts, so the actual code to control them is going to be all slightly different from each other. For the sake of simplicity, let's just say we have two different classes – a "Toyota" class, and a "Ford" class.
Now, let's say that we want to create a method that'll be responsible for turning this car on, and driving it in a line for about 5 seconds, then turning it off in order to test the car.
Our tester program might look something like this:
public class TestCar { public static void main(String[] args) { Toyota t = new Toyota(); Ford f = new Ford(); testToyota(t); testFord(f); } public static void testToyota(Toyota t) { t.turnOn(); t.driveForward(5); t.turnOff(); } public static void testFord(Ford f) { f.turnOn(); f.driveForward(5); f.turnOff(); } }
Ok, cool, this works.
However, just looking at it, it seems sort of redundant, and a little silly. If all we want to do is just move the car forward for 5 seconds, why does it matter what kind of car it is? Forget about the internals and the parts – the steps to turn a car on and drive it forward are going to be pretty much the same, no matter what brand or model the car actually is. If the steps are the same, why do we need to make this distinction between the Ford and the Toyota?
Well, we can solve this problem by using interfaces. If we modify our Toyota or Ford cars to either inherit from a "Car" base class or extend the "Car" interface, then we can modify our testing program to look like this:
public class TestCar2 { public static void main(String[] args) { Car t = new Toyota(); Car f = new Ford(); testCar(t); testCar(f); } public static void testCar(Car c) { c.turnOn(); c.driveForward(5); c.turnOff(); } }
This is much cleaner. By doing Car t = new Toyota()
, we're telling Java
"yes, I'm making a new Toyota, but I want you to treat it as a Car". Then, our
testCar
method can now accept any kind of car, regardless of the specific
brand or type, and be flexible enough to handle all those different kinds of cars.
This is similar to the Critters assignment in CSE 142 – you had the "Critter" base class or interface, and made subclasses of it – the Ant, Husky, Lion, etc classes.
This same principle applies to Java's classes. When you do
ArrayList<String> words = new ArrayList<String>();
...you're telling Java to create a new instance of an ArrayList, and treat it as an ArrayList. In contrast, if you do this:
List<String> words = new ArrayList<String>();
...you're telling Java to create a new instance of an ArrayList, but treat it as a general "List" object. This is much more flexible -- if you change your mind about using an ArrayList and want to use a different kind of list instead, then you only need to modify a single line:
List<String> words = new LinkedList<String>();
To put in other words, this line is essentially saying "yes, I have to pick a certain kind of list (in this case, a LinkedList), but I'd like my code to work with pretty much any type of List in existence without any fuss". Again, we want our code to be as flexible and modular as possible.
Avoid creating unnecessary objects
You should create just as many new objects as you need to solve a problem – no more, and no less.
Note that creating new objects is not the same thing as declaring new variables. You are allowed to declare as many variables as you'd like.
When programming, it is often tempting to create a large number of objects such as
List
s, Stack
s, Queue
s, Set
s,
Map
s, and so forth.
However, it is important to keep in mind that each new object you instantiate will have a slight cost, either by having complex logic in their constructor, or by allocating a chunk of memory.
While in most cases, these costs are relatively low (and in fact, Java specializes in efficiently creating and managing a huge number of objects), we still want you to get into the habit of carefully thinking through what exactly you need when writing your code.
For example, if you have a list of numbers, wanted to subtract 5 from each number, retain all even numbers, then convert those numbers to a string, it would NOT be appropriate to do something like this:
public List<String> transform(List<Integer> numbers) { List<Integer> temp1 = new ArrayList<Integer>(); for (int num : numbers) { temp1.add(num - 5); } List<Integer> temp2 = new ArrayList<Integer>(); for (int num : temp1) { if (num % 2 == 1) { temp2.add(num); } } List<String> out = new ArrayList<String>(); for (int num : temp2) { out.add("foo-" + num); } return out; }
Rather, it would be more efficient and would require less memory to do something like so:
public List<String> transform(List<Integer> numbers) { List<String> out = new ArrayList<String>(); for (int num : numbers) { num -= 5; if (num % 2 == 1) { out.add("foo-" + num); } } return out; }
That said, it is ok to create new objects in cases where it's not strictly speaking necessary if doing so will result in cleaner code. For example, consider the following code:
public static void foo(List<String> lines) { for (String line : lines) { // New ArrayList is created each iteration List<T> accum = new ArrayList<String>(); for (String word : line.split(" ") { if (word.startswith("a")) { accum.add(word); } } System.out.println(accum); } }
Although we are creating a new ArrayList each iteration, doing so here is ok partially because Java is free to garbage-collect the ArrayLists we're creating at the end of each iteration, which keeps memory usage at a reasonable level, and partially because the alternative approach is clearly worse here:
public static void foo(List<String> lines) { List<T> accum = new ArrayList<String>(); for (String line : lines) { for (String word : line.split(" ") { if (word.startswith("a")) { accum.add(word); } } System.out.println(accum); accum.clear(); } }
While this snippet of code does create only one new object instead of many, it's harder
to understand because the accum
variable is scoped in a way such that it
implies that it's keeping track of data between each iteration, which is misleading.
The actual amount of memory in use is also the same between both snippets because we only have a reference to only one object at a time in both examples.