Prelude
First of all, thank you for your time and for your patience.
I know that when I was in school I never wanted to read a book and, except for The Cay and Great Expectations, I got away without having to.
So while it is hypocritical for me to ask this of you, please read this book like a book and go from the start to the end, one section at a time.
At the end of each section there will also be challenges. I want you to at least attempt all of them before moving on to the next section.
This book is written specifically for those folks that feel like giving up, like they are too stupid to get it, or like they didn't understand a damn thing their professor has said for the last three months.
I see you. You are not stupid. This is not your fault.1
probably.
Asking for Help
If anything in this confuses you or feels intimidating please, please reach out to someone.
If you are in a position to, make use of your teacher. That is what they are there for.
If you are not, on every page there is a button in the top right corner which you can click to ask a question.
When you do, keep in mind these basic rules.
1. You get what you pay for
Often the people willing to help you will be volunteers.
Do not expect them to do your assignments for you, do not expect them to be available at a moments notice, and accept that they are under no obligation to help anyone or you in particular.
2. Ask early
If you are struggling with something and you have a deadline in 5 hours, that might not be enough time to do anything.
Asking questions well before a deadline is best for all those involved.
3. Don't Ask to Ask
Don't ask to ask, nor only say hello, just ask your actual question right away.
Consult https://dontasktoask.com/ for what that means exactly.
Toy Problems
As you go through this book there will be a lot of what can be considered "toy problems."
That is, you will be shown or asked to write programs that have no obvious real world use. They will be similar in spirit to math problems like "if you have 8 apples and I take 3, how many apples do you have left?"
The intent is not for these to be annoying, although I am sure they will be. The intent is for these problems to help you build an understanding of mechanics and structure.
Lies
At various points in this book I am also going to lie to you. Specifically I will make lies of omission.
If there are five ways to do something, I might pretend that there is only one way to do it until I give you enough context so that I can talk about the other four.
This can be particularly annoying if you are in a course that is teaching things in a different order or if you feel rushed to just get some project done.
I get that. I just ask that you trust me and go in the sequence I am laying out. I am honestly trying to make it so that you end with a stronger understanding than you would have otherwise.
There is the question mark in the top right of every page.
AI
Do not use any form of AI when you are learning.
I could go into exactly why or shadowbox against why you might think its a good idea, but instead I am just going to ask that you trust me.
Don't do it.
If you find it hard to resist the temptation, install something like this on your machine to block yourself from accessing AI websites.
When you get stuck reach out to a person for help.
Java

The bulk of this book will be, as the title suggests, covering the Java programming language.
To clear up some common misconceptions:
- No, Java was not made by or for Minecraft. Minecraft came into existence in 2010. Java has been around since 1996.
- No, you do not have to pay Oracle if you use Java. You can download Java from other organizations with non-litigious track records like Adoptium if you are nervous.
- No, Java is not the same thing as JavaScript. JavaScript is a very different beast. The naming is very confusing yes. The history is wild.
The drawing at the top is Java's official1 mascot.2 The drawing below is JavaScript's.

The differences between these mascots are representative of the differences between the two languages.
There is another logo people use for Java - a coffee cup with steam coming off of it - that is a trademarked symbol of Oracle. If you use it there is a high likelyhood of Oracle juicero-ing your first-born.
What is that mascot? Well it is named "Duke," but beyond that your guess is as good as mine. I'll be using that ambiguity to its fullest.
Getting Started
There are a lot of ways to "get set up" to run Java code.
For at least the first chunk of this, you should be able to get away with using the editor on https://run.mccue.dev. That might be the easiest.
I will add tutorials here as they are requested or as I have time, but for the start all that matters is that you have the ability to run and edit the following code.
void main() {
IO.println("Hello, World");
}
Windows
Download the "JDK MSI" from adoptium.net.
Run the installer, selecting all the default options.
Mac OS
Download the "JDK .pkg" from adoptium.net.
Run the installer, selecting all the default options.
Linux
Linux is a little annoying. If you are using it you are likely used to it by now, but you can use adoptium.net like everyone else, but there is no universal installer there.
You can either download the .tar.gz
file that matches your machine, extract it,
and add the bin
folder to your PATH
, or you can try to find an installer for your
specific linux distribution.
First Steps

If you made it through the Getting Started section you've successfully run this program.
void main() {
IO.println("Hello, World!");
}
This "prints" - not in the sense of a physical printer, but like "displays on the screen" -
the text "Hello, World!"
.
Its a tradition for this to be your first program in any language.
We aren't quite at the point where I can explain what void main()
means, but
all you need to know for now is that it is what Java looks for to know where to start the program.
void main() {
< WRITE YOUR CODE HERE >
}
So for all intents and purposes, this is the whole program.
void main() {
IO.println("Hello, World!");
}
This bit of magic here - IO.println
- is a "statement" that "prints" the text inside the (
and )
as well as a "new line" to the screen.
The "IO
" part stands for "Input/Output" then it is print with new line.
If you were to replace it with IO.print
, then the output would lack that new line. This makes the following program be functionally identical to the first.
void main() {
IO.print("Hello, ");
IO.print("World");
IO.println("!");
}
Which, when we add back void main()
, looks like this.
void main() {
IO.print("Hello, ");
IO.print("World");
IO.println("!");
}
You should get in the habit of, whenever you see some bit of code, trying to physically type it out, run it, tweak it, and play with it.
So try playing around with this program. If you actively engage then you will retain information better.
Comments
At various points, I am going to leave "comments" in the code. Comments are parts of the code that are solely there for a human to be able to read as an explanation and can be written in regular words.
Single-line Comments
void main() {
// This prints hello world!
IO.println("Hello, World!");
}
The rules for this are that if you see a //
, everything after that in the same line
is ignored.
If you put //
in front of something that is "code" and not an English explanation we colloquially call that "commenting out" the line.
void main() {
IO.println("Hello, World!");
// The line that prints out goodbye is "commented out"
// IO.println("Goodbye!");
}
You might want to do that at various points where you want to see what happens if you "turn off" parts of the code.
Multi-line comments
If you put /*
in the code then everything up until the next */
will be treated as a comment. The distinction
here is that this style of comment can span multiple lines.
void main() {
/*
I have eaten
the plums
that were in
the icebox
and which
you were probably
saving
for breakfast
Forgive me
they were delicious
so sweet
and so cold
*/
IO.println("Hello, World!");
}
So that's a mechanism you will see me use and you can use yourself however you see fit.
Semicolons
The ;
at the end of each of those lines is a "semicolon".
void main(){
IO.print("Hello, "); // <-- this thing
// ^
}
It indicates that the statement has finished. A "statement" is a line of code that "does something." The reason we call it a statement and not just a "line of code" is that it can technically span multiple lines and be more complicated than these examples.
void main(){
IO.print(
"Hello, "
);
}
The ;
at the end is needed so that Java knows that the statement is over.
You need to put a ;
at the end of every statement. If you do not, Java will get confused and your code will not work.
If you happen to have an extra semi-colon or two that is technically okay. It just reads it as an "empty statement." It's pointless, but it is allowed.
void main() {
IO.print(
"Hello, "
);;
}
Or even
void main() {
IO.print(
"Hello, "
);
// Technically legal, but kinda sus
;;;;;;;;;;;;;
;;; ;;
;;; ;;
;;;;;;;;;;;;;
;; ;;; ;;
;;;;;;;;;;;;;
; ; ; ;
; ; ; ;
;;; ;;;
}
Formatting
You may have noticed that after each {
all the code that comes after it is "indented" in one "level."
void main() {
IO.println("Hello, World!");
}
I will kindly ask that you try to stick to this rule when writing your own code as well. If you try to find help online and you haven't, it will be hard for people to read your code.
This is easier to show than to explain in detail. Just try to make your code look like this.
✅
void main() {
IO.println("Hello, World!");
}
And not like this.
❌
void main()
{
IO.println("Hello, World!");}
And keep in mind that this rule of thumb applies to every language construct that requires a {
and }
many of which I will introduce later.
Challenges
At the end of each larger section, I am going to write down some things you can do to make sure you get what was just gone over.
The rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1
Write a program that prints your name twice. So if your name is "Jasmine", the output of the program should be this.
Jasmine
Jasmine
Challenge 2
What will this program output when run? Write down your guess and then try actually running it.
void main() {
IO.println("A");
//IO.println("B");
IO.println("C");//
IO.println("D");
/*
IO.println("E");
IO.println("F");*/
IO.println("G");
}
Challenge 3
There are four semicolons in this perfectly functional program. Delete one of them at random and see what the errors you get look like.
How could you use that error to figure out where you might have forgotten to put a semicolon?
void main() {
IO.println("Apple");
IO.println("Banana");
IO.println("Clementine");
IO.println("Durian");
}
Local Variables

Mechanically, the next thing to cover is "variables".
void main() {
String boss = "Jaqueline";
IO.println(boss);
}
A variable declaration has three components - the "type", the "name", and the "initial value".
String boss = "Jaqueline";
// type name initial value
In this case, we are declaring a variable called "boss" and assigning it the initial value
of "Jaqueline"
. Its "type" is "String", which I'll explain in more detail a bit ahead.
After you declare a variable and assign it a value, the "name" refers to the value on the right hand side and you can use that name instead of the value.
void main() {
// Does the same thing as IO.println("Jaqueline");
String boss = "Jaqueline";
IO.println(boss);
}
You may even use that name to give an initial value to another variable.
void main() {
// Does the same thing as IO.println("Jaqueline");
String boss = "Jaqueline";
// You can use variables on the right of the = too.
String person = boss;
IO.println(person);
}
Naming
It is a social convention1 that local variables be named likeThis
.
That is if their name is one word, that word should be in lowercase.
String apple = "Red Delicious";
If it is multiple words, the first word should be lowercase and the others should start with a capital letter.
This convention is called camelCase
because the capitals looks like the humps on a Camels back.
Just like proper formatting, sticking to this style will increase your chances of someone online being able to help you with your code.
Reassignment
After a variable is declared and assigned an initial value, that value can be later reassigned.
void main() {
String boss = "Jaqueline";
IO.println(boss);
boss = "Chelsea";
IO.println(boss);
}
Reassignments just involve the name and the new value. The type should not be redeclared.
boss = "Chelsea";
// name new value
After a variable is reassigned, the value associated with the name will reflect the new value from that point in the program onwards.
void main() {
String boss = "Jaqueline";
// This will output "Jaqueline"
IO.println(boss);
boss = "Chelsea";
// But this will output "Chelsea"
IO.println(boss);
}
Delayed Assignment
The declaration of a variable and the assignment of its initial value can be done separately.
void main() {
String contestWinner;
contestWinner = "Boaty McBoatface";
IO.println(contestWinner);
}
In which case the "variable declaration" will only have the "type" and "name" components.
String contestWinner;
// type name
And the "initial assignment" will look identical to a "re-assignment".
contestWinner = "Boaty McBoatface";
// name initial value
Before an initial value is assigned to a variable, it is not allowed to be used.1
void main() {
String contestWinner;
// This will not run, since Java knows that
// you never gave contestWinner an initial value.
IO.println(contestWinner);
}
There is no direct use for separating declaration and initial assignment at this point, but it's a surprise tool that will help us later.
Types
When a variable is declared, it is given a type.
String color = "green";
In this case, the variable color
is declared to have the type String
.
After this declaration, color
cannot be assigned to a value that is not a String
.
// A number is not a String!
String color = 8;
This applies to all situations where a variable might be given a value, including delayed assignment and reassignment.
One mental model is that types are like shapes. If the type of something is a circle, you can only put circles into it.
◯ thing = ◯;
You cannot put square pegs in that round hole.
// If Java actually functioned in terms of shapes, this
// would not work since a Square is not the same "type"
// of thing as a Circle.
◯ thing = ▢;
Final Variables
There is an optional extra part to a variable declaration where you can mark a variable as "final", meaning its value can never be reassigned.
void main() {
final String coolestChef = "Anthony Bourdain";
IO.println(coolestChef);
}
If you try to reassign a final variable, Java will not accept your program.
void main() {
final String coolestChef = "Anthony Bourdain";
IO.println(coolestChef);
// I'm sorry, but no. Cool guy, but no.
coolestChef = "Gordan Ramsey";
IO.println(coolestChef);
}
This is useful if you have a lot of lines of code and want the mental comfort of knowing you couldn't have reassigned that variable at any point between its declaration and its use.
final String genie = "Robin Williams";
// imagine
// 100s of lines
// of code
// and it is
// hard to
// read all of it
// at a glance
// ......
// ......
// You can still be sure "genie"
// has the value of "Robin Williams"
IO.println(genie);
Variables whose assignment is delayed can also be marked final.
void main() {
final String mario;
mario = "Charles Martinet";
IO.println(mario);
}
The restriction is the same - after the initial assignment, the variable cannot be reassigned.
void main() {
final String mario;
// An initial assignment is fine
mario = "Charles Martinet";
// But you cannot reassign it afterwards
mario = "Chris Pratt";
IO.println(mario);
}
The downside to this, of course, is more visual noise. If a variable is only
"alive" for a small part of the code, then adding final
might make it harder
to read the code, not easier.
Inferred Types
In many cases, Java is smart enough to know what the type of a variable should be
based on what it is initially assigned to.
In these cases, you can write var
in place of the type and let java "infer" what it should
be.
void main() {
// Since what is to the right hand side
// of the = is in quotes, Java knows that
// it is a String.
var theDude = "Lebowski";
IO.println(theDude);
}
You cannot use var
with variables whose assignment is delayed.
void main() {
// With just the line "var theDude;",
// Java doesn't know enough to infer the type
var theDude;
theDude = "Lebowski";
IO.println(theDude);
}
You can use var
with final
to make a variable whose type is inferred
and cannot be reassigned.
void main() {
final var theDude = "Lebowski";
IO.println(theDude);
}
Important to note that even if Java is smart enough to automatically know the type, you might not be yet. There is no shame in writing out the type explicitly.
void main() {
String theDude = "lebowski";
IO.println(theDude);
}
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1
What will this program output when run? Write down your guess and then try running it.
void main() {
String mascot = "The Noid";
IO.println(mascot);
mascot = "Pizza the Hut";
IO.println(mascot);
mascot = "Little Caesar";
IO.println(mascot);
}
Challenge 2
Why won't this code run? Make it run by only changing one line.
void main() {
String fruit;
fruit = "apple";
IO.println(fruit);
final String vegetable = "carrot";
IO.println(fruit);
IO.println(vegetable);
fruit = "orange";
vegetable = "celery";
IO.println(fruit);
IO.println(vegetable);
}
Challenge 3
What is the output of this code?
void main() {
String a = "A";
String b = "B";
b = a;
a = b;
b = a;
a = b;
IO.println(a);
IO.println(b);
}
Challenge 4
Only adding lines in the middle and without writing "A"
or "B"
again,
make it so that the output of the program is
B
A
void main() {
String a = "A";
String b = "B";
// Don't touch above this
// You can add code here
// Don't touch below this
IO.println(a);
IO.println(b);
}
To be clear: you are not allowed to write b = "A";
or a = "B";
Hint 1:
You can always make new variables.
Hint 2:
What you need to do is make a "temporary variable" to hold one of the values before you swap them.
Solution
String temp = a; a = b; b = temp;
Challenge 5
Some of the variables in this program are named "wrong."1 Fix them.
void main() {
String apple = "red";
String clown_car = "polka dot";
String SeriousCar = "black";
String FASTRunner = "bolt";
String slowRunner = "tortoise";
}
By currently prevalent social conventions. None are actually "wrong" from the perspective of Java.
Booleans

A boolean
is either true
or false
.
boolean onFleek = true;
boolean badVibes = false;
This is used to represent situations where there are exactly two possible states.
Not
Booleans can also be "negated" using the "not" operator - !
.
boolean haveOreosInHouse = true;
boolean stuckToCalorieLimit = !haveOreosInHouse;
So in this case, I have stuck to my calorie limit if there are not Oreos in the house.
haveOreosInHouse | stuckToCalorieLimit |
---|---|
false | true |
true | false |
And
One way multiple booleans can be combined is by using the "and" operator - &&
.
boolean funToBeAround = true;
boolean believesInFundamentalHumanRights = true;
boolean willAskOnDate = funToBeAround && believesInFundamentalHumanRights;
So in this case, I will ask someone on a date if they are fun to be around and they wholeheartedly believe in the assertions made in the Universal Declaration of Human Rights.
funToBeAround | believesInFundamentalHumanRights | willAskOnDate |
---|---|---|
true | true | true |
true | false | false |
false | true | false |
false | false | false |
Or
Another way booleans can be combined is by using the "or" operator - ||
.
boolean dogLooksNice = true;
boolean personLooksNice = false;
boolean willAskToPetDog = dogLooksNice || personLooksNice;
So in this case, I will ask to pet someone's dog if either the the dog looks nice or the person walking the dog looks nice.
dogLooksNice | personLooksNice | willAskToPetDog |
---|---|---|
true | true | true |
true | false | true |
false | true | true |
false | false | false |
Exclusive vs. Inclusive
It is important to note that this is not an "exclusive" OR.
An exclusive OR would be something like a child being allowed to have ice cream or a cookie, but not both.
The ||
operator is an "inclusive" OR, meaning the child is allowed ice cream, a cookie, or both ice cream and the cookie.
Operator Precedence
The operators that work on booleans have a "precedence order."
This defines an order of operations similar to mathematics, where multiplication and division happen before addition and subtraction.
For booleans !
always happens first. This is followed by &&
and then by ||
.
boolean a = true;
boolean b = false;
boolean c = false;
// just as 2 + 5 * 3 "evaluates" 5 * 3 before adding 2
// first, !b is true
// second, a && true is true
// third true || c is true.
boolean result = a && !b || c;
Also like mathematics, parentheses can be used to control this order.
// Even though || has a lower precedence than &&, we evaluate
// !b || c first because of the parentheses.
boolean result = a && (!b || c);
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1
What will this program output when run? Write down your guess and then try running it.
void main() {
boolean a = true;
boolean b = false;
boolean c = true;
boolean d = false;
boolean result = a || b && c || !d;
IO.println(result);
}
Challenge 2
What will this program output when run? Write down your guess and then try running it.
void main() {
boolean a = true;
boolean b = false;
boolean c = true;
boolean d = false;
boolean result = !(a || b && c || !d) || (a && b || c);
IO.println(result);
}
Challenge 3
Say you have two boolean variables, how could you use the operators we've covered to get the "exclusive or" of the two.
void main() {
// Change these two variables to test your solution
boolean hasIceCream = true;
boolean hasCookie = false;
boolean validChoice = < YOUR CODE HERE >;
IO.println(validChoice);
}
Make sure to test all the possibilities.
hasIceCream | hasCookie | validChoice |
---|---|---|
true | true | false |
true | false | true |
false | true | true |
false | false | false |
Integers

An integer is any number in the set { ..., -2, -1, 0, 1, 2, ... }.
int x = 1;
int y = 8;
int z = -4;
Integer Literals
In order to write an integer in a program, you write an "integer literal."
789
We call them this because the integer is literally written down in the program.
int trueCrime = 789;
Addition
You can add any two int
s using the +
operator.
void main() {
int x = 5;
// y will be 6
int y = x + 1;
// z will be 11
int z = x + y;
IO.println(x);
IO.println(y);
IO.println(z);
}
Adding a negative number does the same thing as subtraction.
void main() {
int x = 5;
// y will be 1
int y = x + -4;
IO.println(x);
IO.println(y);
}
Subtraction
You can subtract any two int
s using the -
operator.
void main() {
int x = 5;
// y will be 4
int y = x - 1;
// z will be 1
int z = x - y;
IO.println(x);
IO.println(y);
IO.println(z);
}
Subtracting a negative number does the same thing as addition.
void main() {
int x = 5;
// y will be 9
int y = x - -4;
IO.println(x);
IO.println(y);
}
Multiplication
You can multiply any two int
s using the *
operator.
void main() {
// x will be 15
int x = 3 * 5;
// y will be 75
int y = x * 5;
// z will be 1125
int z = x * y;
IO.println(x);
IO.println(y);
IO.println(z);
}
Division
You can divide any two int
s using the /
operator.
void main() {
int x = 8;
// y will be 4
int y = x / 2;
IO.println(x);
IO.println(y);
}
Division with integers gives results in only the quotient of the result, not the remainder.
So 5 / 2
does not result in 2.5
, but instead just 2
.
void main() {
// 5 / 2 is not 2.5, but instead 2.
int x = 5 / 2;
// 13 / 3 is not 4.3333, but instead 4.
int y = 13 / 3;
IO.println(x);
IO.println(y);
}
Remainder
To get the remainder of the division between two integers you can use the %
operator.
This is called the "modulo operator."
void main() {
int x = 5;
// The remainder of 5 / 2 is 1
// y will be 1
int y = x % 2;
// The remainder of 5 / 3 is 2
// z will be 2
int z = x % 3;
IO.println(x);
IO.println(y);
IO.println(z);
}
A common use for this is to make numbers "go in a circle."
For instance, say you wanted to count from 0 up to 3 and then go back to 0.
void main() {
int value = 0;
IO.println(value);
// the remainder of (0 + 1) divided by 3 is 1
// value will be 1
value = (value + 1) % 3;
IO.println(value);
// the remainder of (1 + 1) divided by 3 is 2
// value will be 2
value = (value + 1) % 3;
IO.println(value);
// the remainder of (2 + 1) divided by 3 is 0
// value will again be 0.
//
// We never reach 3 because 3 divided by 3
// always has a remainder of zero.
value = (value + 1) % 3;
IO.println(value);
// the remainder of (0 + 1) divided by 3 is 1
// value will be 1
value = (value + 1) % 3;
IO.println(value);
// and so on.
//
// If you did this process with 5 you would go
// 0, 1, 2, 3, 4, 0, 1, ...
//
// If you did this process with 7 you would go
// 0, 1, 2, 3, 4, 5, 6, 0, 1, ...
//
// You always go back to the start just before you reach
// the number you are getting the remainder by.
}
The fact that all the reassignments of value look identical is something that will be useful in tandem with loops.
Equality
Any two int
s can be inspected to see if their value is equal by using the ==
operator.
Unlike the previous operators, which all take int
s and produce int
s as their result, ==
takes two int
s
and produces a boolean
as its result.
void main() {
// 1 is never equal to 2
// this will be false
boolean universeBroken = 1 == 2;
IO.println(universeBroken);
int loneliestNumber = 1;
int canBeAsBadAsOne = 2;
// this will be true
boolean bothLonely = loneliestNumber == (canBeAsBadAsOne - 1);
IO.println(bothLonely);
}
It is very important to remember that a single =
does an assignment. Two equals signs ==
checks for equality.
The opposite check, whether things are not equal, can be done with !=
.
void main() {
// 1 is never equal to 2
// this will be true
boolean universeOkay = 1 != 2;
IO.println(universeOkay);
}
Comparison
In addition to comparing for equality with ==
and !=
, int
s can be compared to see if one is bigger than another using
>
, <
, >=
, and <=
.
>
will evaluate to true if the number on the left is greater than the one on the right.
boolean willBeTrue = 5 > 2;
boolean willBeFalse = 2 > 5;
<
will evaluate to true if the number on the right is greater than the one on the left.
boolean willBeFalse = 5 < 2;
boolean willBeTrue = 2 < 5;
If you went to public school like I did, you should be used to imagining that the >
was the jaw of a shark.
Whatever direction the Jaws are facing, thats the one that would need to be bigger for the statement to be true.1
// true if the shark is facing the bigger number
// false otherwise.
boolean result = 9 🦈 5;
>=
behaves the same as >
except >=
will evaluate to true
if the numbers are identical, >
will not.
boolean willBeTrue = 5 >= 5;
boolean willBeFalse = 5 > 5;
<=
has the same relationship to <
as >=
does to >
.
boolean willBeTrue = 5 <= 5;
boolean willBeFalse = 5 < 5;
Shark attacks are far more rare than people think they are. You are not a seal.
Chained Comparisons
When writing an expression in math to say something along the lines of
"x
is greater than zero and less than 5," it is natural to put that x
in the middle of both operators like so.
0 < x < 5
This does not work in Java. In order to "chain" comparisons like this, you have to combine
the results of comparisons using the &&
operator.
boolean xInRange = 0 < x && x < 5;
Operator Precedence
Just like boolean operators, +
, -
, *
, /
, and %
have a defined precedence order.
The order of operations is the same as mathematics. Multiplication and division happen before addition and subtraction, with the modulo operator being a sort of "funky division." Parentheses can be used to control this order.
None of this should be a surprise if you learned PEMDAS in school.
void main() {
// Following the order of operations:
// 2 * 3 + 3 * 9 / 2 - 2
// 2 * 3 happens first
// 6 + 3 * 9 / 2 - 2
// 3 * 9 happens next
// 6 + 27 / 2 - 2
// 27 / 2 happens next
// because of integer division, that gives 13
// 6 + 13 - 2
// 6 + 13 comes next
// 19 - 2
// and the final result is 17;
int result = 2 * 3 + 3 * 9 / 2 - 2;
IO.println(result);
}
The ==
, !=
, >
, <
, >=
, and <=
operators play a part here too1. They all have a lower precedence order than all the math operators, so you can
put them in the middle of any two math expressions.
void main() {
// The == check happens last.
boolean areThingsSame = 3 * (4 - 1 + 3) * 4 == 5 * 3 + 1 * 3 * 9;
IO.println(areThingsSame);
}
Every operator in the language has a defined order of operations with respect to all of the others. I am just showing them to you as they become relevant.
Reassignment
When the value of a variable is reassigned, the value stored in the variable before the reassignment can be used to compute the new value.
This is true for all data types, but it is easiest to demonstrate with numbers.
void main() {
int x = 1;
IO.println(x);
// x starts as 1, 1 + 1 is 2.
// 2 is the new value of x.
x = x + 1;
IO.println(x);
// x is now 2, 2 * 2 * 3 is 12
// 12 is the new value of x.
x = x * x * 3;
IO.println(x);
}
This property was used in the previous example for the %
operator, but I think it worth calling attention to even if it is "intuitive".
Shorthands for Reassignment
A very common thing to do is to take the current value of a variable, perform some simple operation like addition on it, and reassign the newly computed value back into the variable.
void main() {
int x = 2;
IO.println(x);
x = x * 5; // 10
IO.println(x);
}
As such, there is a dedicated way to do just that.
void main() {
int x = 1;
// This is the same as
// x = x + 2;
x += 2;
// This is the same as
// x = x * 4
x *= 4;
// This is the same as
// x = x - (x * 5)
x -= (x * 5);
// This is the same as
// x = x / 6
x /= 6;
// This is the same as
// x = x % 3
x %= 3;
// Pop quiz!
IO.println(x);
}
Of note is that adding or subtracting exactly 1 is common enough that it has its own special shorthand.
void main() {
int x = 0;
IO.println(x);
// Same as
// x = x + 1;
// x += 1;
x++;
IO.println(x);
// Same as
// x = x - 1;
// x -= 1;
x--;
IO.println(x);
}
Limits
Unlike in math, where numbers can be arbitrarily big or small, a Java int
is "fixed width."
Say you had a piece of paper that was only big enough to write two numbers on.
The only numbers you could write in a base ten system would be those from 0 to 99. You could not write 100 or anything larger.
A Java int
is similar except instead of only being able to write 0 to 99 on a piece of paper, a variable that has
the type int
can represent numbers from -231 to 231 - 1.
If you try to directly write out a number that is outside of that range, Java will not let you.
void main() {
// This will not run
int tooBig = 999999999999;
}
If you do math that should produce a larger number than is representable, the value will "loop around."
void main() {
// This is the value of 2^31 - 1
int atLimit = 2147483647;
// The value will "loop around" to -2^31
int beyondLimit = atLimit + 1;
// This will output -2147483648
IO.println(beyondLimit);
}
When a value loops around because it got too big we call that "overflow." When it loops around because it got too small we call that "underflow."
There are other types which can represent a larger range of integers, as well as types
which do not have any limits, but for now int
is the only one you will need.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1
What will this program output when run? Write down your guess and then try running it.
void main() {
int x = 5;
int y = 8;
IO.println(x + y);
}
Challenge 2
What will this program output when run? Write down your guess and then try running it.
void main() {
int x = 5;
x--;
x--;
x = x + x;
IO.println(x);
}
Challenge 3
Make it so that this program correctly determines if the numbers are even or not.
Assume that the values of x
, y
, and z
could be changed. Don't just write out
literally true
and false
for their current values.
void main() {
int x = 5;
int y = 4;
int z = 98;
boolean xIsEven = < CODE HERE >;
IO.println(xIsEven);
boolean yIsEven = < CODE HERE >;
IO.println(yIsEven);
boolean zIsEven = < CODE HERE >;
IO.println(zIsEven);
}
Challenge 4
Try dividing a number by zero. What happens?
Write down your guess and then try running the program below to see.
void main() {
IO.println(5 / 0);
}
Challenge 5
What can you write in the spot marked that will make the program output 2?
void main() {
int x = 5;
int y = <CODE HERE>;
IO.println(x + y);
}
Challenge 6
What is the output of this code.1
void main() {
IO.println(
6 / 2 * (1 + 2)
);
}
Floating Point Numbers

Floating point numbers are used to represent numbers
which cannot be stored as Integers like 2.5
or 3.14
.
double x = 1.5;
double y = 8.0;
double z = -3.14;
The type you will use to store a floating point number is double
.
double
stands for "double precision floating point."
Floating Point Literals
In order to write a floating point number in a program, you use a "floating-point literal."
1.5
Any number written with a decimal point is a floating point literal.
double pi = 3.14;
This includes numbers where a decimal point is written, but there is no fractional part to the number.
5.0
You cannot directly give a value to an integer variable using a floating point literal, even if there is no fractional part to the number.
// this will not work
int x = 5.0;
The reverse is possible though. You can give a value to a variable that stores a floating point number using an integer literal.
double x = 5;
Accuracy
Floating Point numbers are not an exact representation of numbers.
The reasons for this are twofold.
- It is much more efficient for a computer to work with data that is a "fixed size". You can't cram all the infinite possible numbers into 32, 64, or any fixed number of bits.
- For most systems, the inaccuracy is okay. When it is not, there are ways to do "real math" that we will cover much later.
For an explanation of the mechanics, I'll defer to this old Computerphile video.
Addition
You can add any two double
s using the +
operator.
void main() {
double x = 5.1;
// y will be 14.2
double y = x + 9.1;
IO.println(y);
}
Because of the previously mentioned inaccuracy, the results of additions might not always be what you expect.
void main() {
double x = 5.1;
// y will be 14.2
double y = x + 9.1;
// z will be 19.299999999999997
double z = x + y;
IO.println(z);
}
You can add any int
to a double
and the result of any such addition will also be a double
.
void main() {
int x = 5;
double y = 4.4;
// z will be 9.4
double z = x + y;
IO.println(x);
IO.println(y);
IO.println(z);
}
Even if the result of such an expression will not have any fractional parts, you cannot directly assign it to an int.
void main() {
int x = 5;
double y = 4;
// even though z would be 9, which can be stored in an int
// this will not work. The result of the expression is a double.
int z = x + y;
IO.println(x);
IO.println(y);
IO.println(z);
}
Subtraction
You can subtract any two double
s using the -
operator.
void main() {
double x = 5.1;
// y will be 4.1
double y = x - 9.2;
IO.println(x);
IO.println(y);
}
Because of the previously mentioned inaccuracy, the results of subtractions might not always be what you expect.
void main() {
double x = 5.1;
// y will be -4.1
double y = x - 9.2;
// z will be -4.199999999999999
double z = y - 0.1;
IO.println(z);
}
You can subtract any int
to or from a double
and the result of any such subtraction will also be a double
.
void main() {
int x = 5;
double y = 4.5;
// z will be 0.5
double z = x - y;
IO.println(x);
IO.println(y);
IO.println(z);
}
Even if the result of such an expression will not have any fractional parts, you cannot directly assign it to an int
.
void main() {
int x = 5;
double y = 4;
// even though z would be 1, which can be stored in an int
// this will not work. The result of the expression is a double.
int z = x - y;
}
Multiplication
You can multiply any two double
s using the *
operator.
void main() {
double x = 3;
// y will be 27
double y = x * 9;
// z will be 13.5
double z = y * 0.5;
IO.println(x);
IO.println(y);
IO.println(z);
}
Just like with addition and subtraction, it is fine to use both integers and integer literals when doing
multiplication on doubles. So long as any number being used is a double
the overall result will be a double.
void main() {
// a will be 3.0
double a = 1.5 * 2;
IO.println(a);
}
Division
You can divide any two double
s using the /
operator.
void main() {
double x = 8;
// y will be 4.0
double y = x / 2;
// z will be 1.3333333333333333
double z = y / 3;
IO.println(x);
IO.println(y);
IO.println(z);
}
Unlike with integer division, floating point division will include the remainder in the result.1
With the caveat that the result is now potentially inaccurate.
Equality
Just like int
s, double
s can be inspected to see if they are equal to one another using ==
.
void main() {
double numberOfToes = 10.0;
double numberOfFingers = 10.0;
boolean humanGenerated = numberOfToes == numberOfFingers;
IO.println(humanGenerated);
}
Because of floating point inaccuracy, this might not always give you the result you expect though.
void main() {
double x = 0.1;
double y = 0.2;
// z will be 0.30000000000000004
double z = x + y;
// this will be false.
boolean doesWhatYouExpect = z == 0.3;
IO.println(doesWhatYouExpect);
}
To account for that you can check if a number is "close enough" to another one by seeing if the "absolute value" of the difference is a small number.
void main() {
double x = 0.1;
double y = 0.2;
// z will be 0.30000000000000004
double z = x + y;
double compare = 0.3;
// this will be true.
boolean doesWhatYouExpect =
Math.abs(z - 0.3) < 0.00001;
IO.println(doesWhatYouExpect);
}
A double
can also be compared to an int
and, if they represent the same value, they will be reported as equal.
void main() {
int x = 5;
double y = 5.0;
// will be true
boolean fiveIsFive = x == y;
IO.println(fiveIsFive);
}
Comparison
In addition to comparing for equality with ==
and !=
, doubles
s can be compared to see if one is bigger than another using
>
, <
, >=
, and <=
.
This works the same as it does with int
s.
void main() {
double x = 1.5;
double y = 0.2;
// true
IO.println(x > y);
// false
IO.println(x < y);
}
Shorthands for Reassignment
All the same shorthands for reassignment work with double
s the same as they do with int
s.
void main() {
double x = 0.5;
// 0.5
IO.println(x);
x += 3;
// 3.5
IO.println(x);
x -= 1;
// 2.5
IO.println(x);
x++;
// 3.5
IO.println(x);
x--;
// 2.5
IO.println(x);
x *= 5;
// 12.5
IO.println(x);
x /= 2;
// 6.25
IO.println(x);
}
NaN
There is a special floating point number called NaN
, which stands for "Not a Number."
You generally only encounter NaN
as the result of doing something silly like dividing zero by zero.
double nan = 0.0 / 0.0;
NaN
is not equal to itself.
void main() {
double nan = 0.0 / 0.0;
// will be false
boolean equalToItself = nan == nan;
IO.println(equalToItself);
}
NaN
is not greater than itself.
void main() {
double nan = 0.0 / 0.0;
// will be false
boolean greaterThanItself = nan > nan;
IO.println(greaterThanItself);
}
NaN
is not less than itself.
void main() {
double nan = 0.0 / 0.0;
// will be false
boolean lessThanItself = nan < nan;
IO.println(lessThanItself);
}
NaN
is not greater than, less than, or equal to any number.
void main() {
double nan = 0.0 / 0.0;
// will all be false
IO.println(nan < 5);
IO.println(nan > 5);
IO.println(nan == 5);
}
None of this is usually useful, but it is fun to know about.
Positive and Negative Infinity
In addition to the wackyness of NaN
, floating point numbers can also represent both positive
and negative infinity.
You can get positive infinity by dividing any positive number by zero.
double positiveInfinity = 1.0 / 0.0;
You can get negative infinity by dividing any negative number by zero.
double negativeInfinity = -1.0 / 0.0;
As you might expect, positive infinity is greater than any number and negative infinity is less than any number.
void main() {
double positiveInfinity = 1.0 / 0.0;
double negativeInfinity = -1.0 / 0.0;
// true
IO.println(positiveInfinity > 99999999);
// true
IO.println(negativeInfinity < -99999999);
}
Except for NaN
, of course.
void main() {
double positiveInfinity = 1.0 / 0.0;
double negativeInfinity = -1.0 / 0.0;
double nan = 0.0 / 0.0;
// false
IO.println(positiveInfinity > nan);
// false
IO.println(negativeInfinity < nan);
}
Square Root
A relatively common operation to want to perform on floating point numbers is to find their square root.
You can do this with Math.sqrt
.
void main() {
double x = 4;
double y = Math.sqrt(x);
// This will output 2
IO.println(y);
}
You need to write Math.sqrt
and then inside of parentheses the expression whose value you want to take the square root of..
void main() {
double x = 5;
double y = 13;
double z = Math.sqrt(9 * x + y);
// This will output 7.615773105863909
IO.println(z);
}
If you try to take the square root of a negative number, the result will be NaN
.
void main() {
// will output NaN
IO.println(Math.sqrt(-5.2));
}
Conversion to Integers
Normally, a double
value cannot be assigned to an int
.
void main() {
double x = 5.0;
// will not work
int y = x;
}
The reason for this is that there are numbers like 2.5
, the infinities, and NaN
which do not have an
obvious way to be represented as an integer.
There are also numbers which a double
can represent like 4207483647.0
and -9999999999.0
which happen to be too big or too small to fit into the limits of an int
even though they
do have an obvious "integer representation."
As such, to make an int
out of a double
you need to accept that it is a "narrowing conversion."
The number you put in won't neccesarily be the number you get out.
To perform such a narrowing conversion, you need to put (int)
before a literal or expression that
evaluates to a double
.
void main() {
double x = 5.0;
// will be 5
int y = (int) x;
IO.println(y);
}
Any decimal part of the number will be dropped. So numbers like 2.1
, 2.5
, and 2.9
will all be converted into
simply 2
.
void main() {
int x = (int) 2.1;
int y = (int) 2.5;
int z = (int) 2.9;
IO.println(x);
IO.println(y);
IO.println(z);
}
Any number that is too big to store in an int
will be converted to the biggest possible int
, 231 - 1.
void main() {
// 2147483647
IO.println((int) 4207483647.0);
double positiveInfinity = 5.0 / 0.0;
// 2147483647
IO.println((int) positiveInfinity);
}
Any number that is too small to store in an int
will be converted to the smallest possible int
, -231.
void main() {
// -2147483648
IO.println((int) -9999999999.0);
double negativeInfinity = -5.0 / 0.0;
// -2147483648
IO.println((int) negativeInfinity);
}
And NaN
will be converted to zero.
void main() {
double nan = 0.0 / 0.0;
IO.println((int) nan);
}
When you use (int)
to convert, we call that a "cast1 expression". The (int)
is a "cast operator." It even has
a place in the operator precedence order just like +
, -
, ==
, etc.
The main difference is that instead of appearing between two expressions like the +
in 2 + 5
, it appears to the left of a single expression.
https://english.stackexchange.com/questions/220001/etymology-of-type-cast
Conversion from Integers
To convert from an int
to a double
, you don't need to do any special work. All int
s are
representable as double
s so it is a "widening conversion" and will be handled automatically
by Java when performing an assignment.
void main() {
int x = 5;
double y = x;
IO.println(x);
IO.println(y);
}
This is not true in an expression. Even if the result of a computation between int
s is being assigned to
a double
, the computation will still be performed using the same rules int
s usually follow.
void main() {
int x = 7;
int y = 2;
// integer division of 7 and 2 gives 3.
double z = x / y;
IO.println(z);
}
To perform math on an int
and have that int
behave as if it were a double
, you need to convert said int
into
a double
using a cast expression and the (double)
cast operator.
void main() {
int x = 7;
int y = 2;
// This converts x into a double before performing the division
// so the result will be 3.5.
double z = (double) x / y;
IO.println(z);
}
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1
What will this program output when run? Write down your guess and then try running it.
void main() {
double x = 5.1;
double y = 2.4;
IO.println(x + y);
}
Challenge 2
What will this program output when run? Write down your guess and then try running it.
void main() {
double x = 5.1;
double y = 2.1;
IO.println(x - y);
}
Challenge 3
What will this program output when run? Write down your guess and then try running it.
How can you make it give the "right" answer?
void main() {
double x = 5 / 2;
IO.println(x);
}
Challenge 4
These two expressions give different results. Why is that, and what results do they give?
void main() {
double resultOne = (int) 5.0 / 2 + 5.0 / 2;
double resultTwo = (int) (5.0 / 2 + 5.0 / 2);
IO.println(resultOne);
IO.println(resultTwo);
}
Challenge 5
The following is a quadratic equation.
\[ 2x^2 + 8x + 3 = 0 \]
To find the solutions of any quadratic equation you can use the following formula.
\[ x = \frac{-b \pm \sqrt{b^2 - 4ac} }{2a} \]
Where \(a\), \(b\), and \(c\) are the prefixes of each term in the following equation.
\[ ax^2 + bx + c = 0 \]
Write some code that finds both solutions to any quadratic equation defined by some variables
a
, b
, and c
. If the equation has imaginary solutions, you are allowed to just output NaN
.
void main() {
// For this one in particular, you should output
// -3.5811388300842 and -0.41886116991581
// but your code should work with these three numbers changed to
// represent any given quadratic equation.
double a = 2;
double b = 8;
double c = 3;
double resultOne = ???;
double resultTwo = ???;
IO.println(resultOne);
IO.println(resultTwo);
}
Characters

A character, represented by the data type char
, is a single
letter or symbol.
char letter = 'a';
I choose to pronounce it like the "char" in "Charmander."
Character Literals
In order to write a character in a program, you write that one character surrounded by single quotes.
'a'
This is called a "character literal." It has the same relationship to char
that an integer literal like 123 has to int
.
// Same as this "integer literal" is used to write a number
int sesameStreet = 123;
// A "character literal" is used to write text
char thisEpisodeIsBroughtToYouBy = 'c';
Common Escape Sequences
While most characters can be written directly into a program, as is the
case for a
, b
, or t
, there are some which cannot.
For these, you need to use what is called an "escape sequence."
The most common escape sequence you will use will be the one for a "new line."
Which is a backslash followed by an n
.
char newline = '\n';
Because a backslash is used for this special syntax, to put a backslash into a character literal you need to escape it with a backslash of its own.
char backslash = '\\';
And because single quotes are used to mark the start and end of a character literal, they need to be escaped as well.
char singleQuote = '\'';
Conversion to Integers
All char
s have a matching numeric value. 'a'
is 97
, 'b'
is 98
,
'&'
is 38
, and so on.
Same as assigning an int
to a double
, you can perform a widening conversion
by attempting to assign a char
to an int
.
void main() {
int valueOfA = 'a';
IO.println(valueOfA);
}
char
s will be automatically converted to int
s when used with mathmatical operators like +
, -
, >
, <
, etc.
void main() {
char gee = 'g';
// all the letters from a to z have consecutive numeric values.
boolean isLetter = gee >= 'a' && gee <= 'z';
IO.println(isLetter);
}
This can be useful if you are stranded on Mars1 or if you want to see if a character is in some range.
https://www.youtube.com/watch?v=0xkP_FQUsuM
Conversion from Integers
An int
can represent more values than a char
, so conversion from int
to
char
requires the use of the cast operator (char)
.
void main() {
int x = 120;
char xAsChar = (char) x;
IO.println(xAsChar);
}
This conversion is narrowing, so information might be lost if the int
value is too big or too small to fit into a char
.
The initial value of a char
can also be given by an integer literal if the integer literal represents a small enough letter.
void main() {
char z = 122;
IO.println(z);
}
Unicode
Most letters and symbols that are common in the English speaking world fit into
a single char
, so pretending that a char
is always "a single
letter or symbol" is generally a good enough mental model.
Where this falls apart is with things like emoji (👨🍳) which are generally considered to be one symbol, but
cannot be represented in a single char
.
char chef = '👨🍳';
char
s are actually "utf-16 code units". Many symbols require multiple "code units" to represent.
For a full explanation, refer to this old Computerphile video.
It describes "utf-8", which is 8 bits per "code unit." Java's char
uses 16 bits, but that is the only difference.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1
A lot of math problems ask you to find \( x^2 \). What is the value of the character x
squared?
Try to work it out on paper before running the program below.
void main() {
char x = 'x';
IO.println(x * x);
}
Challenge 2
Alter the program below so that it will output true
if the character declared at the top is a letter.
Make use of the fact that the numeric values for a
- z
and A
- Z
are contiguous.
void main() {
char c = 'a';
boolean isLetter = ???;
IO.println(isLetter);
}
Challenge 3
How many UTF-16 code units does it take to represent this emoji? 👨🍳
.
Strings

The String
data type is used to represent text.
String shortStory = "Everyone lived happily ever after, the end."
The word "string" comes from the fact that text is just individual characters "strung together".
As a concrete example j
, o
, and e
can be "strung together" into the "string"
joe
.
String Literals
In order to write text in a program, you surround it with double quotes.
"Hello, World"
This is called a "string literal." It has the same relationship to String
that an integer literal like 123
has to int
.
void main() {
// Same as this "integer literal" is used to write a number
int abc = 123;
// A "string literal" is used to write text
String name = "penelope";
}
Common Escape Sequences
Inside of a string literal, there are some characters that cannot be written normally.
An easy example is double quotes. You can't write double quotes in the middle of
a string literal because Java will think the extra quote is the end of the String
.
void main() {
String title = "The "Honorable" Judge Judy";
}
In order to make it work, the "
s need to be "escaped" with a backslash.
void main() {
String title = "The \"Honorable\" Judge Judy";
}
Since the backslash is used to escape characters, it too needs to escaped
in order to have it be in a String
. So to encode ¯\_(ツ)_/¯
into a String
you need to escape the first backslash.1
void main() {
// The first backslash needs to be escaped. ¯\_(ツ)_/¯
String shruggie = "¯\\_(ツ)_/¯";
}
And much the same as with char
, you need to use \n
to write in a newline.
void main() {
String letter = "To Whom It May Concern,\n\nI am writing this letter to complain.";
}
We call \
a "backslash" and /
a "forward slash." In ¯\_(ツ)_/¯
the left arm is drawn using the backslash and the right arm with a forward slash. What makes left "backwards" and right "forwards" is just social norms.
The Empty String
There is a special String
which contains no characters at all.
void main() {
// There is nothing to say.
String conversationWithDog = "";
}
You write it just like any other string, just with nothing between the double quotes.
""
It is different from a String
that just contains spaces because to Java those "space characters"
are just as much real characters as a
, b
, or c
.
void main() {
// There is noteworthy silence.
String conversationWithInlaws = " ";
}
This is one of those things that feels totally useless, but comes in handy pretty often.
- Say you are writing a message to send to your friend. The messenger
app can represent the state of the input box before you type anything with
an empty
String
. - If you want to digitally record responses to legal paperwork, you might choose
to represent skipped fields as empty
String
s. - Video Games where characters have assigned names can assign an empty
String
as the name of otherwise "unnamed" characters. - etc.
Multiline Strings
If the text you want to store in a String
has multiple lines, you can use
three quotation marks to represent it in code.
void main() {
String poem = """
I met a traveller from an antique land,
Who said—“Two vast and trunkless legs of stone
Stand in the desert. . . . Near them, on the sand,
Half sunk a shattered visage lies, whose frown,
And wrinkled lip, and sneer of cold command,
Tell that its sculptor well those passions read
Which yet survive, stamped on these lifeless things,
The hand that mocked them, and the heart that fed;
And on the pedestal, these words appear:
My name is Ozymandias, King of Kings;
Look on my Works, ye Mighty, and despair!
Nothing beside remains. Round the decay
Of that colossal Wreck, boundless and bare
The lone and level sands stretch far away.
""";
}
Inside of the this "Multiline String Literal" you don't need to escape quotation marks "
and you gain the ability to write newlines without having to use \n
.
Concatenation
Any two strings can be concatenated by using the +
operator.
void main() {
String he = "he";
String llo = "llo";
String hello = he + llo;
IO.println(hello);
}
This will make a new String
where the characters from the first one all appear followed by the characters in the second one.
If you try to concatenate a String
to something that is not a String
, like an int
or a double
,
then the result will be a new String
with the characters from the "string representation" of that
other thing.
void main() {
int numberOfApples = 5;
double dollars = 1.52;
String message = "I have " + numberOfApples +
" apples and $" + dollars + " in my pocket.";
IO.println(message);
}
Equality
You can check if two String
s have the same contents by using .equals
.
void main() {
String lyricOne = "Green, Green, Dress";
String lyricTwo = "Green, Green, Dress";
boolean areSameLyric = lyricOne.equals(lyricTwo);
boolean isMyName = lyricOne.equals("Bop Bop");
IO.println(areSameLyric);
IO.println(isMyName);
}
You write one String
on the left, .equals
, and then the String
you want to check it
against inside of parentheses.
To see if strings have different contents, you need to use the not operator (!
) on
the result of .equals
.
void main() {
String bow = "bow";
String wow = "WOW";
boolean areNotSame = !bow.equals(wow);
IO.println(areNotSame);
}
Note that you should not use ==
. Java will let you do it but you won't get what you expect.1
It is confusing in a way that we aren't ready to explain yet. Just remember for int
, double
, char
, etc. you can use ==
. For String
use .equals
.
Length
The number of char
s which comprise a String
can be accessed by using .length()
.1
void main() {
String fruit = "strawberry";
int numberOfChars = fruit.length();
// strawberry is 10 characters long
IO.println(
fruit + " is " + numberOfChars + " characters long"
);
}
This is different from the number of unicode codepoints.
Access Individual Characters
Given a String
, you can access the individual characters which
comprise it by using .charAt
.
The first character can be accessed by putting 0
in the parentheses.
The second by using 1
, and so on.
void main() {
String spy = "loid";
char l = spy.charAt(0);
IO.println(l);
char o = spy.charAt(1);
IO.println(o);
char i = spy.charAt(2);
IO.println(i);
char d = spy.charAt(3);
IO.println(d);
}
We call this number the "index" of the character.1
The index of the character to access can come from a variable.
void main() {
String assassin = "yor";
int indexOfR = 2;
char r = assassin.charAt(indexOfR);
IO.println(r);
}
If you give a number equal to or greater than the length of the String
or a number less than zero,
you will get an error.
void main() {
String student = "anya";
// Crash!
student.charAt(999);
}
void main() {
String dog = "bond";
// Crash!
dog.charAt(-1);
}
There will be more things which have their individual elements accessible by indexes. They will all generally start from 0 for the first element but there are rare exceptions.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1
What will this program output when run? Write down your guess and then try running it.
void main() {
String first = "1";
String second = "2";
String result = first + second;
IO.println(result);
}
Challenge 2
What will this program output when run? Write down your guess and then try running it.
void main() {
String first = "1";
int second = 2;
IO.println(first + second);
}
Challenge 3
What will this program output when run? Write down your guess and then try running it.
void main() {
char first = 'a';
String second = "b";
String third = "ab";
IO.println((first + second).equals(third));
}
Challenge 4
Make it so this program will output abc
by only changing one line and
not altering the println
statement.
Before your change, why did it output 294
?
void main() {
char a = 'a';
char b = 'b';
char c = 'c';
IO.println(a + b + c);
}
Challenge 5
Without adding any new println
s,
change one line in this program so that it outputs racecar
.
Try to find two ways to do that.
void main() {
String racecar = "racecar";
int diff = 1;
int index = 6;
IO.print(racecar.charAt(index));
index += diff;
IO.print(racecar.charAt(index));
index += diff;
IO.print(racecar.charAt(index));
index += diff;
IO.print(racecar.charAt(index));
index += diff;
IO.print(racecar.charAt(index));
index += diff;
IO.print(racecar.charAt(index));
index += diff;
IO.println(racecar.charAt(index));
}
Standard Input

Programs are pretty boring if they just make a machine warm and do some math.
There are a lot of ways to make a program interactive, but the easiest is to read from what is called "standard input."
Prompting
To prompt a user for information you use the IO.readln
function.
IO.readln
takes a String
to output as a prompt. This will
work the same as if the String
was passed to IO.print
.
The program will then wait until a human types some text and clicks the enter key.
Whatever they typed will be returned to the program as a String
.
void main() {
String name = IO.readln("What is your name? ");
IO.println("Hello, " + name);
}
readln
stands for "read line." It reads the next line a person types on "standard input".
Interpreting Input
When you call IO.readln
the human on the other side can type whatever they want.
This means that, depending on the question you asked, you might need to interpret
what they typed as something other than a String
.
In its most basic form this will look like seeing if one String
equals another String
.
void main() {
String color = IO.readln("What is your favorite color? ");
if (color.equals("green")) {
IO.println("Me too!");
}
else {
IO.println("neat.")
}
}
Integers
If you expect someone to type an integer value you can turn the String
you get from IO.readln
into an int
using Integer.parseInt
.
void main() {
String ageString = IO.readln("How old are you? ");
int age = Integer.parseInt(ageString);
IO.println("You are " + age + " years old!");
}
So long as they type something which can be interpreted as an int
(like 123
)
you will get a value for the int
variable. Otherwise
the program will crash.
Floating Point Numbers
If you expect someone to type a floating point value you can turn the String
you get from IO.readln
into a double
using Double.parseDouble
.
void main() {
String gpaString = IO.readln("What is your GPA? ");
double gpa = Double.parseDouble(gpaString);
IO.println("You're GPA is " + gpa);
}
So long as they type something which can be interpreted as a double
(like 123
or 14.5
)
you will get a value for the double
variable. Otherwise the program will crash.
Other Types
Just as you can turn a String
into an int
using Integer.parseInt
and you can
turn a String
into a double
using Double.parseDouble
, you can use Boolean.parseBoolean
to turn a String
into a boolean
.
void main() {
String happyString = IO.readln("Are you happy? ");
boolean happy = Boolean.parseBoolean(happyString);
IO.println("You are happy? " + happy);
}
But lest you become too comfortable, know that there is no Character.parseCharacter
to turn a String
into a character.
It is not always going to be the case that there is just one way to convert a String
to any given type.
void main() {
String gradeString = IO.readln("What is your letter grade? ");
// Does not exist!
char grade = Character.parseCharacter(gradeString);
IO.println("You have a " + grade + " in the class.");
}
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1
Write a program that asks a person for their name and then says "Hello <their name>" back to them.
void main() {
// 1. Call IO.readln to get their name
// 2. Call IO.print/IO.println to say hello to them
}
Challenge 2
Write a program that asks a person their age and tells them what age they will be this time next year.
void main() {
// 1. Call IO.readln to get their age
// 2. Interpret their age as an int
// 3. Add one to that age
// 4. Call IO.print/IO.println to say what age they will be next year
}
Challenge 3
Write a program that asks a person for two floating point numbers and tells them what the sum of those two numbers is
void main() {
// 1. Call IO.readln to get the first number
// 2. Interpret that first number as a double
// 3. Call IO.readln to get the second number
// 4. Interpret that second number as a double
// 5. Add the two numbers together
// 6. Call IO.print/IO.println to say what the sum is
}
Challenge 4
"Mad Libs" are a word game where you ask people for nouns, verbs, adjectives, etc. absent any context and then fill them in to a template.
For example
I saw a <noun> today and <past tense verb>.
Unfortunately the <noun> stopped me at the <noun>.
Can become
I saw a dog today and flew.
Unfortunately the clown stopped me at the elephant.
Make a program that asks a user for some nouns, verbs, etc. and prints a Mad Lib using those words.
Branching Paths

All the code I have shown you so far has run from top to bottom. That is, it has followed a single "path."
Not all programs can follow a single path though.
Imagine trying to rent a car online. The first thing you might be asked is your age. This is because most car rental companies do not want to rent to people under the age of 25.1.
If you enter an age that is less than 25, the program should immediately tell you that you cannot rent a car. If you enter an age that is greater than or equal to 25, the program should continue to prompt you for more information.
There are multiple "branching paths" that the program might take.
For insurance reasons.
If
The way to represent a branching path in Java is by using an if
statement.
void main() {
int age = 5; // 👶
if (age < 25) {
IO.println("You are too young to rent a car!");
}
}
You write the word if
followed by an expression which evaluates to a boolean
inside of (
and )
.
This expression is the "condition". Then you write some code inside
of {
and }
.
if (CONDITION) {
<CODE HERE>
}
When the condition evaluates to true
, the code inside of the {
and }
will run.
If it evaluates to false
that code will not run.
In this example the condition is age < 25
. When age
is less than 25 it will evaluate to true
and you will be told that you cannot rent a car.
void main() {
int age = 80; // 👵
if (age < 25) {
IO.println("You are too young to rent a car!");
}
}
If this condition evaluates to false
, then the code inside of {
and }
will not run.
Nested Ifs
The code inside of the {
and }
can be anything, including more if
statments.
void main() {
int age = 5; // 👶
if (age < 25) {
IO.println("You are too young to rent a car!");
if (age == 24) {
IO.println("(but it was close)");
}
}
}
When an if
is inside another if
we say that it is "nested".
If you find yourself nesting more than a few if
s that might be a sign that
you should reach out for help.
if (...) {
if (...) {
if (...) {
if (...) {
// Seek professional help
}
}
}
}
Else
When you want to do one thing when a condition evaluates to true
and another when that same condition evaluates to false
you can use else
.
void main() {
int age = 30; // 🙎♀️
if (age < 25) {
IO.println("You cannot rent a car!");
}
else {
IO.println("You might be able to rent a car.");
}
}
You write the word else
immediately after the }
at the end of an if
statement, then
some code inside of a new {
and }
.
if (CONDITION) {
<CODE TO RUN>
}
else {
<CODE TO RUN>
}
When the condition evaluates to false
, the code inside of else
's {
and }
will run.
else
cannot exist without a matching if
, so this code does not work.
void main() {
else {
IO.println("No if.");
}
}
Else If
If you have an if
nested in an else
branch, you can simplify that by using
else if
.
void main() {
boolean cool = true; // 🕶️
int age = 30; // 🙎♀️
if (age < 25) {
IO.println("You cannot rent a car!");
}
else {
if (!cool) {
IO.println("You failed the vibe check.");
}
else {
IO.println("You are rad enough to rent a car.");
}
}
}
So the following will work the same as the code above.
void main() {
boolean cool = true; // 🕶️
int age = 30; // 🙎♀️
if (age < 25) {
IO.println("You cannot rent a car!");
}
else if (!cool) {
IO.println("You failed the vibe check.");
}
else {
IO.println("You are rad enough to rent a car.");
}
}
You can have as many else if
s as you need. Each one will only run if all the previous conditions
evaluate to false
.
void main() {
boolean cool = true; // 🕶️
int age = 100; // 👴
if (age < 25) {
IO.println("You cannot rent a car!");
}
else if (!cool) {
IO.println("You failed the vibe check.");
}
else if (age > 99) {
IO.println("You are too old to safely drive a car!");
}
else if (age > 450) {
IO.println("There can only be one! ⚔️🏴");
}
else {
IO.println("You are rad enough to rent a car.");
}
}
Relation to Delayed Assignment
Delayed assignment of variables becomes useful with if
and else
.
So long as Java can figure out that a variable will always be given an initial value
inside of an if
and else
, you will be allowed to use that variable.
void main() {
int age = 22;
String message;
if (age > 25) {
message = "You might be able to rent a car";
}
else {
message = "You cannot rent a car!";
}
IO.println(message);
}
If it will not always be given an initial value, then you will not be allowed to use that variable.
void main() {
int age = 22;
String message;
if (age > 25) {
message = "You might be able to rent a car";
}
// message is not always given an initial value
// so you cannot use it.
IO.println(message);
}
Scoped Variables
If you make a variable declaration inside of an if
or an else
block,
that declaration will be "scoped" to the block.
void main() {
int age = 5;
if (age == 5) {
int nextAge = age + 1;
IO.println(nextAge);
}
// If you uncomment this line, there will be an issue
// `nextAge` is not available to the scope outside of the `if`
// IO.println(nextAge);
}
This scoping applies even if all branches declare the same variable within their logic.
void main() {
int age = 22;
if (age > 25) {
String message = "You might be able to rent a car";
}
else {
String message = "You cannot rent a car!";
}
// This will not work, because although `message` is declared
// in all branches, it is not declared in the "outer scope"
IO.println(message);
}
This is why you will sometimes need to use delayed assignment.
Conditional Operator
When the only operation being performed inside of an if
and else
pair
is setting the initial value of a variable, you can use the "conditional operator"1
to perform that assignment instead.
void main() {
int age = 22;
String message = age < 25
? "You cannot rent a car!"
: "You might be able to rent a car";
IO.println(message);
}
You write a condition followed by a ?
, a value to use when that condition evaluates to true
, a :
,
and then a value to use when that condition evaluates to false
.
CONDITION ? WHEN_TRUE : WHEN_FALSE
Some people will call this a ternary expression. Ternary meaning "three things." Same idea as tres leches.
Boolean Expressions
A common thing I've seen students do is set the initial value of some
boolean
variable based on some condition.
void main() {
int age = 22;
boolean canRent;
if (age > 25) {
canRent = true;
}
else {
canRent = false;
}
// or
// boolean canRent = age > 25 ? true : false;
IO.println(canRent);
}
This is valid code, but very often can be made simpler if you remember that the condition
itself already evaluates to a boolean
. You can directly assign the variable to that value.
void main() {
int age = 22;
boolean canRent = age > 25;
IO.println(canRent);
}
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1
Write code that will outputs The number is even
if x
is an even number.
void main() {
// Change this variable to different numbers
// to test your code
int x = 5;
// < YOUR CODE HERE >
}
Challenge 2
Make it so that your code from the previous problem will also output The number is odd
if the number is odd.
Challenge 3
Write code that will output allowed
if the the password
variable is equal to
"abc123"
and not allowed
if it isn't.
void main() {
// Change this variable to different strings
// to test your code
String password = "apple";
// < YOUR CODE HERE >
}
Challenge 4
Write code that will assign the string The number {x} is even
to message
if x
is an even number
and The number {x} is odd
if x
is an odd number.
So if x
is 12 the string you should assign The number 12 is even
to message
.
void main() {
String message;
// Change this variable to different numbers
// to test your code
int x = 5;
// < YOUR CODE HERE >
IO.println(message);
}
Loops

if
and else
let you write programs which can take branching paths,
but they will still run from the beginning to the end.
Not all programs can just end though. Video games should draw their world at one second and do it again the next. If you enter the wrong password on your phone, it should ask you for your password again.
This is what "loops" are for. You run code starting from some point and then loop back to that same point and run that code again.
While
One way to make a loop in code is to use while
.
void main() {
int x = 5;
while (x != 0) {
IO.println(x);
x--;
}
}
You write while
followed by a condition inside of (
and )
and some code inside of {
and }
.
while (CONDITION) {
<CODE HERE>
}
If the condition evaluates to true
then the code inside of {
and }
will run.
After that code runs, the condition will be evaluated again. If it still evaluates to
true
then the code {
and }
will run again.
This will continue until the code in the condition evaluates to false
.
void main() {
int glassesOfMilk = 99;
while (glassesOfMilk > 0) {
IO.println(
glassesOfMilk + " glasses of milk left"
);
glassesOfMilk--;
}
}
If a loop is made with while
we call it a "while loop."1
"We called him Tortoise because he taught us." - Lewis Carroll
Endless Loops
If a while loop will never end, we call that an endless loop.
This can happen if the condition is a constant like while (true)
void main() {
while (true) {
IO.println("This is the song that never ends");
}
}
Or if the variables tested in the condition are not updated inside of the loop.
void main() {
// x is never changed
int x = 0;
while (x != 1) {
IO.println("It goes on and on my friends");
}
}
Many games should never really "finish" so at the very start of that sort of program it is not uncommon
to see a while (true)
.
Break
While loops will usually stop running when the condition at the top evaluates
to false
.
This can be bypassed by using the break
statement.
void main() {
int x = 5;
while (x > 0) {
if (x == 2) {
break;
}
x--;
}
IO.println(
"Final value of x is " + x
);
}
If a break
is reached, the code in the loop stops running immediately.
The condition of the loop is not checked again.
This can be useful in a variety of situations, but notably it is the only way to exit from an otherwise endless loop.
void main() {
while (true) {
IO.println(
"The people started singing it not knowing what it was"
);
// Will immediately leave the loop
break;
}
}
Continue
Unless there is a break
, while loops will usually run all the code in their body from top to bottom.
The only other situation this will not happen is if a continue
statement is reached.
void main() {
// Will output a message for every number except 4
int x = 5;
while (x > 0) {
if (x == 4) {
x--; // Make sure the loop continues to 3
continue;
}
IO.println(x + " is a good number");
x--;
}
}
If a continue
is reached the code in the loop stops running immediately but, unlike break
,
the condition of the loop is checked again. If it still evaluates to true
then the code
in the loop will run again.
Unreachable Code
If you write some code directly after a break
or continue
that code will be "unreachable."
Java knows this and so won't let any code like that run.
void main() {
// This will not work
while (true) {
continue;
IO.println("this is unreachable");
}
}
Do
One variation on a while
loop is a "do-while loop."
void main() {
int x = 0;
do {
IO.println(x);
x++;
} while(x < 5);
}
You write do
, some code inside of {
and }
, and then while
, a condition inside of
(
and )
, and finally a semicolon.
do {
<CODE HERE>
} while (CONDITION);
In most situations it works exactly the same as a regular while loop. The only difference is that the first time the loop is reached the condition for the loop is not checked.
void main() {
int x = 0;
do {
IO.println("this will run");
} while (x != 0);
while (x != 0) {
IO.println("this will not run");
}
}
One way to remember the difference is that in a "do-while loop" you always "do the thing" at least once.
Nested Loops
Just like with if
, The code inside of the {
and }
can be anything, including more loops.
void main() {
int x = 5;
int y = 3;
while (x != 0) {
while (y != 0) {
IO.println(
"x is " + x
);
IO.println(
"y is " + y
);
x--;
y--;
}
}
}
If you are inside such a "nested loop", continue
and break
apply to the
"closest" loop.
That is, if a continue
or a break
were to appear here
void main() {
while (x != 0) {
while (y != 0) {
if (y == 2) {
break;
}
IO.println(
"x is " + x
);
IO.println(
"y is " + y
);
x--;
y--;
}
}
}
Then the y != 0
loop will be broken out of, not the x != 0
one.
And if a continue
or a break
were to appear here
void main() {
while (x != 0) {
if (x == 2) {
break;
}
while (y != 0) {
IO.println(
"x is " + x
);
IO.println(
"y is " + y
);
x--;
y--;
}
}
}
Then the x != 0
loop would be the "target."
Labeled Break
If you want to break out of a nested loop from one of the inner loops, you can use a "labeled break."
void main() {
outerLoop:
while (true) {
while (true) {
break outerLoop;
}
}
}
To do this, before your outer while or do-while loop you need to add a "label" followed by a :
.
A label is an arbitrary name just like a variable's name.
<LABEL>:
while (<CONDITION>) {
<CODE HERE>
}
<LABEL>:
do {
<CODE HERE>
} while (<CONDITION>);
Then inside of an inner loop, you just need to write break
followed by the label name.
void main() {
int x = 5;
int y = 3;
xLoop:
while (x != 0) {
while (y != 0) {
if (x == 2 && y == 2) {
break xLoop;
}
IO.println(
"x is " + x
);
IO.println(
"y is " + y
);
x--;
y--;
}
}
IO.println("done.");
}
In this case the x != 0
loop will be broken out of, not the y != 0
one.
Labeled Continue
In the same way a labeled break can break out of a nested loop, a labeled continue can jump back to the start of a nested loop.
You just write continue
followed by the label name.
void main() {
// Will keep going back to the top of the outer loop
outerLoop:
while (true) {
IO.println("inside outer loop");
while (true) {
IO.println("inside inner loop");
continue outerLoop;
}
}
}
Iteration
Loops potentially run code multiple times. Each time one goes from its top to its bottom we call that an "iteration" of the loop.
void main() {
int x = 0;
while (x < 5) {
// On the 1st iteration x will be 0
// On the 2nd iteration x will be 1
// ...
// On the final iteration x will be 4
IO.println(x);
x++
}
}
When the purpose of a loop is to run for every thing in some sequence of things, we say that the loop is "iterating over" those things.
Counting Up
Say your loop is intended to run some code for every number from 1
to 100
.
The general pattern for code like this is to have some variable which tracks the current number, a loop whose condition is that the number is less than the number you want to stop at, and a line at the bottom of the loop which increments the current number.
void main() {
int currentNumber = 1;
while (currentNumber <= 100) {
IO.println(currentNumber);
currentNumber++;
}
}
Take note that in this example the condition is currentNumber <= 100
, so the code in the
loop will run when currentNumber
is equal to 100
. If the condition was currentNumber < 100
it would stop at 99
.
void main() {
int currentNumber = 1;
// Stops at 99
while (currentNumber < 100) {
IO.println(currentNumber);
currentNumber++;
}
}
Counting Down
If you want to do the opposite, starting from a number like 100
and count down to 1
,
the pattern will be similar.
You still have some variable tracking the current number, but with a loop whose condition is that the number is greater than the number you want to stop at, and a line at the bottom of the loop which decrements the current number.
void main() {
int currentNumber = 100;
while (currentNumber >= 1) {
IO.println(currentNumber);
currentNumber--;
}
}
Similar to when counting up if the condition was not currentNumber >= 1
and instead was
currentNumber > 1
, the loop would stop at 2
void main() {
int currentNumber = 100;
// Stops at 2
while (currentNumber > 1) {
IO.println(currentNumber);
currentNumber--;
}
}
Iterate over a String
This general pattern of counting up and counting down becomes
especially useful when you want to iterate over each character in
a String
.
void main() {
String name = "Avril";
int index = 0;
while (index < name.length()) {
IO.println(name.charAt(index));
index++;
}
}
Challenges
Early on, most students tend to have a lot of trouble with loops. Its also what is quizzed on in a lot of standardized tests.
Because of that there will be a lot of challenges in this section for you to practice. Try to at least do the first ten or so to make sure you have the concept down, but the more the better.
It might take awhile before you feel truly comfortable with this. That is normal.
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1
Write code that outputs every number from 1
to 10
.
void main() {
<CODE HERE>
}
Challenge 2
What will this program output when run? Write down your guess and then try running it.
void main() {
int x = 0;
while (x < 10) {
IO.println(x);
x++;
}
}
Challenge 3
What will this program output when run? Write down your guess and then try running it.
void main() {
int x = 0;
while (x <= 10) {
IO.println(x);
x++;
}
}
Challenge 4
What will this program output when run? Write down your guess and then try running it.
void main() {
int x = 0;
while (x < 10) {
if (x % 3 == 0) {
break;
}
IO.println(x);
x++;
}
}
Challenge 5
What will this program output when run? Write down your guess and then try running it.
void main() {
int x = 0;
while (x < 10) {
if (x % 3 == 0) {
continue;
}
IO.println(x);
x++;
}
}
Challenge 6
What will this program output when run? Write down your guess and then try running it.
void main() {
int x = 1;
while (x < 10) {
int y = 2;
while (y < 5) {
IO.println(x * y);
y++;
}
x++;
}
}
Challenge 7
Write code that will output each character of name
on its own line.
So if name
is equal to "Bridget"
, I would expect the following as output.
B
r
i
d
g
e
t
void main() {
<CODE HERE>
}
Challenge 8
Write code that will output each character of name
on its own line, starting with the last
character and going backwards.
So for if name
is equal to "Samantha"
, I would expect the following as output.
a
h
t
n
a
m
a
S
void main() {
// Change this value to test your code.
String name = "Samantha";
// <CODE HERE>
}
Challenge 9
Write code that will take a number and if it is divisible by two, divides it by two. If it is not, multiplies it by three and adds one.
Keep doing this until the number equals one. Output it each time.
If the initial number is 6
you should have this as output.
6
3
10
5
16
8
4
2
1
If the initial number is 15
you should have this as output.
15
46
23
70
35
106
53
160
80
40
20
10
5
16
8
4
2
1
void main() {
// Change this value to test your code.
int n = 15;
// <CODE HERE>
}
Challenge 10
Write code that outputs every third number from 37
to 160
.
void main() {
<CODE HERE>
}
Challenge 11
Write code that outputs the number of vowels in name
. Treat y
as a vowel.
Treat the characters a
, A
, e
, E
, i
, I
, o
, O
, u
, U
, y
, and Y
as vowels.
void main() {
// Change this value to test your code.
String name = "Damian";
// <CODE HERE>
}
Challenge 12
Write code that outputs {name} is mostly vowels
if the number of vowels in name
is greater
than the number of consonants. and {name} is mostly consonants
if the opposite is true.
Output {name} has an equal number of vowels and consonants
if the count of both is the
same.
Make sure to not treat non-alphabetic characters like !
and ?
as consonants.
void main() {
// Change this value to test your code.
String name = "Messi";
// <CODE HERE>
}
Challenge 13
Rewrite the following code to not have the shouldBreak
variable
and instead to use a labeled break.
void main() {
// Don't think too hard about what these numbers mean.
int x = 3;
int y = 0;
boolean shouldBreak = false;
while (!shouldBreak && x < 100) {
while (y < 100) {
IO.println("x is " + x);
IO.println("y is " + y);
x = x * y;
if (x == 0) {
shouldBreak = true;
break;
}
y++;
}
}
IO.println("Done");
}
Prelude
Separate from the challenges at the end of each section I will be putting some projects sections throughout the book.
While the purpose of the challenges is to make you practice what you just read, the purpose of the projects is for you to put that knowledge into action.
This is for two reasons
- If you don't practice for real, it is very difficult to learn things.
- I want to drive home that software is an interdisciplinary field.
The first reason means that, while be some early hand-holding, I largely expect you to put together projects on your own using what you have been shown. This means you will struggle. The hope is that in that struggle you are forced to learn, but you can always ask for help.
By that second point I mean we make software to do things in the real world. To do this requires understanding who would use the software, why, and for what purpose. I am going to try my best to have projects that make you interact with the world outside of software construction.
Calorie Tracker
Problem Statement
A calorie is the amount of energy needed to raise the temperature of 1 gram of water by 1 degree celcius. A kilo-calorie, often shortened to kcal, is one thousand calories.
Food consumed by humans provides a certain amount of calories. Somewhat confusingly when people talk about "calories," as might be reported on food labels, they really mean kilo-calories.
For most of the history of the human species food was not an abundant resource. As such when food is abundant our brains are predisposed to consuming as much of it as possible. This is a behavior likely evolved in order that one best withstand periods of famine.1
The problem is that, while many places in the world still experience regular food shortages, many people have an extreme abundance of food available to them at all times. Not only that, the food they do have access to is often designed to be as addictive to consume as possible.
As such many of these people gain extreme amounts of weight. This, in turn, lead to many health problems.
Whether someone gains or loses weight over a given period of time comes down to "CiCo" - Calories In, Calories Out. If someone eats more in calories than they burn they are at a calorie surplus and will gain weight. If someone eats fewer calories than they burn they are at a calorie deficit and will lose weight.
There are also people out there in the world with the opposite problem. Not eating enough leads to starvation. Whether because of trauma, upbringing, or some other circumstance: some people will not naturally eat enough even when food is available to them.
Your Goal
Your goal is to make a program that helps someone track the number of calories they have consumed in a given day.
The intent is to help them be intentional about the number of calories they are consuming.
We will count it as a success if the program you produce at least helps them track the total.
Hint: You will need to use IO.readln
alongside Integer.parseInt
and/or Double.parseDouble
.
Future Goals
When you learn enough to do the following, come back to this project and expand it.
- Make it so that they can also track a "calorie goal" and see how they are doing with respect to that goal.
- Make it so that they can also track the name of the food.
- Make it so that if the computer running the program is turned off they do not lose information.
- Expand the program to also help them record the macro-nutritional value of the food they ate.
- Make it so that they can more easily track foods they eat often.
- Expand the program to also let them track their weight over time.
- Make it so that they can track their progress over multiple days, months, or years.
- Anything else you can think of.
I am not an evolutionary biologist, but this is my understanding.
Arrays

Arrays are used to represent a fixed-size collection of things.
int[] oddNumbers = { 1, 3, 5, 7, 9 };
Fixed-size means that once an array is made, it will always hold the same number of things.
We call the things stored in an array its "elements."
You can make an array of any type of element by using the name of the type followed by
[]
.
char[] letters = { 'a', 'b', 'c' };
String[] words = { "its", "as", "easy", "as" }
int[] numbers = { 1, 2, 3 };
double[] doubles = { 97.0, 98.0, 99.0, 1.0, 2.0, 3.0 };
Array Initializers
To give an initial value to an array you can use an array initializer.
After the equals sign you write {
followed by a comma separated list of elements and a final }
.
int[] numbers = { 1, 2, 3 };
// |---------|
// this part is
// the initializer
The elements in an initializer do not have to be literals and can also be variables or expressions.
int two = 2;
// Will hold 1, 2, 3 just like the array above
int[] numbers = { 1, two, two + 1 }
We call them array initializers because you use them to give an initial value to an array.1
You may be noticing a pattern. Confusing sounding names are often kinda "obvious" with enough context.
Length
The number of elements which comprise an array can be accessed by using .length
.1
void main() {
String[] veggies = { "brussels", "cabbage", "carrots" };
int numberOfElements = veggies.length;
// veggies is 3 elements long
IO.println(
"veggies is " + numberOfElements + " characters long"
);
}
Unlike with a String
, you do not write ()
after .length
.
Access Individual Elements
Given an array, you can access any of its elements by index.
You write the name of a variable containing an array, [
, a number, and then a ]
.
The first element can be accessed by using 0
, the second by using 1
, and so on.
void main(){
String[] lyrics = { "you", "say", "goodbye" };
String you = lyrics[0];
IO.println(you);
String say = lyrics[1];
IO.println(say);
String goodbye = lyrics[2];
IO.println(goodbye);
}
The index of the element can also come from a variable.
void main(){
int index = 2;
String[] lyrics = { "I", "say", "hello" };
IO.println(lyrics[index]);
}
If you give a number equal to or greater than the length of the array or a number less than zero, you will get an error.
void main(){
String[] lyrics = { "I", "say", "hi" };
// Crash!
IO.println(lyrics[999]);
}
void main(){
String[] lyrics = { "you", "say", "low" };
// Crash!
IO.println(lyrics[-1]);
}
Set Individual Elements
You can also set any of the elements of an array to have a new value.
To do this, on the left hand side of an equals sign you write the name of a variable
followed by [
, an index, and ]
. Then on the right hand side of the equals you write
the new value.1
void main() {
String[] sentence = { "you", "are", "found", "guilty" };
IO.println(
sentence[0]
+ " "
+ sentence[1]
+ " "
+ sentence[2]
+ " "
+ sentence[3]
);
sentence[1] = "aren't";
IO.println(
sentence[0]
+ " "
+ sentence[1]
+ " "
+ sentence[2]
+ " "
+ sentence[3]
);
}
The index of the element to set can also come from a variable.
void main() {
int index = 2;
String[] response = { "and", "it", "isn't", "opposite", "day" };
IO.println(
response[0]
+ " "
+ response[1]
+ " "
+ response[2]
+ " "
+ response[3]
+ " "
+ response[4]
);
response[2] = "is";
IO.println(
response[0]
+ " "
+ response[1]
+ " "
+ response[2]
+ " "
+ response[3]
+ " "
+ response[4]
);
}
If you give a number equal to or greater than the length of the array or a number less than zero, you will get an error.
void main() {
String[] response = { "objection" };
// Crash
response[1] = "!";
}
void main() {
String[] response = { "sustained" };
// Crash
response[-1] = "not";
}
You cannot change the contents of a String
like you would an array. This is one of the biggest differences between a String
and a char[]
.
Aliasing
When you assign a variable containing an array to another variable, the array referenced by both variables will be the exact same.
This means that if the contents of the array are updated, that change will be reflected by both variables.
void main() {
char[] lettersOne = { 'B', 'a', 't', 'm', 'a', 'n' };
char[] lettersTwo = lettersOne;
// Batman
IO.print(lettersOne[0]);
IO.print(lettersOne[1]);
IO.print(lettersOne[2]);
IO.print(lettersOne[3]);
IO.print(lettersOne[4]);
IO.print(lettersOne[5]);
IO.println();
// Batman
IO.print(lettersTwo[0]);
IO.print(lettersTwo[1]);
IO.print(lettersTwo[2]);
IO.print(lettersTwo[3]);
IO.print(lettersTwo[4]);
IO.print(lettersTwo[5]);
IO.println();
lettersOne[0] = 'C';
// Catman
IO.print(lettersOne[0]);
IO.print(lettersOne[1]);
IO.print(lettersOne[2]);
IO.print(lettersOne[3]);
IO.print(lettersOne[4]);
IO.print(lettersOne[5]);
IO.println();
// Catman
IO.print(lettersTwo[0]);
IO.print(lettersTwo[1]);
IO.print(lettersTwo[2]);
IO.print(lettersTwo[3]);
IO.print(lettersTwo[4]);
IO.print(lettersTwo[5]);
IO.println();
lettersTwo[0] = 'R';
// Ratman
IO.print(lettersOne[0]);
IO.print(lettersOne[1]);
IO.print(lettersOne[2]);
IO.print(lettersOne[3]);
IO.print(lettersOne[4]);
IO.print(lettersOne[5]);
IO.println();
// Ratman
IO.print(lettersTwo[0]);
IO.print(lettersTwo[1]);
IO.print(lettersTwo[2]);
IO.print(lettersTwo[3]);
IO.print(lettersTwo[4]);
IO.print(lettersTwo[5]);
IO.println();
}
When two variables point to the same thing like this we say that both variables are "aliases" for eachother.
Reassignment
The length of an array cannot change, but a variable holding an array can be reassigned to a new array that has a different length.
When reassigning the value of an array variable you need to put new
followed by a space, the type
of element held by the array, and then []
all before the initializer.
So to reassign an int[]
you need to write something like new int[] { 1, 2, 3 }
.
void main() {
int[] numbers = { 1, 2 };
// 2
IO.println(numbers.length);
numbers = new int[] { numbers[0], numbers[1], 3 };
// 3
IO.println(numbers.length);
}
This reassignment will not affect any variables which are aliases for the variable's old value.
void main() {
char[] wordOne = { 'g', 'o' };
char[] wordTwo = wordOne;
// go
IO.print("wordOne Length: ");
IO.println(wordOne.length);
IO.print(wordOne[0]);
IO.print(wordOne[1]);
IO.println();
// go
IO.print("wordTwo Length: ");
IO.println(wordTwo.length);
IO.print(wordTwo[0]);
IO.print(wordTwo[1]);
IO.println();
IO.println("-------");
wordOne = new char[] { wordOne[0], wordOne[1], 's', 'h' };
// gosh
IO.print("wordOne Length: ");
IO.println(wordOne.length);
IO.print(wordOne[0]);
IO.print(wordOne[1]);
IO.print(wordOne[2]);
IO.print(wordOne[3]);
IO.println();
// go
IO.print("wordTwo Length: ");
IO.println(wordTwo.length);
IO.print(wordTwo[0]);
IO.print(wordTwo[1]);
IO.println();
IO.println("-------");
wordTwo[0] = 'n';
// gosh
IO.print("wordOne Length: ");
IO.println(wordOne.length);
IO.print(wordOne[0]);
IO.print(wordOne[1]);
IO.print(wordOne[2]);
IO.print(wordOne[3]);
IO.println();
// no
IO.print("wordTwo Length: ");
IO.println(wordTwo.length);
IO.print(wordTwo[0]);
IO.print(wordTwo[1]);
IO.println();
IO.println("-------");
wordOne[0] = 'p';
// posh
IO.print("wordOne Length: ");
IO.println(wordOne.length);
IO.print(wordOne[0]);
IO.print(wordOne[1]);
IO.print(wordOne[2]);
IO.print(wordOne[3]);
IO.println();
// no
IO.print("wordTwo Length: ");
IO.println(wordTwo.length);
IO.print(wordTwo[0]);
IO.print(wordTwo[1]);
IO.println();
}
Relation to Final Variables
Just like anything else, arrays can be stored in variables marked final
.
This means that the variable cannot be reassigned, but it does not mean that the array's contents cannot be changed directly or through an alias.
void main() {
final char[] catchphrase = { 'w', 'o', 'a', 'h', '!' };
// woah!
IO.print(catchphrase[0]);
IO.print(catchphrase[1]);
IO.print(catchphrase[2]);
IO.print(catchphrase[3]);
IO.print(catchphrase[4]);
IO.println();
// Cannot reassign
// catchphrase = new char[] { 'e', 'g', 'a', 'd', 's' }
// but can set elements directly
catchphrase[0] = 'e';
catchphrase[1] = 'g';
// or through an alias
char[] alias = catchphrase;
alias[2] = 'a';
alias[3] = 'd';
alias[4] = 's';
// egads
IO.print(catchphrase[0]);
IO.print(catchphrase[1]);
IO.print(catchphrase[2]);
IO.print(catchphrase[3]);
IO.print(catchphrase[4]);
IO.println();
}
Printing the Contents of an Array
If you try to use IO.println
to output a String[]
you won't see the contents of the array. Instead you will see
something like [Ljava.lang.String;@1c655221
.
void main() {
String[] shout = { "fus", "ro", "dah" };
// [Ljava.lang.String;@5a07e868
IO.println(shout);
}
A similar thing will happen with int[]
, boolean[]
, and double[]
.1
void main() {
int[] nums = { 11, 11, 11 };
// [I@5a07e868
IO.println(nums);
boolean[] bools = { true, false };
// [Z@5a07e868
IO.println(bools);
double[] doubles = { 1.1, 1.1, 1.1 };
// [D@5a07e868
IO.println(bools);
}
If you want to actually see the contents of an array, you should use a loop.2
void main() {
String[] factions = { "empire", "stormcloaks", "dragons" };
int index = 0;
while (index < factions.length) {
IO.println(factions[index]);
index++;
}
}
What [I@5a07e868
and co. mean isn't really important. Try not to get too distracted by it.
Later on, there will be easier ways to do this sort of inspection. This is just the one I can demonstrate now.
Empty Array
If you use an array initializer that has no elements between the {
and }
you can create an empty array.
char[] emptyCharArray = {};
An empty array is very similar to an empty String
. It has a length of 0, it has no elements,
and it is generally useful only as a placeholder value for when you have no data yet but will
be able to reassign the variable holding it when you get some.
void main() {
char[] emptyCharArray = {};
// 0
IO.println(emptyCharArray.length);
// Crash
IO.println(emptyCharArray[0]);
}
Difference between Initializer and Literal
The reason { 1, 2, 3 }
is called an "array initializer" and not
an "array literal" is somewhat subtle.
When you have a literal, like a String
literal, you can assign that to a variable and
then use that String
afterwards.
void main() {
String name = "Alana";
// l
IO.println(name.charAt(1));
}
But you can also perform those operations using the literal itself, without an intermediate variable.
void main() {
// l
IO.println("Alana".charAt(1));
}
Array initializers work in the case where you first assign them to a variable before using the array.
void main() {
char[] name = { 'A', 'm', 'a', 'n', 'd', 'a' };
// m
IO.println(name[1]);
}
But they do not work to perform operations on directly.
void main() {
// Will not run
IO.println({ 'A', 'm', 'a', 'n', 'd', 'a' }[1]);
}
Initialization with new
Before the initializer for an array, you are allowed to write
new
followed by a space, the type of thing in the array,
and an empty []
.
void main() {
char[] mainCharacter = { 'A', 'a', 'n', 'g' };
IO.println(mainCharacter[0]);
IO.println(mainCharacter[1]);
IO.println(mainCharacter[2]);
IO.println(mainCharacter[3]);
IO.println();
char[] sideCharacter = new char[] { 'A', 'a', 'n', 'g' };
IO.println(sideCharacter[0]);
IO.println(sideCharacter[1]);
IO.println(sideCharacter[2]);
IO.println(sideCharacter[3]);
IO.println();
}
This is required for performing delayed initialization of a variable holding an array.
void main() {
char[] element;
element = new char[] { 'f', 'i', 'r', 'e' };
IO.println(element[0]);
IO.println(element[1]);
IO.println(element[2]);
IO.println(element[3]);
IO.println();
// This would not work
// element = { 'f', 'i', 'r', 'e' };
}
One ability this gives you is to use an array in an expression. I.E.
the initializer coupled with the new char[]
is akin to an "array expression."
void main() {
IO.println(new char[]{ 'K', 'a', 't', 'a', 'r', 'a' }[1]);
}
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1
Edit the following program so that the output is zero.
void main() {
// Only change this line
String[] words = { "Sam", "I", "Am" };
IO.println(words.length);
}
Challenge 2
Using only IO.println
and array accesses,
print hello world
to the screen.
void main() {
char[] letters = {
' ',
'h',
'e',
'l',
'o',
'w',
'r',
'd'
};
// Your code here
}
Challenge 3
Without editing either the array declaration or the loop at the bottom,
make the output of this program 0
.
void main() {
final int[] numbers = { 1, 2, 3, 4 };
// -----------
// YOUR CODE HERE
// -----------
int total = 0;
int index = 0;
while (index < numbers.length) {
total += numbers[index];
index += 1;
}
IO.println(total);
}
Challenge 4
Make this program output bulbasaur
without changing anything
above or below the marked areas.
void main() {
char[] name = { 'b', 'u', 'l', 'b' };
// -----------
// YOUR CODE HERE
// -----------
char[] toPrint = name;
int index = 0;
while (index < toPrint.length) {
IO.print(toPrint[index]);
index++;
}
IO.println();
}
Loops II

while
loops are enough to make any looping logic that you might want, but
they aren't the only kind of loops you will see.
There are tasks which would require a while
loop, but are common enough that there are other kinds
of loops that are shortcuts to writing them.
For
The for
loop is a shortcut to writing a while
loop which
has distinct steps that
- Declare a variable
- Check that variable to know whether to stop iterating
- Update the variable at the end of each iteration
As with many things, this might be easiest to see by looking at an example.
void main() {
// Will run 10 times
for (int number = 0; number < 10; number++) {
IO.println(number);
}
}
That for
loop works about the same as this while
loop.
void main() {
int number = 0;
while (number < 10) {
IO.println(number);
number++;
}
}
For Syntax
A for
loop has three distinct parts.
-
An initializer.
-
An expression which evaluates to a
boolean
. -
A statement.
for (<INITIALIZER> ; <EXPRESSION> ; <STATEMENT>) {
<CODE HERE>
}
The initializer is a statement which declares and initializes a variable
like int number = 0
.
The expression is some check like number < 5
that is checked each iteration
to know if the loop should continue.
The statement is ran at the end of every iteration, generally updating the variable tracked by the initializer and expression.
These can be thought of as being the same as a while
loop written like so.
<INITIALIZER>;
while (<EXPRESSION>) {
<CODE HERE>
<STATEMENT>;
}
Counting Up and Down
One of the easiest things to do with a for
loop is count up to or down from
a given number.
void main() {
// Goes from 1 to 100
for (int currentNumber = 1; currentNumber <= 100; currentNumber++) {
IO.println(currentNumber);
}
// Goes from 100 to 1
for (int currentNumber = 100; currentNumber >= 1; currentNumber--) {
IO.println(currentNumber);
}
}
You use the initializer to set some variable to a starting number like int currentNumber = 1
,
have the expression check if the number is at the target end number like currentNumber <= 100
,
and use the statement to change the number by one like currentNumber++
.1
Very often, if you are given a test on for
loops it will focus on doing all sorts of counting up, down, and around. Be prepared.
Iterate over a String
As was shown with while
loops, being able to count up and down lets
you iterate over each character in a String
.
void main() {
String name = "Lavigne";
for (int index = 0; index < name.length(); index++) {
IO.println(name.charAt(index));
}
}
Iterate over an Array
In the same way you can use a for
loop to go through each character of a String
,
you can use it to go through each element in an array.
void main() {
int[] numbers = { 4, 1, 6, 9 };
for (int index = 0; index < numbers.length; index++) {
IO.println(numbers[index]);
}
}
The only difference from String
s is that instead of .length()
and .charAt(...)
, you use .length
and []
.
Comparison to while
If you were to compare the code needed to loop over an array using a for
loop
and the code needed with a while
loop, there might not seem like much of a difference.
void main() {
double[] numbers = { 4.4, 1.1, 4.1, 4.7 };
for (int index = 0; index < numbers.length; index++) {
IO.println(numbers[index]);
}
int index = 0;
while (index < numbers.length) {
IO.println(numbers[index]);
index++;
}
}
This is doubly true when we are looking at toy examples where the only thing done
with the element is IO.println
.
The biggest benefit to a for
is subtle. With a while
based loop, the initializer and boolean expression
can potentially be many lines from the statement which updates the variable.
void main() {
double[] numbers = { 4.4, 1.1, 4.1, 4.7 };
int index = 0;
while (index < numbers.length) {
/*
Can
potentially
have
arbitrary
code
you want to run
a bunch
of times
*/
index++;
}
}
Us humans, with our tiny monkey brains, can get very lost when things that are related to each other are separated by long distances.
In this dimension, for loops are superior. All the bits of code that "control the loop" can be right at the top.
void main() {
double[] numbers = { 4.4, 1.1, 4.1, 4.7 };
for (int index = 0; index < numbers.length; index++) {
/*
Can
potentially
have
arbitrary
code
you want to run
a bunch
of times
*/
}
}
i
One thing you will very often see if you read other peoples'
code is that the variable being tracked in a for loop is often called
i
.
void main() {
String word = "bird";
for (int i = 0; i < array.length; i++) {
char letter = word.charAt(i);
IO.println(letter);
}
// b
// i
// r
// d
}
While usually naming variables with single letters isn't a great idea,
most developers carve out an exception for cases like this. Writing index
gets
tedious.
Its also helpful to go i -> j -> k
when you end up nesting for
loops.1
void main() {
char[] letters = { 'A', 'B', 'C' };
int[] numbers = { 1, 2 };
for (int i = 0; i < letters.length; i++) {
for (int j = 0; j < numbers.length; j++) {
IO.print(letters[i]);
IO.println(numbers[j]);
}
}
// A1
// A2
// B1
// B2
// C1
// C2
}
Just do not start naming all your variables single letters.
j
and k
standing for jindex an kindex respectfully.
Break
break
works the same with for
loops as it does with while
loops.
Any time you hit a line with break
you will immediately exit the loop.
void main() {
for (int i = 0; i < 1000; i++) {
if (i == 5) {
break;
}
IO.println(i);
}
IO.println("Over");
// 0
// 1
// 2
// 3
// 4
// Over
}
Continue
continue
works slightly differently with for
loops than how it does with while
loops.
Any time you hit a line with continue
you will immediately jump back to the top of the loop, but
unlike with a while
loop, the statement which updates your variable will still run.
void main() {
for (int i = 0; i < 5; i++) {
if (i == 2) {
// i++ will still run
continue;
}
IO.println(i);
}
// 0
// 1
// 3
// 4
}
So the above for
loop is not equivalent to this while
loop.
void main() {
int i = 0;
while (i < 5) {
if (i == 2) {
continue;
}
IO.println(i);
i++;
}
// 0
// 1
// ... spins forever ...
}
It is equivalent to this one.
void main() {
int i = 0;
while (i < 5) {
if (i == 2) {
i++
continue;
}
IO.println(i);
i++;
}
// 0
// 1
// 3
// 4
}
Delayed Assignment
The initializer of a for
loop can give an initial value to a variable
declared outside of the loop.
void main() {
int number;
for (number = 0; number < 5; number++) {
IO.println("At: " + number);
}
}
You might choose to do this so that after the loop is finished, you can still access the variable.
void main() {
int number;
for (number = 0; number < 5; number++) {
IO.println("At: " + number);
}
// This will work, we can access the variable still.
IO.println("Ended at: " + number);
}
If you had put both the declaration and initial value inside the initializer, you won't be able to use that variable after the loop
void main() {
for (int number = 0; number < 5; number++) {
IO.println("At: " + number);
}
// This will not work. number is no longer available
IO.println("Ended at: " + number);
}
Inferred Types
The initializer of a for
loop works the same as any variable assignment, so
you still are allowed to use var
so that the type of the declared variable is inferred.
void main() {
for (var i = 0; i < 10; i++) {
IO.println(i);
}
}
var
is the same number of letters as int
so you aren't gaining much when your for
loop
is just counting over numbers.
But if your for
loop is doing something more exotic, it might make sense.
void main() {
for (var repeated = ""; repeated.length() < 5; repeated = repeated + "a") {
IO.println(repeated);
}
// a
// aa
// aaa
// aaaa
}
Empty Initializers
You are allowed to leave the initializer part of a for
loop blank
so long as you still have the ;
.
void main() {
int number = 0;
for (;number < 5; number++) {
IO.println(number);
}
}
You might choose to do this for the same reasons you might choose to split the declaration and assignment of the "loop variable." So that the variable will be accessible after the end of the loop.
This way its initialization and declaration can be on the same line, which might be desirable.
void main() {
int number = 0;
for (;number < 5; number++) {
IO.println(number);
}
IO.println("Still have number: " + number);
}
Empty Expressions
You are also allowed to leave the expression part of a for
loop blank.
void main() {
for (int i = 0;;i++) {
IO.println(i);
}
// 0
// 1
// 2
// 3
// ... and so on
}
This means that each time through there is no check to see if the loop will exit.
The loop will only exit if there is an explicit break
somewhere.
void main() {
for (int i = 0;;i++) {
if (i == 5) {
break;
}
IO.println(i);
}
// 0
// 1
// 2
// 3
// 4
}
Empty Statements
You can even leave the statement part of a for
loop blank. This means that at
the end of an iteration there is nothing guaranteed to run.
void main() {
for (int i = 6; i > 2;) {
IO.println(i);
i--;
}
// 6
// 5
// 4
// 3
}
If you leave both the initializer and statement blank, that will be functionally identical to a while
loop.1
void main() {
int number = 1;
for (;number < 10;) {
IO.println(number);
number *= 2;
}
// Same logic as above
int number2 = 1;
while (number2 < 10) {
IO.println(number2);
number2 *= 2;
}
}
If you leave the initializer, expression, and statement blank it will be the same as a while (true)
loop.
for (;;) {
IO.println("The people stated singing it...");
}
// Runs forever
The only difference is that (;;)
looks somewhat like
- A spider
- The Pokémon Kabuto
- A person crying
And that can be fun.
Final Variables
The initializer of a for
loop can also declare final
variables.
void main() {
int i = 0;
for (final String name = "Bob"; i < 5; i++) {
IO.println(name + ": " + i);
}
}
This doesn't have much use with loops that track int
s and String
s, but if you
are feeling clever you can use this ability along with arrays or other things you
can change without reassigning a variable.
void main() {
for (final char[] letters = { 'I', 'O', 'U' }; letters[0] != 'A';) {
for (int i = 0; i < letters.length; i++) {
letters[i] -= 1;
IO.print(letters[i]);
}
IO.println();
}
// HNT
// GMS
// FLR
// EKQ
// DJP
// CIO
// BHN
// AGM
}
There aren't many reasons to do this, but it is in fact not against the law and I cannot stop you.
Labeled Break
Labeled breaks work the same with for
loops as they do with while
loops.
outerLoop:
for (;;) {
for (;;) {
break outerLoop;
}
}
This applies also to when while
loops are nested within for
loops or the other way around.
void main() {
outerForLoop:
for (int i = 0; i < 10; i++) {
IO.println(i);
while (i < 100) {
if (i == 5) {
break outerForLoop;
}
i++;
}
IO.println(i);
}
// 0
}
Labeled Continue
Labeled continues also work the same in for
loops as while
loops, but with the hopefully expected caveat that
the statement of a for
loop will always run when you get to the top of it.1
void main() {
label:
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 3; j++) {
IO.println ("" + i + ", " + j);
if (i == 2) {
// i++ will run
continue label;
}
}
}
// 0, 0
// 0, 1
// 0, 2
// 1, 0
// 1, 1
// 1, 2
// 2, 0
// 3, 0
// 3, 1
// 3, 2
}
Drawing Right Triangles
One of the more fun things to do with for
loops1 is to use them to print out shapes.
Say you wanted to draw this right triangle.
*
**
***
There is one *
, then two *
s on the next line, and three *
s on the last.
If you were to write the code out to print this explicitly it would look like this.
void main() {
IO.print("*\n**\n**\n");
}
Where \n
is explicitly putting in the new lines.
Since counting up 1 -> 2 -> 3
is easy with for
loops, you can translate this
void main() {
for (int numberOfStars = 1; numberOfStars <= 3; numberOfStars++) {
for (int i = 0; i < numberOfStars; i++) {
IO.print("*");
}
// Same as IO.print("\n");
IO.println();
}
}
Which makes it easy to make one of these triangles however tall you want.
void main() {
int height = 6;
for (int numberOfStars = 1; numberOfStars <= height; numberOfStars++) {
for (int i = 0; i < numberOfStars; i++) {
IO.print("*");
}
IO.println();
}
}
*
**
***
****
*****
******
Drawing Isosceles Triangles
Another fun shape is the isosceles triangle.
*
***
*****
For this one, each row of the triangle needs to have spaces before it to shift it in to the center. How much each row needs to be shifted depends on how big the trangle will be overall.
In this case with three rows of *
s, the top *
needs two space characters before it
and the second row needs one space character.
void main() {
IO.println(" *\n ***\n*****");
}
So any loop we make needs to take this pattern into account.
void main() {
int totalRows = 5;
for (int row = 1; row <= totalRows; row++) {
for (int i = 0; i < totalRows - row; i++) {
IO.print(" ");
}
for (int i = 0; i < row * 2 - 1; i++) {
IO.print("*");
}
IO.println();
}
}
*
***
*****
*******
*********
Which can get tricky. For now, you can just study the code that does it for this shape. There will be more things to draw in the challenges section.1
The reason I'm focusing on this isn't because its likely you will get a job drawing shapes, but if you can draw a shape you can dodge a ball.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
You are extremely likely to be quizzed on for
loops on any standardized tests,
so these challenges are going to include a lot of repetition and sometimes a tricky
case to handle. Its for your own good, I hope.
Challenge 1
Write code that will output every number from 0 to 15, including 0 and 15.
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void main() {
}
Do it using a for
loop and again using a while
loop.
Challenge 2
Write code that will output every number from 15 to 0, including 0 and 15.
15
14
13
12
11
10
9
8
7
6
5
4
3
2
1
0
Do it using a for
loop and again using a while
loop.
void main() {
}
Challenge 3
Write code that will output every number from 15 to 0, excluding 0 and 15.
14
13
12
11
10
9
8
7
6
5
4
3
2
1
Do it using a for
loop and again using a while
loop.
void main() {
}
Challenge 4.
Write code that will output every third number from 51 to 66.
53
56
59
62
65
Do it using a for
loop and again using a while
loop.
void main() {
}
Challenge 5.
Draw a square.
Make it so that you can make the square bigger or smaller by changing a variable at the start of the program.
*****
*****
*****
*****
void main() {
}
Challenge 6.
Draw a rectangle.
Make it so that you can make the rectangle bigger or smaller in either dimension by changing a variable at the start of the program.
******
******
******
void main() {
}
Challenge 7.
Draw a circle!
Make it so that you can make the circle bigger or smaller by changing a variable at the start of the program.
**
****
******
********
******
****
**
void main() {
}
Challenge 8.
Draw a smiley face!
Make it so that you can make the smile bigger or smaller by changing a variable at the start of the program.
void main() {
}
Methods

All the code you have seen up until this point has lived inside of void main() {}
.
void main() {
IO.println("CODE GO HERE");
}
This isn't sustainable for a few reasons. One is that code can get big. Putting a thousand lines inside of one place can be a lot, let alone the hundreds of thousands you need to be Minecraft or the millions you need to be Google.
Another is that there is almost always code that you will want to run at multiple places in a program. Copy pasting that code can get old.
This is what methods are for. Methods let you split up your code into smaller, reusable chunks.1
The word method comes from a "method of getting things done." You might also hear methods referred to as "functions".
Declaration
The simplest kind of method is declared by writing void
followed by some name, ()
, and some code inside of a {
and }
.
void doThing() {
IO.println("Hello from inside a method!");
}
void main() {
doThing();
}
Invocation
Once you've declared a method you can run the code inside of it by writing the
name of the method followed by ()
in a statement.
void doThing() {
IO.println("Hello from inside a method!");
}
void main() {
doThing();
}
Running the code in a method can be called a few things. "Running a method", "Calling a method", or "Invoking a method."1
You can call a method multiple times. If you do, then the code inside of it will be run multiple times.
void doThing() {
IO.println("Hello from inside a method!");
}
void main() {
doThing();
doThing();
}
I like that last one because "invoking" makes me sound like a Wizard.
Scope
Methods can contain any code, including variable declarations.
void sayMathStuff() {
int x = 1;
int y = 2;
IO.println("x is " + x);
IO.println("y is " + y);
IO.println("x + y is " + (x + y));
}
void main() {
sayMathStuff();
}
When a method declares a variable inside of its body, that declaration is "scoped" to that method. Other code cannot see that variable.
void sayMathStuff() {
int x = 1;
int y = 2;
IO.println("x is " + x);
IO.println("y is " + y);
IO.println("x + y is " + (x + y));
}
void main() {
sayMathStuff();
// Error, x doesn't exist here
IO.println(x);
}
This is why we have called variables "local variables." They are local to the "scope" of the method they are declared in.
main
The main
method works the same as any other method. Java just treats it special by choosing to
call it in order to start your programs.
void main() {
IO.println("Java will start here");
}
This means you can do anything in your main
method you can do in any other method.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Declare a method named printSquare
. When invoked it should print a square.
*****
*****
*****
*****
// CODE HERE
void main() {
printSquare();
}
Challenge 2.
Declare a method named printSquareThreeTimes
. When invoked it should print three squares
by invoking a method named printSquare
three times.
// CODE HERE
// Code from challenge 1 here
void main() {
printSquareThreeTimes();
}
Challenge 3.
Write a program that contains at least four methods. Have fun with it.
void main() {
// sing a song or something.
}
Arguments

If methods always had to do the same thing each time they were run, they wouldn't be that useful.
The way to customize what happens when a method is called is to have them take "arguments."
void sayHello(String name) {
IO.println("Hello " + name + "!");
}
void main() {
// Hello Joshua!
sayHello("Joshua");
// Hello Claire!
sayHello("Claire");
}
Declaration
To declare a method which takes arguments, instead of putting ()
after the method name
you need to put a comma separated list of argument declarations.
Each argument declaration looks the same as a variable declaration and has both a type and a name.
// This declares a single argument named "food" that
// has a type of "String".
void eat(String food) {
IO.println("I ate " + food);
}
// This declares two arguments
// "to", which is a String and
// "age", which is an int.
void happyBirthday(String to, int age) {
IO.println(
"Happy " + age + "th birthday " + to + "!"
);
}
Invocation with Arguments
To invoke a method which takes arguments you need to, instead of writing
()
after the method name, write (
followed by a comma separated list
of literals or variable names ending with )
.
void eat(String food) {
IO.println("I ate " + food);
}
void happyBirthday(String to, int age) {
IO.println(
"Happy " + age + "th birthday " + to + "!"
);
}
void main() {
// This calls the 'eat' method with the String "cake"
// as an argument.
eat("Cake");
// You can also call methods using values stored in
// variables.
String veggie = "carrot";
eat(veggie);
// For more than one argument, you separate them with commas
happyBirthday("Charlotte", 24);
}
Reassignment
Inside of a method, arguments work the same as normal variable declarations. This means that their value can be reassigned within the method body;
void eat(String food) {
IO.println("I ate " + food);
food = "nothing";
IO.println("Now I have " + food);
}
void main() {
eat("Cake");
}
Reassigning an argument's value will not affect the value assigned to any variables passed to the method by the caller.
void eat(String food) {
IO.println("I ate " + food);
food = "nothing";
IO.println("Now I have " + food);
}
void main() {
String fruit = "apple";
eat(fruit);
IO.println(
"But in the caller I still have an " + fruit
);
}
Final Arguments
Just like normal variable declarations, arguments can be marked
final
. This makes it so that they cannot be reassigned.
void eat(final String food) {
IO.println("I ate " + food);
}
void main() {
eat("Welsh Rarebit");
}
If you try to reassign a final argument, Java will not accept your program.
void eat(final String food) {
IO.println("I ate " + food);
// Will not work
food = "toast";
IO.println(food);
}
void main() {
eat("Welsh Rarebit");
}
This has the same use as regular final variables. If there are lots of lines of code where a variable might be reassigned, it can be useful to not have to read all that code to know that it does happen.1
Adding final
to all arguments can make it harder to read the code, simply because of visual noise.
Aliasing
Because arguments work like variables, if you pass something like an array as an argument the array referenced by the argument will be the exact same as the array referenced by the variable given to the method.
void incrementFirst(int[] numbers) {
numbers[0] = numbers[0] + 1;
}
void main() {
int[] nums = new int[] { 8 };
// The first number is 8
IO.println(
"The first number is " + nums[0]
);
incrementFirst(nums);
// Now it is 9
IO.println(
"Now it is " + nums[0]
);
}
The argument aliases the value passed to the method.
Overloading
Multiple methods can be declared that have the same name. This is allowed so long as each method takes different types or different numbers of arguments.
void doThing(int x) {
IO.println(x);
}
void doThing(String name) {
IO.println("Hello " + name);
}
void doThing(int x, int y) {
IO.println(x + y);
}
When you call the method, Java will know what code to run because it knows the types of and number of arguments you are passing.
void doThing(int x) {
IO.println(x);
}
void doThing(String name) {
IO.println("Hello " + name);
}
void doThing(int x, int y) {
IO.println(x + y);
}
void main() {
// Java can figure out what to do
doThing(1);
doThing("abc");
doThing(1, 2);
}
When there are multiple methods that have the same name but take different arguments, those methods are considered "overloads" of eachother1
"Overloading" in this context means when one word has more than one possible meaning depending on how it is used. Buffalo buffalo Buffalo buffalo buffalo buffalo Buffalo buffalo.
Inferred Types
With variable declarations, you can use var
to let Java figure out the type
of the variable.
var name = "Jupiter";
This is not allowed with argument declarations.
// You aren't allowed to use var for arguments!
void makeHorchata(var milkFatPercent) {
// ...
}
You must always explicitly write out the types of arguments.
void makeHorchata(double milkFatPercent) {
IO.println(
"Making a horchata with " + milkFatPercent + "% milk."
);
}
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Write a method named printSquare
which takes one int
argument named size
.
The size
argument should control how big of a square is output.
// CODE HERE
void main() {
printSquare(4);
IO.println();
printSquare(3);
IO.println();
printSquare(2);
IO.println();
printSquare(1);
IO.println();
}
Challenge 2.
What happens if a negative number is given to your printSquare
?
Make it so that if a negative number is given, it works the same as if a positive number was given.
// CODE HERE
void main() {
printSquare(3);
IO.println();
printSquare(-3);
IO.println();
IO.println();
printSquare(-2);
IO.println();
printSquare(2);
}
Challenge 3.
Write a method with four overloads such that
the code in main
can run unchanged.
// CODE HERE
void main() {
f(2);
f("b");
f('9');
f(new String[] { "s" });
}
Challenge 4.
Call the defined methods in a way that outputs "I did it!"
void i() {
IO.print("I");
}
void did(String what) {
IO.println("did " + what);
}
void space() {
IO.print(" ");
}
void main() {
// Code here
}
Return

If the only thing you could do with methods was to call them, they would have limited uses.
This is why methods can also "return" results to the code that calls them.
int plusOne(int x) {
return x + 1;
}
Declaration
The return type of a method is written to the left of the name of the method.
To have a method which returns an int
, you write int methodName
.
int returnsEight() {
return 8;
}
To have a method with returns a String
, you write String methodName
.
String getName() {
return "bob";
}
And so on for any type in Java.
Return Statement
Whenever the code in a method reaches a line that looks like return <VALUE>;
, that method
will immediately exit.
This will exit out of any loops, similarly to a break
.
int doMath() {
int x = 0;
while (true) {
x++;
if (x == 8) {
return x;
}
}
// Needed because Java isn't smart enough to know
// that the while loop will always reach the return x;
// line.
return 0;
}
We call this kind of line a return statement.
Exhaustiveness
When a method returns data, Java needs to know that no matter what happens
in the method there will be some return
line reached.
int compute(int x) {
if (x < 0) {
return 5;
}
// Error! No return if x >= 0
}
int compute(int x) {
// Both "branches" have returns, so all is well
if (x < 0) {
return 5;
}
else {
return 1;
}
}
We call this property, whether in every situation a method will return a value, "exhaustiveness." If there could be cases where no return statement is reached, that is "non-exhaustive" and Java won't accept your code.
void
All methods have to declare some return type. void
is what you write when a method won't return any value.
// Returns a String
String title() {
return "All Night";
}
// Returns an int
int views() {
return 4071;
}
// Doesn't return any value.
void talkAboutVideo() {
IO.println(title() + " only has " + views() + " views.");
}
// This is what the void in "void main()" means
void main() {
talkAboutVideo();
}
Return in void methods
In void
methods, you can still exit early with a return
statement, but you do not give any
value after it.
void doStuff() {
int i = 0;
while (true) {
if (i == 8) {
return;
}
IO.println(i);
}
}
void main() {doStuff();}
Conversion
When a value is returned, Java will want to coerce it into the type of value that the method says it returns.
If Java knows how to do that conversion, then it can happen automatically.
// This method declares a return type of double.
double returnFive() {
// x is an int
int x = 5;
// When it is returned, it will be turned into a double
return x;
}
But if that conversion might potentially be lossy or, as with converting double
s to int
s,
you must do it yourself.
double returnNine() {
double nine = 9.0;
// The (int) explicitly converts the double to an int
return (int) nine;
}
Unreachable Statements
If Java can figure out that some line of code is unreachable because it will always come after a
return
, then it will not run your code.
void doThing() {
IO.println("A");
return;
// unreachable statement
IO.println("B");
}
void main() {
doThing();
}
Java is easy to trick though.1
void doThing() {
IO.println("A");
if (true) {
return;
}
IO.println("B");
}
void main() {
doThing();
}
This will always return before the println
, but Java chooses to not figure that out. It can't be smart enough to see through every if
, so it doesn't try for any of them.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Make a method that takes a String
as an argument and returns an int
as a result.
How the value for that int is determined is up to you.
// CODE HERE
void main() {
int x = process("abc");
IO.println("Got " + x);
}
Challenge 2.
Define three methods such that the given main method will run.
// CODE HERE
void main() {
f(g(h(4), "b"), "e", "s");
}
Challenge 3.
Make the following multiply
method work for negative numbers.
Do this without simply multiplying using the *
operator.
int multiply(int x, int y) {
int total = 0;
for (int i = 0; i < y; i++) {
total += x;
}
return total;
}
void main() {
IO.println(multiply(3, 5));
// IO.println(multiply(-5, 5));
// IO.println(multiply(-5, -2));
// IO.println(multiply(9, -2));
}
Challenge 4.
Define a method, subtractInt
, which makes the following
code run and produce the "correct" result.
You will need to perform a narrowing conversion.
// CODE HERE
double add(double x, double y) {
return x + y;
}
double multiply(double x, double y) {
return x * y;
}
void main() {
int x = 5;
int y = 8;
int z = subtractInt(add(4, 5), multiply(4, 2));
IO.println(z);
}
Multi-Dimensional Arrays

When you make an array in which each element is itself an array we call that array of arrays a "multi-dimensional array."
class Main {
void main() {
int width = 30;
int height = 30;
int[][] pixels = new int[width][height];
for (int i = 0; i < width; i++) {
for (int j = 0; j < height; j++) {
pixels[i][j] = 0;
}
}
}
}
Declaration
Declaring a multi-dimensional array is similar to declaring a normal array.
The difference is that instead of only one []
following the type declaration, you add an extra []
for each extra dimension.
// 2d array
int[][] grid;
// 3d array
int[][][] cube;
// 4d array
int[][][][] hypercube;
// ... and so on
Array Initializers
To give an initial value to a multi-dimensional array you can use multiple nested array initializers.
These are the same as normal array initializers but with potentially more initializers inside.1
String[][] ticTacToe = {
{ "O", "", "O" },
{ "X", "O", "X" },
{ "O", "X", "X" }
}
When array initizalize and more initialize inside that's a moray.
Initialization with new
Just like for one-dimensional arrays, adding new
and the type of the array before an initializer
will let you use a multi-dimensional array as an expression or for a delayed assignment.
void main() {
int[][] numbers;
numbers = new int[25][25];
// I think 25 is my favorite number
int length = (new int[25][25] {}).length;
}
Access Individual Elements
Accessing an element in a multi-dimensional array works the same as accessing the elements of a one-dimensional array.
Each time you index into it you will get out another array, so you just keep writing []
until
you've drilled down to an individual element.
void main() {
String[][] ticTacToe = {
{ "O", "", "O" },
{ "X", "O", "X" },
{ "O", "X", "X" }
}
IO.println(ticTacToe[2][1]);
// This is equivalent to the above
String[] row = ticTacToe[2];
IO.println(row[1]);
}
Set Individual Elements
You can set the value for elements in a multi-dimensional array the same way
as for one-dimensional arrays, just with an extra []
for each extra dimension.
String[][] ticTacToe = {
{ "", "", "" },
{ "", "", "" },
{ "", "", "" }
}
ticTacToe[0][0] = "X";
// The above is a shorthand for this
String[] row = ticTacToe[0];
row[1] = "O";
Initialization with Size
Multi-dimensional arrays are used for representing multi-dimensional things. These tend to also be large things for which writing out every value in an initializer is impractical.
As such, you can initialize them with only an initial size for each dimension.
// Will make a 2d array of 160x144 booleans
boolean[][] pixels = new boolean[160][144];
Default Values
When a multi-dimensional array is made by just providing a size, its elements are initialized to the same default values as they would be in a regular array.
The difference is that each "nested array" will not be initialized to null.
void main() {
String[][] ticTacToe = new String[3][3];
// Each array will be non-null
IO.println(ticTacToe[0]);
// But the elements of those arrays will be null
// (or whatever the default value is for the type.)
IO.println(ticTacToe[0][0]);
}
Populate Values
Once you've created a multi-dimensional array you can use loops to give initial values to its elements.
Since you will end up nesting these loops, its a good example of a place where you should progress
naming your variables i -> j -> k
.
// If you prefer true as the default over false
// you need to do that work yourself
boolean[][] booleanField = new boolean[25][25];
for (int i = 0; i < booleanField.length; i++) {
for (int j = 0; j < booleanField[i].length; j++) {
booleanField[i][j] = true;
}
}
This is helpful if the default values (null
, 0
, false
, etc.) are not what you'd prefer
Ragged Arrays
In relatively unique circumstances1 you might want to make a "ragged array". That is, a multi-dimensional array where each array might be of a different size.
You can do this by making the number of elements in nested initializers be different
boolean[][] triangle = {
{ false },
{ false, false, false},
{ false, false, false, false, false },
{ false, false, false},
{ false }
}
Or by using arrays initialized with new
inside of an initializer.
boolean[][] triangle = {
new boolean[1],
new boolean[3],
new boolean[5],
new boolean[3],
new boolean[1]
};
Or even by omitting the trailing dimensions on when initializing with new and later filling in each row.
boolean[][] triangle = new boolean[5][];
triangle[0] = new boolean[1];
triangle[1] = new boolean[3];
triangle[2] = new boolean[5];
triangle[3] = new boolean[3];
triangle[4] = new boolean[1];
And if you are using a multi-dimensional array, you are already doing something interesting.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Initialize 10x10 2D array of int
s by size.
Print every element of this new 2D array.
Will any elements of the outermost array be null
?
Write down your guess before making the program.
void main() {
int[][] xs = /* CODE HERE */
// CODE HERE
}
Challenge 2.
Print out every character in the 2D array of char
s. After each
row print a new line.
void main() {
char[][] picture = new char[][] {
new char[] { ' ', ' ', ' ', ' ' },
new char[] { ' ', '*', '*', ' ' },
new char[] { '\\', ' ', ' ', '/' },
new char[] { ' ', '-', '-', ' ' }
};
// CODE HERE
}
Challenge 3.
"Draw" a smiley face on the "canvas" provided by a 5x5 2D char
array.
Use similar code as the previous challenge to print it out.
void main() {
final char[][] picture = new char[5][5];
// CODE HERE
}
Challenge 4.
Write a method named winner
. It should take in a 2-dimensional
array of String
s where each character is either X
, O
, or an empty string.
This 2D array represents a game of Tic-Tac-Toe.
If there is a winner of the Tic-Tac-Toe game, it should return X
or O
depending on who the winner is.
It should return null
if nobody won or the game is a tie.
String winner(String[][] ticTacToe) {
// CODE HERE
}
void main() {
var winnerA = winner(new String[][] {
new String[] { "X", "X", "" },
new String[] { "O", "", "" },
new String[] { "O", "", "" }
});
IO.println(winnerA);
var winnerB = winner(new String[][] {
new String[] { "X", "X", "X" },
new String[] { "O", "O", "X" },
new String[] { "O", "", "O" }
});
IO.println(winnerB);
var winnerC = winner(new String[][] {
new String[] { "O", "X", "O" },
new String[] { "O", "O", "X" },
new String[] { "O", "X", "O" }
});
IO.println(winnerC);
}
ASCII Art Generator
Problem Statement
Humans like to draw stuff and to look at drawings of stuff.
We know this is in some manner intrinisic to us as a species because we've found drawings in caves dating back at least 51,200 years.
As such it is a normal impulse to use pictures, drawings, iconography, and other forms of art as a tool for communication.
In the early days of the internet the amount of data you could send between computers was extremely limited. This meant that, in practice, most people would communicate using solely text.
Instead of making it so that people couldn't send images to eachother, that restriction birthed a new form of art. Using only the characters available to send as text, people would make and send pictures.
For example, here is a bat from a website that archives examples of this form of art.
/'. .'\
\( \__/ )/
___ / (.)(.) \ ___
_.-"`_ `-.| ____ |.-` _`"-._
.-'.-'//||`'-.\ V--V /.-'`||\\'-.'-.
`'-'-.// || / .___. \ || \\.-'-'`
`-.||_.._| |_.._||.-'
\ (( )) /
jgs '. .'
`\/`
+------+. +------+ +------+ +------+ .+------+
|`. | `. |\ |\ | | /| /| .' | .'|
| `+--+---+ | +----+-+ +------+ +-+----+ | +---+--+' |
| | | | | | | | | | | | | | | | | |
+---+--+. | +-+----+ | +------+ | +----+-+ | .+--+---+
`. | `.| \| \| | | |/ |/ |.' | .'
`+------+ +------+ +------+ +------+ +------+'
__
\ ______/ V`-,
} /~~
/_)^ --,r'
|b |b
We call these drawings "ASCII Art" after the "American Standard Code for Information Interchange" - ASCII. ASCII defined an English-centric set of characters and how to represent them in a computer. Much of this early art was made solely using that character set, hence the name.
Even though sending images is now practical to do over the internet, ASCII art is still a valid form of expression. Either as a deliberate choice or because of using a text only medium (they still exist. Think of in-game chats.) ASCII art can be useful.
If you want to see how far this can be taken check out this entirely ASCII art rendition of the Star Wars IV: A New Hope
Your Goal
Make a program that asks a user for a "height" and then prints out an ASCII art christmas tree that is that many characters tall.
Here is an example tree you can use as a starting point. You can print this when asked for a height of 3
.
*
***
*****
|
Here is another example, but with a height of 5
.
*
***
*****
*******
*********
|
And here it is with a height of 1
*
|
Future Goals
When you learn enough to do the following, come back to this project and expand it.
- Draw something with more varied parts, like a snowman. You might find it convenient to not directly print to the screen, but instead "draw" what you want to print into an array first then print the contents of that array.
---
| |
-----
/ _ ^ \
| * * |
| V |
\^___^/
-------
/ \
* | | *
* | | *
* | | *
* \-------/ *
---------
/ \
| |
| |
| |
| |
\---------/
- Make sure that if the person using your program gives you a negative number, zero, or something that is not a number you don't just crash. This means you might need to reprompt them for information.
- Make the christmas tree prettier. This will require "finding the pattern" in a more interesting piece of art, like this example.
/\
/\*\
/\O\*\
/*/\/\/\
/\O\/\*\/\
/\*\/\*\/\/\
/\O\/\/*/\/O/\
||
||
||
-
Draw both the snowman and a tree. Let the user select which goes on the left and which goes on the right.
-
Turn this into a command-line program that works similarly to the cowsay tool.
_______
< Hello >
-------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
- Expand or refocus into drawing other kinds of things.
null

There is a special value called null
which is assignable to most types.
void main() {
String name = null;
int[] numbers = null;
IO.println(name);
IO.println(numbers);
}
The only types which null
cannot be assigned to are int
, double
, char
, and boolean
.1
void main() {
// Will not work
int x = null;
}
As well as long
, short
, byte
, and float
but I haven't shown you those yet.
Null as Absence
One way to use null
is to have it be a stand in for when there is
an "absence" of a value.
Consider Cher. Unlike most people, Cher does not have a last name.
null
is an appropriate value to use when there is such an absense.
void main() {
String firstName = "Cher";
String lastName = null;
IO.println(firstName);
IO.println(lastName);
}
Null as Unknown
Another equally valid way to use null
is to have it
stand in for information that you do not yet know.
If you have a program ask someone for their name, between the time you start the program and you get a response, their name is unknown to you.
String firstName = null;
// Some lines of code
firstName = askForName();
This is subtly different than delayed assignment. Between when
you don't know the information and when you learn it you are actually allowed
to use a variable initialized to null
.
The difference between this kind of situation and a "known absence" is also subtle. In this situation you do not know what a value would be. In the other you know that there is no value to get.
Checking for null
If you are unsure whether something is null, you can
check by using ==
.
void sayHello(String firstName, String lastName) {
if (lastName == null) {
IO.println("Hello " + firstName);
}
else {
IO.println("Hello " + firstName + " " + lastName);
}
}
void main() {
sayHello("Sonny", "Bono");
sayHello("Cher", null);
}
NullPointerException
If you try to perform an operation on a null
reference,
such as checking the .length()
of a String
, your code will
crash.
void main() {
String thing = null;
// NullPointerException
IO.println(thing.length());
}
When this happens the error that Java shows you will be a "NullPointerException
".
This will look something like the following.
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "thing" is null
at Main.main(Main.java:4)
Because this is easy to make happen by mistake, it is worth familiarizing yourself with the format of it.
This way you can laser focus on the part that says something like "Cannot invoke "String.length()" because "thing" is null
" and know that the issue is with some variable named thing
that you are trying to call .length()
on.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Write a method which takes in a String[]
representing
a series of names and prints out every name in sequence.
If this method is given null
, it should act as if it
was given an empty array.
void printNames(String[] names) {
}
void main() {
printNames(new String[] {
"Joker",
"Batman",
"Alfred"
});
}
Challenge 2.
Alter the method you wrote in the previous challenge
so that if it is given null
it outputs the message
You do not know any names yet
.
If it is given an empty String[]
it should continue
to simply output nothing.
void printNames(String[] names) {
}
void main() {
printNames(new String[] {
"Joker",
"Batman",
"Alfred"
});
}
Challenge 3.
Will the following code throw a NullPointerException
?
Why or why not?
void main() {
String[] jobs = new String[] {
"Carpenter",
"Baker",
null,
"Astronomer"
};
for (int i = 0; i < jobs.length; i++) {
IO.println(jobs[i]);
}
}
Challenge 4.
The following code won't work. Give your best guess as to why and then try running it.
void main() {
int[] numbers = new int[] {
45,
32,
null,
94
};
for (int i = 0; i < numbers.length; i++) {
IO.println(numbers[i]);
}
}
Challenge 5.
Without changing anything in the main
method, make the bigness
method not throw a NullPointerException
and still have the "correct"
behavior for non-null inputs.
String bigness(String letters) {
int bigness = 0;
for (int i = 0; i < letters.length(); i++) {
bigness++;
}
if (bigness < 5) {
return "small";
}
else if (bigness < 10) {
return "medium";
}
else {
return "big";
}
}
void main() {
IO.println(
bigness("bore")
);
IO.println(
bigness("boiler")
);
IO.println(
bigness("filter")
);
IO.println(
bigness("knower")
);
IO.println(
bigness("chrysanthemum")
);
IO.println(
bigness(null)
);
}
Boxed Primitives

The fact that int
, double
, char
, and boolean
cannot have null
values
can be limiting.
For this reason there are versions of those primitive1 types which do not have this restriction.
void sayAge(Integer age) {
if (age == null) {
IO.println("Age is not yet known");
}
else {
IO.println("Age is " + age);
}
}
void main() {
Integer age = null;
sayAge(age);
age = 26;
sayAge(age);
}
We call these primitives which might be null "Boxed Primitives" because they are made by taking the underlying thing and putting it in a "box."2
We call them "primitive" types because there isn't a way for you to implement them yourself in Java. They have to be given to you as a fundamental and magic sort of thing.
Don't worry too much about the word box in this context. This will make more sense once you learn how to define your own types. I just wanted to at least try to gesture at why it has the silly name that it does.
Integer
The type to use for an int
that might be null is Integer
.
void main() {
Integer i = null;
IO.println(i);
i = 5;
IO.println(i);
}
If you try to do any math on an Integer
which holds null
you will
get a NullPointerException
.
void main() {
Integer i = null;
IO.println(i * 5);
}
Double
The type to use for a double
that might be null is Double
.
void main() {
Double d = null;
IO.println(d);
d = 3.14;
IO.println(d);
}
If you try to do any math on a Double
which holds null
you will
get a NullPointerException
.
void main() {
Double d = null;
IO.println(d + 1);
}
Character
The type to use for a char
that might be null is Character
.
void main() {
Character c = null;
IO.println(c);
c = '%';
IO.println(c);
}
Boolean
The type to use for a boolean
that might be null is Boolean
.
void main() {
Boolean b = null;
IO.println(b);
b = true;
IO.println(b);
}
Unboxing Conversion
If you try to use a boxed primitive in a context where the normal type is expected, it will be implicitly "unboxed."
This means you can use Integer
s directly in math expressions.
void main() {
Integer x = 5;
int y = 3;
int z = x * y;
IO.println(z);
}
As well as Boolean
s in logical expressions.
void main() {
Boolean hasHat = true;
if (hasHat) {
IO.println("You have a hat!");
}
}
And so on for Double
, Character
, etc.
But if you use one of these types like this and they happen to be null
you will
get a NullPointerException
.
void main() {
Integer x = null;
// Bool
int y = x;
}
Boxing Conversion
If you try to assign to a boxed type like Integer
from some
code that gives you the unboxed version like int
, then Java will
automatically do that conversion.1
void main() {
int x = 5;
Integer y = x;
IO.println(x);
IO.println(y);
}
This might feel obvious, but this is one of the few places in Java where the type of something "magically" changes. int
and Integer
, char
and Character
, etc. are different types.
Arrays of Boxed Primitives
If you have an array of the boxed version of a type, that is not compatible with an array containing the unboxed version and vice-versa.1
int[] numbersOne = { 1, 2, 3 };
Integer[] numbersTwo = { 4, 5, 6 };
// This line won't work
numbersOne = numbersTwo;
// And neither will this one
numbersTwo = numbersOne;
This means that to turn something like a boolean[]
into a Boolean[]
or vice-versa,
you must manually make a new array and copy over elements. Doing this in either
direction will work because boxing and unboxing conversions exist between the primitives
and their boxed variants.
void main() {
boolean[] yesAndNo = new boolean[] { true, false };
Boolean[] yesAndNoCopy = new Boolean[] { false, false };
for (int i = 0; i < yesAndNo.length; i++) {
// Here a boxing conversion takes place
yesAndNoCopy[i] = yesAndNo[i];
}
boolean[] yesAndNoCopyCopy = new boolean[] { false, false };
for (int i = 0; i < yesAndNoCopy.length; i++) {
// And here an unboxing conversion
yesAndNoCopyCopy[i] = yesAndNoCopy[i];
}
}
The reasons for this are deeply interesting and have to do with the nitty gritty of how Java is actually implemented. It might also change in the future.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Will this code run? Why or why not.
int compute(int x) {
if (x == 0) {
return null;
}
else {
return x * x;
}
}
void main() {
IO.println(compute(5));
}
Challenge 2.
Write a method which takes in an Integer[]
representing
a series of distances and prints out every distance
followed by kilometers
.
So if the array has 1
, 2
, and 3
you should output
1 kilometers
2 kilometers
3 kilometers
If this method is given null
, it should act as if it
was given an empty array.
void printDistances(Integer[] distances) {
}
void main() {
printDistances(new Integer[] {
45,
99,
23
});
}
Challenge 3.
Write a method called onlyPositive
which takes in an int
and returns
the same value out if the number is greater than zero.
If the number is less than or equal to zero, return null
.
// Write onlyPositive here
void main() {
// 45
IO.println(
onlyPositive(45)
);
// 46
IO.println(
onlyPositive(45) + 1
);
// null
IO.println(
onlyPositive(0)
);
// null
IO.println(
onlyPositive(-1)
);
}
Challenge 4.
Will the following code work? Why or why not?
void main() {
int ducks = 5;
Integer sparrows = 3;
int birds = ducks + sparrows;
IO.println(birds);
}
Challenge 4.
Will the following code work? Why or why not?
void main() {
char[] face = new char[] { ':', ')' };
Character[] smile = face;
IO.println(smile);
}
Challenge 5.
Will the following code work? Why or why not?
void main() {
char[] face = new char[] { ':', ')' };
Character[] smile = new Character[face.length];
for (int i = 0; i < face.length; i++) {
smile[i] = face[i];
}
IO.println(smile);
}
Arrays II

Fairly often you will want to have arrays in your program which you either do not know the initial values for or which are too big to physically type out every value in an initializer.
String[] everyStudentName = // ???
char[] everyLetterInEveryAlphabet = // ???
Initialization with Size
The Nintendo GameBoy had a screen resolution of 160 x 144. To store the value of each pixel1 you would need an array 23,040 items long.
To support this without you writing the word false
23,040 times,
arrays can be made just by giving a size and skipping the initializer.
boolean[] pixels = new boolean[23040];
So you have to say new
followed by the type of element in the array, [
, the size of the array and ]
.
The original GameBoy wasn't actually just black and white. It supported 7 shades of gray, so a boolean
wouldn't technically be enough to represent a pixel's state. You'd have to use something with at least 8 states, not just 2.
Default Values
When an array is made by just providing a size, its elements are initialized to some default value.
For primitive types like int
and double
, each element will be initialized to 0
.
void main() {
int[] digits = new int[10];
// 0
IO.println(digits[0]);
double[] readings = new double[5];
// 0.0
IO.println(readings[0]);
}
For boolean
, each element will be initialized to false
.1
void main() {
boolean[] pokedex = new boolean[10];
// false
IO.println(pokedex[0]);
}
And for every non-primitive type, which is every single type including the boxed primitives,
the default value will be null
.
void main() {
String[] names = new String[10];
// null
IO.println(names[0]);
Integer[] scores = new Integer[26];
// null
IO.println(scores[0]);
}
Fun fact. The GameBoy and GameBoy Advance Pokemon games tracked pokedex completion in a big boolean
array. If you saw a Pokemon it would flip that Pokemon's index in the "seen" array to true
. If you caught it, it would do the same in a different array. Those games weren't written in Java, but the concept is the same.
Populate Arrays
If the default value for an array is not valid for what you are doing, you will need to populate the array with better initial values.
For loops are generally good for this purpose.
void main() {
char[] letters = new char[26];
for (int i = 0; i < letters.length; i++) {
letters[i] = (char) ('a' + i);
}
IO.println(letters);
}
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Make an empty String
array without using an empty initializer.
This means you cannot write String[] empty = {}
or String[] empty = new String[] {}
.
void main() {
String[] empty = ???;
// Should be 0
IO.println(empty.length);
}
Challenge 2.
What will the following code output? Change the code between the lines so it instead outputs the following.
1.0
2.0
3.0
4.0
5.0
void main() {
Double[] prices = new Double[5];
// ----------
// CODE HERE
// ----------
for (int i = 0; i < prices.length; i++) {
double price = prices[i];
IO.println(price);
}
}
Challenge 3.
Only writing code between the lines and without reassigning the sandwich
variable,
make the following code output egg and cheese
.
void main() {
char[] sandwich = new char[14];
// ----------
// CODE HERE
// ----------
IO.println(sandwich);
}
Challenge 4.
Populate the triangle
array such that the code
prints a right triangle that looks like the following.
*
**
***
void main() {
char[] triangle = new char[8];
// ----------
// CODE HERE
// ----------
IO.println(triangle);
}
Challenge 5.
Make a method named buildTriangle
which returns a char[]
that can be printed out to display a right triangle of any height.
You can ignore the possibility that a negative or zero height is given.
char[] buildTriangle(int height) {
// CODE HERE
}
void main() {
IO.println(buildTriangle(3));
IO.println("--------------");
IO.println(buildTriangle(5));
IO.println("--------------");
IO.println(buildTriangle(2));
}
Classes

Up until now all the data types you have used - int
, String
, etc. -
came with Java. This works for awhile, but eventually you will need to define your own types.
The way to do this is with a "class."
class Person {
}
The meaning of Class
Classes are descriptions of a "classification" of objects in your program.
The terminology is analagous to biological classification.
Where Plato would classify any "featherless biped" as a human1,
a String
is classified by the set of things you can do to it and the data
it stores. The String
class would specify all of that.2
What an idiot
If that's a bit too heady of an explanation don't fret. You will likely get it intuitively eventually. There are like 50 different ways to explain it and eventually one will land for you.
Class Declaration
To declare a class, you say class
followed by a name for the class and a pair of {
and }
class Muppet {
}
Naming
It is social convention to name classes with the first letter of each word capitalized. So if you wanted to make a class representing an inch worm, you would say the following.1
class InchWorm {
}
For things that are not English or are acronyms the rules get fuzzy. Use your best judgement.
Instances
Once you've declared a class, you can make an instance
of that class by saying new
followed by that class's name
and ()
.1
class Muppet {
}
void main() {
Muppet kermit = new Muppet();
IO.println(kermit);
}
Very similar to arrays, the output from printing an instance of a class might seem like gibberish (Main$Muppet@1be6f5c3
).
You will learn how to make it nicer later.
I haven't used it in many code samples thus far, but if you remember var
this is one of the times
where it can be aesthetically convenient. var kermit = new Muppet();
Fields
Classes contain zero or more fields.
Fields are like variables except they don't live in a method, they are attached to instances of the class they are a part of.
To declare a field in a class you say the type of the field, a name for the field,
and then a ;
.
class Muppet {
String name;
}
One way to think about it is that when you say new Muppet()
, Java makes a box big enough to hold
all of the fields that a muppet needs.1
This "box" metaphor is part of where the name "boxed primitives" comes from.
Field Initialization
You can set an initial value for a field in a few ways.
One is to assign the field directly on the instance created.
class Muppet {
String name;
}
void main() {
Muppet kermit = new Muppet();
kermit.name = "Kermit The Frog";
}
Another is to set a default value directly in the field declaration. This makes the most sense if its a value that most instances will share.
class Muppet {
// Most are!
boolean talented = true;
}
void main() {
Muppet kermit = new Muppet();
}
Field Access
You can access the value of any field in a class by writing the name of a variable holding an instance
of that class, .
, then the name of that field.
class Muppet {
String name;
}
void main() {
Muppet kermit = new Muppet();
kermit.name = "Kermit The Frog";
// The .name accesses the "name" field
IO.println(kermit.name);
}
Field Default Values
Before a field in a class is given a value it will have the same default value as it would if you made an array of the field's type.
That is: 0
for int
, 0.0
for double
, false
for boolean
, and null
for most all else.
class Muppet {
int age;
double salary;
boolean talented;
String name;
}
void main() {
Muppet kermit = new Muppet();
// 0
IO.println(kermit.age);
// 0.0
IO.println(kermit.salary);
// false
IO.println(kermit.talented);
// null
IO.println(kermit.name);
}
Aliasing
When two variables point to the same instance of a class, those variables will be aliases for the same data.
This means that, just like arrays, if you change the value of a field on one that change will be visible on the other.
class Muppet {
String name;
}
void main() {
Muppet kermit = new Muppet();
Muppet darkKermit = kermit;
kermit.name = "Kermit The Frog";
// Kermit The Frog
IO.println(kermit.name);
// Kermit The Frog
IO.println(darkKermit.name);
}
Return Multiple Values
Methods can only ever return one type of thing. This
usually retricts you to only ever returning one int
or
one String
.
But if you make a class that has multiple fields you can use that to return more than one piece of information.1
class Location {
double latitude;
double longitude;
}
Location findTreasureIsland() {
Location location = new Location();
location.latitude = 51.4075;
location.longitude = 0.4636;
return location;
}
void main() {
Location treasureIsland = findTreasureIsland();
IO.println(
"Treasure island is located at " +
treasureIsland.latitude +
" " +
treasureIsland.longitude +
"."
);
}
There are many more reasons to make your own classes, but this one is pretty quick to see even at this stage.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
The following classes are named "wrong." Name them correctly.
class gonzo {}
class fozzie_the_bear {}
class MSPIGGY {}
void main() {
IO.println(new gonzo());
IO.println(new fozzie_the_bear());
IO.println(new MSPIGGY());
}
Challenge 2.
Make a variable named movie
which is an instance of the Movie
class.
Set the value of its title
field to Muppets in Space
.
class Movie {
String title;
}
void main() {
// ---------------
// CODE HERE
// ---------------
IO.println(
movie.title
);
}
Challenge 3.
Alter the ThemePark
class so that the default value
for its entranceFee
field is 35.24
.
class ThemePark {
double entranceFee;
}
void main() {
ThemePark themePark = new ThemePark();
IO.println(
themePark.entranceFee
);
}
Challenge 4.
Changing only the indicated line,
make it so the word Kermit
only appears twice in the
program.
Hint: Remember that inferred types exist.
class Kermit {
boolean angry = true;
}
void main() {
// ------------------------
// CHANGE ONLY THIS LINE v
Kermit kermit = new Kermit();
// ------------------------
IO.println(kermit.angry);
}
Challenge 5.
Make a method named squareRoot
which returns an
instance of the SquareRoot
class containing both
the positiveRoot
and negativeRoot
of the given double
.
So if you are given 4
you should return a positive root
of 2
and a negative root of -2
.
You do not have to account for the possibility of being given a negative
number. You should use Math.sqrt
to find the positive root and common
sense to find the negative root.
class SquareRoot {
double positiveRoot;
double negativeRoot;
}
SquareRoot squareRoot(double value) {
// -----------
// CODE HERE
// -----------
}
void main() {
SquareRoot sqrtOfFour = squareRoot(4);
// 2
IO.println(sqrtOfFour.positiveRoot);
// -2
IO.println(sqrtOfFour.negativeRoot);
SquareRoot sqrtOfFifteen = squareRoot(15);
// 3.872983346207417
IO.println(sqrtOfFifteen.positiveRoot);
// -3.872983346207417
IO.println(sqrtOfFifteen.negativeRoot);
}
Challenge 6.
Only writing code between the lines and without directly accessing any fields on or reassigning the actor
variable, make the program output Tim Curry
.
Hint: The key word is "directly."
class Actor {
String name;
}
void main() {
Actor actor = new Actor();
actor.name = "Frog, Kermit the";
// --------------------------
// CODE HERE
// --------------------------
IO.println(actor.name);
}
Instance Methods

In addition to having fields, classes can also have their own method definitions.
These look the same as the method definitions you've seen so far, they are just put within a class definition.1
class Muppet {
String name;
void freakOut() {
IO.println("**ANGRY KERMIT NOISES**")
}
}
We call these instance methods because you need an instance of the class in order to call the method.
If you haven't seen the muppets this might go over your head, but Kermit randomly gets really mad.
Invocation
To invoke an instance method you first need an instance of the class.
You then write .
followed by the name of the instance method and a list of arguments.1
class Elmo {
void talkAboutRocko() {
IO.println("ROCKO'S NOT ALIVE!!")
}
}
void main() {
Elmo elmo = new Elmo();
elmo.talkAboutRocko();
}
Daily reminder that Elmo absolutely hates Rocko
Arguments
Instance methods can take arguments the same as the methods you have seen so far.
class Muppet {
String name;
void singLyric(int verse) {
if (verse == 1) {
IO.println("Why are there so many");
}
else if (verse == 2) {
IO.println("Songs about rainbows");
}
else {
IO.println("And what's on the other side?");
}
}
}
Field Access
Within an instance method's definition, you can access the values of any fields declared in the class by just writing their name.
class Elmo {
int age;
void sayHello() {
IO.println("Hi, I'm Elmo");
IO.print("I am ");
// You can use elmo's age by just writing "age"
IO.print(age);
IO.println(" years old.");
}
}
void main() {
Elmo elmo = new Elmo();
elmo.age = 3;
elmo.sayHello();
}
Field Updates
You can also update the value of any field from within an instance method the same as if it were a local variable.
class Elmo {
int age;
void sayHello() {
IO.println("Hi, I'm Elmo");
IO.print("I am ");
IO.print(age);
IO.println(" years old.");
}
void haveBirthday() {
age = age + 1;
}
}
void main() {
Elmo elmo = new Elmo();
elmo.age = 3;
elmo.sayHello();
elmo.haveBirthday();
elmo.sayHello();
}
Derived Values
A common use for methods is to provide a value that is derived from the values of other fields.
class Elmo {
int age;
int nextAge() {
return age + 1;
}
}
void main() {
Elmo elmo = new Elmo();
elmo.age = 3;
IO.println("Elmo is " + elmo.age + " right now,");
IO.println("but next year Elmo will be " + elmo.nextAge());
}
Which is useful for situations like where you store someones first and last name but need to ask "what is their full name?"
class Elmo {
String firstName;
String lastName;
String fullName() {
return firstName + " " + lastName;
}
}
void main() {
Elmo elmo = new Elmo();
elmo.firstName = "Elmo";
elmo.lastName = "Furchester";
IO.println("Elmo's full name is " + elmo.fullName());
}
Invoke Other Methods
Inside of an instance method you can invoke other instance methods on the same class by writing their name and providing any needed arguments.
class Elmo {
int age;
void sayHello() {
IO.println("Hi, I'm Elmo");
IO.print("I am ");
IO.print(age);
IO.println(" years old.");
}
void startTheShow(String showName) {
sayHello();
IO.println("Welcome to " + showName);
}
}
void main() {
Elmo elmo = new Elmo();
elmo.age = 3;
elmo.startTheShow("Sesame Street");
}
this
Within an instance method, you have access to a magic variable called this
.
this
is a variable that contains the current instance of the class.
You can use this
to access fields or invoke any method.
class Elmo {
int age;
void sayHello() {
IO.println("Hi, I'm Elmo");
IO.print("I am ");
IO.print(this.age);
IO.println(" years old.");
}
void startTheShow(String showName) {
this.sayHello();
IO.println("Welcome to " + showName);
}
}
void main() {
Elmo elmo = new Elmo();
elmo.age = 3;
elmo.startTheShow("Sesame Street");
}
Disambiguation
One reason you might need to use this
is if the name
of an argument to a method is the same as the name of a field.
class Elmo {
int age;
boolean isOlderThan(int age) {
return this.age > age;
}
}
void main() {
Elmo elmo = new Elmo();
elmo.age = 3;
// true
IO.println(elmo.isOlderThan(2));
}
If you didn't do this, it would be ambiguous whether you were referring to the field or the argument. This removes the ambiguity.1
Really it isn't ambiguous for Java. It will just think you are referring to the argument. It is ambiguous from the perspective of a human being reading the code though.
Clarity
Another reason you might want to use this
is for code clarity.
It is a lot easier to know that a particular line refers to a field
or method on the same class if there is an explicit this.
before
the reference.1
class Elmo {
int age;
void sayHello() {
IO.println("Hi, I'm Elmo");
IO.print("I am ");
IO.print(this.age);
IO.println(" years old.");
}
}
void main() {
Elmo elmo = new Elmo();
elmo.age = 3;
elmo.sayHello();
}
This is very much a personal preference thing. I generally choose to write
this.
whenever I am able to for this reason.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Make a class named PirateShip
which has one int
field
named crewSize
.
Add an instance method to that class named sail
which outputs
N sailors, ready to sail!
Where N is the size of the crew.
// ----------------
// CODE HERE
// ----------------
void main() {
PirateShip ship = new PirateShip();
ship.crewSize = 25;
ship.sail();
}
Challenge 2.
Make a class named StringArrayView
which has
one String[]
field named value
and two methods named get
and length
.
get
should take in an index and return the matching element
of the array.
length
should take no arguments and give the length of the array.
// ----------------
// CODE HERE
// ----------------
void main() {
StringArrayView view = new StringArrayView();
view.value = new String[] { "A", "B", "C" };
// 3
IO.println(view.length());
// A
IO.println(view.get(0));
// C
IO.println(view.get(2));
}
Challenge 3.
Alter the VoiceActor
class so that it has a method named fullName
that returns their firstName
followed by their lastName
and separated
by a space.
If their lastName
is null
, you should have no trailing space.
If their firstName
is null
, you should have no leading space.
If both their firstName
and lastName
are null
, you should
return "No Name"
.
class VoiceActor {
String firstName;
String lastName;
// -----------------
// CODE HERE
// -----------------
}
void main() {
VoiceActor goku = new VoiceActor();
goku.firstName = "Masako";
goku.lastName = "Nozawa";
// "Masako Nozawa"
String gokuFullName = goku.fullName();
IO.println(gokuFullName);
// "Nozawa"
goku.firstName = null;
gokuFullName = goku.fullName();
IO.println(gokuFullName);
// "No Name"
goku.lastName = null;
gokuFullName = goku.fullName();
IO.println(gokuFullName);
// "Horikawa"
VoiceActor vegeta = new VoiceActor();
vegeta.lastName = "Horikawa";
IO.println(vegeta.fullName());
}
Challenge 4.
Make a Rectangle
class which has a width
field and a height
field. Give it an instance method named toCharArray
which gives
a char[]
that can be printed to display a rectangle of the given
width and height.
// ------------
// CODE HERE
// ------------
void main() {
Rectangle rectangle = new Rectangle();
rectangle.width = 3;
rectangle.height = 4;
/*
***
***
***
***
*/
char[] c = rectangle.toCharArray();
IO.println(c);
}
Challenge 5.
Update the definition for the Taco
class so that it has a method named
deluxe
. This should set the taco to have beef, sour cream, cheese,
and onion. Use the existing instance methods instead of directly accessing
fields.
class Taco {
boolean beef;
boolean sourCream;
boolean cheese;
boolean onion;
void addBeef() {
this.beef = true;
}
void addSourCream() {
this.sourCream = true;
}
void addCheese() {
this.cheese = true;
}
void addOnion() {
this.onion = true;
}
void deluxe() {
// ------------
// CODE HERE
// ------------
}
}
void main() {
var taco = new Taco();
taco.deluxe();
IO.println("Has Beef: " + taco.beef);
IO.println("Has Sour Cream: " + taco.sourCream);
IO.println("Has Cheese: " + taco.cheese);
IO.println("Has Onion: " + taco.onion);
}
Challenge 6.
Why doesn't this code function as you'd expect? Fix it by changing one line.
class Oscar {
boolean grouchy;
void setGrouchy(boolean grouchy) {
grouchy = grouchy;
}
}
void main() {
var oscar = new Oscar();
oscar.setGrouchy(true);
IO.println(oscar.grouchy);
}
Point of Sale System
Problem Statement
Consider the operations of a grocery store.
Prospective customers walk in the store, perhaps acquiring a cart or a basket, and gather the items they want to purchase. They then walk to the front of the store, purchase those items using money, and leave.
Some items are priced by quantity. For example, one avacado might cost $0.77. If they purchased 5 avacados they would have to pay $0.77 five times or $3.85 in total.
Other items are priced by weight. Bananas are really cheap for some reason and so 1 pound of bananas costs $0.39. If they purchase 1.5 pounds of bananas they would have to pay 1.5 times $0.39 or $0.585 in total. Currencies like the dollar do not have a "tenth of a penny" so after this math the amount will be rounded either to $0.58 or $0.59 depending on the policy of the store in question.
Either way it is uncommon for someone to have exactly $3.85 or $0.59 on their person. So to facilitate purchases using physical money they must accept larger bills and offer change for the difference. If someone tries to use a $10 bill to buy $3.85 of avacados the store will provide $6.15 back to the customer in change.
While a business can operate this way without any sort of computer involved there are several benefits to having one.
The price a store wants to charge for any given item is bound to change over time. When training a cashier it can make more sense to teach them short codes for different items and have the computer system figure out the price to use1. This would generally include telling them to weigh an item or count the quantity of said item.
In addition to the price, calculating change is easy for computers to do. While it is not hard for a human to do either, a computer will make fewer mistakes calculating the difference between $45 and $13.53. This is especially true over the course of an 8 hour shift where mental fatigue can start to set in.
It is also helpful for a business to know how much they sold in a given day. Having computers in the mix makes it more practical to get these "end of day reports." They might use these reports to know that they need to order more onions because those are selling well recently.2 They can also be used to catch cashiers who are stealing from the register to support their families.3
We call these systems that are used at the point products are sold Point of Sale Systems.
Your Goal
Make a program that can be used as a point of sale system for a hypothetical produce stand.
The products the produce stand sells are as follows:
Item | Cost |
---|---|
Banana | $0.39 per pound |
Avacado | $0.66 each |
Plantains | $0.99 each |
Watermelon | $6.99 each |
Onion | $0.62 each |
Celery | $0.31 per ounce |
Carrot | $0.06 per ounce |
Cabbage | $2.72 each |
Customers may purchase any amount of any items in any combination.
Make sure to at least:
- Track the total for an order.
- Prompt the cashier for the right quantities and weights.
- At the end of an order give the cashier the total and have them enter the amount given to them by the customer.
- Tell the cashier how much in change they need to provide.
Future Goals
When you learn enough to do the following, come back to this project and expand it.
- Grow your program to support double or triple the number of available items.
- Make it so that the cashier has to "log in" to use the system
- Account for a manager whose job it is to record how much money is in the register before and after a day of operations and note any unaccounted for funds.
- Make the program not lose information if the computer running it turns off then on again.
- Make the system work for "self check out," where the person entering the items is also the customer. Theft in these cases is monitored via security cameras and punished via the might of the police state.
- Use a camera attached to the computer to scan a bar code which contains the product number.
- Optionally print a receipt for each order to a physical printer..
Here is a list of "PLU" codes. 4011 is banana.
Due to a viral onion tart recipie or something.
This is common in no small part because cashiers, like many modern professions, are not paid a livable wage.
Enums

While you can use String
, int
, boolean
, and friends
alongside your own custom classes to represent many situations,
that is not always enough.
Consider a stop light. At any given time it is either red, yellow, or green.
The tool we use to model that kind of thing is enums.
enum StopLight {
RED,
YELLOW,
GREEN
}
Enums are types with fixed sets of allowed values. We call them enums because they enumerate multiple different possibilities.
Declaration
To declare an enum
, you write enum
followed by a name for the enumeration
and the names of the different possibilities separated by commas.
enum StopLight {
RED,
YELLOW,
GREEN
}
Variants
We call the different possibilities that an enum represents its "variants."
So in the following code we would say that the TirePressure
enum has three variants: LOW
, NORMAL
,
and HIGH
.
enum TirePressure {
LOW,
NORMAL,
HIGH
}
Naming
You are expected to name enums the same as you would regular classes: WithCapitals
.
The variants of enums are expected to be named in all capital letters, with underscores1
enum CarSeat {
DRIVER,
SHOTGUN,
BACK_LEFT,
BACK_RIGHT
}
We call this convention "SCREAMING_SNAKE_CASE
", which is silly. Snakes can't speak, let alone scream. I should know.
Usage
To use an enum, first make a variable or field whose type is the name of the enum. Then assign to it one of the variants.
You can access the variants of an enum by writing the enum's name,
a .
, then the name of the variant.
enum StopLight {
RED,
YELLOW,
GREEN
}
void main() {
StopLight light = StopLight.RED;
IO.println(
"The light is " + light
);
}
Equality
You can check if two enums contain the same variant
using the ==
operator.
enum StopLight {
RED,
YELLOW,
GREEN
}
void main() {
StopLight light = StopLight.RED;
if (light == StopLight.RED) {
IO.println("You must stop");
}
else {
IO.println("Full speed ahead!");
}
}
Comparison to boolean
Enums are very similar in spirit to boolean
s.
A boolean
has one of two values. true
or false
.
An enum also has one of a fixed set of values. The difference is that each item in this fixed set can have its own name and that there might be more than two values.
Depending on context and personal taste, it might even make sense to use an enum with two variants as a replacement for a boolean.
enum Power {
ON,
OFF
}
void main() {
Power power = Power.ON;
if (power == Power.ON) {
IO.println("The power is on");
}
else {
IO.println("The power is off");
}
}
The benefit here being that the names you give to the enum and to the variants
might be clearer to read in code than boolean
, true
, and false
.
The downside being that you needed to write more code.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Make an enum named Response
which
has three variants. YES
, NO
, and MAYBE_SO
.
// -------------
// CODE HERE
// -------------
void main() {
IO.println(
Response.YES
);
IO.println(
Response.NO
);
IO.println(
Response.MAYBE_SO
);
}
Challenge 2.
Write a method named goodPerformer
which takes
in a String
representing the name of an artist.
If that String
is equal to Pitbull
or Billy Joel
return YES
. If it is equal to Shaggy
return NO
.
Otherwise return MAYBE_SO
.
Use the enum you defined above.
// ------------
// CODE HERE
// ------------
void main() {
Response pitbull = goodPerformer("Pitbull");
IO.println(pitbull);
Response billyJoel = goodPerformer("Billy Joel");
IO.println(billyJoel);
Response shaggy = goodPerformer("Shaggy");
IO.println(shaggy);
Response chappelRoan = goodPerformer("Chappell Roan");
IO.println(chappelRoan);
}
Challenge 3.
Make a method named transition
which takes in a StopLight
and returns the next light it will transition to.
For those who don't drive cars: red lights go to green, green lights go to yellow, and yellow lights go to red.
enum StopLight {
RED,
YELLOW,
GREEN
}
StopLight transition(StopLight current) {
// ------------
// CODE HERE
// ------------
}
void main() {
var light = StopLight.RED;
IO.println(light);
light = transition(light);
IO.println(light);
light = transition(light);
IO.println(light);
light = transition(light);
IO.println(light);
light = transition(light);
IO.println(light);
light = transition(light);
IO.println(light);
light = transition(light);
IO.println(light);
}
Strings II

If you haven't already, you will eventually realize that String
s are
one of the most common data types you will use.
As such, its worth it to keep revisiting the things you can do with them. Expect more sections like this the deeper you get.
lowercase
In English1 letters can be either lower-cased (a
, b
, c
) or upper-cased (A
, B
, C
).
If you have a String
which potentially contains upper-cased letters, you can get a new String
with everything
transformed into lower-case using the .toLowerCase()
method.
void main() {
String message = "Happy Valentines Day";
String lowerCased = message.toLowerCase();
IO.println(lowerCased);
}
This does not change the original String
in place. It just makes a new String
with all lower-case letters.
Other languages also have a notion of case. I am not a polyglot though, so I'm not qualified to talk about them.
UPPERCASE
Similarly, if you have a String
which potentially contains lower-cased letters, you can get a new String
with everything
transformed into upper-case using the .toUpperCase()
method.
void main() {
String message = "Happy Valentines Day";
String upperCased = message.toUpperCase();
IO.println(upperCased);
}
This does not change the original String
in place. It just makes a new String
with all upper-case letters.
Equality ignoring case
If you want to check if two String
s contain the same contents
but you do not care if those contents differ in the casing of letters, you can use the .equalsIgnoreCase
method.
void main() {
String historicalFigureOne = "St. Valentines";
String historicalFigureTwo = "st. valentines";
IO.println(
historicalFigureOne.equalsIgnoreCase(historicalFigureTwo)
);
}
This is different from the result the .equals
method will give you. That method will return false
if there
are any differences in the two String
s.
Check if empty
You can check if a String
is empty in a few ways.
The one you should already have been able to figure out1 is that you can get the .length
of a String
and see if that is zero.
void main() {
String textMessages = "";
IO.println(
textMessages.length() == 0
);
}
But another is to use the explicitly defined .isEmpty()
method.
void main() {
String textMessages = "";
IO.println(
textMessages.isEmpty()
);
}
This can be more convenient. Both to write, as it is fewer characters to type, and to read later on.
Again, no shame if not. I didn't exactly call attention to it.
Check if blank
You can check if a String
is blank by using the .isBlank
method.
The difference is that an empty String
has actually zero characters. A blank String
can have characters, so long as those characters are what we would consider whitespace.
That is, things like spaces and newlines.
void main() {
String brainSounds = """
""";
// false
IO.println(brainSounds.isEmpty());
// true
IO.println(brainSounds.isBlank());
}
Strip extra whitespace
If you have a String
which might contains some extra "trailing" whitespace or extra "leading"
whitespace, you can remove that by using the .strip
method.
This will give a new String
with both the leading and trailing whitespace removed.
void main() {
String message = " Happy Valentines Day. ";
IO.print(message.strip());
IO.println("|");
}
If you want to just remove the leading whitespace, you can use .stripLeading
.
void main() {
String message = " Happy Valentines Day. ";
IO.print(message.stripLeading());
IO.println("|");
}
And to remove only trailing whitespace, .stripTrailing
.
void main() {
String message = " Happy Valentines Day. ";
IO.print(message.stripTrailing());
IO.println("|");
}
All of these methods are useful when you get input from human beings. Humans are generally pretty bad at seeing if they hit the spacebar one too many times.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Write a method named isUpperCase
which tells you if a
given String
is already made of all upper-case characters.
Hint: One way to do this is to call .toUpperCase
and check
if the result is the same as the input you were given.
boolean isUpperCase(String s) {
// -----------
// CODE HERE
// -----------
}
void main() {
// true
IO.println(isUpperCase("ABC"));
// false
IO.println(isUpperCase("abc"));
// false
IO.println(isUpperCase("AbC"));
}
Challenge 2.
Do the same as above, but check if a given String
is
made of all lower case letters.
boolean isLowerCase(String s) {
// -----------
// CODE HERE
// -----------
}
void main() {
// false
IO.println(isLowerCase("ABC"));
// true
IO.println(isLowerCase("abc"));
// false
IO.println(isLowerCase("AbC"));
}
Challenge 3.
Add an instance method named scream
to the Muppet
class. This
should replace the name of the muppet with the name in upper case
letters + an exclamation point (!
) at the end.
class Muppet {
String name;
// -------------
// CODE HERE
// -------------
}
void main() {
var kermit = new Muppet();
kermit.name = "kermit";
// kermit
IO.println(kermit.name);
// KERMIT!
kermit.scream();
IO.println(kermit.name);
}
Challenge 4.
Write a method called echo
.
If echo
is given a non-blank String
, it should print You Said: <...>
where <...>
is the String
they
gave minus any leading or trailing whitespace.
If echo
is given a blank
String
, it should print You Didn't Say Anything
.
void echo(String s) {
// -------------
// CODE HERE
// -------------
}
void main() {
// You Said: Hello
echo("Hello");
// You Said: Hello
echo(" Hello ");
// You Didn't Say Anything
echo("");
// You Didn't Say Anything
echo(" ");
}
Exceptions

When you do something that Java doesn't know how to handle, like dividing a number by zero, your program will fail.
The way that this happens is that an "exception" is thrown.
Say that in "normal conditions" code will proceed top to bottom, line by line. In "exceptional conditions" code will no longer be able to proceed.
void main() {
int x = 5 / 0;
IO.println("Won't get here, an exception will occur");
}
throw
In order to throw an exception from your own code, you say throw
, new
, then the name
of the exception and ()
.
RuntimeException
is one of many kinds of exceptions, but you can make do with only
that in your own code for a bit.
void crashesOnFive(int x) {
if (x == 5) {
throw new RuntimeException();
}
}
void main() {
crashesOnFive(1);
IO.println("Made it to step 1");
crashesOnFive(5);
IO.println("Will not make it to step 2");
}
Messages
You can attach a message to an exception to give context to whoever sees the program crash as to why that happened.
You do this by putting a String
in the parentheses when throwing
your exception.
void crashesOnFive(int x) {
if (x == 5) {
throw new RuntimeException("5 is an evil number");
}
}
void main() {
crashesOnFive(1);
IO.println("Made it to step 1");
crashesOnFive(5);
IO.println("Will not make it to step 2");
}
Stack Traces
When you get an exception, it will contain what is known as a "stack trace."
If a method a
calls a method b
which in turn calls a method c
, that chain of
calls forms a "stack."
If the method at the bottom, c
, throws an exception that exception will contain
information about that entire "call stack."
void c() {
throw new RuntimeException();
}
void b() {
c();
}
void a() {
b();
}
void main() {
a();
}
Exception in thread "main" java.lang.RuntimeException
at Main.c(Main.java:2)
at Main.b(Main.java:6)
at Main.a(Main.java:10)
at Main.main(Main.java:14)
This makes exceptions somewhat offensive to the eyes, but is extremely useful if something goes wrong in a real program. You can see exactly what method had an issue and figure out where it was called from.
Since figuring out what went wrong is a detective game, every clue helps.
try/catch
If you know that some code might fail, and you have an idea of what to do if it does,
you can prevent an exception from crashing your program by catch
-ing it.
To do this, you write try
and put the code that might fail inside of {
and }
.
try {
mightFail();
}
Then you write catch
and in parentheses the kind of exception you want to handle as well as a variable name.1
try {
mightFail();
} catch (RuntimeException e) {
}
And inside the catch block you can write code to run when such an exception occurs.
void doThing(int x) {
if (x == 0) {
throw new RuntimeException("Cannot do something zero times");
}
}
void main() {
int x = 0;
try {
doThing(x);
} catch (RuntimeException e) {
IO.println("Something went wrong doing a thing.");
}
}
Just as you cannot have an else
without an if
, you cannot have a catch
without a try
.
void main() {
catch (RuntimeException e) {
IO.println("Hello");
}
}
Nor can you have a try
without a catch
.2
void main() {
try {
IO.println("Hello");
}
}
Generally you will just use e
for this. Even if you don't call any instance methods on the exception, you still need to give a name for it.
Technically you can have a try
without a catch
, but only when using another feature of
Java you haven't been shown yet. It will make sense when the time comes.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Write a method named arise
which accepts a String
parameter
representing someone's name and prints Awake <NAME>
.
If this function is given an empty string throw a RuntimeException
.
// ------------
// CODE HERE
// ------------
void main() {
arise("Lion El'Jonson");
arise("Roboute Guilliman");
arise("");
}
Challenge 2.
Starting with the code you wrote above, make the thrown runtime exception include a message saying why it was thrown ("given an empty string" or something like that.)
// ------------
// CODE HERE
// ------------
void main() {
arise("Lion El'Jonson");
arise("Roboute Guilliman");
arise("");
}
Challenge 3.
The following code is written in an intentionally confusing way. Instead of reading the code and trying to figure it out that way, run the code and read the stack trace to figure out which method originally throws an exception.
void a(int x) {
if (x == 0) {
throw new RuntimeException();
}
else {
b(x / 2);
}
}
void b(int x) {
if (x == 0) {
throw new RuntimeException();
}
else {
c(x * 3 + 5);
}
}
void c(int x) {
if (x == 0) {
throw new RuntimeException();
}
else {
d(x / 4);
}
}
void d(int x) {
if (x == 0) {
throw new RuntimeException();
}
else {
e(x / 3);
}
}
void e(int x) {
if (x == 0) {
throw new RuntimeException();
}
else {
a(x / 10);
}
}
void main() {
a(1215135236);
}
Challenge 4.
Write a method named command
which takes in a SpaceMarine
.
If the space marine is corrupted throw a RuntimeException
.
Otherwise print out their name.
class SpaceMarine {
boolean corrupted;
String name;
}
// ---------------------
// CODE HERE
// ---------------------
void main() {
SpaceMarine titus = new SpaceMarine();
titus.corrupted = false;
titus.name = "Demetrian Titus";
command(titus);
SpaceMarine imurah = new SpaceMarine();
imurah.corrupted = true;
imurah.name = "Imurah";
command(imurah);
}
Challenge 5.
Alter your code above by adding a new method named safeCommand
. It should
call command
in a try/catch
block. If a RuntimeException
is thrown
it should print Unable to command
.
class SpaceMarine {
boolean corrupted;
String name;
}
// ---------------------
// CODE HERE
// ---------------------
void main() {
SpaceMarine titus = new SpaceMarine();
titus.corrupted = false;
titus.name = "Demetrian Titus";
command(titus);
safeCommand(titus);
SpaceMarine imurah = new SpaceMarine();
imurah.corrupted = true;
imurah.name = "Imurah";
safeCommand(imurah);
}
Switch

if
and else
let you branch logic based on whether any arbitrary
expression that evaluates to a boolean.
This is powerful because it lets you write logic as complicated as you need to.
if (isLeapYear && !bloodMoon && (age > 30 || catName.equals("fred"))) {
startRitual();
}
But it can be burdensome if all you are doing is checking if some variable has a particular value.
void main() {
if (food.equals("apple")) {
IO.println("Red");
}
else if (name.equals("grape")) {
IO.println("Purple");
}
else if (food.equals("orange")) {
IO.println("Orange");
}
else {
IO.println("Other");
}
}
For these situations, you can use a switch
.
switch (fruit) {
case "apple" -> {
IO.println("Red");
}
case "grape" -> {
IO.println("Purple");
}
case "orange" -> {
IO.println("Orange");
}
default -> {
IO.println("Other");
}
}
Case and Default
For a switch statement you write switch
followed by an expression in parentheses
and a list of cases in curly-braces.
Each case consists of a "case label" (case
followed by a literal value), an arrow (->
), and a
body to execute when the value given to the switch matches the value declared in the case.
The final case label can just be the word default
. This will match if none of the previous cases did
and gives you a place to put "default" behavior.
void sayColor(String fruit) {
switch (fruit) {
case "apple" -> {
IO.println("Red");
}
case "grape" -> {
IO.println("Purple");
}
case "orange" -> {
IO.println("Orange");
}
default -> {
IO.println("Other");
}
}
}
void main() {
sayColor("grape");
}
Strings
As already shown, you can use a case to match String
values.
void main() {
String veggie = "cucumber";
switch (veggie) {
case "cabbage" -> {
IO.println("A cabbage");
}
case "brussel sprout" -> {
IO.println("A brussel sprout");
}
case "cucumber" -> {
IO.println("A cucumber");
}
default -> {
IO.println("Other");
}
}
}
ints
You can also use int
s with switches.
void main() {
int year = 2024;
switch (year) {
case 2023 -> {
IO.println("The Chiefs");
}
case 2024 -> {
IO.println("The Chiefs");
}
default -> {
IO.println("I don't know");
}
}
}
Enums
Switches really shine with enums.
For each case label you need to use the name of the variant, not prefixed with the name of the enum.
enum StopLight {
RED,
YELLOW,
GREEN
}
void main() {
StopLight light = StopLight.GREEN;
switch (light) {
case RED -> {
IO.println("Stop!");
}
case YELLOW -> {
IO.println("Speed up, coward!");
}
case GREEN -> {
IO.println("Go!");
}
}
}
Omitted Default
If you have no logic to put in it, you can omit the default
case from a switch
. This is the same conceptually as omitting a final else
from a chain of if
s and else if
s.
void react(String fruit) {
switch (fruit) {
case "apple" -> {
IO.println("WOW");
}
case "orange" -> {
IO.println("Zoinks!");
}
case "grape" -> {
IO.println("Zoopers!");
}
}
}
void main() {
react("passionfruit"); // 🤷
}
Combining Cases
If you have multiple constant values that should be handled in the same way, you can combine case labels by separating their values with a comma.
String scientificName(String vegetable) {
switch (vegetable) {
case "apple" -> {
return "Malus pumila";
}
case "cabbage", "brussel sprouts", "kale", "cauliflower" -> {
// Look it up. Kinda wild.
return "Brassica oleracea";
}
default -> {
return "unknown";
}
}
}
void main() {
IO.println(scientificName("cabbage"));
}
null
When a switch is given a null
value a NullPointerException
will be thrown immediately.
void eat(String food) {
switch (food) {
case "dog food" -> {
IO.println("Crunch");
}
case "cat food" -> {
IO.println("Slorp");
}
default -> {
IO.println("Other food");
}
}
}
void main() {
eat(null);
}
The only exception to this is when a switch has an explcit case null
in its list of
case labels. default
will not suffice.
void eat(String food) {
switch (food) {
case "dog food" -> {
IO.println("Crunch");
}
case "cat food" -> {
IO.println("Slorp");
}
case null -> {
IO.println("No food");
}
default -> {
IO.println("Other food");
}
}
}
void main() {
eat(null);
}
A default branch and a null case can be combined by separating them with a comma.
void eat(String food) {
switch (food) {
case "dog food" -> {
IO.println("Crunch");
}
case "cat food" -> {
IO.println("Slorp");
}
case null, default -> {
IO.println("Other food");
}
}
}
void main() {
eat(null);
}
Exhaustiveness
Switches are considered exhaustive if they have a case label for every possible value of the type of thing they are switching over.
This is important if you try to return from a function within a switch. Since you need to return a value from every possible branch the function may take, you either need to add an extra return after the switch expression or have an exhaustive switch.
String describe(int number) {
switch (number) {
case 1 -> {
return "loneliest";
}
case 2 -> {
return "loneliest since 1";
}
}
// Since no default, need a return here
return "Its a number";
}
When you have something like an enum you might think you don't need a default
case
because you can handle every variant explicitly.1
enum Bird {
TURKEY,
EAGLE,
WOODPECKER
}
boolean isScary(Bird bird) {
switch (bird) {
case TURKEY -> {
return true;
}
case EAGLE -> {
return true;
}
case WOODPECKER -> {
return false;
}
}
}
This is, unfortunately, not the case for a switch statement.
You either need a default
case or to have an explicit case null
to handle the
possibility that the enum value is null
.2
enum Bird {
TURKEY,
EAGLE,
WOODPECKER
}
boolean isScary(Bird bird) {
switch (bird) {
case TURKEY -> {
return true;
}
case EAGLE -> {
return true;
}
case WOODPECKER -> {
return false;
}
// Need to handle the possibility of null
// or give a "default ->"
case null -> {
// You might want to return a value or just crash
return false;
}
}
}
This is sometimes the case! It is really just this specific form of switch that has this restriction.
Remember at the very start when I said I was going to lie to you? This is one of those lies. The real reason for this restriction has to do with how Java compiles switch statements and a concept called "separate compilation." Basically
even though we know you covered all the enum variants, Java still makes you account for if a new enum variant was added later on. It doesn't do this for all forms of switch
though.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Write a method named isSorcerer
. If the method is given
any of "yuji"
or "gojo"
return true
. Otherwise return false
.
Start by writing this with if
and else
. Then alter the code so it uses
switch
instead.
boolean isSorcerer(String name) {
// CODE HERE
}
void main() {
IO.println(
isSorcerer("yuji")
);
IO.println(
isSorcerer("gojo")
);
IO.println(
isSorcerer("yugi") // Wrong series
);
}
Challenge 2.
Same basic challenge as above, but write a method named didRedSoxWin
which takes an int
representing a year.
Return true
if that is a year the Boston Red Sox won a world series.
false
otherwise. This time start off by using a switch.
boolean didRedSoxWin(int year) {
// CODE HERE
}
void main() {
IO.println(
"2004: " + didRedSoxWin(2004)
);
IO.println(
"1998: " + didRedSoxWin(1998)
);
IO.println(
"2013: " + didRedSoxWin(2013)
);
IO.println(
"1903: " + didRedSoxWin(1903)
);
}
Challenge 3.
Make a method named transition
which takes in a StopLight
and returns the next light it will transition to.
For those who don't drive cars: red lights go to green, green lights go to yellow, and yellow lights go to red.
This is a duplicate of a challenge from a previous section. This time
do it using a switch
.
enum StopLight {
RED,
YELLOW,
GREEN
}
StopLight transition(StopLight current) {
// ------------
// CODE HERE
// ------------
}
void main() {
var light = StopLight.RED;
IO.println(light);
light = transition(light);
IO.println(light);
light = transition(light);
IO.println(light);
light = transition(light);
IO.println(light);
light = transition(light);
IO.println(light);
light = transition(light);
IO.println(light);
light = transition(light);
IO.println(light);
}
Challenge 4.
In reality a StopLight
can also be broken and not function
at all. Alter transition
so it accounts for a new BROKEN
state which transitions to itself. (BROKEN
goes to BROKEN
).
enum StopLight {
RED,
YELLOW,
GREEN,
BROKEN
}
StopLight transition(StopLight current) {
// ------------
// CODE HERE
// ------------
}
void main() {
var light = StopLight.RED;
IO.println(light);
light = transition(light);
IO.println(light);
light = transition(light);
IO.println(light);
light = transition(light);
IO.println(light);
light = transition(light);
IO.println(light);
light = transition(light);
IO.println(light);
light = transition(light);
IO.println(light);
}
Challenge 5.
Given a type of bear, return the correct course of action if you run into one in the wild and it attacks you.
If the bear is null
, return null
as the action to take.
If you don't know what to do when you run into a bear, look it up. Use switch
first then try writing the same logic using if
and else
.
enum Bear {
POLAR,
BROWN,
BLACK,
PANDA,
KOALA
}
enum Action {
LAY_DOWN,
FIGHT_BACK,
RUN_AWAY,
YEET
}
Action inCaseOfBearAttack(Bear bear) {
// CODE HERE
}
void main() {
IO.println(
inCaseOfBearAttack(Bear.POLAR)
);
IO.println(
inCaseOfBearAttack(Bear.BROWN)
);
IO.println(
inCaseOfBearAttack(Bear.BLACK)
);
IO.println(
inCaseOfBearAttack(Bear.PANDA)
);
IO.println(
inCaseOfBearAttack(Bear.KOALA)
);
IO.println(
inCaseOfBearAttack(null)
);
}
Standard Input II

If you are using a program and you type something slightly wrong it does not feel good if the program then immediately crashes.
As such it often makes sense to not only request your users type something and then interpret what they typed, but to also give feedback and perhaps ask them again.
Reprompting
If you ask someone a yes or no question and they respond with "huh?" you might want to ask them again.
This is a good use case for loops. You ask a question and, if the answer you get is acceptable, you proceed as normal. If it is not then you loop back and ask again.
void main() {
while (true) {
String response = IO.readln("Answer me: yes or no");
if (response.equals("yes")) {
IO.println("okay then!");
}
else if (response.equals("no")) {
IO.println("also fine!");
}
else {
IO.println("Not a valid response");
// Will go back to the top of the loop
continue;
}
// If a "continue" is not hit, exit the loop
break;
}
}
If the program would normally crash on unexpected input you can use try
and catch
to recover
from this and reprompt the user.
This is applicable to Integer.parseInt
, Double.parseDouble
, and any other method that would
throw an exception on unexpected inputs.
void main() {
int number;
while (true) {
String response = IO.readln("What is your least favorite number? ");
try {
// Here Integer.parseInt might throw an exception,
number = Integer.parseInt(response);
} catch (RuntimeException e) {
// If that happens, go up to the top and reprompt
continue;
}
// If a "continue" is not hit, exit the loop
break;
}
IO.println("Your least favorite number is " + number);
}
Enums
Just as you might want to interpret what someone typed as an int
or double
there are times you will want to interpret input as an enum
value.
To do this you can write .valueOf
after the name of the enum. So for a StopLight
enum StopLight.valueOf
can interpret a String
as a StopLight
.
enum StopLight {
RED,
YELLOW,
GREEN
}
void main() {
String colorString = IO.readln("What color was the stoplight? ");
StopLight color = StopLight.valueOf(colorString);
IO.println("The stop light was " + color);
}
This will throw an exception if the String
does not match, so you can reprompt using the same try
/catch
structure as you would use with Integer.parseInt
.
enum StopLight {
RED,
YELLOW,
GREEN
}
void main() {
StopLight color;
while (true) {
String colorString = IO.readln("What color was the stoplight? ");
try {
color = StopLight.valueOf(colorString);
} catch (RuntimeException e) {
continue;
}
break;
}
IO.println("The stop light was " + color);
}
Unfortunately, this only works if what they typed is exactly the name of an enum variant. So
in the example above they need to type RED
in all capital letters.
To have a different mapping of strings to enum values you need to write code yourself.
enum StopLight {
RED,
YELLOW,
GREEN
}
StopLight stringToStopLight(String s) {
if (s.equals("r")) {
return StopLight.RED;
}
else if (s.equals("y")) {
return StopLight.YELLOW;
}
else if (s.equals("g")) {
return StopLight.GREEN;
}
else {
throw new RuntimeException("Unknown color.")
}
}
void main() {
String colorString = IO.readln("What color was the stoplight? ");
StopLight color = stringToStopLight(colorString);
IO.println("The stop light was " + color);
}
Delayed Assignment
When you have variables declared inside of that loop cannot be seen from the outside. This poses a problem when you are asking someone a question in a loop but want their response to be visible later on in the program.
One strategy for this is to declare any response-holding variables outside the loop.
The problem with this is that Java isn't smart enough to know that you always initialize those variables.
void main() {
String name;
while (true) {
name = IO.readln("What is your name? ");
if (name.isBlank()) {
IO.println("Name cannot be blank!");
continue;
}
break;
}
IO.println("Hello " + name);
}
To get around this you can either give an explicit default value.
void main() {
String name = null;
while (true) {
name = IO.readln("What is your name? ");
if (name.isBlank()) {
IO.println("Name cannot be blank!");
continue;
}
break;
}
IO.println("Hello " + name);
}
Or you can use the do
version of a while loop.
In this context our old friend delayed assignment becomes an option again. This is because Java is smart enough
to see that the code in the loop will run at least once.
void main() {
String name;
do {
name = IO.readln("What is your name? ");
if (name.isBlank()) {
IO.println("Name cannot be blank!");
continue;
}
break;
} while (true);
IO.println("Hello " + name);
}
Leniency
It can make sense to be lenient with your users when interpreting their input.1
This means accounting for common mistakes people make like having extra spaces or capitalizing things incorrectly.
For this purpose, methods like strip
and equalsIgnoreCase
are useful.
void main() {
while (true) {
String response = IO.readln("Answer me: yes or no").strip();
if (response.equalsIgnoreCase("yes")) {
IO.println("aight");
}
else if (response.equalsIgnoreCase("no")) {
IO.println("cool");
}
else {
IO.println("try again");
continue;
}
break;
}
}
People are idiots. Their fingers are fat and their wills are weak.
Aggregating Data
If you ask someone multiple questions you likely will get multiple variables worth of information.
void main() {
String firstName = IO.readln("What is your first name? ");
String lastName = IO.readln("What is your last name? ");
IO.println("Hello " + firstName + " " + lastName + ".");
}
This is fine and dandy so long as you immediately use those variables. But once you add in reprompting logic code can get pretty lengthy.
void main() {
String firstName;
do {
firstName = IO.readln("What is your first name? ");
if (firstName.isBlank()) {
IO.println("First name cannot be blank.");
}
else {
break;
}
} while (true);
String lastName;
do {
lastName = IO.readln("What is your first name? ");
if (lastName.isBlank()) {
IO.println("First name cannot be blank.");
}
else {
break;
}
} while (true);
IO.println("Hello " + firstName + " " + lastName + ".");
}
And once code gets lengthy it is sometimes useful to separate it into smaller functions.
I mention all this as a reminder that when you want to return multiple values from a function you can use a class.1
class Person {
String firstName;
String lastName;
}
Person askForName() {
String firstName;
do {
firstName = IO.readln("What is your first name? ");
if (firstName.isBlank()) {
IO.println("First name cannot be blank.");
}
else {
break;
}
} while (true);
String lastName;
do {
lastName = IO.readln("What is your first name? ");
if (lastName.isBlank()) {
IO.println("First name cannot be blank.");
}
else {
break;
}
} while (true);
var person = new Person();
person.firstName = firstName;
person.lastName = lastName;
return person;
}
void main() {
Person person = askForName();
IO.println("Hello " + person.firstName + " " + person.lastName + ".");
}
When you make a class just to make objects which transfer data between different parts of your program we sometimes call those DTOs - data transfer objects. You will learn better ways to make DTOs in the future.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1
Write a program that asks which Twilight character Bella should have ended up with.
It should keep asking this question until the user types the magic words "i do not care"
void main() {
// CODE HERE
}
Challenge 2
Update the program above to also accept "I DO NOT CARE", "I Do Not Care", and any other mix of capital and lower-case letters
Challenge 3
Write a method called askForBirthday
that asks for the year, month, and day that someone was born.
This method should return all three pieces of information with a Birthday
class.
class Birthday {
// CODE HERE
}
Birthday askForBirthday() {
// CODE ALSO HERE
}
void main() {
Birthday b = askForBirthday();
IO.println(b.year + "-" + b.month + "-" + b.day)
}
Challenge 4
Update the program above to reprompt the user if they enter any nonsensical information for the year, month, or day. This includes negative numbers for the year, month, or day.
Challenge 5
Update the birthday program to represent the month with an enum named Month
.
Accept both the name of the month with any capitalization and the number of the month (starting with 1
for January) as valid ways to specify the month.
Make sure to still reprompt on unexpected inputs.
enum Month {
JANUARY,
FEBRUARY,
MARCH,
APRIL,
MAY,
JUNE,
JULY,
AUGUST,
SEPTEMBER,
NOVEMBER,
DECEMBER
}
class Birthday {
// CODE HERE
}
Birthday askForBirthday() {
// CODE ALSO HERE
}
void main() {
Birthday b = askForBirthday();
IO.println(b.year + "-" + b.month + "-" + b.day)
}
Constructors

When defining a class, you are allowed to make a special kind of method called a constructor.
A constructor runs before any code gets access to an instance of the class. You use it to set up the "initial state" for an object.
Declaration
To declare a constructor you make a method where you don't write any return type and whose name is the name of the class.
class Muppet {
Muppet() {
}
}
Inside of the constructor's body, you can set the initial values for any fields in the class.
class Muppet {
boolean talented;
Muppet() {
talented = true;
}
}
void main() {
Muppet gonzo = new Muppet();
IO.println(gonzo.talented);
}
The Default Constructor
If you don't declare a constructor, it will be the same as declaring an "empty" constructor. All fields will be initialized to their default values.
class Muppet {
String name;
boolean talented;
Muppet() {
}
}
void main() {
Muppet gonzo = new Muppet();
// null
IO.println(gonzo.name);
// false
IO.println(gonzo.talented);
}
Arguments
If you declare a constructor which takes arguments, you can use those arguments to give initial values to fields.1
You need to pass the arguments in the ()
in the new
expression, just like any other method invocation.
class Muppet {
String name;
Muppet(String name) {
this.name = name;
}
}
void main() {
Muppet gonzo = new Muppet("Gonzo");
// "Gonzo"
IO.println(gonzo.name);
}
When you declare a constructor that takes arguments, the default constructor will no longer be available.
class Muppet {
String name;
Muppet(String name) {
this.name = name;
}
}
void main() {
// Need to provide a name now
Muppet gonzo = new Muppet();
}
Using this.
for disambiguation comes in handy here, since often the names of arguments
will be the same as the fields you want to populate with them.
Final Fields
If you declare a field as final
, its value cannot be changed after an instance of a class is made.
You are required to explicitly initialize final
fields in the constructor.
class Muppet {
final String name;
Muppet(String name) {
// Without this, it wouldn't work
this.name = name;
}
}
void main() {
Muppet gonzo = new Muppet("Gonzo");
IO.println(gonzo.name);
// Cannot update the .name field later
// gonzo.name = "Gonzo, the great";
}
You can also do this directly when declaring the field.1
class Muppet {
// Aren't they all though?
final boolean talented = true;
}
void main() {
Muppet gonzo = new Muppet();
IO.println(gonzo.talented);
}
This is primarily useful for "constant" values. You will need these, but having constants attached to instances is a bit unique and won't happen that often.
Invariants
Just like any other method, constructors can throw exceptions.
You can use this fact to establish what we call an "invariant."
Say we have a final
age field and that the constructor for a class throws an exception
if a given age is negative.
class Muppet {
final String name;
final int age;
Muppet(String name, int age) {
this.name = name;
if (age < 0) {
throw new RuntimeException("Age cannot be negative");
}
this.age = age;
}
}
void main() {
Muppet bigBird = new Muppet("Big Bird", 6);
IO.println(
bigBird.name + " is " + bigBird.age + " years old."
);
}
In every other part of our program now we can rely on age being a non-negative number. That is a property of instances that will not change.1
This is a lot more useful than it seems at first, stay tuned.
It will not change. It will not vary, it is in-variant. Get it?
Overloads
Just like normal methods, you can have multiple constructors so long as each constructor takes different types or different numbers of arguments.
class Muppet {
String name;
boolean talented;
Muppet(String name) {
this.name = name;
this.talented = true;
}
Muppet(String name, boolean talented) {
this.name = name;
this.talented = talented;
}
}
When you call a constructor, Java will know what code to run because it knows the types of and number of arguments you are passing.
class Muppet {
String name;
boolean talented;
Muppet(String name) {
this.name = name;
this.talented = true;
}
Muppet(String name, boolean talented) {
this.name = name;
this.talented = talented;
}
}
void announce(Muppet muppet) {
IO.print(muppet.name);
if (muppet.talented) {
IO.print(" is ");
}
else {
IO.print(" is not ");
}
IO.println("talented.");
}
void main() {
Muppet fozzie = new Muppet("Fozzie");
announce(fozzie);
// Always a critic...
Muppet waldorf = new Muppet("Waldorf", false);
announce(waldorf);
}
Delegation
It is common for overloaded constructors to be "shortcuts" for eachother.
That is, if one overload takes two arguments another will take just one argument and do the same logic as the first but fill in a default value for the un-provided value.
class Muppet {
final String name;
final boolean talented;
Muppet(String name) {
this.name = name;
this.talented = true;
}
Muppet(String name, boolean talented) {
this.name = name;
this.talented = talented;
}
}
A downside of this is that any validation logic done in one constructor needs to be copy pasted to the other.
class Muppet {
final String name;
final boolean talented;
Muppet(String name) {
if (name.length() == 0) {
throw new RuntimeException("Cannot have blank name");
}
this.name = name;
this.talented = true;
}
Muppet(String name, boolean talented) {
if (name.length() == 0) {
throw new RuntimeException("Cannot have blank name");
}
this.name = name;
this.talented = talented;
}
}
To avoid this situation, you can have one constructor "delegate" to another.
To do this you write this
in one constructor and call it as if it were a method.
This will run the logic of the constructor which matches the values passed in.
class Muppet {
final String name;
final boolean talented;
Muppet(String name) {
// Will use the other constructor, but with false filled in
// as a default value
this(name, false);
}
Muppet(String name, boolean talented) {
// This logic now only needs to live in one place.
if (name.length() == 0) {
throw new RuntimeException("Cannot have blank name");
}
this.name = name;
this.talented = talented;
}
}
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Write a Shoe
class. Give each shoe a name
field and a quality
field.
Have these fields be initialized inside of a constructor.
enum Quality {
SUPA_FINE,
FINE,
SUB_FINE
}
// CODE HERE
void main() {
Shoe nike = new Shoe("Nikes", Quality.SUB_FINE);
IO.println(
"SHOE: " + nike.name + ", " + nike.quality
);
Shoe moccasin = new Shoe("Moccasins", Quality.SUPA_FINE);
IO.println(
"SHOE: " + moccasin.name + ", " + moccasin.quality
);
}
Challenge 2.
Add a new price
field to the Shoe
class you wrote above.
Add a new constructor which accepts a third argument to set the price
.
Keep the old two argument constructor around as well. When that one is
used set price
to null
.
Hint: Use Double
to represent a nullable price.
enum Quality {
SUPA_FINE,
FINE,
SUB_FINE
}
// CODE HERE
void main() {
Shoe jays = new Shoe("Air Jordans", Quality.FINE, 130.0);
IO.println(
"SHOE: " + jays.name + ", " + jays.quality + ", $" + jays.price
);
Shoe nike = new Shoe("Nikes", Quality.SUB_FINE, 25);
IO.println(
"SHOE: " + nike.name + ", " + nike.quality + ", $" + jays.price
);
Shoe moccasin = new Shoe("Moccasins", Quality.SUPA_FINE);
IO.println(
"SHOE: " + moccasin.name + ", " + moccasin.quality + ", $" + jays.price
);
}
Challenge 3.
Alter the Shoe
class so that the price
field is final.
Alter its constructors so that if they are given a negative
value they throw an exception instead of finishing normally.
Keep in mind that while null
is allowed (you might not know the price)
a negative number wouldn't be. Nobody is paying you to take their shoes.1
enum Quality {
SUPA_FINE,
FINE,
SUB_FINE
}
// CODE HERE
void main() {
Shoe jays = new Shoe("Air Jordans", Quality.FINE, 130.0);
IO.println(
"SHOE: " + jays.name + ", " + jays.quality + ", $" + jays.price
);
Shoe nike = new Shoe("Nikes", Quality.SUB_FINE, 25);
IO.println(
"SHOE: " + nike.name + ", " + nike.quality + ", $" + jays.price
);
Shoe moccasin = new Shoe("Moccasins", Quality.SUPA_FINE);
IO.println(
"SHOE: " + moccasin.name + ", " + moccasin.quality + ", $" + jays.price
);
Shoe shouldCrash = new Shoe("Base Ball Cleats", Quality.SUPA_FINE, -10);
}
If they do, you are going to dig holes in the desert in search of treasure that rightfully belongs to you.
Challenge 4.
If you haven't yet, rewrite your Shoe
constructors so only one of them actually sets fields
and the other just delegates to that one.
enum Quality {
SUPA_FINE,
FINE,
SUB_FINE
}
// CODE HERE
void main() {
Shoe jays = new Shoe("Air Jordans", Quality.FINE, 130.0);
IO.println(
"SHOE: " + jays.name + ", " + jays.quality + ", $" + jays.price
);
Shoe nike = new Shoe("Nikes", Quality.SUB_FINE, 25);
IO.println(
"SHOE: " + nike.name + ", " + nike.quality + ", $" + jays.price
);
Shoe moccasin = new Shoe("Moccasins", Quality.SUPA_FINE);
IO.println(
"SHOE: " + moccasin.name + ", " + moccasin.quality + ", $" + jays.price
);
Shoe shouldCrash = new Shoe("Base Ball Cleats", Quality.SUPA_FINE, -10);
}
Global Fields

You can make field declarations outside of a class. This makes these fields "global" to the program.
We call things global when they are available at every point in the "world" of our program.1
int number = 0;
void main() {
IO.println(number);
number++;
IO.println(number);
}
This explanation is I think a correct description of what it means to be global, but what I am showing won't really hold up as a global thing the more you learn.
Default Values
Just like regular fields in classes, global fields will initialize to their default value unless explicitly initialized.
int x;
int y = 5;
void main() {
IO.println(x);
IO.println(y);
}
Final Fields
And just like regular final fields, you always need to give an initial value to global final fields.
final int x = 0;
void main() {
IO.println(x);
}
Final fields of course cannot be changed.
Field Access
You can access global fields by writing their name. This works from within top level methods as well as instance methods of classes.1
final String monster = "Dracula";
class Mash {
void itWasThe() {
IO.println(monster + " and his son");
}
}
void main() {
IO.println(monster + " was there");
Mash mash = new Mash();
mash.itWasThe();
}
This is so convenient it is actually going to be a bummer once you learn what is going on here and can't do it for most of your programs anymore.
Inferred Types
Just like other field declarations, var
cannot be used
for inferring the type of a global field. You need to explicitly write out the type.
var x = 5;
void main() {
IO.println(x);
}
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1
Write a method named anothaOne
. Each time this method is
called it should return a number one larger than the last time
it was called.
anothaOne
should take no arguments.
// CODE HERE
int anothaOne() {
// CODE HERE
}
void main() {
IO.println(anothaOne()); // 1
IO.println(anothaOne()); // 2
IO.println(anothaOne()); // 3
IO.println(anothaOne()); // 4
IO.println(anothaOne()); // 5
}
Challenge 2
Make a class named DJKhaled
. If you ask DJ Khaled to produceMusic
some lyrics should be printed out but also anothaOne
should be called at least 7 times.1
// CODE FROM LAST SECTION
class DJKhaled {
void produceMusic() {
// CODE HERE
}
}
void main() {
IO.println(anothaOne()); // 1
var dj = new DJKhaled();
dj.produceMusic();
IO.println(anothaOne()); // 8+, at least
}
Challenge 3
Make a method named nextLyric
. It should take no arguments and return a String
.
Each time it is called it should return a subsequent line from Kendrick Lamar's 2025 Superbowl
Halftime Show.
You should be able to find a transcription of the lyrics here.
Once all the lyrics are exhausted nextLyric
should start returning null
.
// CODE HERE
String nextLyric() {
// CODE HERE
}
void main() {
for (
String lyric = nextLyric();
lyric != null;
lyric = nextLyric()
) {
IO.println(lyric);
}
}
We are just going to have fun with the "anotha one" meme here, lets not think too hard about DJ Khaled's relationship with Drake.
Code is Read more than Written
Now that you more or less can write a full program, it won't be long before you write a program of "signficant size."
This makes it a good time to start drip feeding some of the higher level concepts around software development.
The first of these is a statement: Code is read more then it is written.
Meaning
While I'm sure there are some statistics behind this1, its generally accepted that the majority of a programmer's time is taken up reading existing code rather than writing new code.
This should make sense intuitively. If you get hired at Microsoft, chances are you won't be given a blank slate to make a brand new thing. Often you will be thrust into existing codebases. To make a change to an existing codebase, you need to understand the exisiting code.
It gets to the point where you might spend a whole day reading a dozen or so files only to make a 6 line change to one of them.
Software Development is still a relatively new field. Some things we take for granted may end up not being true after all. It doesn't help that its also pretty poorly researched. Keep an eye out for that.
Implications
This means that, sometimes, it makes sense to write code in a way that is "harder" than is absolutely needed.
That's a bit of a fuzzy statement, but on the more obvious side it means doing things like spending extra time on code formatting, variable naming, function contracts, writing comments, etc.
All of those things make it easier to read that code later on.
Information Density
An important related concept is information density.
Yes, you can do things like add a comment on every line of code. This does mean that a later reader will have context on exactly what you were thinking as you were writing the code.1
The downside is that there can be way more "extra" information than any educated person needs to understand the code. That extra information takes up space and spreads out the "important" information.
Put another way, if something has a high information density it can be hard to read.
M.M.A.T.B.C
Same as if it is has too low an information density.
Dearest sir, it would behoove you to meetest mineself posthaste after the school hours. I would perchance suggest that the location be betwixt the basketball courts.
You want an information density that is "just right"
Meet me at the basketball court
There is a whole sect of folks who write their programs as actual books. Look up "Literate Programming" to dive deeper in to that.
Audience
Consider this opening from an astrophysics paper I picked at random.1
The dearth of planets with sizes around 1.8 R⊕ is a key demographic feature discovered by the Kepler mission. Two theories have emerged as potential explanations for this valley: photoevaporation and core-powered mass-loss. However, Rogers et al. (2021) shows that differentiating between the two theories is possible using the three-dimensional parameter space of planet radius, incident flux, and stellar mass.
If you aren't an astrophysicist, this probably requires some explanation. If you are an astrophysicist, you probably understood that without issue.
Whenever anyone writes anything, it is important to consider the audience you are writing to. This applies to code just as much as to any other form of writing.
This is why shorthands like for (int i = 0; i < length; i++)
are generally considered okay even though
in most other situations i
is pretty non-descript. Within the audience of programmers it is a known idiom.
Its also why explaining every single line of code with comments is generally not okay. Programmers know what code is, you don't need to baby them through how loops work.2
https://arxiv.org/abs/2302.00009
No offense intended if you are still having trouble with loops. Its common, you will eventually get past it.
Practice
If you want to do programming as your job or as part of your job, and you want to be competent, it isn't enough to just practice writing code. You also need to practice reading.
There are a lot of ways to go about that, but an easy one is to read the code of your classmates or of strangers on the internet.
Read their code, try to understand all the (maybe wacky!) choices they made while writing it, and try to change it up a little bit.
This will help you get an intuitive sense for what you like in a codebase as well as what you do not. It will also, more so than the early struggles you go through when learning to write, help you understand if you would actually like to do this for work.
Hardware

That was a long stretch of Java, so I think you've earned a bit of a detour into general computing knowledge.
I won't be offended if you skim or skip these sections. Depending on who you are, this might be a bit obvious.
First lets cover hardware. The actual physical components that make up your computer.
CPU
At the core of every computer is a CPU.
This stands for "central processing unit." This is the rock that wizards taught how to think.
If you send the right electrical signals into a CPU it can do all sorts of math and it can do it quickly.
Inside a computer this looks like a little metal square.
RAM
CPUs can do math real fast, but they can't remember much stuff.1
This is what RAM is for. RAM stands for "random access memory." The random access part just means that a CPU can ask for any bit of info at any time.
Most computers nowadays have an even number of RAM sticks. This is for interesting reasons I don't fully understand.
In 8th grade I had someone physically hand me a piece of paper with their name and fun facts about them because I had kept forgetting their name the entire year. I don't know what Sam is up to now, but I remember that she likes pistachio ice cream.
Hard Drives
RAM sticks have a few issues in the memory department.
First, they can only hold around 8-64 gigabytes of info. Any more than that and they can get pretty slow.1
Second, when you turn off the computer they lose anything they are storing. This is a problem when you are writing an essay and need to turn off the computer half way through.
Hard drives are like RAM in that they remember stuff. The difference is that hard drives can store a lot more of it (terrabytes) and the data isn't lost when power is turned off.
The key word here is "latency". When the CPU needs info in RAM it needs to wait for RAM to find it. It takes longer to find something in a bigger pile of stuff, no matter how smart you are about it.
Motherboard
All of these parts, and the parts I didn't mention, are connected to a big circuit board called a "motherboard."
The motherboard connects the CPU, RAM, and any hard drive(s) such that they can "talk" to eachother. The details of how it does this aren't super important, so just imagine that a tiny wizard lives inside it.
Operating Systems
The exact electrical signals you need to send to make a computer "work" are, obviously, a bit complicated.
This is where operating systems come in. Operating systems are programs that "operate" all the parts that make up your computer.
Personal Computers
For "personal computers", which I am defining as the sort of computers that have a keyboard and a screen, the most common operating system is Windows. The second most common is Mac OS.
These are both produced by massive corporations in order to sell pre-installed on computers. As such, they give you a graphical interface that you navigate with a mouse/touchpad and keyboard. This is what people have found comfortable and what most are used to.
To write code and do other programmer things you have to install programs but once you know what you need to install its pretty easy.
Servers
For servers, the kind of machines you run in a datacenter to host websites, the most common operating system is Linux.
Because so many jobs are writing websites to host on servers, you will eventually need to learn how to work with Linux.
That will be a fun adventure. An adventure I implore you not to explore by bricking1 your family's or a school's computer trying to install Linux on it. Only mess with your own stuff.
I.E. making useless
Mobile Phones
Android and iOS are the major players in the mobile device world.
You will find doing the tasks a programmer needs to exceedingly difficult on these. This is because the interfaces made by these operating systems are designed for use by the general public on small devices. Working with a keyboard and editing a Java program aren't tasks that are prioritized.
Game Consoles
Game Consoles generally always do their own wacky thing.
For the same reasons as mobile devices, you will have a lot of trouble trying to program on a game console. They are special made to run Halo.1
Yeah, Halo sucks now. Halo 3 was art. Obtuse, beautiful, orchestral art. I am old enough to have lived in the age of dreams and to know of their dimming.
Abstractions
The most important job of an operating system is to "abstract" over the hardware.
You shouldn't need to know what brand network card you have in order to write code that connects to the internet. You shouldn't need to know what kind of hard drive will be on the machine to store data.
We call these abstractions because they take things that are concrete, like a dozen specific models of hard drive and the exact bits to send to do their operations, and make an "abstract model" of their commonalities.1
This lets programmers write programs that will run on any machine that has the right operating system. It doesn't help writing programs that will run on any operating system because different operating systems provide different abstractions over the same hardware. The code to write to a hard drive in Windows is different than it is in Mac OS.
There are deeper and shallower versions of this explanation which we will get to. There will be plenty of time to talk about this concept as it relates to Java and other things.
Defaults
A social aspect of operating systems is that they control the "default" experience people have with computers.
Installing an operating system is prohibitvely difficult, so computer manufacturers install one before selling a device. When they choose Windows and Windows comes with Internet Explorer built in, people use Internet Explorer.1
There are a lot of kinda grim things that follow from this, and you should dig deeper, but I bring it up to mention the 1980s.
The personal computers available then, like the Commodore 64, only had text based interfaces. In many practical ways, people were closer to the world of programming.2
So think about that whenever you feel like you have a lot left to learn. A lot of what you do and do not know about computers was dictated for you by the fact that you grew up interacting with them on a touch screen instead of on a terminal. Things are not as intimidating as they seem.
https://en.wikipedia.org/wiki/United_States_v._Microsoft_Corp.
Ever wonder why the kid in WarGames was a hacking wizz? The kids in basically every 80s movie? And yet all most of us can pull off is opening Google.
The Terminal

Early computers didn't have graphical interfaces with windows or buttons you could "click." Instead, they offered a text based interface.
We call this sort of interface a "terminal," though you might hear the terms "shell" and "terminal emulator" used interchangibly.
Bash
If you are programming on a computer running either Mac or Linux you should have easy access to a bash terminal. Just search your applications for something called "terminal" or that has the word "term" in it.
This is the most common kind of terminal for working professionals to use.1
Asterisks apply, of course. I didn't run or try to find a survey so this is mostly anecdotal.
Windows Subsystem for Linux
If you are using Windows there are two terminals that come preinstalled. One is cmd.exe
, where
you write in a language called batch, and the other is "PowerShell."
Both of these differ in significant ways from bash so, if at all possible, you should get set up with the Windows Subsystem for Linux.
This will let you follow along with the bash snippets you'll see later in this book.1
It is certainly possible for me to also include instruction for PowerShell and batch but it doesn't feel practical. I spend most of my working hours using bash and can test commands on the machine I use to write this. It would be hard for me to do that with the Windows specific shells
Chromebooks and School Computers
A very common thing that school systems will do is prevent students from accessing a terminal.
This is a good thing if your goal is to prevent high schoolers from committing shenanigans. Less so if your goal is to learn how to program.
There are also computer types, like chromebooks, which aren't really built with programming in mind. While you could access a terminal, the sorts of programs you would generally install to work with Java will be out of reach. It's a similar case for things like iPads.
If you are in one of these situations I would recommend getting a "normal" computer if at all possible. You can make do for a time, but eventually it will be required to continue in this field.1
Thems the ropes, kid. 🤷
Commands
The primary way you interact with a terminal is by running commands.
You start with a blank prompt and write something like the following.
$ echo Hello
Hello
The first word is the name of the program to run. In this case echo
. Everything after
that is an "argument" to the program.
Directories
Directories, also known as folders, are the places where files are in a computer.
When you open a terminal, you are "inside" a particular directory.
You can see the directory you are in by running the pwd
1 command.
$ pwd
/Users/emccue
"Print Working Directory"
Listing Files
You can list the files in your current working directory with the ls
1 command.
$ ls
Applications
Camera
Desktop
Development
Documents
Downloads
This is useful for getting your bearings, but also to find a file you forget the name of or similar.
Short for "List"
Creating Directories
In order to create a directory you can use the mkdir
1 command.
$ mkdir project
$ ls
project
Make Directory.
Changing Directories
You can change what your working directory is using the cd
command.
After the name of the command, you write the name of the directory you want to change in to.
$ pwd
/Users/emccue
$ cd Downloads
$ pwd
/Users/emccue/Downloads
If you want to go to a "parent" directory, you can write ..
. This means "one directory up".
$ pwd
/Users/emccue
$ cd ..
$ pwd
/Users
Short for "change directory"
Creating Files
To create a blank file you can use the touch
1 command.
$ touch hello.txt
This is situationally useful, but more often than not you will create files using a program called a "text editor."
There are some text editors, like vim
, which run entirely
inside the terminal.2
One of the more popular ones at time of writing is Visual Studio Code. If you weren't otherwise shown a different option, that is a decent default. Install it and then you can use it to create and edit files.
I have no clue why this is called touch
.
2: These have their die hard supporters. The lunatics.
3: This book has been written inside Visual Studio Code.
Run Java Programs
If you've properly set up Java on your machine you should be able to use the java
command
to run any code on your machine.
Just write java
followed by the path to a file containing Java code.1
Say we had this code in a file named Main.java
void main() {
IO.println("Hello");
}
To run this, say java Main.java
.
$ java Main.java
Hello
If the file is in a directory that is not your current working directory, you should include the full path.
$ java src/Main.java
Hello
You can get away with just using your editor's run button for awhile, but this will eventually become important to know.
Getting Used to it
So far this is only a shallow dive. It will take a good amount of time for you to get comfortable using the terminal for things.
Thats normal. You'll get there eventually. Just know that at least some familiarity with the terminal is going to be needed.
We'll get back to it relatively soon, but feel free to seek out some bash/terminal specific resources online in the meantime.
Exceptions II

Perhaps unsurprisingly, RuntimeException
is not the only kind of exception
that can be thrown.
There is a huge variety of exception types out there, but the most important thing to understand is the distinction between checked and unchecked exceptions.
Checked Exceptions
When a part of your code might throw a "checked" exception, other parts of your code need to account for that possibility.
int divide(int x, int y) {
if (y == 0) {
// Will not work because nothing handles the exception
throw new Exception();
}
return x / y;
}
void main() {
}
We call them "checked" because you need to "check" for them.
These are generally1 used when you expect calling code to be able to do something intelligent to recover if the exception is thrown.
This rule is merely a suggestion and people's definitions of "something intelligent" and "recover" vary wildly. Expect some things to throw checked exceptions and others to not and just know that you need to check for the checked ones.
Unchecked Exceptions
When a part of your code might throw an "unchecked" exception, other parts of your code do not need to account for that possibility.
int divide(int x, int y) {
if (y == 0) {
// Will work because you are not forced to handle
// an unchecked exception
throw new RuntimeException();
}
return x / y;
}
void main() {
}
We call them unchecked because you don't need to check for them.
throws
One way to handle a checked exception is to add a throws
to the end
of your method declaration.
int divide(int x, int y) throws Exception {
if (y == 0) {
throw new Exception();
}
return x / y;
}
void main() {
}
This will make it so that calling code needs to check for the possibility of that exception being thrown.
int divide(int x, int y) throws Exception {
if (y == 0) {
throw new Exception();
}
return x / y;
}
void doStuff() {
divide(1, 0);
}
void main() {
}
You can also declare unchecked exceptions using throws
but you are never required to.1
int divide(int x, int y) throws RuntimeException {
if (y == 0) {
throw new RuntimeException();
}
return x / y;
}
void main() {
}
This is one of many things that you might choose to do for the benefit of a human reader that isn't strictly needed for correct Java.
Propagating Exceptions
Say we started out with code like this.
void dream() {
IO.println("Shin Godzilla's Jaw unhinging like a snake...")
}
void sleep() {
dream();
}
void main() {
sleep();
}
If a function is declared to throw an exception, that exception will have to "propagate" - meaning spread - to all calling functions.
void dream() throws Exception {
throw new Exception("Something went wrong")
}
void sleep() throws Exception {
dream();
}
void main() throws Exception {
sleep();
}
dream
declares that it might throw Exception
so sleep
must declare that it might throw Exception
. Because sleep
might throw Exception
now main
might throw Exception
.
Exception
The most vanilla1 checked exception is Exception
.
If you want a checked exception and do not know of a better one, Exception
will do.
void main() throws Exception {
throw new Exception("Crash!");
}
people have the audacity to equate vanilla with “plain”. the fruit of a delicate orchid pollinated by hand. worth its weight in solid gold and beyond. the fussy black-and-cream jewel of the american continent. you sick son of a bitch. imagine a world without vanilla. no blondies. no pound cakes. no crème brûlée, no coke floats. no cream soda. no satiny new york-style cheesecakes. no warm apple pie à la mode. no velvety complexity to bring out complex notes in chocolate desserts. no depth of flavour in your cakes and cookies and milkshakes. all in just a few precious seeds or grams of paste or perfumed teaspoons of liquid black platinum. what you don’t understand could fill the library of alexandria seven times over and then some. you ungrateful bastard i’m going to kill you - Tumblr user "kirbyofthestars"
RuntimeException
RuntimeException
is an unchecked exception.1
If you want a unchecked exception and do not know of a better one, RuntimeException
will do.
void main() {
throw new RuntimeException("Crash!");
}
You may be wondering why the names "Exception
" and "RuntimeException
" instead of the
probably easier to understand "CheckedException
" and "UncheckedException
." Java was made by humans
and initially - as I understand it - "normal" exceptions were expected to be checked. It was only
exceptions that came from "The Java Runtime" that would be unchecked. So that is where the name "RuntimeException
" comes from. It just very quickly became a choice that was too late to change
without breaking all the Java code in the world.
Rethrowing Exceptions
When you catch an exception you have the option of again throwing an exception.
This is useful if you want to have behavior that occurs when something goes wrong but still ultimately throw an exception for the rest of the code to deal with.1
void dream() throws Exception {
throw new Exception("Oh no");
}
void sleep() throws Exception {
try {
dream();
}
catch (Exception e) {
IO.println("Something went wrong while dreaming");
throw e;
}
}
It is also an opportunity to "wrap" checked exceptions into unchecked ones.
void dream() throws Exception {
throw new Exception("Oh no");
}
void sleep() {
try {
dream();
}
catch (Exception e) {
throw new RuntimeException(e);
}
}
void main() {
sleep();
}
Which seems crazy, but remember that "the point" of a checked exception is to have the calling code make a choice about how to handle an error condition. If you are okay with just crashing, throwing an unchecked exception is a perfectly valid choice.
These examples are all silly, I know. Once we get to files the mechanics will become useful.
main
The main
method can be declared to throw any kind of checked exception.
It is assumed that if an exception makes it all the way there that the way to handle it is crash the program.
void main() throws Exception {
throw new Exception("crash");
}
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1
Make a method named parseIntChecked
that works the same as Integer.parseInt
but throws a checked exception instead of an unchecked one.
// CODE HERE
void main() throws Exception {
int x = parseIntChecked("45");
IO.println(x);
parseIntChecked("abc");
}
Challenge 2
The following program will not compile. Make it compile by propagating the checked exception
thrown by the doesSomethingDangerous
method.
int doesSomethingDangerous(int value) {
if (value == 1) {
throw new Exception("1 does not work");
}
return 3 * value + 2;
}
int compute(int start) {
return square(doesSomethingDangerous(start));
}
int square(int x) {
return x * x;
}
void main() {
int x = Integer.parseInt(IO.readln("Give a starting number: "));
IO.println(compute(x));
}
Challenge 3
The following program is the same as the last one. Instead of propagating the exception, make it so that
compute
catches and rethrows the checked exception as an unchecked one.
int doesSomethingDangerous(int value) throws Exception {
if (value == 1) {
throw new Exception("1 does not work");
}
return 3 * value + 2;
}
int compute(int start) {
return square(doesSomethingDangerous(start));
}
int square(int x) {
return x * x;
}
void main() {
int x = Integer.parseInt(IO.readln("Give a starting number: "));
IO.println(compute(x));
}
Switch II

A common thing to do is have a switch
statement which assigns a value
to a variable in each branch.
enum StopLight {
RED,
YELLOW,
GREEN
}
enum Action {
STOP,
SLOW_DOWN,
GO
}
void main() {
StopLight light = StopLight.GREEN;
Action action = null; // Delayed assignment rules are funky here.
switch (light) {
case RED -> {
action = Action.STOP;
}
case YELLOW -> {
action = Action.SLOW_DOWN;
}
case GREEN -> {
action = Action.GO;
}
}
IO.println(action);
}
For this purpose, you can instead use a switch "as an expression."
Yield
To use a "switch expression" you put the entire switch to the right hand side of an equals sign1 and, instead of assigning to a variable, you "yield
" the value you want to assign.
enum StopLight {
RED,
YELLOW,
GREEN
}
enum Action {
STOP,
SLOW_DOWN,
GO
}
void main() {
StopLight light = StopLight.GREEN;
Action action = switch (light) {
case RED -> {
yield Action.STOP;
}
case YELLOW -> {
yield Action.SLOW_DOWN;
}
case GREEN -> {
yield Action.GO;
}
};
IO.println(action);
}
yield
is very similar to return
. The difference is that return
will exit the entire method. yield
just decides what the switch evaluates to.
Technically we are talking about an "expression context." Meaning a place where you are allowed to put an expression. The right hand side of an equals sign is one, but there are many others.
Omitted Yield
If a branch of a switch just yields a value but does nothing else interesting you can
omit the yield
along with the surrounding {
and }
.
enum StopLight {
RED,
YELLOW,
GREEN
}
enum Action {
STOP,
SLOW_DOWN,
GO
}
void main() {
StopLight light = StopLight.GREEN;
Action action = switch (light) {
case RED -> Action.STOP;
case YELLOW -> Action.SLOW_DOWN;
case GREEN -> Action.GO;
};
IO.println(action);
}
Which can be mixed with cases that have explicit yield
s and might do other things.
enum StopLight {
RED,
YELLOW,
GREEN
}
enum Action {
STOP,
SLOW_DOWN,
GO
}
void main() {
StopLight light = StopLight.GREEN;
Action action = switch (light) {
case RED -> Action.STOP;
case YELLOW -> {
IO.println("Lemon Light!");
yield Action.SLOW_DOWN;
}
case GREEN -> Action.GO;
};
IO.println(action);
}
Exhaustiveness
When a switch is used as an expression it needs to be exhaustive.
void main() {
String name = "bob";
boolean cool = switch (name) {
case "bob" -> false;
default -> true;
};
IO.println(cool);
}
If you attempt to make a non-exhaustive switch expression, Java will not let you.
void main() {
String name = "bob";
boolean cool = switch (name) {
case "bob" -> false;
};
IO.println(cool);
}
Unlike in a switch statement, covering all the enum values in a switch expression
is enough for that switch to be considered exhaustive. You do not need an
extra default
or case null
1.
enum Species {
ALIEN,
PREDATOR,
HUMAN
}
void main() {
Species species = Species.valueOf(
IO.readln("What do you see? ").toUpperCase()
);
String message = switch (species) {
case ALIEN -> "Run";
case PREDATOR -> "Fight Back";
case HUMAN -> "RUN"
};
IO.println(message);
}
The nitty gritty of why this is comes down to what Java will do if the code is run with an unexpected enum variant. With a switch statement the code will just move on and run none of the switch cases. With a switch expression Java will crash on an unexpected value. This difference is partially due to the fact that switch statements came first in the language and switch expressions came later. What a world, huh?
Return a Switch
If you choose to, you do not need to assign the result of a switch expression into a variable. You can directly return the result from a method.
enum Bird {
TURKEY,
EAGLE,
WOODPECKER
}
boolean isScary(Bird bird) {
return switch (bird) {
case TURKEY -> true;
case EAGLE -> true;
case WOODPECKER -> false;
};
}
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1
Complete the switch expression such that it yields the appropriate value for each case.
In the surfer case, make sure to print out "Radical!" before yielding the value.
enum Profession {
FIREFIGHTER,
PLUMBER,
SURFER
}
enum NaturalEnemy {
FIRE,
LEAKY_PIPES,
BODACIOUS_WAVES
}
NaturalEnemy enemy(Profession p) {
switch (p) {
case FIREFIGHTER -> {
// CODE HERE
};
case PLUMBER -> {
// CODE HERE
}
case SURFER -> {
// CODE HERE
}
}
}
void main() {
IO.println(enemy(Profession.FIREFIGHTER));
IO.println(enemy(Profession.PLUMBER));
IO.println(enemy(Profession.SURFER));
}
Challenge 2
Update your program above to omit yield
in the two cases where it is not needed.
Multi-File Programs

For many reasons, it makes sense to split programs into multiple files.
This is one of those things that is easier to show than tell, so to follow along make a blank folder on your computer.
$ mkdir program
$ cd program
The Sources folder
When people work on "a project" - meaning a program made to accomplish some task - there will often be more than just Java code involved.
To deal with this its common to make a folder to put Java code into. I prefer the name
src
for this.
$ mkdir src
And inside this src
folder we will put the code.1
So you should have this
project/ <- Open this in your text editor
src/ <- Code will go here
There are different ways to layout a project. All are valid. You can call the src
folder STUFF
- it ultimately doesn't matter. This is just another one of those social conventions.
The Main file
Inside your src
folder make a file called Main.java
. This is the file where
we are going to write the "start" of the program.
All the Java code you've written up until now will work if you put it into this file.
void main() {
IO.println("Hello, world!");
}
So you should now have something that looks like this.
project/
src/
Main.java
To run your program, use the java
command.
$ java src/Main.java
Hello, world!
A Second file
In files that are not Main.java
you can put other code, but
only in the form of a class.
By this I mean, while in Main.java
you are able to write something like this.
void sayHello() {
IO.println("Hello");
}
void main() {
sayHello();
}
In a file named Ball.java
you need to put all code inside a Ball
class.
class Ball {
// You can write constructors, methods, fields, etc.
final int size;
Ball(int size) {
this.size = size;
}
}
// But you cannot have any "top level" methods or things outside
// of the Ball class
Then from Main.java
you can make an instance of Ball
void main() {
var ball = new Ball(10);
IO.println("The ball is " + ball.size + "cm across");
}
When you run java src/Main.java
it will find src/Ball.java
and use the code in there.
File names
When you make a file called Ball.java
you should expect to find a class named Ball
inside of it.
This is a general rule. When you split code into multiple files each file should be a class and the name of the class should match the name of the file.
So if I want to write a Position
class
class Position {
int x;
int y;
}
That should go in its own file named Position.java
.
The Anonymous Main Class
Surprise! Everything in Java is actually in a class.
This includes the code in Main.java
.
void main() {
IO.println("What, really?");
}
Everything we've written so far has been in what is called "the anonymous main class." We call it anonymous because we never gave it a name.
We call it the main class because you are only allowed to skip naming a class if it is the one you use to start your program, and that requires a void main()
method.
If you take any code we've produced up until now and wrap it with class Main {}
it will continue to work as-is.
class Main {
void main() {
IO.println("yep.");
}
}
What Java will do is run new Main().main();
to start your program.
Global Fields
Global fields, accordingly, were always a lie.
int number = 0;
void main() {
IO.println(number);
number++;
IO.println(number);
}
They are just normal fields in the anonymous main class.
class Main {
int number = 0;
void main() {
IO.println(number);
number++;
IO.println(number);
}
}
This means that once you move to programs in multiple files they are no longer global to your program - just the code in the main class.1
Huge bummer, but we will learn how to make actually global things later.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1
Put this code in a file named Pickle.java
class Pickle {
boolean dill;
}
Put this code in a file named Ramen.java
class Ramen {
void mixWith(Pickle pickle) {
IO.println("This is gross.");
if (pickle.dill) {
IO.println("Extra super gross.");
}
}
}
And finally put this code in Main.java
class Main {
void main() {
var pickle = new Pickle();
pickle.dill = true;
var ramen = new Ramen();
ramen.mixWith(pickle);
}
}
Then run the entire program from your terminal by using java Main.java
in the directory where you made these files.
Challenge 2
Move Ramen.java
, Pickle.java
, and Main.java
into a folder named src
.
Make sure you can still run the program by typing java src/Main.java
.
Challenge 3
The following code uses an anonymous main class. Make it instead use a named class.
int dogs = 99;
void main() {
while (dogs > 0) {
IO.println(dogs + " dogs on the floor.");
IO.println("Wake one up, take it for a walk");
dogs--;
}
IO.println("No more dogs on the floor.");
}
Challenge 4
Combine the following programs while keeping them in separate files. This will require you to give names to both classes as well as parameterize the "dogs on the floor" program in some way.
int dogs = 99;
void main() {
while (dogs > 0) {
IO.println(dogs + " dogs on the floor.");
IO.println("Wake one up, take it for a walk");
dogs--;
}
IO.println("No more dogs on the floor.");
}
void main() {
while (true) {
try {
int dogs = Integer.parseInt(IO.readln("How many dogs are on the floor? "));
IO.println("TODO"); // Call the first program here somehow.
} catch (RuntimeException e) {
IO.println("Goodbye!");
}
}
}
Visibility

When code is all in one file, everything is "visible." This means that if there is a method you are always allowed to call it.
class Main {
void canCallThis() {
IO.println("of course!")
}
void main() {
canCallThis();
}
}
And if there is a field you can read it, if there is a class you can make an instance of it, etc.
Once we split into multiple files, you are allowed to make things less visible.
Private Methods
If a class has a method that it uses but that you do not want other code to see,
you can mark it private
.
class RiceCooker {
int temperature;
private boolean shouldTurnOff() {
return temperature > 100;
}
void operate() {
if (shouldTurnOff()) {
turnOff();
}
else {
// ...
}
}
}
This makes it so that code in RiceCooker.java
can see and call the shouldTurnOff
method but code in other files cannot.
This is useful if you want to box up some logic but don't want to have to think about what happens if other code calls it.
Private Fields
Similar to private methods, you can also mark a field as private.
class Kaiju {
private int timesLostToGodzilla;
Kaiju() {
this.timesLostToGodzilla = 0;
}
void fightGodzilla() {
this.timesLostToGodzilla++;
}
boolean isLoser() {
return this.timesLostToGodzilla > 0;
}
}
This makes it so that code in other files cannot see or change the field directly.
Invariants
Having private fields and methods is useful when you want to maintain some invariants.
Say you wanted a class that holds an even number.
class EvenNumberHolder {
int value;
EvenNumberHolder(int value) {
if (value % 2 == 1) {
throw new RuntimeException(value + " is not even");
}
this.value = value;
}
}
You can always make the value final
to prevent its value from being changed.
class EvenNumberHolder {
final int value;
EvenNumberHolder(int value) {
if (value % 2 == 1) {
throw new RuntimeException(value + " is not even");
}
this.value = value;
}
}
But if you actually wanted to change its value that isn't enough.
By making the field private you can know that other code has to call methods to access or change the value. That gives you a clean place to "enforce" your invariants.
class EvenNumberHolder {
private int value;
EvenNumberHolder(int value) {
// The constructor explicitly rejects an odd value
if (value % 2 == 1) {
throw new RuntimeException(value + " is not even");
}
this.value = value;
}
// There is no way to get an odd value now - you can only
// change it in steps of two.
int value() {
return this.value;
}
void addTwo() {
this.value += 2;
}
void subtractTwo() {
this.value -= 2;
}
}
Accessors
When a field is hidden that is usually because you want to control how it might be changed.
To access the current value of a private field you need to go through a non-private method. If a method just provides access to a field we call that an "accessor."
class Dog {
private String name;
Dog(String name) {
this.name = name;
}
// The name field is private, but
// you can access it by calling the name method.
String name() {
return this.name;
}
}
class Main {
void main() {
var dog = new Dog("Daisy");
// dog.name won't work because the name field is private
// dog.name() will work because the name method is not
IO.println(dog.name());
}
}
class Dog {
private String name;
Dog(String name) {
this.name = name;
}
String name() {
return this.name;
}
}
We would also consider things like the length
method on String
s to be "accessors."1
void main() {
String s = "abc";
IO.println(
// We can't see what fields underly this,
// but we can access the length
s.length()
);
}
Not that the categorization matters much, but socially we expect "accessor"-looking methods to only give you a value and not "do stuff" like increment a number or mess with a file.
Getters and Setters
A very silly thing you are likely to see if you dig around on the internet is classes that look like this.
class Person {
private String name;
private int age;
String getName() {
return this.name;
}
void setName(String name) {
this.name = name;
}
String getAge() {
return this.age;
}
void setAge(String age) {
this.age = age;
}
}
So people make classes with all private fields and then for each field thing
they
have two methods - getThing
and setThing
.
To which you might immediately ask - what is the difference between this and just having non-private fields.
class Person {
String name;
int age;
}
The answer to that is interesting. We'll get to it, but the short story is that its a bit of a holdover from a very weird period in the early 2000s.
I mention it specifically so that you know that there isn't any important information you are missing and you are not crazy.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1
The following Ratio
class should have an invariant where denominator
cannot be 0
.
class Ratio {
int numerator;
int denominator;
Ratio(int numerator, int denominator) {
if (denominator == 0) {
throw new RuntimeException("Denominator cannot be zero");
}
}
double value() {
return ((double) numerator) / denominator;
}
}
Unfortunately other classes can directly access the denominator
field.
Make the denominator field private and add an accessor method for other classes to use.
You can name this accessor .denominator()
or .getDenominator()
.
Challenge 2
Alter the code from the last challenge such that numerator is also private and only usable via an accessor method.
Challenge 3
Alter the code from the last challenge such that you can not only access but also
mutate the numerator
and denominator
fields via exposed methods.
The method for mutating the denominator
should throw an exception
if someone tries to set it to zero.
Challenge 4
Given this Pineapple
class make every method except digest
private.
class Pineapple {
void digestProtein(String name) {
IO.println("Pineapple juice is digesting the proteins in " + name);
}
void eatPork() {
digestProtein("pork");
}
void eatChicken() {
digestProtein("chicken");
}
void eatBeef() {
digestProtein("beef");
}
void eatYou() {
IO.println("Pineapple juice is eating you from the inside.");
}
void digest(String thing) {
switch (thing) {
case "pork" -> eatPork();
case "chicken" -> eatChicken();
case "beef" -> eatBeef();
default -> eatYou();
}
}
}
Challenge 5
Rewrite the code from the previous challenge such that the digest
method on Pineapple
does the same thing but no other private methods
exist.
Note when doing this that you wouldn't need to consider other code. The behavior of the only exposed method does not change so those private methods would only be "implementation details."
Static Fields

To have a field be truly global for your program you can mark it as static.
class Count {
static int value;
}
Declaration
To declare a field as static, add the static
keyword to its declaration.
class Count {
static int value;
}
Initialization
By default, static fields will be given the same default initial value as other fields.
So a static int
field will be initialized to zero, a static String
field
will be initialized to null
, etc.
class Main {
static int count;
static String name;
void main() {
IO.println(count); // 0
IO.println(name); // null
}
}
If you want to initialize them to a different value you do not do that in a constructor like you would a normal field.
You can give them a value directly with =
.
class Main {
static int count = 5;
static String name = "bob";
void main() {
IO.println(count); // 5
IO.println(name); // bob
}
}
Or you can initialize them in a "static block". This looks like the word static
followed by some braces {}
with code in the middle.1
class Main {
static int count;
static String name;
static {
count = 5;
name = "bob";
}
void main() {
IO.println(count); // 5
IO.println(name); // bob
}
}
The rules for static blocks are actually crazy complicated. Try not to do anything "interesting" in them.
Usage
To use a static field within the class it is declared you just need to write the name of the field.
class Main {
static int count = 0;
void main() {
IO.println(count);
}
}
To use it from another class or to disambiguate it from a regular field with the same
name, you should prefix it with the name of the class it is declared in plus a .
.
class Main {
static int count = 0;
void main() {
IO.println(Main.count);
}
}
Constants
Because static fields are global to the entire program, they are the preferred mechanism for storing "constants."
Constants are things that you don't expect to change, so you would also
mark such fields as final
.
class MathConstants {
static final double PI = 3.14;
}
// Then in other parts of your code you can reference MathConstants.PI
Controversy
Static fields are culturally controversial. Specifically static fields which can change.
class Counter {
static int value = 0;
}
In the example above, any part of the code can change the value at any time by writing to Counter.value
.
This is "fine" in small to mid-sized programs, but once you have a hundred thousand lines it can become difficult to reason about what code changes that field and when.
For this reason1 you will probably get a lot of mean comments if you share code that uses a static field you can change.
Using static fields for constants is less controverial.
class Constants {
static final int DAYS_IN_A_WEEK = 7;
}
Well, in addition to the generally rampant immaturity of programmers.
Naming
For static final fields people generally name them the same way you would an enum - LIKE_THIS
.
class Constants {
static final int DAYS_IN_A_WEEK = 7;
static final int WEEKS_IN_A_YEAR = 52;
}
Because you will get yelled at for using a non-final static field no matter what you do, the rules there are less strict. You can name them in all caps or like a normal variable depending on personal preference.
class Counter {
// The people who would be mad at you for the first
// will probably already be mad at you for the second.
static int value = 0;
static int VALUE = 0;
}
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1
Add a static field to this Ogre
class that keeps track of
how many Ogres have been made thus far in the program.1
class Ogre {
// CODE HERE
Ogre() {
// CODE HERE
}
}
class Main {
void main() {
// 0
IO.println(Ogre.NUMBER_OF_OGRES_MADE);
// 1
Ogre o1 = new Ogre();
IO.println(Ogre.NUMBER_OF_OGRES_MADE);
// 2
Ogre o2 = new Ogre();
IO.println(Ogre.NUMBER_OF_OGRES_MADE);
// 3
Ogre o3 = new Ogre();
IO.println(Ogre.NUMBER_OF_OGRES_MADE);
// 4
Ogre o4 = new Ogre();
IO.println(Ogre.NUMBER_OF_OGRES_MADE);
// 5
Ogre o5 = new Ogre();
IO.println(Ogre.NUMBER_OF_OGRES_MADE);
}
}
Challenge 2
Initialize the PI
and TAU
static final fields inside of a static initializer block.
TAU
should have twice the value of PI
.
class Maths {
static final double PI;
static final double TAU;
static {
// CODE HERE
}
}
class Main {
void main() {
IO.println(Maths.PI);
IO.println(Maths.TAU);
}
}
Challenge 3
Rename the constants in the Doug
class in the way that would
be expected of you by others.
class Doug {
static final String pattyMayonnaise = "Patty Mayonnaise";
static final String sKeEtEr = "Mosquito 'Skeeter' Valentine";
static final String mosquito_valentine = sKeEtEr;
static final String rodgerMKlotz = "Rodger M. Klotz";
static final String DOUG = "Douglas Yancy Funnie";
}
Part of why mutable static fields are such a nightmare is that code like this would not work when you have to write "multi-threaded" Java code. There are things you can do with normal fields to sort of "make unsafe stuff safe in a way," but static fields are a lot harder to wrangle.
Static Methods

If you want to be able to call a method from anywhere in your program you
can use a static
method.
class MyMath {
static int add(int a, int b) {
return a + b;
}
}
class MyMath {
static int add(int a, int b) {
return a + b;
}
}
class Main {
void main() {
int result = MyMath.add(1, 2);
IO.println(result);
}
}
Declaration
Same as static
fields, to mark a method as static
you write the word static
before it.
class MyMath {
static int add(int a, int b) {
return a + b;
}
}
Scope
In the definition of a static method you can use variables like normal and you can reference other static fields and methods.
class ScopeExample {
static final int CAN_ACCESS = 3.14;
static void canCall() {
}
static void doStuff() {
canCall();
IO.println(ScopeExample.CAN_ACCESS);
}
}
But you cannot access any non-static methods or fields. They are not in scope.
class ScopeExample2 {
final int CANNOT_ACCESS = 3.14;
void cannotCall() {
}
static void doStuff() {
cannotCall();
IO.println(
CANNOT_ACCESS
);
}
}
Naming
Unlike static fields, which get new socially accepted naming rules, you name static methods the same as any other method.
class Naming {
static void nameLikeNormal() {
}
}
Usage
You use a static method by writing the name of the class where it is
defined followed by .
and the method name.
class StuffDoer {
static void doStuff() {
IO.println("Doing stuff");
}
}
class StuffDoer {
static void doStuff() {
IO.println("Doing stuff");
}
}
void main() {
StuffDoer.doStuff();
}
Math
One of the most "obvious" usages for static methods is when doing things that are math or math-like.
class MyMath {
static int add(int a, int b) {
return a + b;
}
}
These sorts of methods have a result computed solely from their inputs, so needing an instance of a class to call them would be silly.
The Math
class that comes with Java has methods that work in this way. Math.max(a, b)
is static
and therefore usable everywhere you want the maximum of two numbers.1
There are way more on there. Take a look.
Factories
Another interesting use of static methods is what we would call a "factory" method.
Say you have a class like the following.
class Position {
int x;
int y;
Position(int x, int y) {
this.x = x;
this.y = y;
}
}
void main() {}
It would be reasonable to want to add an overloaded constructor for when y
is 0
.
class Position {
int x;
int y;
Position(int x, int y) {
this.x = x;
this.y = y;
}
Position(int x) {
this.x = x;
this.y = 0;
}
}
void main() {}
But now it is impossible to add a constructor that works for when x
is 0
.
class Position {
int x;
int y;
Position(int x, int y) {
this.x = x;
this.y = y;
}
Position(int x) {
this.x = x;
this.y = 0;
}
Position(int y) {
this.x = 0;
this.y = y;
}
}
void main() {}
Using a static method to create a Position
- i.e. as a "factory" - is a way around the issue.1
class Position {
int x;
int y;
Position(int x, int y) {
this.x = x;
this.y = y;
}
static Position fromX(int x) {
return new Position(x, 0);
}
static Position fromY(int y) {
return new Position(0, y);
}
}
class Position {
int x;
int y;
Position(int x, int y) {
this.x = x;
this.y = y;
}
static Position fromX(int x) {
return new Position(x, 0);
}
static Position fromY(int y) {
return new Position(0, y);
}
}
class Main {
void main() {
var p1 = new Position(1, 2);
var p2 = Position.fromX(4);
var p3 = Position.fromY(5);
IO.println(p1.x + ", " + p1.y);
IO.println(p2.x + ", " + p2.y);
IO.println(p3.x + ", " + p3.y);
}
}
This won't work if you defined Position
inside the anonymous main class. I'll tell you why later.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1
Change this position class such that it doesn't only have an x
and a y
. It should
also have a z
.
Then add the following static methods. fromZ
, fromXY
, fromXZ
, and fromYZ
.
class Position {
int x;
int y;
Position(int x, int y) {
this.x = x;
this.y = y;
}
static Position fromX(int x) {
return new Position(x, 0);
}
static Position fromY(int y) {
return new Position(0, y);
}
}
class Main {
void main() {
var p1 = Position.fromXY(1, 2);
IO.println("(" + p1.x + ", " + p1.y + ", " + p1.z + ")");
var p2 = Position.fromYZ(1, 2);
IO.println("(" + p2.x + ", " + p2.y + ", " + p2.z + ")");
var p3 = Position.fromXZ(1, 2);
IO.println("(" + p3.x + ", " + p3.y + ", " + p3.z + ")");
var p4 = Position.fromZ(4);
IO.println("(" + p4.x + ", " + p4.y + ", " + p4.z + ")");
}
}
Challenge 2
Make a static method in the Maths
class called quadraticFormula
.
It should take in an a
, b
, and c
and run the quadradic formula on those values.
This formula will give you either one "real" solution, two real solutions, or two imaginary solutions.
For now just throw an exception if the solutions are imaginary and treat having one solution as having two solutions with the same value.
class Maths {
// CODE HERE
}
class Main {
void main() {
var result = Maths.quadraticFormula(6, -17, 12);
IO.println(result.solutionOne);
IO.println(result.solutionTwo);
}
}
Challenge 3
Update your code above to also communicate if a solution is imaginary. Use a boolean or an enum field on whatever class you wrote to hold both solutions for this.1
You will learn other ways later.
Challenge 4
Make the following code compile. Do so first by changing one of the fields to static
then by instead passing an extra argument to the static
method.
class Keychain {
final String[] keys;
Keychain(String[] keys) {
this.keys = keys;
}
boolean hasKey(String key) {
for (int i = 0; i < keys.length; i++) {
if (keys[i].equals(key)) {
return true;
}
}
return false;
}
}
class Main {
Keychain keychain = new Keychain(new String[] {
"house",
"car",
"shed"
});
static void unlock(String thing) {
if (keychain.hasKey(thing)) {
IO.println("You have unlocked my " + thing);
}
else {
IO.println("You don't have a key for my " + thing);
}
}
void main() {
unlock("house");
unlock("car");
unlock("shed");
unlock("heart");
}
}
Growable Arrays

Arrays are fixed size collections of elements. This means when we make an array that is 5 elements big it will always be 5 elements big.
void main() {
int[] numbers = new int[5];
}
Something that turns out to be extremely useful is to have something with most of the properties of an array - such as being able quickly get and set arbitrary elements by index - but that can grow over time.
The rest of this section I will walk you through how we can accomplish that.
Concept
The concept behind a growable array is that we store an array as a field and use it for operations like getting and setting elements.
The extra wrinkle is that when someone wants to "add" an element we make a new, bigger, array and copy all the existing elements to it. Then we put the element you wanted to add at the end.
Simple Implementation
Following the previous description to the letter will get you something like this.
Take some time to read it through.
class GrowableIntArray {
// Store an int[] internally
private int[] data;
GrowableIntArray() {
// Make sure to initialize it correctly
this.data = new int[0];
}
// When someone wants to get an item, get it from the array
int get(int index) {
return this.data[index];
}
// Same deal when someone wants to set an item at an index.
void set(int index, int value) {
this.data[index] = value;
}
// And we need an accessor for the size so that someone
// can loop over the array.
int size() {
return this.data.length;
}
void add(int value) {
// Copy the old array to a new, bigger one
int[] newArray = new int[this.data.length + 1];
for (int i = 0; i < this.data.length; i++) {
newArray[i] = this.data[i];
}
// Then put the new element at the end
newArray[newArray.length - 1] = value;
// And swap the array
this.data = newArray;
}
}
Usage
Using the class we defined would look something like this.
class GrowableIntArray {
// Store an int[] internally
private int[] data;
GrowableIntArray() {
// Make sure to initialize it correctly
this.data = new int[0];
}
// When someone wants to get an item, get it from the array
int get(int index) {
return this.data[index];
}
// Same deal when someone wants to set an item at an index.
void set(int index, int value) {
this.data[index] = value;
}
// And we need an accessor for the size so that someone
// can loop over the array.
int size() {
return this.data.length;
}
void add(int value) {
// Copy the old array to a new, bigger one
int[] newArray = new int[this.data.length + 1];
for (int i = 0; i < this.data.length; i++) {
newArray[i] = this.data[i];
}
// Then put the new element at the end
newArray[newArray.length - 1] = value;
// And swap the array
this.data = newArray;
}
}
class Main {
void main() {
// To start we don't know how many elements there are
var array = new GrowableIntArray();
// Each time we add an element the array "grows"
array.add(1);
array.add(2);
array.add(3);
// And we can loop over it like so
for (int i = 0; i < array.size(); i++) {
IO.println(array.get(i));
}
}
}
Important to note that while we see it as if it was a growable array, no actual arrays are "growing." We are just faking it.
Performance Problems
While the code as is works, it will not perform well.
Since we copy the underlying array every time someone adds a new element it can be expensive to add a lot of elements.
Pretend we were going to add a hundred elements to the growable array.
class GrowableIntArray {
// Store an int[] internally
private int[] data;
GrowableIntArray() {
// Make sure to initialize it correctly
this.data = new int[0];
}
// When someone wants to get an item, get it from the array
int get(int index) {
return this.data[index];
}
// Same deal when someone wants to set an item at an index.
void set(int index, int value) {
this.data[index] = value;
}
// And we need an accessor for the size so that someone
// can loop over the array.
int size() {
return this.data.length;
}
void add(int value) {
// Copy the old array to a new, bigger one
int[] newArray = new int[this.data.length + 1];
for (int i = 0; i < this.data.length; i++) {
newArray[i] = this.data[i];
}
// Then put the new element at the end
newArray[newArray.length - 1] = value;
// And swap the array
this.data = newArray;
}
}
class Main {
void main() {
var array = new GrowableIntArray();
for (int i = 0; i < 100; i++) {
array.add(i);
}
IO.println(array.size());
}
}
For the each element we need to make a copy of an array. So when we add the first element we need to make an array 1 element big. The second, copy that 1 element array and make a new 2 element one.
So if you do napkin math on the things that need to happen you get this
1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 ...
Which is just a crazy number of copies. It means calling .add
on an already big list will be slow and calling .add
a lot of times to make a big list will be very slow.
Performance Solutions
If the problem with the simple implementation is that we make too many copies, the solution is to make fewer copies.
The easiest way to do this is to "over-allocate." Make our internal array bigger than
we actually need it to be. That way most of the time when you call .add
it will be
fast.
We can't - or at least shouldn't - over allocate right away. If every growable array secretly has enough room for millions of elements that would be silly. Better to over-allocate as we go.
Exactly how much we should make the internal array bigger than we need is more art than science. People have found that doubling the size each time is a pretty good tradeoff1.
This data structure is crazy important. Maybe the most common one used in the world. Java has it built-in and we'll get to that later.
Optimized Implementation
So with that context we can update the simple implementation as follows.
Note the use of IndexOutOfBoundsException
. It is an unchecked exception that
comes with Java specifically for when an index is out of bounds.1
class GrowableIntArray {
private int[] data;
// We need a different variable to store the size
// since the array's size will different than we want.
private int size;
GrowableIntArray() {
this.data = new int[0];
this.size = 0;
}
int get(int index) {
// Now that we manage the size, we need to do
// "bounds checks" ourselves
if (index >= size) {
throw new IndexOutOfBoundsException(index);
}
return this.data[index];
}
void set(int index, int value) {
// For setting as well
if (index >= size) {
throw new IndexOutOfBoundsException(index);
}
this.data[index] = value;
}
int size() {
// We use the size we store rather than the length of the array
return this.size;
}
void add(int value) {
// if we won't have enough room in the array, make a new one.
if (size >= this.data.length - 1) {
// Overallocate by 2x
int newLength = this.data.length * 2;
if (newLength == 0) {
newLength = 2; // Unfortunately 0 * 2 is 0, so account for that
}
int[] newArray = new int[newLength];
for (int i = 0; i < this.data.length; i++) {
newArray[i] = this.data[i];
}
this.data = newArray;
}
// And at this point we know the array is big enough
this.data[size] = value;
this.size++;
}
}
I love obvious and circular statements like this, if you haven't noticed.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Make your own implementation of a growable array that holds String
s and not
int
s.
Validate that .size
, .add
, .get
, .set
, etc. all work as you would expect.
Challenge 2.
Go back to your calorie tracker program from that project. Make use of a growable array of some kind to keep the full history of calorie amounts entered by the user.
Challenge 3.
When implementing the point of sale system project you may or may not have gone down the path of discovering growable arrays for yourself. Either way refactor that project to make use of a growable array to store the items ordered for the purpose of making a receipt.
Command Line Arguments

When you run a program like so
java src/Main.java
Anything you put to the right of src/Main.java
will be available to your program as a
"command line argument."1
java src/Main.java example
They come from the command line and they are arguments to your program.
Accessing Arguments
To access the command line arguments given to your program you need to change your main method.
Instead of
void main() {
}
Have your main method take a string array as an argument.
void main(String[] args) {
}
This String[]
will hold all the arguments passed.
java src/Main.java Duck Squirrel
class Main {
void main(String[] args) {
for (int i = 0; i < args.length; i++) {
IO.println(
"Hello Mr. " + args[i]
);
}
}
}
// This is a little ugly
void main() {
new Main().main(new String[] { "Duck", "Squirrel" });
}
Conventions
As it is just a String[]
, you can interpret the arguments passed to your
program in any way you choose.
What you will notice, however, from using other tools in your terminal is that there are a few socially accepted conventions.
If your tool takes "options" then you would expect --option-name VALUE
to
be how you specify it. So two dashes followed by the argument name.
If you need a shorter version then you allow for one dash and sometimes a single
letter value -d VALUE
.
And most importantly, if someone types --help
you should give them the
available options. Try java --help
for an example.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Make a program under src/CLI.java
. The rest of these challenges will all be adding
features to that program.
class CLI {
// CODE HERE
void main(String[] args) {
// CODE HERE
}
}
First, make it so that java src/CLI.java
prints "No arguments provided"
and immediately exits.
Challenge 2.
Make it so that java src/CLI.java 8
will print 64
and then exit.
If you ran java src/CLI.java 5
it would print 25
and so on.
For any number it should print it's square.
Challenge 3.
Make the operation to perform overridable. java src/CLI.java --operation square 8
should do the same thing as java src/CLI.java 8
. java src/CLI.java --operation factorial 8
should print out 8
factorial (which is 40320
) instead of 64
.
Challenge 4.
If a -d
argument is provided then, instead of printing the result, the program
should write the result to a file at the specified path.
So java src/CLI.java -d out.txt 9
should write 81
into a file named out.txt
.
The order that -d
and --operation
are provided should not matter. java src/CLI.java -d out.txt --operation factorial 3
and java src/CLI.java --operation factorial -d out.txt 3
should behave exactly the same.
Challenge 5.
Make java src/CLI.java --help
print out a help message describing the different flags
you can provide and their meanings. To see what this should look somewhat like
first run java --help
.
Inner Classes

You can declare a class within another class.
class Car {
class Speedometer {
}
}
We call these inner classes.
Type
The type of an inner class, when used as a variable or as a field,
is the name of the containing class followed by a .
and the
name of the inner class.
class Car {
class Speedometer {}
}
So a field containing an instance of Speedometer
would have the type Car.Speedometer
.
Car.Speedometer speedometer = ...;
The exception is if the inner class is referenced within the class that declares it. In that context you just need to write the name of the class;
class Car {
// Car.Speedometer is not required
// (it will work though)
Speedometer speedometer;
class Speedometer {}
}
Instances
To make an instance of an inner class you can use new
to invoke its constructor
like any other class.
class Car {
class Speedometer {
}
Speedometer getSpeedometer() {
return new Speedometer();
}
}
The restriction is that an inner class can only be constructed from an instance method of the containing class.
This means that, in the example above, you cannot make an instance of Speedometer
unless you first have an instance of Car
.
class Main {
void main() {
var car = new Car();
var speedometer = car.getSpeedometer();
IO.println(speedometer);
// But this will not work
// var speedometer = new Car.Speedometer();
}
}
class Car {
class Speedometer {
}
Speedometer getSpeedometer() {
return new Speedometer();
}
}
New Operator
If you want to make an instance of an inner class without making a method on the class containing it, you can use the "new operator."
Whereas you make an instance of a regular class by saying
something like new ClassName()
, you can make an instance of an
inner class by using .new
on a variable that holds an instance
of the containing class.
That's a confusing verbal description, but it kinda makes sense once you see it.
class Car {
class Speedometer {
}
}
class Car {
class Speedometer {
}
}
class Main {
void main() {
Car car = new Car();
Car.Speedometer speedometer = car.new Speedometer();
IO.println(speedometer);
}
}
Scope
Within an inner class all fields and methods of the instance it was created in are available.
class Car {
int speed = 0;
class Speedometer {
int getSpeed() {
return speed;
}
}
Speedometer getSpeedometer() {
return new Speedometer();
}
}
class Car {
int speed = 0;
class Speedometer {
int getSpeed() {
return speed;
}
}
Speedometer getSpeedometer() {
return new Speedometer();
}
}
class Main {
void main() {
Car car = new Car();
Car.Speedometer = car.getSpeedometer();
}
}
One mental model for this is that its as if the inner class holds a reference to the one it was created in.
class Car {
int speed = 0;
}
class CarSpeedometer {
private final Car madeBy;
CarSpeedometer(Car madeBy) {
this.madeBy = madeBy;
}
int getSpeed() {
return madeBy.speed;
}
}
Disambiguation
If you are within an inner class and want to use a field from the instance it was created in but that field has the same name as a field in the inner class - like the following.
class Car {
int speed = 0;
class Speedometer {
// Speed is declared here, but it is
// a different field
int speed = 5;
}
}
You can disambiguate between the fields by using the name of the containing class
followed by .this
.
class Car {
int speed = 0;
class Speedometer {
// Speed is declared here, but it is
// a different field
int speed = 5;
void saySpeed() {
IO.println(speed); // 5
IO.println(this.speed); // 5
IO.println(Car.this.speed); // 0
}
}
}
class Car {
int speed = 0;
class Speedometer {
// Speed is declared here, but it is
// a different field
int speed = 5;
void saySpeed() {
IO.println(speed); // 5
IO.println(this.speed); // 5
IO.println(Car.this.speed); // 0
}
}
}
class Main {
void main() {
var car = new Car();
var speedometer = car.new Speedometer();
speedometer.saySpeed();
}
}
The anonymous main class
If you remember when I first showed you classes, you were working inside the anonymous main class.
class Muppet {
String name;
}
void main() {
Muppet kermit = new Muppet();
kermit.name = "Kermit The Frog";
}
This means that all the classes you made were, in reality, inner classes.
class Main {
class Muppet {
String name;
}
void main() {
Muppet kermit = new Muppet();
kermit.name = "Kermit The Frog";
}
}
Which is how they would have access to any "global fields" you declared and why static factory methods would not work.
class Main {
class Muppet {
String name;
static Muppet fromName(String name) {
// You cannot make an instance of an inner class
// from within a static method, so this wouldn't work.
Muppet muppet = new Muppet();
muppet.name = name;
return muppet;
}
}
void main() {
Muppet kermit = Muppet.fromName("Kermit The Frog");
}
}
Static Inner Classes
If you mark an inner class as static
then it becomes
much closer to a normal class.
class Car {
static class Speedometer {
}
}
You can make instances of it directly without an instance of the outer class.
Car.Speedometer speedometer = new Car.Speedometer();
And it cannot access fields of the instance it was made in, because it was not made in an instance.
class Car {
int speed; // Speedometer can't magically get this anymore
static class Speedometer {
}
}
I would wager that this is the most common kind of inner class to see in real code, despite requiring more words to define1.
A theme that will start to emerge is that the "best" code sometimes has a few extra modifiers on it and the "default" behavior isn't what you want. Static inner classes are way less magic.
Private Inner Classes
Both static
and regular inner classes can be marked as private
.
class Human {
// No other class can see this human's thoughts
private class Thoughts {
}
// Nor can they see their feelings
private static class Feelings {
}
}
Within the class they are defined, a private inner class works as normal. The difference is that code outside the class cannot make instances of them.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
As written the code in GameConsole.Controller
looks at its own isPoweredOn
field.
Disambiguate the usage so that it is uses the isPoweredOn
field from
the GameConsole
instance wrapping it in its status
method.
class GameConsole {
boolean isPoweredOn;
GameConsole() {
this.isPoweredOn = false;
}
class Controller {
boolean isPoweredOn;
Controller() {
this.isPoweredOn = false;
}
String status() {
"Controller["
+ isPoweredOn ? "ON" : "OFF" + "] - GameConsole["
+ isPoweredOn ? "ON" : "OFF" + "]";
}
}
}
class Main {
void main() {
var console = new GameConsole();
var controller = console.new Controller();
IO.println(controller.status());
console.isPoweredOn = true;
IO.println(controller.status());
controller.isPoweredOn = true;
IO.println(controller.status());
console.isPoweredOn = false;
IO.println(controller.status());
}
}
Challenge 2.
Make the Controller
class from the previous example a static
inner class.
The status
method should keep the same behavior. This means you will need
to explicitly pass an instance of GameConsole
to the constructor of Controller
and store it in a field.
Challenge 3.
Successfully make an instance of Fly
from the Main
class.
class OldLady {
class Horse {
class Cow {
class Goat {
class Dog {
class Cat {
class Bird {
class Spider {
class Fly {
Fly() {
IO.println("She's dead, of course");
}
}
}
}
}
}
}
}
}
}
class Main {
Fly f = /* CODE HERE */;
IO.println(f);
}
Challenge 4.
Go back to some of the programs you wrote entirely within the anonymous main class. Turn that main class into a named class.
First make sure your program works the same as it did. Then
go through all the inner classes in your program and mark them
as many of them as you can static
.
Which ones can't be trivially made static
? Note what state they
access in the enclosing instance.
Packages

Every class in Java "lives" in a package.
A package is a way to group individual classes.
package dungeon;
class BugBear {
}
Declaration
To put a class into a package you need to do two things.
First, put a "package declaration" at the top of the file. This looks like the word package
followed by the name of the package and a ;
package dungeon;
class BugBear {
}
Then you need to make sure that the .java
file is in a folder matching the name of
that package.
So, for the example above, if your code was previously laid out like this
src/
BugBear.java
It needs to be changed to this.
src/
dungeon/
BugBear.java
Visibility
By default, a class can only see the other classes in its package.
package dungeon;
class BugBear {
}
package village;
class Villager {
}
package dungeon;
class Dwarf {
BugBear fight() {
// Can "see" BugBear and thus call its constructor,
// access visible fields and methods, etc.
return new BugBear();
}
// Cannot see Villager because it is in a different package.
}
Public Classes
To be able to use a class in one package from a different package,
you must first mark that class as public
.
package village;
// Now other packages will be able to see it
public class Villager {}
Fully Qualified Class Name
In order to use a public class from a different package you can write out the "fully qualified class name."1
This is the name of the class prefixed with the package it is in.
package village;
public class Villager {}
package dungeon;
class Dwarf {
// Works because we write out the full name
// and because Villager is public.
village.Villager meet() {
return new village.Villager();
}
}
This hints that the "real name" of a class isn't what you write after class
, but instead both that and the package name glued together.
People also often call this the "FQCN". Its a fun acronym to write, but I have no clue how to say it out loud. "Faquacün?"
Import
If you don't want to write out the fully qualified class name everywhere, and few do,
then you can use an import
.
To import a class you write import
followed by the fully qualified class name.
This makes it so that the "simple" class name will be usable.
package village;
public class Villager {}
package dungeon;
// For the rest of the file, you can
// just write "Villager" and Java will know
// what you mean.
import village.Villager;
class Dwarf {
Villager meet() {
return new Villager();
}
}
Package Imports
If you want to import all of the classes in a package all at once you can use a "package import."
To do this write import
followed by the name of the
package and .*
.
package village;
public class Villager {}
package village;
public class Dog {}
package dungeon;
// Imports Village, Dog, and any other classes
// in the "village" package.
import village.*;
class Dwarf {
Villager meet() {
return new Villager();
}
Dog pet() {
return new Dog();
}
}
This has the upside of being only one line of code. This also has the downside of being only one line of code - it is harder to figure out from what package any particular class might be coming from.
The Default Package
When your classes don't have a package declaration, we say those are in the "default package."
// No package declaration means default package
public class Elf {
}
Classes in the default package cannot be imported by classes in named packages, regardless of if those classes are public.
package villager;
public class Villager {
// No way to reference Elf directly,
// even if Elf is public
}
Because of this restriction1 you will mostly use the default package when you are feeling lazy or are making a smaller program.
And more to come!
The Anonymous Main Class
You are only allowed to make an anonymous main class inside the default package.
// Allowed
void main() {
IO.println("Hello, world");
}
// Not Allowed
package myprogram;
void main() {
IO.println("Hello, world");
}
This means that for classes in packages you have to wrap them in an explicitly named class like everything else.
// Allowed
package myprogram;
class Main {
void main() {
IO.println("Hello, world");
}
}
Public Methods
Even though a class might itself be marked public
, the methods
within it will not be unless they are also public
.
package village;
// Now other packages will be able to see it
public class Villager {
public void isVisible() {
IO.println("This method is callable from another package.");
}
void isNotVisible() {
IO.println("This method is not.")
}
}
This applies also to static methods.
package village;
public class Well {
public static int drawWater() {
IO.println("""
You need this to be both public and static to
be able to write Well.drawWater()
""");
return 235;
}
}
Package-Private Methods
If a method is neither public
or private
then it can
be used only by code in the same package.
We call these methods "package-private" because they are "private" to code outside the package.
package village;
public class Villager {
void isNotVisible() {
IO.println("""
This method can be called from code in the 'village'
package, but not from other packages.
""");
}
}
Which again applies to static methods as well.
Public Fields
Similarly to methods, for a field to be used from a different package
it must be marked public
.
package village;
public class Well {
// Both of these you can use from a different package
public static final int DEPTH = 10;
public int remainingWater;
}
Package-Private Fields
Fields that are marked neither public
nor private
are "package-private."
package village;
public class Well {
// Neither of these can be used outside of the "village" package
static final int NUMBER_OF_DEMONS = 4;
boolean exorcismPerformed;
}
The Default Constructor
If a class is public
and has a default constructor - i.e. the constructor you
get when you don't specify one - then the default constructor you get will be
public
.
package dungeon;
public class Skeleton {
// No constructor specified
}
package village;
import dungeon.Skeleton;
class Main {
void main() {
// We can say `new Skeleton()` here
var skeleton = new Skeleton();
}
}
If you write out a constructor yourself this will not be the case.
package dungeon;
public class Skeleton {
public final int bones;
Skeleton() {
this.bones = 206;
}
}
package village;
import dungeon.Skeleton;
class Main {
void main() {
// Now "new Skeleton()" will not work.
var skeleton = new Skeleton();
}
}
Public Constructors
For a constructor you write to be usable across packages1 it needs
to be marked public
.
package dungeon;
public class Skeleton {
public final int bones;
public Skeleton() {
this.bones = 206;
}
}
package village;
import dungeon.Skeleton;
class Main {
void main() {
// Now this works
var skeleton = new Skeleton();
// And we get the right number of bones!
IO.println(skeleton.bones);
}
}
You guessed it!
Package-Private Constructors
A constructor without any other modifier is "package-private" in the same way as methods and fields.1
package dungeon;
public class Slime {
final int size;
// This constructor is public,
// code in other packages can use it.
public Slime() {
this.size = 5;
}
// This constructor is package-private,
// code in other packages cannot use it.
Slime(int size) {
this.size = size;
}
}
Your spider-sense might be tingling wondering if private
constructors
are a thing. They are! I'll talk about them more in-depth later, but they can be
surprisingly useful.
Subpackages
Packages can also have subpackages.
What this looks like is for any package with a .
in its name - say my.fun.project
-
you need a new folder.
So if you have a class like this
package my.fun.project;
public class Apple {}
It should be in a folder structure like this
src/
my/
fun/
project/
Apple.java
And the fully qualified class name would be my.fun.project.Apple
.
Reverse Domain Name Notation
Part of the point of putting classes into packages is to avoid conflicts when using code written by other people.
If you want a class named RiceCooker
and you want to use a code that someone
on the internet wrote which just so happens to also have a class named RiceCooker
,
that only works if you both put your classes in different packages.
Making sure different people cooperate is a hard problem, so the social convention that emerged was to name packages using a "reverse domain name notation."
Only one person could own a domain name - like google.com
- at a time. So all
code coming out of Google1 would start with a package name which is the reverse
of that - com.google
.2
Nowadays people also tend to accept unique prefixes based on accounts you might have on a site. So you might see things like com.github.their_username_here
for people who have an account with a service like Github
.
It isn't perfect, nothing would be, but it's socially dominant so you should be aware of it. If the code you are writing won't be shared with others you do not need to do this sort of thing yourself.
This is why many tools will have your starter project have classes in the com.example
package
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Take all the code you have written so far and put into one project.
To do this put everything under one src
folder just in different packages.
You will need to convert any usages of the anonymous main class into
named classes, but other than that it should be doable.
If you have already been doing that, just start making use of packages. They are, first and foremost, an organizational tool. Use them to organize your code as you see fit.
Challenge 2.
In the packages above have all the growable array implementations you were
asked to make in one package named collections
. Import these collections
wherever you want to use them with import collections.GrowableIntArray;
or similar declarations.
This will require marking the classes and many of the methods within public
.
Challenge 3.
Read through the list of packages that come with Java. You don't need to understand everything you are looking at; just browse and try to find at least one class that interests you.
Import that class and try to use it.
Records

If you have a class whose only purpose is to ferry data around,
you can instead use a record
.
record Person(String name, int age) {}
Declaration
To declare a record you write the word record
followed by a
class name, a list of "record components" in parentheses, and a pair of {}
.
The list of record components should be reminiscent of function arguments.
record Person(String name, int age) {}
Having an empty list of record components is also allowed.1
record Pants() {}
As to why you might want to give an empty list of record components, it's nuanced. For now it's just for fun.
The Canonical Constructor
A record is always given a constructor which matches its list of record components.
record Person(String name, int age) {}
void main() {
// This call to new Person(...) matches up with
// the record declaration.
var person = new Person("Ancient Dragon Man", 2000);
}
Similar to the "default constructor" given to regular classes, this is what you get for "free" with the declaration of a record.
Component Accessors
For each record component, an accessor method will be available that has the name of that component.
You use these to access the values of components.
record Dog(String name) {}
record Dog(String name) {}
class Main {
void main() {
var dog = new Dog("Hunter");
// .name() accessor is available
String name = dog.name();
IO.println(name);
}
}
Component Accessor Visibility
The accessor methods of a record are always public, so all the packages that can see the class can access its components.
package dungeon;
public record Dragon(double wingspan) {}
import dungeon.Dragon;
void main() {
var dragon = new Dragon(224.5);
IO.println(
// Method is visible.
dragon.wingspan()
);
}
Printing a Record
When printing out a record, the output will include each of the components of the record.
record Goblin(String name, int hp) {}
record Goblin(String name, int hp) {}
class Main {
void main() {
var goblin = new Goblin("Gobbo", 11);
IO.println(goblin);
}
}
Goblin[name=Gobbo, hp=11]
This is more intelligable than what you would get by default from a regular class.1
Goblin@609db43b
It is possible to make a regular class print differently, but we'll get to that later.
Check for Equality
To check if the components of a record match with another, you can use the equals
method.
record Elf(boolean pretentious) {}
record Elf(boolean pretentious) {}
class Main {
void main() {
var elfOne = new Elf(true);
var elfTwo = new Elf(true);
IO.println(elfOne.equals(elfTwo));
}
}
This is similar to how you check whether String
s are equal.
Return Multiple Values
One of the initial reasons I gave for wanting to use a class was returning multiple values from a method.
A record is likely better for that purpose than a regular class.
record Location(double latitude, double longitude) {}
Location findTreasureIsland() {
return new Location(51.4075, 0.4636);
}
void main() {
Location treasureIsland = findTreasureIsland();
IO.println(
"Treasure island is located at " +
treasureIsland.latitude() +
" " +
treasureIsland.longitude() +
"."
);
}
Regular classes are, of course, still useful. Its just for classes which only hold some data together (and nothing else interesting) getting a constructor and accessors automatically is very convenient.
Shorthand
One way to think about records is that they are "shorthand"1 for a regular class.
So the following record
public record Cat(boolean spayed, int weight) {}
is shorthand for a regular class that looks like this.
// There are a few parts that I left off here
// so this isn't 100% accurate.
public class Cat {
private final boolean spayed;
private final int weight;
public Cat(boolean spayed, int weight) {
this.spayed = spayed;
this.weight = weight;
}
public boolean spayed() {
return this.spayed;
}
public int weight() {
return this.weight;
}
// + the magic that makes it print nicer
// + the magic that lets you use .equals
// + a little more that will be relevant later
}
For you non-native English speakers, a shorthand is a shortened form of something. TTYL is "shorthand" for "Talk to you later."
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Replace the usage of a normal class in the following code with a record.
import java.util.Arrays;
class InventoryItem {
String name;
InventoryItem(String name) {
this.name = name;
}
}
class Main {
void main() {
var item1 = new InventoryItem("sword");
var item2 = new InventoryItem("shield");
var item3 = new InventoryItem("armor");
InventoryItem[] items = {
item1,
item2,
item3
};
IO.println(Arrays.toString(items));
}
}
Challenge 2.
Make an instance of the Tien
record and print it out.
enum TienMove {
DODON_RAY,
TRI_BEAM,
EXPLODE
}
record Chiaotzu(
Move memorableMove
) {}
record Tien(
Chiaotzu onlyFriend,
Move firstMove,
Move secondMove
) {}
class Main {
void main() {
Tien tien;
// CODE HERE
IO.println(tien);
}
}
Challenge 3.
What will this program output when run?
true
thentrue
true
thenfalse
false
thentrue
false
thenfalse
Write down your guess and then try running it.
class Position {
int x;
int y;
Position(int x, int y) {
this.x = x;
this.y = y;
}
}
record Location(int x, int y) {}
class Main {
void main() {
var p1 = new Position(7, 1);
var p2 = new Position(7, 1);
IO.println(p1.equals(p2));
var l1 = new Location(8, 3);
var l2 = new Location(8, 3);
IO.println(l1.equals(l2));
}
}
Integers II

Integers never stop being relevant and, now that we have covered static methods, we are ready to cover some of the more useful ones that exist for working with integers.
Integer from a String
If you have a String
which contains text that can be interpreted
as an integer you can convert it to an int
using the parseInt
static method on Integer
.1
void main() {
String text = "123";
int oneTwoThree = Integer.parseInt(text);
IO.println(oneTwoThree);
}
If what is in the String
cannot be converted to an int
that method
will throw a NumberFormatException
.
void main() {
String word = "music";
int value = Integer.parseInt(word);
}
If you want to handle input from a user that might not be interpretable
as an integer, you can use try
/catch
alongside delayed assignment.
void main() {
String word = "seltzer";
int value;
try {
value = Integer.parseInt(word);
} catch (NumberFormatException e) {
value = 8; // Default value
}
}
You should actually already know this. I just never explained explicitly that parseInt was a static method or showed that you could catch only the NumberFormatException
. In my defense, IO.readln
at one point came far later in the book.
Integer to a String
If you have an integer you want to turn into a String
you have two options.
One is to "add it" to an empty string.
void main() {
int x = 4;
String xStr = "" + x;
IO.println(xStr);
}
The other is to use the toString
static method on Integer
.
void main() {
int x = 4;
String xStr = Integer.toString(x);
IO.println(xStr);
}
I personally find the second one more direct, but opinions can reasonably vary.
Base 16 Integer Literals
In polite society we use a base 10 number system1. This means
we start counting at 0
and go up by one until we reach 9
.
0
, 1
, 2
, 3
, 4
, 5
, 6
, 7
, 8
, 9
Then when we go up one more we put a 1
at the front and "wrap around."
10
, 11
, 12
, etc.
In a base 16 number system, sometimes called "hexadecimal", you keep going a little bit more before wrapping around.
0
, 1
, 2
, 3
, 4
, 5
, 6
, 7
, 8
, 9
, A
, B
, C
, D
, E
, F
, 10
, 11
, ..., A5
, ..., 5F
, ... etc.
To write an integer literal in Java which contains a hexadecimal number you write 0x
before the number.
void main() {
int sixteen = 0x10;
IO.println(sixteen);
int twoHundredFiftyFive = 0xFF;
IO.println(twoHundredFiftyFive);
}
Hexadecimal numbers like this are very common when you are writing code that
deals with colors in RGB - Red, Green, Blue - format. 0xFFFFFF
is white, 0xFF0000
is red, 0xCCCCFF
is periwinkle, etc.
Okay to be real with you, every number system is a base 10 system. Even if what you call "10" is what we would call "sixteen", you always wrap around your base when you write "10." If that doesn't make sense it doesn't matter, but it's fascinating to me.
Integer from a Base 16 String
If you have a String
which contains text that can be interpreted as a base 16 integer, you can convert it into an int
by using parseInt
and giving the
number 16
as an extra argument.
void main() {
String text = "C";
int twelve = Integer.parseInt(text, 16);
IO.println(twelve);
}
This will not work if the number is prefixed by 0x
like it would be in your code.
void main() {
Integer.parseInt("0xC", 16);
}
If you want to handle both hexadecimal numbers and regular base 10 numbers you should instead use Integer.decode
.
void main() {
IO.println(Integer.decode("0xC"));
IO.println(Integer.decode("0x19"));
IO.println(Integer.decode("19"));
}
Integer to a Base 16 String
If you have an integer you want to turn into a String
in base 16
integer you can use the toHexString
method on Integer
.
void main() {
int x = 29411;
String xStr = Integer.toHexString(x);
// 72e3
IO.println(xStr);
}
Underscores in Integer Literals
When you are writing large numbers 1000000000
isn't very
visually distinct from 10000000000
.
To help with the legibility you are allowed to insert underscores between digits in an integer literal.
void main() {
int x = 1_000_000_000;
int y = 10_000_000_000;
IO.println(x);
IO.println(y);
}
This works with hexadecimal integer literals as well.
void main() {
int white = 0xFF_FF_FF;
IO.println(Integer.toHexString(white));
}
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
RGB Colors can be represented as base 16 numbers.
The following numbers represent colors. Print them out in base 16 to figure out what colors they represent. Rename the variables to be the color names.
You should be able to just look up the hex string.
class Main {
void main() {
int colorA = 255;
int colorB = 65280;
int colorC = 16711935;
int colorD = 16776960;
// CODE HERE
}
}
Challenge 2.
Rewrite the integer literals in the code from the previous challenge to be base 16 integer literals.
Challenge 3.
Add underscores in the following integer literals in the same places you would write commas in regular math (every 3 digits 13,550,145).
class Main {
void main() {
int a = 515326326;
int b = 32523522;
int c = 1221415133;
IO.println(a);
IO.println(b);
IO.println(c);
}
}
Files

Files are how you store information on a1 computer so that it can still be there when your program is done running.
As such, reading files and writing files are tasks you will often want to do.
*normal
Paths
All files and folders on your computer can be located by a "path"
like "Downloads/movie.mp4
."
To store a path in a Java program we use the Path
class from the java.nio.file
1
package. You create these Path
s by giving a String
to the Path.of
static method.
import java.nio.file.Path;
class Main {
void main() {
Path pathToNotes = Path.of("notes.txt");
IO.println(pathToNotes);
}
}
nio
stands for "new IO." So yes, there was an old IO. We will use that too. Its one of many artifacts from Java's interesting and sordid history.
IOException
If some code is "doing IO" - by which we mean while it is trying to read some Input or write some Output - you should expect it to throw an IOException
.
This class lives in the java.io
package so to use it by its simple name you need an import.
import java.io.IOException;
class Main {
void main() throws IOException {
throw new IOException("Something went wrong");
}
}
Since reading a file is reading some input and writing to a file is writing some output, this exception is relevant to reading and writing files.
UncheckedIOException
The unchecked version of an java.io.IOException
is UncheckedIOException
.
You can use this if you have a method which you don't want to propagate IOException
but
also want something more specific than RuntimeException
to re-throw.
And just like IOException
, if you don't want to write
out java.io.UncheckedIOException
more than once you need to add an import.
import java.io.IOException;
import java.io.UncheckedIOException;
class Main {
void main() {
try {
doStuff();
}
catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
Write to a File
To write a String
to a file you can use the Files
class from the java.nio.file
package. It has a static method named writeString
.
Path tasksPath = Path.of("tasks.txt");
String tasks = """
1. Do dishes
2. Do laundry
""";
Files.writeString(tasksPath, tasks);
This method can throw an IOException
, so to handle that you
can have a throws
declaration on the method calling it.
import java.nio.file.Files;
import java.nio.file.Path;
class Main {
void main() throws IOException {
Path tasksPath = Path.of("tasks.txt");
String tasks = """
1. Do dishes
2. Do laundry
""";
Files.writeString(tasksPath, tasks);
}
}
The other option is to catch the IOException
and re-throw it as an unchecked exception.
import java.nio.file.Files;
import java.nio.file.Path;
class Main {
void main() {
Path tasksPath = Path.of("tasks.txt");
String tasks = """
1. Do dishes
2. Do laundry
""";
try {
Files.writeString(tasksPath, tasks);
}
catch (IOException e) {
throw new UncheckedIOException(e);
}
}
}
Read from a File
To read a file's contents as a String
you can use the readString
static method
from java.nio.file.Files
.
Path tasksPath = Path.of("tasks.txt");
String tasks = Files.readString(tasksPath);
Similarly to Files.writeString
, this can throw an IOException
if
something goes wrong1.
This can be dealt with in the same way. Either declare the IOException
in a throws
clause or re-throw an unchecked exception.
import java.nio.file.Files;
import java.nio.file.Path;
class Main {
void main() throws IOExeption {
Path tasksPath = Path.of("tasks.txt");
String tasks = Files.readString(tasksPath);
IO.println(tasks);
}
}
If you choose to re-throw any IOException
as an unchecked exception then
it might be helpful to remember that delayed assignment is allowed in that context.2
import java.nio.file.Files;
import java.nio.file.Path;
class Main {
void main() {
Path tasksPath = Path.of("tasks.txt");
String tasks;
try {
tasks = Files.readString(tasksPath);
}
catch (IOException e) {
throw new UncheckedIOException(e);
}
IO.println(tasks);
}
}
Usually we can hand-wave this as "when something goes wrong it throws IOException
," but this is one of those cases where you might care about what exactly went wrong. Specifically, whether the reason you couldn't read a file because the file wasn't there. We'll use this method as an example when we go deeper into Exceptions
so there will be a chance to talk about that.
Mostly just because it can let you "shrink the scope" of the try-catch.
Creating a Folder
To create a folder, you can use Files.createDirectory
.
import java.nio.file.Files;
import java.nio.file.Path;
class Main {
void main() throws IOExeption {
Path folderPath = Path.of("example1");
Files.createDirectory(folderPath);
}
}
This, like the other methods in Files
, might throw an IOException
.
Files.createDirectory
will fail if the folder already exists.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Write a file named hello.txt
and give it Hello, world
as contents.
Challenge 2.
Write a program that asks the user for a number and writes it into
a file named numbers.txt
.
Challenge 3.
Update the previous program so that the list of numbers entered by a user
is stored in the file. So if they give 1
, 2
, and 3
the file should contain
something like the following.
1
2
3
Hint: \n
is how you embed a newline character in String
. You might find it useful.
Challenge 4.
Update the previous program to also display the biggest number given so far
if the user types biggest
instead of a number.
Challenge 5.
Make the previous program behave sensibly if the file contains data that is not numbers.
Challenge 6.
Complete this program.
import java.nio.file.Path;
import java.io.IOException;
class Main {
record Person(String name, int age) {}
void main() throws IOException {
var people = new Person[] {
new Person("Steve Smith", 15),
new Person("Stan Smith", 42),
new Person("Rodger", 1601)
};
var path = Path.of("people.txt");
save(path, people);
people = load(path);
IO.println(people[0]);
IO.println(people[1]);
IO.println(people[2]);
}
void save(Path path, Person[] people) throws IOException {
// Save to a file
}
Person[] load(Path path) throws IOException {
return null; // Make actually return an array
}
}
Data Visualization
Problem Statement
In 1854 there was a major cholera outbreak on Broad Street in London.
At the time it was thought that the outbreak was caused by "bad air." This was in line with the miasma theory of disease which held that plauges were literally caused by bad air coming from rotting organic matter.
If you've ever seen a drawing of a plague doctor this is part of why they had that long nosed mask. They would fill it with nice smelling herbs in order to counter the miasma they thought was causing disease.
This was basically entirely wrong in terms of the mechanics of disease spread. But it wasn't all bad. Staying away from dead bodies and wearing a mask around the sick aren't the worst ideas in the world.
The actual cause of this cholera outbreak was not bad air but instead contaminated water. The person who figured this out was Dr. John Snow.
He recorded information about where the people who were getting sick lived. Using this he was able to show that the people who were getting sick were the ones drinking from a particular water pump in town. He did this by overlaying the number of people who got sick onto a map of the town.
This visualization was key to determining the actual cause of the outbreak. Unfortunately it took quite a bit longer for the germ theory of disease to be widely accepted, but Dr. Snow did at least get that one contaminated water pump removed.
One moral of the story - and there are a few - is that making a visual representation of data can be key to the interpretation of that data. If you have a file with a million numbers in it that is not exactly interpretable. If you took the data in that file, plotted it, and saw a straight line then that is useful information.
Computers are uniquely suited to making visualizations. Modern data sets are often quite large and already stored on a computer. It is easier for a computer to turn that data into a chart or diagram of some kind than it is for a human to sit down and labor with a sheet of paper and a ruler.
Your Goal
Fill up a file named data.txt
with a bunch of numbers you type randomly on your keyboard. So something like the following
124124523950159219359858327587324573258723458342756734
Write a program which reads that file and writes a new file containing a series of bar charts representing how many times a given digit appears in the file. People aren't that good at picking random numbers so you are bound to see one bar be higher than the others.
This will require you to learn how to create an image from code. The good news is that this is easier than you would expect. There is a file format called "PPM" which represents images like this.
P3
# "P3" means this is a RGB color image in ASCII
# "3 2" is the width and height of the image in pixels
# "255" is the maximum value for each color
# This, up through the "255" line below are the header.
# Everything after that is the image data: RGB triplets.
# In order: red, green, blue, yellow, white, and black.
3 2
255
255 0 0
0 255 0
0 0 255
255 255 0
255 255 255
0 0 0
So if you write text like this into a file and give it a .ppm
extension your computer should be
able to show it to you as an image.
You will still need to figure out how you are going to represent image data in your program, how to produce a file like this, and how to arrange the pixels. That should all be within your ability at this point though.1
Future Goals
- Expand this program to support other kinds of visualizations such as scatterplots, pie charts, and line graphs.
- Visualize some data that comes from a CSV file.
- Make the program use a different method of data visualization based on a command-line flag.
- For scatterplots, allow adding a "line of best fit." Look up "linear regression" for one way to do this.
- Add labels and axes to your charts. This will require figuring out how to render text in your images.
- Try to make a spacial map similar to the one that Dr. Snow made. This is likely pretty involved to do in a program but it is definitely possible.
I believe in you
Object

You may have heard the phrase "everything is an object" parroted about.
It sounds very silly. Of course things are things, what else would they be?
In Java it has a very specific meaning. There is a class named
java.lang.Object
(Object
for short) which all other classes are
derived from.
Everything "is" an Object
.
Subtypes
We consider most everything to be a "subtype"
of Object
.
This means that if you have a variable or field that holds an Object
you can assign any data you want into it.
void main() {
String oak = "oak";
Object tree = oak;
IO.println(tree);
}
If something is a subtype of Object
, we would call Object
its "supertype."1
Super meaning above and Sub meaning below. God how I feel for people who learn english as a second language.
instanceof
If you have an Object
you can recover the actual type
of the data stored in it using instanceof
.
void main() {
Object o = "123";
if (o instanceof String) {
IO.println("This object is a String!");
}
}
Inside an if
you give the name of a field or variable
whose type is Object
. Then you write instanceof
followed by the type you want to see if that object
is an instance of.
You can also give a variable name after the type.
This will let you call methods from the actual type that are otherwise
unavailable when all Java knows is that you have an Object
.
void main() {
Object o = "123";
if (o instanceof String s) {
IO.println(
"Can call String methods after recovering the type: " + s.charAt(0)
);
}
}
toString
All Object
s have a toString
method.
This is intended to be a suitable representation for debugging, although one may choose to favor different concerns.
For built-in classes the result of their toString
method is likely
what you'd expect.
class Apple {}
void main() {
Object o = "123";
// If its already a String, toString() doesn't
// have to do much work
IO.println(o.toString());
o = 123;
// Integers, Longs, etc. all have a representation
// which looks the same as they do in literal form.
IO.println(o.toString());
o = new Apple();
// And custom classes will, by default, just have the
// class name followed by gibberish
IO.println(o.toString());
}
Override toString
To customize the behavior of toString
in your own classes you need to "override"
the toString
method from Object
.
What this means is that you need to define a method
which has the same name, arguments, return type, and visibility
as the one defined in Object
.
That is, a public
method named toString
which takes no
arguments and returns a String
.
class Window {
public String toString() {
return "Window!";
}
}
void main() {
Object o = new Window();
IO.println(o);
}
This is how you can customize the output of IO.println
.
It is common practice for a class holding data to
include the values of its fields in its toString
representation.
This can be very helpful for debugging.
class Position {
int x;
int y;
Position(int x, int y) {
this.x = x;
this.y = y;
}
public String toString() {
return "Position[x=" + x + ", y=" + y + "]";
}
}
void main() {
Object o = new Position(9, 8);
IO.println(o);
}
@Override
If you intend to override a method you should put
@Override
above that method.
class Position {
int x;
int y;
Position(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public String toString() {
return "Position[x=" + x + ", y=" + y + "]";
}
}
void main() {
Object o = new Position(9, 8);
IO.println(o);
}
This doesn't change anything about how the program works,
but it is a signal to Java that you intended to be overriding a method.
If you get something wrong with the name, visibility, return type,
or argument types of the method then putting @Override
means Java will warn you about those sorts of issues.
class Position {
int x;
int y;
Position(int x, int y) {
this.x = x;
this.y = y;
}
// toString on Object doesn't take in an int
// but this would otherwise be allowed
// since its technically a distinct method
@Override
public String toString(int value) {
return "Position[x=" + x + ", y=" + y + "]";
}
}
void main() {
Object o = new Position(9, 8);
IO.println(o);
}
equals and hashCode
In addition to toString
, two methods that all Object
s have defined
are equals
and hashCode
.
equals
is a method that takes another object and returns a boolean
based
on whether you would consider those objects to be equivalent.
By default, equals
will behave identically to ==
.
class Thing {}
class Main {
void main() {
var t1 = new Thing();
var t2 = new Thing();
IO.println(t1 == t1);
IO.println(t1.equals(t1));
IO.println(t2 == t2);
IO.println(t2.equals(t2));
IO.println(t1 == t2);
IO.println(t1.equals(t2));
}
}
Many types will have equals
overridden to do things like return equal if
the types represent the same context, as is the case with Integer
.
class Main {
void main() {
Integer a = 3;
Integer b = 3;
Integer c = 4;
IO.println(a.equals(b));
IO.println(a.equals(c));
}
}
hashCode
is a method that tells you if things might be equal. It returns an int
.
If two objects give different hash codes then its assumed that the result of a.equals(b)
will be false
. If they give the same hash code, then the result of a.equals(b)
might
be either true
or false
.
Its sort-of like asking what the first letter of someone's name is. If their names start with different letters
they definitely have different names. If their names both start with B
they might both be named Bob, but maybe one is Bob and the other is Barry.
class Thing {}
class Main {
void main() {
String a = "abc";
String b = "abc";
String c = "bca";
IO.println(a.hashCode());
// a.equals(b) will return true, so they will have the same hash code
IO.println(b.hashCode());
// a.equals(c) will return false, so they may or may not have the same hash code
IO.println(c.hashCode());
Thing t1 = new Thing();
Thing t2 = new Thing();
// The default .equals() is the same as ==
IO.println(t1.hashCode());
IO.println(t2.hashCode());
}
}
Override equals and hashCode
If you want to customize the equals
method of an object the
general pattern for doing so is the following.
First, declare an equals method that matches the one that comes from Object
.
Don't forget @Override
.
class Position {
int x;
int y;
@Override
public boolean equals(Object o) {
}
}
This definition of equals accepts any Object
as an argument. Therefore we first
need to make sure that the class of that Object
is the same as the class you are
comparing it to.
class Position {
int x;
int y;
@Override
public boolean equals(Object o) {
if (o instanceof Position otherPosition) {
}
else {
return false;
}
}
}
Then you compare all the fields to make sure they are equal to each other as well.
class Position {
int x;
int y;
@Override
public boolean equals(Object o) {
if (o instanceof Position otherPosition) {
return this.x == otherPosition.x && this.y == otherPosition.y;
}
else {
return false;
}
}
}
How you do those comparisons depends on what is in the fields. For int
and such you can use ==
. For fields
like String
you need to use .equals
.
class Tree {
String barkDescription;
@Override
public boolean equals(Object o) {
if (o instanceof Tree otherTree) {
//
return this.barkDescription.equals(otherTree.barkDescription);
}
else {
return false;
}
}
}
If you anticipate, or don't take actions to prevent, a field potentially being null
then instead of a.equals(b)
use java.util.Objects.equals
. All it does special is handle
null
without crashing.
import java.util.Objects;
class Tree {
String barkDescription;
@Override
public boolean equals(Object o) {
if (o instanceof Tree otherTree) {
return Objects.equals(otherTree.barkDescription);
}
else {
return false;
}
}
}
Whenever you override equals
you are supposed to override hashCode
as well.
This is because - by default - every Object
is going to give a hashCode
consistent
with the default equals
method. If you change how equals
works, then you could
violate the contract of "if hashCode gives a different value, they definitely aren't equal."
Some parts of Java will rely on that, so its worth addressing.
If you define your equals
method as above - essentially "they are equal if all their fields are equal" - then you can use java.util.Objects.hash
to define your hashCode
.1
import java.util.Objects;
class Position {
int x;
int y;
@Override
public boolean equals(Object o) {
if (o instanceof Position otherPosition) {
return this.x == otherPosition.x && this.y == otherPosition.y;
}
else {
return false;
}
}
@Override
public int hashCode() {
// Just give it all the fields you have.
return Objects.hash(this.x, this.y);
}
}
If you defined a more exotic form of equals
then how to properly make a hashCode
is an exercise for you, the reader. If you give up then return 1;
will always be "correct," if not ideal for the code
that uses hashCode
as a quick "might be equal" check.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
The Witch
class has a method named pullFromHat
that returns an Object
that is sometimes a Spell
and sometimes a Broom
.
If it is a Broom
call the .fly()
method on it. If it is a Spell
call .cast
.
class Spell {
private final String name;
Spell(String name) {
this.name = name;
}
void cast() {
IO.println("Casting " + name + "...");
}
}
class Broom {
void fly() {
IO.println("Flying!");
}
}
class Witch {
Object pullFromHat() {
double r = Math.random();
if (r < 0.25) {
return new Spell("Ensmallen");
}
else if (r < 0.5) {
return new Spell("Embiggen");
}
else if (r < 0.75) {
return new Spell("Enlongen");
}
else {
return new Broom();
}
}
}
class Main {
void main() {
var witch = new Witch();
Object item = witch.pullFromHat();
// CODE HERE
}
}
Challenge 2.
Will the following code work? Why or why not?
class Main {
void main() {
String s = "abc";
Object o = s;
IO.println(o.length());
}
}
Challenge 3.
Will the following code work? Why or why not?
class Main {
void main() {
Object o = "abc";
String s = o;
IO.println(s.length());
}
}
Challenge 4.
Implement toString
, equals
, and hashCode
on the following Ogre
class such that it behaves
the same as an Ogre
record.
record Ogre(String name, double strength) {}
class Ogre {
final String name;
final double strength;
Ogre(String name, double strength) {
this.name = name;
this.strength = strength;
}
// CODE HERE
}
class Main {
void main() {
Ogre o1 = new Ogre("Morihito", 100);
Ogre o2 = new Ogre("Morihito", 100);
IO.println(o1); // Ogre[name=Morihito, strength=100]
IO.println(o1.equals(o2)); // True
IO.println(o1.hashCode() == o2.hashCode()); // True
IO.println(o1.equals(new Ogre("Reiji", 5))); // False
IO.println(o1.hashCode() == new Ogre("Reiji", 5).hashCode()); // False
}
}
Generics

Certain types of classes, like growable arrays, are simply holders of data.
That is, almost none of how they work has to change to store different kinds of data.
Generics help us make these generically useful containers.
class Box<T> {
T value;
}
Type Variables
After the name of a class in the class definition you can put one or more "type variables."
These look like <
followed by a comma separated list of "type names" and ended by a >
.
class Box<T> {
}
Inside a class definition you can use these type variables on fields, method arguments, and method return types.
class Box<T> {
T data;
void setData(T newData) {
this.data = newData;
}
}
This indicates that you would be okay with any type being used in place of the type variable (in the above code T
).
Naming
Type variables don't have to be only a single letter, though that is common. If you pick a longer name you are generally expected to name them as if they were classes.
class Box<Data> {
Data data;
}
Instantiation
When you make an instance of a class that takes generic parameters you are expected to provide the actual concrete types you want to be used in place of any type variables.
You do this by writing the name of the types inside a <>
alongside the call to new
.
class Box<T> {
T data;
}
void main() {
var boxOfString = new Box<String>();
boxOfString.data = "abc";
String s = boxOfString.data;
IO.println(s);
}
Inference
Sometimes Java has enough information to know what the right values for type variables would be without you specifying them.
In these cases you can rely on the compiler to infer them by writing <>
with no type variables
in between.
class Box<T> {
T data;
}
void main() {
// It is being assigned to a Box<String> on the left
// so Java can figure out what should be on the right.
Box<String> boxOfString = new Box<>();
boxOfString.data = "abc";
String s = boxOfString.data;
IO.println(s);
}
This inference does not work on the left-hand side of an =
.
class Box<T> {
T data;
}
void main() {
// Use var if you want inference for local variables.
Box<> boxOfString = new Box<String>();
boxOfString.data = "abc";
String s = boxOfString.data;
IO.println(s);
}
Soundness
Even though every String
is assignable to Object
,
a Box<String>
is not assignable to Box<Object>
.
If that was allowed then you could do something like the following.
Box<String> stringBox = new Box<>();
// This might feel innocent
Box<Object> objectBox = stringBox;
// But now we can put anything,
// like an Integer, in the Box
objectBox.data = 123;
// Which is a problem since that affects
// stringBox as well
String s = stringBox.data; // But its actually an Integer! Crash!
We call this property - that the types don't let you accidentally make silly situations - soundness. Java isn't fully sound, but its sound enough for most people.
Raw Types
If generics are cramping your style, you are technically allowed to turn them off.
If you make an instance of a generic class without
any <>
we call that a "raw type."
class Box<T> {
T data;
}
void main() {
Box b = new Box();
}
When you have a raw type you will see Object
in any place
you put1 a type variable.
This lets you do whatever you want without the burden of having to make sense to Java.
class Box<T> {
T data;
}
void main() {
Box b = new Box();
b.data = 123;
b.data = "abc";
if (b.data instanceof String s) {
IO.println(s);
}
}
Raw types exist for two basic reasons
- Every now and then Java isn't smart enough. Trust that there are valid reasons to turn off generics, even if I haven't shown you any yet. Avoid doing so yourself - at least for awhile.
- Generics weren't always in Java! Classes that later were made generic had to stay compatible with old "raw" usages somehow.
All that is to say: Be aware raw types exist. Make sure you are always putting <>
otherwise you are falling
into that mode. Avoid that mode.
An "unbounded" type variable to be exact. We'll visit generic bounds later.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Will the following code work? Why or why not?
class Thing<T> {
T value;
}
class Cool {
}
class NotCool {
}
class Main {
void main() {
int n = Integer.parseInt(
IO.readln("Give a number: ")
);
Thing<Object> o = (n % 2 == 0)
? new Thing<Cool>()
: new Thing<NotCool>();
}
}
Challenge 2.
Make a class that holds a NotNull
value. This class should be generic over the kind of
data that it holds but it should throw an exception if the provided value
is null
.
// CODE HERE
class Main {
void main() {
NotNull<String> s = new NotNull<>("abc");
IO.println(s.value);
NotNull<Integer> i = new NotNull<>(123);
IO.println(i.value);
// This should throw an exception
// NotNull<Double> d = new NotNull<>(null);
}
}
Challenge 3.
The following class has 5 generic parameters.
Correct the ones which do not follow expected naming conventions
class Organism<name, h, T, Cat, inch_worm> {
name name;
h h;
T t;
Cat cat;
inch_worm inchWorm;
}
class Main {
void main() {
var o = new Organism<String, Integer, Integer, String, String>();
o.name = "abc";
o.h = 123;
o.t = 5;
o.cat = "...";
o.inchWorm = "\\_/-\\--(*)";
}
}
Interfaces

Generics let you write code that doesn't care
what is different between different things - you would accept a String
or an Integer
, doesn't matter what is different between them.
Interfaces do a related thing. They let you write code that takes advantage of commonalities.
interface Dog {
void bark();
}
Interface Declaration
To declare an interface you write the word interface
followed by the name of the interface
and {}
.
interface Dog {}
Inside of the {}
you can write the signatures for methods followed by a ;
. That means
no method body and no public
or private
modifiers.
interface Dog {
void bark();
String fetch(String ball);
}
Implementation
All interfaces do1 is hold method declarations.
If you want to have a class which "implements"
the interface you can do so by writing implements
followed by the interface
name.
interface Dog {
void bark();
String fetch(String ball);
}
class Mutt implements Dog {
}
Then all you need to do is declare methods which match up with the methods defined in the interface.
Keep in mind that while you didn't write public
in the interface, you need to write public
when implementing a method from an interface.2
interface Dog {
void bark();
String fetch(String ball);
}
class Mutt implements Dog {
public void bark() {
IO.println("Bark");
}
public String fetch(String ball) {
return ball + " (with drool)";
}
}
For now*
All methods that come from an interface must be public
.
@Override
Just like when defining your own equals
, hashCode
, and toString
you
can use @Override
when implementing methods that come from an interface.
interface Dog {
void bark();
String fetch(String ball);
}
class Mutt implements Dog {
@Override
public void bark() {
IO.println("Bark");
}
@Override
public String fetch(String ball) {
return ball + " (with drool)";
}
}
Right now there isn't a mechanical use for this since Java will yell at you if you defined the interface method wrong anyways, but there will be later. A small benefit is that it makes it easier to tell at a glance which methods come from an interface and which do not.
Naming
Interfaces are named in the same way as classes - LikeThis
.
In the wild west of the real world you might see people prefix any interface
name with I
, for interface.
In these cases instead of Dog
you would see IDog
. Instead of PartyAnimal
you would
see IPartyAnimal
and so on.
The reason someone might do this is if they think it is worthwhile to have a visual indicator of whether a type represents an interface or an actual class. Personally, I don't think that is too useful, but there is nothing horrible about it.
Just judge the social context you are in and don't hold it against people too hard if they do it a way you don't like.
Subtypes
Just as everything is a subtype of Object
, anything which
implements an interface is a subtype of that interface.
For the following code this means that any field or variable
which holds a Dog
can be assigned to an instance of Mutt
.
interface Dog {
void bark();
String fetch(String ball);
}
class Mutt implements Dog {
@Override
public void bark() {
IO.println("Bark");
}
@Override
public String fetch(String ball) {
return ball + " (with drool)";
}
}
void main() {
Dog dog = new Mutt();
}
Through a Dog
variable you will be able to call any methods defined on the interface.
These will use the actual underlying implementation - in this case from Mutt
.
interface Dog {
void bark();
String fetch(String ball);
}
class Mutt implements Dog {
@Override
public void bark() {
IO.println("Bark");
}
@Override
public String fetch(String ball) {
return ball + " (with drool)";
}
}
void main() {
Dog dog = new Mutt();
dog.bark();
IO.println(dog.fetch("Ball"));
}
Multiple Implementations
Interfaces can be implemented any number of times. This means that code which accepts an interface can't truly know the specifics of how methods will work.
This is a problem if you want maximum predictability but its also the whole point of using an interface over a regular class. You can write code that depends on a few key methods being defined and be flexible to different ways of defining those methods.
interface Dog {
void bark();
String fetch(String ball);
}
class Mutt implements Dog {
@Override
public void bark() {
IO.println("Bark");
}
@Override
public String fetch(String ball) {
return ball + " (with drool)";
}
}
class Cat implements Dog {
@Override
public void bark() {
IO.println("Meow");
}
@Override
public String fetch(String ball) {
return "no.";
}
}
void barkAndFetch(Dog dog) {
dog.bark();
IO.println(dog.fetch("Ball"));
}
void main() {
barkAndFetch(new Mutt());
barkAndFetch(new Cat());
}
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Declare an interface named DoubleArray
which requires
two methods of classes that implementing classes: length
and get
.
These methods should be specified to work the same as how []
and .length
work on a double[]
.
interface DoubleArray {
// CODE HERE
}
Challenge 2.
Make a class that implements your DoubleArray
interface
using a double[]
in a field to perform all the operations.
interface DoubleArray {
// CODE FROM PREVIOUS CHALLENGE
}
class RealDoubleArray /* CODE HERE */ {
// CODE HERE
}
class Main {
void main() {
DoubleArray arr = new RealDoubleArray(new double[] {
1.0, 1.5, 2.0, 2.5, 3.0
});
for (int i = 0; i < arr.length(); i++) {
IO.println("Got double value: " + arr.get(i));
}
}
}
Challenge 3.
Make a second class that implements DoubleArray
but have this one
be backed by an int[]
and perform widening conversions when returning values.
interface DoubleArray {
// CODE FROM PREVIOUS CHALLENGE
}
class FauxDoubleArray /* CODE HERE */ {
// CODE HERE
}
class Main {
void main() {
DoubleArray arr = new FauxDoubleArray(new int[] {
1, 2, 3, 4, 5
});
for (int i = 0; i < arr.length(); i++) {
IO.println("Got double value: " + arr.get(i));
}
}
}
Challenge 4.
Make an implementation of the following Tarot
interface for each tarot card
featured in JoJo's Bizzare Adventure.
interface Tarot {
String symbolism();
String standUser();
}
Challenge 5.
Make a method named promptGeneric
which can prompt the user for information
but, based on if what they typed is properly interpretable, can reprompt them.
As part of this make a Parser
interface and at least two implementations: IntParser
and DoubleParser
.
// CODE HERE
class Main {
// CODE HERE
void main() {
int x = promptGeneric(
"Give me an x: ", new IntParser()
);
double y = promptGeneric(
"Give me a floating point y: ", new DoubleParser()
);
}
}
Time

Wikipedia defines "time" as "The continued sequence of existence and events that occurs in an apparently irreversible succession from the past, through the present, and into the future."1
Most everything that interacts with the real world needs to understand information about time. As such, Java has various ways to work with data that holds information about time.
https://en.wikipedia.org/wiki/Time
Instant
A java.time.Instant
stores information on a particular moment in time.
You can get the current "instant" with Instant.now
.
import java.time.Instant;
void main() {
var now = Instant.now();
IO.println(now);
}
But if you happen to know the number of milliseconds since January 1, 1970 0:00 UTC1, you
can get an Instant
which represents that point in time with Instant.ofEpochMilli
.
import java.time.Instant;
void main() {
var january2nd = Instant.ofEpochMilli(86400000);
IO.println(january2nd);
}
https://en.wikipedia.org/wiki/Unix_time
Duration
A Duration
stores a duration of time.
You can make these with Duration.ofMinutes
, Duration.ofMillis
and other similarly named methods.
import java.time.Duration;
void main() {
var fiveMinutes = Duration.ofMinutes(5);
IO.println(fiveMinutes);
var twelveMilliSeconds = Duration.ofMillis(12);
IO.println(twelveMilliSeconds);
}
You can use these to get the duration between two Instant
s with
Duration.between
.
import java.time.Instant;
import java.time.Duration;
void main() {
var january2nd = Instant.ofEpochMilli(86400000);
IO.println(january2nd);
var january3rd = Instant.ofEpochMilli(86400000 * 2);
IO.println(january3rd);
Duration twentyFourHours = Duration.between(january2nd, january3rd);
IO.println(twentyFourHours);
}
And you can move Instant
s by a given Duration
of time using its .plus
and .minus
methods.
import java.time.Instant;
import java.time.Duration;
void main() {
var january1st = Instant.ofEpochMilli(0);
IO.println(january1st);
IO.println(
january1st.plus(Duration.ofHours(45))
);
IO.println(
january1st.minus(Duration.ofHours(1))
);
}
LocalDate
A local date is something like "January 10th, 2024."
This just records a day, month, and year. It doesn't know in what part of the world it was January 10th 2024, just that somewhere the "local" date was that.
You can make a LocalDate
with LocalDate.of
.
import java.time.LocalDate;
void main() {
var jan10 = LocalDate.of(2024, 1, 10);
IO.println(jan10);
}
And you can get the current LocalDate
in whatever timezone
your computer is programmed to be in with LocalDate.now();
import java.time.LocalDate;
void main() {
var now = LocalDate.now();
IO.println(now);
}
LocalTime
In the same way a local date leaves off context about where it was and what time it was, a local time just stores the time.
This means it will hold the exact time during the day, down to the nanoseconds.
You can make a LocalTime
with LocalTime.of
giving it the hours, minutes, and
seconds into the day.
import java.time.LocalTime;
void main() {
var tenTwentyFour = LocalTime.of(10, 24, 0);
IO.println(tenTwentyFour);
}
And similarly you can get the current time your computer thinks it is with LocalTime.now()
import java.time.LocalTime;
void main() {
var now = LocalTime.now();
IO.println(now);
}
LocalDateTime
What do you get if you combine a LocalDate
and a LocalTime
? A LocalDateTime
!
A LocalDateTime
stores dates and times.
If you have both a LocalDate
and a LocalTime
you can combine them
with LocalDateTime.of
.
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.LocalDateTime;
class Main {
void main() {
var jan10 = LocalDate.of(2024, 1, 10);
var tenTwentyFour = LocalTime.of(10, 24, 0);
IO.println(LocalDateTime.of(jan10, tenTwentyFour));
}
}
Time Zones
Partially because of how day night cycles work and sometimes because of arcane rules made up to help out farmers, we have time zones.
This means that while it might be 9am in Boston it would simultaneously be 10pm in Tokyo. Boston and Tokyo are in different time zones.
You can get access to the time zone your computer
is using with ZoneId.systemDefault()
. This gives you
a ZoneId
which identifies a time zone.
import java.time.ZoneId;
class Main {
void main() {
ZoneId tz = ZoneId.systemDefault();
IO.println(tz);
}
}
If you want to get the identifier for a different time zone
you use ZoneId.of
and give it a String
identifying
the time zone. These come from a big list
published by the Internet Assigned Numbers Authority (IANA).
import java.time.ZoneId;
class Main {
void main() {
// Eastern Standard Time
ZoneId tz = ZoneId.of("US/Eastern");
IO.println(tz);
}
}
While you won't be using this directly, every ZoneId
has the information needed to determine what time it would
be accessible via getRules()
.
import java.time.ZoneId;
class Main {
void main() {
// Eastern Standard Time
ZoneId tz = ZoneId.of("US/Eastern");
IO.println(tz.getRules());
}
}
And it is this machinery used by the parts of the time API which determine the exact time it would be in a given time zone.
ZonedDateTime
A ZonedDateTime
has all the information of
a LocalDateTime
, but with the addition of a time zone.
These are useful for recording the time that events took place in a way that can be communicated.
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.time.ZoneId;
class Main {
void main() {
var jan10 = LocalDate.of(2024, 1, 10);
var tenTwentyFour = LocalTime.of(10, 24, 0);
var est = ZoneId.of("US/Eastern");
LocalDateTime localDateTime = LocalDateTime.of(jan10, tenTwentyFour);
ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime, est);
IO.println(zonedDateTime);
}
}
You can get the current ZonedDateTime
for the time zone your computer is running in
with ZonedDateTime.now()
.
import java.time.ZonedDateTime;
class Main {
void main() {
var now = ZonedDateTime.now();
IO.println(now);
}
}
And you can do the same for an arbitrary time zone by giving a ZoneId
to
now
.
import java.time.ZonedDateTime;
import java.time.ZoneId;
class Main {
void main() {
var now = ZonedDateTime.now(ZoneId.of("US/Eastern"));
IO.println(now);
}
}
OffsetDateTime
An OffsetDateTime
is similar to a ZonedDateTime
but with the key difference
that an OffsetDateTime
doesn't record a moment in a specific time zone, but instead
as an offset from the UTC1 timezone.
This is useful because timezones change their rules frequently. If you had to pick a representation of dates and times to store, this is a good default.
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.LocalDateTime;
import java.time.ZonedDateTime;
import java.time.ZoneId;
class Main {
void main() {
var feb14 = LocalDate.of(2025, 2, 14);
var fiveTwentyThree = LocalTime.of(5, 23, 0);
var est = ZoneId.of("US/Eastern");
LocalDateTime localDateTime = LocalDateTime.of(feb14, fiveTwentyThree);
ZonedDateTime zonedDateTime = ZonedDateTime.of(localDateTime, est);
OffsetDateTime offsetDateTime = zonedDateTime.toOffsetDateTime();
IO.println(offsetDateTime);
}
}
You can get the current OffsetDateTime
for the time zone your computer is running in
with OffsetDateTime.now()
.
import java.time.OffsetDateTime;
class Main {
void main() {
var now = OffsetDateTime.now();
IO.println(now);
}
}
And you can do the same for an arbitrary time zone by giving a ZoneId
to
now
. Java knows the UTC offset for every time zone.
import java.time.OffsetDateTime;
import java.time.ZoneId;
class Main {
void main() {
var now = OffsetDateTime.now(ZoneId.of("US/Eastern"));
IO.println(now);
}
}
UTC stands for "Coordinated Universal Time." No I don't know why the letters are in a different order and at this point I'm too afraid to ask.
Date
There is one class related to time that isn't much like the others: java.util.Date
.
Java did not originally come with the java.time
classes. At first it just had Date
.
import java.util.Date;
class Main {
void main() {
Date date = new Date();
IO.println(date);
}
}
Date
is somewhat of a chimera between Instant
and ZonedDateTime
. It represents an instant in time but specifically in the UTC timezone.1
It is important to know about because a lot of code, including code that comes with Java, makes use of it.
Whenever you see Date
in the wild you should usually turn it into an Instant
by
calling .toInstant()
.
import java.time.Instant;
import java.util.Date;
class Main {
void main() {
Date date = new Date();
IO.println(date);
Instant instant = date.toInstant();
IO.println(instant);
}
}
You can also construct a Date
from an Instant
using Date.from
. This is useful if there is some code that wants a Date
as an argument.
import java.time.Instant;
import java.util.Date;
class Main {
void main() {
var instant = Instant.now();
IO.println(instant);
Date date = Date.from(instant);
IO.println(date);
}
}
To be clear though, Date
has problems. We aren't ready to explain all of them yet. Just treat Date
as haunted, as in by ghosts, and use the java.time
alternatives when you can.
You will notice that when we print out the date we get GMT. GMT is basically the same as UTC, though the documentation for Date
explains the difference.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Print out every day from January 1st to December 31st. When the program reaches your birthday make it print out "Happy Birthday <your name>" instead of the date.
class Main {
void main() {
// CODE HERE
}
}
Challenge 2.
Make a Poison
class which has a Duration
field which stores how long
the poison will be potent for as well as an Instant
at which the
poison was brewed.
Implement a method that takes an Instant
and returns if the Poison
will be expired by that point.
import java.time.Duration;
import java.time.Instant;
class Poison {
// CODE HERE
}
class Main {
void main() {
var hemlock = new Poison(Instant.now(), Duration.ofDays(365 * 3));
IO.println(hemlock.isPotentAt(Instant.now())); // true
IO.println(hemlock.isPotentAt(Instant.now().plus(Duration.ofDays(5))); // true
IO.println(hemlock.isPotentAt(Instant.now().plus(Duration.ofDays(365 * 10)))); // false
}
}
Challenge 3.
Get as input using IO.readln
a day
, month
, year
, and UTC offset.
Interpret that input as an OffsetDateTime
then print how many seconds will have
passed between that offset date time and midnight of January 1st 1983 GMT.
class Main {
void main() {
// CODE HERE
}
}
Challenge 4.
A train leaves Boston at 12:50pm EDT on August 23rd 2025 and arrives in Chicago at 10:12am CDT August 24th 2025.
How many minutes long was that train ride? Use the Java's time classes to figure out the answer.
class Main {
void main() {
// CODE HERE
}
}
As a small hint, you will first want to represent those events as ZonedDateTime
s, convert the ZonedDateTime
s to Instant
s, and then get the Duration
between those Instant
s. Then get the number
of minutes in that Duration
.
ArrayList

Java comes with a generic growable array. It is called
an ArrayList
.
import java.util.ArrayList;
void main() {
ArrayList<String> names = new ArrayList<>();
names.add("John Wick");
IO.println(names);
}
Ubiquity
Java comes with many classes. Some of these
you might use a few times (like LocalTime
), some you
will see every day of your coding life (like String
).
ArrayList
is in the second category. It turns out
that a growable list of things is needed for a lot
of different tasks. It is ubiquitous in code that
you will see in the real world.
I mention this mostly to make sure you are paying attention. Its not particuarly hard, but try not to zone out on these sorts of chapters.
Add an item
To add an item to an ArrayList
you use the .add
method.
import java.util.ArrayList;
void main() {
ArrayList<String> names = new ArrayList<>();
names.add("The Bowery King");
names.add("Caine");
IO.println(names);
}
The way this works is conceptually the same as the growable array that we went over earlier.
All you need to know though is that you can add as many items as you will and the container will grow dynamically.
Size
Then you can access the number of elements in an
ArrayList
with the .size()
method.
import java.util.ArrayList;
void main() {
ArrayList<String> names = new ArrayList<>();
IO.println(names.size());
names.add("Vincent Bisset de Gramont");
IO.println(names.size());
names.add("Mr. Nobody");
IO.println(names.size());
}
This will tell you the number of elements in the ArrayList
but
not the amount of space allocated by the underlying array.
This is fine because, if you aren't the one making a growable array, it doesn't matter. All you need to concern yourself with is what is actually there, not any book keeping.
Get an item
To get an item at a given index in an ArrayList
you should use .get
import java.util.ArrayList;
void main() {
ArrayList<String> names = new ArrayList<>();
names.add("Winston Scott");
String name = names.get(0);
IO.println(name);
}
If the index you picked is greater than the number of elements in the ArrayList
it will throw an IndexOutOfBoundsException
import java.util.ArrayList;
void main() {
ArrayList<String> names = new ArrayList<>();
names.add("Killa Harkan");
String name = names.get(10);
IO.println(name);
}
Loop over Contents
Just like the .length
and []
operations on arrays,
you can loop over all the elements in an ArrayList
with the combination of .size()
and .get(idx)
import java.util.ArrayList;
void main() {
ArrayList<String> names = new ArrayList<>();
names.add("Sofia Al-Azwar");
names.add("Viggo Tarasov");
names.add("Iosef Tarasov");
for (int i = 0; i < names.size(); i++) {
String name = names.get(i);
IO.println("NAME: " + name);
}
}
Its not a rule, but this is also a convenient use of for loops. You can use all the same tricks you learned when looping over arrays here.
Set an item
You can set an item at an index with .set
import java.util.ArrayList;
void main() {
ArrayList<String> names = new ArrayList<>();
names.add("John Wick");
names.set(0, "Baba Yaga");
IO.println(names);
IO.println(names.get(0));
}
If the index you provide is out of bounds, you will get an IndexOutOfBoundsException
.
import java.util.ArrayList;
void main() {
ArrayList<String> names = new ArrayList<>();
names.add("John Wick");
names.set(15, "Baba Yaga");
}
Remove an item
You can use .remove
to remove an item
from an ArrayList
.
To do this you need to provide it the value you want to remove. It will do all the array resizing required internally, even if the item is in the middle of a list.
import java.util.ArrayList;
void main() {
ArrayList<String> names = new ArrayList<>();
names.add("The Bowry King");
names.add("The Elder");
names.add("The Harbinger");
IO.println(names);
names.remove("The Elder");
IO.println(names);
}
Alternatively you can remove an item by its index.
import java.util.ArrayList;
void main() {
ArrayList<String> names = new ArrayList<>();
names.add("The Bowry King");
names.add("The Elder");
names.add("The Harbinger");
IO.println(names);
names.remove(2);
names.remove(0);
IO.println(names);
}
You need to be careful about that though. If your ArrayList
holds Integer
s then
Java can get confused between the implementation of remove that removes using the item itself
and the one that removes by index.
import java.util.ArrayList;
void main() {
ArrayList<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
IO.println(numbers);
// Notice that this removes "2" which is at index 1!
numbers.remove(1);
IO.println(numbers);
}
This comes down to int
and Integer
being slightly different. When Java
sees an integer literal it assumes it is an int
. It is only when circumstances
are such that it cannot possibly be an int
that it automatically boxes it
into an Integer
.
It is rare, but if you encounter this issue you can use Integer.valueOf
to deal with it.
import java.util.ArrayList;
void main() {
ArrayList<Integer> numbers = new ArrayList<>();
numbers.add(1);
numbers.add(2);
numbers.add(3);
IO.println(numbers);
numbers.remove(Integer.valueOf(1));
IO.println(numbers);
}
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Replace at least one custom usage of a growable array in the code you have written so far
with an ArrayList
.
Challenge 2.
Capitalize every String
in the ArrayList<String>
.
class Main {
void main() {
ArrayList<String> things = new ArrayList<>();
things.add("movies");
things.add("television");
things.add("video games");
// CODE HERE
// Should output
// [MOVIES, TELEVISION, VIDEO GAMES]
IO.println(things);
}
}
Challenge 3.
Every time John Wick assassinates someone he gets one crime coin (to spend at a crime hotel).
Watch the first John Wick movie. For each named character you can remember
John Wick assassinating add one crime coin to the johnWick
ArrayList.
record CrimeCoin(String target) {}
class Main {
void main() {
ArrayList<CrimeCoin> johnWick = new ArrayList<>();
// CODE HERE
IO.println(johnWick);
}
}
If you need to cheat you can find the list here.
Challenge 4.
Using the ArrayList
of CrimeCoin
s, construct an ArrayList<String>
with all the characters'
names.
record CrimeCoin(String target) {}
class Main {
void main() {
ArrayList<CrimeCoin> johnWick = new ArrayList<>();
// CODE FROM LAST CHALLENGE
ArrayList<String> names = new ArrayList<>();
// CODE HERE
IO.println(names);
}
}
HashMap

Arrays and their growable cousin ArrayList
store a sequence of elements.
This is often enough, but if you want to find an element in an array you usually
need to check every element one by one.
record Character(
String name,
boolean protaganist
) {}
Character findCharacter(
Character[] cast,
String name
) {
for (var character : cast) {
if (character.name().equals(name)) {
return character;
}
}
return null;
}
void main() {
Character[] carsCharacters = new Character[] {
new Character("Tow Mater", false),
new Character("Lightning McQueen", true),
new Character("Doc Hudson", false)
};
IO.println(findCharacter(carsCharacters, "Lightning McQueen"));
IO.println(findCharacter(carsCharacters, "Blade Ranger"));
}
For small arrays, this is no biggie. A computer can check over a few dozen things pretty quickly.
But for big arrays, this stinks.
What we want is some way to quickly look up something, regardless of how many things there are to check over. This is what a HashMap gives us.
import java.util.HashMap;
record Character(
String name,
boolean protaganist
) {}
void main() {
HashMap<String, Character> carsCharacters = new HashMap<>();
carsCharacters.put(
"Tow Mater",
new Character("Tow Mater", false)
);
carsCharacters.put(
"Lightning McQueen",
new Character("Lightning McQueen", false)
);
carsCharacters.put(
"Doc Hudson",
new Character("Doc Hudson", false)
);
IO.println(carsCharacters.get("Lightning McQueen"));
IO.println(carsCharacters.get("Blade Ranger"));
}
Filing Cabinets
The basic concept behind a Hash Map is the same concept as a filing cabinet.
If you grew up in a doctor's office like I did this will make sense. If you didn't, look up what a filing cabinet looks like.
Basically, if you have folders for a bunch of people you can separate them into different cabinets.
You start by putting everyone in one cabinet. If you want to find someone's records, you look through everything in that cabinet until you find it.1
Cabinet |
---|
Lightning McQueen |
Tow Mater |
Doc Hudson |
Once you have enough records you separate your folders by something like the patient's last name. So you would
have your A-M
and N-Z
cabinets.
A-M | N-Z |
---|---|
Lightning McQueen | Strip Weathers |
Tow Mater | |
Doc Hudson | |
Sally Carrera | |
Chick Hicks |
Then when you want to find someone, you can quicky pick the right cabinet by looking at their last name.
If you got too many people in one cabinet you would then subdivide that cabinet further.
A-H | A-M | N-Z |
---|---|---|
Doc Hudson | Lightning McQueen | Strip Weathers |
Sally Carrera | Tow Mater | |
Chick Hicks |
And this basic system can scale to thousands of names.
For a real filing cabinet system the contents of each cabinet would also be sorted. Ignore that for now.
Keys and Values
A Hash Map "maps" keys to values.
In the filing cabinet analogy we are mapping names to charts full of all sorts of other information on the person.
Put Items
You can associate a key with a value by calling the .put
method.1
import java.util.HashMap;
class Main {
void main() {
var wins = new HashMap<String, Integer>();
wins.put("Lightning McQueen", 2);
wins.put("Tow Mater", 0);
IO.println(wins);
}
}
Cars was a delightful movie. Go to your local second hand store, get a copy on DVD, and make a night of it.
Get Items
You can get the value associated with a key by using .get
. If there is no
value associated with that key it will return null
.
import java.util.HashMap;
class Main {
void main() {
var wins = new HashMap<String, Integer>();
wins.put("Lightning McQueen", 2);
wins.put("Tow Mater", 0);
IO.println(wins.get("Tow Mater"));
IO.println(wins.get("Doc Hudson"));
}
}
Hash Functions
A hash function is something that takes a piece of data and extracts a smaller, but predictable, piece of data from it.
record CarsCharacter(
String firstName,
String lastName
) {}
char hashFunction(
String lastName
) {
return lastName.charAt(0);
}
void main() {
var lightning = new CarsCharacter("Lightning", "McQueen");
char firstOfLast = hashFunction(lightning.lastName());
IO.println(firstOfLast);
}
Taking the first letter of a last name is an example of this. You can't
reverse M
into McQueen
, but you can take McQueen
and know to look
in the bucket labeled M
.
We need hash functions to decide what "filing cabinet" a thing should go in.
Hash Collision
It is possible for a hash function to give the same result for two distinct elements.
The first letter of both "Smith" and "Sanders" is "S", so if the hash function takes the first letter of the last name they will have the same hash.
We call this situation a "hash collision." All it means is that these two objects will definitely go into the same bucket.
That's allowed to happen, but if everything goes into the same bucket then there
isn't much point to using a HashMap
over an ArrayList
.
Hash Distribution
When picking a hash function you also need to be worried about "hash distribution."
If you open up a doctor's office in South Boston, you might have an issue
ordering charts by last name only. Your M
cabinet will be overflowing.1
For that scenario, using the first letter of the last name is a non-ideal hash function because when so many people have last names starting with the same letter, they will not be evenly distributed amongst the buckets.
Making a hash function with a good distribution is hard. Objects.hash
will do a decent job of it
and thats why we use it.
"Mc" and "Mac" are common irish surnames and Boston has a sizable irish population.
Reference Based Identity
The hash function that HashMap
uses is .hashCode()
. It uses the number
returned from this method to decide which of the buckets it maintains
to put an item into.
Instead of making buckets like A-G
and H-Z
, it will use ranges of numbers. The concept is the
same though
For classes you make yourself, their hashCode
will be based on what we call an object's
"identity." This means that every individual instance of a class is likely to give you a different
value, regardless of if they hold identical fields.
void main() {
new Main().main();
}
class LivingRaceCar {
int cursedness;
// God, could you imagine being judged by a Honda?
LivingRaceCar(int cursedness) {
this.cursedness = cursedness;
}
}
class Main {
void main() {
var carA = new LivingRaceCar(10);
// Car B is a reference to Car A
var carB = carA;
// Car C is a distinct object with its own identity
var carC = new LivingRaceCar(10);
// Accordingly, Car C will probably have a different
// hashCode. This is despite it having the same
// values in its fields. It is a "distinct" object.
IO.println("A: " + carA.hashCode());
IO.println("B: " + carB.hashCode());
IO.println("C: " + carC.hashCode());
}
}
Identity is also what the default .equals
implementation is based off of.
If two variables reference the same object then .equals
will return true
.
void main() {
new Main().main();
}
class LivingRaceCar {
int cursedness;
LivingRaceCar(int cursedness) {
this.cursedness = cursedness;
}
}
class Main {
void main() {
var carA = new LivingRaceCar(10);
// Car B is a reference to Car A
var carB = carA;
// Car C is a distinct object with its own identity
var carC = new LivingRaceCar(10);
// Car C therefore will only equal itself
// Car A and B will equal each other
IO.println("A.equals(A): " + carA.equals(carA));
IO.println("A.equals(B): " + carA.equals(carB));
IO.println("A.equals(C): " + carA.equals(carC));
IO.println("B.equals(A): " + carB.equals(carA));
IO.println("B.equals(B): " + carB.equals(carB));
IO.println("B.equals(C): " + carB.equals(carC));
IO.println("C.equals(A): " + carC.equals(carA));
IO.println("C.equals(B): " + carC.equals(carB));
IO.println("C.equals(C): " + carC.equals(carC));
}
}
Value Based Identity
While reference based identity can be useful, it's often not what you want for keys in a HashMap
.
Ideally if you are looking up "Tow Mater"
you shouldn't have to be careful to ensure it's the same
instance of String
, all you care about is that it contains the right characters.
We call this notion of identity "value based." Two things are the same if they contain the same data - i.e. if they represent the same value.
class Main {
void main() {
// Strings A and B are distinct instances
var stringA = new String(new char[] { 'a', 'b', 'c' });
var stringB = new String(new char[] { 'a', 'b', 'c' });
// but they will give the same hashCode
IO.println(stringA.hashCode());
IO.println(stringB.hashCode());
// and will be equal to eachother
IO.println(stringA.equals(stringB));
IO.println(stringB.equals(stringA));
}
}
String
s, all the numeric types like Integer
and Double
, as well as Boolean
s are defined
in this way.1 So will any record
s and enum
s you make2.
void main() {
new Main().main();
}
record Pos(int x, int y) {}
class Main {
void main() {
// Positions A and B are distinct instances but hold the same values
var posA = new Pos(5, 5);
var posB = new Pos(5, 5);
// therefore they will give the same hashCode
IO.println(posA.hashCode());
IO.println(posB.hashCode());
// and will be equal to eachother
IO.println(posA.equals(posB));
IO.println(posB.equals(posA));
}
}
There is an important distinction here between things where .equals
and .hashCode
are simply defined in terms of value based equality and whether the objects themselves have identity. Operations like ==
operate on what we might call an "intrinsic identity." Problem for later.
At least by default.
Appropriate Keys
Both objects with reference based and value based definitions of equals
and hashCode
are "appropriate" to use as keys in HashMap
s.
The most important thing to be careful of is using objects where equals
and hashCode
are value based, but the object itself is mutable.
import java.util.Objects;
import java.util.HashMap;
void main() {
new Main().main();
}
class Pos {
int x;
int y;
Pos(int x, int y) {
this.x = x;
this.y = y;
}
// equals and hashCode here are defined in terms of X and Y
@Override
public int hashCode() {
return Objects.hash(x, y);
}
@Override
public boolean equals(Object o) {
return o instanceof Pos p &&
this.x == p.x &&
this.y == p.y;
}
}
class Main {
void main() {
var m = new HashMap<Pos, String>();
// Therefore we can get and put values using a Pos
// as a key.
var p = new Pos(4, 5);
m.put(p, "Slippery Ice");
IO.println(
m.get(p)
);
// But if we were to mutate the object
p.x = 99;
// then the original object might be in the wrong bucket
// inside the hash map!
IO.println(
m.get(p)
);
// And because the key is stored, even if it is in the right bucket
// the equals check might not function correctly
IO.println(
m.get(new Pos(4, 5))
);
}
}
So while Pos
has a "value based identity," you can muck it up by directly changing values. HashMap
s assume
that when something is used as a key its equals
and hashCode
are stable and will not change later in the program.
If something has a value-based definition of equals
and hashCode
but can be changed, that is inappropriate to use as a key.
Ubiquity
HashMaps are very common both in Java and the wider programming world as a whole.
You can mostly get away with just knowing how to use them and what they do, but if you want to take a deeper dive this video is a good start.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Make a Library
class. It should have four exposed methods
add
which takes aBook
and adds it to the library.list
which returns anArrayList<String>
of all theBook
s in the library. TheString
s in this list should be the ISBN of the book.find
which takes aString
representing the ISBN and returns a fullBook
record.remove
which takes aString
representing the ISBN and removes thatBook
from the library.
Use a HashMap
for storing this info in the Library
.
record Book(String isbn, String title, String author) {}
class Library {
// CODE HERE
}
class Main {
void main() {
Library publicLibrary = new Library();
publicLibrary.add(new Book("978-0-8041-3902-1", "The Martian", "Andy Weir"));
publicLibrary.add(new Book("978-0062060624", "The Song of Achilles", "Madeline Miller"));
IO.println(publicLibrary.list());
Book b1 = publicLibrary.find("978-0062060624");
IO.println(b1); // The Song of Achilles
Book b2 = publicLibrary.find("123");
IO.println(b2); // null
publicLibrary.remove("978-0062060624");
Book b3 = publicLibrary.find("978-0062060624");
IO.println(b3); // null
IO.println(publicLibrary.list());
}
}
Challenge 2.
Write a method which takes as an argument an ArrayList<String>
and returns a HashMap<String, Integer>
where each key is
a String
from the original ArrayList
and each value is the number
of times it appeared in that ArrayList
.
class Main {
HashMap<String, Integer> count(ArrayList<String> words) {
// CODE HERE
}
void main() {
ArrayList<String> w = new ArrayList<>();
w.add("duck");
w.add("duck");
w.add("duck");
w.add("goose");
w.add("duck");
w.add("duck");
w.add("duck");
w.add("duck");
w.add("duck");
w.add("duck");
w.add("duck");
w.add("duck");
w.add("goose");
w.add("zebra");
// {duck=11,goose=2,zebra=1}
IO.println(
count(w)
);
}
}
If you feel like this is hard to do with just .put
and .get
, you can
check for other methods on HashMap that can help you.
Challenge 3.
Without calling any methods on the HashMap
, make it so that map.get(person)
returns null
.
class Person {
int age;
Person(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person[age=" + age + "]";
}
@Override
public boolean equals(Object o) {
if (o instanceof Person p) {
return age == p.age;
}
else {
return false;
}
}
@Override
public int hashCode() {
return Objects.hash(age);
}
}
class Main {
void main() {
var person = new Person("Patrocolus");
var map = new HashMap<Person, String>();
map.put(person, "Achilles");
// -------------
// CODE HERE
// Do not directly touch "map"
// -------------
// Should output `null`
IO.println(map.get(person));
}
}
Hyrum's Law
This is Hyrum's Law.
With a sufficient number of users of an API, it does not matter what you promise in the contract: all observable behaviors of your system will be depended on by somebody.
Authority
Hyrum's Law, and many other "laws" in software development, are not really laws in the same way as "The Law of Conservation of Energy."
Those sorts of laws are observed truths about a field. Best we can tell they are always true, no matter what.
Hyrum's Law is just an aphorism. It's one specific person's view on the field.
This sounds sketchy, and it is, but the state of the computing field is such that aphorisms and personal anecdotes are often the best information we have.
Validity
Now is this law literally true? No. It can't be. There are more "properties" you can observe about a piece of software than people on the planet.
The specific number of degrees your CPU heats up when running a program can, hypothetically, be relied on. Chances are that won't happen though.
Personally I think that the expanded explanation on his website (https://www.hyrumslaw.com/) is more nuanced than the short version, if a lot less snappy and easy to remember.
Over the past couple years of doing low-level infrastructure migrations in one of the most complex software systems on the planet, I’ve made some observations about the differences between an interface and its implementations. We typically think of the interface as an abstraction for interacting with a system (like the steering wheel and pedals in a car), and the implementation as the way the system does its work (wheels and an engine). This is useful for a number of reasons, foremost among them that most useful systems rapidly become too complex for a single individual or group to completely understand, and abstractions are essential to managing that complexity.
Defining the correct level of abstraction is a completely separate discussion (see Mythical Man-Month), but we like to think that once an abstraction is defined, it is concrete. In other words, an interface should theoretically provide a clear separation between consumers of a system and its implementers. In practice, this theory breaks down as the use of a system grows and its users start to rely upon implementation details intentionally exposed through the interface, or which they divine through regular use. Spolsky’s “Law of Leaky Abstractions” embodies consumers’ reliance upon internal implementation details.
Taken to its logical extreme, this leads to the following observation, colloquially referred to as “The Law of Implicit Interfaces”: Given enough use, there is no such thing as a private implementation. That is, if an interface has enough consumers, they will collectively depend on every aspect of the implementation, intentionally or not. This effect serves to constrain changes to the implementation, which must now conform to both the explicitly documented interface, as well as the implicit interface captured by usage. We often refer to this phenomenon as "bug-for-bug compatibility."
The creation of the implicit interface usually happens gradually, and interface consumers generally aren’t aware as it’s happening. For example, an interface may make no guarantees about performance, yet consumers often come to expect a certain level of performance from its implementation. Those expectations become part of the implicit interface to a system, and changes to the system must maintain these performance characteristics to continue functioning for its consumers.
Not all consumers depend upon the same implicit interface, but given enough consumers, the implicit interface will eventually exactly match the implementation. At this point, the interface has evaporated: the implementation has become the interface, and any changes to it will violate consumer expectations. With a bit of luck, widespread, comprehensive, and automated testing can detect these new expectations but not ameliorate them.
Implicit interfaces result from the organic growth of large systems, and while we may wish the problem did not exist, designers and engineers would be wise to consider it when building and maintaining complex systems. So be aware of how the implicit interface constrains your system design and evolution, and know that for any reasonably popular system, the interface reaches much deeper than you think.
Emergent Properties
Emergent properties are things that simply "emerge" as a consequence of other factors.
Hyrum's law is one of those sorts of things. Nobody sat down and agreed that they will use an API in wacky ways. It just happens when you throw enough monkeys in a pile and they all reach for a keyboard.
As such, the way to deal with it isn't to put blame upon the monkeys. It's to accept it as a naturally occurring phenominon and plan accordingly.
Importance
So why is this person's take important for you to know about?
The reason is that the core concept - that every observable property will be depended on by somebody with enough consumers - is essential context for understanding why Java is the way that it is.
Many of Java's features are intended to give you ways to minimize the number of observable properties of the software you produce.
Private fields and methods are the easiest examples of this so far. If you make fields or methods private then you can start to expect that consumers of that class do not observe them. Thus, if needed, you will not break anyone by changing them.1
There are asterisks to this particular point, unfortunately. There are ways to magically get at private fields like criminal scum.
Switch III

The switch statement in Java originally came from a little language you might know
about called C
.
In C
switches work slightly differently than the ones you have seen so far in Java.
But, due to this history, there is another kind of switch that doesn't use arrows (->
)
and instead uses a colon (:
).
class Main {
boolean shouldBeMainCharacter(String name) {
switch (name) {
case "Gohan":
return true;
case "Goku":
default:
return false;
}
}
void main() {
IO.println(
shouldBeMainCharacter("Goku")
);
}
}
This "C-Style Switch" is important to learn chiefly because, for a long time, it was the only switch in Java. Therefore in your coding life you are very likely to run into it.
break
After each case label in a C-style switch, you should write break;
.1
class Main {
void main() {
String name = "Piccolo";
switch (name) {
case "Squidward":
IO.println("Invited, but not coming.");
break;
case "Piccolo":
IO.println("Coming to the cookout.");
break;
case "Spider-Man":
IO.println("Not coming");
break;
}
}
}
This is different from breaking out of a loop and won't break out of any surrounding loops.
class Main {
void main() {
for (int i = 0; i < 3; i++) {
// Will still run 3 times
String name = "Piccolo";
switch (name) {
case "Squidward":
IO.println("Invited, but not coming.");
break;
case "Piccolo":
IO.println("Coming to the cookout.");
break;
case "Spider-Man":
IO.println("Not coming");
break;
}
}
}
}
To break out of surrounding loops you can use a labeled break.
class Main {
void main() {
outerLoop:
for (int i = 0; i < 3; i++) {
String name = "Piccolo";
switch (name) {
case "Squidward":
IO.println("Invited, but not coming.");
break;
case "Piccolo":
IO.println("Coming to the cookout.");
break outerLoop; // Will break out of the loop
case "Spider-Man":
IO.println("Not coming");
break;
}
}
}
}
Fallthrough
If the code for a given label does not have a break
then it will "fall through"
to the cases below.
This is what makes C-style switches strange. It can occasionaly be useful if the same code should run for some or all cases, but is annoyingly easy to do on accident.1
class Main {
void sayWhoTheyFought(String name) {
switch (name) {
case "Goku":
IO.println("Fought Pilaf");
IO.println("Fought The Red Ribbon Army");
case "Gohan": // "Goku" will fall through to this case
IO.println("Fought Frieza");
IO.println("Fought Cell");
IO.println("Fought Majin Buu");
}
}
void main() {
sayWhoTheyFought("Gohan");
IO.println("----------------------");
sayWhoTheyFought("Goku");
}
}
This StackExchange Post explains how this came about. I don't have a primary source on the "The reason that C did it that way is that the creators of C intended switch statements to be easy to optimize into a jump table." claim, but it lines up with my biases and preconceptions. Therefore it must be true!
return
If you explicitly return
from inside a C-style switch then there is no need to have a break
to avoid fallthrough.
class Main {
void sayWhoTheyFought(String name) {
switch (name) {
case "Launch":
IO.println("Fought Red Ribbon Army");
IO.println("Fought 3 nameless convicts");
return; // This will return from the whole method
case "Goku":
IO.println("Fought Pilaf");
IO.println("Fought The Red Ribbon Army");
case "Gohan":
IO.println("Fought Frieza");
IO.println("Fought Cell");
IO.println("Fought Majin Buu");
}
}
void main() {
sayWhoTheyFought("Launch");
}
}
This should be intuitive.
default
Just like with normal switches, C-style switches can have a default
label
that matches when no other label does.
class Main {
boolean shouldBeMainCharacter(String name) {
switch (name) {
case "Gohan":
return true;
default:
return false;
}
}
void main() {
IO.println(
shouldBeMainCharacter("Goku")
);
}
}
If you have a C-style switch over an enum you need this default
case for exhaustiveness. Java does not
otherwise accept that you have covered all the cases.
enum Technique {
KAMEHAMEHA,
INSTANT_TRANSMISSION,
KAIOKEN,
ULTRA_INSTINCT
}
class Main {
boolean didGokuStealItFromSomeoneElse(Technique technique) {
switch (technique) {
case KAMEHAMEHA:
IO.println("Master Roshi Taught it to him");
return true;
case INSTANT_TRANSMISSION:
IO.println("Space aliens");
return true;
case KAIOKEN:
IO.println("King Kai's name is in it!");
return true;
case ULTRA_INSTINCT:
IO.println("I'd say not");
return false;
// Even though we covered every option, Java doesn't trust us.
// You need a default label or to return later in the function
//
// default:
// throw new IllegalStateException("Unexpected: " + technique);
}
}
void main() {
IO.println(
didGokuStealItFromSomeoneElse(Technique.INSTANT_TRANSMISSION)
);
}
}
yield
If every branch of a C-style switch yield
s a value, you can use that switch as an expression.
enum Technique {
KAMEHAMEHA,
INSTANT_TRANSMISSION,
KAIOKEN,
ULTRA_INSTINCT
}
class Main {
boolean didGokuStealItFromSomeoneElse(Technique technique) {
boolean answer = switch (technique) {
case KAMEHAMEHA:
case INSTANT_TRANSMISSION:
case KAIOKEN:
yield true;
case ULTRA_INSTINCT:
yield false;
};
return answer;
}
void main() {
IO.println(
didGokuStealItFromSomeoneElse(Technique.INSTANT_TRANSMISSION)
);
}
}
In this situation you do not need to have a default
case for switches over enums. Java will believe that
you covered every possibility.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Convert this program which uses a switch without fallthrough to instead use fallthrough for all the common cases.
enum Character {
GOKU,
PICCOLO,
FRIEZA,
VEGETA,
PILAF,
GOHAN,
BEERUS
}
class Main {
String isEvil(Character c) {
switch (c) {
case GOKU -> {
// Man puts the multiverse in danger
// on account of wanting to fight!
return "debatable";
}
case PICCOLO -> {
return "not_anymore";
}
case FRIEZA -> {
return "yes";
}
case VEGETA -> {
return "not_anymore";
}
case PILAF -> {
return "not_anymore";
}
case GOHAN -> {
return "no";
}
case BEERUS -> {
// He's more a force of nature,
// but its relative.
return "debatable";
}
default -> {
return "unknown";
}
}
}
void main() {
IO.println("Goku: " + isEvil(Character.GOKU));
IO.println("Piccolo: " + isEvil(Character.PICCOLO));
IO.println("Frieza: " + isEvil(Character.FRIEZA));
IO.println("Vegeta: " + isEvil(Character.VEGETA));
IO.println("Pilaf: " + isEvil(Character.PILAF));
IO.println("Gohan: " + isEvil(Character.GOHAN));
IO.println("Beerus: " + isEvil(Character.BEERUS));
}
}
Challenge 2.
Convert the previous program using fallthrough to also
be directly returned as a switch expression using yield
.
Challenge 3.
Write a method named aToZ
which, given a character
between a
and z
, will print all the letters
from that letter on until z
.
Make use of a switch with fallthrough instead of loops or any other mechanism.
class Main {
void aToZ(char c) {
// CODE HERE
}
void main() {
aToZ('z'); // z
aToZ('x'); // x y z
aToZ('a'); // a b c d ... x y z
}
}
Recursion

In a method you can call another method.
void doOtherThing() {
IO.println("B");
}
void doThing() {
IO.println("A");
}
void main() {
doThing();
}
This is at the foundation of most code so should, at this point, be a given.
What might not be obvious is that you can call the method currently running.
void countDown(int value) {
IO.println(value);
if (value > 0) {
countDown(value - 1);
}
}
void main() {
countDown(10);
}
This is what we call "recursion."
Disclaimer
Recursion will be annoying to learn.
Sorry.
It's not because it's particuarly hard or because it's beyond your ken. It's just that when you learn loops first, recursion tends to be harder to learn than if you started with it.
The good news is that once you get it, after however much mental anguish, you won't forget it. And it will be useful, however occasional.
Base Case
The general structure of a recursive method is that it will sometimes call itself and sometimes not.
This makes sense if you think about it. If it always called itself, it would never stop calling itself and run forever.
We call the case where the function will not call itself a "base case"
if (<SOMETHING>) {
<CALL SELF>
}
else {
// The base case
return <VALUE>;
}
Comparison to Delegation
A related technique to recursion is "delegation."
This is when you have one method call a different arity version of itself.
// This method is delegated to
void seasonFood(int shakes) {
for (int i = 0; i < shakes; i++) {
IO.println("1 shake of pepper");
}
}
// by this method, which provides a "default" value of 2
void seasonFood() {
seasonFood(2);
}
void main() {
seasonFood();
}
This is distinct from recursion since, while the method delegated to might have the same name as the one delegating, different overloads of methods are considered distinct methods.
void seasonFood()
is therefore a different method than void seasonFood(int)
.
Comparison to Loops
Everything that can be accomplished by using loops can be accomplished using recursion. This means it is always possible to take some code using loops and translate it mechanically to code using recursion.
void seasonFoodRecursive(int times) {
if (times == 0) {
return;
}
else {
IO.println("seasoning");
seasonFoodRecursive(times - 1);
}
}
void seasonFoodIterative(int times) {
for (int i = 0; i < times; i++) {
IO.println("seasoning");
}
}
void main() {
IO.println("Recursive");
seasonFoodRecursive(2);
IO.println("Iterative");
seasonFoodRecursive(2);
}
Not everything that can be accomplished by using recursion can be accomplished using loops. At least without introducing some data structure to track what would otherwise be stored in Java's call stack. The following video goes a little further in depth as to why.
This is part of why its important to power through recursion. There are things that can only be solved with it and there are things that are more easily solved with it.
Counting Down
One example of a recursive function is one that counts down.
You start by taking a number as an argument. If that number is greater than 0 you "recurse" with a number one lower than you were given. if it isn't you are done.
void countDown(int x) {
if (x <= 0) {
IO.println("DONE");
}
else {
IO.println("x: " + x);
countDown(x - 1);
}
}
void main() {
countDown(5);
}
In this situation x <= 0
is our base case since that is the situation where we don't
recurse. Each time we do recurse, we get closer to that base case. x - 1
is always going
to be closer to x being less or equal to 0.
All of this is equivalent to a while
loop that looks like the following.
void countDown(int x) {
while (x > 0) {
IO.println("x: " + x);
x = x - 1;
}
IO.println("DONE");
}
void main() {
countDown(5);
}
Accumulators
A common thing to do with loops is to "accumulate" some value each time you go through the loop.
class Main {
int timesTwo(int x) {
int total = 0;
while (x > 0) {
total += 2;
x--;
}
return total;
}
void main() {
IO.println(
timesTwo(4)
);
}
}
To accomplish the same task with recursion you need two things.
The first is an extra "accumulator" argument to your function. This is what you will update with the new value each time you recurse.
The second is an overload of your method which takes all the same arguments minus that accumulator. This overload should immediately delegate to the one that takes all the arguments with some starting value for the accumulator.
class Main {
int timesTwo(int x, int accumulator) {
if (x > 0) {
return timesTwo(x - 1, accumulator + 2);
}
else {
return accumulator;
}
}
int timesTwo(int x) {
return timesTwo(x, 0);
}
void main() {
IO.println(
timesTwo(4)
);
}
}
The reason we need to do this is because, unlike with a loop, it is difficult to share variables across recursive calls. Adding an extra function parameter lets us have a place to put what would otherwise be a local variable updated by a loop.1
You may have noticed that the name changed from total
to accumulator
. There
is no particularly good reason for this other than to highlight that the total being built
up is an example of an "accumulator."
Recurse Over a String
To write a recursive function which acts over each character
of a String
you need to do much the same task as with regular loops,
just by having your "current index" be a parameter to the function.
class Main {
void printEachUpperCase(String s, int i) {
if (i < s.length()) {
IO.println(Character.toUpperCase(s.charAt(i)));
printEachUpperCase(s, i + 1);
}
}
void printEachUpperCase(String s) {
printEachUpperCase(s, 0);
}
void main() {
printEachUpperCase("hello");
}
}
This overload with the index is an example of a function taking an accumulator.
Recurse Over an Array
To write a recursive function which acts over each element of an array, the technique is the same as with you need to do much the same task as with regular loops, just by having your "current index" be a parameter to the function.
class Main {
void printEachTimesEight(int[] nums, int i) {
if (i < nums.length) {
IO.println(nums[i] * 8);
printEachTimesEight(nums, i + 1);
}
}
void printEachTimesEight(int[] nums) {
printEachTimesEight(nums, 0);
}
void main() {
printEachTimesEight(new int[] { 1, 2, 3 });
}
}
This same general technique can be used to loop over other sorts of collections, like ArrayList
. In that
case you would use .get()
and .size()
instead of []
and .length
, but the concept is the same.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Write code that outputs every number from 1
to an arbitrary number
You are not allowed to use while
or for
loops.
// CODE HERE
void main() {
int n = 30;
// CODE HERE
}
Challenge 2.
Write code that will output each character of name
on its own line.
So if name is equal to "Riyo", I would expect the following as output.
R
i
y
o
You are not allowed to use while
or for
loops.
// CODE HERE
void main() {
String name = "Rudo";
// CODE HERE
}
Challenge 3.
Write code that will take a number and if it is divisible by two, divides it by two. If it is not, multiplies it by three and adds one.
Keep doing this until the number equals one. Output it each time.
If the initial number is 6 you should have this as output.
6
3
10
5
16
8
4
2
1
You are not allowed to use while
or for
loops.
// CODE HERE
void main() {
// Change this value to test your code.
int n = 15;
// CODE HERE
}
Challenge 4.
Write code that outputs the number of vowels in name. Treat y as a vowel.
Treat the characters a, A, e, E, i, I, o, O, u, U, y, and Y as vowels.
You are not allowed to use while
or for
loops.
// CODE HERE
void main() {
// Change this value to test your code.
String name = "Zanka";
// CODE HERE
}
Challenge 5.
Draw a square.
Make it so that you can make the square bigger or smaller by changing a variable at the start of the program.
*****
*****
*****
*****
You are not allowed to use while
or for
loops.
// CODE HERE
void main() {
int size = 5;
// CODE HERE
}
Loops III

Counting indexes of elements is, while effective, a tad exhausting to do every time you want to loop through something.
void main() {
String[] shirts = new String[] {
"T-Shirt",
"Polo Shirt",
"Dress Shirt"
};
for (int i = 0; i < shirts.length; i++) {
String shirt = shirts[i];
IO.println(shirt);
}
}
This is where "for-each" loops come in.
void main() {
String[] shirts = new String[] {
"T-Shirt",
"Polo Shirt",
"Dress Shirt"
};
for (String shirt : shirts) {
IO.println(shirt);
}
}
For-each loops
Where a normal for
loop is more or less a shorthand for a certain kind of while
loop,
a for-each loop1 is a shorthand for the general concept of iterating over a collection
of elements.
To use a for-each loop, write for
then in the parentheses write a variable declaration, a :
, and the
collection of elements you are iterating over.
for (<VARIABLE DECLARATION> : <COLLECTION>) {
<BODY>
}
record Bread(String name, boolean french) {}
class Main {
void main() {
Bread[] breads = {
new Bread("Croissant", true),
new Bread("Baguette", true),
new Bread("Boston Brown Bread", false)
};
for (Bread bread : breads) {
IO.println(
bread.name()
+ (bread.french() ? " is french" : " is not french")
);
}
}
}
You might see this referred to as an "enhanced for statement." That is its name in the language spec but not the name most people will use.
Arrays
When you use a for-each loop on an array, it acts the same as the kind of for-loop you would have written before with an explicit index.
record Drink(String name, double mgCaffeinePerCup) {}
class Main {
void main() {
Drink[] drinks = {
new Drink("Black Coffee", 95),
new Drink("Milk", 0),
new Drink("Green Tea", 35)
};
// This loop functions the same as the loop below
for (int i = 0; i < drinks.length; i++) {
Drink drink = drinks[i];
IO.println(
drink.name()
+ " has "
+ drink.mgCaffeinePerCup()
+ "mg caffiene per cup"
);
}
IO.println("------------------");
for (Drink drink : drinks) {
IO.println(
drink.name()
+ " has "
+ drink.mgCaffeinePerCup()
+ "mg caffiene per cup"
);
}
}
}
This doesn't mean that the old style of for loop is useless now. You will still want that if you need to know which element in your array you are dealing with or if you are doing a more interesting form of iteration.
record Drink(String name, double mgCaffeinePerCup) {}
class Main {
void main() {
Drink[] drinks = {
new Drink("Black Coffee", 95),
new Drink("Milk", 0),
new Drink("Green Tea", 35)
};
// If loop actually cares to use `i` beyond just accessing
// the right element in the array
for (int i = 0; i < drinks.length; i++) {
Drink drink = drinks[i];
// Which you might want for display logic
IO.println(
"[" + i + "]: " + drink.name()
);
// Or to mutate the original array
drinks[i] = new Drink(
drink.name(),
drink.mgCaffeinePerCup() + 100
);
}
}
}
Iterable and Iterator
For things that are not arrays, a for-each loops
are built on top of two interfaces: java.lang.Iterable
and java.lang.Iterator
.
The Iterator
interface defines two methods: hasNext
and next
1. Iterators let you box up the logic of
how to loop over something.
public interface Iterator<T> {
// Will return true if there are more elements
// false otherwise
boolean hasNext();
// Gets an element and advances the iterator forwards
T next();
}
Iterable
then
just has one method which gives you an Iterator
.
interface Iterable<T> {
// Gives a "fresh" Iterator
Iterator<T> iterator();
}
This is needed because Iterator
s are "one shot." It starts at the beginning of a collection
and advances across every element each time next
is called. In order to loop over something multiple
times you need a fresh iterator each time.
A for-each loop over an Iterable
object more or less translates to this style of while
loop.2
for (String thing : iterable) {
// ...
}
// is the same as
Iterator<String> iter = iterable.iterator();
while (iter.hasNext()) {
String thing = iter.next();
// ...
}
There is actually one more method: remove
. Not all Iterator
s support it so we'll cover it once we've introduced more Iterable
things.
I think this is important to know because otherwise it won't make sense when you run in to things you can loop over but don't have .get
/[]
, `
ArrayList
One class which implements Iterable
is ArrayList
.
import java.util.ArrayList;
class Main {
void main() {
ArrayList<String> donutEaters = new ArrayList<>();
donutEaters.add("Chief Wiggum");
donutEaters.add("Homer Simpson");
Iterator<String> donutEatersIterator = donutEaters.iterator();
// Check if there is a next element
while (donutEatersIterator.hasNext()) {
// If there is, get it and advance the iterator
String donutEater = donutEatersIterator.next();
IO.println(donutEater + " eats donuts");
}
}
}
This means you can loop over it with a for-each loop same as an array.
import java.util.ArrayList;
class Main {
void main() {
ArrayList<String> donutEaters = new ArrayList<>();
donutEaters.add("Chief Wiggum");
donutEaters.add("Homer Simpson");
for (String donutEater : donutEaters) {
IO.println(donutEater + " eats donuts");
}
}
}
String
One thing that feels like it should implement the Iterable
interface but does not is String
.
class Main {
void main() {
String letters = "abc";
for (char letter : letters) {
IO.println(letter);
}
}
}
To loop over all the characters in a String
, you have to use a regular loop.
class Main {
void main() {
String letters = "abc";
for (int i = 0; i < letters.length(); i++) {
char letter = letters.charAt(i);
IO.println(letter);
}
}
}
Concurrent Modifications
If you are looping over a collection with a for-each loop you generally
cannot remove things from that collection at the same time. Doing so should
trigger a ConcurrentModificationException
.1
import java.util.ArrayList;
record Sandwich(
int turkeySlices,
int cheeseSlices,
boolean mayo
) {}
class Main {
void main() {
ArrayList<Sandwich> sandwiches = new ArrayList<>();
var turkeyAndCheddar = new Sandwich(2, 2, true);
var grilledCheese = new Sandwich(0, 4, false);
var bigTurkeyAndCheddar = new Sandwich(10, 10, true);
var theWisconsinFreak = new Sandwich(0, 20, true);
sandwiches.add(turkeyAndCheddar);
sandwiches.add(grilledCheese);
sandwiches.add(bigTurkeyAndCheddar);
sandwiches.add(theWisconsinFreak);
for (Sandwich sandwich : sandwiches) {
if (sandwich.mayo()) { // Some people don't like Mayo
// But we can't get rid of them during the loop
sandwiches.remove(sandwich);
}
}
}
}
If you want to remove an element at the same time as iterating over specifically an ArrayList
, you
can get creative with indexes and regular loops.2
import java.util.ArrayList;
record Sandwich(
int turkeySlices,
int cheeseSlices,
boolean mayo
) {}
class Main {
void main() {
ArrayList<Sandwich> sandwiches = new ArrayList<>();
var turkeyAndCheddar = new Sandwich(2, 2, true);
var grilledCheese = new Sandwich(0, 4, false);
var bigTurkeyAndCheddar = new Sandwich(10, 10, true);
var theWisconsinFreak = new Sandwich(0, 20, true);
sandwiches.add(turkeyAndCheddar);
sandwiches.add(grilledCheese);
sandwiches.add(bigTurkeyAndCheddar);
sandwiches.add(theWisconsinFreak);
for (int i = 0; i < sandwiches.size(); i++) {
Sandwich sandwich = sandwiches.get(i);
if (sandwich.mayo()) { // Some people don't like Mayo
sandwiches.remove(sandwich);
i--; // Subtracting one from our current index syncs us back up
}
}
IO.println(sandwiches);
}
}
The reason I say "should" is that doing the book keeping needed to know when
this has happened can be hard and not all Iterator
s in the world will do it. The check is what we would call "best effort."
And, as these footnotes have alluded, there is a .remove
method on Iterator
. We'll cover it
later.
Inferred Types
A variable declaration in a for-each loop can make use of var
to infer its type.
class Main {
void main() {
String[] chairMaterials = { "wicker", "wood", "plastic" }
for (var chairMaterial : chairMaterials) {
IO.println(chairMaterial);
}
}
}
And this is good to know for the same reasons you would use var
other places in Java.
Writing Book book
can be a drain on the soul.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Make your own class named OneToTen
which implements Iterable<Integer>
and will yield the numbers from 1
to 10
.
This will require making a class which implements Iterator<Integer>
as well.
// CODE HERE
class OneToTen implements Iterable<Integer> {
// CODE HERE
}
class Main {
void main() {
var oneToTen = new OneToTen();
// Should output
// 1 2 3 4 5 6 7 8 9 10
// twice
for (int i : oneToTen) {
IO.print(i);
IO.print(" ")
}
IO.println();
// If it only happens once, you might be
// mistaken about the difference between
// Iterable and Iterator
for (int i : oneToTen) {
IO.print(i);
IO.print(" ")
}
IO.println();
}
}
Challenge 2.
Make a class named StringIterable
which implements Iterable<String>
.
Its constructor should take a String
and it should let you iterate over
each character.
// CODE HERE
class Main {
void main() {
var stringIterable = new StringIterable("abc");
// Should output
//
// a
// b
// c
// -------
// a
// b
// c
for (char c : stringIterable) {
IO.println(c);
}
IO.println("-------");
for (char c : stringIterable) {
IO.println(c);
}
}
}
Challenge 3.
The following code will crash with a ConcurrentModificationException
.
Rewrite it so that it does not.
import java.util.ArrayList;
class Main {
void main() {
var trash = new ArrayList<String>();
trash.add("gloves");
trash.add("staff");
trash.add("glasses");
for (var item : trash) {
if (item.equals("glasses")) {
trash.remove(item);
}
}
}
}
Encapsulation

One way of combatting the effects of Hyrum's Law is encapsulation.
To encapsulate something is to hide it from the larger world. By doing so you lower the number of people who are able to observe what we would call "implementation details."
Implementation Details
Just as there is almost always more than one way to remove the epidermis of a feline, there is often more than one way to implement a program.
We call properties and choices made when writing a program that are not related to what that program ultimitely needs to do "implementation details."
As an example, imagine you are about to type a query into Microsoft Bing.1 The website you go to and the search box you see are intended parts of the program. The fact that if you click search you will see results is also just what the program needs to do.
But all the mechanics of how that query is answered are implementation details.
You know, when you have a question and just have to "bing it?"
Implicit Interfaces
There are many kinds of "interfaces."
There are interface
s the Java language construct - these let you
specify methods that different classes will share and write code
that will work against anything which implements that interface.
But there are also "interfaces" that you encounter in the real world. The classic example is an automobile. Once you understand the "interface" of a steering wheel, brake pedal, and gas pedal you can drive most any car.
Then back within Java there are things which form "an interface"
which may or may not use the interface
keyword. The static methods available on a class,
the constructors that are public, the set of classes that come with a library, etc.
I think a more general concept is "the implicit interface." Everything you can observe about a thing forms its "implicit interface."1
I call it "implicit" because there is no place where you write down "all the properties of a thing" that is not the thing itself. And by thing I mean field, method, class, group of classes, package names - everything.
Methods
The simplest mechanism for encapsulating implementation details is a method.
The Math.sin
function that comes with Java has a definition that looks
something like this.1
double sin(double x) {
double[] y = new double[2];
double z = 0.0;
int n, ix;
// High word of x.
ix = __HI(x);
// |x| ~< pi/4
ix &= EXP_SIGNIF_BITS;
if (ix <= 0x3fe9_21fb) {
return __kernel_sin(x, z, 0);
} else if (ix >= EXP_BITS) { // sin(Inf or NaN) is NaN
return x - x;
} else { // argument reduction needed
n = RemPio2.__ieee754_rem_pio2(x, y);
// ...
}
}
People who write programs that depend on the sin
function do not generally
understand how this works. I have a math minor2 and I certainly don't.
All they need to know to use sin
effectively is what it does, not how it does it.
In this way methods are one way to provide encapsulation. You reduce
a potentially complicated body of code to inputs required and outputs produced.3
I took some creative liberties here, roll with it.
Laugh it up.
This shouldn't be news to you at this point, but I think its helpful to point out that property in the context of this topic.
Classes
One step above methods is classes.
When classes have private fields and all interactions with those fields are mediated through methods
The ArrayList
class that comes with Java looks something like this.1
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
Object[] elementData;
private int size;
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}
private Object[] grow(int minCapacity) {
int oldCapacity = elementData.length;
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, /* minimum growth */
oldCapacity >> 1 /* preferred growth */);
return elementData = Arrays.copyOf(elementData, newCapacity);
} else {
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
}
}
private Object[] grow() {
return grow(size + 1);
}
public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}
// ...
}
People who write programs that depend on calling .add
on an ArrayList
do not need
to understand how or when the internal elementData
and size
fields are updated. Those also
need not be the only fields that exist. All that is required is "calling .add
will add an element to the list."
It is in this way that classes provide encapsulation. They can hide the fields which are needed and the way in which they are used - "implementation details" - in order to provide some implicit interface.
Creative liberties taken.
Abstraction
A term closely related to encapsulation is abstraction.
When you encapsulate something you have made an abstraction on top of it.
You could say that "abstraction" is the noun and "encapsulation" is the verb. You encapsulate things, thereby making abstractions.
Coupling
If two things are "coupled" it means that a change in one will mandate a change in the other. Or, at the very least, active consideration of whether such a change is neccesary.
In a way "coupling" is the opposite of "abstraction." Instead of hiding implementation details you freely intermingle them.
This does not mean that coupling is bad. On the contrary, so long as code that is coupled is also "co-located" making changes to that code can be easier than if it were spread over multiple files.
A not insignificant part of practice in the coding world is deciding when pieces of a program deserve to be abstracted from each other (to minimize interdependence) and when they deserve to be coupled together (to keep logic in one place.)
Leaky Abstractions
If an abstraction doesn't fully encapsulate what it is trying to we call that abstraction "leaky."
As an example, say we define a person as having a first name and a given name. That might work for a large number of people. But the moment our program needs to represent Plato, who did not have a given name, that abstraction "leaks."
Now suddenly the code that would work with people needs to account for an "empty" given name
where it didn't before and we need to pick a special value to represent such a name (empty string or null
generally). The abstraction has leaked.
There are maybe more illuminating examples, but thats the general concept. Its not the end of the world if an abstraction leaks. It might just be a sign of a changing world or of not having thought through a problem fully. You can adapt to that. It's just worth knowing about.
Information Hiding
When encapsulating, your fundamental activity is finding ways to hide information that you consider to be implementation details.
If you did not hide this information and you have a large number of consumers1, you would never be able to change those implementation details. Your consumers can be coupled to them.
Something to be careful of with respect to this is "side channels." If you use the mechanisms Java gives you to hide a field from people you can get pretty far, but you still need to be wary of accidentally allowing people to do things you don't want.
import java.util.ArrayList;
class Total {
private final ArrayList<Integer> nums;
Total() {
this.nums = new ArrayList<>();
}
void add(int num) {
nums.add(num);
}
int total() {
int sum = 0;
for (int num : nums) {
sum += num;
}
return sum;
}
@Override
public String toString() {
return "Total[" + nums + "]";
}
}
In the code above the Total
class hides a field holding an ArrayList
and just exposes the total of the numbers added to it.
void main() {
var total = new Total();
total.add(3);
total.add(2);
total.add(1);
IO.println(total.total());
}
This lets it later on decide to maybe not store the list of numbers at all and instead keep a running total.
import java.util.ArrayList;
class Total {
private int runningTotal;
Total() {
this.runningTotal = 0;
}
void add(int num) {
runningTotal += num;
}
int total() {
return runningTotal;
}
@Override
public String toString() {
return "Total[" + runningTotal + "]";
}
}
The problem is that if you had called .toString()
on the previous implementation
you might have gotten a String
that looks like Total[[3, 2, 1]]
. The information
of what numbers are in the list was exposed there.
With enough people using this code some motivated moron will work backwards
from that String
representation to get at the underlying list. Now if you changed the implementation
you would break their code.
We would call that a "side channel." The information is exposed, just through an API you didn't consider. How you handle that is heavily dependent on the exact situation, but its a category of issue you should know about.
Again, if. These concerns do not apply as much to programs written at smaller scales or programs written within some encapsulation boundary. Don't get too paranoid about needing to hide things.
Collections

Arrays, ArrayList
, and HashMap
are all "collections"
of objects. Importantly, they also aren't the only possible
kinds of collections.
Java provides support for a wide range of different collection types through "The Collections Framework."1
There is nothing really special about these interfaces and classes that mean they deserve such a name. I view it as a marketing term. There was a period of time before which Java did not have ArrayList
, HashMap
, etc. and when they were added they said "this is the new collections framework"
instead of "we've added these dozen classes."
List
A List
is an ordered collection of elements that generally allows duplicates.
Lists that come with Java implement the java.util.List
interface
which provides myriad methods for working with such a collection.
interface List<E> {
int size();
boolean isEmpty();
E get(int index);
// and more
}
ArrayList
implements List
and is likely the named class you will see
most often, but there are other implementations you will encounter as time goes on.
import java.util.List;
import java.util.ArrayList;
class Main {
void main() {
List<String> names = new ArrayList<>();
names.add("Andor");
names.add("Bix");
names.add("Luthen");
IO.println(names);
}
}
Every method and capability that ArrayList
has is available from the List
interface.1
This includes being able to be used in for-each loops.
import java.util.List;
import java.util.ArrayList;
class Main {
void main() {
List<String> names = new ArrayList<>();
names.add("Andor");
names.add("Bix");
names.add("Luthen");
for (var name : names) {
IO.println(name);
}
}
}
Save a really niche one
Map
A Map
is a collection that maps keys to values. Maps that come with
Java implement the java.util.Map
interface.
interface Map<K, V> {
V get(Object key);
V put(K key, V value);
// and more
}
HashMap
is one implementation of Map
you are likely to see and use.
import java.util.Map;
import java.util.HashMap;
class Main {
void main() {
Map<String, Integer> ages = new HashMap<>();
ages.put("Andor", 26);
ages.put("Bix", 27);
ages.put("Luthen", 59);
IO.println(ages);
}
}
Just like ArrayList
and List
, all the useful capabilities of HashMap
are available
via the Map
interface.
Set
A Set
is a collection of elements that does not allow duplicates.
Sets that come with Java implement the java.util.Set
interface.
interface Set<E> {
boolean contains(Object o);
boolean add(E e);
boolean remove(Object o);
// and more
}
HashSet
provides an implementation of Set
that works via the same mechanics - .hashCode
and .equals
- as a HashMap
.
import java.util.Set;
import java.util.HashSet;
class Main {
void main() {
Set<String> agents = new HashSet<>();
agents.add("Syril");
agents.add("Dedra");
IO.println(agents);
// If you try to add a duplicate to a set it won't
// crash, but there won't be two in the collection
agents.add("Syril");
IO.println(agents);
}
}
Checking membership in a large set is generally fast for the same reasons adding a new element to a large map is fast.
Arrays
Arrays are the odd duck out in the world of collections. They are basically a List
but aren't List
s.1
You can make a List
which is a view over an array with Arrays.asList
.
import java.util.List;
import java.util.Arrays;
class Main {
void main() {
String[] furniture = new String[] {
"Ottoman",
"Table",
"Dresser"
};
List<String> furnitureList = Arrays.asList(furniture);
IO.println(furnitureList);
}
}
Changes made to the List
returned from Arrays.asList
will be reflected in the underlying array.
import java.util.List;
import java.util.Arrays;
class Main {
void main() {
String[] furniture = new String[] {
"Ottoman",
"Table",
"Dresser"
};
List<String> furnitureList = Arrays.asList(furniture);
furnitureList.set(0, "Recliner");
IO.println(Arrays.toString(furniture));
}
}
Accordingly, any methods on List
which try to perform operations that an array cannot support (such as .add
) will throw exceptions.
import java.util.List;
import java.util.Arrays;
class Main {
void main() {
String[] furniture = new String[] {
"Ottoman",
"Table",
"Dresser"
};
List<String> furnitureList = Arrays.asList(furniture);
// Cannot add to an array!
furnitureList.add("Shelf");
}
}
Arrays are unique beasts. This is true both in Java the language and in the virtual machine Java code runs on.
This is partially attributable to arrays coming first in the history - List
and friends were not in the first version of Java.
UnsupportedOperationException
List
, Map
, and Set
are interfaces with a lot of methods.
There are circumstances - like a List
directly wrapping an array -
where it isn't possible to provide an implementation
for all those methods.
In these situations, the specific exception that will be thrown is an UnsupportedOperationException
.
import java.util.List;
import java.util.Arrays;
class Main {
void main() {
String[] furniture = new String[] {
"Ottoman",
"Table",
"Dresser"
};
List<String> furnitureList = Arrays.asList(furniture);
try {
furnitureList.add("Shelf");
} catch (UnsupportedOperationException e) {
IO.println("Tried to do an unsupported operation");
}
}
}
For each collection some operations are mandatory to implement - like .size
on List
-
while others are allowed to be unsupported and still be a "valid" implementation.
Factories
There are methods on List
, Set
, and Map
1 which can give
you instances of their corresponding collection.
import java.util.List;
import java.util.Set;
import java.util.Map;
class Main {
void main() {
List<String> weapons = List.of("Lightsaber", "Blaster");
Set<String> ships = Set.of("Tie Fighter", "X-Wing");
Map<String, Integer> midichlorians = Map.of(
"Anakin", 27000,
"Jar-Jar Binks", 0
);
IO.println(weapons);
IO.println(ships);
IO.println(midichlorians);
}
}
The collections returned by these of
methods are immutable. This means methods
which would change the underlying collection will throw an UnsupportedOperationException
.2
import java.util.List;
class Main {
void main() {
List<String> weapons = List.of("Lightsaber", "Blaster");
// Unsupported
weapons.add("A winning smile?")
IO.println(weapons);
}
}
If you want the convenience of the factory methods but actually want an ArrayList
, HashMap
, or
a similar collection which supports .add
, .remove
, etc. you are in luck. Those classes generally
have a constructor which will copy another List
, Map
, or Set
.
import java.util.List;
class Main {
void main() {
// Reads better than a bunch of .add calls
List<String> weapons = new ArrayList<>(List.of("Lightsaber", "Blaster"));
// Will work!
weapons.add("A winning smile?")
IO.println(weapons);
}
}
If you want the opposite - if you want to make a copy of something like an ArrayList
which does not support .add
, .remove
, etc. - you can use copyOf
.
import java.util.List;
class Main {
void main() {
List<String> weapons = new ArrayList<>(List.of("Lightsaber", "Blaster"));
weapons.add("A winning smile?")
IO.println(weapons);
// Similar methods exist for Map and Set
List<String> unchangable = List.copyOf(weapons);
IO.println(unchangable);
}
}
Interfaces can have static methods. We'll cover it in a bit. For now all you need to know is that these methods exist, not how to define similar ones yourself.
This is often fine. When something doesn't change after construction its one less thing to
have to think about when reading code. If you pass an ArrayList
to a method you do need to wonder
if it is only going to be read or if something that you forgot about will call .add
, .remove
, etc.
Specificity
When you write methods that accept or return collections, the social rule is to use the interface, not a specific collection type.
// You are generally expected to write this
List<String> process(Set<Integer> elements) {
// ...
}
// And not this
ArrayList<String> process(HashSet<Integer> elements) {
// ...
}
There are valid reasons for this.
- When you take a collection as an argument the code that follows probably would work for any implementation of that collection. Why make someone use any specific one?
- When you return a collection it is possible you might want to change the exact kind of collection you return later. Why make things harder for future you?
But there is also a dimension to it which is similar to how we choose to name classes, variables, and methods.
Socially it will cause you trouble if you make something which works in terms of a "concrete"
collection. Writing List
instead of ArrayList
where possible doesn't make your programs worse and it avoids frustrating interactions.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Replace uses of the "concrete" collection types (ArrayList
, HashMap
, HashSet
)
in the following code with the corresponding collection interfaces.
You want to keep the calls like new ArrayList
- only change how variables are typed.
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
class Main {
void main() {
ArrayList<String> jedi = new ArrayList<>();
jedi.add("Luke");
jedi.add("Anakin");
jedi.add("Qui-Gon");
jedi.add("Obi-Wan");
HashSet<String> sith = new HashSet<>();
sith.add("Palpatine");
HashMap<String, String> winningMatchups
= new HashMap<>();
winningMatchups.put("Anakin", "Palpatine");
winningMatchups.put("Obi-Wan", "Jar-Jar");
for (var j : jedi) {
IO.println(j + " is a jedi");
var matchup = winningMatchups.get(j);
if (matchup != null) {
if (sith.contains(matchup)) {
IO.println(j + " would win against " + matchup);
}
else {
IO.println(j + " would win against " + matchup + " (but they aren't sith)");
}
}
}
}
}
Challenge 2.
Call methodB
using the array returned from methodA
.
class Main {
String[] methodA() {
return new String[] {
"Chewbacca",
"Attichitcuk",
"Mallatobuck",
"Lumpawaroo"
}
}
void methodB(List<String> character) {
IO.println("Characters in the Star Wars Christmas Special:");
IO.println("----------------");
for (item : character) {
IO.println(item);
}
}
void main() {
// CODE HERE
}
}
Reflection

Reflection is what we call it when a program uses information about how it - the program - is structured in order to do things while running.
We call it reflection because it's as if your code looks into a mirror and sees its own reflection.1
When a human looks into a mirror they might see their own eye color. If a mirror weren't there it would be difficult to know such things.
Class Objects
You can get an object representing a class in one of a few ways.
The first is to write the name of the class followed by .class
.
This works if you know exactly the class you want to reflect on.
While this looks like accessing a field, it is technically its own special thing.
class Main {
void main() {
Class<String> stringClass = String.class;
IO.println(stringClass);
}
}
Another is to call the getClass
method on an object. This
is a method available on java.lang.Object
and so will work for
anything.
This is what you will use if you don't know up front exactly what type of thing you will be reflecting over.
class Main {
void main() {
String s = "Hello";
Class<?> stringClass = s.getClass();
IO.println(stringClass);
}
}
These class objects have a generic parameter that can hold the class the class object represents.1 When you don't know that information up front you should use a wildcard.
If that seems confusing and useless, you are half
right. It certainly is confusing, but it is pretty useful sometimes.
If it's still beyond your ken just always write Class<?>
and we'll
loop back to it.
Get all Fields
If you have a class object, you can get all the public
fields of that class using getFields
. This gives you
an array of Field
objects.
import java.lang.reflect.Field;
class Main {
void main() {
Class<Drink> drinkClass = Drink.class;
Field[] fields = drinkClass.getFields();
for (var field : fields) {
IO.println(field.getName());
}
}
}
class Drink {
public String name;
public boolean caffeinated;
}
Its worth knowing that getFields
will not list non-public fields. To
get a list of all fields, including private and package-private ones,
you need to use getDeclaredFields
.
import java.lang.reflect.Field;
class Main {
void main() {
Class<Soup> soupClass = Soup.class;
IO.println("Using getFields");
Field[] publicFields = soupClass.getFields();
for (var field : publicFields) {
IO.println(field.getName());
}
IO.println("-------------");
IO.println("Using getDeclaredFields");
Field[] allFields = soupClass.getDeclaredFields();
for (var field : allFields) {
IO.println(field.getName());
}
}
}
class Soup {
public String name;
boolean isChicken;
private boolean hasVeggies;
}
Get a Field
You can retrieve a single field by its name using getField
. If there
is no field with that name it will throw a NoSuchFieldException
.
import java.lang.reflect.Field;
class Main {
void main() throws NoSuchFieldException {
Class<Drink> drinkClass = Drink.class;
Field nameField = drinkClass.getField("name");
IO.println(nameField);
}
}
class Drink {
public String name;
public boolean caffeinated;
}
And if you need to access a field that might be non-public you can use getDeclaredField
.
import java.lang.reflect.Field;
class Main {
void main() throws NoSuchFieldException {
Class<Soup> soupClass = Soup.class;
Field hasVeggiesField = soupClass.getDeclaredField("hasVeggies");
IO.println(hasVeggiesField);
// Will fail. getField won't see hasVeggies
soupClass.getField("hasVeggies");
}
}
class Soup {
public String name;
boolean isChicken;
private boolean hasVeggies;
}
Read from a Field
Once you have a Field
object you can use its get
method to read the value of that field
from an object. This will throw an IllegalAccessException
if you try to read a field you
are not allowed to.1
import java.lang.reflect.Field;
class Main {
void main() throws IllegalAccessException {
Class<Drink> drinkClass = Drink.class;
Field nameField;
try {
nameField = drinkClass.getField("name");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e); // Should have this field
}
var soda = new Drink("Soda", true);
var water = new Drink("Water", false);
IO.println(nameField.get(soda));
IO.println(nameField.get(water));
}
}
class Drink {
public String name;
public boolean caffeinated;
Drink(String name, boolean caffeinated) {
this.name = name;
this.caffeinated = caffeinated;
}
}
The rules for this go a bit beyond "you cannot read private fields."
Write to a Field
Similarly, you can write to a field using the .set
method on a Field
object.
This will also throw IllegalAccessException
if its not something you are allowed to do.
import java.lang.reflect.Field;
class Main {
void main() throws IllegalAccessException {
Class<Drink> drinkClass = Drink.class;
Field caffeinatedField;
try {
caffeinatedField = drinkClass.getField("caffeinated");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
var water = new Drink("Water", false);
// You can put drugs in anything you set your mind to, kids
caffeinatedField.set(water, true);
IO.println(caffeinatedField.get(water));
}
}
class Drink {
public String name;
public boolean caffeinated;
Drink(String name, boolean caffeinated) {
this.name = name;
this.caffeinated = caffeinated;
}
}
If you try to set a field to the wrong type of value - like setting a boolean
field to have a String
value -
you will get an IllegalArgumentException
. Unlike NoSuchFieldException
and IllegalAccessException
this is an
unchecked exception, so you do not need to explicitly account for it.
import java.lang.reflect.Field;
class Main {
void main() throws IllegalAccessException {
Class<Drink> drinkClass = Drink.class;
Field caffeinatedField;
try {
caffeinatedField = drinkClass.getField("caffeinated");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
var soda = new Drink("Soda", true);
caffeinatedField.set(soda, "yes, very much so");
IO.println(caffeinatedField.get(soda));
}
}
class Drink {
public String name;
public boolean caffeinated;
Drink(String name, boolean caffeinated) {
this.name = name;
this.caffeinated = caffeinated;
}
}
The same will happen if you try to set a final
field.1
import java.lang.reflect.Field;
class Main {
void main() throws IllegalAccessException {
Class<Drink> drinkClass = Drink.class;
Field nameField;
try {
nameField = drinkClass.getField("name");
} catch (NoSuchFieldException e) {
throw new RuntimeException(e); // Should have this field
}
var water = new Drink("Water", false);
// You can put drugs in anything you set your mind to, kids
nameField.set(water, true);
IO.println(nameField.get(water));
}
}
class Drink {
public String name;
public final boolean caffeinated;
Drink(String name, boolean caffeinated) {
this.name = name;
this.caffeinated = caffeinated;
}
}
There are especially evil ways to actually change final
fields using reflection. If you want to know how to do that I'm not telling you.
Get all Methods
If you have a class object, you can get all the public
methods of that class using getMethods
. This gives you
an array of Method
objects.
Note that this will also include available methods that come from java.lang.Object
.
In addition to toString
, equals
, and hashCode
you will see a few you don't recognize.
All in due time.
import java.lang.reflect.Method;
class Main {
void main() {
Class<Tea> teaClass = Tea.class;
Method[] methods = teaClass.getMethods();
for (var method : methods) {
IO.println(method);
}
}
}
class Tea {
public void sip() {
}
public void gulp() {
}
}
Just like there is getDeclaredFields
for seeing non-public fields, getDeclaredMethods
will
give you all methods, regardless of their visibility.
import java.lang.reflect.Method;
class Main {
void main() {
Class<Fruit> fruitClass = Fruit.class;
IO.println("Using getMethods");
Method[] publicMethods = fruitClass.getMethods();
for (var method : publicMethods) {
IO.println(method);
}
IO.println("-------------");
IO.println("Using getDeclaredMethods");
Method[] allMethods = fruitClass.getDeclaredMethods();
for (var method : allMethods) {
IO.println(method);
}
}
}
class Fruit {
public void bite() {
}
void chew() {
}
private void swallow() {
}
}
Get a Method
To get a specific method from a class you can use getMethod
. If there is no method that matches the name and argument types a NoSuchMethodException
will be thrown. 1
import java.lang.reflect.Method;
class Main {
void main() throws NoSuchMethodException {
Class<Tea> teaClass = Tea.class;
Method sipMethod = teaClass.getMethod("sip");
IO.println(sipMethod);
}
}
class Tea {
public void sip() {
}
}
Unlike fields which can be identified only by their name, methods which are distinct overloads of each other are distinguised by the arguments they take in.
import java.lang.reflect.Method;
class Main {
void main() throws NoSuchMethodException {
Class<Tea> teaClass = Tea.class;
// There is a sip method which takes zero arguments
Method sipMethod = teaClass.getMethod("sip");
IO.println(sipMethod);
// which is a different method than
// sip that takes one int
sipMethod = teaClass.getMethod("sip", int.class);
IO.println(sipMethod);
// which is a different method than
// sip that takes a String and an int
sipMethod = teaClass.getMethod("sip", String.class, int.class);
IO.println(sipMethod);
}
}
class Tea {
public void sip() {
}
public void sip(int numberOfSips) {
}
public void sip(String baristaName, int numberOfSips) {
}
}
And, as you might imagine, getDeclaredMethod
will do the same thing with the distinction
of seeing non-public methods.
class Main {
void main() throws NoSuchMethodException {
Class<Fruit> fruitClass = Fruit.class;
IO.println(fruitClass.getDeclaredMethod("bite"));
IO.println(fruitClass.getDeclaredMethod("chew"));
IO.println(fruitClass.getDeclaredMethod("swallow"));
}
}
class Fruit {
public void bite() {
}
void chew() {
}
private void swallow() {
}
}
Invoke a Method
Just as you can get and set a field using the .get
and .set
methods on a Field
, you can invoke
a method by using the .invoke
method on a Method
.
For instance methods there is a first "hidden" argument that is the instance you would be invoking the method on.
This needs to be the first argument to .invoke
.
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
class Main {
void main() throws IllegalAccessException, InvocationTargetException {
Class<Tea> teaClass = Tea.class;
// sip taking zero arguments
Method sipMethod;
try {
sipMethod = teaClass.getMethod("sip", int.class);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
var tea = new Tea();
sipMethod.invoke(tea, 5);
}
}
class Tea {
public void sip(int numberOfSips) {
IO.println("You made " + numberOfSips + " sips");
}
}
For static methods you do not need an instance of the class to invoke them.
Instead you need to pass the class itself as the first argument to .invoke
.
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
class Main {
void main() throws IllegalAccessException, InvocationTargetException {
Class<Apple> appleClass = Apple.class;
// sip taking zero arguments
Method biteMethod;
try {
biteMethod = appleClass.getMethod("bite", int.class);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
biteMethod.invoke(Apple.class, 5);
biteMethod.invoke(Apple.class, 1);
}
}
class Apple {
public static void bite(int times) {
IO.println("You took " + times + " bite" + (times < 1 ? "." : "s."));
}
}
Get a Constructor
Following the pattern, getConstructor
gets a reference to a Constructor
object.
Just like getMethod
, this requires specifying argument types.
Since getField
might throw a NoSuchFieldException
and getMethod
might
throw a NoSuchMethodException
you might expect a getConstructor
to throw
a NoSuchConstructorException
. It does not. If there is no match for the constructor
you are trying to find it will reuse NoSuchMethodException
.
Constructor
objects are also similar to Class
objects in that they can carry a generic parameter
specifying what kind of object will be made when they are invoked.
import java.lang.reflect.Constructor;
class Main {
void main() throws NoSuchMethodException {
Class<AirplaneFood> airplaneFoodClass = AirplaneFood.class;
// Zero argument constructor.
// Note that we have Constructor<AirplaneFood>.
// If you have a Class<?> it will give you a Constructor<?>
Constructor<AirplaneFood> constructor
= airplaneFoodClass.getConstructor();
IO.println(constructor);
// One argument constructor
constructor = airplaneFoodClass.getConstructor(boolean.class);
IO.println(constructor);
}
}
class AirplaneFood {
public final boolean tastesGood;
public AirplaneFood() {
this.tastesGood = false;
}
public AirplaneFood(boolean tastesGood) {
if (tastesGood) {
throw new RuntimeException("Lies");
}
this.tastesGood = false;
}
}
Get all Constructors
If you want a list of all constructors, .getConstructors
will give you that.
What is worth noting is that unlike .getConstructor
the array returned from this
will not have the generic filled in.1
import java.lang.reflect.Constructor;
class Main {
void main() {
Class<AirplaneFood> airplaneFoodClass = AirplaneFood.class;
Constructor<?>[] constructors
= airplaneFoodClass.getConstructors();
for (Constructor<?> constructor : constructors) {
IO.println(constructor);
}
}
}
class AirplaneFood {
public final boolean tastesGood;
public AirplaneFood() {
this.tastesGood = false;
}
public AirplaneFood(boolean tastesGood) {
if (tastesGood) {
throw new RuntimeException("Lies");
}
this.tastesGood = false;
}
}
Arrays of generic objects are, in general, a hairy topic. Java can't really ensure that you use them right,
so there a lot of restrictions on making them. You can't make an ArrayList<String>[]
, for example.
Invoke a Constructor
Just as you can get and set a field using the .get
and .set
methods on a Field
and just as you can invoke
a method by using the .invoke
method on a Method
, you may also
invoke constructors with .newInstance
on a Constructor
object.
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
class Main {
void main() throws
InvocationTargetException,
InstantiationException,
IllegalAccessException,
NoSuchMethodException {
Class<AirplaneFood> airplaneFoodClass = AirplaneFood.class;
Constructor<AirplaneFood> constructor
= airplaneFoodClass.getConstructor();
AirplaneFood airplaneFood = constructor.newInstance();
IO.println(airplaneFood.tastesGood);
constructor = airplaneFoodClass.getConstructor(boolean.class);
airplaneFood = constructor.newInstance(false);
IO.println(airplaneFood.tastesGood);
}
}
class AirplaneFood {
public final boolean tastesGood;
public AirplaneFood() {
this.tastesGood = false;
}
public AirplaneFood(boolean tastesGood) {
if (tastesGood) {
throw new RuntimeException("Lies");
}
this.tastesGood = false;
}
}
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Write a method startsWithVowel
that takes an Object
as input and returns if the name of the underlying
class for that Object
starts with a vowel.
You might need to consult the documentation for Class.
class Apple {}
class Banana {}
class Main {
boolean startsWithVowel(Object o) {
// CODE HERE
}
void main() {
// Integer -> i -> true
IO.println(startsWithVowel(4));
// String -> s -> false
IO.println(startsWithVowel("abc"));
// Apple -> a -> true
IO.println(startsWithVowel(new Apple()));
// Banana -> b -> false
IO.println(startsWithVowel(new Banana()));
}
}
Challenge 2.
Write a method toMap
that takes an Object
as input and returns a Map<String, Object>
with all the object's field names as keys and field values
as the value.
class HankHill {
public double loveForPropane = 100;
public String shock = "bwhaaha";
}
class BobbyHill {
public boolean hasCulinaryAcumen = true;
}
class CottonHill {
public boolean bitter = true;
public boolean angry = true;
public boolean short = true;
public boolean dead = true;
}
class Main {
Map<String, Object> toMap(Object o) {
// CODE HERE
}
void main() {
// {loveForPropane=100, shock=bwhaaha}
IO.println(toMap(new HankHill()));
// {hasCulinaryAcumen=true}
IO.println(toMap(new BobbyHill()));
// {bitter=true, angry=true, short=true, dead=true}
IO.println(toMap(new CottonHill()));
}
}
Challenge 3.
Write a method fromMap
that takes a Map<String, Object>
and a Class
and returns an Object
whose fields are all filled in using
the values in the map.
Assume that the given Class
has a zero argument constructor you can
call to get an "empty" instance of the class.
Add your own toString
methods to the example classes to debug your work.
class HankHill {
public double loveForPropane;
public String shock;
// CODE HERE
}
class BobbyHill {
public boolean hasCulinaryAcumen;
// CODE HERE
}
class CottonHill {
public boolean bitter;
public boolean angry;
public boolean short;
public boolean dead;
// CODE HERE
}
class Main {
Object fromMap(Map<String, Object> o, Class<?> klass) {
// CODE HERE
}
void main() {
IO.println(fromMap(Map.of(
"loveForPropane", 100,
"shock", "bwhaaha"
), HankHill.class));
IO.println(fromMap(Map.of(
"hasCulinaryAccumen", true
), BobbyHill.class));
IO.println(fromMap(Map.of(
"bitter", true,
"angry", true,
"short", true,
"dead", true
), CottonHill.class));
}
}
Challenge 4.
Call all the methods declared on the Dale
class in alphabetical order using reflection.1
Make sure not to call methods inherited from Object
such as toString
, equals
, and hashCode
.
class Dale {
public static void u() {
IO.println("and get yourself out of that tunnel and into some strange woman's bed!");
}
public static void t() {
IO.println("wash off some of that cologne,");
}
public static void d() {
IO.println("and the only way out is through a long dark tunnel.");
}
public static void f() {
IO.println("carrying a boxcar full of heartbreak.");
}
public static void a() {
IO.println("I know how dark it is for you right now");
}
public static void k() {
IO.println("I'm fat and I'm old and every day I'm just going to wake up fatter and older.");
}
public static void q() {
IO.println("Will I be out there next month? If I'm alive, you'd better believe it.");
}
public static void r() {
IO.println("You've got to get up off that tanning bed,");
}
public static void g() {
IO.println("Well let me tell you something:");
}
public static void h() {
IO.println("all you can do is let it hit you and then try to find your legs.");
}
public static void i() {
IO.println("I know - I've taken that hit more times than I can remember");
}
public static void e() {
IO.println("And you're afraid to go in because there is a train coming at ya");
}
public static void j() {
IO.println("Look at me Boomhauer.");
}
public static void m() {
IO.println("I'm out there digging holes, falling into 'em, climbing out, trying again");
}
public static void c() {
IO.println("You're in Hell now Boomhauer");
}
public static void n() {
IO.println("And tomorrow I'm going to hang outside at a ladies' prison,");
}
public static void o() {
IO.println("and the first thing those lady cons are going to see after twenty years is me.");
}
public static void l() {
IO.println("Yet somehow I managed to drag this fat old bald bastard into the alley every day.");
}
public static void p() {
IO.println("Will I get one? Experience says no.");
}
public static void s() {
IO.println("slip into a tight T-shirt,");
}
public static void b() {
IO.println("curled up lying in your own emotional vomit.");
}
}
class Main {
void main() {
var dale = Dale.class;
// CODE HERE
}
}
https://www.youtube.com/watch?v=7nkrzI9GwNk
Annotations

Comments are useful when reading code. Since they can contain any text, you can use them to clarify your intent, make note of issues to address later, etc.
class Main {
// TODO: Make this actually add one
int addOne(int x) {
return x + 2;
}
// The return value here will always be non-negative
int absoluteValue(int x) {
if (x < 0) {
return -x;
}
else {
return x;
}
}
void main() {
IO.println(addOne(5));
IO.println(absoluteValue(-25));
}
}
The problem with comments is that they are just text. It would be difficult to write a program that acts upon or uses information in a comment.
Annotations, which you can think of as "structured comments", are useful for this purpose.
class Main {
@TODO("Make this actually add one")
int addOne(int x) {
return x + 2;
}
@NonNegative int absoluteValue(int x) {
if (x < 0) {
return -x;
}
else {
return x;
}
}
void main() {
IO.println(addOne(5));
IO.println(absoluteValue(-25));
}
}
Declaration
To declare an annotation you use @interface
followed by a type name.
@interface NonNegative {
}
We call these "annotation interfaces."1
Usage
Once you have defined an annotation you can use it to mark different elements of your program.
@interface Even {
}
@interface NumberWrapper {
}
@NumberWrapper // Here @NumberWrapper is annotating the EvenNumber class
class EvenNumber {
final @Even int x; // And @Even is annotating the "x" field
EvenNumber(int x) {
if (x % 2 != 0) {
throw new IllegalArgumentException(Integer.toString(x));
}
this.x = x;
}
}
You can place an annotation on nearly any "structural" element of a program. This includes classes, method definitions, fields, and more.
Elements
Annotation interfaces can contain any number of elements.
@interface Todo {
String description();
}
These are declared in the same way as methods in an interface, save for some important restrictions.
The method should take no arguments.
@interface Todo {
// Cannot take arguments
String description(int x);
}
And the return value needs to be one of a few built-in types like Class
, String
, int
, boolean
, etc.1, an enum
, or an array of one of those.
enum Priority {
HIGH,
LOW
}
class PersonInCharge {}
@interface Todo {
// String ✅
String description();
// int ✅
int someNumber();
// boolean ✅
boolean isImportant();
// Class ✅
Class<?> someRelatedClass();
// Arrays ✅
String[] notes();
// Enums ✅
Priority priority();
// Other Classes ❌
PersonInCharge personInCharge();
}
You can find the full list here. It is a short list.
Usage with Elements
If an annotation is declared with elements then you must specify values for those elements when annotating a part of your program.
@interface Todo {
String description();
}
@Todo(description="Actually write code")
class Code {
}
For String
, int
, and boolean
elements you do this using their corresponding literals.
For Class
elements you use ClassName.class
. And for arrays you use array initializers.
enum Priority {
HIGH,
LOW
}
@interface Todo {
String description();
int someNumber();
boolean isImportant();
Class<?> someRelatedClass();
String[] notes();
Priority priority();
}
@Todo(
description = "Write some code",
someNumber = 444,
isImportant = false,
someRelatedClass = ArrayList.class,
notes = {
"this example is potentially confusing",
"it isn't high priority enough to fix"
},
priority = Priority.LOW
)
class Code {
}
If the only element you need to provide is named value
, then you don't need to give its name.
@interface Todo {
// The name "value" is special
String value();
}
@Todo("Write some code")
class Code {
}
Defaults
You can give a default value for an element by writing default
followed by a value.
You don't need to explicitly specify a value for any elements that have a default.
enum Priority {
HIGH,
LOW
}
@interface Todo {
String description();
Priority priority() default Priority.LOW;
}
// Priority does not need to be specified since
// it has a default specified
@Todo(description="Write the code")
class Code {
// But you can still specify it in situations where you want to
@Todo(description="Write main method", priority=Priority.HIGH)
void main() {
}
}
If all values have defaults then you don't need to specify anything.
enum Priority {
HIGH,
LOW
}
@interface Todo {
String description() default "Do Stuff";
Priority priority() default Priority.LOW;
}
@Todo
class Code {
}
And if the only element that doesn't have a default value is named value
, and thats the only one you want to specify, you don't need to give its name.
enum Priority {
HIGH,
LOW
}
@interface Todo {
String value();
Priority priority() default Priority.LOW;
}
@Todo("Really need to write something in here")
class Code {
}
@Target
Annotations can mark most parts of your program. Annotations are a part of your program. Therefore annotations can have annotations.
The first of these "meta-annotations" you are likely to use is @Target
.
By default an annotation can mark anything, but you can use @Target
to
restrict what can be marked.1
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
// @Even can now only be used on fields and methods,
// but not classes
@Target({
ElementType.FIELD,
ElementType.METHOD
})
@interface Even {
}
The exception to this is the TYPE_USE
target. That one needs to be explicitly specified.
@Retention
The next important meta-annotation is @Retention
.
If you want to be able to access an annotation using reflection
it needs to be marked with a retention policy of RUNTIME
.
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface Special {
}
The other two options are SOURCE
- which will throw away any evidence of the annotation after Java
compiles your code - and CLASS
- which will save the annotation, but not in a way you can use at runtime.
If you are making your own programs that make use of annotations, chances are you will be working with them at runtime. This means you will most likely be using RUNTIME
more than the other options.
That being said, there is nothing particularly wrong with SOURCE
and CLASS
. Its just that writing programs that use the annotations at those steps require some more work and there aren't too many downsides to RUNTIME
.
Reflective Access
If something is annotated with a runtime retained annotation,
you can get an object holding that data using .getAnnotation
.
This will either return null
if the field, method, class, etc. is not
annotated with that annotation or it will give you an object
with methods you can call to see the values specified in the annotation.
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Field;
class Main {
void main() {
Class<Drink> drinkClass = Drink.class;
Field[] fields = drinkClass.getFields();
for (var field : fields) {
IO.print(field.getName());
IO.print(" - ");
Special annotationValue = field.getAnnotation(Special.class);
if (annotationValue == null) {
IO.println("is not special.");
}
else if (annotationValue.isSuperDuperSpecial()) {
IO.println("is super-duper special.");
}
else {
IO.println("is special.");
}
}
}
}
@Retention(RetentionPolicy.RUNTIME)
@interface Special {
boolean isSuperDuperSpecial() default false;
}
class Drink {
public String name;
@Special
public boolean caffeinated;
@Special(isSuperDuperSpecial = true)
public String cost;
public int height;
}
You can make use of this to implement a very wide variety of programs.
@Override
The @Override
you can put on methods to have Java check that you are properly overriding them
is an annotation.
There's not much else to say besides "hey, thats what that was!" but I think its worth noting.
interface Num {
boolean odd();
boolean even();
}
class IntNum implements Num {
final int x;
IntNum(int x) {
this.x = x;
}
@Override // This is an annotation?
public boolean odd() {
return x % 2 != 0;
}
@Override // Always has been.
public boolean even() {
return !odd();
}
}
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Declare your own @Favorite
and @LeastFavorite
annotations.
Use them to mark the code you have written so far that is your favorite and the code that is your least favorite.
Challenge 2.
Declare a @LoFi
annotation. It should have an element for the author
and another element for the song
that you were listening to on
the "24/7 Lofi Hip Hop Radio - Beats to Relax/Study To"
stream when you were writing the annotated code.
Chill out to some tunes, write some code, and use that annotation.
Challenge 3.
Write a method that takes in an Object
. Use reflection to print out the values in that Object
's
declared fields. Skip any that are non-public but also any marked with a @Skip
annotation
that you should also define.
// CODE HERE
class Table {
public boolean flowerPot = true;
public String scissors = "green";
@Skip
public String cat = "tabby";
}
class Main {
void main() {
// CODE HERE
}
}
Interfaces II

Interfaces let you describe a common set of methods shared by different implementing classes.
They can do slightly more than this though and it's helpful to know about.
Default Methods
Interfaces can specify a default
implementation
for a method.
interface Dog {
void bark();
default void barkLoudly() {
IO.print("(loudly) ");
bark();
}
}
Classes which implement interfaces do not need an explicit implementation for methods which have a default.1
interface Dog {
void bark();
default void barkLoudly() {
IO.print("(loudly) ");
bark();
}
}
class Poodle implements Dog {
@Override
public void bark() {
IO.println("bark!")
}
}
If the default implementation is not what you want, then that implementation can be overrided.
interface Dog {
void bark();
default void barkLoudly() {
IO.print("(loudly) ");
bark();
}
}
class Poodle implements Dog {
@Override
public void bark() {
IO.println("bark!")
}
@Override
public void barkLoudly() {
IO.println("BARK!")
}
}
If all of the methods on an interface have a default then you don't need to provide an implementation for any of them.2
interface Cat {
default void meow() {
IO.println("meow");
}
}
class Tabby implements Cat {
// Nothing needed to implement Cat
}
This is rarely useful, but not never.
Interface Extension
Interfaces can extend other interfaces.
interface Dog {
void bark();
}
record Color(int r, int g, int b) {}
interface ColoredDog extends Dog {
Color color();
}
This means a few things. First an implementing class must specify all the methods from both.
class Clifford implements ColoredDog {
@Override
public void bark() { // Must define the methods on Dog
IO.println("BARK BARK");
}
@Override
public Color color() { // As well as on ColoredDog
return new Color(255, 0, 0); // Red
}
}
Second, interface extension "establishes a subtyping relationship." Something which implements a sub-interface can be used in a place expecting the super-interface.
void main() {
Clifford clifford = new Clifford();
clifford.bark();
IO.println(clifford.color());
IO.println("-------");
// clifford is a "ColoredDog"
ColoredDog coloredDog = clifford;
// So all the methods on ColoredDog are available
coloredDog.bark();
IO.println(coloredDog.color());
IO.println("-------");
// all "ColoredDog"s are also "Dog"s
Dog dog = coloredDog;
// So you can use the methods from Dog, but not any
// from ColoredDog
dog.bark();
// IO.println(dog.color()); - Won't work
IO.println("-------");
// and all "Dog"s are "Object"s
Object o = dog;
// So you only have access to the methods from Object unless you
// use instanceof to recover the actual type of the object
if (o instanceof ColoredDog c) {
c.bark();
IO.println(c.color());
}
}
Static Methods
Interfaces may specify static
methods. These work similarly to static methods
on classes in that they can be used without an actual instance of a class.
interface Animal {
static boolean allowedLooseInHouse(String species) {
if ("dog".equals(species) || "cat".equals(species)) {
return true;
}
else {
// My cousin has a Pig that we were all afraid
// was going to straight up eat her child.
//
// The child got old enough that it's not a concern,
// but good God.
return false;
}
}
}
void main() {
IO.println(Animal.allowedLooseInHouse("dog"));
IO.println(Animal.allowedLooseInHouse("cat"));
IO.println(Animal.allowedLooseInHouse("pig"));
}
You may use this mechanic for any reason, but often it is most convenient for "factory methods" - methods which make it easy to construct objects related to the interface hierarchy.
A prime example of this is List.of
. of
is a static method defined on the List
interface.
void main() {
// of will return a List<String>, but code using this
// factory doesn't see the actual implementing class
List<String> critters = List.of("dog", "cat", "bat");
IO.println(critters);
}
If the logic in a static interface method gets complex enough, it is also allowed to define a private static method.
This is unique for interfaces, as usually
everything in them is considered public
.
Even without writing public
, a static
interface method is by default a public method.
interface Animal {
private static void eat() {
}
private static void sleep() {
}
static void live() {
eat();
sleep();
}
}
Static Fields
If there are constants associated with or useful for an interface,
those can be attached to the interface declaration as public static final
fields.
The declaration of these looks the same as declaring a normal field in a class.
The public static final
are implied.
interface Biped {
// This does the same as
// "public static final int NUMBER_OF_FEET = 2"
// in a normal class.
int NUMBER_OF_FEET = 2;
void walk();
}
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Create an interface named TabbyCat
. It should
extend the provided Cat
interface and provide
a lounge
method along with a default implementation of that
method.
interface Cat {
void purr();
}
// CODE HERE
class Garfield implements TabbyCat {
@Override
public void purr() {
IO.println("mmmm lasagna");
}
}
class Main {
void main() {
TabbyCat c = new Garfield();
// Should come in via a default method.
c.lounge();
}
}
Challenge 2.
Add a static field to the Cat
interface
which indicates the healthy amount of lasagna for a cat to
consume.1
interface Cat {
void purr();
}
class Main {
void main() {
// 0
IO.println(Cat.HEALTHY_AMOUNT_OF_LASAGNA);
}
}
Challenge 3.
Put a static method on the Cat
interface named garfield" which returns an instance of
TabbyCat`.
public interface Cat {
// CODE HERE
}
public interface TabbyCat extends Cat {
// CODE FROM PREVIOUS CHALLENGES
}
class Garfield implements TabbyCat {
@Override
public void purr() {
IO.println("mmmm lasagna");
}
}
public class Main {
void main() {
TabbyCat tc = Cat.garfield();
tc.lounge();
}
}
Note that this gives you a way to expose a Garfield
instance
to other packages, even if the Garfield
class itself
is non-public
.2
Zero, it can literally kill them.
Apologies for the inconvenience. To make the point about package visibility I had to make the code in browser non runnable or editable. You should be able to manage at this point though.
Class Extension

Just like interfaces can extend other interfaces, classes can extend other classes.
class BodyOfWater {}
class Ocean extends BodyOfWater {}
Extend a Class
For a class to extend another one you need to write extends
and then the name of the class
which is being extended.
class BodyOfWater {}
class Ocean extends BodyOfWater {}
If the class being extended has non-zero argument constructors,
the subclass must pass arguments to those constructors in it's constructor
using super()
.
class BodyOfWater {
long depth;
BodyOfWater(long depth) {
if (this.depth < 0) {
throw new IllegalArgumentException();
}
this.depth = depth;
}
}
class Ocean extends BodyOfWater {
String name;
Ocean(String name, long depth) {
this.name = name;
// Before you exit, you must pass
// arguments to the super-class constructor.
super(depth);
}
}
void main() {
Ocean pacific = new Ocean("Pacific", 36201L);
IO.println(
"The " + pacific.name +
" ocean is " + pacific.depth +
"' deep."
);
}
Inheritance
When one class extends another we say that it is "inheriting" from the superclass.
What this means is that all the fields and methods of the class being extended carry over into the extending class.
class BodyOfWater {
long depth;
BodyOfWater(long depth) {
if (this.depth < 0) {
throw new IllegalArgumentException();
}
this.depth = depth;
}
void swim() {
if (depth > 50) {
IO.println("You have drowned");
}
else {
IO.println("You are fine");
}
}
}
class Ocean extends BodyOfWater {
String name;
Ocean(String name, long depth) {
this.name = name;
super(depth);
}
}
void main() {
BodyOfWater pacific = new Ocean("Pacific", 36201L);
pacific.swim(); // The swim method is "inherited"
}
Override
Just as you can provide an alternative implementation for a default method in an interface, you can override a method inherited from a superclass.
class BodyOfWater {
long depth;
BodyOfWater(long depth) {
if (this.depth < 0) {
throw new IllegalArgumentException();
}
this.depth = depth;
}
void swim() {
if (depth > 50) {
IO.println("You have drowned");
}
else {
IO.println("You are fine");
}
}
}
class Ocean extends BodyOfWater {
String name;
Ocean(String name, long depth) {
this.name = name;
super(depth);
}
@Override
void swim() {
IO.println("You have been eaten by a shark");
}
}
void main() {
BodyOfWater pacific = new Ocean("Pacific", 36201L);
pacific.swim();
}
This can be prevented by marking a method as final
. This explicitly disallows
overriding it in a subclass.
class BodyOfWater {
long depth;
BodyOfWater(long depth) {
if (this.depth < 0) {
throw new IllegalArgumentException();
}
this.depth = depth;
}
// Cannot be overriden
final void swim() {
if (depth > 50) {
IO.println("You have drowned");
}
else {
IO.println("You are fine");
}
}
}
class Ocean extends BodyOfWater {
String name;
Ocean(String name, long depth) {
this.name = name;
super(depth);
}
@Override
void swim() { // We cannot override a final method
IO.println("You have been eaten by a shark");
}
}
void main() {
BodyOfWater pacific = new Ocean("Pacific", 36201L);
pacific.swim();
}
Another way to prevent this is to make it so the method is not visible to the subclass.
This can be done either by marking the method private
or by having it be package-private
and simply in a different package.
class BodyOfWater {
long depth;
BodyOfWater(long depth) {
if (this.depth < 0) {
throw new IllegalArgumentException();
}
this.depth = depth;
}
// Cannot be override what you cannot see
private void swim() {
if (depth > 50) {
IO.println("You have drowned");
}
else {
IO.println("You are fine");
}
}
}
class Ocean extends BodyOfWater {
String name;
Ocean(String name, long depth) {
this.name = name;
super(depth);
}
@Override
void swim() { // Cannot be override what you cannot see
IO.println("You have been eaten by a shark");
}
}
void main() {
BodyOfWater pacific = new Ocean("Pacific", 36201L);
pacific.swim();
}
If you want to use the definition of the method you are overriding you can access it by writing super.
before the name of the method.
class BodyOfWater {
long depth;
BodyOfWater(long depth) {
if (this.depth < 0) {
throw new IllegalArgumentException();
}
this.depth = depth;
}
void swim() {
if (depth > 50) {
IO.println("You have drowned");
}
else {
IO.println("You are fine");
}
}
}
class Ocean extends BodyOfWater {
String name;
Ocean(String name, long depth) {
this.name = name;
super(depth);
}
@Override
void swim() {
if ("Pacific".equals(name)) {
IO.println("You have been eaten by a shark");
}
else {
// Calls the definition on BodyOfWater
super.swim();
}
}
}
void main() {
BodyOfWater pacific = new Ocean("Pacific", 36201L);
pacific.swim();
BodyOfWater atlantic = new Ocean("Atlantic", 28232L);
atlantic.swim();
}
Protected
If you want code to be accessible by a subclass defined in another package
but not be fully public
, that is where the protected
modifier is useful.
protected
fields, methods, and constructors have the same visibility as package-private ones.
This means classes in the same package can see them but classes in other packages cannot.
The only addition is that it is visible to extending classes regardless of package.
package oceanography;
class BodyOfWater {
// Both the depth field and the following
// constructor are only visible to subclasses
// and classes in the "oceanography" package.
protected long depth;
protected BodyOfWater(long depth) {
if (this.depth < 0) {
throw new IllegalArgumentException();
}
this.depth = depth;
}
}
package bigwater;
class Ocean extends BodyOfWater {
String name;
Ocean(String name, long depth) {
this.name = name;
// The super(depth) constructor is only usable because it is
// visible.
super(depth);
}
}
This is useful for providing methods that you really only expect to be useful for producing subclasses but that you don't want to be available to everyone.
Abstract Classes
Abstract classes are classes which you cannot make an instance of directly.1
abstract class Plant {}
void main() {
// You cannot make an instance because
// it is an abstract class.
var p = new Plant();
}
The use-case for these is making classes which are designed to be subclassed.
An example of this that comes with Java is AbstractList
.
AbstractList
defines most of the methods required by the List
interface and is intended to
lower the effort required to make a custom List
implementation.
import java.util.AbstractList;
// This subclass is a List containing the numbers 5 and 7
class FiveAndSeven
// Almost all of the implementation is inherited from AbstractList
extends AbstractList<Integer> {
@Override
public Integer get(int index) {
return switch (index) {
case 0 -> 5;
case 1 -> 7;
default -> throw new IndexOutOfBoundsException();
};
}
@Override
public int size() {
return 2;
}
}
void main() {
var l = new FiveAndSeven();
IO.println(l);
}
"Abstract" as a term here means something close to "not in reality." You will hear people refer to non-abstract classes as "concrete" classes.
Abstract Methods
Methods on an abstract
class may themselves be marked abstract
.
abstract class BodyOfWater {
long depth;
BodyOfWater(long depth) {
if (this.depth < 0) {
throw new IllegalArgumentException();
}
this.depth = depth;
}
// All classes which extend BodyOfWater must
// define what happens when you swim.
abstract void swim();
}
Any non-abstract class extending from an abstract
class must give a definition
for abstract
methods.
abstract class BodyOfWater {
long depth;
BodyOfWater(long depth) {
if (this.depth < 0) {
throw new IllegalArgumentException();
}
this.depth = depth;
}
abstract void swim();
}
class Lake extends BodyOfWater {
// If you didn't define this it wouldn't work
@Override
void swim() {
IO.println("Relaxing time");
}
}
But if an abstract class is being extended by another abstract class, you don't need to.
abstract class Liquid {
abstract double viscosity();
}
// Water doesn't need to define volume()
// because Water is abstract
abstract class Water extends Liquid {
abstract double purity();
}
// But as soon as you get to a "concrete" implementation
// you need to provide definitions
class TownWater extends Water {
@Override
double viscosity() {
return 0.01;
}
@Override
double purity() {
return 0.99;
}
}
Relation to Interfaces
When you implement an interface, the implementing class guarentees a contract and may inherit some default implementations of methods.
// All implementing classes must define a run method
interface Runner {
void run();
default void runFar() {
run();
run();
run();
}
}
class RoadRunner {
@Override
public void run() {
IO.println("meep meep");
}
}
class Main {
void main() {
Runner r = new RoadRunner();
r.runFar();
}
}
This is also true when extending a class. The difference is that you may inherit fields, behavior dependent on fields, and actual constructors.
abstract class Swimmer {
private int laps = 0;
public abstract void swim();
// The inherited behavior may depend on fields
public void swimFar() {
IO.println("lap: " + laps);
swim();
laps++;
IO.println("lap: " + laps);
swim();
laps++;
IO.println("lap: " + laps);
swim();
laps++;
}
}
class Person extends Swimmer {
@Override
public void swim() {
IO.println("*gasps for air*");
}
}
class Main {
void main() {
Swimmer s = new Person();
s.swimFar();
}
}
Because of these differences while it is possible to implement many interfaces, a class can only extend from exactly one other class.1
Because of these differences, the general consensus is that interfaces are the mechanism to use for abstracting behavior. Abstract classes, by contrast, should be used primarily to reduce duplicated code.2
Some languages do allow for "multiple inheritance." Doing so leads to some wacky and hard to think about things so Java doesn't allow it.
It's a little tricky because both interfaces and abstract classes can be used for similar things. A "pure abstract class" - a class with nothing but a bunch of abstract methods - is very close in concept to an interface. If its all still confusing to you then just ignore everything but interfaces.
Relation to Encapsulation
It takes deliberate effort to properly encapsulate implementation details in the presence of inheritance.
Say there was a class which tracked passengers who booked flights.
class Airplane {
private final List<String> passengers = new ArrayList<>();
void bookOne(String passenger) {
this.passengers.add(passenger);
}
void bookMany(List<String> passengers) {
for (var passenger : passengers) {
bookOne(passenger);
}
}
}
A reasonable extension to this class would be to print a message every time someone books a flight.1
class Airplane {
private final List<String> passengers = new ArrayList<>();
void bookOne(String passenger) {
this.passengers.add(passenger);
}
void bookMany(List<String> passengers) {
for (var passenger : passengers) {
bookOne(passenger);
}
}
}
class PrintingAirplane extends Airplane {
@Override
void bookOne(String passenger) {
IO.println(passenger);
super.bookOne(passenger);
}
}
class Main {
void main() {
var airplane = new PrintingAirplane();
airplane.bookOne("Ned Ludd");
airplane.bookMany(List.of("Robin Hood", "Friar Tuck"));
}
}
But a subtle problem with this is that in Airplane bookMany
is defined in terms of bookOne
.
So in our subclass when we override bookOne
we get the behavior we want. Every time someone is booked
we print their name.
But a small innocuous change in Airplane
could break this. If bookMany
was redefined to not
use bookOne
then you would miss names.
class Airplane {
private final List<String> passengers = new ArrayList<>();
void bookOne(String passenger) {
this.passengers.add(passenger);
}
void bookMany(List<String> passengers) {
for (var passenger : passengers) {
bookOne(passenger);
}
}
}
A reasonable extension to this class would be to print a message every time someone books a flight.1
class Airplane {
private final List<String> passengers = new ArrayList<>();
void bookOne(String passenger) {
this.passengers.add(passenger);
}
void bookMany(List<String> passengers) {
for (var passenger : passengers) {
// Only change
this.passengers.add(passenger);
}
}
}
class PrintingAirplane extends Airplane {
@Override
void bookOne(String passenger) {
IO.println(passenger);
super.bookOne(passenger);
}
}
class Main {
void main() {
var airplane = new PrintingAirplane();
airplane.bookOne("Ned Ludd");
airplane.bookMany(List.of("Robin Hood", "Friar Tuck"));
}
}
This can be adjusted for by overriding both bookOne
and bookMany
in PrintingAirplane
, but you are equally vulnerable to the opposite change.
class PrintingAirplane extends Airplane {
@Override
void bookOne(String passenger) {
IO.println(passenger);
super.bookOne(passenger);
}
@Override
void bookMany(List<String> passengers) {
for (var passenger : passengers) {
IO.println(passenger);
}
super.bookMany(passengers);
}
}
As a general rule, changing a class which may have been extended by other classes requires more effort than if doing so were not an option. This is because in a few ways "inheritance breaks encapsulation." At least in the sense of there being an interplay between mechanics you wouldn't otherwise have to consider.
If you don't want to deal with these issues, you can disallow extending a class.
Well, reasonable in the world of contrived programming examples.
Final Classes
If a class is marked final
it cannot be extended.
final class Apple {}
// Cannot extend a final class
class RedApple extends Apple {}
void main() {}
Because thinking about what the implications are if someone extends a class is hard,
it is reasonable to always declare your classes as final
unless you have some reason not to.
// It's not that something bad would happen if someone
// extended this class. It's that not having to think
// about it is useful.
final class Saltine {
int saltiness;
Saltine(int saltiness) {
this.saltiness = saltiness;
}
}
Records and Enums are always implicitly final.
record Pos(int x, int y) {}
// Records are final
class Pos2 extends Pos {}
enum StopLight { RED, YELLOW, GREEN }
// Enums are final
class StopLight2 extends StopLight {}
class Main {void main() {}}
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Write a class called Tree
which extends ArrayList<Apple>
.
Add a .grow()
method which adds two apples to itself
of arbitrary size.
record Apple(double size) {}
// CODE HERE
class Main {
Tree tree = new Tree();
tree.grow();
tree.grow();
// You should have inherited the toString
// from ArrayList
IO.println(tree);
// As well as all the other methods
tree.add(new Apple(100));
IO.println(tree);
for (var apple : tree) {
IO.println(apple);
}
}
Challenge 2.
Rewrite your Tree
class from above to instead extend AbstractList<Apple>
.
You can find the documentation for AbstractList
here.
Challenge 3.
Make an abstract
class called Fish
. It should provide a method named
swim
and a method named howLongSwim
that returns how many times swim
was called.
This
// CODE HERE
class Dory extends Fish {
void justKeepSwimming() {
IO.println("Just keep swimming");
this.swim();
IO.println("Just keep swimming");
this.swim();
IO.println("Just keep swimming");
this.swim();
IO.println("swimming");
this.swim();
IO.println("swimming");
this.swim();
}
}
class Main {
void main() {
var dory = new Dory();
dory.justKeepSwimming();
IO.println(dory.howLongSwim());
}
}
Challenge 4.
Make the field you use to track how many times
the fish swam protected
. Also make the swim
method protected
and abstract
.
Write comments in your code such that the "contract"
between your class and classes implementing it
is that they must keep the swam
field up to date.
This
// CODE HERE
class Dory extends Fish {
@Override
void swim() {
IO.println("Just keep swimming");
this.swam++;
IO.println("Just keep swimming");
this.swam++;
IO.println("Just keep swimming");
this.swam++;
IO.println("swimming");
this.swam++;
IO.println("swimming");
this.swam++;
}
}
class Main {
void main() {
var dory = new Dory();
dory.swim();
IO.println(dory.howLongSwim());
}
}
Niche Numerics

For a surprisingly wide range of programs int
and double
(along with their boxed counterparts Integer
and Double
)
are the only numeric types you will need.
Every now and then, however, you will want something more exotic1.
void main() {
byte b = 123;
IO.println(b);
}
The reason I am calling these "exotic" and "niche" has nothing to do with how
useful they are. If you want a byte
then a byte
is what you want. Its just that most
of the time (in my experience) you can get by with a little help from your int
s.
byte
A byte
represents a signed value between -128
and 127
.
void main() {
byte a = 127;
IO.println(a);
byte b = -128;
IO.println(b);
}
Operations like +
and *
on a byte
will "promote" the result an int
and you will need to cast the result. Going from an int
to a byte
is a narrowing conversion.
void main() {
byte a = 5;
byte b = 6;
// Need to cast the result to a (byte) again
byte c = (byte) (a * b);
IO.println(c);
}
Conversely, going from a byte
to an int
is a widening conversion and you won't
need a cast.
void main() {
byte a = 5;
int a2 = a; // Widening conversion
IO.println(a2);
}
And if you have need of a potentially nullable byte
, Byte
with a capital B
is the boxed version.
void main() {
// Can't have a null "byte"
// byte b = null;
// But you can have a null "Byte"
Byte b = null;
IO.println(b);
}
You will most often want a byte
when you are trying to save space in memory.
// This array of 4 bytes
byte[] bytes = { 1, 2, 3, 4 };
// Will take up as much space as this
// array with 1 int
int[] oneInt = { 1 };
short
A short
represents a signed value between -32768
and 32767
. Representing a short
takes twice as much memory as representing
a byte
and half as much memory as representing an int
.
void main() {
short a = 32767;
IO.println(a);
byte b = -32768;
IO.println(b);
}
Operations like +
and *
on a short
will "promote" the result an int
and you will need to cast the result. Going from an int
to a short
is a narrowing conversion.
void main() {
short a = 5;
short b = 6;
// Need to cast the result to a (byte) again
short c = (short) (a * b);
IO.println(c);
}
Conversely, going from a short
to an int
is a widening conversion and you won't
need a cast.
void main() {
short a = 5;
int a2 = a; // Widening conversion
IO.println(a2);
}
And if you have need of a potentially nullable short
, Short
with a capital S
is the boxed version.
void main() {
// Can't have a null "short"
// short b = null;
// But you can have a null "Short"
Short b = null;
IO.println(b);
}
A short
also takes up exactly as much space as a char
and converting between the two
is allowed, but will still require an explicit cast in both directions.1
void main() {
short s = 50;
char c = (char) s;
s = (short) c;
IO.println(c);
}
You will most often want a short
when you are trying to save space in memory but
need to represent numbers beyond what a byte
can represent.2
// This array of 2 shorts
short[] shorts = { 1, 2 };
// Will take up as much space as this
// array with 1 int
int[] oneInt = { 1 };
// And as much space as this array with 4 bytes
byte[] bytes = { 1, 2, 3, 4 };
These are neither narrowing or widening conversions. Java just makes you put
the cast. One way to view this is that when you go from a short
to a char
you've transformed a "number" into a "character." So while no information is lost, what the information "represents" has changed.
As you might suspect, this is more rare than the situations where you would want a byte
.
long
If an int
is not big enough for your needs, a long
is twice as big as an int
and can represent numbers from -2^63
to 2^63 - 1
.
You can make a long
from an integer literal, but integer literals do not
normally allow for numbers that an int
cannot store.
void main() {
// Smaller numbers work without issue
long smallNumber = 5;
IO.println(smallNumber);
// This is too big for an int
long bigNumber = 55555555555;
IO.println(bigNumber);
}
For those cases you need to add an L
to the end of the literal.1
void main() {
long smallNumber = 5;
IO.println(smallNumber);
// But with an L at the end, its not too big for a long
long bigNumber = 55555555555L;
IO.println(bigNumber);
}
All operations with a long
will result in a long
. Conversions to int
and
other "smaller" integer types will be narrowing and require a cast.
void main() {
long a = 5;
int b = 3;
// a long times an int will result in a long
long c = a * b;
IO.println(c);
}
And if you have need of a potentially nullable long
, Long
with a capital L
is the boxed version.
void main() {
// Can't have a null "long"
// long l = null;
// But you can have a null "Long"
Long l = null;
IO.println(l);
}
The reason you will likely end up using int
more than long
is that int
works more with other parts of Java. Like array indexing - you can't
get an item in an array with a long
, you need an int
for that.
void main() {
String[] sadRobots = { "2B", "9S", "A2" };
long index = 2;
// Longs can't be used as indexes
String sadRobot = sadRobots[index];
}
But there is nothing wrong with a long
. If you need to represent a number that is potentially bigger than an int
then it is useful.
"L is for long" would be a total cop-out in a children's picture book.
Unsigned Operations
While a byte
represents a signed value between -128
and 127
, its not uncommon to instead want to represent a number between 0
and 255
.
We call representations like that, where we repurpose what would be negative numbers to instead be large positive numbers, "unsigned."
All of Java's built-in numeric types are signed, but you can use static methods to perform operations on them that work as if they were unsigned.
void main() {
// Java sees 255 as being out of range for a byte
// so we have to cast
byte b = (byte) 255;
// And by default this will be seen as -1
IO.println(b);
// But we can use Byte.toUnsignedInt to see it
// as 255
IO.println(Byte.toUnsignedInt(b));
}
Operations like addition with +
don't need special unsigned versions since they work "the same" regardless of whether you ultimately choose to interpret your numbers as unsigned or signed. Other operations like division or comparisons need special logic
to work right when doing "unsigned math."
void main() {
int i = -1;
// -1 is actually 4294967295 when viewed as an unsigned int
IO.println(Integer.toUnsignedString(i));
// So normal comparisons won't do what you want
boolean isFiveBigger = 5 > i;
IO.println(isFiveBigger);
// You'd want to use special unsigned comparisons
isFiveBigger = Integer.compareUnsigned(5, i) > 0;
IO.println(isFiveBigger);
}
All these special unsigned operations are static
methods on each type's corresponding
boxed equivalent class. What static methods are available varies from type to type.
float
What int
is to long
, float
is to double
. Even the type name double
implicitly means "double the size of a float." So if you want something half the size of a double
you can use a float
.
To write a float in a program you need to write f
at the end of the floating
point literal.
void main() {
float f = 3.5f;
IO.println(f);
}
This is because Java sees floating point literals without a trailing f
as representing a double
.
void main() {
float f = 3.5;
IO.println(f);
}
Conversions from a double
to a float
are narrowing and require an explicit cast.
Conversions from a float
to a double
are widening and do not require a cast.
void main() {
double a = 6.5;
// Need a cast
float b = (float) a;
IO.println(b);
float c = 9.5f;
// Do not need a cast
double d = c;
IO.println(d);
}
And if you have need of a potentially nullable float
, Float
with a capital F
is the boxed version.
void main() {
// Can't have a null "float"
// float f = null;
// But you can have a null "Float"
Float f = null;
IO.println(f);
}
You will really only want a float
when you are trying to save space in memory.
Otherwise its best to just use a double
.
// This array of 2 floats
float[] floats = { 1.0f, 2.0f };
// Will take up as much space as this
// array with 1 double
double[] oneDouble = { 1.0 };
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Write two static methods in the following Bytes
class.
The first should take an int
and return an array of 4
bytes.
The second should take that array of 4 bytes and give back the original int.
There are a few ways to do this, but you only pass the test if you can do a "full round trip."
class Bytes {
static int toInt(byte[] bytes) {
// CODE HERE
}
static byte[] toBytes(int value) {
// CODE HERE
}
}
class Main {
void main() {
int i = 172089379;
byte[] bs = Bytes.toBytes(i);
// 172089379
IO.println(Bytes.fromInt(bs));
}
}
Challenge 2.
Turn the String
"hello world"
into a short[]
. Print the contents
of this short[]
and then turn it back into a String
.
class Main {
void main() {
String s = "hello world";
short[] shorts;
// CODE HERE
String roundTripped;
// CODE HERE
}
}
Challenge 3.
Make a class named UnsignedInteger
. It should wrap up an int
such that
all operations on it are performed using the appropriate unsigned specific methods
when needed.
At a minimum:
- Implement
toString
so that the unsigned representation is used - Implement
equals
andhashCode
- Implement
plus
andminus
- Implement the
Comparable<UnsignedInteger>
interface.
// CODE HERE
class Main {
void main() {
var i = new UnsignedInteger(0);
IO.println(i.minus(new UnsignedInteger(1)));
}
}
Challenge 4.
Convert this program to use float
instead of double
.
class Main {
void main() {
double radius = Double.parseDouble(
IO.readln("What is the radius of the circle? ")
);
double pi = 3.14;
double circumference = 2 * pi * radius;
IO.println("Circumference: " + circumference);
}
}
Music Maker
Problem Statement
Sounds are formed by waves propagating through the air at various frequencies. When sound enters your ear it vibrates your eardrum which in turn vibrates other parts of your ear ultimately culminating in your brain perceiving a sound. The frequencies of sound determine in what way your eardrum will vibrate and therefore are what determine how you will percieve any given sound.
If you want to recreate a sound you need to in some way record what that sound was. The oldest example we have of someone doing this dates back to a thousands of years old stone tablet. The idea being that if someone could read that tablet and knew what the symbols on it meant, they could use an instrument of some kind to reproduce the song.
While stone tablets were awesome and paper acceptable for the task, writing down what someone else should play only lets you record music. Writing down words similarly doesn't help record the sound of someone's voice.1
The first of recording of someone's voice was done in 1860 in Paris by Édouard-Léon Scott de Martinville. He would later have his glory stolen by Thomas Edison; a common occurrence for the time.
Nowadays computers are generally more convenient for sharing music and recordings than other methods, if sometimes of lower quality than something like a vinyl record2.
Storing audio on a computer requires translating audio information into a form that a computer can store. We call this translation step "digitization." It requires in some manner storing what frequencies of sounds and in what proportion need to be produced to "play back" a sound.
Your Goal
Make a program that produces a WAV file that, when played, will sound like an instrumental version of Three Blind Mice3.
As a hint: byte
and short
can be helpful when representing "binary formats" like WAV.
Reading comprehension as well as reading stamina will also be useful for figuring out
how a WAV file works. You will need to learn a lot about audio.
Future Goals
When you learn enough to do the following, come back to this project and expand it.
- Make the program produce other songs.
- Expand the program to take as input a text file that in some way describes a song and produce a WAV file in turn.
- Make a "virtual keyboard" where somebody can play notes by typing and whatever they played can be "exported" as a WAV file.
- Support a file format other than WAV as the output.
Abraham Lincoln had a really high pitched trill voice apparently. It is a real bummer we don't have recordings.
This is a subject of much debate.
When Halo Reach came out there was a lot of internet fighting about the "reticle bloom" on a weapon called the DMR. This meant that if you fired it too quickly it would get less and less accurate. The advice I heard around that time was to pull the trigger to the tune of "three blind mice" and that would be about the right timing.
Modules

Every package in Java "lives" in a module.
Just as classes may be grouped into packages, packages may be grouped into modules.
Declaration
To declare a module, you need to create a file named module-info.java
and put it at the top of the folder where your code is.
src/
example/
Example.java
some/
packageName/
AClass.java
module-info.java
Within this file you put module
followed by a name for the grouping of packages and {}
.1
module example {
}
Just like with package names, module names can have multiple parts separated by .
s.
module a.longer.name {
}
Nothing in this declaration says explicitly what packages are part of the module; it is just assumed that it holds the packages it is "next to."
This name doesn't need to be related to the names of the packages, but whenever possible it's best to pick something that makes sense
Restrictions
All classes within a module must be in named packages
Unlike packages, where two packages can have classes of the same name, two modules cannot contain the same package.
This means that you cannot have a class in the java.lang
package defined
in any of your modules, as that already comes with Java.
If you accidentally make a situation where one package is in multiple modules we call that having a "split package" and Java will raise an error.
Exports
Inside your module-info.java
file you declare which packages are
"exported" from that module.
module reality {
exports earth;
exports sea;
exports sky;
}
Any packages that do not have an exports
declaration are "unexported packages."
The classes in unexported packages will not be visible to other modules
even if those classes are public
.
// Because the "backrooms" package is not exported
// from the "reality" module, other modules cannot
// observe classes within it.
package backrooms;
public class YellowCarpet {
}
package sky;
// But classes within other packages of
// the "reality" module can see it just fine.
public class Cloud {
private final YellowCarpet fabricOfExistence
= new YellowCarpet();
// ...
}
Requires
Code within modules can only use classes defined in packages that are part of its module or are in exported packages of modules it "requires."
module reality {
exports earth;
exports sea;
exports sky;
}
module human {
// Code within this "human" module
// will have access to classes in
// the "earth," "sea," and "sky" packages.
requires reality;
}
Modules can both require other modules and export packages for other modules to use.
module human {
requires reality;
exports sadness;
}
These requires
are not allowed form "cycles."
This means that cat
cannot require dog
if dog
also
requires cat
, indirectly or otherwise.
module dog {
requires cat;
}
// Apologies to any hit Nickelodeon shows
// but this is unacceptable.
module cat {
requires dog;
}
Module Imports
A special kind of import is the "module import."
Just like a package import (import package_name.*
) will
import all the classes in that package, an import module
declaration will import all the classes in all the packages
contained within that module.
// Same as writing
//
// import java.util.*;
// import java.lang.annotation.*;
// import java.lang.reflect.*;
// ... and so on for every package in the java.base module
import module java.base;
class Main {
void main() {
// You can now freely use any of the classes from the imported module
Collection<String> c = List.of("do", "re", "mi");
IO.println(c);
}
}
This has much the same upsides and downsides as a package import. It is much easier to write this and get all the classes you want, but in exchange it might be harder to see where a particular class comes from when you are reading code later.
java.base
The java.base
module is where all packages like java.lang
, java.util
, etc.
are defined.
This means it contains classes used by nearly every Java program like java.lang.String
,
java.lang.Integer
, and java.util.ArrayList
.
Because of this, it is the only module you do not need to explicitly require in a module-info.java
file.
module cool.code {
// You can leave this line off
// and it will still require java.base
requires java.base;
}
And when you have a file that makes use of "The Anonymous Main class,"
Java will also act as if you had a module import for java.base
. This
means that you don't actually need an explicit import for classes like ArrayList
.
So if a file has the following
void main() {
var names = ArrayList<String>();
names.add("Him");
names.add("Jim");
names.add("Bim");
IO.println(names);
}
It is equivalent to
import module java.base;
class Main {
void main() {
var names = ArrayList<String>();
names.add("Him");
names.add("Jim");
names.add("Bim");
IO.println(names);
}
}
The Unnamed Module
All packages that are not in a named module are placed in "the unnamed module."
This includes all the code you have written thus far and all code written without
a module-info.java
alongside it.
The unnamed module is special in that code within it requires
every other module
and therefore can see every exported package and every class within.
It is fine to have your code in the unnamed module, but code you get from other people should generally be in named modules. This is because modules are mostly useful for sharing code across "maintenance boundaries."
If you are not going to be sharing your code with another person then the ability to have "unexported packages" matters less. If you are the person who code is shared with, the group sharing it with you will expect you to not touch classes in packages they did not export.
Multi-Module Directory Layout
If you yourself want to develop a project using multiple of your own modules there is a special way to layout the files to do so.
First, make folders with the name of each module. If a module name
has a .
in it the folder should too.
reality/
backrooms/
horrible.monsters/
Then in those folders under a src
folder put the module-info.java
files.1
reality/
src/
module-info.java
backrooms/
src/
module-info.java
horrible.monsters/
src/
module-info.java
From there you can put all the classes you want into each module, so long as they don't conflict or create split packages.
reality/
src/
earth/
Dirt.java
Worm.java
sea/
Starfish.java
sky/
Cloud.java
module-info.java
backrooms/
src/
backrooms/
YellowCarpet.java
module-info.java
horrible.monsters/
src/
horrible/
monsters/
Slime.java
Skeleton.java
module-info.java
This can be helpful in structuring larger projects.
The src
folder isn't technically required. You will see what a --module-source-path
looks like in a bit. I think its a good idea anyways.
Purpose
It might be a little unclear at first what reason modules have for existing.
After all, don't packages already group code? Why would you want groupings of a grouping?
The answer is that1 when you share code with other people you will often want to have that code contain multiple packages. If you couldn't "hide" packages of code from the people using the code you are sharing it would be a bummer.
Just as private fields help you encapsulate state in a class and package-private classes help you encapsulate whole classes, unexported packages are your mechanism for encapsulating whole package.
And by "you" I mean you and whoever else is directly working with you.
There are more answers, but they mostly involve tales of Java's past and mistakes that were made there.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Add a module-info.java
to one of your projects.
Challenge 2.
Move some of your code to use the multi-module directory layout. If you need to split one of your projects into multiple modules to pull this off, do so.
Challenge 3.
Intentionally create a split package situation. What error does Java give you?
Challenge 4.
Intentionally create a cycle in the "module graph." What error does Java give you?
Lambdas

Making an implementation of interface
s that
only require one method turns out to be a common
task for a Java developer.
interface Band {
void playHitSong();
}
class Starcadian implements Band {
@Override
public void playHitSong() {
IO.println("ultralove");
}
}
As such there is a mechanism called "lambdas" which lowers the effort required to do so.
interface Band {
void playHitSong();
}
class Main {
void main() {
Band starcadian = () -> IO.println("ultralove");
starcadian.playHitSong();
}
}
Functional Interfaces
If an interface has only a one method that needs to be implemented we would call that a "functional interface."1
interface Band {
// Only one method to implement
// "single abstract method"
void playHitSong();
}
Any other methods on the interface must be default
or static
.
interface Band {
void playHitSong();
// Neither the default or static method are considered
default void encore() {
this.playHitSong();
this.playHitSong();
}
static int turnDial(int level) {
if (level == 10) {
return 11;
}
else {
return level;
}
}
}
Functions take input and return an output. We call them functional interfaces because with you can treat them as being functions whose input and output are the same as that one method to be implemented.
You might also see these referred to as SAM interfaces. SAM for Single Abstract Method.
@FunctionalInterface
If an interface is marked with the @FunctionalInterface
annotation,
Java will verify that it fulfils the requirements for a functional interface.
@FunctionalInterface
interface BankRunner {
void runOnBank();
}
void main() {}
This is similar to the @Override
annotation in that it doesn't affect how code works
but just adds in an extra guard rail.
@FunctionalInterface
interface BankRunner {
// More than one required method, will error
void runOnBank();
int applyInflation(int money);
}
void main() {}
Lambda Expressions
To make an implementation of a functional interface you can use a "lambda expression."
If the method on the functional interface
takes no arguments, you write () ->
followed
by the code to run when that method is called.
@FunctionalInterface
interface Band {
void playHitSong();
}
class Main {
void main() {
Band twrp = () -> {
IO.println("Got no commitments today");
IO.println("No work all play");
}
}
}
Just like with switch
if there is only one line to run you may omit the {}
s.
@FunctionalInterface
interface Band {
void playHitSong();
}
class Main {
void main() {
Band theProtomen = () -> IO.println("Light up the Night!");
}
}
Arguments
If the method on the functional interface takes arguments
you can include those in the parentheses before the ->
.
@FunctionalInterface
interface Singer {
void sing(String title, double volume);
}
class Main {
void main() {
Singer woodkid = (String title, double volume) -> {
IO.println(
title
+ " by woodkid is now "
+ (volume > 10 ? "BLASTING" : "playing"
)
);
};
// AC Revelations wasn't the best game
// but that trailer was awesome.
woodkid.sing("Iron", 11);
}
}
If it won't lead to any ambiguity Java also allows you to omit the types from the argument list.
@FunctionalInterface
interface Singer {
void sing(String title, double volume);
}
class Main {
void main() {
Singer twrp = (title, volume) -> {
IO.println(
title
+ " by twrp is now "
+ (volume > 10 ? "BLASTING" : "playing"
)
);
};
woodkid.sing("Pets", 7);
}
}
Further, if there is only one argument you may omit even the ()
.
@FunctionalInterface
interface Conductor {
void conduct(String tempo);
}
class Main {
void main() {
Conductor jkSimmons = tempo -> IO.println("Not my tempo.")
jkSimmons.conduct("4/4");
}
}
Return
If the method on the functional interface has a return value
that is not void
you can return a value from a lambda
the same as from a method.
This will not return from any enclosing method.
@FunctionalInterface
interface Farmer {
String farm(String item);
}
class Main {
void main() {
Farmer jeremyClarkson = item -> {
if (Math.random() < 0.1) {
return "failure";
}
else {
return item;
}
};
IO.println(jeremyClarkson.farm("potato"));
IO.println(jeremyClarkson.farm("sheep"));
}
}
If the value is returned by a single expression you
do not need return
or {}
.
@FunctionalInterface
interface Farmer {
String farm(String item);
}
class Main {
void main() {
Farmer caleb = item -> item;
IO.println(caleb.farm("potato"));
IO.println(caleb.farm("sheep"));
caleb = item -> item.toUpperCase();
IO.println(caleb.farm("potato"));
IO.println(caleb.farm("sheep"));
}
}
Method References
Often it will make sense to separate the code for a lambda into its own method.
interface Drummer {
void drum();
}
class Main {
void doDrum() {
IO.println("ratatatatat");
IO.println("crash!");
IO.println("crash!");
IO.println("ratatatatat");
}
void main() {
Drummer drummer = () -> doDrum();
drummer.drum();
}
}
When you have a method that matches1 the method required by a functional interface you can use a "method reference."
For instance methods this will look like instance::methodName
. So inside a method
this can be this::methodName
but you can also have a variableName::methodName
interface Drummer {
void drum();
}
class Main {
void doDrum() {
IO.println("ratatatatat");
IO.println("crash!");
IO.println("crash!");
IO.println("ratatatatat");
}
void main() {
Drummer drummer = this::doDrum;
drummer.drum();
Runnable run = drummer::drum;
}
}
For static methods it will look like ClassName::doDrum
.
interface Drummer {
void drum();
}
class Main {
static void doDrum() {
IO.println("ratatatatat");
IO.println("crash!");
IO.println("crash!");
IO.println("ratatatatat");
}
void main() {
Drummer drummer = Drummer::doDrum;
drummer.drum();
}
}
And finally for instance methods where the the functional interface's method takes the instance explicitly as an argument it will look the same as a static method reference.
interface StringTransformer {
String transform(String s);
}
class Main {
void main() {
// Instance method, but looks like a reference
// to a static method that just so happens to
// take a String as a first argument
StringTransformer t = String::toUpperCase;
IO.println(t.transform("ratatatatat"));
}
}
It doesn't have to match exactly but describing the exact matching mechanism isn't that important. Play it by ear.
Inference
If Java cannot figure out exactly what functional interface you want to use it will not allow you to use a lambda or a method reference.
class Main {
void main() {
// Does not know what type the "var" should be
var ride = () -> IO.println("cruisin'");
}
}
To resolve this you need to give Java a hint as to what interface it should resolve to. In addition to simply giving explicit types to variables and fields you can do this by "casting" the expression to a functional interface.
@FunctionalInterface
interface Trip {
void takeOff();
}
class Main {
void main() {
var ride = (Trip) () -> IO.println("cruisin'");
ride.takeOff();
}
}
Built-In Functional Interfaces
Java comes with many functional interfaces as part of its standard library.
You are under no obligation to use any of these. They are convenient because they are already at hand, but they tend to have very "generic" names. It is often better to make your own. That being said:
One functional interface that comes with Java is Runnable
1.
This describes a method that takes no arguments and returns no values.
@FunctionalInterface
public interface Runnable {
void run();
}
Another is Function
. This is a generic interface that describes
a function taking only one argument. Function
is notable
in that it is an example of a functional interface that contains
both default methods and static methods.
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
}
default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}
static <T> Function<T, T> identity() {
return t -> t;
}
}
Then there is Consumer
for something that takes an argument and returns something. BiFunction
and BiConsumer
for things that take two arguments, Supplier
for something
that takes nothing and returns a value... and so on.
Really the important thing is just to know that some of these exist. There are only slim benefits to using them over interfaces you make yourself. Lambdas and method references work the same for everyone.
I think it is really neat that Runnable came with Java 1.0 - nearly two decades before lambdas were in the language - and yet works perfectly fine with them. That's design bay-bee2.
Read that in the Austin Powers voice.
Checked Exceptions
One thing that might catch you off guard is how lambdas and method references interact with checked exceptions.
Normally if the implementation of a functional interface throws an exception that is allowed.
class Main {
void main() {
Function<String, Integer> parseObject
= Integer::parseInt;
int x = parseObject.apply("1657");
IO.println(x);
}
}
But if the implementation throws a checked exception Java will complain.
This is because checked exceptions need to be declared in a method signature. If the functional interface you are making an implementation does not declare a checked exception is thrown in its single method
import module java.base;
@FunctionalInterface
interface Loader<T> {
T load();
}
class Main {
void main() throws IOException {
Files.writeString(Path.of("one.txt"), "One and only one");
// Files.readString can throw an IOException and that is checked
Loader<String> loader = () -> Files.readString(Path.of("one.txt"));
IO.println(loader.load());
}
}
One solution is to edit the functional interface such that it declares the thrown exception.
import module java.base;
@FunctionalInterface
interface Loader<T> {
// If we add this throws it will be fine.
T load() throws IOException;
}
class Main {
void main() throws IOException {
Files.writeString(Path.of("one.txt"), "One and only one");
Loader<String> loader = () -> Files.readString(Path.of("one.txt"));
IO.println(loader.load());
}
}
The other solution is to catch and rethrow any exceptions as unchecked.
import module java.base;
@FunctionalInterface
interface Loader<T> {
T load();
}
class Main {
void main() throws IOException {
Files.writeString(Path.of("one.txt"), "One and only one");
Loader<String> loader = () -> {
try {
return Files.readString(Path.of("one.txt"));
} catch (IOException e) {
// No issue throwing unchecked exceptions
throw new UncheckedIOException(e);
}
};
IO.println(loader.load());
}
}
Which of those two solutions you pick will be context dependent.
If you don't control the implementation of the functional interface
(such as with the built-in Runnable
, Function
, Supplier
, etc.)
your best option is the second one.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Replace the Sunflower
class with a lambda expression.
@FunctionalInterface
interface Flower {
void bloom();
}
class Sunflower implements Flower {
@Override
public void bloom() {
IO.println("bright yellow flower.");
}
}
class Main {
void bloomField(Flower flower) {
for (int i = 0; i < 15; i++) {
flower.bloom();
}
}
void main() {
Flower sunflower = new Sunflower();
bloomField(sunflower);
}
}
Challenge 2.
Add a new method to the Flower
interface above while
keeping the @FunctionalInterface
annotation. What error does Java
give you?
What errors do you see if you remove the annotation but still have lambdas in your code?
Challenge 3.
Change the lambda expression you used for the Flower
interface
to a method reference.
Challenge 4.
One of these snippets of code will throw a NullPointerException
. The other
will not. Which do you think will do which? Why do you think that is?
@FunctionalInterface
interface Screamer {
void scream();
}
class Lemon {
void grab() {
IO.println("UNACCEPTABLE!");
}
}
class Main {
void main() {
Lemon lemon = null;
Screamer s = lemon::grab;
}
}
@FunctionalInterface
interface Screamer {
void scream();
}
class Lemon {
void grab() {
IO.println("UNACCEPTABLE!");
}
}
class Main {
void main() {
Lemon lemon = null;
Screamer s = () -> lemon.grab();
}
}
Challenge 4.
You should have done this challenge before:
Make a method named
promptGeneric
which can prompt the user for information but, based on if what they typed is properly interpretable, can reprompt them.As part of this make a
Parser
interface and at least two implementations:IntParser
andDoubleParser
.
If not, do so now. Then in either case replace the usages of IntParser
and DoubleParser
with method references.
// CODE HERE
class Main {
// CODE HERE
void main() {
int x = promptGeneric(
"Give me an x: ", new IntParser()
);
double y = promptGeneric(
"Give me a floating point y: ", new DoubleParser()
);
}
}
Compilation

The first step to sharing code you've written with other people is to compile it.
Compilation in the context of programming languages means that a program reads all your code, ensures it abides by the rules of the language, and translates your code to some other form. Usually this other form is in some manner more directly "runnable" than the code you started with.
We call programs that do this reading, ensuring, and translation "compilers."
This happens behind the scenes when you run java src/Main.java
but for sharing code you will need to do it yourself as a separate
step.
javac
The compiler used for compiling Java code is called javac
.
This stands for "Java Compiler".
Its job is to take a list of .java
files and compile
them into .class
files.
For a single file Java program you can do this by running a command similar to the following.
javac -d output src/Main.java
The -d output
in that example means "put the compiled class files in a folder called output
."
After running the command above you would expect to see something like the following.
src/
Main.java
output/
Main.class
You aren't guarenteed that any given .java
file will produce only one .class
file
as output. Inner classes are one reason for this, but there are others.
src/
Main.java
output/
Main.class
Main$1.class
Main$Thing.class
Class Files
The format that the Java compiler outputs is called a "class file."
These class files are what is loaded into the "Java Virtual Machine" to actually run your program. This is in part because the Java Virtual Machine was conceived as a general tool not technicially specific to Java.
There are other languages besides Java - such as Clojure and Kotlin - which also can be compiled into class files.
Java Source Code --> javac -------------\
----> Class Files --> Java Virtual Machine
Kotlin Source Code --> kotlinc ---------/ |
/
Lombok Source Code --> lombokc ----------/
... and more
This lets those languages make use of the Java Virtual Machine and all the millions of dollars and decades of work put into making it run code fast.1
I bring this up because if you are only thinking about Java it might seem like a pointless extra step. It somewhat is, but at the same time it lets other languages "compete" on more or less even ground. The JVM is a labor of fill-in-the-blank, that is for sure.
Modules
For reasons that will become apparent as we proceed, it is best if all all the code you intend to share is contained within a named module.
This means at minimum your classes would need to be in packages
and would need to provide a module-info.java
.
Many things will work even without that but some important ones will not.
Compile Multiple Files
To have javac
compile multiple files you have a few options.
The first is to simply list every file you want to compile one after
the other when calling javac
.
javac -d output src/Main.java src/Other.java src/Another.java
This has the obvious downside of needing you to add new files to what can become a large list over time.
If you get far enough in learning bash you can paper over this
with commonly available tools like find
.
javac -d output $(find ./src -name "*.java" -type f)
Where the $()
is bash syntax that runs the command in the parentheses and uses its
output as arguments to the command being run. find
is a tool that lists all files that
match some criteria. In this case all files (as opposed to folders) that have a name that
ends with .java
.
The other option is to structure your project using the multi-module directory layout.
your.project/
src/
code/
Main.java
Other.java
module-info.java
If you do this then you can compile all the code in a module by using the --module-source-path
and --module
options.
javac -d output --module-source-path "./*/src" --module your.project
Clean
If you compile code multiple times you should "clean" your working directory before each compilation.
This is because when you run javac -d output ...
the compiler
will simply dump class files into the destination folder.
It will not remove any "stale" class files from earlier compiler runs.
This can lead you to be in a state where you think your code is working but if you ever compiled it again, from scratch, you would get errors.
The simplest way to handle this in bash is to use the rm
tool. rm
removes
files. Before you run the compiler clear out any old output with rm -rf output
.
-rf
stands for "recursive" and "force." This is what you need to delete whole
folders.1
This technique - just deleting folders and doing the work from scratch again - can be slow. The upside of this approach is that it is simple to understand. It also probably doesn't matter since your computer is fast. When you need to really efficiently compile huge codebases you turn to a special kind of program called a build tool. These are complex beasts but can avoid unneccesary work.
-g
There are a lot of options you can pass to javac
to alter its behavior.
One of the ones that I find useful is -g
. The g
stands for "generate debug info."1
This makes sure the generated class files have information about things like what variables were named what. This, in turn, helps Java give better error messages
For example, if you have a program like the following:
class Main {
void main() {
String nocturne = null;
IO.println(nocturne.length());
}
}
While in all situations it will ultimately crash with a NullPointerException
, if
you did not compile with -g
you might get a stack trace like the following.
You will probably need to scroll to the right to see the relevant bit.
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "<local1>" is null
at Main.main(Main.java:4)
Whereas using -g
will get you an exception that includes the variable name of what was null
.
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "nocturne" is null
at Main.main(Main.java:4)
So, with rare exceptions, you should always use -g
.
Obviously.
Running Compiled Code
The way you ultimately run your compiled code depends on whether or not all your code is in packages.
If you have any classes in the unnamed package - which will only be the case when said code is also not in a named module - you should run your code like so:
java --class-path output Main
Where you can substitute Main
for whatever the class you want to run is.
So if you want to run a class named Impromptu
in the chopin
package
you would run java --class-path output chopin.Impromptu
.
--class-path
should be self-explanatory. It is the path where java
will look for class files.
But if you do not have any classes in the unnamed package - which will be hopefully be the case when you share code with others1 - you instead want to run your code like this.
java \
--module-path output \
--add-modules ALL-MODULE-PATH
composers.Main
The --module-path
option is very similar to the --class-path
option. The difference
is that all the code on the --module-path
will be loaded with more strict rules.2
The --add-modules ALL-MODULE-PATH
bit just means "load all the code on the module path."3
Remember the social convention of reverse domain name notation (com.google
, etc)
You can think of --class-path
as sort of a "compatibility mode" for libraries
and code written before Java had a concept of modules. The difference isn't super important
other than the very specific case of wanting to compile and run classes in the unnamed package.
Well that and "troublesome" libraries, but we will get to that later.
Chances are Java will make this the default in the future. For now you must write it though.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Take your code from one of the previous projects and compile all of it to class files.
Make sure you can then run it using those class files.
Challenge 2.
Take the code for one of the previous projects and put it in the multi-module directory layout.
Compile it using the --module-source-path
and --module
options instead of listing the files out explicitly.
Make sure you can then run it using the --module-path
and --add-modules ALL-MODULE-PATH
flags.
Challenge 3.
Run the command from the previous command again, this time making sure to pass -g
.
Be sure to also clean whatever directory you used for output.
Packaging

Once you have a folder full of class files you technically have enough to share your code.
You can take that folder, put it on a flash drive, and give it to your friend. They can then run your code, presuming they have Java installed as well.
But for several reasons sharing a folder of "loose" classes is unideal. What you really want is to package those classes up into a single file.
This way you never accidentally forget one of the files and its easier to share.
jar
The tool you use to package up class files in Java is called jar
.
The output from this tool is a "JAR file."
To make a JAR file, first compile your code into a folder.
If you do this by listing all the files out one-by-one, your output folder will be structured something like the following.
output/
some_pkg/
A.class
nested/
B.class
If you did it using the --module-source-path
your class files will be nested
under a folder with the name of the module.
output/
ex.mod/
module-info.class
some_pkg/
A.class
nested/
B.class
Then use a command like the following:
jar \
--create \
--file ex.mod.jar \
-C output/ex.mod .
The --create
part means "we are creating a new jar file"
and --file
says what file to put the jar in. For the file name
you should generally use the name of the module followed by .jar
.1
This is technically optional though.
The -C
flag is where it gets interesting. The jar
tool works somewhat like how your terminal
does when you cd
into directories.
The tool starts at the directory you are running it at.
project/ <--- jar "is here"
output/
ex.mod/
module-info.class
some_pkg/
A.class
nested/
B.class
Then with -C
you "Change" into a directory.
project/
output/
ex.mod/ <--- jar is here after "-C output/ex.mod"
module-info.class
some_pkg/
A.class
nested/
B.class
After that you are meant to specify what files you want to add into the jar. Just putting one period (.
)
means "take everything in this folder." Hence -C output/ex.mod .
puts all those files into the final JAR.
And, like I've mentioned before, you should ideally be packaging up code in modules.
Jar Files
JAR files - short for Java Archive - are just ZIP files with a few special bits of "metadata."
ZIP files are a common way of bundling a bunch of files up into one file.1
You don't need to know exactly where this metadata goes or what all of it is for yet, just that at a high level its all just files in a ZIP.
This bundling up also generally includes "compression," where the single file might be smaller than the combined sizes of its components. Most people don't need to think about this nowadays. Class files are small and hard drives are big. I say that knowing full well that 145GB of the 500GB hard drive my work laptop is Baldurs Gate 3. I have 4GB left.
--module-path
After packaging code into a JAR file you can put that
JAR onto the --module-path
in the same way you would a folder
of classes.
java \
--module-path ex.mod.jar \
--add-modules ALL-MODULE-PATH
some.pkg.Main
If you have multiple JARs to put on the module path
you can do so by separating them with a :
.1
java \
--module-path ex.mod.jar:other.thing.jar \
--add-modules ALL-MODULE-PATH
some.pkg.Main
Instead of --add-modules ALL-MODULE-PATH
followed by a class name you
can use --module
followed by <module name>/<class name>
java \
--module-path ex.mod.jar \
--module ex.mod/some.pkg.Main
On Windows instead of a :
you use a ;
. I am assuming when making these examples
that you are using Windows Subsystem for Linux. If you are not, just adjust as needed.
--main-class
When making a JAR you can specify a "main class"
for that JAR using the --main-class
flag.
jar \
--create \
--file ex.mod.jar \
--main-class some.pkg.Main \
-C output/ex.mod .
This makes it so that you can run the code in a JAR without knowing which class in particular you intend to run. You only need to specify the module name.
java \
--module-path ex.mod.jar \
--module ex.mod
Libraries
It is often the case that it isn't practical to produce a program using only the modules that come with Java.
To cope with this reality you can use code written by others as "libraries" of functionality.1
When running a Java program from source you can provide the libraries
using --module-path <path to library jar> --add-modules ALL-MODULE-PATH
.
So if you had a library named tangerine.jar
you could run something like the following
command.
java \
--module-path tangerine.jar \
--add-modules ALL-MODULE-PATH \
src/Main.java
You do not need the --add-modules ALL-MODULE-PATH
if your code is itself inside of a named
module. The requires
in the module take care of telling Java what to include.
java \
--module-path tangerine.jar \
ex.mod/src/Main.java
If you use libraries you need to provide the same flags to javac
when compiling
your own code to share.
java \
-g \
-d output \
--module-path tangerine.jar \
--module-source-path "./*/src" \
--module example.module
Now that you can package your own code into JARs you can share code with others for this purpose.
Libraries can depend on libraries which depend on other libraries in sprawling nightmare graphs. For now let us assume that the libraries you want to use are relatively self-contained and reasonable to download manually. Dependency resolution and procurement can be a topic for a later day.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Package up the code from the previous section's challenges into JAR files.
Run those JAR files using --module-path ...
and --module
.
Challenge 2.
Inspect the contents of the JARs you just created with jar --list <file>
and jar --describe-module <file>
.
Ensure that the module names you picked match up with the file names for the jars.
Challenge 3.
Use --main-class
to specify the main classes for the previous modules.
You can make sure you did it right by trying to run them directly.
just

As you might be noticing, commands in the terminal can get quite long.
Not only are you liable to make a mistake typing out java --module-path a:bunch:of:files --add-modules ALL-MODULE-PATH src/Main.java
for the 20th time, you are also just going to get annoyed doing so.1
To remember what commands to run to do certain tasks I recommend using a tool called "just."
help:
just --list
clean:
rm -rf output
compile: clean
javac \
-d output/javac \
--module-source-path "./*/src" \
--module dan.da.dan
package: compile
jar \
--create \
--file output/jar/dan.da.dan.jar \
-C output/javac/dan.da.dan .
This section is ultimately optional, but you will quickly see a need for something that helps you remember commands.
Generally when something is "your job" or "the right way" you do need to just suck it up and do the manual labor. But there are limits to this, human nature being what it is.
Installation
As you might have noted, just
is not a tool that comes with Java.
This means that you will need to install it yourself. The process for doing so differs based on what kind of computer you have but, if you've made it this far, you should have it in you to figure it out.
You can find instructions from the authors starting at https://just.systems/man/en/prerequisites.html.
Justfile
To use just
, first make a file named Justfile
and put it at the top of your project.
project/
dan.da.dan/
src/
module-info.java
characters/
MoMo.java
JiJi.java
Justfile
In this file put the following contents.
help:
just --list
This makes it so that if you run just
or just help
it will list the "recipes" available for the project.
Recipes
A Justfile contains multiple "recipes."
These are lists of commands to run when you type just <recipe name>
.
help:
just --list
compile:
rm -rf output
javac \
-d output \
--module-source-path "./*/src" \
--module dan.da.dan
So in the example above just compile
will first run rm -rf output
followed by javac <...>
.
The first recipe in a file is run by default when you type just
. This is why we put
the help
recipe first in the file - it is convenient.
$ just
just --list
Available recipes:
compile
help
$ just help
just --list
Available recipes:
compile
help
Dependencies
If there are recipes you want to run before other recipies
you specify them after the :
of the recipe declaration.
This can be useful for things like always removing an output directory before compiling or packaging.
help:
just --list
clean:
rm -rf output
compile: clean
javac \
-d output/javac \
--module-source-path "./*/src" \
--module dan.da.dan
package: compile
jar \
--create \
--file output/jar/dan.da.dan.jar \
-C output/javac/dan.da.dan .
So in the example above, all the commands in the "compile
" recipe
run before the commands in the "package
" recipe. Before "compile
"
the commands in the "clean
" recipe run.
This is useful for making these sorts of "chains" of commands, but isn't strictly required.
Documentation Comments
You can describe what a recipe does by putting a #
followed by the description
above the recipe
# Lists available recipes
help:
just --list
# Cleans build output
clean:
rm -rf output
# Compiles the code
compile: clean
javac \
-d output/javac \
--module-source-path "./*/src" \
--module dan.da.dan
# Packages the code
package: compile
jar \
--create \
--file output/jar/dan.da.dan.jar \
-C output/javac/dan.da.dan .
These documentation comments will appear alongside the recipe name with just --list
, which
is the command our help
recipe runs.
$ just
Available recipes:
clean # Cleans build output
compile # Compiles the code
help # Lists available recipes
package # Packages the code
Further Reading
What has been covered so far should be enough for our stated goal of remembering what commands to run to do stuff.
But there is more to just
than that. If you are interested, you should start
by reading the documentation put out by the people who develop it.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Make a Justfile with a recipe named hello
that runs echo "hello"
.
Test it by running just hello
.
Challenge 2.
Make a run
recipe that launches one of your programs.
Challenge 3.
Take the commands you used to compile one of your projects
and put them into a Justfile
.
Ensure that just compile
will compile your code and just clean
will
delete any output directories.
Challenge 4.
Expand the Justfile
from the previous challenge to also package
your code into a JAR.
Challenge 5.
Expand the Justfile
from the previous challenge to compile
and package multiple modules, not just one.
Documentation

When sharing code that is intended to be used as a library it is important to explain how exactly your code should be used.
If you can physically or digitally converse with the people you are sharing with this is a solved problem. They can ask you questions and you can answer them. Rinse and repeat until they no longer need to ask you questions.
But seeing as there are millions of developers in the world that isn't always practical.1
One tool we use to combat this reality is documentation. Documentation is where we write down what things are, what they do and why.
Would make for one hell of a hectic group text.
Documentation Comments
To document our code we can use "documentation comments."
These work the same as regular comments but instead of using two slashes (//
)
we use three slashes (///
).1
You put these documentation comments above different elements of your program with text describing what the purpose of those elements are.
/// This class represents a ninja
public class Ninja {
/// Says a catchphrase
public void forsooth() {
// ..
}
}
There is an older way to make documentation comments using /**
and */
(note the extra *
in /**
), but we prefer this one because the way you write the actual content of the comment is easier. You are sure to see these around though.
Format
import java.io.IOException;
/// This class serves as a place to put examples
/// of the different kinds of documentation as well
/// as how to write documentation properly.
///
/// When specified after the three slashes
/// you can write documentation using [Markdown](https://en.wikipedia.org/wiki/Markdown).
///
/// In Markdown you can just write text as you would in any file,
/// line after line.
///
/// # For headings you can use a single hash
///
/// ## For subheadings you can use two
///
/// ### And so on
///
/// You can make unordered lists using hyphens
///
/// - A
/// - B
/// - C
///
/// And numbered lists like so
///
/// 1. One
/// 2. Two
/// 3. Thre
///
/// And so on. Definitely peruse up [tutorial on markdown](https://www.markdownguide.org/getting-started/)
/// when you have the time.
///
/// There are some additions specific to Java though.
/// We call these additions "tags."
///
/// One notable tag is the author tag. It lets you mark who worked
/// on a given unit of code
///
/// @author Ethan McCue
/// @see [https://www.oracle.com/technical-resources/articles/java/javadoc-tool.html](https://www.oracle.com/technical-resources/articles/java/javadoc-tool.html)
public class DocumentationExample {
/// You can document the purpose of parameters
/// to constructors and methods with the param tag
///
/// @param o Demonstrates a param tag on a constructor
public DocumentationExample(Object o) {
}
/// Generic parameters can also be documented using the param tag
/// as well.
///
/// @param item The item to just return back
/// @param <T> The type of the item.
public static <T> T identity(T item) {
return item;
}
/// The exceptions thrown by a method can also be documented
/// to explain under what conditions the exceptions might be thrown
///
/// @param s A parameter to show that throws can be used alongside params
/// @throws IOException whenever the method is called, no matter what
public void crash(String s) throws IOException {
throw new IOException();
}
/// You can reference other classes and methods on those classes with the
/// link tag.
///
/// For instance {@link String} and {@link String#length()}.
public void seeMore() {
}
}
javadoc
The way to take code with documentation comments and produce a documentation website
is with the javadoc
tool.
There are multiple ways to run this1, but if you have your code in the
multi-module directory layout you can use --module-path
and --module
the
same as javac
does.
javadoc \
-d output/javadoc \
--module-source-path "./*/src" \
--module one.piece
This will produce a directory full of HTML files that contain all the documentation from the specified modules.
This is what is used to make the official Java documenation as well as at least part of the documentation for most Java libraries.
As is a theme with command-line tools
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Run javadoc
on some of your code. Add the command for doing so to a Justfile
.
Make sure to include cleaning stale of output.
Challenge 2.
When you ran javadoc
on your code you were almost certainly hit by a deluge of warning: no comment
messages.
Fix these by documenting that code enough that javadoc
no longer gives you warnings.
Challenge 3.
Read the documentation for java.util.StringJoiner.
If you can, alter one of your projects to use it.
Streams

Programs often have to transform all of the elements in a collection in order to produce a new collection.
For instance: take all the people in a List
, remove any who are
older than 65, and extract their names into a set.
import module java.base;
record Person(String name, int age) {}
class Main {
void main() {
List<Person> people = List.of(
new Person("Jess", 29),
new Person("Sally", 72),
new Person("Bess", 41)
);
Set<String> names = new HashSet<>();
for (var person : people) {
if (person.age() < 65) {
names.add(person.name());
}
}
IO.println(names);
}
}
While such transformations can always be done in "normal code," it can be preferable to use "streams."
import module java.base;
record Person(String name, int age) {}
class Main {
void main() {
List<Person> people = List.of(
new Person("Jess", 29),
new Person("Sally", 72),
new Person("Bess", 41)
);
Set<String> names = people.stream()
.filter(person -> person.age() < 65)
.map(Person::name)
.collect(Collectors.toSet());
IO.println(names);
}
}
stream
A Java Stream
represents a "stream1"
of elements coming from some "source."
You can stream the elements of a List
or Set
by using the .stream()
instance method
on each.
Stream<String> heroes = List.of(
"Deku",
"Explosive Hero: Great Explosion Murder God Dynamight",
"Froppy"
).stream();
Stream<String> villains = Set.of(
"All for One",
"Muscular"
).stream();
For arrays there is a static method on Arrays
which
can stream their contents. You could also first convert the
array to a collection with Arrays.asList
.
Stream<String> heroes = Arrays.stream(new String[] {
"Lin Ling",
"Lucky Cyan"
});
Stream<String> villains = Arrays.asList(new String[] {
"E-Soul"
}).stream();
If you legitimately do not have a source collection
you can also create a stream directly with Stream.of
.
Stream<Character> letterStream = Stream.of('a', 'b', 'c');
In real life a stream flows from some source of water to some destination. The water can carry rocks and other things along with it. So that is where the metaphor comes from. Real life streams carry rocks, Java streams carry chunks of data.
map
Once you have a stream of elements you can transform the
elements of the stream as they flow by1 with .map
.
.map
applies a Function
to the elements of the stream
one by one and returns you a new Stream
containing the new elements.
var numbers = List.of("1", "2", "3");
Stream<Integer> numberStream = numbers.stream()
.map(Integer::parseInt); // 1, 2, 3
In the real life stream metaphor, this is akin to rocks getting polished by sand as they flow.
filter
With a stream of elements you can also drop the
elements of the stream as they flow by1 with .filter
.
.filter
will test each element with a predicate. If the
predicate returns true
the element will be retained.
If it returns false
the element will be dropped.
var numbers = List.of("1", "2", "3");
Stream<Integer> numberStream = numbers.stream()
.map(Integer::parseInt) // 1, 2, 3
.filter(x -> x % 2 == 1); // 1, 3
In the real life stream metaphor, this is akin to rocks stuck along the way and not continuing to go with the flow of water.
Terminal Operations
We call .map
, .filter
, and methods like them "intermediate operations."
This is because they run "in the middle" of the entire process.
For consuming a stream you use "terminal operations." Terminal operations "consume" the stream and produce some result.
The simplest terminal operation is .forEach
. It consumes the entire stream and does
something for each element in the flow.
void main() {
List<String> cities = List.of(
"St. Louis", "Dallas", "London", "Tokyo"
);
cities.stream()
.filter(city -> !city.startsWith("S"))
.forEach(IO::println); // Dallas, London, Tokyo
}
Once a terminal operation has been performed the stream is no longer usable.
void main() {
List<String> cities = List.of(
"St. Louis", "Dallas", "London", "Tokyo"
);
Stream<String> citiesStream = cities.stream()
.filter(city -> !city.startsWith("S"));
// Dallas, London, Tokyo
citiesStream.forEach(IO::println);
// java.lang.IllegalStateException: stream has already been operated upon or closed
citiesStream.forEach(IO::println);
}
Collectors
The most common kind of terminal operation you will perform on a stream is "collecting" elements into a new collection.
For this you use the .collect
method along with an implemention of the
Collector
interface.
void main() {
List<String> roles = List.of("seer", "clown", "nightmare");
Function<String, Integer> countVowels = s -> {
int vowels = 0;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u') {
vowels++;
}
}
return vowels;
};
List<Integer> vowelCounts = roles.stream()
.map(countVowels)
.collect(Collectors.toList());
IO.println(vowelCounts);
}
There are implementations available as static methods on the Collectors
class for
collecting into List
s, Set
s, and even Map
s.
Because collecting into specifically a List
is so common, there is
also a .toList()
method directly on Stream
that serves as a shortcut
for .collect(Collectors.toUnmodifiableList())
.
void main() {
List<String> roles = List.of("seer", "clown", "nightmare");
Function<String, Integer> countVowels = s -> {
int vowels = 0;
for (int i = 0; i < s.length(); i++) {
char c = s.charAt(i);
if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u') {
vowels++;
}
}
return vowels;
};
// There is also Collectors.toUnmodifiableList
List<Integer> vowelCountsList = roles.stream()
.map(countVowels)
.collect(Collectors.toList());
IO.println(vowelCountsList);
vowelCountsList = roles.stream()
.map(countVowels)
.toList();
IO.println(vowelCountsList);
// ...and Collectors.toUnmodifiableSet()
Set<Integer> vowelCountsSet = roles.stream()
.map(countVowels)
.collect(Collectors.toSet());
IO.println(vowelCountsSet);
// ...and Collectors.toUnmodifiableMap
Map<String, Integer> vowelCountsMap = roles.stream()
.collect(Collectors.toMap(
s -> s,
s -> countVowels.apply(s)
));
IO.println(vowelCountsMap);
}
Purpose
The purpose of streams is to "de-couple" the source of data, the transformations to apply on that data, and the final shape you want that data in.
This serves one somewhat holistic goal and one practical one.
The holistic goal is that you are "declaring what you want done" as opposed to "expressing how you want something done." This is often referred to as "declarative" vs "imperative" programming.
Compare these two bits of code. They both add one to every number in a list.
List<Double> doubles = List.of(1.5, 2.5, 3.9);
List<Double> newDoubles = new ArrayList<>();
for (double d : doubles) {
newDoubles.add(d + 1);
}
List<Double> doubles = List.of(1.5, 2.5, 3.9);
List<Double> newDoubles = doubles.stream()
.map(d -> d + 1)
.toList();
You can argue that the second code snippet more transparently "reflects the intent"
than the first one. The mechanics of looping and adding to a new list are
hidden and the only things on screen are doubles.stream()
(using this collection as a source),
.map(d -> d + 1)
(apply this transformation), and .toList()
(put the results in a list.)
This has its upsides and downsides. Part of the trouble is that there isn't a good rule of thumb as to when you should use a regular loop or use a stream - it often comes down to personal taste and other "soft" factors.
The mechanical reason for streams is that, because the operations to perform are separated somewhat from how to perform them, there are opportunities for Java to be "smart" about how it does them. The usual example given for this is "parallel streams," which we can get into eventually.
Challenges
Remember the rules for this are
- Try to use only the information given up to this point in this book.
- Try not to give up until you've given it a solid attempt
Challenge 1.
Translate the following code using a for-loop to code using streams.
import module java.base;
class Main {
void main() {
for (int i = 0; i < 10; i++) {
IO.println(i);
}
}
}
Challenge 2.
Translate the following code using a for-loop to code using streams.
import module java.base;
class Main {
void main() {
List<Integer> timestamps = List.of(1, 1756137441);
Set<Instant> instants = new HashSet<>();
for (int timestamp : timestamps) {
instants.add(Instant.ofEpochSecond(timestamp));
}
IO.println(instants);
}
}
Challenge 3.
Read the documentation on Collector
and Collectors
.
Make an implementation of Collector
that can collect elements into MySpecialList
.
import module java.base;
class MySpecialList<T> extends ArrayList<T> {}
class Main {
// CODE HERE
void main() {
MySpecialList<Integer> l = Stream.of(1, 2, 3)
.collect(/* CODE HERE */);
}
}
What Now?
If you have made it this far:
- Congratulations, you have now learned Java!
- You are probably wondering what you should do next.
Go Deeper
No matter how much I write there is no chance I will have covered all of the Java language nor all of what you might want to know to write software in Java.
With what you've learned so far you should have a solid enough foundation to go off and learn from other sources. I'll try and paint a picture of the landscape for you before you run off though.
Build Tools
First you probably are going to want to learn a build tool. I haven't covered how to get dependencies yet and that is on purpose.
In the Java world - due to the ability to launch a program with java src/Main.java
being
a pretty recent development - all the tools that help you automatically download
libraries written by other people are married with tools that "build" - i.e. run javac
, jar
, etc.
for you - your code.
There are two major build tools (meaning widely used) in the Java world (Maven and Gradle) as well as many more niche ones (bld, mill, etc.).
If you want a gentle introduction to this world you can start with bld, though be aware this will be a road less travelled.
If you want to learn the one that will probably be the most useful to you in a professional setting you should learn Maven first.
("Maven By Example" book here)
If you are angling to get into Android development you should learn Gradle. Hop ahead and check out the resources for Kotlin too because Kotlin is the language you will use for Gradle build scripts.
Minecraft

If your age begins with the number 1
you are either near death or statistically
very interested in Minecraft.
A lot of people who learn Java do so in order to be able to write Minecraft mods or plugins for Minecraft servers, it is normal.
Just a few words of caution:
- The world of Minecraft development can be deeply exploitative. If you are not a full adult please do not try to "work" for anyone. Be careful. Talk to a parent or an adult you trust when people online seem to want something from you.
- The kinds of code you write to make mods work will be pretty different than the kind of code you would write for most other kinds of software. This is partially because of what modding is (adjusting software whose evolution you do not control) and partially because of peculiarities around Minecraft in particular.
With that all said: there are two basic "mod compatibility layers." These are "Fabric" and "Forge."
The point of these is to give you code to program against that isn't the direct Minecraft source code, which can change very frequently, and to give your mod a fighting chance of being compatible across multiple Minecraft versions.
Of the two: this book probably prepared you the best for Forge.
Forge requires you to use Gradle which in turn will require at least a little knowledge of Kotlin. You don't need to take a full detour through that to get started, but you should put both of those on your list of things to learn.
"Modded Minecraft" Discord here
"Minecraft Mod Development" Discord here
Fabric pretty quickly requires you to interact with a concept called a "Mixin." This is a mechanism the Minecraft modding world made for magically editing the code inside Minecraft among other things. If you go this path just be ready for that.
For making plugins that run on a custom Minecraft server - so things that handle custom chat commands and things of that nature - you have to use the plugin required by whatever server you are using. I am not the most up to date with Minecraft, but I know there is both Spigot and PaperMC. I have been told that Spigot is the preferred option as it allows for Bedrock players to play as well.
PaperMC community Discord here
Websites
Making websites is a profitable career path. At least it is at the time of writing. There are a few essential things you will need to learn about to get started with that path.
The first is how to make an HTTP Server. HTTP Servers are what web browsers talk to in order to render websites.
There are a lot of tools for this in Java. A lot a lot. I would recommend
starting with the one that comes built-in: the jdk.httpserver
module.
Then you will need to learn about SQL databases and how to query from/insert into them from Java.
A good start for that is SQLite - it is a database that runs self-contained in a single file.
After that - or as part of that - you should learn about JDBC. This is the way you interact with a database from Java.
Then from there you should learn about Dependency Injection
And finally you can dive into the world of Spring - which is likely the most employable one of the many HTTP server options out there.
Spring Academy Courses here - the basic ones are free
Desktop Applications
If you want to learn how to make desktop applications in Java you have basically three paths.
Path #1 is to learn Java Swing. This is an old crusty GUI framework that is kinda difficult to use but has the pro of coming with Java and being able to run on every potato in existence.
Docs for java.desktop
(Swing) here
Path #2 is the learn JavaFX. By all accounts JavaFX is better software than Swing, but it was cursed by coming out at a point in history where desktop apps were no longer big business to develop. It was eventually removed from the JDK and you will need to procure it like any other dependency.
Learn a New Language
An unfortunate truth is that you cannot use Java for everything.
Sometimes this is intrinsic - Java would not be a good choice for code driving a pacemaker - and some of it is just how the ecosystems around languages are right now.
Even if you could get away with only using Java, there is significant value in knowing multiple languages. Particuarly languages that are different than Java.
JavaScript

To make highly interactive programs that run inside a browser you will need JavaScript. JavaScript and languages that compile to JavaScript are basically the only language it is practical to use for the frontend of a website1.
It is probably worth your time to learn how to write JavaScript.
Mozilla Developer Network's JavaScript Guide here
The most popular language that compiles to JavaScript is TypeScript. After you've learned JavaScript that is a good one to touch next.
There are languages out there like TypeScript that compile to JavaScript - and you can find some projects out there that do much the same for Java - but just practically speaking learning JavaScript is going to be something you have to do at some point if you get into making websites.
C#

C# is a language broadly similar to Java. It has a lot of features Java doesn't - for better or worse - but the basics are comparable.
C# is very prevalent in the game development world. The Unity game engine, corporate blunders notwithstanding, is still very big and still has you use C# for game scripts. Competitors like Godot have C# Scripting as well.
You will also find C# being used to make websites and desktop apps but it doesn't have as much unique pull there as it does in game development.
Kotlin

Kotlin is one of a family of languages that try to be a "better Java." Better is relative, but you are likely to learn something when diving into it.
Regardless, Kotlin is what you nowadays use to write Gradle build scripts in and Kotlin is the de-facto language for writing Android apps.
This means that, while Java in some form is still technically an option, all the documentation for Android will be using Kotlin for their examples and most new frameworks will assume you are using Kotlin in preference to Java.
And Gradle is a build tool that many Java projects choose to use. You probably don't need as deep of an understanding of Kotlin to work with Gradle build scripts as you do to make a full app in it, but it can't hurt.
Getting Started with Kotlin tutorial here
If you want to make Desktop, Mobile, or Web apps in Kotlin it is probably also worth checking out Jetbrains Compose Multiplatfrom.
Getting Started for Jetbrains Compose Multiplatfrom here
Others
Other languages you might want to learn that I haven't written up context for quite yet:
- C

- C++
- Clojure
- Elm
- Haskell
- Python

- Ruby

- Go

- Rust
- Zig
- Clojure
Fight me, WebAssembly fans.