3

I'm reading Bruce Eckel's Thinking in Java and there's an exercise I'm just not getting:

Pg. 161: Exercise 8: (4) Following the form of the example Lunch.java, create a class called ConnectionManager that manages a fixed array of Connection objects. The client programmer must not be able to explicitly create Connection objects, but can only get them via a static method in ConnectionManager. When the ConnectionManager runs out of objects, it returns a null reference. Test the classes in main( ).

I came up with the following solution:

// TestConnection.java
import java.util.*;

public class TestConnections {
    public static void main( String[] args ) {
        Connection cn = Connection.makeConnection();

        for (int i = 0; i != 6; ++i) {
            Connection tmp = ConnectionManager.newConnectiton();
            if ( tmp == null )
                System.out.println("Out of Connection objects");
            else {
                System.out.println("Got object: " + tmp );
            }
        }
    }
}

And a second file in the same directory meaning everything ends up in the same default package:

// ConnectionManager.java
class Connection { 
    private Connection() {}

    static Connection makeConnection() {
        return new Connection();
    }
}

public class ConnectionManager {
    static private Connection[] _connections = new Connection[5];

    private ConnectionManager() {}

    static public Connection newConnectiton() {
        for ( int i = 0; i != _connections.length; ++i ) {
            if ( _connections[i] == null ) {
                _connections[i] = Connection.makeConnection();
                return _connections[i];
            }
        }
        return null;
    }
}

The thing is that the client program can directly create Connection objects via the static Connection.makeConnection factory, which seems to violate the exercises goals. Yet if I make the ConnectionManager.java a separate package then import it complains that it can't find a definition for Connection.

I feel like something is going over my head here, but I'm not sure what.

Here is the code for Lunch.java which is referenced in the question:

//: access/Lunch.java
// Demonstrates class access specifiers. Make a class
// effectively private with private constructors:

class Soup1 {
  private Soup1() {}
  // (1) Allow creation via static method:
  public static Soup1 makeSoup() {
    return new Soup1();
  }
}

class Soup2 {
  private Soup2() {}
  // (2) Create a static object and return a reference
  // upon request.(The "Singleton" pattern):
  private static Soup2 ps1 = new Soup2();
  public static Soup2 access() {
    return ps1;
  }
  public void f() {}
}

// Only one public class allowed per file:
public class Lunch {
  void testPrivate() {
    // Can't do this! Private constructor:
    //! Soup1 soup = new Soup1();
  }
  void testStatic() {
    Soup1 soup = Soup1.makeSoup();
  }
  void testSingleton() {
    Soup2.access().f();
  }
} ///:~
Robert S. Barnes
  • 39,711
  • 30
  • 131
  • 179
  • Great book, but on this one Bruce made it a little more confusing than required. His naming of variables and methods could also do with a little bit of improvement on this example. – Martijn Verburg Nov 09 '10 at 09:44

7 Answers7

4

Each class has a scope. In your code, the Connection class has package scope (i.e., it can only be accessed by classes within that same package). This means that moving the ConnectionManager class to another package moves it outside the scope that can see the Connection class; that's the failure you see. Those connections have to be co-located with their factory.

In reality, what you would actually do is you create a public interface that defines what operations on the Connection are, and a private implementation of that interface inside the ConnectionManager; the newConnection method can then just declare that it returns an instance of the interface, and the package protection mechanism stops anyone from prying behind that interface (well, not without using the funkier parts of reflection).

Donal Fellows
  • 133,037
  • 18
  • 149
  • 215
  • I didn't notice that you suggested the same thing as Andreas_D suggested. +1 – Adeel Ansari Nov 09 '10 at 09:44
  • BTW, the only reason this whole complexity with factory classes is really needed is because the `new` operator has very exact semantics that can't be modified. – Donal Fellows Nov 09 '10 at 10:16
  • Great points, but I don't think they're relevant to this particular exercise as he hasn't even covered interfaces at this point in the book. It seems to me he's striving for something else, I just don't know what. – Robert S. Barnes Nov 09 '10 at 10:45
  • @Robert: Then focus on the first paragraph of my answer, and bear in mind that factories can become very complex. (Eventually, they turn into Spring, but that's another story…) – Donal Fellows Nov 09 '10 at 11:29
3

The trick: Connection shouldn't be a concrete class but an interface. A second class (ConnectionImpl) provides an implementation of this interface.

Only the ConnectionManager can instantiate instances of this concrete class and returns the interface.

Example (reduced to show access modifiers):

public interface Connection() {
}

public class ConnectionManager {
  private static class ConnectionImpl implement Connection {
    private ConnectionImpl() {
    }
  }
  public static Connection createConnection() {
    return new ConnectionImpl();
  }
}
Andreas Dolk
  • 113,398
  • 19
  • 180
  • 268
2

Another way is to make Connection a public nested class inside the ConnectionManager. But you might not want that. Andreas_D's and Donal's suggestions are better and more practical, indeed.

public class ConnectionManager {
    private static Connection[] _connections = new Connection[5];

    private ConnectionManager() {}

    public static Connection newConnectiton() {
        for ( int i = 0; i != _connections.length; ++i ) {
            if ( _connections[i] == null ) {
                _connections[i] = new Connection();
                return _connections[i];
            }
        }
        return null;
    }

    public static class Connection { 
        private Connection() {}
    }

}

Edited to show how the TestConnections will look like,

public class TestConnections {

    public static void main(String[] args) {
        Connection conn = ConnectionManager.newConnectiton();
    }
}
Adeel Ansari
  • 39,541
  • 12
  • 93
  • 133
  • But isn't the Connection class only visible in ConnectionManager here? How could I create a ref to a Connection object outside of the ConnectionManager? – Robert S. Barnes Nov 09 '10 at 10:50
  • @Robert S.: No it will be visible out there to any class that `imports` it, as the modifier is `public`. Remember you want to create instance of `Connection` using `ConnectionManager`, but not directly. SO, this way you can. But I would like you to see my other answer down here. That is much according to the exercise. Here is the link, http://stackoverflow.com/questions/4132084/understanding-java-packages-and-factories/4132326#4132326 – Adeel Ansari Nov 09 '10 at 10:58
  • +1 - Very cool. In TestConnections.java I had to qualify `Connection conn ...` with `ConnectionManager.` also. Could you explain a little more in depth how this works? When I remove the `static` keyword from Connection's declaration I get the error `ConnectionManager.java:9 non-static variable this cannot be referenced from a static context` – Robert S. Barnes Nov 09 '10 at 12:15
  • @Robert S.: For explanation look at my answer in this thread, http://stackoverflow.com/questions/1845731/inner-class-in-interface-vs-in-class/1845768#1845768 . Cheers. – Adeel Ansari Nov 10 '10 at 02:31
1

This is following Lunch.java more in a sense that its done by placing some classes under a different with appropriate access modifiers. Assuming the student/reader of the book isn't introduced to interfaces, yet.

// File cm.Connection.java
package cm;

public class Connection {
    // The constructor has package access and so is available to 
    // ConnectionManager, but not to any class outside package cm
    Connection() { }
}

// File cm.ConnectionManager.java
package cm;

public class ConnectionManager {
    static private Connection[] _connections = new Connection[5];

    private ConnectionManager() {}

    static public Connection newConnectiton() {
        for ( int i = 0; i != _connections.length; ++i ) {
            if ( _connections[i] == null ) {
                _connections[i] = new Connection();
                return _connections[i];
            }
        }
        return null;
    }
}

Both, Connection and ConnectionManager are in the same package cm.

// File different.TestConnections.java
package different;

import cm.*;

public class TestConnections {

    public static void main(String[] args) {
        Connection conn = ConnectionManager.newConnectiton();
    }
}

Notice, TestConnections is in the different package.

Adeel Ansari
  • 39,541
  • 12
  • 93
  • 133
  • But I thought you can't have more than one public class per package? Also, the whole point of the exercise is to forbid the client the ability to directly create Connection objects. If Connection is public with a public constructor than the client will be able to instantiate it directly. – Robert S. Barnes Nov 09 '10 at 10:54
  • @Robert S.: No its not that, actually you can't have more than 1 `public class` per file. And please notice, with eyes wide open, that the constructor is not `public`, but the class is. Further, why not you try my whole example -- exactly as I instructed -- and try to make a `Connection` instance directly. And tell me whether you can or you can't. – Adeel Ansari Nov 09 '10 at 11:01
  • +1: Great, making the constructor for Connection have package access instead of private access solves the problem and fulfills the requirements of the exercise. Just for other's benefit Connection and ConnectionManager have to be in separate files in the same package for this to work. That was unclear to me at first. For instance, I created a subdirectory / package named CM and put the two files for these classes in there with the `package CM;` directive at the beginning of each file. Originally I had them in the same file and got compilation errors. – Robert S. Barnes Nov 09 '10 at 12:55
  • @Robert S.: Thanks buddy. I modified a few more bits, hope that didn't spoil the clarity. – Adeel Ansari Nov 10 '10 at 02:37
0

You said

The thing is that the client program can directly create Connection objects via the static Connection.makeConnection factory

I think if it is via a static method, it is not directly.

Also, looking back at the task

The client programmer must not be able to explicitly create Connection objects

You solution hides the fact that new connections are created. You can emphasize this by renaming "newConnection" to "getConnection" or something.

Maybe the expected solution should reuse connection objects, but it does not say so in the excercise. (And it does not make much sense for connections).

Cephalopod
  • 14,632
  • 7
  • 51
  • 70
  • So you think I'm reading something into the exercise that isn't really required, namely completely forbidding the client program from being able to create a Connection object any way except via the ConnectionManager factory? – Robert S. Barnes Nov 09 '10 at 10:56
0

The purpose of getting the connection via ConnectionManager is to have the resource related things in a separated class. In this case you can mock the connection when writing a unit test.

HamoriZ
  • 2,370
  • 18
  • 38
0

The makeConnection() method has default visibility, which means it cannot be used directly by code in different packages. (Actually the same is true for the Connection class itself, which I don't think you want).

Note that client programmers can get around this by placing their code in the same pacakge; but code visibility should generally be seen as an aid to help client programmers see the API rather than the implementation details, not as a way to prevent willful stupidity or malice. Though if you really need to run untrusted code, injecting code into your packages can be prevented by signing your JARs.

Michael Borgwardt
  • 342,105
  • 78
  • 482
  • 720
  • I think this default-visibility stuff is related to the fact that this book jams everything into a single file. – Cephalopod Nov 09 '10 at 09:43
  • Right, the crux of the problem is that if I put ConnectionManager and Connection together in a separate package then the client code can't see the definition of the Connection class at all and thus has no way of getting Connection objects from the ConnectionManager. – Robert S. Barnes Nov 09 '10 at 11:01