Java Tip 84: Customize scoping with object keys

Java's standard scopes may be insufficient for some projects

Correct scoping is one of the keys to success when developing large-scale programs. Paying too little attention to scoping often leads to spaghetti code that is difficult to understand, extend, or maintain. Java provides four standard scopes to restrict access to the data and methods in a class. However, custom-tailored scoping is occasionally desired on large projects, usually to avoid a choice between speed and safety. I'll show you how you can create custom scoping to avoid this tradeoff. Following the techniques described here, you should be able to do so without a significant increase in complexity.

Why scoping?

Scoping controls the depth of access to data and methods, and is one of the tools used to control large software projects. In a program with hundreds of thousands of lines of code, allowing one piece of code to modify any piece of data it wants rapidly leads to chaos. More restrictive scoping of data and methods -- private versus public, for example -- makes programs easier to understand, extend, and maintain, because you don't need to examine as much code when making changes. For small projects, scoping is often not critical -- if a program is only 10,000 lines long, it's easier to keep track of everything. For large projects with many programmers, however, scoping can be the difference between long-term success and failure. Keep in mind, then, that the techniques I present to you work for programs of all sizes, but are most appropriate for large applications.

Some scenarios

When is standard scoping insufficient -- and when is it not? I know of three cases when tailoring a scope to do exactly what you want is more practical than using a standard Java scope. Custom scoping can help you out when you want to:

  • Disallow certain scripting capabilities
  • Control mutability across packages
  • Boost performance

We will consider each use in turn.

Disallowing certain scripting capabilities

A scripting language can add value to applications by allowing users to extend and tailor applications themselves. Occasionally, your application will have functionality that must be used cautiously, such as the ability to reset remote systems. You can restrict access to the GUI by adding a password and warnings to prevent accidents. However, you may wish to prevent scripting from making use of this capability entirely. In such a case, you would place the GUI and the forbidden functionality in the same package, and then create a reset function-package scope.

Sometimes, however, this isn't acceptable. In such cases, you need an approach that doesn't require packaging the GUI and the functionality together. Custom scoping allows the two to be separate, and still protects the functionality from scripting.

Controlling mutability

Creating objects in Java is slower than creating stack objects in C++. Because the cost of object creation in Java is higher than in C++, well-designed Java programs pay close attention to object creation than well-designed C++ programs. One way to safely create fewer objects in Java is to make them

immutable

-- to not provide a way for the object's state to be changed, in other words. An example of this in the standard libraries is the

String

class. If an object's state cannot change, references can be safely passed to the object throughout the application.

However, this doesn't always work. A data-processing program might start with an object containing raw data and slowly add information to the object as it passes through the application. A good example of this would be a stock-screening application that starts with objects containing raw stock data, and then adds more information -- the performance of a single stock relative to the industry, say -- as the objects are processed.

In a small application, you can provide the add methods with a javadoc saying, in effect, "Don't call this unless you are the relative performance comparator." As the application gets larger, and more engineers work on it, this approach becomes riskier. Hardcoding restrictions into the program, instead of using comments and hoping that everyone reads them, allows the application to detect access bugs automatically.

Boosting performance

You occasionally need raw access to the internals of an object for speed. As an example, you might have an object containing a large array. The large array is private, and contains getters but no setters because the object is immutable:

public class ImmutableIntArray { private int[] data;

public ImmutableIntArray (int[] data) { this.data = data; }

int getElementAt (int index) { return data[index]; } }

There is no method that returns the raw data element, because this would defeat the purpose of making the object immutable. Occasionally, however, client code may need to process the contents of data quickly. Although it may take too long to return a copy of the data, working on the raw data element might be fast enough. It would be a reasonable design decision to grant access to data only to those clients who need it, and not to the entire application.

The custom approach

There are two aspects to the idea of creating custom scoping. First, class names, which are always strings, are visible throughout an application. And second, objects of various classes can only be created by a restricted set of clients.

You can create objects whose class names are visible to the entire program, but which can only be constructed by very restricted clients. The objects of these classes can then be used as key objects. Callers are required to pass in these key objects to method calls, and the method being called can check them against a list of legal keys.

Here's a working example of this:

public class Main { static public void main (String[] args) { try { System.out.print ("This should work ..."); ValidExampleCaller.sampleMethod(); System.out.println (" and it did."); } catch (InvalidKeyException e1) { e1.printStackTrace(); }

try { System.out.println ("This should fail ..."); InValidExampleCaller.sampleMethod1(); } catch (InvalidKeyException e2) { e2.printStackTrace(); } } }

/** * */ class InvalidKeyException extends java.lang.Exception { public InvalidKeyException (String message) { super (message); } }

class ExampleCallee { /** * This method prints 'Hi!' to standard out, but can only * be called by the ExampleCaller class. * * @param key 'key' is an object restricting access to this * method. If 'key' is not an instance of * class ValidExampleCaller.Key, an InvalidKeyException * is thrown. */ public static void sampleMethod (Object key) throws InvalidKeyException { if (key.getClass().getName().equals ("ValidExampleCaller$Key") == false) throw new InvalidKeyException("Can't call sampleMethod() with key " + key);

System.out.println ("Hi!"); } }

class ValidExampleCaller { static private final class Key { }

static public void sampleMethod () throws InvalidKeyException { ExampleCallee.sampleMethod (new Key()); } }

class InValidExampleCaller { static private final class Key { }

static public void sampleMethod1 () throws InvalidKeyException { ExampleCallee.sampleMethod (new Key()); } }

The ExampleCaller class can call the sampleMethod in ExampleCallee successfully, but no other class can.

Before using this approach, you should consider a number of its details and implications:

  • ExampleCallee depends on ValidCaller's cooperation: The ExampleCallee class depends on ValidCaller cooperation in enforcing the scoping restrictions. ValidCaller can cheat by making the Key class public, or by handing out references to Key objects. However, even with standard scoping, legal callers can cheat. Our approach assumes cooperation between the valid caller classes and the callee, just as standard Java scoping does.

  • The Key object is a static inner class: This means that both static and nonstatic member functions in the ValidCaller class have access to sampleMethod. To prevent static methods from calling sampleMethod, make the Key object nonstatic.

  • The Key object is a private inner class: Making Key private means that subclasses of ValidCaller cannot construct Key objects. Making Key a protected inner class grants access to child classes as well.

  • Key need not be an inner class: Key could be a package-scope class if your intent is to grant any class in a package access to sampleMethod().

  • Key must not be anonymous: The Java language does not specify a naming convention for anonymous inner classes, and different compilers generate different names for anonymous inner classes. Because of this, you should name the Key class.

  • The compiler can't help you: The compiler detects violations of standard scoping, but not custom scoping. If you use the approach illustrated here, you will not detect an illegal access until runtime. Detecting mistakes at compile time is preferable, of course, so you should use standard scoping if possible.

Conclusion

Standard Java scoping is sufficient for most projects. For large projects, however, you occasionally need to construct very specific scoping relationships. The approach to doing so presented in this article uses only the standard JDK 1.1 language constructs, and does not require any extensions to the Java language. This approach does add some complexity, though, so you should use it with caution. If you find yourself using this technique often, you need to reconsider your design.

Mark Roulo is JavaWorld's Java Tip technical coordinator. He has been programming professionally since 1989 and has been using Java since the alpha-3 release. He works full time at KLA-Tencor, where he is part of a team building a large, distributed, parallel, multicomputer application for image processing (among other things) written almost entirely in Java.

Learn more about this topic

Related: