For this course, being comfortable with object oriented programming concepts and having a good understand of why they are beneficial is required.
Let’s start with interfaces!
First, what are interfaces?
We could say that interfaces are “contracts” between the implementers of some code and the users of that code.
In other words, in an interface, you are stating what methods you guarantee will be provided by any class implementing that interface.
Next, how do interfaces differ from classes?
The old school explanation would be that interfaces can not contain implementations of methods. They may only contain method signatures.
(This isn’t actually true anymore, though)
public interface Transform {
String apply(final String input);
}
This interface represents the abstract concept of code which performs String transformations.
Note that this code does not do anything! It just states that any class implementing this interface will provide a method called apply that will perform a String transformation.
Now how about classes?
We mentioned above that classes contain actual implementation details. In other words, they contain code that actually does stuff.
public class Reverse {
public String transform(final String input){
final StringBuilder sb = new StringBuilder();
for(int i=input.length-1; i>=0; i--){
sb.append(input.charAt(i));
}
return sb.toString();
}
}
Is this a class?
Does it perform a String transformation?
Does it implement the Transform interface?
public class Reverse implements Transform {
public String apply(final String input){
// ...
}
}
Now we have stated that we are agreeing to the “contract” defined by Transform
final Reverse transformer = new Reverse();
System.out.println(transformer.apply("Hello");
final Transform transformer = new Reverse();
System.out.println(transformer.apply("Hello"));
What’s the difference here? Why would we do this?
At this point, there is no real reason to use interfaces at all. What happens, though, when we decide to add another type of transformation to our code.
if(doReverse){
final Reverse reverse = new Reverse();
return reverse.apply("Hello");
}else if(doNoVowels){
final NoVowels noVowels = new NoVowels();
return noVowels.apply("Hello");
}else if ...
While all of these classes do different things, they all, at a high level, are transforming Strings.
So, if we use the Transform interface for all of them, then we can unify them with one variable.
final Transform transform;
if(doReverse){
transform = new Reverse();
}else if(doNoVowels){
transform = new NoVowels();
}else if ...
return transform.apply("Hello");
This may not seem to impressive, but what if the logic choosing the transformation is not in our code?
What if we have to pass a transform into someone elses code?
What if we need a collection of transformations?
final SomeoneElses code = new SomeoneElsesCode();
// We have no idea what this could possibly return!
final Transform transform = code.getTransform();
// ...but we can use it anyway, because we know its interface!
return transform.apply("Hello");
In someone else’s code:
public void setTransform(final Transform t){
transform = t;
}
In our code:
//They have no idea what this is
final Transformer tx = new Reverse();
//...but they can still use it!
code.setTransform(tx);
final List<Transform> txs = new ArrayList<>();
txs.add(new Reverse());
txs.add(new NoVowels());
//...
What are all of the angle brackets?
final List<Transform> txs = new ArrayList<>();
I guess we’re going to need to talk about generics.
We mentioned how an interface can be used to represent different classes by referring to them as instances of the interface instead.
We can also do this with an ancestor classes, if all of the classes have a common ancestor. So, in the bad old days, a List would simply store Objects, because Object is a common ancestor for all other classes.
This led to problems:
final List txs = new ArrayList();
txs.add(new Reverse());
// Not an error, because it's an object
txs.add("Hello");
// Have to cast, because we need a Transform, not an Object
final Transform tx = (Transform)txs.get(1);
To fix this, generics were introduced to allow for more type safe code to be written:
final List<Transform> txs = new ArrayList<>();
txs.add(new Reverse());
//Now this is an error
txs.add("Hello");
//And a cast wouldn't be needed if the above didn't crash!
final Transform tx = txs.get(0);
Let’s improve our Transform interface now:
public interface Transform<T,U> {
U apply(final T input);
}
Now we’re not limited to String transformations!
public class Reverse implements Transform<String,String> {
public String apply(final String input){
//...
}
}
final List<Transform<String,String>> stringTransformers;
final List<Transform<Integer,Integer>> intTransformers;
final List<Transform<String,Integer>> stringToIntTransformers;
Can you feel the power??? Programming is so awesome.
Ok, let’s dial things back a bit and get back to some basics.
What is inheritance? How does it work?
Previously, we implemented an interface.
Sometimes, though, we want to extend a class.
When a class B extends a class A, then the class B will inherit the methods and fields of class A.
public class ReReverse extends Reverse {
}
What will the following do? Will it compile?
final Reverse r1 = new ReReverse();
System.out.println(r1.apply("hello");
public class ReReverse extends Reverse {
public String apply(final String input){
final String reversed = super.apply(input);
final String reReversed = super.apply(reveresed);
return reversed;
}
}
Super?
What will the following do?
final Reverse r1 = new ReReverse();
System.out.println(r1.apply("hello");
final ReReverse r2 = new Reverse();
System.out.println(r2.apply("hello");
final Transform t = new ReReverse();
System.out.println("hello");
When we declare a variable’s type, that tells us what fields and methods must exist, but it might not be the precise type of the value bound to that variable.
(The actual value may have more fields and methods!)
When we call a method declared in the parent class, the JVM will determine, at run time, what the actual type of the value is, and will use this information to select the correct implementation of the called method.
What is the correct implementation?
If the child class has not overridden the method, then the parent class will be checked for an implementation.
If the child class has overriden the method, then the implementation in the child class is chosen.
Remember, though, that you can have several levels of inheritance!
Don’t forget that access modifiers affect what fields and methods are visible to children!
If, for example, a parent class declares a private field, the child can not access it!
none | private | protected | public | |
---|---|---|---|---|
class | Y | Y | Y | Y |
package | Y | N | Y | Y |
subclass | N | N | Y | Y |
world | N | N | N | Y |
A common mistake students (and professionals!) make is to assume that the access modifiers apply to instances of a class. This is not true!
For instance, a private field in class A is visible ot every instance of A!
For example, this code is perfectly valid!
public class A {
private final int x;
//...
public void compareXs(final A anotherA){
// works even though x is private!
return x==anotherA.x;
}
}
Let’s circle back around to some of the basics:
An interface (sort of) only provides method signatures.
A class must be completely implemented.
An abstract class is when you want something in between.
public abstract class Censor
implements Transform<String,String> {
public abstract List<String> getBadWords();
public String apply(String input){
for(final String word : badWords){
input = input.replace(word, "!!!")
}
return input;
}
}
Now we can create different variations by subclassing and implementing getBadWords.
What if we want to inherit from two classes?
You can’t!
You can implement several interfaces, but you can only extend one class (concrete or abstract).
However…
You can now, actually, provide “default” implementations of methods in interfaces.
Which means that you can often accomplish what you were trying to do with two parent classes by instead implementing two interfaces with default methods.
There are, however, limitations. First, remember that you can only store static, constant fields in an interface. If you need instance data for your default methods, you are out of luck.
You can, though, add a getter to the interface for the instance data, and use that in your default method.
Then implementers will declare their own instance fields and implements the getter.
public interface Censor extends Transform<String,String> {
List<String> getBadWords();
default String apply(String input){
for(final String bad : getBadWords()){
input = input.replace(bad, "!!!");
}
return input;
}
}
Another common mistake is to try and implement an interface in another interface. This makes no sense, because you aren’t (generally) providing an implementation!
So, interfaces extend other interfaces!
An interesting use for multiple interfaces is to create an interface for each kind of behavior that might be needed, and mixing them together.
public interface Labeled {
public String getLabel();
}
public interface Logged {
public List<String> getLog();
}
public class Reverse
implements Transform<String,String>, Labeled, Logged {
private final List<String> log = new ArrayList<>();
public String getLabel(){return "Reverse (Transform)";}
public List<String> getLog(){return log;}
public String apply(final input){
log.add("Starting transform of input: " + input);
//...
log.add("Transform complete:" + result);
return result;
}
}
So, in addition to being able to use this class wherever we need a Transform, we can use it anywhere we expect, for example, instances with a label.
For example, imagine a UI where we can select operations to perform. Some might be transformers, other might not be, but we can put them all in a common UI widget with a nicely displayed label if they all implement the Labeled interface!
At runtime, though, we may not know what interfaces are implemented, so we will need to check using the instanceof operator.
public void doTransform(final Transform<String,String> t){
final String input = getInput();
final String output = t.apply(input);
if(t instanceof Logged){
final Logged logged = (Logged) t;
printLog(logged.getLog());
}
writeOutput(output);
}