The title of this article is the outcome of a discussion with Warwick Irwin of the University of Canterbury Department of Computer Science. This article investigates how the visibility status of Java package visible methods, properties and classes could be altered to reflect more accurately their nesting positions in the hierarchy of packages. This would make Java's package visibility modifier almost as general as equivalent notion in C++ of friendship without the downsides of C++ friendship. Two design patterns are presented that achieve almost all of this more general notion of package visibility without having to re-engineer the Java language. I gave a talk about this article to the IEE Short Papers Evening competition but I failed to secure any winning place in the competition.
Given a general operator ~ on C++ and Java classes here are three properties that ~ can satisfy:
Reflexive | A ~ A is true for all classes A |
Symmetric | A ~ B implies B ~ A for all classes A and B |
Transitive | A ~ B and B ~ C implies A ~ C for all classes A, B and C. |
The C++ Friendship Operator, henceforth known as ~C++
is
reflexive but not symmetric or transitive because classes are friends
with themselves is the only thing you can say about a C++ program
without actually looking at the source code to see what class is
friends with what. Before we can look at the corresponding operator
in Java, we need to look at a sample diagram of the Java source files
and directories of a sample Java project. In the following diagram,
red dots represent Java source files and green
boxes represent directories:
The following graph shows how the classes of the above diagram relate
in terms of the Java Package Field Visibility Operator, henceforth
known as ~Java
. Importantly, we are looking at package
visibility applied to fields (methods and properties) but not to
classes. Therefore we assume that all classes are declared with
public visibility and not package visibility. As shown below, each
distinct package is an island into itself because classes in
subdirectories cannot access package visible fields in parent
directories.
The ~Java
operator is reflexive, symmetric and transitive so it is
less general than ~C++
. Here is a possible way that ~Java
operator
could be generalised:
The Generalised Java Can Access Operator, henceforth referred to
as ~GJ
is reflexive, weakly symmetric and transitive. By weakly
symmetric I mean A ~GJ
B implies B ~GJ
A only when A and
B are in the exact same package. The ~GJ
operator gives rise to
Augmented Trees. By an Augmented Tree I mean a tree augmented
into a directed graph in such a way that if A ~GJ
B and B ~GJ
C then A ~GJ
C is in the graph.
The operators discussed above lie in the following order from least
general to most general: ~Java
, ~GJ
, ~C++
as the
properties of ~GJ
are a strict subset of the properties of
~Java
and the properties of ~C++
are a strict subset of the
properties of ~GJ
. Therefore changing the way that Java package
visibility is implemented from ~Java
to ~GJ
would make the
language more expressive and closer to the expressiveness of C++.
There are two downsides to friendship in C++. The first downside is that to achieve a complicated friendship arrangement, many friend declarations are needed. The second downside is that to achieve a complicated friendship arrangement many source files need to be touched (i.e. modified) by having friend declarations added to them. The extra touching of files has poor ramifications for programs like make, which generally recompiles files that include files that have been touched since the last call to make. Even with Java modified in the way previously discussed, it would still be superior to C++ in respect of not needing any friend declarations.
Here is the second file:package alpha; public class A { //////////////////////////////////////////////////////////////////// /// /// PROPERTIES: /// //////////////////////////////////////////////////////////////////// public int property1; protected int property2; int property3; private int property4; public static int property5; protected static int property6; static int property7; private static int property8; //////////////////////////////////////////////////////////////////// /// /// METHODS: /// //////////////////////////////////////////////////////////////////// public void method1() { System.out.println("A's method1"); } protected void method2() { System.out.println("A's method2"); } void method3() { System.out.println("A's method3"); } private void method4() { System.out.println("A's method4"); } public static void method5() { System.out.println("A's method5"); } protected static void method6() { System.out.println("A's method6"); } static void method7() { System.out.println("A's method7"); } private static void method8() { System.out.println("A's method8"); } //////////////////////////////////////////////////////////////////// /// /// MAIN METHOD: /// //////////////////////////////////////////////////////////////////// public static void main(String args) { A a = new A(); alpha.beta.B b = new alpha.beta.B(); b.wibble(a); } }
package alpha.beta; public class B { public void wibble(alpha.A a) { // Compiler error: method3() is not public in alpha.A; // cannot be accessed from outside package a.method3(); // *** // Compiler error: method7() is not public in alpha.A; // cannot be accessed from outside package a.method7(); // *** // Compiler error: property3 is not public in alpha.A; // cannot be accessed from outside package a.property3++; // *** // Compiler error: property7 is not public in alpha.A; // cannot be accessed from outside package a.property7++; // *** } }
Compiling the second file gives an error at the lines marked *** because the package alpha.beta is considered to be different to the package alpha. This is despite the fact that the second file is stored in a subdirectory of the directory that the first file is stored in. It is natural to follow the analogy established by the directory structure and to think of package alpha.beta as being inside package alpha. Taking this idea to its limit we would expect that code in package alpha.beta could access methods and properties in package alpha which have package visibility. What follows is a design pattern that allows one to get around this flaw so that sub-packages can access methods and properties from parent packages.
To bypass this flaw in Java, and achieve a different visibility system the first file needs to be split into two parts. Here is the first part:
Here is the second part:package alpha; public class AInner { //////////////////////////////////////////////////////////////////// /// /// PROPERTIES: /// //////////////////////////////////////////////////////////////////// public int property1; public int property2; public int property3; public int property4; public static int property5; public static int property6; public static int property7; public static int property8; //////////////////////////////////////////////////////////////////// /// /// METHODS: /// //////////////////////////////////////////////////////////////////// public void method1() { System.out.println("AInner's method1"); } public void method2() { System.out.println("AInner's method2"); } public void method3() { System.out.println("AInner's method3"); } public void method4() { System.out.println("AInner's method4"); } public static void method5() { System.out.println("AInner's method5"); } public static void method6() { System.out.println("AInner's method6"); } public static void method7() { System.out.println("AInner's method7"); } public static void method8() { System.out.println("AInner's method8"); } }
package alpha; public class AOuter { private AInner aInner = new AInner(); //////////////////////////////////////////////////////////////////// /// /// SETTERS AND GETTERS FOR PROPERTIES: /// //////////////////////////////////////////////////////////////////// public int getProperty1() { return aInner.property1; } protected int getProperty2() { return aInner.property2; } int getProperty3() { return aInner.property3; } private int getProperty4() { return aInner.property4; } public static int getProperty5() { return AInner.property5; } protected static int getProperty6() { return AInner.property6; } static int getProperty7() { return AInner.property7; } private static int getProperty8() { return AInner.property8; } public void setProperty1(int i) { aInner.property1 = i; } protected void setProperty2(int i) { aInner.property2 = i; } void setProperty3(int i) { aInner.property3 = i; } private void setProperty4(int i) { aInner.property4 = i; } public static void setProperty5(int i) { AInner.property5 = i; } protected static void setProperty6(int i) { AInner.property6 = i; } static void setProperty7(int i) { AInner.property7 = i; } private static void setProperty8(int i) { AInner.property8 = i; } //////////////////////////////////////////////////////////////////// /// /// METHODS: /// //////////////////////////////////////////////////////////////////// public void method1() { aInner.method1(); } protected void method2() { aInner.method2(); } void method3() { aInner.method3(); } private void method4() { aInner.method4(); } public static void method5() { AInner.method5(); } protected static void method6() { AInner.method6(); } static void method7() { AInner.method7(); } private static void method8() { AInner.method8(); } //////////////////////////////////////////////////////////////////// /// /// MAIN METHOD: /// //////////////////////////////////////////////////////////////////// public static void main(String args) { AOuter a = new AOuter(); alpha.beta.B b = new alpha.beta.B(); b.wibble(a.aInner); } }
It is important in the above code that property AInner is private so that only the AOuter class can access it. The AInner property also has no getter method as having such would violate encapsulation and allow a back door into the class by allowing arbitrary outside access to the class AOuter's private methods and properties. For a similar reason the AInner property has no setter method. Here is the second file reworked to make it compile:
package alpha.beta; public class B { public void wibble(alpha.AInner aInner) { // Demonstration of successful attempt // to access every method and property // of a given class. aInner.property1++; aInner.property2++; aInner.property3++; aInner.property4++; // these properties are static // so they could also be prefixed // by alpha.AInner aInner.property5++; aInner.property6++; aInner.property7++; aInner.property8++; aInner.method1(); aInner.method2(); aInner.method3(); aInner.method4(); // these methods are static // so they could also be prefixed // by alpha.AInner aInner.method5(); aInner.method6(); aInner.method7(); aInner.method8(); } }
The downside of this system is that only a method of the class AOuter can call B's wibble method. Therefore to call a method in a subpackage, you need to define and call a method in the class AOuter that in turn calls the method in the subpackage. You can think of the act of calling a method of a class X with a reference to the internals of a different class Y as a kind of friendship, as by passing a reference to the internals of Y you are granting access to private methods and properties of Y to methods in X which is outside of Y.
Note: Care must be taken to where the alpha.aInner reference is used, for example if it is stored somewhere else than a local variable of the wibble method, then this opens up access to the internals of the AOuter class to everywhere the alpha.aInner reference can be accessed.
Instead of splitting a class into an inner and outer components, a simpler but limited functionality version of the above-mentioned system can be achieved. Consider the following source file:
package alpha; public class A { // Defines a property with package visibility java.util.Vector vector = new java.util.Vector(); // Provides the point of entry for an application public static void main(String args) { A a = new A(); alpha.beta.B b = new alpha.beta.B(); b.wibble(a.vector); } }
In the above code, public access to the java.util.Vector internals of A has been granted by the call to the wibble method from the main method of the A class. Here is the file that contains the definition of the wibble method:
package alpha.beta; public class B { public void wibble(java.util.Vector vector) { System.out.println("B's wibble method"); // Successful attempt to access a property // in a parent package vector.addElement("apple"); } }
Note that under this restricted system the Vector inside the A class cannot be re-bound to another Vector outside of the A class. It can only be altered by calling the public methods of the Vector class.
Extending the above-mentioned design pattern to methods that pass and return values of different types is left as an exercise for the reader. At the present time I haven't worked out how to apply this design pattern to private or package visibility methods in classes that inherit from and use properties of other classes.
If the developers of Java could be made aware of the limitation of the Java under the present system, then Java could be re-engineered so that it no longer has this limitation.
Adding support to the more general system of package visibility for fields would not change the behaviour of Java source files that presently compile. All it would do is make certain files that don't compile into files that do compile.
On the other hand if package visibility as applied to classes is to be reinterpreted, then some compiler trickery will be needed to ensure that files that compile under the old system give the same result as under the new system. As an example of a potential problem is for code that uses import statements to import a class from a from a parent package. Here is a file called Vector.java:
Here is the second file, called Main.java:package alpha; class Vector { }
package alpha.beta; import java.util.*; import alpha.*; class Main { public static void main(String args) { Vector v = new Vector(); // *** chooses java.util.Vector rather // than alpha.Vector as alpha.Vector is // not considered to be in the same package System.out.println(v); } }
Because the Vector class has package visibility it is not accessible to the alpha.beta package. Therefore the compiler creates a new java.util.Vector at the line marked ***. Changing javac to get around this design flaw should not change the behaviour of classes that do compile under the current system. Therefore there needs to be a special case for classes defined with package visibility.
I hope that the cost of this burden in terms of additional compiler complexity is less than the value of the benefit obtained by the programmer by extending Java in this way.
Back to Research Projects |
This page has the following hit count:
|