by Mark Seminatore
As you learned in earlier chapters, the Visual J++ compiler, like many of Microsoft's latest development tools, is hosted under the Microsoft Developer Studio. The concept of the Developer Studio is very nice. It provides a consistent and comfortable working environment with easy access to online books, class browsers, project-management tools, and optional links to source code revision control. Microsoft has really tried to leverage the strengths of the Developer Studio by hosting Visual C++, FORTRAN PowerStation, Visual Test, and the Microsoft Developer Network in the same environment.
For the most part, when working with Visual J++ most of your interactions are actually with the Developer Studio, which is shown in Figure 10.1. The Developer Studio also contains several other useful utilities, including WinDiff, Zoomin, and JView. Although the integrated environment certainly is convenient, at times the command-line tools can increase your productivity. As an example, if you wanted to create a very simple Java applet consisting of only a single class, it may be more convenient to edit and compile the applet from the command line. The command-line tools are also useful if you want to make only a small change and then rebuild a project using NMAKE. You'll find out more about the command-line compiler and other tools later in the chapter.
Figure 10.1 : The Visual J++ compiler environment.
| NOTE |
Don't look for the Visual J++ compiler in this figure. The compiler is not exactly visible because it is called behind the scenes by the Developer Studio. Of course, a command-line version of the Visual J++ compiler is available. |
The Visual J++ compiler is really quite fast even with full optimization and debug support enabled. It easily handles the sample programs that Sun provides as part of the Java Developer's Kit (JDK). Microsoft also supplies a number of examples to demonstrate key features of Visual J++. These examples include capabilities such as creating Java links to COM objects, VBScript, and ActiveX controls. One of the Microsoft examples even demonstrates how to implement a COM object in Java that can be called from another application such as Visual Basic.
One of the most important considerations with any development tool is the performance of the resulting code. Programmers tend to spend a great deal of time working with compilers, so they are always happy to work with a fast compiler. However, end users don't care about the speed of the edit-compile-debug cycle. They want fast and responsive software, which, if you think about it, is exactly what the programmer is looking for in a fast compiler. In the end, both end users and programmers want the programs they use to be the result of good code generation. Programmers sometimes deal with this problem by using one compiler for development and another for the final builds. End users seldom have this luxury.
With Visual J++, Microsoft has attacked this problem by separating the development environment from the compiler. This approach allows one development team to focus on developing an environment that can be used in several products while another team focuses on the compiler itself. Other compiler vendors have attacked the same problem simply by providing several compiler back ends, perhaps licensing code generators from other companies.
Does this information have any relation to the Visual J++ compiler and Java programming? Yes, because Java as an interpreted language already has one strike against it with regard to code performance. Getting a bytecode interpreter to execute anywhere near the speed of a compiled language is very difficult. However, as the success of Visual Basic and PowerBuilder proved, applications are spending more and more time executing the user interface code of the operating system. The result is that for most applications, the need for blistering speed is becoming less of a concern than it used to be.
The differences between interpreted code and machine code become readily apparent, however, during complex mathematical calculations. Newer processors such as the Pentium can, in certain situations involving integer and floating-point arithmetic, dispatch multiple instructions per cycle. This is called superscalar execution. Superscalar execution allows certain machine code instructions to execute in parallel, which can provide a substantial performance improvement over non-superscalar designs.
The Java Virtual Machine is limited in its capability to take advantage of such advanced processor features. Why? Because the Java compiler and the Java client may run on different hardware, the Java compiler cannot possibly know how to optimally arrange bytecodes for the interpreter. Any performance optimizations targeted at one particular Java platform will usually be offset by significant losses on other platforms. Platform independence is one of Java's greatest strengths, so trying to perform hardware-specific optimizations doesn't make sense.
In order to test the code generation of the Visual J++ compiler, I wrote a test applet. The applet draws an animated 3D wire-frame surface. As such, the applet performs a significant amount of floating-point math while it calculates the vertices of the mesh, transforms the vertices, and then projects the surface to the screen. The applet also tests a compiler's ability to recognize and optimize incremental array accesses.
I compiled the test applet using the Visual J++ compiler and the Sun javac compiler that comes with the JDK. I tested both compilers with full optimizations turned on and off. In all four cases I timed the runs in milliseconds by bracketing the animation loop with calls to the System.currentTimeMillis() method. When the applet finishes, it displays the resulting frames, duration in milliseconds, and calculated frames per second.
The code for the test applet appears in Listings 10.1 through 10.4. Because programming Java applets isn't the subject of this chapter, only a quick overview of the code follows. The flow of the program is relatively straightforward. It calculates the vertices of a 3D sine surface, draws the surface in an offscreen buffer, copies the buffer to the screen, advances the phase angle of the surface, and then starts again.
The code calculates approximately 1,000 iterations of the surface animation. The grid is 20 by 20, or 400 individual squares. Each square is made up of 4 line segments, giving approximately 1,600 line segments for each frame. All told, the program has to execute a lot of bytecodes.
The first listing is the HTML file that executes the Java applet. The HTML file is very simple and straightforward. The <APPLET> tag tells the applet viewer to load and then execute the Java applet contained in Surface.class. Listings 10.2 and 10.3 provide the two helper classes, Vertex and View3D, used by the applet. Listing 10.4 contains the main class, Surface, which is a subclass of Applet.
Listing 10.1. Surface.html.
<HTML> <HEAD> <TITLE>3D Surface!</TITLE> </HEAD><BODY> <P>Wavy: <APPLET CODE="Surface.class" WIDTH=320 HEIGHT=200> Non-JAVA browser! </APPLET> </BODY> </HTML>
Listing 10.2. Vertex.java.
public class Vertex // extends Object implied
{
// object/world coordinates
double x, y, z;
// screen coordinates
int sx, sy;
}
Listing 10.3. View3D.java.
public class View3D
{
double ScaleX, ScaleY;
int TransX, TransY;
// default constructor
View3D()
{
ScaleX = ScaleY = 1;
TransX = TransY = 0;
}
// constructor with initializers
View3D(double sx, double sy, int tx, int ty)
{
ScaleX = sx;
ScaleY = sy;
TransX = tx;
TransY = ty;
}
public void setScale(double sx, double sy)
{
ScaleX = sx;
ScaleY = sy;
}
public void setTrans(int tx, int ty)
{
TransX = tx;
TransY = ty;
}
public void transform(Vertex AVertex)
{
AVertex.sx = TransX + (int)(ScaleX * 0.7071 * (AVertex.x - AVertex.z));
AVertex.sy = TransY + (int)(ScaleY * (0.7071 *
(AVertex.x + AVertex.z) - AVertex.y));
}
}
Listing 10.4. Surface.java.
import java.awt.Graphics;
import java.awt.Image;
import java.applet.Applet;
public class Surface extends Applet implements Runnable
{
final double phaseAngleMax = 150.0;
final double phaseAngleInc = 0.15;
Thread runner;
Image ImageBuffer;
Graphics OffScreenGraphics;
double phi;
View3D aView;
Vertex[] Vertices;
// called at each start
public void start()
{
if(runner == null)
{
runner = new Thread(this);
runner.start();
}
}
// called at each stop
public void stop()
{
if(runner != null)
{
runner.stop();
runner = null;
}
}
// run the thread
public void run()
{
long startTime = System.currentTimeMillis();
while(phi < phaseAngleMax)
{
// calculate vertices and transform
surface();
// show the surface - comment below is intentional
// repaint();
// advance the phase angle
phi += phaseAngleInc;
}
long endTime = System.currentTimeMillis();
System.out.println("Frames: " + phaseAngleMax / phaseAngleInc);
System.out.println("Millis: " + (endTime - startTime));
System.out.println("Frame rate: " + 1000*(phaseAngleMax /
phaseAngleInc)/(endTime-startTime));
}
// called once per app
public void init()
{
int i;
// phase angle is 0
phi = 0;
// create 3D view object
aView = new View3D(30, 30, this.size().width/2, this.size().height/2);
// allocate an array of object refs
Vertices = new Vertex[20*20];
// now we must actually create vertex objs
for(i=0; i< 20*20; i++)
Vertices[i] = new Vertex();
// create an empty image
ImageBuffer = createImage(this.size().width, this.size().height);
// get an image buffer
OffScreenGraphics = ImageBuffer.getGraphics();
}
// calculate the vertices
public void surface()
{
double x, z;
int i,j;
// calculate vertex coords
z = -1.5;
for(i=0; i < 20; i++)
{
x = -1.5;
for(j=0; j < 20; j++)
{
Vertices[i*20+j].x = x;
Vertices[i*20+j].y = Math.sin(x*x + z*z + phi);
Vertices[i*20+j].z = z;
x += phaseAngleInc;
}
z += phaseAngleInc;
}
// translate, transform and project each vertex
for(i=0; i < 20*20; i++)
aView.transform(Vertices[i]);
}
// don't clear the client area before paint!
public void update(Graphics g)
{
paint(g);
}
// paint the screen
public void paint(Graphics g)
{
int i,j;
// clear our memory buffer
OffScreenGraphics.setColor(getBackground());
OffScreenGraphics.fillRect(0,0, this.size().width, this.size().height);
OffScreenGraphics.setColor(getForeground());
// draw XZ lines?
for(i=0; i < 20; i++)
for(j=0; j < 19; j++)
OffScreenGraphics.drawLine(Vertices[i*20+j].sx,
Vertices[i*20+j].sy,
Vertices[i*20+j+1].sx,
Vertices[i*20+j+1].sy);
// draw YZ lines?
for(j=0; j < 20; j++)
for(i=0; i < 19; i++)
OffScreenGraphics.drawLine(Vertices[i*20+j].sx,
Vertices[i*20+j].sy,
Vertices[(i+1)*20+j].sx,
Vertices[(i+1)*20+j].sy);
// draw image from buffer to Window
g.drawImage(ImageBuffer, 0, 0, this);
}
}
The results of this very unscientific testing are shown in Table
10.1. They are very interesting not only for what they show but
also for what they don't show. You will notice little variation
among the examples. In fact, with optimizations turned on, only
a slight (but measurable) difference occurs between the javac
compiler and Visual J++, with Visual J++ edging out javac.
When optimizations are turned off, Visual J++ still produces slightly
faster code than the version of javac I tested.
| Compiler and Settings | |
| Visual J++ with no optimization | |
| Visual J++ with full optimization | |
| Sun javac with no optimization | |
| Sun javac with full optimization |
What can you take away from this bit of testing? For one thing, it shows, at least for code that involves a lot of math and animation, that using the compiler optimizations doesn't generate a lot of gains. In fact, it's quite likely that all I've been able to measure is the performance of the Java Virtual Machine and not the generated code. Therefore, you might choose to investigate one virtual machine against another.
On the other hand, the test could just as well be showing that both the Visual J++ and javac compilers generate terrific code whether optimizations are selected or not. Either way, as with any programming language, as Java compilers mature the code generation is certain to improve.
One of the more meaningful ways that a C compiler can be benchmarked is by comparing the code generated by the compiler against hand-tuned assembly language. A good assembly language implementation provides an idealized upper bound for performance against which you can measure a compiler. Sadly, Sun does not provide a Java bytecode assembler, or disassembler, with the JDK. As a result, I was unable to create a hand-tuned Java assembly language version for comparison.
In the end, the true performance of any Java application depends on any number of factors. These include the capabilities of the underlying hardware, the operating system, system load, and the browser that implements the Java Virtual Machine. For all the tests, I used the appletviewer program that comes with the JDK. The system I used for testing was running Windows 95 on a 100MHz Pentium system with 16MB of EDO RAM, 256KB of pipelined burst cache, and an S3-based PCI video board. You might want to test the code under other virtual machines to see how well they perform.
If the previous discussion of code performance left you a bit disappointed, take heart. A technique called just-in-time compilation may allow future Java applications to approach the speed of native applications.
Just-in-time compilation (often referred to simply as JIT) is a rather unique approach to dealing with the performance penalties associated with interpreted bytecodes. Rather than try to
optimize the Java Virtual Machine, which is already a game of diminishing returns, JIT takes a radically different approach.
The concept of JIT compilation is generally credited to developers of the SmallTalk language, which is usually implemented as a bytecode interpreter. The idea is very simple: The JIT compiler walks through the stream of bytecodes that make up a Java program. As each Java bytecode is encountered, actual machine code that implements that bytecode is substituted for it. When finished, the result is a program in a machine's native machine code! The benefits of this approach are tremendous, with potential speedups measured in factors of 10.
Even better, because the JIT compiler runs on the client, it has enough information about the client to perform hardware-specific optimizations on the machine code. Explicit knowledge of the client hardware allows a smart JIT compiler to use advanced features of processors such as the Pentium. Expect to see a number of Java Virtual Machines supporting JIT compilation very soon. Microsoft Internet Explorer 3.0, which comes with Visual J++, includes a JIT compiler. Several other browsers have announced this capability as well.
Are there other ways to improve the performance of Java code short of using a Java Virtual Machine with JIT compilation? Sure! In Java, as with most programming languages, performing high-level optimizations is almost always worthwhile. In this case, high-level optimizations means optimizations that affect the structure of code (and hopefully generated code) without regard for the CPU. High-level optimizations ignore architectural idiosyncrasies and focus instead on improving code through general features of the programming language. Lower-level optimizations are performed by the compiler, and they directly affect the organization of generated code.
Actually, the best high-level code optimization is always performed by the programmer. It involves using the best algorithms and the best data structures for the task at hand. For example, no amount of compiler optimization can make up for the inefficiency inherent in using a linear search when a hash table could be used instead.
As you design new Java classes, you should routinely ask yourself if you are using the best algorithm or data structure. Sometimes, however, you may want to use an inefficient, but easy to implement, algorithm to develop and test a new class quickly. If you later find that you need more performance, you may take advantage of the object-oriented nature of Java to derive a subclass that employs a more efficient algorithm.
An important side note is that the lack of a code profiler as part of the JDK is significant. Of course, this void creates a potentially lucrative market niche for software vendors. A profiler makes code optimization more of a science and less of an art form. Without a profiler, the programmer must rely on intuition and experience to determine the location of performance bottlenecks in Java code. Although determining where a program spends most of its time is usually obvious, having real data as confirmation is helpful. Sometimes, in fact, the profiler reveals bottlenecks that the programmer did not anticipate.
Beyond choosing the proper algorithms, the programmer can manually perform a number of high-level optimizations. As mentioned previously, the interpreted nature of Java bytecodes limits the scope and depth of these types of optimizations. In order to understand how to optimize Java code, you need a basic understanding of how the Java Virtual Machine works.
Just like a CPU, the Java Virtual Machine follows a rather standardized process for executing code in three distinct phases: fetch, decode, and execute. Many of the recent advances in microprocessor design have involved using hardware to execute these phases in parallel with the output of one stage feeding the next. This technique is called pipelining. The current Java Virtual Machine does not have this capability.
A unique feature of the Java Virtual Machine is that it employs a stack-based design. This is in contrast to the more common register-based designs found in most CPUs. The purpose behind the register-based design is to allow the CPU to store data in very high-speed units called registers, rather than storing data in main memory. The exact configuration of registers is CPU specific and is accomplished via hardware. Therefore, a Java Virtual Machine cannot efficiently use a register-based design. Because all data storage is effectively to main memory, the Java designers felt that the stack-based design would be more appropriate.
The Java Virtual Machine stores all operand data on a last-in, first-out (LIFO) stack. The Java bytecodes represent instructions to push (store), pop (retrieve), and perform mathematical and logical operations on data contained in the stack. Program data is retrieved from user memory and pushed on the stack, manipulated, and then possibly stored back in user memory.
For all the above reasons, optimizing Java code boils down to one simple rule: Minimize the number of bytecodes to be executed. If you ever programmed for the 8086 or the 8088, these optimization techniques should be familiar territory. As with many of the older CPU designs, the overhead in the fetch, decode, and execute pipeline effectively limits the performance of the Java Virtual Machine.
In order to minimize the number of bytecodes, you should focus your efforts on the most-used code in your application. This category includes heavily looping code and frequently called methods. Reducing the number of operations and therefore bytecodes inside a loop will always produce a performance improvement. For example, look at the following code:
// original method
public void foo()
{
for(int i = 0; i < 100; i++)
{
for(int j = 0; j < 100; j++)
{
sum += Math.sin(i*i + j*j);
}
}
}
// faster method
public void fastfoo()
{
int t1;
for(int i = 0; i < 100; i++)
{
t1 = i*i;
for(int j = 0; j < 100; j++)
{
sum += Math.sin(t1 + j*j);
}
}
}
The first method is implemented in a straightforward manner calculating a sum. The method executes 20,000 multiplies and 20,000 adds. By introducing a temporary variable in the second method, you can pull one of the multiplications outside of the inner loop. The second method executes 10,100 multiplies and 20,000 adds, saving 9,900 multiplies. This method is actually a very old optimization technique called code motion. Most compilers employ code motion and loop invariant analysis as a basic optimization technique. The current Java compilers do not appear to use this optimization, but it is easy enough to perform manually.
Also avoid calling methods because each method call has associated overhead. Every method call involves saving the current state, including the return address, and jumping to a new code location. None of these activities results in useful work by the application. They are just necessary to support the structure of the language.
Another way to reduce the number of bytecodes is to only use the int and float data types when appropriate, as opposed to long and double. The Java int and float data types are both 32-bit quantities. Therefore, they are both shorter and more efficient on typical Java client platforms than wider data types. The wider data types have great range and accuracy at the cost of more bytes of data to be processed.
Avoiding or minimizing the number of calls to the Java AWT classes is also desirable, although doing so may not seem intuitive at first. Nearly all the methods in the AWT classes eventually map down to calls to the native operating system. Whether implemented by the Java Virtual Machine or natively by the operating system, these methods tend to be highly unpredictable in their performance. As an example, drawing polygons may be very quick on your Java client due to optimized operating system drivers, but it may be incredibly slow on another system in which such features must be emulated.
Another potential source of poor performance is the use of the generic Java classes such as Stack or Vector. These classes are very useful, but in some circumstances they may not be appropriate. For example, consider an application that requires a stack of floating-point values. To implement this stack using the Java class Stack, you would need to use the Java object-wrapper class Float, as shown in the following code:
public foo()
{
float aValue;
Float aFloatObj;
aStack.push(new Float(14.7));
aFloatObj = aStack.pop();
aValue = aFloatObj.floatValue();
}
The creation of the object-wrapper class does not serve a useful purpose. Rather, it is required because the Stack class can manage only objects, not Java primitives. A better solution in this case is to create a Java class that implements a stack of floating-point values directly:
public void foo()
{
float aValue;
aBetterStack.push(14.7);
aValue = aBetterStack.pop();
}
One useful method for improving performance is inlining frequently called code. This technique is functionally equivalent to manually retyping the body of a method where it is called rather than actually performing a method call. Of course, you would not want to retype the same code in a number of places throughout a Java application. In fact, for most circumstances the data access rules of objects with private data wouldn't allow you to do so.
The member access rules of Java require that certain restrictions be observed for inlining to occur. These restrictions currently permit only static, private, and final methods to be inlined. The following code shows how method inlining would look if it could be done manually:
public final int sum(int a, int b)
{
return a + b;
}
public void Method1()
{
int result;
result = sum(1, 2);
}
public void Method2()
{
int result;
// call inlined sum() method
result = sum(1, 2);
// code actually executed is result = 1 + 2;
}
In Method1() the sum() method is called without inlining. This results in the normal method call overhead. If the sum() method were final, static, or private, it could be inlined by the compiler as depicted by Method2(). Notice how Method2() does not actually call the sum() method. Instead, the code that is executed behaves as if the body of the sum() method were retyped in place of the call.
Visual J++ supports method inlining, and you should take full advantage of this feature for accessor methods. An accessor method is a method that does nothing more than retrieve or store a data member of an object. Such methods are common in object-oriented languages, and they can be a source of poor performance. Method inlining was developed to maintain the integrity of private data members while allowing improved performance.
The concept of synchronized methods is a feature of the Java language that supports multithreaded applications. The idea is that each object has a lock that must be acquired by a thread that wants to execute a synchronized method. Any other thread that wants to call any synchronized method of the same class will be blocked until the first method completes. Synchronized methods are useful for developing stable, multithreaded code. However, improper use of synchronized methods can result in very poor performance. One thread could enter a synchronized method and block a number of other threads for some time. I won't discuss multithreading any further here, because this complex subject is discussed in detail in Chapter 7 "Advanced Java Programming." Just remember to use synchronized methods only when necessary to avoid the possibility of blocking other threads unintentionally.
The Java programming language supports array bounds checking. This feature is especially useful for locating troublesome bugs. Unfortunately, array bounds checking does impose some runtime overhead on each and every array access. Therefore, you should try to minimize the number of array accesses, particularly redundant ones. You can use temporary variables to do so. Consider these two methods:
public void foo()
{
for(int i=0; i < 100; i++)
for(int j=0; j < 100; j++)
A[i] += B[j] + B[j]*C[i];
}
public void better()
{
float a, c;
for(int i=0; i < 100; i++)
{
c = C[i];
a = A[i];
for(int j=0; j < 100; j++)
a+= B[j] + B[j]*c;
A[i] = a;
}
}
The first method shows a hypothetical calculation involving some matrix manipulations. You can improve this method by introducing the temporary variables a and c to hold the values of A[i] and C[i]. This makes more sense once you note that A[i] and C[i] are constants inside the inner loop. This modification results in a savings of approximately 20,000 (actually 19,700) array access calculations.
Many more optimization techniques are available to Java programmers. You may even think that most Java code-optimization techniques are just common sense, as most logical things appear to be once they are understood. Beyond this level of optimization, the next lower level involves manipulations of the actual Java bytecodes generated by the compiler. Current Java compilers do not support very sophisticated transformations of generated code. Visual J++ currently supports only method inlining and jump optimizations. However, Java compilers including Visual J++ will certainly continue to become more sophisticated in their optimization techniques as they mature.
Several other compiler options can be set through the Developer Studio. Each option can be specified more than once because the Developer Studio maintains parallel debug and release versions of your project by default. The options are accessed via the Build | Settings... menu option. When selected, a dialog box called Project Settings appears. On the left side of the dialog box is a list of the projects that are part of the currently opened workspace. The list is in the form of an expandable outline view of each project. Expanding a project shows the files that make up the project.
On the right side of the screen is a properties sheet with three tabs: General, Debug, and Java. Figure 10.2 shows the General options tab. The items on this tab let you specify a path or paths where additional class files for your project are stored. You could use this option if you have a common network subdirectory that is shared by several users. The output directory lets you specify where the resulting class files are stored.
Figure 10.2 : The Visual J++ compiler General options tab.
Several options under the Debug tab, shown in Figure 10.3, tell Visual J++ how to manage debugging tasks. The Category drop-down box selects General, Browser, Stand-alone interpreter, and Additional classes. Each selection enables you specify different debug options. For example, the Browser category contains options such as the browser you want to run for debugging. The Stand-alone interpreter category lets you choose a Java Virtual Machine for running applications.
Figure 10.3 : The Visual J++ Debug options tab.
The Java tab, shown in Figure 10.4, lets you specify the Warning Level as well as the optimization and debug level. The Warning Level defaults to 2-1 provides the fewest warning messages and 4 provides the most. The Full Optimization check box either enables or disables full optimization. The only way to set individual optimization options such as method inlining (/O:I) or jump optimizations (/O:J) is to type them in the Project Options text box.
Figure 10.4 : Visual J++ Project Settings optimization options.
The Generate Debug info check box either enables or disables debug information in your class files. Again, individual debug options such as Generate line numbers or Debug tables are not provided as check boxes, but you may type them in the Project Options text box.
As mentioned earlier, Visual J++ includes a command-line version of the compiler. The compiler executable is JVC.EXE, and it is located in the \MSDEV\BIN subdirectory. You can run JVC from a DOS prompt or from within a makefile by using the following syntax:
JVC [options] <filename>
As Table 10.2 shows, a number of command-line options are available.
| Option | Description |
| /cp <classpath> | Set class path for compilation |
| /cp:p <path> | Prepend path to class path |
| /cp:o[-] | Print class path |
| /d <directory> | Root directory for class file output |
| /g[-] | Full debug information (g:l, g:t) |
| /g:l[-] | Generate line numbers (default is none) |
| /g:t[-] | Generate debug tables (default is none) |
| /nowarn | Turn off warnings (default is warn) |
| /nowrite | Compile only-do not generate class files |
| /O[-] | Full optimization (O:I, O:J) |
| /O:I[-] | Optimize by inlining (default is no opt) |
| /O:J[-] | Optimize bytecode jumps (default is no opt) |
| /verbose | Print messages about compilation progress |
| /w{0-4} | Set warning level (default is 2) |
| /x[-] | Disable extensions (default is enabled) |
All options must appear before the Java filename. When the compiler is executed, it reads in the specified Java source file and writes out a class file containing the Java bytecodes. Normally you can specify only a single Java filename on the command line. However, JVC allows you to specify a response file on the command line by replacing the Java source filename with an at symbol (@) followed by the response filename. You can use either an absolute or a relative path name.
A response file is a plain text file that can contain only Java source filenames. It cannot contain JVC compiler options, and it cannot be used to invoke the JVC compiler itself. Any compiler options must still be specified on the command line.
The /cp options allow you to specify a path for class files generated by JVC. Versions exist for specifying absolute class paths or relative class paths. The /d option allows you to specify a root directory for class file output.
The /g option enables or disables generation of debug symbol information in the class files. The suboptions /g:l and /g:t tell JVC to generate line numbers and debug tables, respectively. Debug information is turned off by default.
The /nowarn message turns off warning messages from the compiler. Warnings are enabled by default. The /nowrite option tells JVC to parse the input file for errors and to produce warning messages, but not to generate any class file output. This option can be used to syntax-check code.
The /O option tells the compiler to perform full optimization, including code movement. The suboption /O:I tells JVC to optimize via inlining small static methods. The suboption /O:J tells JVC to optimize jump bytecode usage. no opt is the default optimization for Visual J++ projects.
The /verbose option tells the compiler to provide additional messages as it compiles source code files. The compiler informs you of every class reference it detects as it parses the source code.
The /w option allows you to specify the warning level. The higher the warning level, the more JVC warnings are produced. A warning level of 2 is selected by default. You should always enable as many warning messages as possible in a compiler to give the compiler the opportunity to suggest possible errors in your code.
The last option, /x, tells JVC to disable any nonstandard Java extensions. It is enabled by default. No such extensions were documented at the time this book was written.
Another useful tool included with Visual J++ is the WinDiff utility, which is a Windows version of the classic UNIX diff utility. The program allows you to compare directories as well as individual files. To run WinDiff, simply double-click the icon in the Microsoft Visual J++ folder or program group. Select either Compare Files or Compare Directories from the File menu.
A typical WinDiff display is shown in Figure 10.5. In the figure, the two versions of the View3D file in Listing 10.3 are being compared. The differences between the files are shown both graphically and using the standard diff notation. In the second version of the code, lines 36 and 37 of the listing were edited to add a call to the Math.floor() method.
Figure 10.5 : File comparison using the WinDiff utility.
The WinDiff utility can also be run from the command line using the following syntax:
WinDiff path1 [path2] [-s [options] savefile]
The options you can enter from the command line are shown in Table
10.3. Specifying only a single path causes WinDiff to compare
files in the current directory with those in the path. Specifying
two paths compares files in both paths. The command-line switches
modify these actions in different ways.
| Option | Description |
| path1 | Compares files in path1 with files in current directory. |
| path1 path2 | Compares files in path1 with files in path2. |
| options | Can be any combination of the following options: |
| /s: | Compares files that are in both paths. |
| /l: | Compares only files in the first (left) path. |
| /r: | Compares only files in the second (right) path. |
| /d: | Compares two different files in both paths. |
| savefile | Name of text file where comparison results are written. |
The /s option tells WinDiff to compare files that appear on both paths. WinDiff shows a list of all the files and identifies which directory has the most recent copy. You can see the file differences by selecting a file and clicking on the Expand button.
The /l and /r options compare only files in the left and right paths, respectively. The last option, /d, compares two different files in both paths. All the options are available as menu options. By specifying the -s option, you can tell WinDiff to save the results of any file comparisons to a file.
When comparing files from one directory, WinDiff shows a list of all the files in the current directory and any subdirectories showing files. You can view the contents of a file by selecting the file and clicking on the Expand button.
When comparing two different directories, WinDiff tells you which files are newer and which are identical. You can show the differences between two versions of a file by selecting the file from the list and clicking on the Expand button.
The Visual J++ compiler is a great tool for writing Java programs. I had no trouble creating Java applets using the Applet and Class Wizards. The integration of Internet Explorer with Visual J++ as a debugging tool makes development and testing of Java applets even easier. Because both Visual J++ and Internet Explorer support the use of embedded ActiveX controls, you can expect to see a tidal wave of creative Java applets and applications.