Code as Data: Java 8 Interfaces
- Interface Advantages / Java 8 Default Methods
- Code as Data in Java 8: Lambda Expressions / Installing Java 8
Why are interfaces so important in programming? Is an interface a form of contract? This talk of interfaces all seems very complicated. Is it really worth going to the trouble of designing interfaces?
These are all good questions! The answer is that interfaces are contracts, they are really important artifacts, and designing them well is an excellent investment. Why is this so?
Interfaces are an example of the Separation of Concerns design pattern. Separation of Concerns is one of the keys to successful development because it's an example of the principle of "divide and conquer," where you break down a complicated problem into a series of simpler problems. By defining an interface in advance of writing the implementation code, you give yourself an important opportunity to step away from the nitty-gritty details. This distancing provides you with a design overview: Is your design right or wrong? As technology advances, increasing demands on programmers give us fewer opportunities to do this kind of self-directed reviewing. In this context, interfaces are hugely beneficial.
To view or use the code examples in this article, download the code file.
Interface Advantages
In a nutshell, interfaces provide the basis for a sound software architecture. In other words, each interface you design can ultimately be seen as—or even become—a node in your architecture. If your software structure resides on one machine, then at least in theory you should be able to divide the code into node-specific elements as defined in the underlying interfaces. Given the massive proliferation of virtual machine technology, machines are probably cheaper than they have ever been. Dividing software onto separate machines is almost free, aside from the administrative burden. Such division onto virtual machines can help to improve scalability and performance.
Another merit of interfaces is extensibility. As the clients of a given Java interface mature, additional methods can be added to the interface as required. These new methods can then be implemented and subsequently used by the clients of that interface. Trying to write a final version of a given interface can be very difficult. Experience teaches that interfaces have a tendency to grow over time as new capabilities are required.
Let's look at a simple interface called ClientReadIf in Listing 1.
Listing 1: A simple Java interface
package persistence.client; import java.util.List; import persistence.model.ServiceCustomer; public interface ClientReadIf { public abstract ServiceCustomer readServiceCustomer(Long serviceCustomerId); public abstract List<ServiceCustomer> readAllServiceCustomers(); }
Notice that the methods are all abstract in Listing 1. This means that the implementation is supplied in another file.
Now suppose the simple interface in Listing 1 is expanded by the addition of a new method called removeServiceCustomer(). Listing 2 shows the result.
Listing 2: An expanded Java interface
package persistence.client; import java.util.List; import persistence.model.ServiceCustomer; public interface ClientReadIf { public abstract ServiceCustomer readServiceCustomer(Long serviceCustomerId); public abstract List<ServiceCustomer> readAllServiceCustomers(); public abstract void removeServiceCustomer(Long serviceCustomerId); }
Notice the new method at the end of Listing 2. Let's take a quick look at an excerpt of a sample implementation of the new method, as illustrated in Listing 3.
Listing 3: Partial implementation of an interface method.
@Override public void removeServiceCustomer(Long serviceCustomerId) { // Start EntityManagerFactory EntityManagerFactory emf = Persistence.createEntityManagerFactory(getPersistenceUnitName()); // First unit of work EntityManager entityManager = emf.createEntityManager(); EntityTransaction entityTransaction = entityManager.getTransaction(); entityTransaction.begin(); ServiceCustomer serviceCustomer = READ ENTITY FROM DATABASE entityManager.remove(serviceCustomer); } entityTransaction.commit(); entityManager.close(); emf.close(); }
Notice the use of the @Override annotation at the very beginning of Listing 3. This annotation indicates that the following code implements an abstract method. Remember, once we add the new method to the interface, it's mandatory to supply an implementation.
The remainder of the code in Listing 3 is pretty standard database access and modification. A transaction is created to ensure that the code is not interrupted by other clients. The required entity is fetched from the database, removed, and then the transaction ends. You can find many examples of such code in various online forums. Another useful resource in this context is the definition of ACID (atomicity, consistency, isolation, durability). For more on JPA and Java database programming in particular, see my earlier article "End-to-End JPA Collections with MySQL."
Expanding an interface seems pretty easy, right? Well, it is, but the disadvantage of an interface extension as I've just shown is its effect on the existing implementation code for the interface. New interface methods must be implemented by the client code. This could potentially mean a lot of legacy code changes—even if the new code isn't needed in all of the legacy code use-cases.
Java 8 Default Methods
Fortunately, Java 8 provides assistance for just this type of situation: an additional interface facility in the form of default methods. This means that you can add a new interface method without having to change the implementation code.
Suppose we want to add a new method to an interface. In Java 8, we can also add the implementation to the interface as a default method. Let's look at another interface as illustrated in Listing 4. This code is based on some of the sample code included by Oracle to assist with learning the Java 8 SDK.
Listing 4: A time-and-date service interface
import java.time.*; public interface TimeClient { void setTime(int hour, int minute, int second); void setDate(int day, int month, int year); void setDateAndTime(int day, int month, int year, int hour, int minute, int second); LocalDateTime getLocalDateTime(); static ZoneId getZoneId (String zoneString) { try { return ZoneId.of(zoneString); } catch (DateTimeException e) { System.err.println("Invalid time zone: " + zoneString + "; using default time zone instead."); return ZoneId.systemDefault(); } } }
Listing 4 contains a simple interface that offers a small set of time- and date-related service calls: setTime, setDate, and so on. Listing 5 illustrates an implementation of the interface.
Listing 5: A time-and-date service interface implementation
public class SimpleTimeClient implements TimeClient { private LocalDateTime dateAndTime; public SimpleTimeClient() { dateAndTime = LocalDateTime.now(); } public void setTime(int hour, int minute, int second) { LocalDate currentDate = LocalDate.from(dateAndTime); LocalTime timeToSet = LocalTime.of(hour, minute, second); dateAndTime = LocalDateTime.of(currentDate, timeToSet); } public void setDate(int day, int month, int year) { LocalDate dateToSet = LocalDate.of(day, month, year); LocalTime currentTime = LocalTime.from(dateAndTime); dateAndTime = LocalDateTime.of(dateToSet, currentTime); } public void setDateAndTime(int day, int month, int year, int hour, int minute, int second) { LocalDate dateToSet = LocalDate.of(day, month, year); LocalTime timeToSet = LocalTime.of(hour, minute, second); dateAndTime = LocalDateTime.of(dateToSet, timeToSet); } public LocalDateTime getLocalDateTime() { return dateAndTime; } public String toString() { return dateAndTime.toString(); } }
Listing 4 defines the interface and Listing 5 provides the implementation for the interface. You might have noticed that this interface code differs from Listing 1 in one respect: no public abstract qualifiers. This is a personal preference, but I think it's a nice practice to spell out the qualifiers explicitly; it emphasizes that this is a programming service interface specification.
Listing 6 illustrates some code to invoke the implementation code.
Listing 6: A time-and-date service implementation in action
import java.time.*; import java.lang.*; import java.util.*; public class TestSimpleTimeClient { public static void main(String... args) { TimeClient myTimeClient = new SimpleTimeClient(); System.out.println("Current time: " + myTimeClient.toString()); } }
Building and running the code in Listing 6 produces the output in Listing 7.
Listing 7: Example output
java TestSimpleTimeClient Current time: 2014-04-08T17:39:34.180
Now, suppose we want to extend the original interface in Listing 4 by adding an extra method called getZonedDateTime, as shown in Listing 8. This method allows the client to specify a time zone string and then get back an instance of ZonedDateTime. If the time zone specification is invalid, the method returns a default time zone object.
Listing 8: An extra interface method: getZonedDateTime()
import java.time.*; public interface TimeClient { void setTime(int hour, int minute, int second); void setDate(int day, int month, int year); void setDateAndTime(int day, int month, int year, int hour, int minute, int second); LocalDateTime getLocalDateTime(); static ZoneId getZoneId (String zoneString) { try { return ZoneId.of(zoneString); } catch (DateTimeException e) { System.err.println("Invalid time zone: " + zoneString + "; using default time zone instead."); return ZoneId.systemDefault(); } } default ZonedDateTime getZonedDateTime(String zoneString) { return ZonedDateTime.of(getLocalDateTime(), getZoneId(zoneString)); } }
Notice in Listing 8 that the new method getZonedDateTime includes the default specifier. This approach obviates the need to modify any existing implementation code. Instead, the interface now supplies the implementation code, and Listing 5 remains unchanged. Only the test class code needs to change, as illustrated in Listing 9, where there is an invocation of the new method with a local time zone of "Europe/Dublin."
Listing 9: Invoking the extra interface method
import java.time.*; import java.lang.*; import java.util.*; public class TestSimpleTimeClient { public static void main(String... args) { TimeClient myTimeClient = new SimpleTimeClient(); System.out.println("Current time: " + myTimeClient.toString()); System.out.println("Time in Ireland: " + myTimeClient.getZonedDateTime("Europe/Dublin").toString()); } }
Listing 10 shows an example run of the new code.
Listing 10: Running the new code
java TestSimpleTimeClient Current time: 2014-04-08T19:18:02.640 Time in Ireland: 2014-04-08T19:18:02.640+01:00[Europe/Dublin]
In passing, note that mixing implementation code with interface code is a kind of anti-pattern. In other words, just because you can doesn't mean you should! It might be more advisable to define a new interface that extends the existing interface. This is another approach with minimal impact on existing code. However, adding a new interface reduces the encapsulation of the original interface. Who ever said programming is easy?!
Another important Java 8 change relates to the use of lambda functions, an interesting and useful feature taken from functional programming languages. Let's look at that next.