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
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.
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.
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.
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 differences between these mascots are representative of the differences between the two languages.
1
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.
2
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.
Code is written in "text files." These are files that just contain
text.
This is different from something like Microsoft Word or the notes app on a phone.
Those kinds of programs are for "rich text." Meaning you can bold words, change font
colors, embed images - all sorts of stuff.
Text files only store text.
To create and edit text files we use programs called "text editors."
A term you might have heard or will hear in the future is "IDE."
IDE stands for "Integrated Development Environment." It is basically a text editor
with a bunch of features "integrated" that ease "development" of software.
Programs like Eclipse, IntelliJ, and Netbeans
are all considered Java IDEs. This is because they come integrated with
functionality that makes developing specficially programs in Java easier.
Text editors that have plugins available can often be juiced up enough that they will also
be considered "IDEs." This is the case for VSCode
which has two Java plugins as well as plugins which support development in other languages.1
1
Now does any of this matter right now? No. I just think its good to not get confused
or get unneeded FOMO.
There are a lot of text editors people use to write programs.
For the purposes of this book I am going to reccomend you use
one called "VSCodium."
If you have already been introduced to a different text editor
and want to use it that will be fine. The reasons I am suggesting
this one are as follows:
It will not have AI extensions enabled by default.
It will not have a Java plugin installed by default.
It will work about the same as VSCode, another competent editor.
Now you might read that list and think "why wouldn't I want a Java plugin?"
That is a valid question. The answer is that such plugins are usually made with
working professional developers as their intended user. The kinds of support
they provide can be helpful in preventing you from making all sorts of mistakes
and help you to produce working code quicker.
Somewhat counter-intuitively - and you'll have to take my word for this -
when you are learning you actually want to be making mistakes and producing
working code slower. Your primary goal should
be to acquire a skillset and "bumping into the guard rails"
is helpful for that.
Unfortunately the defining characteristic of a Chromebook is that you cannot install
"normal" software on it.
This is also generally true for computers provided to you by a school. Sometimes they will
be okay with you running things normally, sometimes not.
For these situations VSCodium is not an option so for now I am going to recommend falling
back to using a service called "Github Codespaces." This is a service provided by Microsoft
where they let you use VSCode in a web browser.
After pressing . you should be redirected to a page that looks like this
Click the button on the bottom labeled "Continue Working in GitHub Codespaces."
Select the smallest instance size from the dropdown that appears.
After it loads for awhile you will see a pop-up in the bottom left of your screen asking
if you want to install an extension for Java files.
You want to say no to this.
You will also want to say no to all extension suggestions.
Next on the chopping block is the AI thing on the right. You want to uncheck the "Show View by Default." option on it then click the `x` to dismiss it.
After vanquishing evil, your next task is to type `java src/Main.java` into the "Terminal" area at the bottom of your screen.
Press the enter key and you should see "Hello, world" be printed out.
You can skip the next section on installing Java for now. You are ready to proceed with the book.
If you are on Windows you want to click this link.
This will download a file. Double click it and it will open an installer like this. Follow it through to the end selecting all the default options
When you open the program it installs you should see a screen like this.
In the top-left corner select File -> Open Folder
Make a folder somewhere on your computer to store your code. It doesn't matter what you call it.
Then make a new file named src/Main.java
Inside of this file put the following contents.
void main() {
IO.println("Hello, world");
}
Now skip ahead to the next section on installing Java. Come back when you are done.
Once you are back you want to open a new terminal.
Type java src/Main.java in the terminal and press enter to run your first program.
If this doesn't work you might need to restart your computer or you might have messed up a step.
If you have an Apple M1 Mac or newer then you need to scroll down
to the "ARM 64bits" section (of whatever the newest release is) and click this link.
If you have an older Mac then you want to click this link.
This will download a file. Double click it and it will open a window like this.
Then drag the VSCodium logo to the Applications folder.
Then you should be able to find and open the app from the Launchpad app.
When you open it you should see a screen like this.
In the top-right corner select File -> Open Folder
Make a folder somewhere on your computer to store your code. It doesn't matter what you call it.
Then make a new file named src/Main.java
Inside of this file put the following contents.
void main() {
IO.println("Hello, world");
}
Now skip ahead to the next section on installing Java. Come back when you are done.
Once you are back you want to open a new terminal.
Type java src/Main.java in the terminal and press enter to run your first program.
If this doesn't work you might need to restart your computer or you might have messed up a step.
You can skip this step. The instructions I gave to get set up with Github Codespaces
should have given you an environment where Java is already set up.
Download the "JDK MSI" from adoptium.net.
Select whatever the newest version is. You will need at least Java 25 to follow along with the
examples in this book.
Run the installer, selecting all the default options. This should make the java command available to
you.
Download the "JDK .pkg" from adoptium.net.
Select whatever the newest version is. You will need at least Java 25 to follow along with the
examples in this book.
Run the installer, selecting all the default options. This should make the java command available to
you.
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.
1
Apologies if you find these instructions a little barebones.
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
1
Asterisks apply, of course. I didn't run or try to find a survey so this is mostly anecdotal.
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
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
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
To create a blank file you can use the touch1 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
At the start of this book I showed you VSCodium. If you weren't otherwise shown a different option, that is a decent default. You can use it to create and edit files.
1
This is called touch because it will "touch" a file and update its "last updated timestamp" but change nothing else.
2
These have their die hard supporters. The lunatics.
3
This book has been written inside Visual Studio Code.
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.
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.
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.
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.
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.
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);
}
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);
}
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);
}
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 = ▢;
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.
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.
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.
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.
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);
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);
}
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);
}
You can multiply any two ints 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);
}
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);
}
To get the remainder of the division between two integers you can use the % operator.
This is called the "modulo operator."
With ints 7 / 2 will give you 3. That 3 is the "quotient" from the division
and is the number of times 2 can be taken out of 7. This leaves a "remainder" of 1.
The modulo operator gives you that remainder.
void main() {
int x = 5;
// 5 / 2 is 2 with a remainder of 1
// y will be 1
int y = x % 2;
// 5 / 3 is 1 with a remainder of 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.
Any two ints can be inspected to see if their value is equal by using the == operator.
Unlike the previous operators, which all take ints and produce ints as their result, == takes two ints
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);
}
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.
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.
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.
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.
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".
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);
}
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.
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);
}
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.
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);
}
You can subtract any two doubles 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;
}
You can multiply any two doubles 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);
}
You can divide any two doubles 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
1
With the caveat that the result is now potentially inaccurate.
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);
}
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);
}
Negative numbers will also have their decimal part dropped. So numbers like -7.2, -7.6, and -7.9 will
all be converted into -7.
void main() {
int x = (int) -7.2;
int y = (int) -7.6;
int z = (int) -7.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.
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.
To convert from an int to a double, you don't need to do any special work. All ints are
representable as doubles 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 ints is being assigned to
a double, the computation will still be performed using the same rules ints 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);
}
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);
}
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';
All chars have a matching numeric value1. '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);
}
chars will be automatically converted to ints 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 Mars2 or
if you want to see if a character is in some range.
1
You can find some of these values in an "ASCII Table."
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 = '👨🍳';
chars 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.
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";
}
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 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.";
}
1
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.
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 Strings.
Video Games where characters have assigned names can assign an empty String
as the name of otherwise "unnamed" characters.
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.
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);
}
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.
Without adding any new printlns,
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));
}
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.
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.
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.
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.");
}
For characters specifically you can get the first character of a String with .charAt(0),
but you might also want to check that said String is only one character long. Each type
will be special.
void main() {
String gradeString = IO.readln("What is your letter grade? ");
char grade = gradeString.charAt(0);
IO.println("You have a " + grade + " in the class.");
}
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
}
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
}
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.
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.
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 ifs that might be a sign that
you should reach out for help1.
if (...) {
if (...) {
if (...) {
if (...) {
// Seek professional help
}
}
}
}
1
Or a sign that you should keep reading. There are things I will show you that can help you avoid this - like "methods."
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 ifs 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.");
}
}
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);
}
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.
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
1
Some people will call this a ternary expression. Ternary meaning "three things." Same idea as tres leches.
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);
}
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);
}
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.
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
1
"We called him Tortoise because he taught us." - Lewis Carroll
If a while loop will never end, we call that an endless loop1.
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).
1
If you run an endless loop that does nothing but loop your computer might start overheating. This is a natural part of the process and ultimately healthy for your CPU fans.
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
// When this line is reached it will skip
// all the lines after this in the function
// and immediately go back to the top of the
// loop
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.
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.
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.
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;
}
}
}
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.
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++;
}
}
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--;
}
}
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
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>
}
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");
}
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.
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.
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
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 }.
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.
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.
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."
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);
}
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
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.
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
*/
}
}
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
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
}
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);
}
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
}
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);
}
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
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 ints and Strings, but if you
are feeling clever you can use this ability along with arrays or other things you
can change without reassigning a variable.
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 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
}
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
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.
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.
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
1
The word method comes from a "method of getting things done." You might also hear methods referred to as "functions".
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.
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 + "!"
);
}
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);
}
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
);
}
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
1
Adding final to all arguments can make it harder to read the code, simply because of visual noise.
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.
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.
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
1
"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.
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;
}
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.
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() {
// Printing something to the screen is different
// than returning a value.
IO.println(title() + " only has " + views() + " views.");
}
// This is what the void in "void main()" means
void main() {
talkAboutVideo();
}
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 doubles to ints,
you must do it yourself.
double returnNine() {
double nine = 9.0;
// The (int) explicitly converts the double to an int
return (int) nine;
}
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.
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);
}
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;
}
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";
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];
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]);
}
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
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
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];
1
And if you are using a multi-dimensional array, you are already doing something interesting.
Write a method named winner. It should take in a 2-dimensional
array of Strings 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);
}
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.
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.
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.
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.
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.
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.
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.
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
1
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.
2
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.
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);
}
1
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.
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];
}
}
1
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.
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.
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 ].
1
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.
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]);
}
1
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.
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.
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
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.
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 {
}
1
For things that are not English or are acronyms the rules get fuzzy. Use your best judgement.
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.
1
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();
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);
}
If you make multiple instances of a class with new
those classes will each have their own values for
fields.
class Muppet {
String name;
}
void main() {
Muppet kermit = new Muppet();
kermit.name = "Kermit The Frog";
Muppet animal = new Muppet();
animal.name = "animal";
// kermit and animal are distinct muppets
// and thus each have their own name.
IO.println(kermit.name);
IO.println(animal.name);
}
Each instance has what we would call its own "identity."
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);
}
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.
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 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?");
}
}
}
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();
}
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());
}
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
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.
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.
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.
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);
}
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.
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..
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.
In English, 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.
This does not change the original String in place. It just makes a new String with all lower-case letters.
Many other languages also have a distinction between upper-case and lower-case and, usually, .toLowerCase() does the "right thing" for them.
void main() {
// Cyrillic
IO.println("ПРИВЕТ".toLowerCase()); // prints "привет"
}
In different languages the mapping between upper-case and lower-case letters can differ.
For example, in Turkish the lower-case form of the letter I is ı and not i.
By default, Java uses the rules for the language of the computer it is running on.
So "I".toLowerCase() will return "i" on an English system and "ı" on a Turkish one.
To get behavior that is consistent regardless of the machine it is being run on
you can pass use an explicit Locale.
void main() {
IO.println("I".toLowerCase(Locale.US)); // i - on every computer
IO.println("I".toLowerCase(Locale.forLanguageTag("tr-TR"))); // ı - on every computer
}
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.
If you want to check if two Strings contain the same contents
but you do not care if those contents differ in the casing of letters, you can use the .equalsIgnoreCase
method.
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.
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.
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);
}
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");
}
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");
}
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.
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
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.
2
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.
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.)
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.
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);
}
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.
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 ifs and else ifs.
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
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;
}
}
}
1
This is sometimes the case! It is really just this specific form of switch that has this restriction.
2
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.
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).
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.
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);
}
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);
}
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);
}
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 + ".");
}
1
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.
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)
}
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.
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)
}
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);
}
1
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.
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.
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;
}
}
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.
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
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);
}
1
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.
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
}
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);
}
}
1
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.
During the summer between 3rd grade and 4th grade my parents sent me to "baseball camp."
This was because despite playing baseball for three years at that point
I had only hit the ball twice1. I was often put in the left outfield position
where I would pick dandelions. Being literally "left out" made me sad
but the whole display made my father proud.
Instead of applying myself and learning how to play baseball I had a notebook
where I wrote out all the different games of tic-tac-toe determined to "crack the code"
and never lose again. I thought I was being so smart and, in my defense, so did the
other children. A loser, yes - but smart.
Even today when I go to dinner with my siblings or parents we ask for the kids'
menu and crayons over 50% of the time. We then
play at least one game of tic-tac-toe at the table of a nice Italian restaurant2.
Make a program that lets you play tic-tac-toe against the computer.
Make sure that the player is not allowed to make invalid moves, that the computer
does not make invalid moves, and that the game will not crash on unexpected
input from the player.
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.
1
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.
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.
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
1
There is a whole sect of folks who write their programs as actual books. Look up "Literate Programming" to dive deeper in to that.
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
1
https://arxiv.org/abs/2302.00009
2
No offense intended if you are still having trouble with loops. Its common, you will eventually get past it.
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.
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.
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.
1
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.
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.
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.
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!");
}
1
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"
If you want a unchecked exception and do not know of a better one, RuntimeException will do.
void main() {
throw new RuntimeException("Crash!");
}
1
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.
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
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.
1
These examples are all silly, I know. Once we get to files the mechanics will become useful.
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));
}
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));
}
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.
1
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.
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 null1.
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);
}
1
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?
project/ <- Open this in your text editor
src/ <- Code will go here
1
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.
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.
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.
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.
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.");
}
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!");
}
}
}
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;
}
}
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 Strings 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()
);
}
1
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.
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().
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.
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."
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);
}
}
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;
}
1
Well, in addition to the generally rampant immaturity of programmers.
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;
}
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);
}
}
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";
}
1
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.
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
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);
}
}
1
This won't work if you defined Position inside the anonymous main class. I'll tell you why later.
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);
}
}
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
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.
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.
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;
}
}
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.
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
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.
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.
1
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.
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++;
}
}
1
I love obvious and circular statements like this, if you haven't noticed.
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.
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.
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" });
}
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.
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.
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.
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.
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 {}
}
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();
}
}
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);
}
}
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;
}
}
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();
}
}
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");
}
}
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.
1
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.
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.
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.
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);
}
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.
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.
}
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.
1
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?"
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();
}
}
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.
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;
}
}
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.
""");
}
}
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;
}
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;
}
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();
}
}
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);
}
}
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;
}
}
1
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.
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.
1
That might be mixed with other code written by different companies or by different people.
2
This is why many tools will have your starter project have classes in the com.example package
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.
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.
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.
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.
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);
}
}
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 Strings are equal.
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.
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
}
1
For you non-native English speakers, a shorthand is a shortened form of something. TTYL is "shorthand" for "Talk to you later."
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 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.
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
}
}
1
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.
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.
1
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.
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.
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
}
}
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.file1
package. You create these Paths 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);
}
}
1
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.
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.
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);
}
}
}
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
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.
2
Mostly just because it can let you "shrink the scope" of the try-catch.
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.
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
}
}
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.
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
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)
);
}
}
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());
}
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);
}
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);
}
In addition to toString, two methods that all Objects 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());
}
}
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);
}
}
1
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.
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).
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.
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);
}
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);
}
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.
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.
1
An "unbounded" type variable to be exact. We'll visit generic bounds later.
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>();
}
}
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);
}
}
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.
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)";
}
}
1
For now*
2
All methods that come from an interface must be public.
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.
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.
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"));
}
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());
}
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()
);
}
}
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.
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.
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.
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.
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);
}
}
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);
}
}
1
UTC stands for "Coordinated Universal Time" in English and "Temps Universel Coordonné" in French. The specific letter order of "UTC" (rather than "CUT" or "TUC") was chosen as a compromise between the two languages.
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.
1
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.
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.
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
}
}
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.
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 ZonedDateTimes, convert the ZonedDateTimes to Instants, and then get the Duration between those Instants. Then get the number
of minutes in that Duration.
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.
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.
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.
You need to be careful about that though. If your ArrayList holds Integers 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.
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);
}
}
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);
}
}
Using the ArrayList of CrimeCoins, 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);
}
}
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"));
}
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.
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.
1
"Mc" and "Mac" are common irish surnames and Boston has a sizable irish population.
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));
}
}
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));
}
}
Strings, all the numeric types like Integer and Double, as well as Booleans are defined
in this way.1 So will any records and enums 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));
}
}
1
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.
Both objects with reference based and value based definitions of equals and hashCode
are "appropriate" to use as keys in HashMaps.
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. HashMaps 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.
Make a Library class. It should have four exposed methods
add which takes a Book and adds it to the library.
list which returns an ArrayList<String> of all the Books in the library.
The Strings in this list should be the ISBN
of the book.
find which takes a String representing the ISBN and returns a full Book record.
remove which takes a String representing the ISBN and removes that Book 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());
}
}
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)
);
}
}
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));
}
}
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.
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.
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 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.
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
1
There are asterisks to this particular point, unfortunately.
There are ways to magically get at private fields like criminal scum.
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.
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;
}
}
}
}
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");
}
}
1
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!
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");
}
}
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)
);
}
}
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.
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).
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.
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.
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.
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);
}
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
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."
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.
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.
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")
);
}
}
}
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
);
}
}
}
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 next1. 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.
This is needed because Iterators 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();
// ...
}
1
There is actually one more method: remove. Not all Iterators support it so we'll cover it once we've introduced more Iterable things.
2
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/[], `
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");
}
}
}
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);
}
}
}
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);
}
}
1
The reason I say "should" is that doing the book keeping needed to know when
this has happened can be hard and not all Iterators in the world will do it. The check is what we would call "best effort."
2
And, as these footnotes have alluded, there is a .remove method on Iterator. We'll cover it
later.
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();
}
}
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);
}
}
}
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);
}
}
}
}
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."
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.
1
You know, when you have a question and just have to "bing it?"
There are interfaces 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
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.
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
1
I took some creative liberties here, roll with it.
2
Laugh it up.
3
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.
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.
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.)
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.
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.
1
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.
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
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."
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);
}
}
}
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 are the odd duck out in the world of collections. They are basically a List but aren't Lists.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");
}
}
1
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.
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.
The collections returned by these of methods are immutable. This means methods
which would change the underlying collection will throw an UnsupportedOperationException.2
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);
}
}
1
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.
2
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.
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.
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.
1
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.
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;
}
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;
}
}
1
The rules for this go a bit beyond "you cannot read private fields."
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;
}
}
1
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.
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() {
}
}
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() {
}
}
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, the first argument is ignored. You can pass null1.
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(null, 5);
biteMethod.invoke(null, 1);
}
}
class Apple {
public static void bite(int times) {
IO.println("You took " + times + " bite" + (times < 1 ? "." : "s."));
}
}
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;
}
}
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;
}
}
1
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.
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;
}
}
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()));
}
}
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()));
}
}
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));
}
}
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
}
}
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));
}
}
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.
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, another annotation interface, or an array of one of those.
enum Priority {
HIGH,
LOW
}
@interface Deadline {
int year();
int month();
int day();
}
class PersonInCharge {}
@interface Todo {
// String ✅
String description();
// int ✅
int someNumber();
// boolean ✅
boolean isImportant();
// Class ✅
Class<?> someRelatedClass();
// Arrays ✅
String[] notes();
// Enums ✅
Priority priority();
// Other annotations ✅
Deadline deadline();
// Other classes ❌
PersonInCharge personInCharge();
}
1
You can find the full list here. It is a short list.
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 {
}
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.
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 {
}
1
The exception to this is the TYPE_USE target. That one needs to be explicitly specified.
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.
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.
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();
}
}
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.
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
}
}
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());
}
}
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.
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();
}
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();
}
}
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
1
Zero, it can literally kill them.
2
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.
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."
);
}
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();
}
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 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);
}
1
"Abstract" as a term here means something close to "not in reality." You will hear people refer to non-abstract classes as "concrete" classes.
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;
}
}
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
1
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.
2
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.
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) {
// Only change
this.passengers.add(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.
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.
1
Well, reasonable in the world of contrived programming examples.
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() {}}
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);
}
}
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);
}
1
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 ints.
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 use the byte type when working with data as sequences of bytes, such as reading from and writing to binary files. Representing binary data as arrays of byte values is more memory-efficient than representing each individual byte as, say, an int.
// 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 };
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 };
1
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.
2
As you might suspect, this is more rare than the situations where you would want a byte.
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.
1
"L is for long" would be a total cop-out in a children's picture book.
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.
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 };
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 and hashCode
Implement plus and minus
Implement the Comparable<UnsignedInteger> interface.
// CODE HERE
class Main {
void main() {
var i = new UnsignedInteger(0);
IO.println(i.minus(new UnsignedInteger(1)));
}
}
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);
}
}
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
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.
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.
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.
1
Abraham Lincoln had a really high pitched trill voice apparently. It is a real bummer we don't have recordings.
2
This is a subject of much debate.
3
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.
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();
// ...
}
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.
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 = new 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 = new ArrayList<String>();
names.add("Him");
names.add("Jim");
names.add("Bim");
IO.println(names);
}
}
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.
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 you2 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.
1
There are more answers, but they mostly involve tales of Java's past
and mistakes that were made there.
2
And by "you" I mean you and whoever else is directly working with you.
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.
If an interface has only 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 you can treat them as being functions whose input and output are the same as that one method to be implemented.
1
You might also see these referred to as SAM interfaces. SAM for Single Abstract Method.
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() {}
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!");
}
}
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");
}
}
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
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"));
}
}
1
It doesn't have to match exactly but describing the exact matching mechanism isn't that important. Play it by ear.
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();
}
}
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 Runnable1.
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.
1
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.
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.
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.
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()
);
}
}
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.
The compiler used for compiling Java code is called javac.
This stands for "JavaCompiler".
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.
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
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.
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.
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. rmremoves
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
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.
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.
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 hopefully be the case when you share code with others1 -
you instead want to run your code like this.
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
1
Remember the social convention of reverse domain name notation (com.google, etc)
2
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.
3
Chances are Java will make this the default in the future. For now you must write it though.
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 \
--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/
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.
1
And, like I've mentioned before, you should ideally be packaging up code
in modules.
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 it's all just files in a ZIP.
1
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 on my work laptop is Baldurs Gate 3.
I have 4GB left.
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.
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.
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.
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.
Now that you can package your own code into JARs you can share code with others
for this purpose.
1
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.
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."
This section is ultimately optional, but you will quickly see a need for something
that helps you remember commands.
1
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.
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.
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.
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.
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() {
// ..
}
}
1
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.
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() {
}
}
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.
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);
}
}
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.
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.
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.
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 Lists, Sets, and even Maps.
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);
}
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);
}
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.
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 */);
}
}
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.
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 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.
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.
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.
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.
If you want to learn how to make desktop applications in Java you have basically
two 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.
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.
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.
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.
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.
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.