Running on Java 24-ea+24-2960 (Preview)
Home of The JavaSpecialists' Newsletter

225Hiding Interface Methods

Author: Dr. Heinz M. KabutzDate: 2015-01-21Java Version: 8Category: Language
 

Abstract: Whenever a class implements an interface, all the implemented methods have to be public. In this newsletter we look at a trick that we can use to make them private.

 

Welcome to the 225th issue of The Java(tm) Specialists' Newsletter, written at 30000 feet en route from Denmark's JDK.IO conference back to Chania. The conference was fun. I gave a live demonstration of what you can do with reflection. It worked pretty well. The Danish audience laughed twice in the first two minutes. If you have ever spoken in Scandinavia, you will know that is not an easy feat :-) I've spoken about reflection many many times and have been teaching on the topic since 1999. It is even included in my Java Specialist Master Course as a chapter (always very popular). Click here to view a recording of the talk. If you'd like to get notices like this quicker, please either subscribe to my email list or follow me on twitter.

A few months ago, I was watching some friends making tsikoudia, a strong spirit single-distilled from grape skins and pips. This always involves great food and company. One of the guests wanted to know where I was from. Not so easy. I told him I was a "Germano-Hollandaiso-Notia-Afrikano-Ellinas" (German-Dutch-South-African-Greek). My friend waved a stern finger at me and said: "Oxi, den eisai Ellinas!" (No, you are not Greek!) Not quite sure what was coming next, and also not wanting to argue with him (he's really big), I waited. I didn't expect this: "Eisai Chorafakianos!" (You are a Chorafakian - from our village Chorafakia.) It was a great honour, that I don't think many foreigners have experienced.

javaspecialists.teachable.com: Please visit our new self-study course catalog to see how you can upskill your Java knowledge.

Hiding Interface Methods

Many years ago, I had the privilege of coding Java with one of the finest engineers I've known. We had started a new system using Java 1.0, which luckily quickly became Java 1.1. At the time there were very few Java idioms to follow, so we developed our own trade craft. Some of the best practices that I follow today are still based on what I discovered together with Paul van Spronsen. Besides coding Java, we also went on a few business trips to Germany together. On the long haul flights back to South Africa, we would spend hours hacking away at our ideas (or as long as the laptop batteries would last).

One of the principles that we followed was to be rigorous about encapsulation. No field was allowed to be public. But that was not all. Most classes were final. This was not for performance reasons, but to limit how others could incorrectly subclass them. You need to design for inheritance, it does not just happen automatically in a proper and orderly fashion. Once you've allowed someone to subclass your class, you are committed to supporting that interface for generations to come.

One trick that Paul showed me was so clever that I have used it over and over. Last month I was teaching my Design Patterns course in Paris and asked the software engineers if they had heard of this. No one had, which surprised me. It might be that this indeed is a rather unique idea that has not been spread around a lot. It should have. And lambdas make this even more compelling.

Let's say we would like to have our own observer. The java.util package contains, amongst other things, an Observer and an Observable. The implementation of these are not brilliant, but I'm just using them as an illustration. One way to create your observer is to let your class implement the Observer interface, like so:

import java.util.*;

public final class MyObserver implements Observer {
  public void update(Observable o, Object arg) {
    System.out.println("MyObserver.update");
    System.out.println("o = [" + o + "], arg = [" + arg + "]");
  }

  public void register(Observable observable) {
    observable.addObserver(this);
  }
}

However, in view of our quest for rigorous encapsulation, we see that this now exposes the update() method. There is no way in Java to implement an interface and to make the method anything but public. Or is there?

Paul van Spronsen's trick is to use a private anonymous inner class to implement the interface. Thus instead of letting our outer class implement the interface, we delegate that to a private class. This class is never made public, and thus we can prevent someone without authorization from calling the update() method directly. The result is something like this:

import java.util.*;

public final class MyObserverEncapsulated {
  private final Observer observer = new Observer() {
    public void update(Observable o, Object arg) {
      MyObserverEncapsulated.this.update(o, arg);
    }
  };

  private void update(Observable o, Object arg) {
    System.out.println("MyObserver.update");
    System.out.println("o = [" + o + "], arg = [" + arg + "]");
  }

  public void register(Observable observable) {
    observable.addObserver(observer);
  }
}

By also making MyObserverEncapsulated final, we prevent someone from overriding the register() method and somehow using that to capture the Observer instance. It is not completely bullet proof. Someone could still pass a bogus Observable in to the register() method. The Observable would then capture the observer instance, thus giving access to the private update() method.

Using anonymous inner classes is a solution that worked very nicely in the past. But in Java 8, we can do away with this boilerplate code and use lambdas instead:

import java.util.*;

public final class MyObserverLambda {
  private final Observer observer = (o, arg) -> update(o, arg);

  private void update(Observable o, Object arg) {
    System.out.println("MyObserver.update");
    System.out.println("o = [" + o + "], arg = [" + arg + "]");
  }

  public void register(Observable observable) {
    observable.addObserver(observer);
  }
}

Since the method parameters are the same for the two update() methods, we could instead simply use a method reference:

import java.util.*;

public final class MyObserverMethodReference {
  private final Observer observer = this::update;

  private void update(Observable o, Object arg) {
    System.out.println("MyObserver.update");
    System.out.println("o = [" + o + "], arg = [" + arg + "]");
  }

  public void register(Observable observable) {
    observable.addObserver(observer);
  }
}

We could even do away with the field altogether in Java 8 and inline the method reference in the call to addObserver(). This would cause a new lambda object to be created whenever register() is called, but since we would not expect that to happen very often, it would not become a bottleneck in our application.

import java.util.*;

public final class MyObserverMethodReferenceInlined {
  private void update(Observable o, Object arg) {
    System.out.println("MyObserver.update");
    System.out.println("o = [" + o + "], arg = [" + arg + "]");
  }

  public void register(Observable observable) {
    observable.addObserver(this::update);
  }
}

ObjectInputValidation

Another place where this type of code idiom would be useful is with ObjectInputValidation. When an object is deserialized, none of our constructors are called. Your field sanity checking in your constructor will thus have no effect. Let's say we have a Person and we want to make sure that the first and last names are set and that the age is within a reasonable range. We might code something like this:

import java.io.*;

public final class Person implements Serializable {
  private final String firstName;
  private final String lastName;
  private final int age;

  public Person(String firstName, String lastName, int age) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
    if (this.age < 0)
      throw new IllegalArgumentException("age negative");
    if (this.age > 150)
      throw new IllegalArgumentException("age more than 150");
    if (this.firstName == null || this.firstName.isEmpty())
      throw new IllegalArgumentException("firstName empty");
    if (this.lastName == null || this.lastName.isEmpty())
      throw new IllegalArgumentException("lastName empty");
  }

  public String toString() {
    return firstName + " " + lastName +
        " is " + age + " years old";
  }
}

However, this would allow someone to write the Person object to a byte[], find the age, and then change that to something incredible. By the way, if you hear tales of the amazing longevity of Greeks, this is not the result of the healthy Mediterranean diet (we are #1 in childhood obesity in the world), nor the olive oil (which is delicious), but rather the relatives of the elderly with shared bank accounts forgetting to inform the state that said granny has fallen asleep permanently and that the pension should please be cancelled forthwith ;-)

Here is our PersonHack:

import java.io.*;

public class PersonHack {
  public static void main(String... args)
      throws IOException, ClassNotFoundException {
    Person heinz = new Person("Heinz", "Kabutz", 43);
    // convert to a byte[]
    ByteArrayOutputStream bout = new ByteArrayOutputStream();
    ObjectOutputStream oout = new ObjectOutputStream(bout);
    oout.writeObject(heinz);
    oout.close();

    byte[] bytes = bout.toByteArray();
    int index = -1;
    for (int i = 0; i < bytes.length; i++) {
      if (bytes[i] == 0 && bytes[i+1] == 0 && bytes[i+2] == 0 &&
          bytes[i+3] == 43) {
        if (index != -1)
          throw new IllegalStateException("Duplicate index");
        index = i;
      }
    }
    int newAge = -50;
    setAge(bytes, index, newAge);

    ObjectInputStream oin = new ObjectInputStream(
        new ByteArrayInputStream(bytes)
    );
    Person youngerHeinz = (Person) oin.readObject();
    System.out.println("heinz = " + heinz);
    System.out.println("younger heinz = " + youngerHeinz);
  }

  private static void setAge(byte[] bytes, int index, int age) {
    // in the object output stream, ints are encoded Big Endian
    bytes[index] = (byte) (age >>> 24);
    bytes[index+1] = (byte) (age >>> 16);
    bytes[index+2] = (byte) (age >>> 8);
    bytes[index+3] = (byte) age;
  }
}

Here is the output from PersonHack on our plain Person above:

heinz = Heinz Kabutz is 43 years old
younger heinz = Heinz Kabutz is -50 years old

The solution is to make Person implement ObjectInputValidation, which would then contain a validateObject() method. We also add a private readObject() method that registers ourselves with the validator. The result would be something like this:

import java.io.*;

public final class Person implements Serializable,
    ObjectInputValidation {
  private final String firstName;
  private final String lastName;
  private final int age;

  public Person(String firstName, String lastName, int age) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
    try {
      validateObject();
    } catch (InvalidObjectException ex) {
      throw new IllegalArgumentException(ex.getMessage());
    }
  }

  public void validateObject() throws InvalidObjectException {
    if (age < 0)
      throw new InvalidObjectException("age negative");
    if (age > 150)
      throw new InvalidObjectException("age more than 150");
    if (firstName == null || firstName.isEmpty())
      throw new InvalidObjectException("firstName empty");
    if (lastName == null || lastName.isEmpty())
      throw new InvalidObjectException("lastName empty");
  }

  private void readObject(ObjectInputStream in)
      throws IOException, ClassNotFoundException {
    in.registerValidation(this, 0);
    in.defaultReadObject();
  }

  public String toString() {
    return firstName + " " + lastName +
        " is " + age + " years old";
  }
}

The output from PersonHack with our new improved Person:

Exception in thread "main" InvalidObjectException: age negative
	at Person.validateObject(Person.java:21)
	at Person$$Lambda$1/1831932724.validateObject
    etc.

However, we have again broken encapsulation by being forced by the interface to make the validateObject() method public. In this case, Paul's trick will not work, because we would need to then also make the anonymous inner class serializable. Even if we did, at the time that we call registerValidation(), the field would still be null, because the Person object at this point has not been deserialized yet. Lambdas would have similar problems. What would work would be to inline the method reference into the call within readObject() to in.registerValidation(this::validateObject, 0), like so:

import java.io.*;

public final class Person implements Serializable {
  private final String firstName;
  private final String lastName;
  private final int age;

  public Person(String firstName, String lastName, int age) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
    try {
      validateObject();
    } catch (InvalidObjectException ex) {
      throw new IllegalArgumentException(ex.getMessage());
    }
  }

  private void validateObject() throws InvalidObjectException {
    if (age < 0)
      throw new InvalidObjectException("age negative");
    if (age > 150)
      throw new InvalidObjectException("age more than 150");
    if (firstName == null || firstName.isEmpty())
      throw new InvalidObjectException("firstName empty");
    if (lastName == null || lastName.isEmpty())
      throw new InvalidObjectException("lastName empty");
  }

  private void readObject(ObjectInputStream in)
      throws IOException, ClassNotFoundException {
    in.registerValidation(this::validateObject, 0);
    in.defaultReadObject();
  }

  public String toString() {
    return firstName + " " + lastName +
        " is " + age + " years old";
  }
}

(By the way, if you don't make Person final, then it would still be possible to create one with a ridiculous age, but I will leave that as an exercise to the reader.)

I hope you enjoyed this trick and will be able to find good use cases for it. Please let me know if you've seen this before :-)

Kind regards

Heinz

 

Comments

We are always happy to receive comments from our readers. Feel free to send me a comment via email or discuss the newsletter in our JavaSpecialists Slack Channel (Get an invite here)

When you load these comments, you'll be connected to Disqus. Privacy Statement.

Related Articles

Browse the Newsletter Archive

About the Author

Heinz Kabutz Java Conference Speaker

Java Champion, author of the Javaspecialists Newsletter, conference speaking regular... About Heinz

Superpack '23

Superpack '23 Our entire Java Specialists Training in one huge bundle more...

Free Java Book

Dynamic Proxies in Java Book
Java Training

We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.

Java Consulting

We can help make your Java application run faster and trouble-shoot concurrency and performance bugs...