This article is about the original Java interface before version 1.8. It is about the role of interfaces as well-documented software-component boundaries. More precisely, it is about the compiler's behavior when it meets interfaces.
Since Java version 1.8, interfaces can contain implementations, both static and instance-bound ones, which was not possible before. Thus they are more like abstract classes now, but featuring multiple inheritance.
In case you ever tried to compile a Java application from command line, you may have noticed that the Java compiler would not compile every class that your Main
class depends on.
cd src/main/java
javac interfaces/compilation/Main.java
It would find any constructor call and compile the referenced class, recursively. But it would not search for classes that are behind interfaces, i.e. it would not compile classes implementing an interface that your Main
class uses if there is no constructor call to these classes.
That means the compiler regards interfaces to be boundaries towards implementations that may be known at runtime only. Such would actually work (when accurately deployed), without losing strict type checks, because interfaces provide types.
Try to compile following classes using the command line above (you can use any Java compiler, also above 1.8).
Here is an interface:
package interfaces.compilation;
public interface Drum
{
void sound();
}
Here is the application that uses that interface, but loads its implementations not through constructor calls but through reflective calls (not detectable by the compiler):
package interfaces.compilation;
public class Main
{
public static void main(String[] args) throws Exception {
final Drum bassDrum = (Drum) Class.forName("interfaces.compilation.BassDrum")
.getDeclaredConstructor()
.newInstance();
bassDrum.sound();
final Drum snareDrum = (Drum) Class.forName("interfaces.compilation.SnareDrum")
.getDeclaredConstructor()
.newInstance();
snareDrum.sound();
}
}
Here are two different implementations for the interface, in the very same package:
package interfaces.compilation;
public class BassDrum implements Drum
{
public void sound() {
System.out.println("dumb");
}
}
package interfaces.compilation;
public class SnareDrum implements Drum
{
public void sound() {
System.out.println("jack");
}
}
When all classes have been compiled and deployed, output of this application should be:
java -cp . interfaces.compilation.Main
dumb
jack
Having compiled using the command-line above, the resulting .class
files should be in same directory as the .java
sources. You will observe that just Main.java
and Drum.java
have been compiled:
That means the application would not work when launched:
java -cp . interfaces.compilation.Main
Exception in thread "main" java.lang.ClassNotFoundException: interfaces.compilation.BassDrum
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:583)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
at java.base/java.lang.Class.forName0(Native Method)
at java.base/java.lang.Class.forName(Class.java:315)
at interfaces.compilation.Main.main(Main.java:6)
Now try to replace the dynamic class loading by concrete constructor calls:
package interfaces.compilation;
public class Main
{
public static void main(String[] args) {
final Drum bassDrum = new BassDrum();
bassDrum.sound();
final Drum snareDrum = new SnareDrum();
snareDrum.sound();
}
}
Here is what you get:
Now the compiler detected any dependency inside the Main.java
source and compiled it. Launching the application now yields:
java -cp . interfaces.compilation.Main
dumb
jack
So why would we need dynamically loaded "components"?
Since Java 9 we need to ask: Why would we need "service modules"?
Software needs to be highly configurable. That means we want it to behave accordingly to the environment where it runs. We could choose some configuration library and implement lots of if-conditions and feature-toggles in our source code. Or we could define responsibilities via interfaces and rely on the right interface-implementations being deployed and thus present in CLASSPATH at runtime.
That's the way most Java applications work nowadays. Dependency injection (DI) has been invented to do that. Good DI containers require interfaces to represent component boundaries. Component-oriented software also demands a new responsibility: deployment. These people build together an application fitting to the customer.
Here is an example of dynamic service loading as it is possible since Java 1.6.
Mind that this still works in Java above or equal 9, but services are now defined inmodule-info.java
files instead ofMETA-INF/services/<name.of.interface>
text files containing the fully qualified class-names of implementations.
The service definition file in META-INF
directory must be named exactly like the fully-qualified class-name of the service interface, in this case interfaces.compilation.Drum
. It contains the fully-qualified class-names of all implementations, separated by newlines:
fri.interfaces.compilation.BassDrum
fri.interfaces.compilation.SnareDrum
Here is the application using service implementations through the JDK's ServiceLoader
utility class:
package interfaces.compilation;
import java.util.Iterator;
import java.util.ServiceLoader;
public class Main
{
public static void main(String[] args) {
final ServiceLoader<Drum> serviceLoader = ServiceLoader.load(Drum.class);
final Iterator<Drum> iterator = serviceLoader.iterator();
while (iterator.hasNext()) {
final Drum drum = iterator.next();
drum.sound();
}
}
}
Output of this application:
java -cp . interfaces.compilation.Main
dumb
jack
Java interfaces are more than just contracts between software components. Besides the new Java 8 features, the original Java interfaces could also provide "function pointers", i.e. class-methods that can be passed around as parameters, working even without the new @FunctionalInterface
annotation. This will be subject of my next Blog.
ɔ⃝ Fritz Ritzberger, 2021-03-09