More often than not Java developers end up using an IDE like Eclipse or Netbeans for writing, compiling, and packaging their projects. There's also a good chance a tool such as Ant or Maven is being leveraged in this environment. These are all great tools for productive development which I actually use myself, but they shield a developer from what's going on under the hood. This is generally a good thing, but for those occasions when something goes wrong or you're having a hard time understanding why class Foo is not being picked up by the compiler, it's good to have an understanding of what magic is really happening underneath all the bells and whistles.
In this article we'll be discussing the ins and outs of the javac compiler and how it's used with the sourcepath and classpath arguments as well as how the compilation process is affected by code packaging schemes. This article assumes you already have java 1.6 installed and properly configured on your machine and that you're using Windows XP. While some of the commands are OS-specific, the lessons are fairly universal so Unix/Linux users shouldn't have much trouble following along as well. By no means will this be an exhaustive discussion, but it should have you well on your way to using the command prompt (or shell) for compiling Java applications.
Let's start with a simple example to get the ball rolling. First, open up a command prompt and get your project structure in order. Here's my sample project structure:
For a text editor I've grown quite fond of gvim. It's a Windows port of the popular and ubiquitous Unix editor, Vi, and has many powerful text editing features. I highly recommend learning an editor like this, but it's not something you pick up overnight so feel free to use any plain text editor you like for this tutorial.
The last thing I'd like you to do here is clear out the CLASSPATH environment variable. More on why I'm having you do this later, but for now just go with it and run the following in your command shell:
C:\DevProjects\ExampleProject>set CLASSPATH=
Now let's run javac and see what happens...
C:\DevProjects\ExampleProject>javac -verbose src\MainClassNoPkg.java
[parsing started src\MainClassNoPkg.java]
[parsing completed 31ms]
[search path for source files: .]
[search path for class files: C:\java\jdk1.6.0_18\jre\lib\resources.jar,C:\java\
jdk1.6.0_18\jre\lib\rt.jar,C:\java\jdk1.6.0_18\jre\lib\sunrsasign.jar,C:\java\jd
k1.6.0_18\jre\lib\jsse.jar,C:\java\jdk1.6.0_18\jre\lib\jce.jar,C:\java\jdk1.6.0_
18\jre\lib\charsets.jar,C:\java\jdk1.6.0_18\jre\classes,C:\java\jdk1.6.0_18\jre\
lib\ext\dnsns.jar,C:\java\jdk1.6.0_18\jre\lib\ext\localedata.jar,C:\java\jdk1.6.
0_18\jre\lib\ext\sunjce_provider.jar,C:\java\jdk1.6.0_18\jre\lib\ext\sunmscapi.j
ar,C:\java\jdk1.6.0_18\jre\lib\ext\sunpkcs11.jar,.]
[loading java\lang\Object.class(java\lang:Object.class)]
[loading java\lang\String.class(java\lang:String.class)]
[checking MainClassNoPkg]
[loading java\lang\System.class(java\lang:System.class)]
[loading java\io\PrintStream.class(java\io:PrintStream.class)]
[loading java\io\FilterOutputStream.class(java\io:FilterOutputStream.class)]
[loading java\io\OutputStream.class(java\io:OutputStream.class)]
[wrote src\MainClassNoPkg.class]
[total 297ms]
That might look overwhelming at first, but bear with me and all we'll walk through it. On the first line we're actually executing
javac. I've intentionally included the -verbose argument so we can take a look at what's going on under the hood, but normally you can feel free to leave it out. Immediately following you will see the reference to our MainClassNoPkg.java class file we wrote. Everything else that follows is output from the javac compiler.
First, you can see that the compiler picked up our java source file and parsed it. Notice that immediately following the compiler mentions searching paths for source and class files. The default for both sourcepath and classpath is the current directory. In addition, the compiler will add the bootstrap and extension classes that shipped with the javac compiler being used, though you can modify this behavior with the -
bootclasspath and
-extdirs options if you wish. However, we won't be changing these options in this article. You can see all the bootstrap and ext classes listed in the output for the class file search path. Finally, the output shows the compiler loading the classes MainClassNoPkg references.
For one simple java source file that was pretty painless, right? You could even simplify this further by using wildcards
C:\DevProjects\ExampleProject>javac src\*.java
But we've only compiled one file, you might say. The standard Java tutorial shows me how to do that, there must be more to it than this, right? Indeed, there is. Next we'll discuss compiling multiple source files at once and using the -
sourcepath and -
classpath options.
Let's Get To the Source
In this section I'm going to cover how the compiler searches for your uncompiled Java source files and turns them into executable bytecode. We're going to add to our example now by creating another class appropriately named AnotherClass with the following contents
// AnotherClass.java
// Default package
class AnotherClass {
AnotherClass() {}
public void printMessage(String msg) {
System.out.println("The message is: " + msg);
}
}
Next, modify the MainClassNoPkg.java to use the newly created class.
// MainClassNoPkg.java
// Default package
class MainClassNoPkg {
MainClassNoPkg() {}
public static void main(String[] args) {
System.out.println("Msg from MainClassNoPkg");
AnotherClass lAnother = new AnotherClass();
lAnother.printMessage("Hello Java developers!");
}
}
Your source tree should look as follows, assuming you've cleaned up the previously created
.class file.
C:\DevProjects\ExampleProject>tree /F
Folder PATH listing
Volume serial number is
C:.
├───classes
└───src
AnotherClass.java
MainClassNoPkg.java
Now run javac as we did before, specifying the main class name. Don't use the wildcard character this time.
C:\DevProjects\ExampleProject>javac src\MainClassNoPkg.java
src\MainClassNoPkg.java:8: cannot find symbol
symbol : class AnotherClass
location: class MainClassNoPkg
AnotherClass lAnother = new AnotherClass();
^
src\MainClassNoPkg.java:8: cannot find symbol
symbol : class AnotherClass
location: class MainClassNoPkg
AnotherClass lAnother = new AnotherClass();
^
2 errors
Uh-oh what happened!? That is most definitely NOT what we want to happen, but no worries as this was just a way to make a point about how the javac compiler finds the classes it needs during compilation. Remember when we cleared out the
CLASSPATH environment variable? By doing so we instructed javac to default it's classpath to the current directory (normally denoted by a period). The compiler also takes a
-sourcepath argument and when we don't specify this the default is also the current directory.
The point to make here is that while we specified a java file to compile in the
src directory, the compiler couldn't find the referenced
AnotherClass.java file. Why, you might ask? But isn't the compiler smart enough to grab other files in the same directory as the source file we instructed it to compile? Well, no actually. The Java compiler does not really do any searching based on the source files (and their paths) you pass as arguments to javac. If you want javac to find
AnotherClass.java you have to handle it like this:
C:\DevProjects\ExampleProject>javac -sourcepath src src\MainClassNoPkg.java
or this
C:\DevProjects\ExampleProject>javac -cp src src\MainClassNoPkg.java
or this
C:\DevProjects\ExampleProject>cd src
C:\DevProjects\ExampleProject\src>javac MainClassNoPkg.java
In the first two examples we specified a location for the compiler to find other ancillary classes by passing the
src directory as the
-sourcepath or
-cp (same as
-classpath) options. In the third example we cd'ed into the src directory and executed the javac command. Specifying a sourcepath or classpath was unnecessary because remember that the compiler uses the current directory as the default when those options aren't specified. If you need to pass more than one directory, simply separate them with a semicolon (;), no spaces. Also note that we could have set the CLASSPATH environment variable instead of using the command line arguments. This is particularly well-suited for long path definitions or for occasions when you'll need to run the javac command more than once.
A more complete order of operations for searching for source files is as follows and additional details can be found in the javac documentation listed in the references at the end of this article.
- paths listed via the -sourcepath option are searched
- -cp or -classpath arguments are searched
- The CLASSPATH environment variable is searched
- The current directory
OK, now assuming you've successfully run one of the examples above, the compiler should spit out a couple .class files and you should be able to run your program without any errors as follows:
C:\DevProjects\ExampleProject>java -cp src MainClassNoPkg
Msg from MainClassNoPkg
The message is: Hello Java developers!
The point in this section's example is to show that you
must include the relative or absolute path in your source file list and you must include at least one reference to a source file, whether by wildcard or explicit declaration, regardless of what you include as arguments to
-cp and
-sourcepath. The following, while seemingly clever enough and seemingly within javac's rules, just simply will not work!
C:\DevProjects\ExampleProject>javac -sourcepath src *.java
javac: file not found: *.java
Usage: javac <options> <source files>
use -help for a list of possible options
C:\DevProjects\ExampleProject>javac -sourcepath src MainClassNoPkg.java
javac: file not found: MainClassNoPkg.java
Usage: javac <options> <source files>
use -help for a list of possible options
Furthermore, the compiler will oblige you to grab source files referenced by any source files you are compiling, but
only if it can find them on the source or class paths. There is simply no way to instruct javac to search recursively for a source or class file. This comes into play when we deal with packages, but we'll discuss this further in the next section. Now onto our next lesson, compiling using references to source that's already been compiled.
Javac and Source Files Vs. Class Files and JARs - What the Diff?
We've already compiled a number of example projects so now let's expand on this code to illustrate another point - how javac treats java source files differently from previously-compiled class files (java bytecode in a file with a
.class extension).
Imagine you've been coding a project for a while now and have numerous source files distributed across multiple packages (we'll cover this later) and projects and are using a couple external libraries. Obviously, there are times that we might not want to compile every last bit of code in our project, but more importantly there are times when this might not be feasible. Think of the situation where you've purchased a jar file from an external company and all you've received is compiled java bytecode. In other words, you don't have the source code because it's proprietary. But you still want to use the wonderful API/library that, perhaps ClevelandFlash, has developed for you. Will ClevelandFlash let it be so? Can this be done? Of course it can! Otherwise we wouldn't be here and instead developers would be off using some other language for its code reuse facilities. Even if you have access to the source code, which is par for the course for open source projects (thus the term "open source!"), you don't want to be compiling them from source unless absolutely necessary. Leveraging previously compiled code is in fact pretty simple to do, but first let's modify our code so that it's not using the yucky default package anymore.
Hopefully you're familiar with Java packages as I'm not going to go into the details here, but if not no worries as you should still be able to run the examples. I do, however highly recommend your taking the time to check out the Java Tutorials
basic package explanation and
package trail at your earliest convenience. Okay, onto the examples now. First, let's move our java source files to their new package locations and rename
MainClassNoPkg to
MainClass. I'm going to be using
com.clevelandflash as my root package name to stick with convention, but you're free to use your own values here if you wish. Just be sure to adjust the source code and command prompt commands accordingly to reflect these changes!
My tree now looks like the following
C:\DevProjects\ExampleProject>tree /F
Folder PATH listing
Volume serial number is
C:.
├───classes
└───src
└───com
└───clevelandflash
└───javac_tut
├───core
│ MainClass.java
│
└───helper
AnotherClass.java
And here are the updated source files:
// MainClass.java
// No longer the default package
package com.clevelandflash.javac_tut.core;
import com.clevelandflash.javac_tut.helper.AnotherClass;
class MainClass {
MainClass() {}
public static void main(String[] args) {
System.out.println("Msg from MainClass");
AnotherClass lAnother = new AnotherClass();
lAnother.printMessage("Hello Java developers!");
}
}
// AnotherClass.java
// No longer the default package
package com.clevelandflash.javac_tut.helper;
public class AnotherClass {
public AnotherClass() {}
public void printMessage(String msg) {
System.out.println("The message is: " + msg);
}
}
Now, run the following command
C:\DevProjects\ExampleProject>javac -d classes -sourcepath src src\com\cleveland
flash\javac_tut\core\MainClass.java
Here we've used the
-d option with a value of
classes to instruct the Java compiler to output the
.class files to the
classes directory rather than alongside the
.java source files, as is the default. The compiler does something additionally useful in that it automatically generates the package folder structure in the output directory. Cool!
C:\DevProjects\ExampleProject>tree /F
Folder PATH listing
Volume serial number is
C:.
├───classes
│ └───com
│ └───clevelandflash
│ └───javac_tut
│ ├───core
│ │ MainClass.class
│ │
│ └───helper
│ AnotherClass.class
│
└───src
└───com
└───clevelandflash
└───javac_tut
├───core
│ MainClass.java
│
└───helper
AnotherClass.java
This is really good considering the Java language
requires you to match the folder structure to your package names. One small exception, though not considered a best practice, would be if you had a source file in the
core directory but left it declared as part of the default package. Javac would compile it just fine, but when it writes out the
.class file it would put it in
.\classes instead of in
.\classes\com\clevelandflash\javac_tut\core. Another important thing to note is that unless you use the
*.java wildcard on a each directory or enumerate each file that needs compiling, the compiler will NOT pick up non-referenced source files. So for instance, if you had a file IO helper class (just thinking of something random here) in the
helper directory that was atomic from the rest of the code, you'd have to be sure and include it in your javac command explicitly, otherwise it won't get compiled. The compiler only picks up necessary references (i.e. import statements in code) or files you explicitly declared in the source files list when calling javac. Make sense so far? Good! Let's just run our sample real quick to make sure everything works with our new package structure.
C:\DevProjects\ExampleProject>java -cp classes com.clevelandflash.javac_tut.core
.MainClass
Msg from MainClass
The message is: Hello Java developers!
Looks good, but I do want to draw attention to the fact that you need to specify two things when running the program so that the JVM can find your program. First, you must specify the classpath to include the
classes directory, and second, call the class that contains the
main method (
MainClass) by its full qualified name. If you went through the packages trail on the Java tuts site (if you didn't, now's a great time) you would immediately notice this is done simply by appending the class's package name as a prefix to the class name. If you've made it this far, you're well on your way to understanding Java compilation and classpaths from the command line. Thanks for reading!
But You Didn't Cover JAR and External Class Files Yet!@#%
The last section was just getting so long and it seemed only appropriate to wrap it up. Alright, on with the show as promised. In this final section we'll be compiling source code against some external libraries as well as some previously compiled code that we wrote ourselves. First thing's first, we need to setup another project directory and download a good library to use for our code examples.
Modify your folder setup so that you now have another project named
LibProject at the same level as
ExamplProject. Mine looks like this:
C:\DevProjects\LibProject>tree /F
Folder PATH listing
Volume serial number is
C:.
├───classes
├───lib
├───resources
└───src
└───com
└───clevelandflash
└───goodtimes
├───core
│ ClassForXerces.java
│
└───model
Company.java
Go ahead and also create the additional subdirectories and files as I have shown above. We'll work out the source code for the two java files shortly but first we need to find a library.
Just for fun, let's go with the Xerces XML parser. XML parsing, while potentially memory intensive, is a very popular and necessary activity for programmers these days. You can download it for yourself here -
Xerces2 Java Parser. I'm not going to go into setup instructions, but at the very least you'll need the following jar files:
- resolver.jar
- serializer.jar
- xercesImpl.jar
- xml-apis.jar
Let's put these in the
lib directory located in the
LibProject you just created. Also, we're going to need an XML file to work with. Seeing as the focus of this article is not XML nor Xerces, we'll save the complicated examples for another day and use something simple. This is the XML file,
companies.xml.
<companies>
<company status="spectacular">ClevelandFlash</company>
<company status="good">Some Company</company>
<company status="bad">Terrible Corporation</company>
</companies>
You can also download it here if you're so inclined
companies.xml. Place this in
resources directory in your
LibProject folder.
Okay, so let's code something already. We'll put the XML parsing code in
ClassForXerces.java and the model structure in
Company.java. This is an admittedly contrived example with very little if any emphasis on elegant design patterns, but we're not really concerned with that right now.
Here's
ClassForXerces.java
package com.clevelandflash.goodtimes.core;
import java.io.IOException;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.Attributes;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.SAXException;
import com.clevelandflash.goodtimes.model.Company;
public class ClassForXerces extends DefaultHandler {
private Company mCompany;
private StringBuffer mCurrentName;
private String mCurrentStatus;
public ClassForXerces() {
this.mCompany = new Company();
}
public Company getCompany() {
return this.mCompany;
}
public void parseFile(String pFileToParse) {
SAXParserFactory lSpf = SAXParserFactory.newInstance();
try {
SAXParser lSp = lSpf.newSAXParser();
lSp.parse(pFileToParse, this);
} catch(FactoryConfigurationError fce) {
fce.printStackTrace();
} catch(ParserConfigurationException pce) {
pce.printStackTrace();
} catch(SAXException se) {
se.printStackTrace();
} catch(IOException ioe) {
ioe.printStackTrace();
}
}
// Handle events from SAX parsing
public void startDocument () {
System.out.println("Start document");
}
public void endDocument () {
System.out.println("End document");
}
public void startElement(String pUri, String pName, String pQName, Attributes pAtts) {
this.mCurrentName = new StringBuffer();
this.mCurrentStatus = "";
if(pQName.equalsIgnoreCase("company")) {
this.mCurrentStatus = pAtts.getValue("status");
if(this.mCurrentStatus.equalsIgnoreCase("spectacular")) {
this.mCompany.setStatus(pAtts.getValue("status"));
}
}
}
public void characters(char pCh[], int pStart, int pLength) {
this.mCurrentName.append(new String(pCh, pStart, pLength));
}
public void endElement(String pUri, String pName, String pQName) {
if(pQName.equalsIgnoreCase("company")) {
if(this.mCurrentStatus.equals("spectacular")) {
this.mCompany.setName(mCurrentName.toString());
}
}
}
}
and
Company.java, our model class that will act as a container for Company data:
package com.clevelandflash.goodtimes.model;
public class Company {
private String mStatus;
private String mName;
public Company() {}
public Company(String pStatus, String pName) {
this.mStatus = pStatus;
this.mName = pName;
}
// Getters and Setters
public void setStatus(String pStatus) {
this.mStatus = pStatus;
}
public String getStatus() {
return this.mStatus;
}
public void setName(String pName) {
this.mName = pName;
}
public String getName() {
return this.mName;
}
// Override toString so we have something pretty to show
public String toString() {
StringBuffer lSb = new StringBuffer();
lSb.append("Company detail: ");
lSb.append("name=" + getName() + ", ");
lSb.append("status=" + getStatus());
return lSb.toString();
}
}
Also go ahead and update
MainClass.java to use the Xerces parser classes we created in
LibProject:
// No longer the default package
package com.clevelandflash.javac_tut.core;
import com.clevelandflash.goodtimes.core.ClassForXerces;
import com.clevelandflash.goodtimes.model.Company;
import com.clevelandflash.javac_tut.helper.AnotherClass;
class MainClass {
MainClass() {}
public static void main(String[] args) {
System.out.println("Msg from MainClass");
AnotherClass lAnother = new AnotherClass();
lAnother.printMessage("Hello Java developers!");
// Use Xerces!
ClassForXerces lParser = new ClassForXerces();
lParser.parseFile("..\\LibProject\\resources\\Companies.xml");
Company lComp = lParser.getCompany();
System.out.println("Found a company here...");
System.out.println(lComp.toString());
}
}
Now that you've typed, copied/pasted, or dictated your way through getting that code where it needs to be we're ready to compile. The first logical thing we'll want to do is compile the LibProject source code and this will be our first chance to include some external Java libraries (jars) in our compilation. There are no other code dependencies, unlike
ExampleProject which depends on
LibProject, so it's safe to start here when compiling. At this time, go ahead and clear out any previously compiled
.class files from your projects as well as the entire directory tree under the
classes directories so we have a fresh start. Your project directories in their clean state should look like the following:
C:\DevProjects\LibProject>tree /F
Folder PATH listing
Volume serial number is
C:.
├───classes
├───lib
│ resolver.jar
│ serializer.jar
│ xercesImpl.jar
│ xml-apis.jar
│
├───resources
│ companies.xml
│
└───src
└───com
└───clevelandflash
└───goodtimes
├───core
│ ClassForXerces.java
│
└───model
Company.java
C:\DevProjects\ExampleProject>tree /F
Folder PATH listing
Volume serial number is
C:.
├───classes
└───src
└───com
└───clevelandflash
└───javac_tut
├───core
│ MainClass.java
│
└───helper
AnotherClass.java
Now, navigate to the LibProject directory and execute the following:
C:\DevProjects\LibProject>javac -d classes -cp "lib/*" -sourcepath src src\com\c
levelandflash\goodtimes\core\ClassForXerces.java
You shouldn't receive any errors. If you do, you should first do a thorough job reading the error output to make sure it's not something simple like a misspelled word (case sensitivity matters in Java, if you haven't run into this yet) or missing semicolon.
The first argument to javac you should recognize from earlier - we're just telling the compiler again to output compiled bytecode to the
classes directory instead of alongside our source files. The next argument,
-cp, is where the dependency magic really happens. Here we're telling the compiler to include
lib directory on the classpath. Even better, the compiler allows us to use the wildcard symbol "*" to automatically include any and all jar files that happen to be in that directory. This saves us from having to type every single jar file in the classpath, though you could just as easily do that if you so desire. One more thing to note is that in using the wildcard symbol I'm forced to surround my classpath with quotes. This isn't true for all environments but it is for my Windows XP machine as it's currently configured. Again, check the javac documentation in the references below for more information about using the
-classpath argument. The next argument,
-sourcepath, is nothing special and again we include the full relative (that's relative from where we're executing javac) path to the root class we want to compile.
Now, navigate to the ExampleProject directory and execute the following:
C:\DevProjects\ExampleProject>javac -d classes -cp ..\LibProject\classes -source
path src src\com\clevelandflash\javac_tut\core\MainClass.java
This time we're leveraging the classes we just compiled from
LibProject, thus we include their relative root compiled classes location as a classpath argument to javac. Unlike when we compiled LibProject there's no need to include the Xerces jar files on the classpath. Why? Because while the project ultimately needs those jars to execute, the dependency on them is further down the chain and we've already compiled the code that depends on those jars directly. In other words, since
ExampleProject's source code doesn't directly rely on the Xerces jars, we don't need to worry about them when compiling
ExampleProject.
The last thing for us to do is run our little experiment with the java tool. So finally, type the following in your command prompt and hit enter.
C:\DevProjects\ExampleProject>java -cp ..\LibProject\classes;..\LibProject\lib\*
;classes com.clevelandflash.javac_tut.core.MainClass
Msg from MainClass
The message is: Hello Java developers!
Start document
End document
Found a company here...
Company detail: name=ClevelandFlash, status=spectacular
Assuming all goes as planned, you should see the output as I've shown above. This time, the classpath includes references to the classes directories for both projects as well as the
lib directory from the
LibProject. Remember when I said you'd still need to think about that? Now, had our code not actually invoked any classes/methods that depended on those Xerces jar files we would not run into any problems. However, if you want to see what happens, rerun the example excluding the jar directory on the classpath, and you will be greeted with a grotesque error message from the JVM complaining about not being able to find such and such class. Last thing to note - the path you include to the Companies.xml file is relative to where you are actually call "java" from. Just something to keep in mind if you decide to do some of your own tinkering (which I encourage). Sorry, just one more thing... Those Xerces jars I had you download? Well, they come packaged with Java as of JDK 1.5, so really we probably didn't need to download them separately. But then again, that would have ruined the jar dependency lesson ;-) Okay, that's about it for this article on using the Java compiler and Java execution tool from the command line.
Conclusion
Give yourself a hand! In the world of advanced tooling and IDEs, many developers never get a chance to learn the basics and sometimes they just plain forget them. Now that you understand the Java compiler, go download Ant, or better yet, Maven!
Thank you for reading and I hope this article leaves you much more informed in compiling and running your own projects. Stay tuned for the next part in this series when I show you how to create your own class libraries in conjunction with the jar command!
Cheers,
Mike
Reference: