Java basics for beginners: A personal storyWant to learn Java but don't know where to begin? Here's an anecdote to get you startedBy Chuck Musser |
|
| Summary This article describes the components of the shared whiteboard system and how they were implemented in Java. Like everything else in computing, Java is being touted as the Ideal Solution For Everything by Those Who Don't Know Any Better. Java is a programming language, not a panacea, so part of the exercise is to show problems that are best solved using another language. If you are new to Java, read the next section which explains what it actually is. If you're already familiar with Java, or just impatient, skip ahead to the next section Overview of my development environment. (6,300 words plus a glossary) |
When I learned about Java, however, the whiteboard suddenly seemed much more possible. It has built in support for networking, graphics, and threads -- all important features for a shared whiteboard program. Another feature of Java is its cross-platform compatibility. This translates into two advantages. First, that there's no installation involved in using the whiteboard. Anyone with a modern Web browser and the URL can use the program with little effort. The second is that I could write one application. Having a shared whiteboard system implies a group of computers. In the real world, groups of computers are mixed bags of Macs, PCs, and the occasional mutant user (me) running Unix.
The basic ideas of an electronic whiteboard are simple. You are given a simple paint application to scribble in. You have a choice of colors and pen widths and you can use an eraser. The kicker is that when you draw on your screen, the scribbles show up on the screens of all other users who are using the same paint program. What fun! You could draw someone a map while talking to them on the phone, use it as a community graffiti wall, or, heaven forbid, use it for business things like flowcharts or network diagrams.
This means that with careful design, you can take a general purpose object and, with only a little more work, customize it for a more specific task. Because Java looks like C and acts like an object-oriented programming language, many people are tempted to compare it with C++. It's not a great comparison. C++ has many object-oriented features that Java's designers chose to leave out for the sake of simplicity. A C++ object, for example, can inherit function implementations from more than one other kind of object. In contrast, Java implementations can inherit from exactly one "parent."
The execution environment is a bit more interesting and innovative. When a Java program is compiled, it turns into a "bytecode" -- a set of instructions that look suspiciously like assembly language. Most computers cannot understand bytecodes directly, but are able to store them. How do Java programs run? The answer is that a special program called a Java Virtual Machine (JVM) has been written for most popular computer platforms -- Macintoshes, Windows, and the various flavors of Unix . This program translates the generic bytecode into machine-specific instructions.
Using a small "virtual machine" which executes Java bytecodes offers a number of advantages besides cross-platform compatibility. It means that Java programs run inside a controlled environment that offers much less opportunity to cause security problems or inadvertent damage to the underlying system. And JVMs are "embeddable," meaning they can show up in any number of surprising and useful places. The Java-enabled versions of popular Web browsers such as Netscape Navigator and Microsoft Internet Explorer are familiar examples of an embedded JVM . Other examples are the "appletviewer" and "java" programs that comes with Sun's Java Development Kit.
We are still in Java's Early Paleolithic epoch. The JDK is bare-bones and primitive, but it works. In a few months, we'll all be using visual development environments, and the Saber-Toothed javac will be found only in museums. Or classrooms...
When you access the Web page, the Java applet loads and appears in a separate window. Next, a snapshot of the current canvas is sent, so you can see what other people have drawn. When the canvas has been loaded, you are free to start drawing. Your doodles are sent to the server program, which broadcasts them to all the other happy souls who are using the programs.
Though this project was small in comparison to a commercial software project, it was complex enough to require a little advance planning. Because I didn't want to overwhelm myself with trying to get everything working at once, I broke the project into discrete components. To a large degree, these components didn't require each other for testing. In the following sections, I'll show you how I broke the shared whiteboard into manageable pieces and suggest how you could do the same in your projects.
Even with that simplification, my design for the scribble applet consisted of nine distinct pieces. I've split them up into two groups: visual components that the user sees and "boiler-room" components that you, the wise and crafty Java programmer, will find interesting:
That's a lot of stuff for just a toy application. Good thing this article isn't about the guts of Adobe Illustrator. Fortunately, almost all of these components could be written and tested without trying to make the connection to the server. As we'll see later, testing the applet in conjunction with the server revealed only one minor problem with the applet's design.
As you peruse the above list, you'll notice that some of the parts are run-of-the-mill and others are more specific to the job of a shared whiteboard. Because Java provides many common programmatic objects for free, you'll be spending the majority of time building new objects that are unique to your application. For example, you don't have to work too hard to incorporate network connections, threads, or menu bars in your application because Java already provides these objects. Rather than telling you about the stuff that's already been solved, I'll use the following sections to show how I implemented things specific to the shared whiteboard -- objects that Java doesn't provide by default.
I realized that these palettes would make good use of Java's object-oriented features. This is because the three palettes shared a number of common behaviors and attributes. All three must lay out their "choices" in a neatly formatted grid. They all need a way of specifying how big a box to use for each "choice" and how much space should be in between them. Finally, they all need to provide a way of letting other classes know which "choice" was currently selected. So to start out with, I encapsulated all these behaviors into a generic Palette class.
Next, I worked out the differences between the individual cases. It turns out that the palette that lets the user pick colors is significantly different from the other two palettes -- the ones for choosing the pen width and allowing you to toggle between draw and erase mode. So I ended up with two different classes: a ColorPalette and a PicturePalette. The differences, and why the differences are important, are described below.
The key to understanding palettes is the fact that a palette must store two types of information about the choices it offers to the user. First, each choice in the palette needs a visual representation to display to the user. Second, and perhaps more importantly from the programmer's point of view, a choice needs an internal representation that allows the program to query the palette for its current selection and get a meaningful answer back. Meaningful answers are things like "Red" or "5-pixels wide" or "erase mode."
The color palette is easier because the internal and external representations can be the same piece of data -- an RGB color value, which in Java is represented by an instance of the java.awt.Color class. To display the value to the user, the palette can simply paint each choice box with the color value associated with that box. When the program queries the palette to figure out what the current representation is, it simply returns the color associated with the currently selected choice.
The other two palettes use graphics for representing the choices
to a user and are a little harder. Instead of returning the graphic
as the answer to the question, "what is your currently selected
item," I chose to store a separate integer value with each choice and
return that instead. This serves as a much more efficient internal
representation. So when the applet asks the PenPalette, "what is the
current penwidth?" the PenPalette can return a useful answer such as
"1", "3", or "5". The same goes for the ModePalette, although the
numbers chosen for the draw and erase mode, 1 and 0, are somewhat
arbitrary. To make the code more readable, I used declared "static
final" variables (Java's equivalent of a defined constant) so that 0
and 1 show up in the code as ERASE_MODE and DRAW_MODE,
respectively.
What's the result of all this? The bulk of the functionality is located at the generic Palette.java file. An application programmer doesn't need to know what's inside that file, she just needs to know the interface. The Color or PicturePalette-specific code lives in separate, very short files: A good example of code re-use is in the constructor for the ColorPalette object. It simply calls the constructor of its ancestor, the generic Palette object. That constructor does all the hard labor to lay out the grid that makes up the Palette. The ColorPalette specific code is only one line long. It simply remembers the identifier for another object that we'll be communicating with later -- the ScrollableScribble object that defines the drawing canvas:
public ColorPalette( int rows, int cols, int spacing,
int square_sz, ScrollableScribble scribbler )
{
super( rows, cols, spacing, square_sz );
myScribbler = scribbler;
} A Java Vector is an array that, unlike most arrays found in other programming languages, can grow as needed. This was a nifty way of doing it, but I found that the scrolling slowed down as the drawing got more complex. This was because every scribble was saved, even if another scribble obliterated it. So you'd get this nifty, but excruciating effect, where scrolling the canvas resulted in the newly visible portion being recreated as if a little man was scribbling it all over again really fast.
The answer to this was to replace the Vector storage with two "canvases:" one offscreen and one onscreen. This allowed me to use the "double-buffering" technique used in computer animation to both increase the speed and restrict the memory used by the canvas to a fixed amount. The Vector version of the canvas grew as users scribbled; the new version's size is limited by the size in canvas in pixels.
The basic idea is to use the slope -- the ratio between the rise and the run -- to calculate the coordinates of every point in the line. So for every point between the starting and ending x coordinate, I multiply the x coordinate by the slope, then add it to the starting y coordinate to find the y coordinate of the current point. Lines with positive and negative slope need to be handled differently, as do vertical and horizontal lines, so the algorithm takes all these cases into account. The result is my World Famous drawFatLine() method.
public void drawFatLine ( Graphics g, int x1, int y1,
int x2, int y2, int penWidth, Color color )
// Remember the current pen color for this canvas
Color oldColor = g.getColor();
g.setColor( color );
// if the user's pen width is 1, use the standard Java
// drawLine() method.
if( penWidth == 1 )
{
// Simple 1-pixel-wide line
g.drawLine( x1, y1, x2, y2 );
}
else {
// Otherwise, do the more complicated job of
// drawing a fat line
// Figure out how far the scribble moves
// in the x and y directions.
int xdiff = x2 - x1;
int ydiff = y2 - y1;
// Figure out which way to go!
// If the x or y "end" point is numerically less than the
// corresponding "start" point, we need to count backwards.
int xincr = (Math.min( x1, x2 ) == x2 ) ? -1 : 1;
int yincr = (Math.min( y1, y2 ) == y2 ) ? -1 : 1;
// Deal with special cases: vertical and horizontal lines
if( xdiff == 0 ) // vertical line
{
// This is easy, we just count out each point between the start
// and end y-axis points and draw an appropriately sized filled
// circle at each point. Since this is a vertical line, we always
// use the same x-axis coordinate.
for( int i=0; i!=ydiff; i+=yincr )
g.fillOval( x1, y1 + i, penWidth, penWidth );
} else if( ydiff == 0 ) // horizontal line
{
// Same idea but for horizontal lines.
for( int i=0; i!=xdiff; i+=xincr )
g.fillOval( x1 + i, y1, penWidth, penWidth );
} else // diagonal line
{
// This is the case where the slope matters. We count through all the
// starting and ending points for the x-axis and at each point,use the
// slope ratio to find where the corresponding y-axis coordinate is,
// draw a filled circle of the appropriate diameter at that point.
float slope = ydiff / xdiff;
for( int i=0; i!=xdiff; i+=xincr )
g.fillOval( x1 + i, y1 + ((int)slope*i), penWidth, penWidth );
}
}
//restore the pen color that we had at the start of this method
g.setColor( oldColor );
}The first item was a protocol that the whiteboard server and applet would both use when talking to each other. The next two items were slight changes that needed to be made to the whiteboard applet. The last item was an entirely new program. The next sections describe the components in detail.
When the Java's windowing system reports the x and y coordinates to the whiteboard applet, the coordinates are relative to the currently viewable area of the canvas. For transmission over the network, these coordinates must be translated to the coordinates relative to the top left corner of the entire canvas. This allows one user to draw into an area of the canvas that another user has scrolled away from and thus cannot see. When the second user scrolls back to that area, the scribbles done by the first user show up in their proper position. Zowie! The way the whiteboard client sends the correct information is by keeping track of how far over and down the user has scrolled, and padding the coordinates it sends accordingly.
Since the server could handle many clients at once, each of which could change the data described above, I used Java's mutual exclusion mechanism to ensure that two clients couldn't change data in a way that would corrupt it.
This was an opportunity to refine the design that I'd come up with over the preceding weeks. It wasn't perfect, but it wasn't beyond saving either. The short version of the story was that the client was largely OK, but major work needed to be done on the server. Details are below.
I first concentrated on fixing the background color bugs because they seemed to be isolated to the scribble client. By paying attention to how I was setting the colors, I was able to make the background the right color (lightGray in my case) on every platform.
Next, I turned my attention to the other problems, which seemed to be server-related. After taking stock of the situation: bugs and performance problems, I decided to start with a clean sheet of paper. One lesson you learn from a project of any size is the places where a tool doesn't fit your needs. I looked at the design of the whiteboard server and realized that it was too simple for the complex object-oriented approach I'd taken originally. So I rewrote it in C. It was a difficult decision to make, because a major goal of this exercise was to use Java for everything. However, the things I learned from making the switch were more valuable than if I'd kept hacking away at the broken Java version of the server. The lesson here is "don't be afraid to back up."
The palettes described above were examples of a good fit for object-oriented design techniques. They featured a large amount of shared and reusable logic, and a small number of overridden methods to create unique and specialized subclasses. The scribble server was not a good fit because it was inherently simple and did not really share many characteristics with other servers.
Programs that are inherently simple tend be dwarfed by object-oriented concepts -- you spend more time making the program fit into the object-oriented mold than concentrating on what the program is supposed to be doing. The scribble server was a perfect example of this. A simple receive-and-broadcast TCP/IP server is best expressed in ordinary procedural terms.
Finally, I had to rewrite many functions that were in the parent TCP/IP server class to do whiteboard-specific stuff. This is not a good sign -- the beauty of object-oriented languages is reusability. If you're rewriting, rather than reusing, something is wrong. A good rule of thumb is that if more than one or two functions are significantly rewritten, it's time to rethink your inheritance strategy.
I could have just rewritten a simple dedicated server in Java, but chose not to. For performance reasons, I rewrote it in C. Running a Java interpreter is still 10-20 times slower than the equivalent C program, and I figured that the server needed all the help it could get. Never content to leave things alone, I delved into two more technologies: the POSIX thread API for Unix and Win32, the API for programming the Windows operating system. The final result was two very efficient server programs, one for Unix and the other for Windows NT. Currently the NT version has the performance edge. I'll explain why down below.
In the worker thread model, a small number of threads is able to service all the sockets that represent the population of connected clients. You set the number of threads equal to the number of processors on the machine. When a scribble from any client arrives at the server, any of the server's threads can service it, regardless of where it came from. This results in little or no context switching between threads. And since the number of threads has a small upper bound, servicing a large number of clients does not turn your server into a hungry multithreaded hydra that devours all the system's memory and process table slots. Admittedly, this is unlikely in a toy application like NetScribble, but it's helpful to go through the exercise if you're ever designing an Important Application such as a database or Web server.
It's a bit trickier to write such a server, but for very heavily used servers it would be well worth it. In Unix, this involves maintaining a global variable of the type "FD_SET" and using the select(2) system call to read and broadcast scribbles from all the socket descriptors that are ready for reading. In Windows NT, you'd register all the active socket connections with an object known as an IoCompletionPort. Every time you query this object with the GetQueuedCompletionStatus() call, it consults its list of socket descriptors and returns the next one that needs attention.
Late breaking news: because the features that allow the worker thread model are thoroughly explained in the documentation that comes with Visual C++, I went ahead and made this improvement in the NT verision. Not being ready to ditch my Unix allegiance just yet, I'm working on making these changes to the Unix version as well.
Aside from the minor and easily resolvable differences in appearance and the bad behavior of a few unnamed browsers (Netscape 3.01 for the Mac), the Scribbler had that amazing "It Just Works" quality. This is a very real advantage to businesses who want to deploy client/server programs to a large user base. Anyone familiar with installing something like a Powerbuilder or Delphi application on hundreds of machines and then watching the complaints and cries for help flood in can appreciate Java's simplicity. An applet will hop happily into your browser and just start running.
As the scribble applet shows, Java programs are real applications -- with multiple windows and a sophisticated user interface. They are much more than just spicy HTML pages or forms masquerading as applications.
You can find the shared whiteboard at http://www.matisse.net/whiteboard/.
|
About the author
Chuck
Musser, Programmer Bearer of Einstein's mantle, moss and lichen enthusiast. Drinks soup through his nose.
Internet Literacy Consultants, The Company.
ILC is a general-purpose Internet consulting firm, based in San
Francisco, California. Founded in February 1994 ILC mostly builds
infrastructure using Internet technologies. That means expensive
things that are complicated and are supposed to last a long time,
like roads, bridges, and this month's software.
Reach Musser at Chuck.Musser@netscapeworld.com.
Feedback: nweditors@netscapeworld.com
Technical difficulties: webmaster@netscapeworld.com
URL: http://www.netscapeworld.com/nw-06-1997/nw-06-java.html
Last updated: Sunday, June 01, 1997
Some of these Glossary items are copyright by Internet Literacy Consultants from their Glossary of Internet Terms (http://www.matisse.net/files/glossary.html) and are used here with permission.