This is the second post in a series looking to understand Java projects. Earlier, we looked at how Java actually gets installed onto a macOS system, and now we're going to build a basic app. Then we'll move on to incorporating the same app with help from Gradle, a popular build tool, and we'll finish by incorporating our project into the IntelliJ IDE.
Our directory structure
We're going to be compiling a totally jazzed up version of Hello World, and our overblown example will make use of packages, libraries and tests because it's a more indicative of a project we'd spot in the wild. This project is intended for people who have worked a little with Java and isn't going to dwell much on syntax outside of tooling.
We'll be loosely following the conventions of the Maven standard directory layout, which is admittedly a little overkill for our tiny app - but we're also going to charge ahead with it because it's indicative of the conventions we'll commonly run into when working with Java. Here's how that looks:
.
├── bin
├── lib
│ ├── jfiglet-0.0.8.jar
│ └── junit-platform-console-standalone-1.4.2.jar
└── src
├── main
│ └── java
│ └── space
│ └── gaston
│ ├── greeter
│ │ └── Greeter.java
│ ├── helloWorld
│ │ └── HelloWorld.java
│ └── textEffects
│ └── Bubble.java
└── test
└── java
└── space
└── gaston
├── greeter
│ └── GreeterTest.java
└── textEffects
└── BubbleTest.java
Let's unpack what we've got here:
-
bin
will contain our compiled.class
files -
lib
will hold our third-party libraries. In this case we're using JFiglet and the JUnit test runner, which we'll get back to later. -
src
will house our.java
source code. Withinsrc
you have subdirectoriesmain
andtest
, which both have ajava
subdirectory to denote the language, and then the standard Java package hierarchy. Eventually, after months of digging, you finally stumble upon our actual.java
files.
Just a quick caveat: I haven't checked the lib
folder into Git so you'll need to grab the JFiglet and JUnit .jar
files from Maven if you're looking to build this project yourself. This is, partially, to expose how fiddly it is to manage dependencies manually and help us understand how, later on, how awesome it is that other tools can come to our rescue.
.java, .class and .jar
Okay, so I threw around quite a few filetypes just then. I think it's entirely common to be hidden from a lot of these specifics by our IDE, so here's a quick summary of what each of those types does:
- A
.java
file is a plain-text file of human-readable Java source code - though admittedly that 'readable' designation is, ahem, rather debatable when it comes to certain aspects of Java syntax. Basically,.java
files are the files we squishy human flesh monsters sling our code in. - A
.class
file is compiled Java bytecode that can be executed by the Java Virtual Machine, an admittedly snazzy tool which runs our Java programs. These cold, mechanical files are the ones the computer likes read. - A
.jar
file is an archive of.class
files (and other necessary resources) all neatly zipped up for convenience.
javac and java
Two of our JDK binaries are responsible for compiling and running our code: javac
and java
. In short, javac
is responsible for turning our .java
files into .class
files that java
can run.
If we put together a totally barebones HelloWorld.java
, we can then feed it as an argument to javac
and run it with java
:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Oh no, not another Hello World example...");
}
}
Without multiple .java
files, external libraries or output directories to worry about, compiling and running this is as straightforward as sending the file path to javac
and then running the output:
$ javac HelloWorld.java
$ java HelloWorld
Oh no, not another Hello World example...
Notice how the .class
file will be currently be compiled into the same directory as its companion source code, and that we call the HelloWorld
class directly with java
with no .class
extension - the latter would trigger a dreaded java.lang.ClassNotFoundException
error, but that's a story for another day. It turns out we use java
to run Java classes, not Java .class
files.
Classpath
Another potential error we might see right about now is a java.lang.NoClassDefFoundError
, which usually comes about because there's a .class
file that was created at compile time (with javac
) but has got lost somewhere when we're trying to run it with java
.
Be warned to those of a fragile constitution, the next word is enough to give you the vapours. This frightful concept has dashed the hopes and dreams of event the strongest minds, adventurer. Abandon all hope all ye who enter... the classpath.
Let's zoom in our our main .java
files:
.
├── greeter
│ └── Greeter.java
├── helloWorld
│ └── HelloWorld.java
└── textEffects
└── Bubble.java
Our Greeter.java
, HelloWorld.java
and Bubble.java
are all being stored in different packages (which, in Java, also means different directories) and have requirements of their own. HelloWorld
includes both a Greeter
and a Bubble
, with Bubble
itself an adapter to the third-party FigletFont
class from the JFiglet .jar
in our lib
folder.
Over in our test
folder we've got tests for Greeter
and Bubble
, which includes classes from JUnit in lib
as well as requiring the actual Greeter
and Bubble
classes to test.
Welcome, friends, to dependencies.
Java, while pretty smart, needs to know where to go sniffing for all these requirements - hence the classpath. We can get the scoop right from the horse's mouth:
The default value of the class path is ".", meaning that only the current directory is searched. Specifying either the CLASSPATH variable or the
-cp
command line switch overrides this value.
Also:
Setting the
CLASSPATH
can be tricky and should be performed with care.
Which is just charming. Essentially, our classpath needs to contain the path to our .jar
files to the top of our package hierarchies. It can be set either via environment variable, which you shouldn't do, or with the much better option of the -cp
flag.
It also helps explain a common Java gotcha, where you try and run java
without cd
'ing into the directory first.
$ java bin.space.gaston.helloworld.HelloWorld
Error: Could not find or load main class bin.space.gaston.helloworld.HelloWorld
Caused by: java.lang.NoClassDefFoundError: space/gaston/helloworld/HelloWorld (wrong name: bin/space/gaston/helloworld/HelloWorld)
bin
, you see, isn't part of the Java package hierarchy, but does need to be on the classpath if you're outside of the folder. So both of the below would work:
$ cd bin
$ java space.gaston.helloworld.HelloWorld
$ java -cp bin space.gaston.helloworld.HelloWorld
Bringing it all together
With all that in mind, we can start to compile our files into the bin
directory. From the top of our project directory, we can get to work.
1. Compiling our main
folder:
$ javac -d bin -cp lib/jfiglet-0.0.8.jar src/main/java/space/gaston/greeter/Greeter.java src/main/java/space/gaston/helloWorld/HelloWorld.java src/main/java/space/gaston/textEffects/Bubble.java
We're now using the -d
flag to specify where our compiled files should end up. We're manually feeding each of our .java
files to javac
, so we don't need to add them to the classpath, but we do need to add our JFiglet .jar
file so that Bubble
can compile.
2. Compiling our test
folder:
$ javac -d bin -cp lib/junit-platform-console-standalone-1.4.2.jar:lib/jfiglet-0.0.8.jar src/main/java/space/gaston/textEffects/Bubble.java src/test/java/space/gaston/textEffects/BubbleTest.java src/main/java/space/gaston/greeter/Greeter.java src/test/java/space/gaston/greeter/GreeterTest.java
We need to add both the JFiglet and JUnit .jar
files to our classpath, and now we've also got to feed in each test file and the file its testing to the compiler. We could effectively consolidate steps 1 and 2, but it's good to break them up for demonstration purposes here as I think it helps illustrate what's going on.
Our bin file will now look like this - notice that the directory structure of our .class
files must maintain the same package hierarchy as the .java
source files:
.
├── BubbleTests.class
├── GreeterTests.class
└── space
└── gaston
├── greeter
│ └── Greeter.class
├── helloworld
│ └── HelloWorld.class
└── textEffects
└── Bubble.class
3. Running our tests:
$ java -jar lib/junit-platform-console-standalone-1.4.2.jar -cp bin:lib/jfiglet-0.0.8.jar --scan-class-path
╷
├─ JUnit Jupiter ✔
│ ├─ BubbleTests ✔
│ │ └─ helloReturnsAsciiHello() ✔
│ └─ GreeterTests ✔
│ ├─ greetWithArgumentSteveReturnsHelloSteve() ✔
│ └─ greetWithNoArgsReturnsHelloWorld() ✔
└─ JUnit Vintage ✔
Our version of JUnit can be run on the command line. We're passing in the --scan-class-path
flag to automatically have JUnit look for all tests on our classpath, so this requires adding the bin
folder to the classpath (because we're at the top of our project folder) as well as JFiglet, which is still required by Bubble
.
Also, yay, the tests pass.
4. Running our main app:
$ java -cp bin:lib/jfiglet-0.0.8.jar space.gaston.helloworld.HelloWorld
_ _ _ _ ____ _
| | | | ___ | | | | ___ / ___| | |_ ___ __ __ ___
| |_| | / _ \ | | | | / _ \ \___ \ | __| / _ \ \ \ / / / _ \
| _ | | __/ | | | | | (_) | ___) | | |_ | __/ \ V / | __/
|_| |_| \___| |_| |_| \___/ |____/ \__| \___| \_/ \___|
No, I have no idea either why we tasked it to output 'Hello Steve'.
So, great. We've got that working. But, gosh, wasn't that a bit much for even a simple, totally contrived application? Can you imagine that literally every time you make a change to the app and need to recompile? I don't know about you, but if I had to work like that for over a week I'd be permanently stuck looking like I was cosplaying Edvard Munch's The Scream.
In the next post, the cavalry will march in to bail us out of a lifetime of perpetual build horror.
Has this post been useful for you? I'd really appreciate any comments and feedback on whether there's anything that could be made clearer or explained better. I'd also massively appreciate any corrections!
Top comments (5)
Yes, very helpful to see things laid out on the CLI in glorious, gory detail! It does a lot to enhance understanding.
I have a couple minor quibbles/corrections for you to consider. The first is that a .jar file is not restricted to holding .class files, but can also hold source and/or resources. True, a build is going to reference the jar almost without exception for the class files it holds. I did say this was a quibble.
Second, I stumbled a bit over the section "java and javac". I think I might have been less confused if the wording "If we put together a totally barebones HelloWorld.java" were changed to "If we put together the following totally barebones HelloWorld.java". That way, I'm more likely to figure out that the following is a new, ad hoc code example, and not some portion of the project. (I am slow to pick stuff like this up, and can use all the help a writer is willing to give.)
A couple other spots got me, but I figured things out. Overall very helpful article. Thank you!
A really good overview, although I was wondering how would I view the classpath of a directory? I can set it but what about viewing it (via cmd line)?
Great article. Would keep it in my bookmarks for reference...
Very helpful article. Thank you.
Waiting for your following article about how the build tool like Gradle and Maven could make the compile and run process less painful
is there a github repositoryfor this project?