Congratulations! Give yourself a pat on the back for completing the Java Basics tutorial series. I’d like to thank you for sticking with it and giving The Renegade Coder your support. If this is your first Java tutorial experience, let me know how it’s going in the comments. Hopefully, by the time you’ve finished this review, you’ll be comfortable with writing your own basic classes.
Table of Contents
- Refresher
- Review with a Grading Program
- Introduction to Arrays
- Making a Test Class
- Creating A Test Set
- Establishing a Grader
- Wrapping Up a Series
Refresher
By this point, we’ve covered the following topics:
- Digital Logic and Binary
- Primitive Types, Variables, Operators, and Type Casting
- Reference Types, Strings, and Objects
- Instances, Methods, Overloading, and the Stack
- Classes, the Command Line, Constructors, Garbage Collection, and the Main Method
- Conditions, Logic Operators, If Statements, and Switch Statements
- JUnit Testing, Design by Contract, Debugging, and Code Coverage
- Iterative Looping, Recursion, and Refactoring
- Coding Styles and Comments
Now to wrap up the series, we’re going to put together a real life example. That way we’ll be able to see a lot of the concepts above in practice. This will give us the opportunity to clarify any topics that we might be struggling with.
Review with a Grading Program
Let’s start by creating a program that can grade tests. That statement alone should give us an idea of the types of classes we might want to have:
- A Grader Class
- A Test Class
In this scenario, we’ve replaced a real teacher with a grader program. The grader program should be able to receive a test and give it a letter grade based on the overall score. The grader will define the letter grade range for each test. For instance, maybe on one test an A is given to students who score above a 90%, and on another test an A is given to students who score above a 50%.
Meanwhile, the tests should be configurable such that they should have a number of total questions and a number of questions correct. For simplicity, we won’t define what those questions are or how much each question is weighted. The score for an exam will be the total number of correct answers over the total number of questions.
With the ranges defined and the score achieved, we should be able to give any test to the grader. The grader should then be able to quickly calculate the percent of correct answers and determine the letter grade. To make the grader more interesting, we’ll allow them to accept multiple tests at once, so they can determine the average for the class. That means we’ll probably want to also create a test set class.
Introduction to Arrays
That last line may worry us a bit. We haven’t actually talked about any sort of collections yet. No worries though! Arrays aren’t terribly hard, so we shouldn’t feel too bad about learning something new in the review article. That said, we won’t dive in too deep just yet. After all, we already have an entire tutorial on arrays.
An array is a data structure that allows us to store a set of data in a contiguous block of memory. Because it’s contiguous, we need to specify its size ahead of time. In our case, we’ll be defining an array of tests, so we’ll want to know how many students we have:
CodingTest tests[] = new CodingTest[15];
On the left side of the assignment statement, we declared a CodingTest
array with the name tests
. That means the array can only store objects of type CodingTest
.
On the other side, we have defined tests
as a CodingTest
array of size 15. That means we can only store 15 tests in our array. If our array gets full, we’re stuck, so we’ll want to pick a good size to start.
To add a test to the array, we might do something like the following:
tests[0] = new CodingTest();
This inserts a new test into the first slot in the array. Much like strings, arrays are also indexed starting at zero. Also like strings, arrays don’t allow us to go poking around outside their bounds. Otherwise, we will get an IndexOutOfBoundsException
. This protects us from illegally accessing memory.
Making a Test Class
Now, we’ve learned everything we need to know to start lying down some groundwork. Let’s start by making the CodingTest
class:
public class CodingTest { public static final double A_MIN = 90; public static final double B_MIN = 80; public static final double C_MIN = 65; public static final double D_MIN = 50; private int numberOfQuestions; private int numberOfCorrectAnswers; public int getNumberOfQuestions() { return this.numberOfQuestions; } public int getNumberOfCorrectAnswers() { return this.numberOfCorrectAnswers; } }
As we can see, a CodingTest is defined as a class with four constants and two fields. It’s possible that we would want to configure the grade ranges, but we’ll keep it simple by defining them for now.
Refactoring Our Solution
Now, the grader just needs to be able to take a list of tests and determine the letter grade. For the sake of abstraction, we’re going to refactor our existing CodingTest
class to do the calculation for us.
public class CodingTest { public static final double A_MIN = 90; public static final double B_MIN = 80; public static final double C_MIN = 65; public static final double D_MIN = 50; private int numberOfQuestions; private int numberOfCorrectAnswers; public double getGradePercentage() { return (this.numberOfCorrectAnswers / this.numberOfQuestions) * 100; } }
Great! Now, our grader needs to get the grade percentage and compare it against the letter grade ranges. However, we have a couple of problems. For one, the new method we just wrote doesn’t work, but we’ll leave that for testing.
On the other hand, it probably doesn’t make sense for letter grades to be understood at the test level. In reality, the grader is going to determine the actual ranges. Otherwise, the test could just grade itself, and that’s probably bad object-oriented design.
All a test needs to know is how many questions it has and how many of those questions were answered correctly. So, let’s pull those constants out for now and write up some tests for our new CodingTest
class:
public class CodingTest { private int numberOfQuestions; private int numberOfCorrectAnswers; public double getGradePercentage() { return (this.numberOfCorrectAnswers / this.numberOfQuestions) * 100; } }
Uh oh! There’s another problem. We can’t write any tests until we create a constructor. Let’s go ahead and do that now:
public class CodingTest { private int numberOfQuestions; private int numberOfCorrectAnswers; public CodingTest(int numberOfQuestions, int numberOfCorrectAnswers) { this.numberOfQuestions = numberOfQuestions; this.numberOfCorrectAnswers = numberOfCorrectAnswers; } public double getGradePercentage() { return (this.numberOfCorrectAnswers / this.numberOfQuestions) * 100; } }
With a proper constructor, we can now create CodingTest
objects which are properly initialized.
Testing Our Solution
Now, we can quickly draft up a couple tests:
import junit.framework.TestCase; public class CodingTestTest extends TestCase { public void testGetGradePercentage() { CodingTest validTest = new CodingTest(10, 7); assertEquals(70.0, validTest.getGradePercentage(), .0001); CodingTest invalidTest = new CodingTest(-5, 5); // UH OH! } }
The testing syntax above is probably a little different than we’re used to, but it accomplishes exactly the same outcome. We just don’t have to tag every method with @Test
or import all of the assert functionality.
Strengthening Our Solution
About halfway through writing the first test, we should realize we have a mistake. What is supposed to happen if we create a CodingTest
with negative values on the input? Remember what happens when we poke outside the bounds of a String or an array? Java throws an exception. Let’s go ahead and do the same:
public class CodingTest { private int numberOfQuestions; private int numberOfCorrectAnswers; public CodingTest(int numberOfQuestions, int numberOfCorrectAnswers) throws IllegalArgumentException { if (numberOfQuestions <= 0 || numberOfCorrectAnswers < 0) { throw new IllegalArgumentException("You must supply valid input when creating a CodingTest"); } this.numberOfQuestions = numberOfQuestions; this.numberOfCorrectAnswers = numberOfCorrectAnswers; } public double getGradePercentage() { return (this.numberOfCorrectAnswers / this.numberOfQuestions) * 100; } }
Now, we can go ahead and test to make sure the exception gets thrown, but that’s probably a little beyond the scope of this tutorial. Instead, let’s remove the invalidTest
definition from our testing code and run it as-is:
import junit.framework.TestCase; public class CodingTestTest extends TestCase { public void testGetGradePercentage() { CodingTest validTest = new CodingTest(10, 7); assertEquals(70.0, validTest.getGradePercentage(), .0001); } }
Immediately, we should see the test fail. If we read the error, we’ll see that 0 does not equal 70.0 even within the delta.
If we do some digging, we’ll realize our error is the result of integer division. In this case, we’re computing 7 / 10
, and then multiplying the result by 100. That division yields a result of 0 before the multiplication.
Fortunately, we can cast one of the integers to a double before the division:
public class CodingTest { private int numberOfQuestions; private int numberOfCorrectAnswers; public CodingTest(int numberOfQuestions, int numberOfCorrectAnswers) throws IllegalArgumentException { if (numberOfQuestions <= 0 || numberOfCorrectAnswers < 0) { throw new IllegalArgumentException("You must supply valid input when creating a CodingTest"); } this.numberOfQuestions = numberOfQuestions; this.numberOfCorrectAnswers = numberOfCorrectAnswers; } public double getGradePercentage() { return (this.numberOfCorrectAnswers / (double) this.numberOfQuestions) * 100; } }
If we run the tests at this point, they should pass with flying colors.
Creating A Test Set
Now, we can create an array of tests. However, an array doesn’t keep any meta data about the test set. For instance, we might want the grader to be able to define letter grade ranges for various test sets, so let’s create a test array wrapper class.
A wrapper class is a class that encapsulates some data and provides some additional functionality. For an array of tests, the test set wrapper class might look like this:
public class CodingTestSet { private CodingTest tests[]; private double aMin; private double bMin; private double cMin; private double dMin; public CodingTestSet(double aMin, double bMin, double cMin, double dMin, CodingTest[] tests) { this.aMin = aMin; this.bMin = bMin; this.cMin = cMin; this.dMin = dMin; this.tests = tests; } public double testAverage() { double sum = 0; for (int i = 0; i < this.tests.length; i++) { sum += this.tests[i].getGradePercentage(); } return sum / this.tests.length; } public int getTestSetSize() { return tests.length; } public String getLetterGrade(int index) { double score = this.tests[index].getGradePercentage(); if (score >= aMin) { return "A"; } else if (score >= bMin) { return "B"; } else if (score >= cMin) { return "C"; } else if (score >= dMin) { return "D"; } else { return "F"; } }
By creating a test set, we can do fun things like get the test average or get the letter grade for each test. We also have a handy method for getting the number of tests we have without revealing any of the actual tests.
Of course, in production code we probably shouldn’t use equals with doubles. However, for our purposes, this should work fine. At this point, we should create some test code, but let’s move on and start making the grader.
Establishing a Grader
In our system, the grader will just be a glorified main method. We’ll drop in, make a test set, and run back the grades. If we want to create any helper methods, we should probably make them static. Other than that, let’s get started!
public class Grader { public static void main(String[] args) { CodingTest tests[] = { new CodingTest(5, 2), new CodingTest(5, 3), new CodingTest(5, 4), new CodingTest(5, 5) }; CodingTestSet morningExams = new CodingTestSet(90, 80, 65, 50, tests); for (int i = 0; i < morningExams.getTestSetSize(); i++) { String gradeMessage = String.format("Grade for test %d is %s", i, morningExams.getLetterGrade(i)); System.out.println(gradeMessage); } } }
And, there we have it! In just a handful of lines, a bunch of magic happens, and we get a list of letter grades printed out to the console.
As a challenge, we could expand some of the functionality that exists here. For instance, we could add another class called Question
which can be either incorrect or correct. Then, we could add a list of them to our CodingTest
class. That would make the tests quite a bit more configurable and fun.
On the front end, we could make the Grader
read in real tests. We would have to specify some format then generate some real test files. From there, we could parse the test files and convert them into CodingTests
. At that point, we’d have a complete grader system. Then, we could make tests for our friends or students and have the system do all the grading for us. That’s what automation is all about!
Wrapping Up a Series
With the completion of this tutorial, I’ve covered everything we need to know to start playing around with our own projects.
In the future, I’ll have advanced tutorials that cover a whole range of topics from data structures to compilers, operating systems to artificial intelligence, and design patterns to software craftsmanship. No topic is too small. Just let me know what you want to learn in the comments, and I’ll try to make it happen.
As always, thanks for studying with The Renegade Coder. I hope you’re learning a lot, and I hope you continue to stick around. If you’re looking to keep up to date with the latest articles, why not subscribe? If you’re feeling particularly bold after finishing this series, check out the Sample Programs repository. Maybe you can help out!
Recent Posts
Recently, I was thinking about the old Pavlov's dog story and how we hardly treat our students any different. While reflecting on this idea, I decided to write the whole thing up for others to read....
In the world of programming languages, expressions are an interesting concept that folks tend to implicitly understand but might not be able to define. As a result, I figured I'd take a crack at...