The idea behind private
members—methods, fields, and types—is that they're simply implementation
details: The implementer of a class can feel free to add new ones and change or
remove old ones without fear of harming clients of the class. In other words,
private members are fully encapsulated by the class that contains them.
Unfortunately, there are a few chinks in the armor. For
example, serialization can break this encapsulation. Making a class serializable
and accepting the default serialized form causes the class's private instance
fields to become part of its exported API [EJ Items 54, 55]. Changes in the
private representation can then lead to exceptions or erratic behavior when
clients use existing serialized objects.
But what about compile-time errors? Can you write a final
"library" class and a "client" class, both of which compile without error, and
then add a private member to the library class so that it still compiles but the
client class no longer does?
Solution 73: Your Privates Are Showing
If your solution involves adding a private constructor to the
library class to suppress the creation of a default public constructor, give
yourself half a point. The puzzle required you to add a private member and,
strictly speaking, constructors aren't members [JLS 6.4.3].
This puzzle has several solutions. One solution uses
shadowing:
package library;
public final class Api {
// private static class String {}
public static String newString() {
return new String();
}
}
package client;
import library.Api;
public class Client {
String s = Api.newString();
}
As written, the program compiles without error. If we uncomment
the private declaration of the local class String in
library.Api, the method Api.newString no longer has the return
type java.lang.String, so the initialization of the variable
Client.s fails to compile:
client/Client.java:4: incompatible types
found: library.Api.String, required: java.lang.String
String s = Api.newString();
^
Although the only textual change we made was to add a private
class declaration, we indirectly changed the return type of an existing public
method, which is an incompatible API change. We changed the meaning of a name
used in our exported API.
Many variations on this solution are possible. The shadowed
type could come from an enclosing class instead of java.lang. You could
shadow a variable instead of a type. The shadowed variable could come from a
static import declaration or an enclosing class.
It is possible to solve this puzzle without changing the type
of an exported member of the library class. Here is such a solution, which uses
hiding in place of shadowing:
package library;
class ApiBase {
public static final int ANSWER = 42;
}
public final class Api extends ApiBase {
// private static final int ANSWER = 6 * 9;
}
package client;
import library.Api;
public class Client {
int answer = Api.ANSWER;
}
As written, this program compiles without error. If we
uncomment the private declaration in library.Api, the client fails to
compile:
client/Client.java:4: ANSWER has private access in library.Api
int answer = Api.ANSWER;
^
The new private field Api.ANSWER hides the public
field ApiBase.ANSWER, which would otherwise be inherited into
Api. Because the new field is declared private, it can't be
accessed from Client. Many variations on this solution are possible.
You can hide an instance field instead of a static field, or a type instead of a
field.
You can also solve this puzzle with obscuring. All the
solutions involve reusing a name to break the client. Reusing names is dangerous; avoid hiding, shadowing, and
obscuring. Is this starting to sound familiar? Good!
No comments:
Post a Comment
Your comments are welcome!